Base Admin Hardening — Operations Surface
Date: 2026-08-09 Status: Design approved, pending implementation plan Scope: Harden the stock Django admin only. No Django Unfold, no new JS/CDN dependency, no new endpoints. An opt-in admin package layer (e.g. Unfold) is explicitly deferred to a separate future effort.
Problem
The cluster pipeline works, but the admin is not a usable operations surface:
- The inbox (durable PENDING/PROCESSING pipeline runs) cannot be monitored — its state is only visible via the CLI drain.
- Alert history is too complex and fragmented; there is no single place to trace an incident’s full journey.
- Preflight results are invisible unless you run the CLI — they are not persisted, so nothing survives the terminal session.
- Pipelines cannot be visualized by the axes that matter operationally: which server (hub vs agent), how the run started (incoming push vs checker-generated), and status. Pipeline history reads as undifferentiated noise with no proper relationships.
- Trends (e.g. disk filling up) cannot be seen against alert history.
Non-goals
- No Django Unfold or any third-party admin package in this pass.
- No hub-push of preflight data (kept strictly node-local; a future concern).
- No new HTTP endpoints, auth schemes, or registries — admin-only changes that reuse existing mechanisms (
process_inbox, the Node registry,CheckRun).
Key modeling decision — two orthogonal axes
“Node” was conflating two different questions. They are separated here:
| Axis | Question | Agent alert | Hub self-check |
|---|---|---|---|
| Subject (node) | Which server is this about? | the agent Node | the hub itself |
| Origin | How did this pipeline start? | incoming_webhook | checker_generated |
Server-checkers vs. node-alerts is a difference on the origin axis, not a conflict on the node axis. Because agents run their own checkers and push results to the hub, on the hub those arrive as incoming_webhook; the only checker_generated pipelines on the hub are the hub monitoring itself.
To keep the node axis uniform, the hub is materialized as a self-node. The project is already a peer-node model and the hub already has an identity (settings.INSTANCE_ID); this simply gives it a Node row like any peer.
Components
1. Hub self-node — apps/alerts
- Add
is_selfboolean toNode(defaultFalse). Node.ensure_self()classmethod +bootstrap_self_nodemanagement command that upserts aNodefromsettings.INSTANCE_ID(hostname auto-filled). Idempotent; also invoked lazily when achecker_generatedpipeline is created.- Admin:
is_selfbadge, hostname,last_seen. The Node page becomes the hub for “this server.”
2. Pipeline node + origin — apps/orchestration
- Add to
PipelineRun:nodeFK →alerts.Node,null=True,on_delete=SET_NULL.originenum:incoming_webhook/checker_generated/manual.
- Populate at creation:
- incoming webhook → node resolved from instance_id/incident,
origin=incoming_webhook; --checks-only→ node = self-node,origin=checker_generated;- CLI / manual →
origin=manual.
- incoming webhook → node resolved from instance_id/incident,
- Backfill migration: infer
originfromsource; copynodefromincident.nodewhere an incident is present. - Admin:
list_filteron node, origin, status, current_stage; clear columns — this delivers the “group by hub/agent + incoming vs checker-generated + status” requirement.
3. Inbox monitor — apps/orchestration
InboxItemproxy model overPipelineRun, registered in admin, filtered to PENDING/PROCESSING, oldest-first.- Columns: run_id, source, node, origin, status, age, stuck? (PROCESSING past the reclaim cutoff).
- Admin actions: Drain selected now and Reclaim stuck, reusing
process_inboxinternals (no logic duplication).
4. Preflight persistence — apps/checkers
- New node-local models:
PreflightRun: created_at, node/instance_id, passed/warn/error counts, overall_status, triggered_by.PreflightCheck: FK → PreflightRun, name, level, message, hint.
- The
preflightcommand persists aPreflightRun+ child checks on every run by default, with a--no-saveopt-out (CI / ad-hoc). A simple retention/pruning note is included to bound DB growth. No hub-push. - Admin: history list with pass/warn/error counts and overall status,
date_hierarchy, readonly inline of checks.
5. Incident timeline (alert-history cleanup) — apps/alerts
- On the
Incidentadmin page, a readonly merged timeline combiningAlertHistoryevents +StageExecutions + notification refs in chronological order, withtrace_id/run_idshown once. This realises the pipeline contract: from a notification, jump back to the full journey. - Simplify
AlertHistoryAdmindefaults: human event labels, collapse rawdetailsJSON, better filters. History remains readonly (audit records).
6. Inline SVG sparklines — shared admin util + apps/checkers
render_sparkline(points, markers=…) -> SafeString: self-contained inline<svg>, no JS, no external assets (honours the self-contained/CSP rule).- Rendered on Node and Incident pages: disk % (and other
CheckRunmetrics) over time, with alert-firing markers overlaid. Data sourced fromCheckRunmetric history for that node/metric.
7. Navigation / “missing models”
- Finding: every model is already registered — the real gap is relationships/navigation. Addressed by the above: the Node page gains inlines for recent pipelines, recent alerts, latest preflight, and the disk sparkline; plus cross-links between
PipelineRun↔Node↔Incident.
Data flow
webhook ingest → PipelineRun(node=<agent>, origin=incoming_webhook, PENDING)
→ InboxItem view shows it until process_inbox drains it
checks-only run → PipelineRun(node=<self-node>, origin=checker_generated)
preflight run → PreflightRun (+ PreflightCheck children), node-local
CheckRun metrics ─┐
AlertHistory ─────┼→ Incident page: merged timeline + disk sparkline
StageExecution ───┘
Error handling & edge cases
- Self-node bootstrap must be idempotent and safe when
INSTANCE_IDis unset (skip/no-op with a clear message rather than creating a blank node). nodeFK is nullable andSET_NULLso node deletion never orphans/deletes pipeline history.- Backfill migration must be reversible and tolerate rows with no incident and ambiguous
source(fall back to a sensible origin default). - Inbox “Drain now” / “Reclaim stuck” must reuse the atomic claim in
process_inboxso admin-triggered drains never double-process. - Sparkline renderer must handle empty/short series and never emit external references.
- Preflight persistence must not break
--json/ CI usage;--no-savepreserves the old print-only behaviour.
Testing
- 100% branch coverage on all changed code (
uv run coverage run -m pytest && uv run coverage report). - Self-node: bootstrap idempotency, missing
INSTANCE_ID, lazy creation. - PipelineRun node/origin: creation paths (webhook / checks-only / CLI) and the backfill migration (with/without incident, ambiguous source).
- InboxItem: filtering, age/stuck computation, drain + reclaim actions.
- Preflight models + command: default persist,
--no-save,--json, counts/overall_status correctness. - Incident timeline: chronological merge across the three sources, readonly.
- Sparkline: empty series, single point, marker overlay, no external refs.
Acceptance criteria
- Admin can monitor the inbox (pending/processing/stuck) and drain/reclaim from the UI.
- Pipelines are filterable/groupable by node (hub self-node + agents), origin, and status.
- Preflight results are persisted by default and browsable with history.
- Incident page shows a single readable merged timeline.
- Node/Incident pages show disk-usage sparklines with alert markers (inline SVG, no JS/CDN).
- No Unfold, no new endpoints; base admin only; CI green; coverage 100% on changed lines.