thermograph/frontend/README.md

127 lines
6.3 KiB
Markdown
Raw Normal View History

# 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.