"""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 starlette.responses import FileResponse from app.api.auth import router as auth_router from app.api.chat import router as chat_router from app.api.chats import ( public_router as chats_public_router, ) from app.api.chats import ( router as chats_router, ) from app.api.chats import ( shared_page_router as chats_shared_page_router, ) from app.api.config import router as config_router from app.api.doc_drafts import router as doc_drafts_router from app.api.docs import router as docs_router from app.api.git_sources import router as git_sources_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.api.tokens import router as tokens_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 from app.core.security_headers import SecurityHeadersMiddleware configure_logging() configure_debugging() settings = get_settings() logger = logging.getLogger("app") def _shell_routes(app: FastAPI, static_dir: Path, paths: tuple[str, ...]) -> None: """Phase 76: the navbar views are views of ONE shell document. Every registered path serves ``frontend/index.html`` (the shell) instead of its own page file: the client-side router (``frontend/assets/router.js``) reads ``location.pathname`` at boot and shows the matching view, so a direct load of e.g. ``/tuning.html`` deep-links to the Tuning view. Registered AFTER the API routers and BEFORE the static catch-all mount (routes-first), so the phase-33 caching middleware — which wraps the whole app and already lists every one of these paths in ``HTML_PAGES`` — applies the no-cache + ``?v=`` contract to the response untouched. The list is driven by the caller: tasks 02/03 fold the remaining views in by extending the tuple (task 03 lands History — all four non-chat navbar views are in; the old per-view ``.html`` files are deleted in the same change as their shell route lands — one source of truth). """ shell_file = static_dir / "index.html" async def _shell_view() -> FileResponse: return FileResponse(shell_file, media_type="text/html") for path in paths: # GET (document loads, the browser path) + HEAD — the pre-fold # static file answered both, so the shell route keeps that # method parity (the body is the same FileResponse; HEAD ships # headers only). app.api_route( path, methods=["GET", "HEAD"], include_in_schema=False )(_shell_view) 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(config_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(git_sources_router, prefix="/api") app.include_router(chat_router, prefix="/api") app.include_router(steering_router, prefix="/api") app.include_router(sync_router, prefix="/api") app.include_router(chats_router, prefix="/api") app.include_router(doc_drafts_router, prefix="/api") # Phase 79: the admin token surface (create/list/revoke) — admin-only # (router-wide require_admin; a token USER stays 403 here, task 03). app.include_router(tokens_router, prefix="/api") # Phase 51: the anonymous shared-chat read — NO admin dependency. # /api/shared/ is the JSON snapshot; /shared/ (the # page route below, registered without a prefix) is the page. app.include_router(chats_public_router, prefix="/api") app.include_router(chats_shared_page_router) # no prefix — /shared/ # Cache busting (phase 33): the five HTML pages revalidate (no-cache) # with ?v= 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) # Phase 82: security headers (CSP + no-framing + nosniff) — outermost on # purpose (last add_middleware): every response carries them, including # the static catch-all's 404s below (audit SEC-04). app.add_middleware(SecurityHeadersMiddleware) static_dir = Path(settings.static_dir).resolve() if static_dir.is_dir(): # Phase 76: the folded navbar views serve the shell — the # router picks the view from the pathname. Task 01 landed # Tuning; task 02 folds RAG + Sources; task 03 lands History # (list-driven — all four non-chat navbar views are in); # phase 79 task 06 folds the sixth view (Tokens). _shell_routes( app, static_dir, ( "/tuning.html", "/sources.html", "/git-sources.html", "/history.html", "/tokens.html", # phase 79 task 06: the Tokens view ), ) 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()