thermograph/backend/CLAUDE.md
emi 18f50ad09b
All checks were successful
secrets-guard / encrypted (push) Successful in 16s
shell-lint / shellcheck (push) Successful in 14s
Build + push images (Forgejo registry) / build-push (backend) (push) Successful in 1m24s
Build + push images (Forgejo registry) / build-push (frontend) (push) Successful in 1m23s
Deploy / deploy (backend) (push) Successful in 2m36s
Deploy / deploy (frontend) (push) Successful in 2m49s
frontend: fix the three inconsistencies the onboarding guide found (#99)
2026-07-25 21:27:44 +00:00

3.9 KiB

backend/ — agent instructions

The Thermograph API service: a FastAPI app serving the graded-climate API (/api/v2/...), user accounts, notifications, and the SSR-content JSON API the frontend renders pages from. It owns the climate data pipeline and the derived-payload cache. It serves no HTML/CSS/JS — that is frontend/.

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

Build & verify

  • make test — builds a py3.12 venv and runs the hermetic pytest suite (scripts/test.sh; pass ARGS=...). Tests are hermetic per tests/conftest.py: no real Open-Meteo/Nominatim/GeoNames calls, throwaway SQLite, notifier thread disabled.
  • make smoke — boots the built image plus a throwaway db and asserts /healthz + /api/version.
  • CI runs this suite inside the built image (build.yml), so the exact interpreter and deps that ship are what gets tested.

app.py at the root is a one-line re-export shim for web/app.py, kept stable for systemd/CI callers. Entry target is app:app.

Contracts that break silently if you change them

  • GET /api/version{backend_version, min_frontend, payload_ver}, driven by API_CONTRACT_VERSION and MIN_SUPPORTED_FRONTEND in web/app.py. A breaking change ships as a new v3 router mounted alongside v2, re-registering only changed handlers, with the constant bumped in the same PR. /api and /api/v1 stay mounted as aliases.
  • PAYLOAD_VER (api/payloads.py) is separate from URL versioning — it is the cache/ETag invalidation token. Bump it whenever a response payload's shape changes, so one bump atomically orphans every pre-upgrade cached row.
  • ETag / If-None-Match — every graded payload is cached against a validity token that doubles as a weak ETag. expose_headers=["ETag"] in the CORS middleware matters as much as allow_origins: without it the frontend's cache.js reads null cross-origin and never revalidates. Don't change the ETag derivation or that list without checking frontend/static/cache.js.
  • data/grading.py::pct_ordinal() is mirrored by frontend's server/internal/format::PctOrdinal (SSR) and static/shared.js::pctOrd() (browser) — floor a percentile into 1..99, never 0 or 100. TEMP_BANDS/RAIN_BANDS are the source of truth for tier names and thresholds; changing them without the frontend produces tiers drawn in colours that disagree with the labels the API returns.
  • Fahrenheit country setapi/content_payloads.py's F_COUNTRIES is canonical; frontend's server/internal/format::FCountries and static/units.js's F_REGIONS must stay identical to it. Both are asserted by tests in frontend/server/internal/format/format_test.go — but note the backend cross-check skips in CI (the frontend image's build context is frontend/, so this file is unreachable from the builder stage, which is the only place CI runs those tests). It fires on a full checkout. The units.js check does run in the image build. frontend/format.py's copy is the superseded Python service's and is not deployed.
  • /healthz and /api/version are deliberately I/O-free so they stay cheap under tight healthcheck intervals.

Layout

accounts/ (fastapi-users + alembic), api/ (payload builders + SSR content routes), core/ (metrics, audit, singleton helper), data/ (grid, climate fetch/cache, grading/scoring, places/cities, derived-payload store), notifications/ (push, email, Discord bot, digest, scheduler), web/app.py (the real app), daemon/.

paths.py resolves every filesystem location from the repo root — don't reintroduce __file__-relative paths in a module; it breaks the moment a module moves. deploy/entrypoint.sh is the container entrypoint (alembic migrate, then serve, with a Swarm-secrets-as-files shim).

Commits & PRs

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