Ops
A read-mostly dashboard for a portfolio of side projects: what is it costing, what needs attention, ranked by score and deep-linked. Workers + D1.
Outcomes
- Nine pollers on cron (GitHub, uptime, Anthropic usage, Claude Code, OpenAI, X, Cloudflare, plugin manifests, and an advisory judge) plus a bearer-authenticated ingest endpoint any CI can POST to
- Read here, act there: Ops never writes to GitHub or anywhere else; the only write-shaped affordance in the UI is a pre-filled new-issue link
- Append-only, deduplicated signals with namespaced metrics: a brand-new domain lands on the findings feed with zero code changes
- Runs inside the Cloudflare free tier, with the D1 row-read and row-write allowances measured, budgeted, and watched by the dashboard itself
Stack
Context
Keep more than a handful of side projects alive and the state you care about is smeared across GitHub Actions, Dependabot, Lighthouse, uptime checks, and half a dozen billing consoles. None of them know about each other. Ops pulls those signals into one table on Cloudflare Workers and D1, scores them, and sorts by what is actually worth the next hour. This site reports its own Lighthouse scores into it from CI.
The Problem
Two questions, fifteen browser tabs: what is this costing me, and what needs attention? A dashboard that answers them is easy to start and easy to ruin: the moment it becomes a second system of record, it drifts from the first one and you have two things to keep true.
My Role
Designer and builder. The GTM-systems habit (one lens over many systems, with
the score explained in a why column rather than hidden in a weighting),
applied to my own portfolio.
Approach
- Read-mostly by constitution. Ops is a lens over other systems, never another system of record. Every row deep-links to GitHub, the vendor console, or the workflow run that produced it. That rule is ADR-001 and the UI enforces it.
- Signals are append-only and idempotent. A signal is a timestamped observation about an entity, deduplicated on entity, metric, and a caller supplied key, so a re-run CI job is safe and history stays queryable. Budget breaches, anomalies, hygiene gaps, and scores are derived on each pass, not stored as separate truth.
- Namespaced metrics are the extension point. Push
seo.crawl_errorsfrom any pipeline and it appears on the findings feed, filterable by domain, with no code change. The ingest endpoint is the only route exempt from the same-origin gate, and it answers 503 rather than 401 when its token is unset, so a misconfigured deploy fails loudly. - Failure is a signal, not a crash. A poller whose upstream is down records the failure and degrades its page; a poller whose credential is absent reports itself as unconfigured at low severity, so you can start with one token and add vendors later without the dashboard crying wolf. Each poller run is itself a signal on a synthetic entity: the thing that watches the things is visible in the same table.
- The judge is advisory. Open issues with no severity label are tiered by a judgment model at severity zero (visible, never scored), and the model is graded on a blind sample against the labels a human actually applied.
- The free tier is a budget, not a hope. D1 bills every row a query touches. The daily read and write allowances are measured, watched, and surfaced by the dashboard, and the cron stays alive when a budget trips.
The discipline running through this approach is the site’s thesis: Decisions Are the Interface Now.
What I’d Do Differently
Treat D1 row reads as a budget from the first migration. The early hot paths (the retention sweep and the health page) walked whole tables once a day and per request, which is invisible until the account-wide daily read allowance trips. Two later migrations bounded them with partial indexes and a latest-pointer table, and ADR-007 turned the write side into a measured budget the dashboard watches. Both should have been the starting constraint, the same way the JavaScript budget is on this site.