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 21:11:32 +00:00
|
|
|
## The live service is Go — `server/`
|
|
|
|
|
|
|
|
|
|
`server/` (module `thermograph/frontend`, Go 1.26) is what builds, tests, ships
|
2026-07-31 03:53:26 +00:00
|
|
|
and runs. It is the only thing in the `jinemi/thermograph/frontend` image.
|
2026-07-25 21:11:32 +00:00
|
|
|
|
|
|
|
|
The Python files at this level — `app.py`, `content.py`, `api_client.py`,
|
|
|
|
|
`format.py`, `content_loader.py`, `paths.py`, `templates/*.html.j2` — are the
|
|
|
|
|
**superseded original implementation**, kept as the reference the port was made
|
|
|
|
|
from. Nothing deploys them. Don't add features there; don't take their file
|
|
|
|
|
paths as current. (They still carry duplicate copies of some contracts below —
|
|
|
|
|
notably `api_client.py`'s `API_VERSION` pin and `format.py`'s `F_COUNTRIES`. If
|
|
|
|
|
you use them locally, keep them in step or you will debug a phantom.)
|
|
|
|
|
|
|
|
|
|
Deleting them is a live question, not a settled one — the Python test tiers
|
|
|
|
|
still run against the fixtures the Go tests share. Raise it rather than doing it
|
|
|
|
|
in passing.
|
|
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
## Build & verify
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
- `cd server && go build ./... && go vet ./... && go test ./...` — **the suite
|
|
|
|
|
that matters.**
|
|
|
|
|
- **CI runs it inside `Dockerfile`'s builder stage**, which does `gofmt -l` +
|
|
|
|
|
`go vet` + `go test ./...` before compiling. A failing Go test therefore fails
|
|
|
|
|
the image build — that is the whole frontend check. There is no separate
|
|
|
|
|
frontend test step in any workflow.
|
|
|
|
|
- `make test-unit` / `make test-integration` — the **Python** tiers. Local only;
|
|
|
|
|
CI runs neither. The unit tier is hermetic (SSR rendering fed committed
|
|
|
|
|
fixtures); the integration tier pulls and runs a real backend image.
|
2026-07-25 07:08:54 +00:00
|
|
|
- `make backend-up` / `make backend-down` — a local backend container for dev.
|
2026-07-25 21:11:32 +00:00
|
|
|
The image tag is derived from this checkout (last commit touching `backend/`,
|
|
|
|
|
the same domain-keyed rule build-push and deploy use); override with
|
|
|
|
|
`THERMOGRAPH_BACKEND_TEST_TAG=sha-<12hex>`.
|
2026-07-25 07:08:54 +00:00
|
|
|
- `make capture-fixtures` — refresh `tests/fixtures/*.json` from a live backend.
|
2026-07-25 21:11:32 +00:00
|
|
|
**These fixtures feed the Go tests too**, not just the Python ones.
|
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 21:11:32 +00:00
|
|
|
`THERMOGRAPH_API_BASE_INTERNAL` is **required** — the boot fails loudly without
|
|
|
|
|
it, as the Python raised at import.
|
|
|
|
|
|
|
|
|
|
Build in `server/`, run from `frontend/`: `static/` and `content/` resolve
|
|
|
|
|
relative to the working directory, while templates are embedded in the binary.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
|
|
|
|
```bash
|
2026-07-25 21:11:32 +00:00
|
|
|
cd server && go build -o thermograph-frontend .
|
|
|
|
|
cd ..
|
2026-07-25 07:08:54 +00:00
|
|
|
THERMOGRAPH_API_BASE_INTERNAL=http://127.0.0.1:8137 THERMOGRAPH_BASE=/thermograph \
|
2026-07-25 21:11:32 +00:00
|
|
|
./server/thermograph-frontend
|
2026-07-22 18:59:59 +00:00
|
|
|
```
|
|
|
|
|
|
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
|
2026-07-25 21:11:32 +00:00
|
|
|
default), `THERMOGRAPH_SSR_CACHE_TTL` (default 600s),
|
|
|
|
|
`THERMOGRAPH_GOOGLE_VERIFY` / `THERMOGRAPH_BING_VERIFY`, `PORT` (default 8080).
|
|
|
|
|
|
|
|
|
|
Boot is fail-loud on bad **config or content**: a missing backend URL, a garbage
|
|
|
|
|
TTL, or malformed `content/*.yaml` all stop the process rather than 500 on the
|
|
|
|
|
first request.
|
2026-07-25 07:08:54 +00:00
|
|
|
|
|
|
|
|
## Layout
|
|
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
- **`server/internal/content/`** — the SSR page handlers (climate hub, city,
|
|
|
|
|
month, records, glossary, about, privacy) + `robots.txt` + `sitemap.xml`,
|
|
|
|
|
plus the template funcmap and SEO helpers. The backend's
|
|
|
|
|
`content_payloads.py` owns `page_title` / `canonical_path` / `breadcrumb` /
|
|
|
|
|
`jsonld`, **not this domain**.
|
|
|
|
|
- **`server/internal/contentapi/`** — the backend `/content/*` client: TTL
|
|
|
|
|
cache, bounded LRU, per-key single-flight, origin forwarding. All four
|
|
|
|
|
properties are load-bearing; read the file before changing caching.
|
|
|
|
|
- **`server/internal/render/`** + `render/templates/*.tmpl` — `html/template`
|
|
|
|
|
over an `embed.FS`, plus the ETag helpers. Templates are **embedded**, so a
|
|
|
|
|
template change needs a rebuild.
|
|
|
|
|
- **`server/internal/{config,format,contentdata,handlers}/`** — env parsing,
|
|
|
|
|
unit-aware °C/°F formatting and band names, the glossary/pages loader, and
|
|
|
|
|
the SPA-shell + static handlers.
|
2026-07-25 07:08:54 +00:00
|
|
|
- **`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`.
|
2026-07-25 21:11:32 +00:00
|
|
|
- **`content/*.yaml`** — structured SSR copy, loaded by
|
|
|
|
|
`server/internal/contentdata`.
|
2026-07-25 07:08:54 +00:00
|
|
|
|
|
|
|
|
## Contracts with the backend
|
|
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
- **API version is pinned in exactly two places** — `server/internal/config`'s
|
|
|
|
|
`THERMOGRAPH_API_VERSION` (Go) 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.
|
2026-07-25 07:08:54 +00:00
|
|
|
- **`shared.js::pctOrd()` must mirror `backend`'s `data/grading.py::pct_ordinal()`**
|
2026-07-25 21:11:32 +00:00
|
|
|
in behaviour — floor into `1..99`, never 0 or 100 — and so must
|
|
|
|
|
`server/internal/format`'s `PctOrdinal`. Every percentile on every surface
|
|
|
|
|
goes through one of the three; if they diverge, the same reading says two
|
2026-07-25 07:08:54 +00:00
|
|
|
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.
|
2026-07-25 21:11:32 +00:00
|
|
|
- **Fahrenheit country set** — `server/internal/format`'s `FCountries` and
|
|
|
|
|
`static/units.js`'s `F_REGIONS` must stay identical to the backend's
|
|
|
|
|
`F_COUNTRIES`. Both are now asserted by tests in
|
|
|
|
|
`server/internal/format/format_test.go`: the backend cross-check is a
|
|
|
|
|
checkout-only guard (the image build context is `frontend/`, so `backend/` is
|
|
|
|
|
unreachable there), while the `units.js` check runs in the image build too —
|
|
|
|
|
`Dockerfile` copies that one file into the builder stage for exactly that
|
|
|
|
|
reason.
|
|
|
|
|
- **Boot must survive an unreachable backend.** The IndexNow-key fetch is tried
|
|
|
|
|
once at boot, caught, 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.
|