thermograph/frontend
Emi Griffith df409f88b3
All checks were successful
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 4s
shell-lint / shellcheck (pull_request) Successful in 6s
PR build (required check) / validate-observability (pull_request) Successful in 20s
PR build (required check) / build-frontend (pull_request) Successful in 37s
PR build (required check) / build-backend (pull_request) Successful in 51s
PR build (required check) / gate (pull_request) Successful in 1s
registry: move image and repo references to the Jinemi namespace
Repos moved to the Jinemi org; the container packages did not follow, since
Forgejo does not transfer packages with a repo. The deploy path still resolved
`emi/thermograph/*` — a user_redirect to admin_emi — while build-push.yml
derives its push path from ${github.repository}, now jinemi/thermograph. The
next backend or frontend build would have published somewhere no deploy looks.

Point the image paths at jinemi/thermograph/* (lowercase: OCI references admit
no uppercase, which is why build-push.yml already pipes through tr), the clone
URLs at Jinemi/thermograph, and the registry logins at admin_emi — the account
that actually owns the tokens, rather than the redirect.

The live tags and both ci-runner tags were copied into the Jinemi namespace
first, so the switch has something to pull. thermograph-infra,
thermograph-observability and the retired */app packages stay under admin_emi;
they did not move.
2026-08-01 09:25:02 -07:00
..
content Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
scripts frontend: fix the three inconsistencies the onboarding guide found (#99) 2026-07-25 21:11:32 +00:00
server registry: move image and repo references to the Jinemi namespace 2026-08-01 09:25:02 -07:00
static accounts: sign in with Google, on a shared provider-agnostic OAuth engine (#122) 2026-07-27 00:56:43 +00:00
templates bookmarks: map + account UI for saved locations 2026-07-26 18:36:43 +00:00
tests fix(frontend): Weekly view must always send an explicit date to /grade 2026-07-26 06:20:19 +00:00
tools Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
.dockerignore web/worker: add a process-level liveness heartbeat (#80) 2026-07-25 04:13:47 +00:00
.gitignore Subtree-sync frontend to split main 510d94d (dev->main promotion: reconciled hardening, two-tier suite); subtree workflow copy stays deleted (CI lives at root) 2026-07-22 22:37:29 -07:00
api_client.py Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
app.py Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
CLAUDE.md registry: move image and repo references to the Jinemi namespace 2026-08-01 09:25:02 -07:00
content.py Frontend QA batch: date/TZ, https origin, date-422, VAPID rotation, trace-precip, partial-day gate (#70) 2026-07-24 23:13:36 +00:00
content_loader.py Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
DESIGN.md Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
docker-compose.test.yml registry: move image and repo references to the Jinemi namespace 2026-08-01 09:25:02 -07:00
Dockerfile frontend: fix the three inconsistencies the onboarding guide found (#99) 2026-07-25 21:11:32 +00:00
format.py Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
Makefile Subtree-sync frontend to split main 510d94d (dev->main promotion: reconciled hardening, two-tier suite); subtree workflow copy stays deleted (CI lives at root) 2026-07-22 22:37:29 -07:00
paths.py Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00
README.md registry: move image and repo references to the Jinemi namespace 2026-08-01 09:25:02 -07:00
requirements-dev.txt Subtree-sync frontend to split main 510d94d (dev->main promotion: reconciled hardening, two-tier suite); subtree workflow copy stays deleted (CI lives at root) 2026-07-22 22:37:29 -07:00
requirements.txt Subtree-merge thermograph-frontend (origin/main) into frontend/ 2026-07-22 22:01:11 -07:00

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 (jinemi/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:8137 in compose, or 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).

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) and tools/shoot.py visual verification.
  • docs/onboarding/ — the developer onboarding path for the whole monorepo.