main was 16 days and 259 commits behind the line the product is actually built on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It was neither current nor curated - just stale. This brings it forward and, at the same time, makes it mean something: main is what a receiving site installs, and nothing else. WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides, knowledgebase, machines, network, notifications, printers, slides, tools, warranty - plus applications, which is manifest-less core and always ships. The only manifest dependency inside that set is endpoints on computers, and both are in it, so the closure is self-contained. WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb stay on the site line. So do the Windows installer tree, the VM rehearsal helpers, the agent tooling config, one site's data loader, the internal audit and findings records, and the planner spreadsheet. 1,135 tracked files against 1,438. A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped: deploy/site-profile-universal.json still named all seventeen plugins. It is the file docs/DEPLOY.md tells a site to provision from, so a fresh install would have failed at `flask plugin apply-profile` with PluginError - the first thing a receiving site does, broken by the branch meant to serve them. It now names the twelve that exist, and its comment says the two must agree. agent/ was never refreshed. Whoever populated this branch copied shopdb/, plugins/, frontend/ and tests/ from the site line and left the agent tree at its September state: 22 C# and PowerShell sources missing, including AgentCredentials, EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the site line had deleted still present. The old tree is internally consistent, so it compiles, and nothing in CI builds it - every gate was green over a server that speaks enrollment and per-PC credentials to a bundled agent source that cannot. Resynced whole. DECISIONS THIS ENCODES, rather than defers: - The plugin-authoring lab builds printedparts, whose manifest depends on employees, so it could not stay. The lab remains on the site line and main ships no build-along tutorial. That is a real loss for an adopting developer and is recorded here rather than discovered later. - Recognition, Training and Recertification notifications are no longer seeded. Each splits one notification into a card per employee and renders a photo, which needs a plugin this branch does not bundle. Seeding them would offer an author a type that cannot resolve anybody. A site that installs employees gets them from that plugin's own seed. - Windows/IIS is documented as the legacy path throughout, and the README leads with Docker, because that is what a receiving site gets. The API description is regenerated for THIS branch - 347 paths and 483 operations, down from 415 and 573 - so the published contract describes what is here rather than what the site line serves. docs/CONFIG.md lost the two measuring-tool label keys the code no longer declares, which two lanes had decided opposite ways. Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run rather than skip, and every index line count is derived from the file it names.
7.6 KiB
Proposal: a dashboard that shows the fleet, not the row count
- Status: IMPLEMENTED (a proposal, not a decision record - the contract it introduced is ADR-010 and contract 0.19.0)
- Date: 2026-08-11
- Author: ShopDB maintainers
- 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.
{
'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 thingmetric- 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:
- Enforcement failures - current reports with
status = failed, showing the failing entry and exit code. - PCs not reporting -
lastreporteddatebeyond the quiet window, and PCs with no GE-Enforce report at all, which is a different and worse case. - Backups missing or stale - machines with no revision of a kind their PC type should produce, and revisions older than the interval.
- Toner critical, then low - links to the printer, then to its admin page.
- Warranties expiring - within the configured horizon.
- 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.