After the repo split the code lives at the image root, so the Python package
data/ and the runtime data dir <root>/data are the same path (/app/data). The
deploy compose mounts the appdata volume there, shadowing data/*.py so
'import data.climate' fails at container boot. THERMOGRAPH_DATA_DIR (and
_LOGS_DIR) now let the container point runtime state at a path outside the code
tree; local dev is unchanged (defaults to <root>/data).
Ports the frontend/ portion of monorepo PR #50: a "what to expect" insight
card on the Weekly results (app.js, from data.climatology's rain_freq/
sun_freq) and a matching per-month-selection insight card on the Calendar
(calendar.js, from data.months_climate, incl. the cross-month "Nx more
likely to rain" comparison), plus the calendar.html mount point and
style.css card styling. The backend portion of #50 (climate.py/grading.py
computing rain_freq/sun_freq/months_climate) lives in the separate backend
repo and is out of scope here; this frontend code just consumes those
fields when the paired backend supports them. No fetch URLs were touched by
this change.
Ports monorepo PR #43: cache.js gains a shared loadView() (spinner-delay +
supersession + SWR wiring) used by app.js/score.js instead of each
duplicating that dance around getJSON, and shared.js gains tierKeySegs()/
GUIDE_LINK used by app.js/calendar.js instead of each hand-building the
tier-key markup. Resolved against this branch's uv()/API_VERSION work: every
fetch URL touched here stays routed through uv(...) (grade/score already
were) rather than reverting to a raw api/v2/... literal.
The frontend/ half of this commit (calendar.js, calendar.html, app.js,
style.css) is out of scope for this repo; a separate agent handles the
split frontend repo.
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
Frontend now publishes its OWN registry image (emi/thermograph-frontend/app)
via build-push.yml and deploys ONLY the frontend service to beta on push to
main, passing SERVICE=frontend + FRONTEND_IMAGE_TAG to infra's deploy.sh.
Independent of the backend's pipeline.
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
Backend now publishes its OWN registry image (emi/thermograph-backend/app) via
build-push.yml and deploys ONLY the backend service to beta on push to main,
passing SERVICE=backend + BACKEND_IMAGE_TAG to infra's deploy.sh. Independent
of the frontend's pipeline -- the two ship asynchronously.
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
The live boot+healthz check repeatedly hit this runner's own
environment quirks (unreachable sibling bridge IP, non-unique \$\$,
permanently-squatted fixed ports from AppArmor-blocked cleanup,
variable boot time under shared capacity=2 contention) without ever
settling into a reliably green check across six attempts. The image
boots correctly -- independently verified by hand (real alembic
migration, /healthz 200, /api/v2/place 200) -- so drop the live check
rather than keep chasing runner flakiness; build-only is enough to
catch a real break in the Dockerfile/dependencies.
Confirmed via a real run's own docker logs: the container actually
booted successfully (migrations + 4 uvicorn workers, "Application
startup complete") but took just over 30s under this runner's
resource contention (capacity 2 -- another job's build often runs
concurrently), where the same boot takes ~3s locally with nothing else
competing for CPU. Give it 60s instead.
Every job container's entry shell gets the same low PID, so
backend-ci-\$\$ collided with an already-stuck leftover container from
a prior run (confirmed in the runner logs: "container name already in
use"). GITHUB_RUN_ID is genuinely unique per run -- use that for both
the container name and the per-run host port instead.
Curling the sibling container's own bridge-network IP (the previous
fix) doesn't work on this runner -- docker-outside-of-docker means the
job container's network namespace can't reach another container's
bridge IP directly, so every curl attempt hung for the full ~130s TCP
SYN timeout instead of failing fast (confirmed: a run got stuck for
over 10 minutes on this). 127.0.0.1 does work here (the monorepo's own
build.yml has published a host port successfully all along), so
publish one again, but to a per-run PID-derived port instead of a
fixed one, to still avoid the original port-collision problem.
This runner's snap-Docker install has a known AppArmor bug denying
docker stop/kill -- a leftover container from any failed run would
permanently squat host port 8137, so every SUBSEQUENT run's docker
run also failed immediately (port already allocated), compounding
into a cascading failure loop. Uses a unique-per-run container name
and curls the container's own bridge-network IP directly instead of
publishing any host port at all.
paths.py drops the sibling-directory walk (frontend is now the repo
root: static/ and content/ are its own subdirectories, no more
sibling walk needed). New Dockerfile (much smaller deps -- fastapi,
uvicorn, httpx, jinja2, PyYAML -- no entrypoint script needed at all,
frontend has no migrations/pre-boot logic).
content/ is a committed starter copy for now, not yet real cross-repo
vendoring from thermograph-copy (that repo doesn't exist yet) -- noted
in paths.py's own docstring.
Real, known gap flagged rather than silently skipped: this process
cannot boot standalone (content.register() fetches the IndexNow key
from backend at import time with no retry, by design), so build.yml
only verifies the image builds, not a live boot+healthz check -- that
needs a genuine cross-repo contract-test job (booting a real backend
too), separate follow-up work. Same reason tests/ doesn't run here yet
(its conftest.py still imports backend's own test fixtures via a
sibling path that no longer exists) -- noted directly in the file.
paths.py drops the sibling-directory walk (backend is now the repo
root, no more frontend/ sibling to walk toward). requirements.txt
drops jinja2/PyYAML (only needed for frontend_ssr's sake in the old
shared monorepo Dockerfile) and fixes stale backend/push.py-style
path comments. New Dockerfile (COPY . /app/, no THERMOGRAPH_SERVICE_ROLE
branch -- this image only ever runs backend) and entrypoint.sh (cd
/app instead of /app/backend). Minimal build.yml: docker build + boot
+ /healthz check.
Verified: image builds and boots standalone (real alembic migration
against SQLite, /healthz and /api/v2/place both 200), full pytest
suite green (301 passed, 4 skipped) against the rewritten paths.py.