thermograph/backend/CLAUDE.md
Emi Griffith c98512cfcc
All checks were successful
secrets-guard / encrypted (pull_request) Successful in 10s
PR build (required check) / changes (pull_request) Successful in 17s
shell-lint / shellcheck (pull_request) Successful in 14s
PR build (required check) / validate-observability (pull_request) Successful in 44s
PR build (required check) / build-frontend (pull_request) Successful in 2m13s
PR build (required check) / build-backend (pull_request) Successful in 2m31s
PR build (required check) / gate (pull_request) Successful in 3s
docs: rewrite the agent context layer to match the live system
The four domain CLAUDE.md files still described the pre-monorepo split-repo
topology, and several statements were the exact inverse of current reality:
infra/CLAUDE.md told a reader that emi/thermograph is archived and must not be
pointed at, when that is the live repo; backend/ and frontend/ both claimed
there is no Makefile and no requirements-dev.txt when all four files exist;
frontend/ described itself as one of four sibling repos; observability/ claimed
a single unprotected main branch.

These files are read before every change, so a stale one is a correctness
problem rather than a documentation one. Rewritten against the tree:

- Root CLAUDE.md now owns the cross-cutting truth once — branch model, the
  SERVICE + *_IMAGE_TAG deploy contract, image names, which orchestrator each
  environment runs, and that everything is a PR. Domain files carry only what
  differs and are capped at ~70 lines.
- Records that prod runs Swarm from deploy/stack/thermograph-stack.yml while
  beta and LAN dev run compose, routed by /etc/thermograph/deploy-mode.
- Documents the *-deploy-dev.yml workflows as inert rather than leaving a
  reader to discover it.

Deletes infra/docker-stack.yml. It defined db/app/worker, matched nothing that
deploys, and was referenced only by prose describing it as a future design
record — while the real 8-service prod stack lives under deploy/stack/. A file
that looks authoritative and affects nothing is the worst case for a reader
asked to change the prod stack.

infra/README.md claimed "compose in production today" and that Swarm was "not
currently live"; corrected, and infra-sync.yml's existence is now recorded
instead of "there is no separate infra deploy trigger". The compose file's
timescale-pin comment now points at how deploy-stack.sh actually resolves the
digest from the running container.
2026-07-24 20:59:44 -07:00

3.4 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 shared.js::pctOrd() — 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 must stay identical to frontend's format.py::F_COUNTRIES and static/units.js's F_REGIONS. There is a test asserting this.
  • /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.