diff --git a/backend/routers/eco.py b/backend/routers/eco.py index f0a0b6f..ba6bc48 100644 --- a/backend/routers/eco.py +++ b/backend/routers/eco.py @@ -7,7 +7,7 @@ import logging import os from typing import Any, Dict, List, Optional -from fastapi import APIRouter, BackgroundTasks, HTTPException, Query +from fastapi import APIRouter, BackgroundTasks, File, HTTPException, Query, UploadFile logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/eco", tags=["Economic Calendar"]) @@ -296,17 +296,27 @@ def upcoming_events() -> List[Dict[str, Any]]: _ff_import_status: Dict[str, Any] = {"running": False, "last_result": None} _ff_sync_status: Dict[str, Any] = {"running": False, "last_result": None} -_FF_CSV_PATH = os.path.join( - os.path.dirname(__file__), "..", "..", "forex_factory_cache.csv" -) +# CSV may sit at project root (local dev) or in /tmp (uploaded via browser on server) +_FF_CSV_CANDIDATES = [ + "/tmp/forex_factory_cache.csv", + os.path.join(os.path.dirname(__file__), "..", "forex_factory_cache.csv"), + os.path.join(os.path.dirname(__file__), "..", "..", "forex_factory_cache.csv"), +] -def _run_ff_import(): +def _find_csv() -> str | None: + for p in _FF_CSV_CANDIDATES: + full = os.path.abspath(p) + if os.path.exists(full): + return full + return None + + +def _run_ff_import(csv_path: str): global _ff_import_status _ff_import_status["running"] = True try: from services.ff_calendar import import_csv - csv_path = os.path.abspath(_FF_CSV_PATH) result = import_csv(csv_path) _ff_import_status["last_result"] = result except Exception as e: @@ -330,15 +340,30 @@ def _run_ff_sync(): _ff_sync_status["running"] = False +@router.post("/ff-upload") +async def ff_upload(file: UploadFile = File(...)) -> Dict[str, Any]: + """Upload forex_factory_cache.csv directly from the browser (saves to /tmp).""" + dest = "/tmp/forex_factory_cache.csv" + try: + content = await file.read() + with open(dest, "wb") as f: + f.write(content) + size_mb = round(len(content) / 1_048_576, 1) + print(f"[FF upload] Saved {size_mb} MB to {dest}", flush=True) + return {"status": "uploaded", "path": dest, "size_mb": size_mb} + except Exception as e: + raise HTTPException(500, f"Upload failed: {e}") + + @router.post("/ff-import") def ff_import(background_tasks: BackgroundTasks) -> Dict[str, Any]: """Import forex_factory_cache.csv into ff_calendar table (background).""" if _ff_import_status["running"]: raise HTTPException(409, "Import already running") - csv_path = os.path.abspath(_FF_CSV_PATH) - if not os.path.exists(csv_path): - raise HTTPException(404, f"CSV not found: {csv_path}") - background_tasks.add_task(_run_ff_import) + csv_path = _find_csv() + if not csv_path: + raise HTTPException(404, "CSV not found. Upload it first via POST /api/eco/ff-upload") + background_tasks.add_task(_run_ff_import, csv_path) return {"status": "started", "csv": csv_path} diff --git a/frontend/src/pages/CalendarPage.tsx b/frontend/src/pages/CalendarPage.tsx index c3362f3..f321903 100644 --- a/frontend/src/pages/CalendarPage.tsx +++ b/frontend/src/pages/CalendarPage.tsx @@ -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({ running: false, last_result: null }) const [syncResult, setSyncResult] = useState(null) + const [uploadMsg, setUploadMsg] = useState(null) + const [uploading, setUploading] = useState(false) const pollRef = useRef | null>(null) + const fileRef = useRef(null) + + const handleUpload = async (e: ChangeEvent) => { + 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: () => + {/* Upload CSV (for server deployment) */} + + + {uploadMsg && ( + + {uploadMsg} + + )} {status.running && ( Importing CSV — this may take 30-60 s… )}