2026-07-25 07:08:54 +00:00
|
|
|
# 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
|
2026-07-25 21:11:32 +00:00
|
|
|
`server/internal/format::PctOrdinal` (SSR) and `static/shared.js::pctOrd()`
|
|
|
|
|
(browser) — floor a percentile into `1..99`, never 0 or 100.
|
2026-07-25 07:08:54 +00:00
|
|
|
`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.
|
2026-07-25 21:11:32 +00:00
|
|
|
- **Fahrenheit country set** — `api/content_payloads.py`'s `F_COUNTRIES` is
|
|
|
|
|
canonical; `frontend`'s `server/internal/format::FCountries` and
|
docs: correct file references and the dev reachability claim
Audited the five CLAUDE.md files and all twenty-one README.md files against the
tree, machine-checking every in-repo path they name and verifying the testable
claims against the live hosts.
The one that matters is in the root file: dev was documented as reachable on
the mesh at 10.10.0.2:8137. It is not, and never was from anywhere but vps1 —
infra/docker-compose.yml binds the port to 127.0.0.1, and the address answers
from neither vps2 nor vps1 itself. Anyone following it gets a connection
refused with nothing to explain it.
The rest are stale paths, several from the reunification:
* assetlinks.json moved under frontend/static/ in the subtree merge; the TWA
README kept the pre-merge path in both places it names it. Following it
would put the file where nothing serves it and Android app-link
verification would fail silently.
* push.py and notify.py now live in backend/notifications/.
* INFRA.md and deploy/stack/README have never existed in this repo, in any
branch.
* the Caddyfile is at deploy/stack/lb/Caddyfile.
* three bare relative paths that resolve for a reader but not from the
directory the file sits in: units.js is the frontend's, deploy.sh is
infra's, entrypoint.sh is the backend's.
Also records why mesh clients must pin the ROOT_URL host and not only the image
host: the registry's bearer-token realm follows ROOT_URL, so pinning
git.thermograph.org alone still sends the token request out the public route,
where the /v2/* matcher returns 403 and docker falls back to anonymous. That
surfaces as `unauthorized: reqPackageAccess`, indistinguishable from a bad
credential.
Verified true and left alone: the four-domain layout, both .claude runbooks,
the absence of any domain-level .forgejo directory, the pinned compose project
name, the deploy contract, prod's eight stack services, beta's five prefixed
ones with no db of its own, dev's five, and every documented make target.
2026-08-01 18:49:28 +00:00
|
|
|
`frontend/static/units.js`'s `F_REGIONS` must stay identical to it. Both are asserted
|
2026-07-25 21:11:32 +00:00
|
|
|
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.
|
2026-07-25 07:08:54 +00:00
|
|
|
- **`/healthz` and `/api/version` are deliberately I/O-free** so they stay cheap
|
|
|
|
|
under tight healthcheck intervals.
|
2026-07-22 18:58:36 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
## Layout
|
2026-07-22 18:58:36 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
`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/`.
|
2026-07-22 18:58:36 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
`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).
|
2026-07-22 18:58:36 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
## Commits & PRs
|
2026-07-22 18:58:36 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
Concise and technical. Never mention AI, assistants or automated authorship.
|