"""chart.amow.com FastAPI backend.""" import asyncio from contextlib import asynccontextmanager from pathlib import Path import re from hashlib import sha256 from fastapi import FastAPI from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from app.api.meta import router as meta_router from app.api.routes import router as api_router from app.api.schwab_auth import router as schwab_auth_router from app.api.ws import router as ws_router from app.config import Settings from app.runtime import Runtime BASE_DIR = Path(__file__).parent STATIC_DIR = BASE_DIR / "static" @asynccontextmanager async def lifespan(app: FastAPI): runtime = Runtime(Settings()) app.state.runtime = runtime task = await runtime.start() yield runtime.stream.stop() task.cancel() try: await task except asyncio.CancelledError: pass app = FastAPI(title="chart", lifespan=lifespan) app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") app.include_router(meta_router) app.include_router(schwab_auth_router) app.include_router(api_router) app.include_router(ws_router) ASSET_REF = re.compile(r'((?:src|href)="/static/[^"?]+)"') def asset_version() -> str: """A digest of the served assets, so the URL changes iff the content does. StaticFiles sends an ETag but no Cache-Control, so a browser is free to keep using the copy it already has — and a tab left open simply never fetches again. That turned a fixed bug into a bug that still reproduced, because the page was running the JavaScript it had loaded hours earlier. Hashing rather than stamping mtimes: a deploy checks every file out fresh, which would otherwise invalidate assets that never changed. """ digest = sha256() for path in sorted(STATIC_DIR.glob("*.*")): digest.update(path.read_bytes()) return digest.hexdigest()[:12] @app.get("/") def index(): html = (STATIC_DIR / "index.html").read_text(encoding="utf-8") html = ASSET_REF.sub(rf'\1?v={asset_version()}"', html) # The document itself must never be cached, or the versioned URLs inside it # are the stale thing instead. return HTMLResponse(html, headers={"Cache-Control": "no-store"})