Files
brain-of-reese/app/main.py
T
ducoterra d22d260b8b
Build and Push Containers / build-and-push-app (push) Successful in 5m43s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 91_admin_theme_tab
All verification is complete — this pass needed no code changes. Final report:

**Phase 91 — Admin Theme tab: final verification pass (all 6 tasks already in `complete/`)**

- Verified pre-paint theming end-to-end: `ui_settings` store + resolver, admin `GET/PUT /api/ui-settings`, `CachingMiddleware` inline-`<style id="bor-theme">` injection before `</head>` (incl. `/shared/<token>` prefix branch, unit-pinned), CSP sha256 exemption for the inline tag, Theme tab shell + `theme.js` editor, CSS-file theming fully retired.
- No defects found; zero changes made — working tree left exactly as the task executors left it.
- Tests: `uv run pytest --cov=app` → 1841 passed, 0 failed (TOTAL coverage **99%**; theming/ui_settings/caching all 100%); `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** in isolation.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.
- Criteria: (1) unset deployment byte-identical, no `#bor-theme` anywhere — ✓ (unit no-op test + E2E reset byte-compare); `rg "BOR_THEME|themes/"` → single hit is the permitted doc-history comment in `frontend/index.html`. (2) admin-only gate + 403s for anonymous and token users — ✓ (E2E test 3). (3) saved theme inline before `</head>` on every page incl. `/shared/<token>`, computed `--brand` on first paint for admin + anonymous — ✓ (E2E test 2 + unit). (4) reset → byte-identical; 5 contrast pairs warn <4.5:1, non-blocking — ✓ (E2E tests 4–5). (5) suite green, >90% coverage, lint clean — ✓. (6) commit deferred to harness per rules.
- Notable: `.agents/PLAN.md` is absent from the repo — the phase overview's Design section was used as the binding spec; no deviation resulted.
- Next pending phase: **none** — 91 is the last phase in `todo/`.
2026-09-09 17:22:24 -04:00

175 lines
7.5 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 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.api.ui_settings import router as ui_settings_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=<token>`` 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 91 (task 01): the admin UI-settings surface (GET/PUT the
# single ui_settings row — the Theme tab's persistence) — admin-only
# (router-wide require_admin; anonymous AND token users stay 403).
app.include_router(ui_settings_router, prefix="/api")
# Phase 51: the anonymous shared-chat read — NO admin dependency.
# /api/shared/<token> is the JSON snapshot; /shared/<token> (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/<token>
# 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)
# 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
"/theme.html", # phase 91 task 04: the Theme view (shell route)
),
)
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()