From af21d8e477762ef4f76c0bda7421b7209bd3d2e9 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Sat, 1 Aug 2026 11:49:28 -0700 Subject: [PATCH 1/2] docs: correct file references and the dev reachability claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 6 ++++-- backend/CLAUDE.md | 2 +- backend/README.md | 2 +- infra/deploy/db/README.md | 2 +- infra/deploy/forgejo/README.md | 24 +++++++++++++++++++++++- infra/deploy/twa/README.md | 9 +++++---- infra/terraform/README.md | 3 ++- observability/README.md | 2 +- 8 files changed, 38 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e7e36f7..13c3446 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,8 +29,10 @@ concurrently. Read both before dispatching parallel work. **`dev` is a first-class hosted environment, not just an integration branch.** It runs on vps1 — the same box as Forgejo and the monitoring stack — with its -own Postgres container, reachable only on the WireGuard mesh -(`10.10.0.2:8137`): no public DNS record, no Caddy site, no TLS. It is deployed +own Postgres container, reachable only from vps1 itself (`127.0.0.1:8137` — +`infra/docker-compose.yml` binds the port to loopback, so the mesh does not +reach it either; use an SSH tunnel): no public DNS record, no Caddy site, no +TLS. It is deployed by CI like beta and prod. The desktop hosts no Thermograph environment at all; `make dev-up` there is a laptop convenience for running the stack locally, not a deployment target. diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 02b479a..6ad5216 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -45,7 +45,7 @@ for systemd/CI callers. Entry target is `app:app`. that disagree with the labels the API returns. - **Fahrenheit country set** — `api/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 + `frontend/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 diff --git a/backend/README.md b/backend/README.md index f80f698..6c811fc 100644 --- a/backend/README.md +++ b/backend/README.md @@ -69,7 +69,7 @@ cities_flavor.json bundled reference data (generated by gen_cities.py / only builds and publishes its own container image (`git.thermograph.org/admin_emi/thermograph-backend/app`) and hands infra a tag to roll out (`SERVICE=backend` + `BACKEND_IMAGE_TAG` into infra's - `deploy/deploy.sh`). + `infra/deploy/deploy.sh`). - **`thermograph-docs`** holds the cross-repo architecture/runbook docs; this README only covers what's local to this service. diff --git a/infra/deploy/db/README.md b/infra/deploy/db/README.md index 4f341c6..047312b 100644 --- a/infra/deploy/db/README.md +++ b/infra/deploy/db/README.md @@ -16,7 +16,7 @@ The `db` service runs the stock **`timescale/timescaledb:latest-pg18`** image TTL, and the `recent_stamp` token embedded in derived-payload validity. The schema is created by Alembic (`backend/alembic/versions/0002_climate_hypertables.py`, -run at app boot via `deploy/entrypoint.sh`). The app reads/writes it through +run at app boot via `backend/deploy/entrypoint.sh`). The app reads/writes it through `backend/data/climate_store.py` (psycopg + polars). See `deploy/POSTGRES-MIGRATION.md` for the parquet→hypertable cutover. diff --git a/infra/deploy/forgejo/README.md b/infra/deploy/forgejo/README.md index 461f1ad..46a0381 100644 --- a/infra/deploy/forgejo/README.md +++ b/infra/deploy/forgejo/README.md @@ -3,7 +3,7 @@ Runs as `deploy/forgejo/docker-stack.yml` — the only Swarm-scheduled workload this cluster carries (prod and beta's app stacks are separate `docker stack deploy`s that happen to run on the manager node, vps2 — see -`deploy/stack/README` context in `DEPLOY.md`). Pinned to the **vps1** node +`DEPLOY.md`). Pinned to the **vps1** node (`75.119.132.91`) via the `role=forge` label from `deploy/swarm/label-forge-node.sh`. @@ -153,6 +153,28 @@ echo "10.10.0.2 git.thermograph.org" | sudo tee -a /etc/hosts numbering — adjust if you assigned it differently.) The git/web UI keeps working normally for everyone else since only `/v2/*` is restricted. +**Pin the canonical name too, not just the image host.** Forgejo derives the +registry's bearer-token realm from `ROOT_URL`, so a client that already +resolves `git.thermograph.org` over the mesh is still sent to +`https:///v2/token` to collect a token. When `ROOT_URL` became +`dev.jinemi.com`, that second request took the public route, the `/v2/*` +matcher above answered 403, and docker fell back to anonymous — every push then +failed with `unauthorized: reqPackageAccess`, which reads exactly like a +revoked token or a missing scope. It is neither. Pin both names: + +``` +echo "10.10.0.2 git.thermograph.org dev.jinemi.com" | sudo tee -a /etc/hosts +``` + +The realm moves whenever `ROOT_URL` does, so read it rather than assuming: + +```sh +curl -sI https://git.thermograph.org/v2/ | grep -i www-authenticate +``` + +These `/etc/hosts` pins are host state. Nothing in this repo writes them, so a +reprovisioned node has to be pinned again before it can pull. + ## Register the Actions runner Once Forgejo answers at its domain: diff --git a/infra/deploy/twa/README.md b/infra/deploy/twa/README.md index fa30d5f..8621835 100644 --- a/infra/deploy/twa/README.md +++ b/infra/deploy/twa/README.md @@ -14,7 +14,7 @@ submission ($25 one-time) is deferred. ## What's in the repo - `twa-manifest.json` — Bubblewrap config (reference values; see comment inside). -- `../../frontend/.well-known/assetlinks.json` — Digital Asset Links, served at +- `../../../frontend/static/.well-known/assetlinks.json` — Digital Asset Links, served at `https://thermograph.org/.well-known/assetlinks.json`. Its fingerprint is a **placeholder** until you generate the signing key (step 4). @@ -45,7 +45,7 @@ bubblewrap build bubblewrap fingerprint list # or: keytool -list -v -keystore android-keystore.jks ``` -Copy the `SHA256` value into `frontend/.well-known/assetlinks.json`, replacing +Copy the `SHA256` value into `frontend/static/.well-known/assetlinks.json`, replacing `REPLACE_WITH_TWA_SIGNING_KEY_SHA256_FINGERPRINT`, then **deploy the site** so the new file is live. Confirm: @@ -80,6 +80,7 @@ install for every user. Store it out of the repo, backed up. `.aab`; the store review for a well-formed TWA is light. Not part of this pass. ## Why no backend changes -Android delegates the TWA's web notifications to the OS, so `backend/push.py` -(VAPID) and `backend/notify.py` deliver push to the app unchanged. Nothing in +Android delegates the TWA's web notifications to the OS, so +`backend/notifications/push.py` (VAPID) and `backend/notifications/notify.py` +deliver push to the app unchanged. Nothing in `backend/` is touched by this track. diff --git a/infra/terraform/README.md b/infra/terraform/README.md index ea040f2..b308183 100644 --- a/infra/terraform/README.md +++ b/infra/terraform/README.md @@ -183,7 +183,8 @@ Terraform brings the stack up — don't let Terraform recreate containers mid-mi `git.thermograph.org` reaches Forgejo's loopback port on vps1. `COOKIE_SECURE` is auto-set to `0` when there's no domain (a Secure cookie is never sent over plain HTTP) and `1` behind Caddy TLS. -- The rendered Caddyfile only reverse-proxies the app. The repo's `deploy/Caddyfile` +- The rendered Caddyfile only reverse-proxies the app. The repo's + `deploy/stack/lb/Caddyfile` additionally serves the `emigriffith.dev` portfolio and legacy redirects; those are host-specific and not templated here. - Provisioner-based by design: the hosts already exist, so this is not a diff --git a/observability/README.md b/observability/README.md index d2eb77e..a479e98 100644 --- a/observability/README.md +++ b/observability/README.md @@ -147,7 +147,7 @@ LOKI_URL=http://10.10.0.2:3100/loki/api/v1/push \ docker compose -f docker-compose.agent.yml -f docker-compose.agent.beta.yml up -d ``` -The mesh must already be up (see the main repo's `deploy/swarm/` / `INFRA.md`); +The mesh must already be up (see `infra/deploy/swarm/`); Swarm is **not** required for the agent itself — it talks plain HTTP over wg0.