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.
This commit is contained in:
170
docs/proposals/dashboard-live-fleet.md
Normal file
170
docs/proposals/dashboard-live-fleet.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user