""" FastAPI REST API for timing-systemet. """ import asyncio import logging from contextlib import asynccontextmanager from pathlib import Path import aiosqlite from fastapi import Depends, FastAPI, File, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from typing import Optional from ingest import watch_depot, process_existing from passage_log import delete_passage, get_passages, log_passage, resolve_passage from profile_db import ( clear_startlist, delete_athlete, get_db, import_startlist_csv, list_athletes, upsert_athlete, ) from results import get_results logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): # Behandle bilder som lå i depot fra før oppstart await process_existing() # Start fil-overvåkning i bakgrunnen asyncio.create_task(watch_depot()) yield app = FastAPI(title="Timing API", version="0.1.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # --- DB-avhengighet --- async def get_connection(): db = await get_db() try: yield db finally: await db.close() # ===================== # Startliste-endepunkter # ===================== @app.get("/api/athletes") async def list_athletes_endpoint(db=Depends(get_connection)): return await list_athletes(db) @app.post("/api/athletes/import", summary="Last opp startliste som CSV") async def import_csv( file: UploadFile = File(...), db=Depends(get_connection), ): if not file.filename.endswith(".csv"): raise HTTPException(400, "Kun CSV-filer støttes") content = (await file.read()).decode("utf-8-sig") # utf-8-sig håndterer BOM result = await import_startlist_csv(db, content) return result @app.delete("/api/athletes/all") async def clear_all_athletes(db=Depends(get_connection)): await clear_startlist(db) return {"ok": True} @app.delete("/api/athletes/{profile_id}") async def remove_athlete(profile_id: str, db=Depends(get_connection)): ok = await delete_athlete(db, profile_id) if not ok: raise HTTPException(404, "Utøver ikke funnet") return {"ok": True} # ===================== # Passeringer # ===================== @app.get("/api/passages") async def list_passages( station: Optional[str] = None, needs_review: Optional[bool] = None, profile_id: Optional[str] = None, db=Depends(get_connection), ): return await get_passages(db, profile_id=profile_id, station=station, needs_review=needs_review) @app.get("/api/passages/review", summary="Hent passeringer som trenger manuell gjennomgang") async def review_queue(db=Depends(get_connection)): return await get_passages(db, needs_review=True) class ResolveRequest(BaseModel): profile_id: Optional[str] = None bib_number: Optional[str] = None review_note: Optional[str] = None @app.post("/api/passages/{passage_id}/resolve") async def resolve(passage_id: str, body: ResolveRequest, db=Depends(get_connection)): ok = await resolve_passage( db, passage_id, profile_id=body.profile_id, bib_number=body.bib_number, review_note=body.review_note, ) if not ok: raise HTTPException(404, "Passering ikke funnet") return {"ok": True} @app.delete("/api/passages/{passage_id}") async def remove_passage(passage_id: str, db=Depends(get_connection)): ok = await delete_passage(db, passage_id) if not ok: raise HTTPException(404, "Passering ikke funnet") return {"ok": True} # ===================== # Resultater # ===================== @app.get("/api/results") async def get_results_endpoint(db=Depends(get_connection)): return await get_results(db) # ===================== # Bilder # ===================== @app.get("/api/images/{path:path}") async def serve_image(path: str): """Lever behandlede bilder til frontend.""" full_path = Path("/processed") / path if not full_path.exists() or not full_path.is_file(): raise HTTPException(404, "Bilde ikke funnet") # Sjekk at stien er innenfor /processed (path traversal) try: full_path.resolve().relative_to(Path("/processed").resolve()) except ValueError: raise HTTPException(403, "Forbudt") return FileResponse(full_path) # ===================== # Helse # ===================== @app.get("/api/health") async def health(): return {"status": "ok"}