thermograph/frontend/CLAUDE.md
emi 5f20fba9f5
All checks were successful
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 8s
secrets-guard / encrypted (pull_request) Successful in 18s
PR build (required check) / changes (pull_request) Successful in 22s
shell-lint / shellcheck (pull_request) Successful in 19s
Build + push images (Forgejo registry) / build-push (backend) (push) Successful in 1m14s
PR build (required check) / build-backend (pull_request) Successful in 1m21s
PR build (required check) / gate (pull_request) Successful in 2s
Build + push images (Forgejo registry) / build-push (frontend) (push) Successful in 1m11s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 1m1s
frontend: fix the three inconsistencies the onboarding guide found (#99)
2026-07-25 21:11:32 +00:00

6.4 KiB

frontend/ — agent instructions

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.

Read the root CLAUDE.md first for the branch model, deploy contract and image names.

The live service is Go — server/

server/ (module thermograph/frontend, Go 1.26) is what builds, tests, ships and runs. It is the only thing in the emi/thermograph/frontend image.

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.

Build & verify

  • 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.
  • make backend-up / make backend-down — a local backend container for dev. 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>.
  • make capture-fixtures — refresh tests/fixtures/*.json from a live backend. These fixtures feed the Go tests too, not just the Python ones.

Running it

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.

cd server && go build -o thermograph-frontend .
cd ..
THERMOGRAPH_API_BASE_INTERNAL=http://127.0.0.1:8137 THERMOGRAPH_BASE=/thermograph \
  ./server/thermograph-frontend

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), 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.

Layout

  • 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/*.tmplhtml/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.
  • static/*.jsapp.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.
  • content/*.yaml — structured SSR copy, loaded by server/internal/contentdata.

Contracts with the backend

  • API version is pinned in exactly two placesserver/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.
  • shared.js::pctOrd() must mirror backend's data/grading.py::pct_ordinal() 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 different things in two places.
  • /cell bundle + ETagcache.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 setserver/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.

Commits & PRs

Concise and technical. Never mention AI, assistants or automated authorship.