import logging import sys from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException # When started via uvicorn --log-config the "subfeed" logger is already configured # (see log_config.json). This block is a timestamped fallback for other entrypoints # (tests, scripts) so our logs are never silently dropped. _subfeed_logger = logging.getLogger("subfeed") if not _subfeed_logger.handlers: _handler = logging.StreamHandler(sys.stdout) _handler.setFormatter( logging.Formatter("%(asctime)s %(levelname)-5s [%(name)s] %(message)s") ) _subfeed_logger.addHandler(_handler) _subfeed_logger.setLevel(logging.INFO) _subfeed_logger.propagate = False log = logging.getLogger("subfeed.app") from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from app import auth from app.config import settings from app.routes import feed, health, me, sync, tags from app.scheduler import shutdown_scheduler, start_scheduler @asynccontextmanager async def lifespan(app: FastAPI): log.info("Subfeed starting up") start_scheduler() try: yield finally: log.info("Subfeed shutting down") shutdown_scheduler() app = FastAPI(title=settings.app_name, lifespan=lifespan) app.add_middleware( SessionMiddleware, secret_key=settings.secret_key, same_site="lax", https_only=False, ) if settings.frontend_origin: app.add_middleware( CORSMiddleware, allow_origins=[settings.frontend_origin], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(health.router) app.include_router(auth.router) app.include_router(sync.router) app.include_router(tags.router) app.include_router(feed.router) app.include_router(me.router) # The built SPA (populated by the Docker frontend build stage). STATIC_DIR = Path(__file__).parent / "static_spa" app.mount( "/assets", StaticFiles(directory=STATIC_DIR / "assets", check_dir=False), name="assets", ) @app.get("/") async def index() -> FileResponse: return FileResponse(STATIC_DIR / "index.html") @app.get("/{full_path:path}") async def spa_fallback(full_path: str) -> FileResponse: # Client-side routes fall back to index.html; real API/asset paths are matched above. if full_path.startswith(("api/", "auth/", "healthz", "assets/")): raise HTTPException(status_code=404) return FileResponse(STATIC_DIR / "index.html")