From 02d7619b1a74d38c468225b9b8fa4f87d60052fe Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 11 Aug 2026 12:50:16 -0400 Subject: [PATCH] Propose a dashboard that shows the fleet, not the row count The dashboard reports totals that are true every day and actionable on none of them. Meanwhile a bay returned 500 to every collector report for a day and a half, eleven markers' backups overwrote each other for weeks, and GE-Enforce records failures, stale manifests and repeated self-heals that surface nowhere. Every one of those was found by someone going to look. Two things found while writing this shaped it. The plugin mechanism already exists - get_dashboard_widgets, an endpoint that aggregates across enabled plugins and isolates failures, and five plugins declaring widgets. And nothing consumes it: the frontend never calls the endpoint and not one of the declared components was ever written. So this finishes a half-built feature rather than starting one. It also proposes fixing the contract while it is still unused. Naming a component per widget does not survive a lean build, where a plugin's component may not be staged into the bundle at all. ADR-010 already solved this for asset panels - declare data and a render mode, let a generic core component draw it - and the same three renderers cover every card listed. The rest is principles that came out of this week: empty cards shrink, because a card that says "nothing wrong" daily trains people to stop reading, which is how a log reached 3,234 lines with 17 that mattered; every row links to the thing; cards declare a permission so the dashboard cannot become a way around RBAC; thresholds are settings with neutral defaults. Wave one is six cards whose data is already reliable. Wave two is the valuable one: diffing what a manifest says a PC should have against what it reports having, which nothing answers today and which is what GE-Enforce exists to guarantee. --- docs/proposals/dashboard-live-fleet.md | 170 +++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 docs/proposals/dashboard-live-fleet.md diff --git a/docs/proposals/dashboard-live-fleet.md b/docs/proposals/dashboard-live-fleet.md new file mode 100644 index 0000000..fd3f2a9 --- /dev/null +++ b/docs/proposals/dashboard-live-fleet.md @@ -0,0 +1,170 @@ +# Proposal: a dashboard that shows the fleet, not the row count + +- Status: DRAFT +- Date: 2026-08-11 +- Author: cproudlock +- Relates to: ADR-010 (frontend plugin hooks), ADR-013 / ADR-014 (lean per-site builds), ADR-006 (collector contract), ADR-012 (GE-Enforce manifest ownership) + +## The problem + +The dashboard reports totals: assets, machines, PCs, network devices, printers, +measuring tools. Those numbers are true every day and actionable on none of +them. Nobody opens the page to learn there are 417 assets. + +Meanwhile the system already knows things worth acting on and shows them +nowhere: + +- a bay returned 500 to every collector report for a day and a half, and the + only evidence was a log file on the PC +- eleven part markers' backups were overwriting each other for weeks +- a PC can stop reporting entirely and nothing says so +- GE-Enforce records enforcement failures, stale manifests and repeated + self-heals, and none of it surfaces outside a report modal + +Every one of those was found by someone going to look. The dashboard should be +where they find you. + +## What already exists + +Two discoveries shaped this proposal. + +**The plugin mechanism is built and dead.** `BasePlugin.get_dashboard_widgets` +exists, `GET /api/dashboard/widgets` aggregates across enabled plugins, skips +disabled ones, isolates a broken plugin in production and sorts by position. +Five plugins - notifications, network, machines, computers, printers - already +declare widgets. + +**Nothing consumes it.** The frontend never calls the endpoint, and not one of +the declared components (`PrinterStatusWidget`, `ComputerStatusWidget`, +`MachineStatusWidget`, `NotificationsWidget`, `NetworkStatusWidget`) exists. +The declarations point at components that were never written. + +So this is less "build a dashboard" than "finish one", plus a contract fix. + +**The data is already collected.** Nothing here needs a new collector field: + +| Signal | Source | +|---|---| +| PC reporting / silent | `computers.lastreporteddate` | +| Machine has a controlling PC | `controls` links, `collector:machine` | +| Backed up, stale, never | `backuprevisions` per asset and kind | +| Enforcement failed | `manifestenforcementreports.status`, `failedcount` | +| Which entry failed, and why | `manifestenforcementresults.action`, `exitcode`, `message` | +| Stale manifest | report `appliedversion` versus the scope's published version | +| Drift that will not stick | repeated `selfhealed` results for one entry | +| Enforcer version spread | `manifestenforcementreports.enforcerversion` | +| Installed software and versions | `computerinstalledapps` | +| Toner critical / low | printer supplies, Zabbix-fed | +| Warranty expiring | warranty plugin | +| Mis-numbered bays | the `check-shared-machines` query | + +## Proposed change to the widget contract + +The existing contract names a **component per widget**. That does not scale and +does not survive a lean build: every widget needs a bespoke Vue component, and a +plugin-provided component has to be staged into the frontend bundle to exist at +all. + +ADR-010 already solved this for asset panels: a plugin declares `render: 'list'` +or `'keyvalue'` or `'tabs'` plus a `map` of fields, and a GENERIC core component +renders it. Do the same here. A plugin declares data and shape; core owns the +rendering. + +```python +{ + 'id': 'geenforce-failures', + 'title': 'Enforcement failures', + 'endpoint': '/api/geenforce/dashboard/failures', + 'render': 'exceptions', # generic renderer + 'severity': 'critical', # critical | warning | info + 'permission': 'geenforce.view', + 'position': 10, + 'empty': 'hide', # hide | line + 'map': { + 'title': 'hostname', + 'detail': 'entryname', + 'meta': [{'key': 'message'}, {'key': 'exitcode'}], + 'link': '/pcs/{computerid}', + }, +} +``` + +Three renderers cover everything listed above: + +- `exceptions` - a list of things that are wrong, each linking to the thing +- `metric` - a single number with a trend or threshold, for the cases where the + count IS the story (toner critical: 3) +- `list` - recent items, for notifications and KB adds + +## Principles + +**An exception board, not a stat board.** The default view is what needs a +person today. A count appears only where the count is the story. + +**Empty cards must shrink.** A card that says "nothing wrong" every day trains +people to stop reading the page. That is exactly how `ntlars-backup.log` reached +3,234 lines with 17 that mattered. `empty: 'hide'` is the default; `'line'` for +the few where absence is itself news. + +**Every row links to the thing.** A dashboard that says three printers are low +without linking to them is a worse version of a report. + +**Cards respect RBAC.** A card declares a `permission`; a user without it never +sees the card or its endpoint. Toner levels are not sensitive, but "which PCs +expose VNC" is, and the dashboard must not become a way around role gating. + +**Thresholds are settings with neutral defaults** (ADR-015): quiet window, toner +percentage, warranty horizon. Every site will disagree with the numbers. + +**One endpoint per card, lazily loaded, failing independently.** A hung Zabbix +call must not blank the page. + +## Wave one + +Cards whose data is already reliable, ordered by how actionable they are: + +1. **Enforcement failures** - current reports with `status = failed`, showing + the failing entry and exit code. +2. **PCs not reporting** - `lastreporteddate` beyond the quiet window, and PCs + with no GE-Enforce report at all, which is a different and worse case. +3. **Backups missing or stale** - machines with no revision of a kind their + PC type should produce, and revisions older than the interval. +4. **Toner critical, then low** - links to the printer, then to its admin page. +5. **Warranties expiring** - within the configured horizon. +6. **Mis-numbered bays** - `check-shared-machines`, promoted from a CLI command + nobody will remember to run. + +Then, lower and quieter: stale manifest versions, repeated self-heals, enforcer +version spread, active notifications, recent application and KB additions. + +## Wave two: desired versus observed + +The highest-value card and the most work. The manifest declares what a PC type +should have; `computerinstalledapps` records what it does; `filters.py` already +decides which entries apply to a given PC. Diffing those answers "which bays are +missing something they should have" - which nothing in the system answers today, +and which is the thing GE-Enforce exists to guarantee. + +Deferred to wave two because it needs the resolver wired into a query path, and +because wave one needs nothing new. + +## Risks + +**Aggregate queries on the most-visited page.** These are fleet-wide scans on +every load. Cache 30-60 seconds, and log slow cards - a lazily-loaded card can +be slow for months before anyone mentions it. + +**Cards become the new noise.** Six cards that are always empty are as useless +as one number that never changes. If a card is empty for a month, delete it. + +**Plugin gating must be real.** A lean site without `printers` must render no +toner card. The existing endpoint already skips disabled plugins; the frontend +must not hardcode a card list alongside it. + +## Non-goals + +- Per-user dashboard customisation. Later, if asked for. +- Historical charting. This is a "what needs doing now" board; trends belong in + reports. +- Replacing the shopfloor TV dashboard, which is a different audience with + different needs.