2026-07-22 18:59:59 +00:00
|
|
|
# Thermograph frontend
|
|
|
|
|
|
|
|
|
|
The server-rendered content pages, the interactive-tool SPA shells, and every
|
|
|
|
|
static asset for [Thermograph](https://thermograph.org) — grades recent local
|
2026-07-25 21:11:32 +00:00
|
|
|
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
|
2026-07-31 03:53:26 +00:00
|
|
|
(`jinemi/thermograph/frontend`) and deploys independently of the backend.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
See [`CLAUDE.md`](CLAUDE.md) for the agent-facing detail on the deploy contract
|
|
|
|
|
and the cross-service contracts, and [`DESIGN.md`](DESIGN.md) for the visual
|
|
|
|
|
system.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
## The service is Go
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
`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.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
```
|
|
|
|
|
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
|
|
|
|
|
```
|
2026-07-22 18:59:59 +00:00
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
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.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
|
|
|
|
## Talking to the backend
|
|
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
All data comes from the backend's content API over HTTP — this process holds
|
|
|
|
|
nothing. Two env vars control the topology:
|
2026-07-22 18:59:59 +00:00
|
|
|
|
|
|
|
|
- **`THERMOGRAPH_API_BASE_INTERNAL`** (required) — where this process reaches
|
|
|
|
|
the backend server-side, e.g. `http://backend:8137` in compose, or
|
2026-07-25 21:11:32 +00:00
|
|
|
`http://127.0.0.1:8137` for 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).
|
2026-07-22 18:59:59 +00:00
|
|
|
|
|
|
|
|
**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
|
2026-07-25 21:11:32 +00:00
|
|
|
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.
|
2026-07-22 18:59:59 +00:00
|
|
|
|
|
|
|
|
## Build & run
|
|
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
Build in `server/`, run from here — `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-22 18:59:59 +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
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
## Test
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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>`.
|
|
|
|
|
|
2026-07-22 18:59:59 +00:00
|
|
|
## More detail
|
|
|
|
|
|
2026-07-25 21:11:32 +00:00
|
|
|
- [`CLAUDE.md`](CLAUDE.md) — agent-facing instructions: build/verify, layout,
|
|
|
|
|
API-version pinning, and the `/cell`/ETag, `pctOrd()` and Fahrenheit-set
|
|
|
|
|
contracts.
|
|
|
|
|
- [`server/README.md`](server/README.md) — the Go port's own notes.
|
2026-07-22 18:59:59 +00:00
|
|
|
- [`DESIGN.md`](DESIGN.md) — the visual system (tokens, components,
|
2026-07-25 21:11:32 +00:00
|
|
|
breakpoints) and `tools/shoot.py` visual verification.
|
|
|
|
|
- [`docs/onboarding/`](../docs/onboarding/README.md) — the developer onboarding
|
|
|
|
|
path for the whole monorepo.
|