The per-site nginx ingress for rob.tn lives on oolon (the host the external router forwards 443 traffic to), not on nikola. Adjust the topology so: - web (static ui + nginx) → oolon.hanzalova.internal - api binds 0.0.0.0:42424 on nikola.kosherinata.internal so oolon can reverse-proxy across the WG mesh - new firewalld service moments-api opens 42424 in the default zone on nikola - oolon labels port 42424 http_port_t so httpd_t may name_connect outbound to it (httpd_can_network_connect was already set) - nginx ssl_certificate switched to oolon's host cert; upstream rewritten to nikola.kosherinata.internal:42424 Plaintext between oolon and nikola for now — the WG mesh provides the encryption layer and the data is already public. Documented the deferral so a future move to per-hop mTLS is obvious. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
moments
Personal activity timeline for rob.tn. Polls public sources (GitHub, Gitea, hg-edge.mozilla.org, bugzilla.mozilla.org), stores raw payloads in Postgres, and serves a reshaped timeline to a static React frontend.
Successor to the now-defunct grenade-events-react, which depended on MongoDB Stitch (retired by MongoDB in September 2022).
Layout
crates/
moments-entities/ # types and DTOs
moments-core/ # ingestion + reshape logic
moments-data/ # postgres adapter + migrations
moments-api/ # axum read-only HTTP API (binary)
moments-worker/ # ingestion daemon (binary)
ui/ # vite + react + swc + ts frontend
asset/ # systemd, nginx, firewalld, manifest.yml
script/deploy.sh
Architectural conventions follow grenade/architecture/generic.md.
Local development
cargo build --workspace
cargo run -p moments-api # serves on 127.0.0.1:8080
cargo run -p moments-worker # one-shot ingest tick (until --interval is wired up)
The API expects a Postgres reachable at DATABASE_URL. For magrathea, that's an mTLS connection using the host cert. For local dev against a throwaway database:
DATABASE_URL=postgres://localhost/moments cargo run -p moments-api
Migrations live in crates/moments-data/migrations/ and run automatically on worker startup. The API connects as moments_ro and never runs migrations — the worker (as moments_rw) is the schema owner.
Deployment
./script/deploy.sh prod all # api + worker + web
./script/deploy.sh prod api worker # subset
./script/deploy.sh prod default # api + web only (worker untouched)
./script/deploy.sh prod all --dry-run
Topology:
| Component | Host | Notes |
|---|---|---|
| api | nikola.kosherinata.internal |
binds 0.0.0.0:42424; firewalld service moments-api |
| worker | frootmig.kosherinata.internal |
no listening port; pollers only |
| web | oolon.hanzalova.internal |
per-site nginx ingress for rob.tn; /api/* → nikola across the WG |
| db | magrathea.kosherinata.internal |
postgres mTLS, passwordless |
Postgres roles moments_rw and moments_ro must exist on the primary, with pg_ident.conf mappings in place for nikola.kosherinata.internal → moments_ro and frootmig.kosherinata.internal → moments_rw. See asset/sql/bootstrap-moments.sql and asset/postgres/ident.conf.tmpl.
Inter-host traffic over the WG mesh: oolon's nginx connects to http://nikola.kosherinata.internal:42424 in plaintext. The mesh provides the encryption layer; per-hop TLS for an internal HTTP read-only API on already-public data is deferred. If that changes, swap the api binary to rustls + the host cert pair, and update the nginx upstream to https://.
Secrets resolved by deploy.sh via pass:
github.com/grenade/admin-token— GitHub PAT for events + search APIs (worker only).
Optional, set if needed in worker.env: GITEA_TOKEN, BUGZILLA_API_KEY.
DNS cutover
rob.tn currently resolves to GitHub Pages. After the first successful prod deploy:
- Update Cloudflare DNS for
rob.tnto the WAN IP that frontsnikola(unproxied — see architecture doc §11). - Confirm
curl -fsS https://rob.tn/api/v1/healthzreturnsok. - Add an archival notice to the top of grenade-events-react/readme.md pointing at this repo, and archive the GitHub repo.