Files
shopdb-flask/docs/proposals/dashboard-live-fleet.md
cproudlock f9d6b57f0f Move the documentation to the line that ships, and correct it against the code
The docs lived on one lineage and the fleet code on another. That is why six
pages described a shared fleet key as how an agent authenticates, why
COLLECTOR-AUTH had to warn that the per-PC credential code was not in the
repository it sat in, and why ADR-019 recorded a decision whose implementation
was somewhere else. Documentation that cannot see the code it documents goes
stale by construction, and no amount of editing fixes that.

So the corpus moves here: 84 files, 56 pages, 20 ADRs, the proposals, INDEX.md,
llms.txt and both gates. Filenames are unchanged, because a wiki page name is
derived from the filename and a rename breaks every link anybody saved.

THE POINT OF THE MOVE IS THE CORRECTION IT ENABLES. Every caveat that said "not
in this tree" was false the moment the files landed beside the code, so each was
rewritten from what is actually here:

- COLLECTOR-AUTH gained the real enroll request body, the two bounds on the
  unauthenticated route (per-IP rate limit, pending cap with stale rows dropped
  before a real PC is refused), and the rejected pending reason that binds to
  the endpoint identity rather than the nonce.
- ADR-019 now describes an implementation that is present, including its tests,
  and says plainly that what remains is the FLEET migration, not the code.
- proposals/device-identity claimed phase 1 was unbuilt. plugins/computers
  declares deviceid on the collector schema and prefers it over the older
  fallbacks. Corrected in the page, its summary and the index.

WINDOWS/IIS IS NOW DOCUMENTED AS THE LEGACY PATH, per the product decision, and
carefully: it is what production runs today. Every Windows page opens by saying
it is supported, not for a new site, and where a new site should go instead. No
procedure was cut - an operator on an IIS box at 2am needs these pages to work
exactly as well tomorrow. MIGRATE-TOPOLOGY stops being a footnote and becomes
the off-ramp, which is why it nearly doubled: what was there did not match how
the Docker stack really works here.

The Docker material that already lived on this line was kept and given
ownership rather than buried under the ported Windows-centric prose: DEPLOY now
describes the four services, the migrate service that runs the upgrade chain
before api starts, TLS terminated by nginx, and the plugin count corrected from
thirteen to seventeen - a number both lineages had wrong.

ROADMAP is rewritten from evidence rather than memory: what has shipped, read
out of the changelog, the ADRs and the manifests, and ahead of it exactly two
items - finalize a distributed release and polish the setup wizard.

PROJECT-MAP is generated and was stale; the GENERATOR was what was wrong.
--check passes now.

API-REFERENCE is retired to a pointer. /api/docs serves 573 operations with
derived auth, query parameters and harvested examples, and a hand-written page
competing with that is a liability. The file stays so its wiki page and its
inbound links survive.

AND THE MOVE TURNED ON A SCRUB THAT HAD NEVER RUN HERE. test_docs_publishable
keys on docs/*.md, which this line did not have, so it skipped - its own comment
predicted what would surface. Porting the corpus activated it and it found 48
lines that must not reach a published wiki: branch names abbreviating the site,
assistant-tooling paths, the site name in two deployment pages, and an employee
ID in a Windows path that the scrub does not even look for.
2026-09-19 09:10:15 -04:00

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 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.