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.
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 jinemi/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 doesgofmt -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 touchingbackend/, the same domain-keyed rule build-push and deploy use); override withTHERMOGRAPH_BACKEND_TEST_TAG=sha-<12hex>.make capture-fixtures— refreshtests/fixtures/*.jsonfrom 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'scontent_payloads.pyownspage_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/templateover anembed.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/*.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 +/cellbundle prefetch),shared.js.static/style.css— the single hand-written stylesheet; design tokens live here, documented inDESIGN.md.content/*.yaml— structured SSR copy, loaded byserver/internal/contentdata.
Contracts with the backend
- API version is pinned in exactly two places —
server/internal/config'sTHERMOGRAPH_API_VERSION(Go) andstatic/account.js's exportedAPI_VERSIONplus theuv(path)helper (JS). Never hardcodeapi/v2/...anywhere else. Bump both in one PR, and only after the target backend's/api/versionconfirms it serves that version and itsmin_frontenddoesn't exclude the one you're leaving. shared.js::pctOrd()must mirrorbackend'sdata/grading.py::pct_ordinal()in behaviour — floor into1..99, never 0 or 100 — and so mustserver/internal/format'sPctOrdinal. Every percentile on every surface goes through one of the three; if they diverge, the same reading says two different things in two places./cellbundle + ETag —cache.jsfetches/api/v2/cellonce per view-set and slices it, revalidating withIf-None-Match. The slice→view map must track the backend's payload shape.- Fahrenheit country set —
server/internal/format'sFCountriesandstatic/units.js'sF_REGIONSmust stay identical to the backend'sF_COUNTRIES. Both are now asserted by tests inserver/internal/format/format_test.go: the backend cross-check is a checkout-only guard (the image build context isfrontend/, sobackend/is unreachable there), while theunits.jscheck runs in the image build too —Dockerfilecopies 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.