Wave one of the dashboard proposal, built as a vertical slice so the contract is proven by something real before the other five cards follow. GET /api/geenforce/dashboard/failures lists entries that FAILED on their PC's most recent enforcement cycle. Per ENTRY, not per report: "three PCs failed" is a number, while "Install OpenText failed with exit 1603 on WJSF1234" is something a person can act on. Only current reports count, so a failure that has since been fixed clears itself instead of needing dismissing. Hostnames resolve to computerids in one query so each row links to the PC, and a PC shopdb does not know still appears - the failure is real even when the inventory is behind, and that is the bay most likely to be misconfigured. The data has been there all along. The only way to see any of it was to open one PC's report modal, one PC at a time. The widget declaration is the contract change. The old shape named a Vue component per widget, which cannot survive a lean build where a plugin's component may never be staged into the bundle - which is exactly why five plugins declare widgets pointing at components nobody ever wrote. This declares data, a generic renderer, a permission and a link template, the way ADR-010 already does for asset panels. A test asserts no 'component' key, so the old shape cannot creep back. empty: hide is part of the contract, not decoration. A card reporting "nothing wrong" daily teaches people to stop reading the page, which is how a fleet log reached 3,234 lines with 17 that mattered. Frontend rendering comes next; the endpoint and declaration stand alone and change nothing that exists.
171 lines
7.3 KiB
Markdown
171 lines
7.3 KiB
Markdown
# Proposal: a dashboard that shows the fleet, not the row count
|
|
|
|
- Status: ACCEPTED
|
|
- 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.
|