Commit graph

474 commits

Author SHA1 Message Date
emi
51889d6979 Merge pull request 'Promote main to release: NASA retirement' (#74) from main into release
All checks were successful
secrets-guard / encrypted (push) Successful in 17s
shell-lint / shellcheck (push) Successful in 24s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 1m22s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m28s
Deploy backend to prod VPS / deploy (push) Successful in 3m24s
Deploy frontend to prod VPS / deploy (push) Successful in 3m50s
2026-07-24 23:59:26 +00:00
emi
f596fa87d8 Merge pull request 'Promote dev to main: NASA retirement' (#73) from dev into main
All checks were successful
secrets-guard / encrypted (push) Successful in 12s
shell-lint / shellcheck (push) Successful in 11s
secrets-guard / encrypted (pull_request) Successful in 12s
shell-lint / shellcheck (pull_request) Successful in 10s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m54s
Deploy backend to beta VPS / deploy (push) Successful in 2m24s
2026-07-24 23:59:00 +00:00
emi
005567dc51 Retire NASA POWER from every serving path (#72)
Some checks failed
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 12s
secrets-guard / encrypted (pull_request) Successful in 14s
PR build (required check) / changes (pull_request) Successful in 18s
shell-lint / shellcheck (pull_request) Successful in 14s
PR build (required check) / validate-observability (pull_request) Has been skipped
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m22s
Deploy backend to LAN dev server / build (push) Successful in 1m42s
PR build (required check) / build-frontend (pull_request) Successful in 1m19s
PR build (required check) / build-backend (pull_request) Successful in 1m33s
PR build (required check) / gate (pull_request) Successful in 1s
Deploy backend to LAN dev server / deploy (push) Failing after 2m0s
2026-07-24 23:56:34 +00:00
emi
ca84e0ce95 Promote dev → main (frontend QA batch → beta) (#71)
Some checks failed
Deploy frontend to beta VPS / deploy (push) Failing after 8s
Sync infra to hosts / sync-beta (push) Successful in 7s
Deploy backend to beta VPS / deploy (push) Failing after 13s
secrets-guard / encrypted (push) Successful in 12s
Sync infra to hosts / sync-prod (push) Successful in 14s
shell-lint / shellcheck (push) Successful in 12s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 57s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 1m2s
2026-07-24 23:20:16 +00:00
emi
6df4968679 Frontend QA batch: date/TZ, https origin, date-422, VAPID rotation, trace-precip, partial-day gate (#70)
Some checks failed
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 13s
secrets-guard / encrypted (pull_request) Successful in 14s
PR build (required check) / changes (pull_request) Successful in 16s
PR build (required check) / validate-observability (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 28s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m58s
Deploy frontend to LAN dev server / build (push) Successful in 1m59s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 2m5s
Deploy backend to LAN dev server / build (push) Successful in 2m20s
PR build (required check) / build-frontend (pull_request) Successful in 1m44s
Deploy frontend to LAN dev server / deploy (push) Successful in 30s
PR build (required check) / build-backend (pull_request) Successful in 2m0s
PR build (required check) / gate (pull_request) Successful in 2s
Deploy backend to LAN dev server / deploy (push) Failing after 1m58s
2026-07-24 23:13:36 +00:00
emi
1c763a61c5 Merge pull request 'key-gaps: the key regex was blind to digits, inventing missing secrets' (#69) from fix/key-gaps-digit-regex into dev
All checks were successful
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 6s
2026-07-24 22:59:12 +00:00
Emi Griffith
f4313b5d87 Drop accidentally-committed worktrees; ignore .claude/worktrees
All checks were successful
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 6s
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-backend (pull_request) Successful in 44s
PR build (required check) / gate (pull_request) Successful in 3s
The previous commit swept in .claude/worktrees/city-resolver and
.claude/worktrees/thermograph-mentions as embedded git repositories. Those are
other Claude sessions' live checkouts and have no business in this tree.

Ignoring the directory so `git add -A` cannot do it again — the repo already
has a documented history of parallel sessions colliding through shared
checkouts, and this is the same hazard wearing a different hat.

Claude-Session: https://claude.ai/code/session_0182KTMrsTHJc3TcewCatJFY
2026-07-24 15:57:34 -07:00
Emi Griffith
7ed375f32e key-gaps: the key regex was blind to digits, inventing missing secrets
`grep -oE "^[A-Z_]+="` cannot match a key name containing a digit. This estate
has exactly six such names — all the *_S3_* keys — so a prod audit returned 26
keys against 32 real ones, and reported the S3 and lake credentials as missing
from both live hosts.

They were present the whole time. The phantom was independently reproduced
twice by re-running the same pattern, which is what made it convincing, and it
was briefly recorded as one of two root causes of the lake being unqueryable
(#56). That issue has one cause: the missing duckdb-lake image.

An audit that under-reports is worse than no audit. A missing-secret finding
sends someone to provision a credential that already exists, and in a rotation
tool it would justify writing over one.

Fixed to `^[A-Z][A-Z0-9_]*=` in both the skill and key_gaps.py's docstring. The
match still stops at the `=`, so no value is read — that property is the reason
this grep exists rather than a parser.

Centralis's secrets_gaps carried the same bug and is fixed separately, with a
regression test.

Claude-Session: https://claude.ai/code/session_0182KTMrsTHJc3TcewCatJFY
2026-07-24 15:57:15 -07:00
emi
66f2cf0dfe Merge pull request 'Promote main to release: MET forecast coverage gate' (#65) from main into release
All checks were successful
secrets-guard / encrypted (push) Successful in 17s
shell-lint / shellcheck (push) Successful in 16s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m3s
Deploy backend to prod VPS / deploy (push) Successful in 2m0s
2026-07-24 22:26:48 +00:00
emi
150029075e Merge pull request 'Promote dev to main: MET forecast coverage gate' (#64) from dev into main
All checks were successful
secrets-guard / encrypted (push) Successful in 9s
shell-lint / shellcheck (push) Successful in 16s
secrets-guard / encrypted (pull_request) Successful in 11s
shell-lint / shellcheck (pull_request) Successful in 13s
Deploy backend to beta VPS / deploy (push) Successful in 51s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m22s
2026-07-24 22:26:20 +00:00
emi
248eeee110 Gate MET forecast days on diurnal sample coverage (#63)
All checks were successful
secrets-guard / encrypted (push) Successful in 10s
shell-lint / shellcheck (push) Successful in 12s
secrets-guard / encrypted (pull_request) Successful in 10s
PR build (required check) / changes (pull_request) Successful in 14s
shell-lint / shellcheck (pull_request) Successful in 11s
PR build (required check) / validate-observability (pull_request) Has been skipped
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m41s
Deploy backend to LAN dev server / build (push) Successful in 1m48s
PR build (required check) / build-frontend (pull_request) Successful in 1m23s
PR build (required check) / build-backend (pull_request) Successful in 1m38s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Successful in 33s
2026-07-24 22:23:51 +00:00
emi
67996d5b77 Merge pull request 'Promote main to release: first alerting + Postfix supervision' (#61) from main into release
All checks were successful
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 7s
2026-07-24 20:58:14 +00:00
emi
633c6a5ace Merge pull request 'Promote dev to main: first alerting + Postfix supervision' (#60) from dev into main
Some checks failed
Sync infra to hosts / sync-prod (push) Failing after 7s
Sync infra to hosts / sync-beta (push) Successful in 13s
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 13s
Validate observability stack / validate (push) Successful in 17s
secrets-guard / encrypted (pull_request) Successful in 6s
shell-lint / shellcheck (pull_request) Successful in 6s
2026-07-24 20:49:08 +00:00
emi
008052b412 Merge pull request 'observability: add the estate's first alerting; supervise Postfix' (#58) from ops/mail-supervision-and-alerting into dev
All checks were successful
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 8s
Validate observability stack / validate (push) Successful in 13s
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 8s
PR build (required check) / validate-observability (pull_request) Successful in 36s
PR build (required check) / build-frontend (pull_request) Successful in 1m6s
PR build (required check) / gate (pull_request) Successful in 2s
2026-07-24 20:44:21 +00:00
Emi Griffith
e4693dce58 observability: add the estate's first alerting; supervise Postfix
All checks were successful
secrets-guard / encrypted (pull_request) Successful in 8s
shell-lint / shellcheck (pull_request) Successful in 10s
PR build (required check) / changes (pull_request) Successful in 16s
PR build (required check) / validate-observability (pull_request) Successful in 18s
PR build (required check) / gate (pull_request) Successful in 2s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
The estate had zero alert rules. The only contact point was Grafana's factory
default pointing at the literal string <example@email.com>, and beta's
untracked compose override routed Grafana's SMTP at prod's Postfix — so
alerts, had any existed, would have been delivered by the box most likely to
be on fire. Prod served 86 5xx in 24h including five /healthz failures and
nobody was told.

Alerting (deploys to beta, which is where Grafana runs):
- 12 Loki-based rules. There is no Prometheus in this estate and Loki is the
  only datasource, so every rule is log-derived.
- Thresholds come from a 24h backtest that happens to contain a real ~20min
  prod outage at 04:20Z. Absolute counts, not ratios: prod runs ~7 req/min, so
  one bad request is 1.4% and a ratio alert would scream all night. The 5xx
  burst rule fires on the outage's 45 and 22 buckets and on nothing else in
  the day; the largest benign bucket all day was 5.
- Routed to a new private #ops-alerts channel, not to any existing channel —
  #weather-events, #announcements and #prod are product surfaces that notify
  real subscribers.
- AlertingWatchdog is a dead-man's switch; its value is its absence.
- Delivery is proven, not assumed: the rules were provisioned into a throwaway
  Grafana against beta's live Loki and a real alert arrived in Discord. This
  matters because GET /api/v1/provisioning/contact-points returns [REDACTED]
  for the URL — a contact point holding an uninterpolated env var looks
  perfectly healthy and pages nobody. The only proof is a message arriving.

CI gains a structural check, because the existing one only proves YAML parses:
an alert rule whose condition names a missing refId is valid YAML, provisions
cleanly, and never fires. It also hard-fails on a literal Discord webhook in
the repo. Verified against all three breakages deliberately introduced.

Postfix supervision:
- The 13h outage was a boot-ordering race, not a Docker renumbering: postfix
  started at 08:09:49, wg0 came up at :51, postfix fataled at :52 on a missing
  docker_gwbridge address, and dockerd did not finish starting until 08:10:16.
  Stock postfix@.service is ordered only After=network-online.target and ships
  no Restart=, so one lost race became a permanent outage.
- An ExecStartPre gate now blocks up to 60s until every inet_interfaces
  address actually exists, which absorbs the transient case inside a single
  start attempt. That makes bounded retry correct: 5 attempts in 600s, then
  failed — a genuinely broken config reaches a visible failed state in ~100s
  instead of re-fataling every 15s forever.
- A 5-minute watchdog timer retries indefinitely and runs reset-failed, so
  "failed" still self-heals. Worst case is ~5 minutes, not 13 hours.
- Wants=, not Requires=: a dockerd failure must not take down the loopback and
  mesh listeners that do not depend on Docker at all.

Health checks must read config with `postconf -c`, never postmulti/postqueue/
postfix — those three RESOLVE inet_interfaces and so fatal precisely when an
address is missing, which made the first version of this check report
status=ok bound=0/0. A health check that fails open is worse than none.

DEPLOY.md carries the monitoring contract, including that systemctl is-active
postfix is a known-false signal: postfix.service is a wrapper whose
ExecStart=/bin/true, so it reports active forever while the real postfix@-
instance is failed with zero listeners. Reproduced live.

Known gap, documented not fixed: Alloy ships Docker stdout, Caddy files and
app JSONL, not journald — so no Postfix line reaches Loki and the mail rules
cannot fire until loki.source.journal is added.

Claude-Session: https://claude.ai/code/session_0182KTMrsTHJc3TcewCatJFY
2026-07-24 13:19:19 -07:00
emi
60d5af41dd Merge pull request 'Promote dev to main: off-box encrypted backups' (#57) from dev into main
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 6s
secrets-guard / encrypted (push) Successful in 5s
Sync infra to hosts / sync-prod (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 7s
2026-07-24 20:10:12 +00:00
emi
519541c378 Merge pull request 'Off-box encrypted backups to Contabo Object Storage' (#14) from feature/storage-backups into dev
All checks were successful
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 8s
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 7s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 46s
PR build (required check) / gate (pull_request) Successful in 3s
2026-07-24 20:08:13 +00:00
Emi Griffith
e91b49a000 Merge remote-tracking branch 'origin/dev' into feature/storage-backups-merged
All checks were successful
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 7s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 2s
# Conflicts:
#	infra/deploy/secrets/beta.yaml
#	infra/deploy/secrets/prod.yaml
2026-07-24 13:07:10 -07:00
emi
4daa10c76d Merge pull request 'Promote main to release: geocode P0 fix + ops tooling + privacy logging' (#53) from main into release
All checks were successful
secrets-guard / encrypted (push) Successful in 9s
shell-lint / shellcheck (push) Successful in 8s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 47s
Deploy backend to prod VPS / deploy (push) Successful in 1m30s
2026-07-24 19:53:19 +00:00
emi
0935eea394 Merge pull request 'Promote dev to main: geocode P0 fix + ops tooling + privacy logging' (#52) from dev into main
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 8s
secrets-guard / encrypted (push) Successful in 12s
shell-lint / shellcheck (push) Successful in 10s
Sync infra to hosts / sync-prod (push) Successful in 16s
Deploy backend to beta VPS / deploy (push) Successful in 38s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 52s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 10s
2026-07-24 19:48:39 +00:00
emi
cda6e75b38 data/climate: fix geocode_nominatim's NameError on every call (P0, live) (#51)
All checks were successful
secrets-guard / encrypted (push) Successful in 14s
shell-lint / shellcheck (push) Successful in 13s
PR build (required check) / changes (pull_request) Successful in 10s
secrets-guard / encrypted (pull_request) Successful in 10s
shell-lint / shellcheck (pull_request) Successful in 10s
PR build (required check) / validate-observability (pull_request) Has been skipped
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m24s
Deploy backend to LAN dev server / build (push) Successful in 1m49s
PR build (required check) / build-frontend (pull_request) Successful in 1m21s
PR build (required check) / build-backend (pull_request) Successful in 1m35s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Successful in 22s
2026-07-24 19:45:49 +00:00
emi
5f0f5990ae Merge pull request 'render-secrets: drop the RETURN trap, it broke deploy silently' (#50) from shell-lint into dev
All checks were successful
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 8s
2026-07-24 19:39:39 +00:00
Emi Griffith
ca7bae5933 Merge remote-tracking branch 'origin/dev' into shell-lint-merged
All checks were successful
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / changes (pull_request) Successful in 9s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 3s
2026-07-24 12:38:13 -07:00
emi
67d1734e99 ops/iceberg.sh: read-only Iceberg lake queries across all environments (#23)
All checks were successful
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 7s
2026-07-24 19:34:30 +00:00
emi
bf0aaf1ec0 Pre-warm the SEO content derived-store off-request (#49)
All checks were successful
secrets-guard / encrypted (push) Successful in 10s
shell-lint / shellcheck (push) Successful in 13s
Deploy backend to LAN dev server / build (push) Successful in 1m40s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m47s
Deploy backend to LAN dev server / deploy (push) Successful in 30s
After a cell's archive advances, the first content request recomputes a 45yr payload cold. Add warm_cities.warm_content — idempotent, cache-only, bounded — that pre-populates the content derived-store under the same (kind,key,content_token) the routes read, wired into the leader-gated notifier loop (50 cities/30-min tick) so the ~1000-city set stays warm within hours of each daily advance.
2026-07-24 19:31:08 +00:00
emi
6fef57f05f Check derived store before loading history on content routes (#48)
Some checks failed
Deploy backend to LAN dev server / deploy (push) Blocked by required conditions
Build + push backend image (Forgejo registry) / build-push (push) Has been cancelled
Deploy backend to LAN dev server / build (push) Has been cancelled
shell-lint / shellcheck (push) Has been cancelled
secrets-guard / encrypted (push) Has been cancelled
The three content handlers loaded the full ~45yr archive before checking the cache, so even hits paid the load. Restructure to resolve cell + compute content_token cheaply first, check the store, and load history only inside the miss-only build() closure. Cache hits and 304s now return without touching the archive. Keys/kinds/ETag unchanged.
2026-07-24 19:30:57 +00:00
emi
a4fa72639f Add cheap, stable content-page cache token (#47)
Some checks failed
Deploy backend to LAN dev server / deploy (push) Blocked by required conditions
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 7s
Build + push backend image (Forgejo registry) / build-push (push) Has been cancelled
Deploy backend to LAN dev server / build (push) Has been cancelled
Content pages keyed cache validity on the full-history hist_end, invalidating ~1-2x/day and paying a 45yr load even to compute the token. Add content_token(cell_id) = PAYLOAD_VER:CONTENT_VER:max_date, backed by an indexed MAX(date) (climate_store.history_max_date / climate.history_max_date), so it survives intra-day top-ups and needs no full-history load.
2026-07-24 19:30:38 +00:00
emi
864d38d772 ops/dbq.sh: read-only Postgres queries across all environments (#18)
All checks were successful
secrets-guard / encrypted (push) Successful in 16s
shell-lint / shellcheck (push) Successful in 17s
2026-07-24 19:28:58 +00:00
emi
4ff113f2cf Stop logging /healthz + internal SSR hop, truncate logged IPs (#35)
Some checks failed
secrets-guard / encrypted (push) Has been cancelled
shell-lint / shellcheck (push) Has been cancelled
Deploy backend to LAN dev server / build (push) Successful in 1m22s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m24s
Deploy backend to LAN dev server / deploy (push) Successful in 19s
2026-07-24 19:28:47 +00:00
emi
b1b97e7c7d Merge pull request 'Promote main to release: Iceberg-backed lake query' (#46) from main into release
All checks were successful
secrets-guard / encrypted (push) Successful in 9s
shell-lint / shellcheck (push) Successful in 13s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 58s
Deploy backend to prod VPS / deploy (push) Successful in 2m59s
2026-07-24 19:12:19 +00:00
emi
6f5682b7d7 Merge pull request 'Promote dev to main: Iceberg-backed lake query' (#45) from dev into main
All checks were successful
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 8s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 10s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 41s
Deploy backend to beta VPS / deploy (push) Successful in 47s
2026-07-24 19:11:50 +00:00
emi
0f1b3a168c Lake /query reads era5_daily through Iceberg metadata (#44)
All checks were successful
shell-lint / shellcheck (push) Successful in 7s
secrets-guard / encrypted (push) Successful in 9s
PR build (required check) / changes (pull_request) Successful in 14s
secrets-guard / encrypted (pull_request) Successful in 12s
shell-lint / shellcheck (pull_request) Successful in 13s
PR build (required check) / validate-observability (pull_request) Has been skipped
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m27s
Deploy backend to LAN dev server / build (push) Successful in 1m43s
PR build (required check) / build-frontend (pull_request) Successful in 1m23s
PR build (required check) / build-backend (pull_request) Successful in 1m31s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Successful in 25s
2026-07-24 19:09:22 +00:00
emi
81c579ddcb infra/forgejo: raise LAN runner concurrency to 8 (#43)
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 8s
Sync infra to hosts / sync-prod (push) Successful in 10s
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 8s
2026-07-24 19:05:21 +00:00
emi
a3a1209645 Merge pull request 'Promote main to release: daemon roll + Iceberg-backed lake query' (#42) from main into release
All checks were successful
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 12s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 37s
Deploy frontend to prod VPS / deploy (push) Successful in 48s
2026-07-24 19:02:19 +00:00
emi
88f5d0cce0 Merge pull request 'Promote dev to main: daemon roll + Iceberg-backed lake query' (#41) from dev into main
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 6s
Sync infra to hosts / sync-prod (push) Successful in 11s
secrets-guard / encrypted (push) Successful in 9s
Validate observability stack / validate (push) Successful in 14s
shell-lint / shellcheck (push) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 8s
2026-07-24 19:01:52 +00:00
emi
578fb563a4 Roll the daemon with backend deploys in the Swarm stack (#40)
All checks were successful
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 7s
PR build (required check) / changes (pull_request) Successful in 9s
secrets-guard / encrypted (pull_request) Successful in 8s
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Successful in 23s
PR build (required check) / build-frontend (pull_request) Successful in 57s
PR build (required check) / gate (pull_request) Successful in 2s
2026-07-24 18:59:46 +00:00
emi
dcf15ea572 infra/forgejo: add resource limits and a leaner CI job image (#38)
Some checks failed
Sync infra to hosts / sync-beta (push) Successful in 7s
Sync infra to hosts / sync-prod (push) Failing after 8s
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 8s
2026-07-24 18:56:46 +00:00
emi
da82abda27 frontend: fetch City() concurrently with the page's primary API call (#37)
All checks were successful
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 14s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 42s
Deploy frontend to beta VPS / deploy (push) Successful in 1m5s
MonthPage and RecordsPage each made two backend calls sequentially
(CityMonth/CityRecords, then City) where the second never depended on the
first's result. fetchWithCity launches both concurrently via goroutines
and a WaitGroup.

Verified live against a stub with an injected 400ms delay on both
endpoints: city page (1 call) and month/records pages (2 calls each) all
cost ~0.404s now, not double for the two-call pages.

Error priority preserved exactly: primary's error wins even when City
also fails, matching the old sequential code. One trade-off: City() is
now always launched even on a request about to 404 from primary, costing
one extra cheap lookup on that rare path.

Tests include a deterministic concurrency proof via rendezvous channels
(the old sequential code would deadlock this test, not just run it
slower). Full suite green under -race -count=2.
2026-07-24 06:25:06 +00:00
emi
0862395dda Log hygiene: Alloy CPU, Loki chunks/limits, Caddy field-stripping (#36)
All checks were successful
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 7s
Validate observability stack / validate (push) Successful in 12s
2026-07-24 04:37:41 +00:00
emi
979653f407 deploy.sh: fix the daemon-binary probe, which always dropped daemon (#33)
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 5s
Sync infra to hosts / sync-prod (push) Successful in 4s
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 7s
docker compose config --images daemon does not filter to the named service
on this host's Compose v5.3.1 -- it prints every service's image, one per
line, in file order, so `| head -1` was silently grabbing db's image
(timescaledb) instead of daemon's. The probe then always found no
/usr/local/bin/thermograph-daemon in a Postgres image and dropped daemon
from every backend deploy, regardless of what the real backend image
contained.

Fixed by building the image reference directly from the same vars
docker-compose.yml's daemon.image: already interpolates, instead of going
through docker compose config at all.

Reproduced against beta directly: the old sequence selected the wrong
image; the new construction resolves correctly and the binary probe
passes.
2026-07-24 04:03:26 +00:00
emi
372813b67b Merge pull request 'Promote main to release: lake extension bake + daemon healthcheck fixes' (#32) from main into release
All checks were successful
secrets-guard / encrypted (push) Successful in 11s
shell-lint / shellcheck (push) Successful in 10s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 50s
Deploy backend to prod VPS / deploy (push) Successful in 1m15s
2026-07-24 03:50:47 +00:00
emi
b83bfdbd67 Merge pull request 'Promote dev to main: lake extension bake + daemon healthcheck fixes' (#31) from dev into main
All checks were successful
Deploy backend to beta VPS / deploy (push) Successful in 42s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 44s
Sync infra to hosts / sync-beta (push) Successful in 5s
Sync infra to hosts / sync-prod (push) Successful in 6s
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 8s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 9s
2026-07-24 03:50:21 +00:00
emi
eaaa4f1ad9 Fix lake extension bake user; disable the daemon healthcheck in the stack (#30)
Some checks failed
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 18s
PR build (required check) / changes (pull_request) Successful in 9s
secrets-guard / encrypted (pull_request) Successful in 10s
shell-lint / shellcheck (pull_request) Successful in 8s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m12s
Deploy backend to LAN dev server / build (push) Successful in 1m27s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-backend (pull_request) Successful in 1m17s
PR build (required check) / build-frontend (pull_request) Successful in 1m8s
PR build (required check) / gate (pull_request) Successful in 2s
Deploy backend to LAN dev server / deploy (push) Failing after 1m55s
2026-07-24 03:47:39 +00:00
emi
f8481ecf70 Merge pull request 'Promote main to release (deploy prod): ERA5 lake stack + Go daemon' (#29) from main into release
All checks were successful
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 56s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 59s
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 9s
Deploy backend to prod VPS / deploy (push) Successful in 1m17s
Deploy frontend to prod VPS / deploy (push) Successful in 33s
2026-07-24 03:28:06 +00:00
emi
92e74c585a frontend: rewrite the SSR content service in Go (#28)
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 13s
Sync infra to hosts / sync-prod (push) Successful in 12s
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 8s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 53s
Deploy frontend to beta VPS / deploy (push) Successful in 1m16s
secrets-guard / encrypted (pull_request) Successful in 5s
shell-lint / shellcheck (pull_request) Successful in 6s
Ports frontend/ (Jinja2/FastAPI, ~1180 LOC) to Go with html/template. No
climate math, no DB, no auth -- every route fetches from the backend's
/content/* API.

Verified with a golden-HTML diff, not just unit tests: both the Python
original and the Go rewrite were run against the same committed fixtures
and every route compared byte-for-byte, confirmed programmatically. That
process caught defects unit tests alone missed, since map[string]any has
no compile-time field check:

- Render-context keys were snake_case throughout while the templates read
  PascalCase fields. A missing map key doesn't error, it silently renders
  empty -- title, meta description, canonical URL, OpenGraph tags, and the
  homepage's entire ranked list were blank on every page despite every
  route returning 200. Fixed by renaming every key to match each
  template's own documented field contract, and passing API structs
  straight through wherever their fields already matched (removes a whole
  layer of future drift risk).
- Three pages 500'd: ToolHref needed a composed href, not a bare
  "lat,lon" fragment; the records table needed the raw API struct.
- JSON-LD was double-encoded: <script type="application/ld+json"> is
  JAVASCRIPT context to html/template's escaper regardless of the
  script's type attribute, so template.HTML gets re-escaped as a quoted
  JS string. Needed template.JS. The glossary term page's JSON-LD was
  never built at all -- added.
- html/template silently strips literal HTML and JS comments from parsed
  output (verified in isolation) -- both need a FuncMap function
  returning template.HTML/template.JS to survive.

Packaging: 187MB -> 22.6MB. Two defects caught before reaching a host: the
Swarm stack's entrypoint override with no explicit command drops the
image's CMD entirely (every deploy would have exited 127), and
COPY --chown by name fails under the classic Docker builder on Alpine.
Both fixed.

go build/vet/test -race clean; docker build passes its embedded test step
under both BuildKit and the classic builder; shellcheck 0 findings.
2026-07-24 00:53:48 +00:00
emi
755886e244 Merge pull request 'Promote dev to main (deploy beta): ERA5 lake stack' (#26) from dev into main
All checks were successful
Build + push backend image (Forgejo registry) / build-push (push) Successful in 28s
Sync infra to hosts / sync-beta (push) Successful in 9s
Sync infra to hosts / sync-prod (push) Successful in 7s
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 10s
Deploy backend to beta VPS / deploy (push) Successful in 50s
2026-07-24 00:12:32 +00:00
emi
4e3c7e82c6 Merge pull request 'Reconcile: merge main (shellcheck, Go daemon) into dev' (#27) from reconcile/main-into-dev into dev
Some checks failed
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 1m15s
Deploy backend to LAN dev server / build (push) Successful in 1m23s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m32s
secrets-guard / encrypted (push) Successful in 8s
PR build (required check) / changes (pull_request) Successful in 14s
shell-lint / shellcheck (push) Successful in 16s
secrets-guard / encrypted (pull_request) Successful in 7s
shell-lint / shellcheck (pull_request) Successful in 8s
Deploy frontend to LAN dev server / build (push) Successful in 51s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-backend (pull_request) Successful in 53s
PR build (required check) / gate (pull_request) Successful in 2s
Deploy backend to LAN dev server / deploy (push) Failing after 1m54s
Deploy frontend to LAN dev server / deploy (push) Successful in 2m0s
2026-07-24 00:09:03 +00:00
Emi Griffith
30b0c70bbb Reconcile: merge main (shellcheck guard, Go daemon) into dev
All checks were successful
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / changes (pull_request) Successful in 8s
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 1m9s
PR build (required check) / build-backend (pull_request) Successful in 1m44s
PR build (required check) / gate (pull_request) Successful in 2s
main took two direct PRs (#19 shell-lint, #21 the thermograph-daemon Go
service) while dev accumulated the ERA5 lake stack; both sides added compose/
stack services and touched the same seams. Resolutions keep both worlds:

- compose + Swarm stack: lake AND daemon are sibling services; backend env
  carries THERMOGRAPH_LAKE_URL and THERMOGRAPH_INTERNAL_TOKEN.
- deploy.sh: backend deploys roll (backend lake daemon); all adds frontend.
- requirements.txt: websockets/apscheduler stay removed (moved to the Go
  daemon), duckdb stays (the lake role's engine).

Merged tree: backend suite green, both compose configs validate.
2026-07-23 17:06:52 -07:00
emi
16bddb6625 Iceberg conversion container for the ERA5 lake (infra/lake-iceberg) (#24)
All checks were successful
secrets-guard / encrypted (push) Successful in 16s
PR build (required check) / changes (pull_request) Successful in 8s
secrets-guard / encrypted (pull_request) Successful in 8s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 1m20s
PR build (required check) / build-backend (pull_request) Successful in 1m28s
PR build (required check) / gate (pull_request) Successful in 3s
2026-07-23 22:56:27 +00:00
emi
6396d553d2 Own /state in the image; lake cache degrades instead of failing (#25)
All checks were successful
secrets-guard / encrypted (push) Successful in 12s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 57s
Deploy backend to LAN dev server / build (push) Successful in 1m13s
Deploy backend to LAN dev server / deploy (push) Successful in 18s
2026-07-23 22:55:41 +00:00