fix: FF calendar upload endpoint + path resolution for server deployment

- Add POST /api/eco/ff-upload to receive CSV from browser (saves to /tmp)
- _find_csv() checks /tmp, /app, project root — works local + Docker
- _run_ff_import now takes explicit csv_path parameter
- Frontend: Upload CSV button (file input) → uploads to server first,
  then Import into DB — with upload progress messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 16:36:31 +02:00
parent 4053e0669d
commit b187107468
2 changed files with 81 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useState, useEffect, useCallback, useRef, ChangeEvent } from 'react'
import clsx from 'clsx'
import { RefreshCw, Download, ChevronDown, AlertTriangle, Check } from 'lucide-react'
import { RefreshCw, Download, ChevronDown, AlertTriangle, Check, Upload } from 'lucide-react'
const API = ''
@@ -132,10 +132,37 @@ function SurpriseChip({ ev }: { ev: FFEvent }) {
function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () => void }) {
const [status, setStatus] = useState<ImportStatus>({ running: false, last_result: null })
const [syncResult, setSyncResult] = useState<string | null>(null)
const [uploadMsg, setUploadMsg] = useState<string | null>(null)
const [uploading, setUploading] = useState(false)
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const fileRef = useRef<HTMLInputElement>(null)
const handleUpload = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
setUploadMsg(`Uploading ${(file.size / 1_048_576).toFixed(1)} MB…`)
const form = new FormData()
form.append('file', file)
try {
const r = await fetch(`${API}/api/eco/ff-upload`, { method: 'POST', body: form }).then(x => x.json())
if (r.error) { setUploadMsg(`Upload error: ${r.error}`); return }
setUploadMsg(`✓ Uploaded ${r.size_mb} MB — click Import to load into DB`)
} catch (err) {
setUploadMsg(`Upload failed: ${err}`)
} finally {
setUploading(false)
if (fileRef.current) fileRef.current.value = ''
}
}
const startImport = async () => {
await fetch(`${API}/api/eco/ff-import`, { method: 'POST' })
const r = await fetch(`${API}/api/eco/ff-import`, { method: 'POST' })
if (!r.ok) {
const err = await r.json().catch(() => ({}))
setUploadMsg(`Import error: ${err.detail ?? r.status}`)
return
}
pollRef.current = setInterval(async () => {
const r = await fetch(`${API}/api/eco/ff-import/status`).then(x => x.json())
setStatus(r)
@@ -180,13 +207,23 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
</div>
</div>
{/* Upload CSV (for server deployment) */}
<label className={clsx(
'px-3 py-1.5 rounded bg-slate-600 hover:bg-slate-500 flex items-center gap-1.5 text-white cursor-pointer',
uploading && 'opacity-50 cursor-wait'
)}>
<Upload size={13} />
{uploading ? 'Uploading…' : 'Upload CSV'}
<input ref={fileRef} type="file" accept=".csv" className="hidden" onChange={handleUpload} disabled={uploading} />
</label>
<button
onClick={startImport}
disabled={status.running}
className="px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-wait flex items-center gap-1.5 text-white"
>
<Download size={13} />
{status.running ? 'Importing…' : 'Import CSV (2007-2025)'}
{status.running ? 'Importing…' : 'Import into DB'}
</button>
<button
@@ -197,6 +234,11 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
Sync Live (this week)
</button>
{uploadMsg && (
<span className={clsx('text-xs', uploadMsg.startsWith('✓') ? 'text-emerald-400' : uploadMsg.startsWith('Upload error') || uploadMsg.startsWith('Import error') ? 'text-red-400' : 'text-yellow-400')}>
{uploadMsg}
</span>
)}
{status.running && (
<span className="text-yellow-400 text-xs animate-pulse">Importing CSV this may take 30-60 s</span>
)}