thermograph/backend/CLAUDE.md

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.