2026-07-25 07:08:54 +00:00
|
|
|
# frontend/ — agent instructions
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
The **SSR + static-asset service**: server-rendered crawlable pages, the
|
|
|
|
|
interactive tool's SPA shells, and every static asset. No climate data, no DB,
|
|
|
|
|
no compute — everything comes from `backend/`'s content API over HTTP.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
Read the root `CLAUDE.md` first for the branch model, deploy contract and image
|
|
|
|
|
names.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
## Build & verify
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
- `make test` — whole suite (`scripts/test.sh`).
|
|
|
|
|
- `make test-unit` — hermetic unit tier: SSR rendering fed committed fixtures,
|
|
|
|
|
no Docker. **This is the tier CI runs.**
|
|
|
|
|
- `make test-integration` — pulls and runs the real backend image and tests the
|
|
|
|
|
live contract. Local only; CI does not run it.
|
|
|
|
|
- `make backend-up` / `make backend-down` — a local backend container for dev.
|
|
|
|
|
- `make capture-fixtures` — refresh `tests/fixtures/*.json` from a live backend.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
## Running it
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
`THERMOGRAPH_API_BASE_INTERNAL` is **required** — `api_client.py` raises at
|
|
|
|
|
import if unset.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
|
|
|
|
```bash
|
2026-07-25 07:08:54 +00:00
|
|
|
THERMOGRAPH_API_BASE_INTERNAL=http://127.0.0.1:8137 THERMOGRAPH_BASE=/thermograph \
|
2026-07-22 18:59:59 +00:00
|
|
|
.venv/bin/uvicorn app:app --host 0.0.0.0 --port 8080
|
|
|
|
|
```
|
|
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
Other env: `THERMOGRAPH_BASE` (default `/thermograph`; the Dockerfile sets `/`),
|
|
|
|
|
`THERMOGRAPH_API_VERSION` (default `v2`), `THERMOGRAPH_API_BASE_PUBLIC`
|
|
|
|
|
(browser-facing backend origin when cross-origin; empty = same-origin, today's
|
|
|
|
|
default), `THERMOGRAPH_SSR_CACHE_TTL` (default 600s).
|
|
|
|
|
|
|
|
|
|
## Layout
|
|
|
|
|
|
|
|
|
|
- **`content.py`** — SSR routes (climate hub, city, month, records, glossary,
|
|
|
|
|
about, privacy) + `robots.txt` + `sitemap.xml`. Each fetches from the backend
|
|
|
|
|
via `api_client.py`; the backend's `content_payloads.py` owns `page_title` /
|
|
|
|
|
`canonical_path` / `breadcrumb` / `jsonld`, not this domain.
|
|
|
|
|
- **`static/*.js`** — `app.js` (map/search/results + inline SVG chart),
|
|
|
|
|
`calendar.js`/`day.js`/`score.js`/`compare.js` (SPA shells), `account.js`
|
|
|
|
|
(auth), `cache.js` (IndexedDB cache + `/cell` bundle prefetch), `shared.js`.
|
|
|
|
|
- **`static/style.css`** — the single hand-written stylesheet; design tokens live
|
|
|
|
|
here, documented in `DESIGN.md`.
|
|
|
|
|
- **`templates/*.html.j2`**, **`content/*.yaml`** (structured SSR copy, loaded by
|
|
|
|
|
`content_loader.py`).
|
|
|
|
|
|
|
|
|
|
## Contracts with the backend
|
|
|
|
|
|
|
|
|
|
- **API version is pinned in exactly two places** — `api_client.py`'s
|
|
|
|
|
`API_VERSION` (Python) and `static/account.js`'s exported `API_VERSION` plus the
|
|
|
|
|
`uv(path)` helper (JS). Never hardcode `api/v2/...` anywhere else. Bump both in
|
|
|
|
|
one PR, and only after the target backend's `/api/version` confirms it serves
|
|
|
|
|
that version and its `min_frontend` doesn't exclude the one you're leaving.
|
|
|
|
|
- **`shared.js::pctOrd()` must mirror `backend`'s `data/grading.py::pct_ordinal()`**
|
|
|
|
|
in behaviour — floor into `1..99`, never 0 or 100. Every percentile on every
|
|
|
|
|
surface goes through one of the two; if they diverge, the same reading says two
|
|
|
|
|
different things in two places.
|
|
|
|
|
- **`/cell` bundle + ETag** — `cache.js` fetches `/api/v2/cell` once per view-set
|
|
|
|
|
and slices it, revalidating with `If-None-Match`. The slice→view map must track
|
|
|
|
|
the backend's payload shape.
|
|
|
|
|
- **Fahrenheit country set** — `format.py::F_COUNTRIES` and `static/units.js`'s
|
|
|
|
|
`F_REGIONS` must stay identical to the backend's `F_COUNTRIES`.
|
|
|
|
|
- **Boot must survive an unreachable backend.** `content.py`'s `register()` tries
|
|
|
|
|
the IndexNow-key fetch once, catches failure, and falls back to a lazy
|
|
|
|
|
per-request lookup. Asynchronous FE/BE deploys depend on this — don't make it
|
|
|
|
|
fatal.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
|
|
|
|
## Commits & PRs
|
|
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
Concise and technical. Never mention AI, assistants or automated authorship.
|