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