personal activity timeline and portfolio site. polls public sources (github, gitea, mercurial, bugzilla), stores raw payloads in postgres, and serves a dashboard + project detail views to a react frontend.
successor to the now-defunct grenade-events-react, which depended on mongodb stitch (retired by mongodb in september 2022).
crates/
moments-entities/ # types and dtos (event, source, project/daily summaries)
moments-core/ # ingestion traits, presentation reshape, poller loop
moments-data/ # postgres adapter, migrations, all event-source impls
moments-api/ # axum read-only http api + forge proxy + og image (binary)
moments-worker/ # ingestion daemon (binary)
ui/ # vite + react + swc + typescript frontend
asset/ # systemd, nginx, firewalld, manifest.yml
script/
deploy.sh # manifest-driven deploy to prod
hg-ingest.sh # one-shot local hg clone + psql ingest
certify.sh # letsencrypt cert management
teardown.sh # service removal
db-perms.sh # postgres role + ident setup
architectural conventions follow grenade/architecture/generic.md.
| source | impl | endpoint | notes |
|---|---|---|---|
| github events | github.rs | /users/{user}/events | last 90 days, etag-optimised polling |
| github search | github_search.rs | /search/commits + /search/issues | historical backfill, 1000-result cap |
| github repo | github_repo.rs | /user/repos + /repos/{o}/{r}/commits | full commit history, no cap, weekly poll |
| gitea | gitea.rs | user + org activity feeds | auto-discovers orgs, filters by user |
| mercurial | hg.rs | json-log?rev=author() | revset-based, one-shot backfill then skip |
| bugzilla | bugzilla.rs | /rest/bug?creator= | mozilla bugzilla |
hg repos are archived (mozilla retired hg). the worker skips hg after the first successful scan. for bulk ingestion, script/hg-ingest.sh clones repos locally and inserts via psql, avoiding rate limits on hg-edge.mozilla.org.
| path | page | description |
|---|---|---|
/ or /dash | dashboard | contribution graphs (daily + all-time weekly) + ranked project cards with forge icons and language info |
/activity | timeline | filterable activity feed with source toggles, date range slider, and event limit |
/activity/:timespan | timeline | pre-filtered by date (YYYY-MM-DD) or range (YYYY-MM-DD..YYYY-MM-DD) |
/project/:source/* | project detail | repo readme, language breakdown bar, per-repo activity timeline |
/cv | resume | loaded from github gist, markdown-rendered |
shared layout provides nav header (dash, activity, cv + external links) and footer across all routes.
| method | path | description |
|---|---|---|
| GET | /v1/healthz | liveness probe |
| GET | /v1/events?from=&to=&source=&repo=&limit= | reshaped timeline items |
| GET | /v1/sources | per-source summary (count, earliest, latest) |
| GET | /v1/projects | per-repo aggregated stats (commits, issues, prs, date range) |
| GET | /v1/activity/daily?from=&to= | per-day event counts for contribution graphs |
| GET | /v1/forge/{source}/*?host= | proxy to github/gitea apis (avoids cors) |
| GET | /v1/og/contributions.png | server-rendered contribution graph as png (resvg) |
the og image endpoint renders the all-time weekly contribution graph as svg, rasterizes to png via resvg, and serves it with a 1-hour cache. used as the og:image meta tag for social media previews.
cargo build --workspace
cargo run -p moments-api # serves on 127.0.0.1:8080
cargo run -p moments-worker # starts all pollers
cd ui && npm install && npm run dev # vite dev server on :5173
the api expects a postgres reachable at DATABASE_URL. in production this is 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.
./script/deploy.sh <env> all # api + worker + web
./script/deploy.sh <env> api worker # subset
./script/deploy.sh <env> default # api + web only (worker untouched)
./script/deploy.sh <env> all --dry-run
concrete hosts, ports, and the site's server_name live in asset/manifest.yml. the shape of the deployment:
| component | notes |
|---|---|
| api | binds the port from api.config.bind; firewalld service moments-api |
| worker | no listening port; pollers only |
| web | per-site nginx ingress; /api/* reverse-proxies to the api host |
| db | postgres mtls, passwordless |
postgres roles moments_rw and moments_ro must exist on the primary, with pg_ident.conf.d/<host>.conf mapping the api host's fqdn to moments_ro and the worker host's fqdn to moments_rw. see asset/sql/bootstrap-moments.sql, asset/postgres/ident.conf.tmpl, and script/db-perms.sh.
secrets are resolved at deploy time via pass. the mapping of env-var name to pass-store path lives under worker.secrets in manifest.yml; deploy.sh iterates the map, fetches each secret, and substitutes the matching {{NAME}} placeholder in worker.env.tmpl.
| variable | default | description |
|---|---|---|
DATABASE_URL | required | postgres connection string |
GITHUB_USER | grenade | github username |
GITHUB_TOKEN | optional | github pat for higher rate limits + private events |
POLL_INTERVAL_SECS | 600 | github events api poll interval |
SEARCH_POLL_INTERVAL_SECS | 86400 | github search backfill interval |
REPO_POLL_INTERVAL_SECS | 604800 | github per-repo commit enumeration (weekly) |
GITEA_HOST | git.lair.cafe | gitea instance hostname |
GITEA_USER | grenade | gitea username |
GITEA_TOKEN | optional | gitea token for org discovery |
GITEA_POLL_INTERVAL_SECS | 600 | gitea activity feed poll interval |
HG_HOST | hg-edge.mozilla.org | mercurial host |
HG_GROUPS | build,integration | hg repo groups to discover |
HG_REPOS | mozilla-central | individual hg repos |
HG_AUTHOR_TERMS | rthijssen,grenade | author substrings for revset queries |
HG_POLL_INTERVAL_SECS | 86400 | hg poll interval (skips after first scan) |
BUGZILLA_HOST | bugzilla.mozilla.org | bugzilla instance |
BUGZILLA_EMAIL | [email protected] | bugzilla creator email filter |
BUGZILLA_POLL_INTERVAL_SECS | 86400 | bugzilla poll interval |
| variable | default | description |
|---|---|---|
DATABASE_URL | required | postgres connection string (read-only role) |
BIND_ADDR | 127.0.0.1:8080 | api listen address |
59 activities
ba96cdd ci: install web deps with --ignore-scripts + explicit rebuild8e11d02 ci: move pnpm build-script allowlist to pnpm-workspace.yaml163a36f ci: allow esbuild/@swc native build scripts under pnpm 10ed5acd9 ci: split build across rust + fedora runners, deploy on fedora-44a7750de ci: install pnpm via npm (gongfoo rust image lacks corepack)3761333 fix: make the workspace pass the CI lint/test gate1b753f9 feat: prerender every route + Gitea Actions deploy70b4b26 style(blog): scale down in-content post headings908ab33 style(blog): smaller list headers, readable date color37c4490 feat(blog): prune posts removed or renamed upstreamcd3dc2d fix(ui): add @types/node for process.env in vite.config88ce993 feat(blog): add markdown blog sourced from a gitea repo2821548 feat(ui): add avg-by-hour panel to dashboard stats72eeb54 chore(deploy): self-heal /tmp perms before staging86411bb fix(worker): dedup gitea events from overlapping user and org feedsacb061b chore(deploy): build rust binaries in a podman container8a7177a feat(ui): render GFM and embedded HTML in project READMEs818a535 feat(worker): capture commits on non-default branches and forks9a8c095 chore: phrasing25eab2d feat: add robots.txt allowing all crawlers including social bots2130032 chore: update Cargo.lock for fontdb dependency92a6642 feat(ui): add meta description, og:locale, and og:site_name94b6fbe feat(ui): add og:logo meta tag pointing to 512px icon048646a feat(ui): add og:url meta tag for canonical URL1f2fea3 fix: load system fonts for OG image text rendering283b212 feat(ui): color contribution graph circles by dominant languagee8dcb5f feat(ui): show private activity count on timeline when no public eventsb41e8c3 feat: include private repo contributions in graph metricsf386e0b feat(ui): reshape all-time graph and add dashboard stats panels111a2af feat(ui): language distribution bar on project cards6f30a61 feat(ui): smooth language stream graph with Catmull-Rom splines1464327 fix: weight language graph by repo language proportionsee93429 feat: language stream graph on dashboardc66aaeb feat: discover contributed repos via GitHub GraphQL API2a20b47 fix: resolve clippy redundant_closure warning in moments-apif77a8ab fix: use since cursor in github-repo polls to prevent missed commits1679153 docs: add CLAUDE.md and ignore .zed/0aa53d3 docs: rewrite readme to reflect current architecturecd833b1 fix(ui): demote repos with >= 10k commits to end of dashboard293d112 fix: fall back to _repo in commit reshape for github-repo eventsef1e84a feat(ui): link forge icon to repo on project pagef8c13b5 fix: icon colors for dark backgroundsabc90c8 feat(ui): forge icon on project page headerd46a0e3 fix: add _repo fallback to events repo filter for github-repo commits2284a88 fix(ui): all-time graph as year rows with 52 weekly columns each1ca85fe feat(ui): all-time weekly contribution graph + date range timespan support822def3 fix(ui): scale contribution graph to full container width27ce16e feat(ui): contribution graph with daily activity heatmap7de2330 chore(ui): add favicon set to index.html7a4939c chore(ui): add favicon set to index.html0d350ce fix: decode base64 readme content as utf-8 instead of latin-11275a77 chore: update Cargo.lock for reqwest in moments-api6b9ce99 fix: proxy forge API requests to avoid CORS, case-insensitive readmef676ecd fix: try multiple readme filename casings for Gitea reposba21658 feat(ui): project readme, language bars, and per-card language summary80f3f7c feat(ui): project drill-down route with repo-filtered event timelinea70fab4 feat(ui): add /dash route, shared nav, project dashboard with /v1/projects APIa71b4e6 feat(github): per-repo commit enumeration for full history backfill2da9461 fix(hg): show clone errors, stable cwd; shrink timeline fonts3f3a1fb fix: connection string88fbbba feat(hg): revset-based author query, group discovery, one-shot ingest script1bbe55d feat(gitea): poll org activity feeds to capture cross-namespace events4c8a663 feat(ui): add /cv route, site-wide lowercase, no-cookies footer8867ff5 feat(deploy): manifest-driven config, teardown + db-perms, hardeningf30f949 fix: ensure root ownership when syncing staged folders7843c2c chore(deploy): co-locate api + worker on anjiec81512f fix: conventional paths, oolon fqdn, public certabce380 chore(deploy): strip infra commentary from asset/ config files52b7d0b fix(deploy): split ingress to oolon, expose api on nikola interface110b523 chore(deploy): add manifest, systemd units, nginx config, deploy.sh7919a2d feat(worker): add hg-edge and bugzilla pollersf750e8d feat(worker): add gitea activity feed poller4355353 fix(presentation): handle force-push, branch-create, empty pushesbf04f8a fix(api): log internal handler errorsbf7f829 fix(api): don't run migrations as moments_rob04afd8 feat(ui): scaffold vite + react 19 frontend7772393 feat(worker): add commits to github search backfille4052c4 feat(worker): add github search api source for historical backfill3c02535 feat: ingest private events; surface public-only003f427 feat(api): reshape raw events into TimelineItem418834c docs(asset/sql): document mtls and ssh-sudo run modes45ceec2 feat(worker): add github events pollere40d6b0 chore(asset): add postgres bootstrap and pg_ident template