1. CLAUDE.md and README.md described the superseded Python service. Both now describe server/ (Go), say plainly that the Python files at that level are the original the port was made from, and drop CLAUDE.md's claim that `make test-unit` is "the tier CI runs" — CI's only frontend check is the Dockerfile builder stage's gofmt + vet + go test. 2. static/units.js's F_REGIONS was guarded by nothing, despite three source comments claiming "a test asserts all three stay identical": the only check compared the Go set against the backend's Python. TestFCountriesMatchesUnitsJS now diffs the browser copy both directions. That backend cross-check also skips in CI — the image build context is frontend/, so backend/ is unreachable from the builder stage, which is the only place CI runs these tests. static/ IS in the context, so the Dockerfile copies units.js into the builder and the new assertion runs during the image build. Verified by mutating units.js and confirming the build fails. 3. Both docker-compose.test.yml files defaulted to the retired emi/thermograph-backend/app path, and the frontend harness pinned the split-era v0.0.2-split-ci tag. Path corrected in both. Rather than swap one hardcoded pin for another, backend-for-tests.sh now derives the tag from the checkout — sha-<12hex of `git log -1 -- backend/`>, the same domain-keyed rule build-push.yml and deploy.yml use — and compose requires the variable so a stale pin cannot creep back in. Verified: backend 429 passed/8 skipped; frontend go vet clean and all packages ok; frontend image builds; `make backend-up` pulls and serves on the derived tag; shellcheck zero findings across the tree. Unrelated pre-existing issue noted in the docs, not fixed here: `make test-integration` fails 7/16 with 503 against a cold throwaway backend (empty database, nothing warm). Reproduced identically on the old image, so it predates this change. Claude-Session: https://claude.ai/code/session_01AfXqHrxCJLs2D7hpQkiUiJ
6.3 KiB
Thermograph frontend
The server-rendered content pages, the interactive-tool SPA shells, and every
static asset for Thermograph — grades recent local
weather against ~45 years of climate history. This domain 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 builds its own image
(emi/thermograph/frontend) and deploys independently of the backend.
See CLAUDE.md for the agent-facing detail on the deploy contract
and the cross-service contracts, and DESIGN.md for the visual
system.
The service is Go
server/ is the live implementation. 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, kept as the reference
the port was made from — nothing deploys them. See server/README.md for the
port's own notes.
server/ the service (module thermograph/frontend, Go 1.26)
main.go config + mux + static + graceful shutdown
internal/config/ every env var it reads
internal/contentapi/ backend /content/* client: TTL cache, bounded LRU,
per-key single-flight, origin forwarding
internal/content/ SSR page handlers, template funcmap, SEO
internal/contentdata/ glossary.yaml / pages.yaml loader (fail-loud)
internal/format/ unit-aware °C/°F formatting + band names
internal/handlers/ SPA shells + static serving
internal/render/ html/template over embed.FS + ETag helpers
internal/render/templates/ the page templates (embedded, *.tmpl)
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/fixtures/ golden payloads — consumed by the GO tests as well
as the Python tiers
tests/{unit,integration}/ the Python tiers (local only; CI runs neither)
tools/shoot.py the screenshot sweep for visual verification
Dockerfile golang builder (gofmt + vet + test) → alpine runtime
Dependencies are stdlib plus gopkg.in/yaml.v3 — the committed SSR copy in
content/*.yaml uses block/folded scalars, so a real YAML parser is required.
Talking to the backend
All data comes from the backend's content API over HTTP — this process holds nothing. Two env vars control the topology:
THERMOGRAPH_API_BASE_INTERNAL(required) — where this process reaches the backend server-side, e.g.http://backend:8137in compose, orhttp://127.0.0.1:8137for a local backend. Boot fails loudly if unset: a missing backend URL should break the boot, not silently 500 on the first request.THERMOGRAPH_API_BASE_PUBLIC(optional) — the backend's browser-facing origin, used only when frontend and backend are served cross-origin. Left empty (the default), asset/API references stay relative and same-origin, which is today's real deployed topology.
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).
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 configuration first.
Build & run
Build in server/, run from here — 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
Or build the image directly:
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).
Test
cd server && go build ./... && go vet ./... && go test ./...
Dockerfile's builder stage runs gofmt -l + go vet + go test ./... before
compiling, so a failing Go test fails the image build — and that is the
whole frontend check in CI. There is no separate frontend test step in any
workflow.
The Python tiers are local-only:
make test-unit # hermetic: SSR rendering fed committed fixtures
make test-integration # pulls + runs a real backend image
make backend-up # just the backend container, for dev
make capture-fixtures # refresh tests/fixtures/*.json from a live backend
make backend-up derives the backend image tag from this checkout (the last
commit touching backend/ — the same domain-keyed rule build-push and deploy
use); pin one with THERMOGRAPH_BACKEND_TEST_TAG=sha-<12hex>.
More detail
CLAUDE.md— agent-facing instructions: build/verify, layout, API-version pinning, and the/cell/ETag,pctOrd()and Fahrenheit-set contracts.server/README.md— the Go port's own notes.DESIGN.md— the visual system (tokens, components, breakpoints) andtools/shoot.pyvisual verification.docs/onboarding/— the developer onboarding path for the whole monorepo.