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.
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; passARGS=...). Tests are hermetic pertests/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 byAPI_CONTRACT_VERSIONandMIN_SUPPORTED_FRONTENDinweb/app.py. A breaking change ships as a newv3router mounted alongsidev2, re-registering only changed handlers, with the constant bumped in the same PR./apiand/api/v1stay 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 asallow_origins: without it the frontend'scache.jsreadsnullcross-origin and never revalidates. Don't change the ETag derivation or that list without checkingfrontend/static/cache.js. data/grading.py::pct_ordinal()is mirrored byfrontend'sshared.js::pctOrd()— floor a percentile into1..99, never 0 or 100.TEMP_BANDS/RAIN_BANDSare 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 set —
api/content_payloads.py'sF_COUNTRIESmust stay identical tofrontend'sformat.py::F_COUNTRIESandstatic/units.js'sF_REGIONS. There is a test asserting this. /healthzand/api/versionare 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.