|
All checks were successful
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-frontend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / validate-observability (pull_request) Successful in 38s
PR build (required check) / build-backend (pull_request) Successful in 1m19s
PR build (required check) / gate (pull_request) Successful in 2s
Reported from #general: "@Thermograph Vilnius, Lithuania" came back "I don't track a city called 'Vilnius, Lithuania' yet." Vilnius has been in cities.json the whole time, with country "Lithuania" sitting right there in the record. The resolver matched the query against `name` alone, exact-or-prefix, so the comma form could never match anything and the country field was never consulted. The same message also showed the two neighbouring failures: "Vilinus" (one transposition) fell through to the same refusal, which reads as "we've never heard of Vilnius" rather than "you typed it wrong"; and "Tell me about weather in lithuania" was quoted back verbatim as a city we don't track, which is what made it look broken rather than merely unmatched. So the resolver now goes, in descending order of confidence: exact name honouring a "City, Country/Region" qualifier; the same ignoring an unrecognised qualifier (better to grade Paris and name it in the reply than refuse over "Paris, Wherever"); prefix; close-enough name for typos; and finally a city or country named somewhere inside a sentence, so "weather in lithuania" lands on Vilnius. Accents fold both ways, since people type Zurich and Sao Paulo far more often than Zürich and São Paulo. Free-text matching is whole-word and ignores names under four characters, so ordinary chatter doesn't turn into a weather report -- "how are you today" still resolves to nothing. And the unmatched reply no longer reads a whole sentence back as a place name. This fixes /grade identically; it shared the resolver and the same bug. Claude-Session: https://claude.ai/code/session_015Z1ebLbhUxeZ9ozpNrVTCP |
||
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| frontend | ||
| infra | ||
| observability | ||
| CLAUDE.md | ||
| CUTOVER-NOTES.md | ||
| README.md | ||
thermograph
The Thermograph monorepo — the split repos reunified (2026-07-22) with full history via subtree merges, while keeping everything the split was actually for: per-domain images, per-domain deploys, and an async FE/BE contract.
Domains
| Dir | What | CI |
|---|---|---|
backend/ |
FastAPI graded-climate API, accounts, notifications (Discord bot, push, mail), data pipeline | backend-build-push → image emi/thermograph/backend; backend-deploy[-prod|-dev] |
frontend/ |
Public client: static JS/CSS + SSR pages | frontend-* mirrors of the above; image emi/thermograph/frontend |
infra/ |
Compose, deploy scripts, terraform, SOPS secrets vault, ops cron | infra-sync (host checkout + secrets render), secrets-guard, ops-cron |
observability/ |
Loki + Grafana + Alloy stack | observability-validate |
thermograph-docs deliberately stays its own repo (ADRs + runbooks, no
build artifacts, different change cadence).
How CI stays decoupled
Every workflow in .forgejo/workflows/ is path-filtered to its domain: a
push touching only frontend/** builds/deploys nothing else. Images stay
separate (emi/thermograph/backend, emi/thermograph/frontend, each tagged
sha-<12hex>), deploys stay per-service (infra/deploy/deploy.sh SERVICE=backend|frontend|all), and the API version contract
(GET /api/version, PAYLOAD_VER) still lets FE and BE ship out of lockstep.
The one intentionally coupled piece is pr-build.yml: a single always-running
gate required check that builds only the domains a PR touches (a
path-filtered required check would deadlock auto-merge).
Branch model (unchanged from the split era): PRs → dev, main → beta,
release → prod; infra tracked via main on all hosts.
Before pointing anything live at this repo, read CUTOVER-NOTES.md.