Files
brain-of-reese/app/main.py
T
ducoterra 8fabb7efda perf(ui): cache busting — HTML no-cache + versioned asset URLs (?v=) with immutable 1y asset caching
Phase 33 (story: .agent/user_stories/cache-busting.md).

- app/core/caching.py: asset_version() — git short SHA (a commit is a
  deploy), stable content-hash fallback for non-git checkouts, "dev"
  for a missing static dir; computed once per process. CachingMiddleware
  — the five HTML pages revalidate (no-cache) with ?v=<token> asset refs
  rewritten in flight; /assets/* is public, max-age=31536000, immutable;
  everything else (all /api/*, the SSE chat stream in particular) passes
  through byte-identical.
- tests/e2e/test_cache_busting.py: fresh-Chromium wire assertions —
  document no-cache, versioned CSS/JS request URLs sharing one token,
  immutable asset headers, /api/health baseline headers, SSE chat to
  done (mock LLM).
- README 'Caching / deploys' section + story file.

Also fixed two prod-image defects surfaced by this phase's podman smoke
(the full app would not boot):
- Containerfile: ship the scripts/ package — app/api/sync.py (phase 32)
  imports scripts.git_sync / scripts.import_docs at module level, so the
  container crashed on boot (ModuleNotFoundError: No module named
  'scripts').
- compose.yaml: pass BOR_ADMIN_PASSWORD / BOR_SESSION_SECRET through to
  the app service (:- defaults keep 'podman compose up -d db' working;
  the app's own fail-loud gate still names missing admin auth).

Smoke: podman compose --profile prod up -d on a fresh image + a fresh
Chromium profile — /, /sources.html and /login.html all served
Cache-Control: no-cache; all 8 asset requests versioned with one shared
token (content-hash fallback inside the image — no .git there);
/assets/* immutable for a year.
2026-08-25 22:42:10 -04:00

88 lines
3.3 KiB
Python

"""Brain of Reese — application entrypoint.
Boots logging + conditional debugpy, then creates the FastAPI app:
API routes first (so they win over the catch-all), and the static frontend
mounted last. No CDN: everything the browser needs is served by this
process from local files (see PLAN §UI/UX — No External Dependencies).
Phase 16 (A10 revised): before anything is served, admin auth must be
configured (fail-loud), and the app wraps every route in Starlette's
SessionMiddleware — a signed ``bor_session`` cookie is the only session
state in the system.
"""
from __future__ import annotations
import logging
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from app.api.auth import router as auth_router
from app.api.chat import router as chat_router
from app.api.docs import router as docs_router
from app.api.health import router as health_router
from app.api.steering import router as steering_router
from app.api.suggestions import router as suggestions_router
from app.api.sync import router as sync_router
from app.config import get_settings
from app.core.auth import ensure_admin_configured
from app.core.caching import configure_caching
from app.core.debugging import configure_debugging
from app.core.logging import configure_logging
configure_logging()
configure_debugging()
settings = get_settings()
logger = logging.getLogger("app")
def create_app() -> FastAPI:
# Fail loud BEFORE serving anything (phase 16): missing
# BOR_ADMIN_PASSWORD / BOR_SESSION_SECRET raises at boot, naming the
# variable(s) — the app never starts in a half-authenticated state.
ensure_admin_configured(settings)
app = FastAPI(title=settings.app_name, version=settings.app_version)
# Signed single-admin session cookie (Starlette middleware, itsdangerous
# signer — no server-side store, no new services). Homelab HTTP: same_site
# is "lax" and https_only stays off (documented in the README).
app.add_middleware(
SessionMiddleware,
secret_key=settings.session_secret,
session_cookie=settings.session_cookie,
max_age=settings.session_max_age,
same_site="lax",
https_only=False,
)
# API routes first so they take precedence over the catch-all static mount.
app.include_router(health_router, prefix="/api")
app.include_router(auth_router, prefix="/api")
app.include_router(suggestions_router, prefix="/api")
app.include_router(docs_router, prefix="/api")
app.include_router(chat_router, prefix="/api")
app.include_router(steering_router, prefix="/api")
app.include_router(sync_router, prefix="/api")
# Cache busting (phase 33): the five HTML pages revalidate (no-cache)
# with ?v=<token> asset refs; /assets/* becomes immutable for a year.
# Added after the session middleware, so it wraps the whole app
# (including the static catch-all below); /api/* — the SSE chat
# stream in particular — passes through untouched.
configure_caching(app)
static_dir = Path(settings.static_dir).resolve()
if static_dir.is_dir():
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
else:
logger.warning("static dir %s not found — serving API only", static_dir)
return app
app = create_app()