Commit graph

549 commits

Author SHA1 Message Date
Emi Griffith
cc0b1e298f Port a0ec4bc Extract the shared graded-window tail in views.py from monorepo (drift reconciliation)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 12:08:13 -07:00
Emi Griffith
8cec935b06 Port 983bceb Extract the grading day-of-year window helpers from monorepo (drift reconciliation)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 12:08:03 -07:00
Emi Griffith
b1869af70a Port 3753bbc Refactor climate.py source->frame builders from monorepo (drift reconciliation)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 12:07:54 -07:00
Emi Griffith
f830bce4d9 Port 871ab9c Route notifier output to Discord channels via the bot from monorepo (drift reconciliation)
Note: this commit's deploy/thermograph.env.example changes live at the
monorepo root (outside backend/) and are out of scope for this split repo.

Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 12:07:46 -07:00
Emi Griffith
cd760f7e44 Port eb8f039 Activity-stream logging: notifier, auth, delivery, lifecycle, 500s, slow requests from monorepo (drift reconciliation)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 12:07:35 -07:00
Emi Griffith
787461656e Port 3504dd6 Send notifier push/Discord DMs concurrently instead of one at a time from monorepo (drift reconciliation)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 12:07:25 -07:00
Emi Griffith
83b6e2ebc0 Port a57d753 Move blocking calls off the event loop in async request handlers from monorepo (drift reconciliation)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 12:07:15 -07:00
Emi Griffith
290187db6b Add frontend-scoped CLAUDE.md + README + DESIGN.md, relocate tools/
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:59:59 -07:00
Emi Griffith
20b81c0afe Add backend-scoped CLAUDE.md + README, relocate register_discord_commands.py
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:58:36 -07:00
Emi Griffith
bbc9b28bfd Port orphaned infra files from monorepo (Makefile, .env.example, migrate-db, twa, DEPLOY docs)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:57:02 -07:00
Emi Griffith
4d52226a6a Add release->prod deploy (per-service, PROD_SSH_* secrets)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:52:37 -07:00
Emi Griffith
6ef7a37bf1 Add release->prod deploy (per-service, PROD_SSH_* secrets)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:52:36 -07:00
Emi Griffith
c95d5ec5f8 Pin API version + make frontend boot resilient to backend (async FE/BE deploy)
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:50:00 -07:00
emi
19e74af9ca Add validator CI + CLAUDE.md; fix dashboard-domain typo (#3) 2026-07-22 18:47:21 +00:00
Emi Griffith
8d15c314b0 Fix test split introduced by previous commit's insertion point
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:42:22 -07:00
Emi Griffith
3a2f507353 Add /api/version capability endpoint for FE/BE version negotiation
Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:41:46 -07:00
Emi Griffith
ce454e0be6 deploy.sh + compose: two independent app images, per-service rolls
backend and frontend now reference their own images (BACKEND_IMAGE_PATH/TAG,
FRONTEND_IMAGE_PATH/TAG) instead of the shared emi/thermograph/app. deploy.sh
takes SERVICE=backend|frontend|all, rolls just that service with --no-deps,
persists each service's live tag in deploy/.image-tags.env so a single-service
deploy never disturbs the sibling, and fixes the old IMAGE_TAG:? guard that
made every workflow deploy hard-fail. Health-checks per service (8137/8080).

Claude-Session: https://claude.ai/code/session_01RdARHDJaYC1wSQRTYM6t3Z
2026-07-22 11:39:16 -07:00
Emi Griffith
7ece52db1b Add own image build-push + per-service beta deploy (FE/BE CI-CD split)
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
2026-07-22 11:39:16 -07:00
Emi Griffith
8eb3ffbb6b Add own image build-push + per-service beta deploy (FE/BE CI-CD split)
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
2026-07-22 11:39:16 -07:00
Emi Griffith
0e3196fa05 Decouple Terraform from the app repo; add a GCP host scaffold
Content-change pass following the extraction from the app monorepo (this repo
now stands alone, sourced via git filter-repo to preserve history):

- terraform/variables.tf, secrets.tf, modules/thermograph-host: remove every
  app-secret Terraform variable (postgres_password, auth_secret, VAPID keys,
  registry_token, Discord/SMTP creds, ...) and the random_password/random_id
  generators. The SOPS+age vault (deploy/secrets/*.yaml) is now the sole
  source of app secrets, rendered at deploy time by deploy/render-secrets.sh;
  Terraform renders only a non-secret /etc/thermograph-topology.env (sizing,
  routing) via the renamed thermograph-topology.env.tftpl template.
- hosts gains a required app_image_tag field: the host's own checkout is now
  this infra repo, not the app repo, so there is no "current commit" to
  derive an image tag from — every host pins one explicitly. repo_url now
  points at this repo (private; typically needs an embedded read token).
- deploy.sh: IMAGE_TAG is now required from the environment instead of
  derived via `git rev-parse HEAD` of the (now infra-repo) checkout, which
  would have silently resolved to the wrong or a nonexistent tag.
- New terraform/modules/gcp-host: creates a GCE VM + minimal VPC/firewall
  only, then feeds its IP into the same thermograph-host module every
  SSH-managed host already uses — one provisioning path regardless of how a
  host came to exist. var.gcp_hosts defaults to {}, so no google_* resource
  is planned and the provider is never invoked without it (verified: plan
  and validate succeed with no GCP credentials configured).
- terraform/README.md, ACCESS.md (renamed from INFRA.md), README.md: updated
  for the new secrets model, the GCP scaffold, and this repo's own identity.

Verified: terraform fmt/validate/init clean; plan succeeds against realistic
dummy hosts (prod+beta shape) and against a populated gcp_hosts entry (plans
6 resources with no live credentials, confirming the composition wires
correctly end to end).
2026-07-21 21:46:05 -07:00
emi
792e2c5d3b Git-native secrets vault (SOPS + age) (#34) 2026-07-22 03:21:46 +00:00
9de12534a5 Add App Activity & Delivery dashboard (#2)
Co-authored-by: Emi Griffith <emerytgriffith@gmail.com>
Co-committed-by: Emi Griffith <emerytgriffith@gmail.com>
2026-07-22 03:20:20 +00:00
emi
c7503cf5a0 Route notifier output to Discord channels via the bot (#26) 2026-07-22 03:18:40 +00:00
147b1c51a5 Add Traffic & Domains dashboard (domain-grouped, visual) (#1)
Co-authored-by: Emi Griffith <emerytgriffith@gmail.com>
Co-committed-by: Emi Griffith <emerytgriffith@gmail.com>
2026-07-22 02:23:16 +00:00
emi
0d1ed5df47 Retry deploy.sh registry pull; document the registry /etc/hosts gotcha (#31) 2026-07-22 02:12:44 +00:00
emi
f7e6ce1a87 Fix deploy.sh self-modification and orphaned-container bugs (#27) 2026-07-22 00:15:33 +00:00
emi
a19e7f03be Simplify CI to build-only, matching thermograph-frontend
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.
2026-07-21 16:29:24 -07:00
Emi Griffith
5ed78f8fe9 Re-trigger CI with queue clear 2026-07-21 16:26:26 -07:00
emi
6e655722e4 Forgejo: OAuth-only login with approval-gated access + mesh mailer (#24) 2026-07-21 23:23:37 +00:00
emi
6b4a294db0 Fix CI boot check take 4: 30s timeout too short under CI load
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.
2026-07-21 16:22:34 -07:00
emi
eff0b1ee18 Fix CI boot check take 3: \$\$ isn't unique across job containers
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.
2026-07-21 16:18:13 -07:00
emi
b4690a0a5e Fix CI boot check take 2: bridge IP is unreachable from the job container
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.
2026-07-21 16:16:02 -07:00
emi
e4f28fa948 Fix CI boot check: don't publish a fixed host port
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.
2026-07-21 16:04:11 -07:00
Emi Griffith
ccc74f6f08 Re-trigger CI after clearing leaked Docker networks 2026-07-21 16:02:04 -07:00
Emi Griffith
66d8d3db24 Re-trigger CI after clearing leaked Docker networks 2026-07-21 16:01:50 -07:00
emi
f97df715d1 Standalone frontend: no-walk paths.py, own Dockerfile, own CI
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.
2026-07-21 15:59:29 -07:00
emi
8e9dfa1539 Standalone backend: no-walk paths.py, own Dockerfile, own CI
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.
2026-07-21 15:58:03 -07:00
emi
24749de0c7 Move static asset + SPA-shell ownership from backend to frontend (repo-split Stage 7a) (#22) 2026-07-21 22:48:59 +00:00
emi
d6056afabe Move static asset + SPA-shell ownership from backend to frontend (repo-split Stage 7a) (#22) 2026-07-21 22:48:59 +00:00
emi
452788fb18 Move static asset + SPA-shell ownership from backend to frontend (repo-split Stage 7a) (#22) 2026-07-21 22:48:59 +00:00
emi
20054ab198 Registry-pull cutover for prod/beta deploys (repo-split Stage 6) (#21) 2026-07-21 21:36:03 +00:00
emi
90f313cf56 Extract docs/ into thermograph-docs (repo-split Stage 6) (#20) 2026-07-21 21:29:14 +00:00
emi
c51c60bd94 Configurable API base + CORS for the frontend/backend split (Stage 5) (#18) 2026-07-21 20:52:11 +00:00
emi
897f413f3c Configurable API base + CORS for the frontend/backend split (Stage 5) (#18) 2026-07-21 20:52:11 +00:00
emi
fe14c7ab64 Forgejo: enable OAuth auto-registration so Google logins auto-link (#17) 2026-07-21 20:39:14 +00:00
dafed81f88 Grafana: match Google logins to pre-provisioned users by email
Grafana 9.3+ won't look up an OAuth login by email, so a Google sign-in for
the API-created admin (no prior Google-auth linkage) tried to auto-create the
user and hit allow_sign_up=false ("signup is disabled"). Enable
oauth_allow_insecure_email_lookup so the login matches the existing user by
its Google-verified email. Single provider + verified email = no takeover risk.
2026-07-21 13:24:37 -07:00
emi
6ad51e3597 Forgejo: auto-link Google SSO logins by verified email (#16) 2026-07-21 20:22:10 +00:00
emi
62a8c8c805 CI/CD: cache pip downloads, add concurrency guards, cut a redundant docker tag (#15) 2026-07-21 20:09:32 +00:00
emi
11872d7e64 Repo-split Stage 4: cut prod traffic to backend + frontend, dual-service (#14) 2026-07-21 20:01:30 +00:00
emi
f21aa2c2e0 Repo-split Stage 4: cut prod traffic to backend + frontend, dual-service (#14) 2026-07-21 20:01:30 +00:00