import asyncio from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from app.api.admin import router as admin_router from app.api.public import router as public_router from app.config import settings from app.db import Base, SessionLocal, engine from app import models # noqa: F401 — register models with Base.metadata from app.services import cleaner as cleaner_service def _resolve_frontend_dist() -> Path | None: """Locate frontend/dist in Docker (/app/frontend/dist) or monorepo checkout.""" here = Path(__file__).resolve() candidates = ( here.parents[2] / "frontend" / "dist", # .../backend/app/main.py → repo root here.parents[1] / "frontend" / "dist", # /app/app/main.py → /app/frontend/dist Path("/app/frontend/dist"), ) for path in candidates: if (path / "index.html").is_file(): return path return None async def _cleaner_loop() -> None: while True: db = SessionLocal() try: cleaner_service.run_once(db) except Exception: pass finally: db.close() await asyncio.sleep(60) @asynccontextmanager async def lifespan(_app: FastAPI): settings.data_dir.mkdir(parents=True, exist_ok=True) settings.uploads_dir.mkdir(parents=True, exist_ok=True) Base.metadata.create_all(bind=engine) task = asyncio.create_task(_cleaner_loop()) try: yield finally: task.cancel() try: await task except asyncio.CancelledError: pass app = FastAPI(title="MincedPad", lifespan=lifespan) app.include_router(public_router) app.include_router(admin_router) @app.get("/api/health") def health(): return {"ok": True, "name": "MincedPad"} _dist = _resolve_frontend_dist() if _dist is not None: assets_dir = _dist / "assets" if assets_dir.is_dir(): app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") @app.api_route("/{full_path:path}", methods=["GET", "HEAD"]) async def spa(full_path: str): if full_path == "api" or full_path.startswith("api/"): raise HTTPException(status_code=404, detail="Not Found") file_path = _dist / full_path if full_path and file_path.is_file(): return FileResponse(file_path) return FileResponse(_dist / "index.html")