# Thermograph frontend The server-rendered content pages, the interactive-tool SPA shells, and every static asset for [Thermograph](https://thermograph.org) — grades recent local weather against ~45 years of climate history. This repo holds **no climate data and does no compute**; it renders pages and serves the JS/CSS/image assets that call the backend's API from the browser. It was split out of the `emi/thermograph` monorepo (`frontend_ssr/` + `frontend/` there) so it can build its own image and deploy independently of the backend. See [`CLAUDE.md`](CLAUDE.md) for the full agent-facing detail on the split topology and deploy contract, and [`thermograph-docs`](../thermograph-docs) (sibling repo) for the architecture decision record behind the split. ## Layout ``` api_client.py HTTP client for the backend's content API (api/content_routes.py on the backend). Fails loud at import if THERMOGRAPH_API_BASE_INTERNAL is unset. TTL-cached (10 min default) in front of every call. app.py FastAPI app: /healthz, the interactive tool's SPA-shell routes (calendar/day/score/compare/legend/alerts), and the StaticFiles mount for everything else. Calls content.register(app) for the SSR routes below. content.py Server-rendered pages: climate hub, per-city, month, records, glossary, about, privacy, robots.txt, sitemap.xml. content_loader.py Loads content/glossary.yaml + content/pages.yaml (SSR copy), validated fail-loud at load time. format.py Unit-aware (°C/°F) formatting for the SSR templates, ContextVar-scoped active unit. paths.py Canonical filesystem locations (STATIC_DIR, TEMPLATES_DIR, CONTENT_DIR), resolved from the repo root. templates/ Jinja templates content.py renders (base, home, city, month, hub, records, glossary, glossary_term, about, privacy). static/ Every static asset: the interactive tool's JS (app.js, calendar.js, day.js, score.js, compare.js, account.js, cache.js, shared.js, units.js, mappicker.js, …), the SPA-shell HTML pages, style.css (all design tokens — see DESIGN.md), icons/favicons, manifest.webmanifest, sw.js. content/ Structured SSR copy: glossary.yaml, pages.yaml. tests/ pytest suite — see the KNOWN GAP note below before relying on it. tools/ shoot.py — the make-shots visual-verify screenshot tool (see DESIGN.md). Dockerfile Builds this repo's own image (python:3.12-slim, uvicorn app:app), independent of the backend's. ``` ## How it fits the split Four sibling repos replace the old monorepo: - **`thermograph-backend`** — the FastAPI API, grading/scoring/grid compute, and DB. This repo's only runtime dependency. - **`thermograph-frontend`** (this repo). - **`thermograph-infra`** — Terraform, compose files, `deploy/deploy.sh` (the script both repos' deploy workflows SSH into), the secrets vault. - **`thermograph-docs`** — architecture/decision docs and runbooks. Each app repo now builds and publishes its **own** image (`git.thermograph.org/emi/thermograph-frontend/app`, tagged by git SHA and semver) instead of the old shared `emi/thermograph/app` image, and deploys via its own `.forgejo/workflows/deploy.yml` (main → beta) and `deploy-prod.yml` (release → prod), each rolling only the `frontend` compose service on the target VPS. ## Talking to the backend All data comes from the backend's content API over HTTP (`api_client.py`) — this process holds nothing in-process. Two env vars control the topology: - **`THERMOGRAPH_API_BASE_INTERNAL`** (required) — where this process reaches the backend server-side, e.g. `http://backend:8137` in compose, or `http://127.0.0.1:8137` for a local backend checkout. `api_client.py` raises at import if this is unset — a missing backend URL should break boot, not silently 500 the first request. - **`THERMOGRAPH_API_BASE_PUBLIC`** (optional) — the backend's browser-facing origin, used only when frontend and backend are served cross-origin (e.g. a genuinely separate LAN-dev topology). Left empty (the default), asset/API references stay relative and same-origin, which is today's real deployed topology. **Same-origin cookie-auth caveat:** the interactive tool's auth (`static/account.js`) is an HttpOnly session cookie. It uses `credentials: "include"` (not the fetch default) specifically so the cookie still rides along if this script is ever loaded cross-origin, but the backend must actually be configured to accept credentialed cross-origin requests (CORS + `SameSite`) for that case to work — in the default same-origin deployment this is a non-issue. Don't assume a fully decoupled, independently- hosted frontend "just works" for logged-in features without checking that cookie/CORS configuration first. `THERMOGRAPH_BASE` (default `/thermograph`) must be set identically on both frontend and backend in every real deployment — it's how both processes agree on the shared URL prefix (or the deployed clean-root `/`, which the Dockerfile sets by default). ## Build & run ```bash python3 -m venv .venv && .venv/bin/pip install -r requirements.txt THERMOGRAPH_API_BASE_INTERNAL=http://127.0.0.1:8137 \ THERMOGRAPH_BASE=/thermograph \ .venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080 ``` Or build the image directly: ```bash docker build -t thermograph-frontend . docker run -p 8080:8080 -e THERMOGRAPH_API_BASE_INTERNAL=http://backend:8137 thermograph-frontend ``` The image healthchecks `GET /healthz` (liveness only — it does no I/O, so it stays cheap; it does not prove the backend is reachable). ## More detail - [`CLAUDE.md`](CLAUDE.md) — agent-facing instructions: deploy contract, API-version pinning, the cross-repo `/cell`/ETag and `pctOrd()` contracts, and the current known test-suite gap. - [`DESIGN.md`](DESIGN.md) — the visual system (tokens, components, breakpoints) and `make shots` visual verification. - [`thermograph-docs`](../thermograph-docs) — the repo-topology decision record this split implements.