Station
One cockpit over a dozen backends, where an automation that goes quiet gets caught.
What this is
Station is my personal cockpit. One page, one login, and behind it the systems that run my week: the Life OS API, the Watchtower host monitor, the Claude usage and cost view, a project board, a mail triage lane, and the run ledger for every scheduled job I own.
The rule I set on 2026-06-14 and haven't broken: Station is an aggregator, not a monolith. It surfaces backends as panels. It does not absorb their data. When I was tempted to rebuild the Smokin' Oak numbers inside Station, I built a summary card that reads the existing slice and links out instead.
Why it's built this way
The alternative I rejected first was "one more dashboard." I already had two surfaces rendering the same Life OS entities, plus a Watchtower page, plus an Observatory page for cost. Three monitoring screens answering one question. Phase 1 was consolidation, and the nav went 11 items to 8 by merging views rather than rewriting them, so a rollback is one revert.
The second choice was harder. For job monitoring, pull or push? launchd and cron keep no history, so a pull-based check only knows what's running right now. A job that died three days ago is invisible. I went push: every job writes a running row at start and a terminal row on every exit path, and the registry declares an expected cadence so a dead job surfaces as overdue rather than as nothing at all. Silence had to be a detectable state.
That decision hardened into a written job contract in July: local-canonical files, one wrapper with a hard timeout and a lockfile, and a rule I keep quoting at myself... success means delivery, not exit 0. claude -p returning 0 is never by itself proof the artifact landed.
The build log, failures left in
2026-04-09
— v1.0. Astro 6 + React 19 + Supabase auth, links CRUD, four home widgets.
2026-04-17
— Kanban board over Life OS entities, with drag-and-drop.
2026-06-08
— The Supabase project auto-paused after about a week idle and took its API DNS with it. The app's getSession() had no .catch(), so it hung on a spinner forever instead of erroring. No data lost, 63 links intact, but I'd shipped a UI whose failure mode was "wait forever."
2026-06-16/20
— Watchtower and Observatory folded in as first-class views. Apps & Automations monitoring ships with the run ledger.
2026-07-01
— Reliability and security audit. Service-role API routes went JWT-gated, RLS owner-scoped, signup off.
2026-07-19
— Home rebuilt as a command deck: alerts strip, ranked queue with inline decisions. This reversed my own decision from 2026-07-04, which was that the home page should show counts and deep links only and never take actions. Two weeks of use proved that wrong. The reversal cost me a rule: one action, one component, one store, so a decision made on Home and the same decision made on the Mailroom tab can't drift apart.
2026-07-19 · SAME DAY
— My first real day driving it found four broken things, the worst being queue items that dead-ended on a page that didn't render them. Verify every button against what its target surface actually shows.
⚠ What would break
Tailscale. The Life OS and Watchtower APIs are reachable only over the tailnet, proxied to HTTPS. If that proxy stops, the panels degrade gracefully but half the cockpit goes blank, and the fix is on a box, not in the repo.
Drag-and-drop. dnd-kit works in some browsers and not others, a known issue since April. The card dropdown is the reliable path and the Icebox got tap-safe buttons, which is a workaround wearing a nice shirt.
The push ledger's honest gap. A job that never declares a cadence reads as unknown, not overdue. Deliberate, since a false red is worse than a blank, but it means a job I forget to declare is a job nobody watches.
What I learned
Two dashboards over the same data isn't redundancy you tolerate. It's redundancy that quietly makes both of them stale.
Where AI was confidently wrong: a previous session had written into the plan that the Life OS API "loses CORS on restart," with a plausible cause attached. Wrong. CORSMiddleware had never been committed to api.py in the first place. It got caught by reading the file instead of the note. The note was a story about the code, told by something that hadn't looked.
The other one is subtler. The semantic graph layout was supposed to produce clean domain clusters. It produced a dense core, because the work genuinely is interconnected. That caveat went in the changelog instead of the claim.
What this demonstrates
Observability as a build requirement rather than a retrofit. Durable run records, cost tagged with its billing source, heartbeats so that silence gets noticed. Aggregation over absorption. A written contract that new automations pass or don't ship.
The change in how I work is small and specific: I stopped asking "did it run?" and started asking "if this broke right now, how long until I noticed?" That question, asked early, put the ledger in before the pretty parts.