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.
5.1 KiB
How the backups card decides good vs stale
The backups plugin's health verdict, as shown on the dashboard's Backups
stopped card. Read it when the card looks wrong, or before changing the rule.
Adding a new kind needs none of this - see BACKUP-KINDS.md.
The trap this is built around
A revision is only written when the config CHANGES. Dedup compares against
the latest revision for the chain (asset, kind, sourcehostname), so a machine
whose config has been stable for six months has a six-month-old newest revision
and is perfectly healthy. The question worth answering is not "when was the last
backup taken" - it is "is this still being checked".
So the timestamp the card uses is backuprevisions.lastseenat: the last time
the collector CONFIRMED this config, whether or not anything changed. It moves
on every successful collection; the revision does not.
It is a card of exceptions rather than a column of raw dates because a raw date
read as neglect. At the default backups_intervalhours of 24 the collector
only attempts once a day, so a day-old confirmation IS the healthy steady state -
and the date made the reader do arithmetic against a setting they would have to
go and look up.
What it does now
GET /api/backups/dashboard/stale returns one row per chain that has gone
quiet, worst first:
| Field | Meaning |
|---|---|
assetid, assetnumber |
The asset the backup belongs to. |
backupkind |
Which kind stopped. Chains fail independently, so the kind is part of the identity, not a summary. |
sourcehostname |
The PC that was doing the collecting. |
lastseenat |
When this chain was last CONFIRMED still current (ISO). |
quietdays |
How long it has been quiet, which is what the card sorts on. |
lastseenat is reported rather than the capture time because they answer
different questions and one cannot stand for both: the first proves the check is
running, the second only says what is stored. Collapsing them was what made a
healthy machine look neglected.
The chain is the unit, not the host
A marker bay runs both an NTLARS backup and a part-marker backup. Reporting one verdict per host would let a healthy kind mask a dead one: the part-marker capture could stop for a month and the host would look fine on the strength of its NTLARS row. So the card is keyed on the CHAIN - asset, kind and source PC together - because that is the unit that fails independently.
Silence is not a verdict
A chain whose newest revision has never been confirmed - lastseenat NULL,
which means a backfilled row or one written before the column existed - is not
stale, it is UNMEASURABLE, and it is skipped rather than listed. Substituting
collectedat for it looked right and was wrong: on a real fleet it reported
every machine whose config had simply been stable for months as a stopped
backup, so the whole board lit up and said the site was broken when it was fine.
A chain becomes measurable the first time its PC posts, and the card says
nothing about it until then.
The card is equally silent about an asset that has NEVER been backed up. Whether one SHOULD be is a question the asset record cannot answer - most machines carry no NTLARS at all - and guessing would list a hundred healthy assets, which is the noise that makes a board worth ignoring.
Known consequence: a device retired from a PC leaves its last revision behind, so that chain stays listed until the revision is pruned. That is the honest failure direction - it complains about a backup that genuinely is not running any more, rather than going quiet about one.
The threshold is the backups plugin's own backups_staledays setting (default
3), read through plugins.backups.services.staleness.staledays() rather
than re-derived, so there is ONE definition of stale. 0 disables the card.
Why time-based rather than per-kind
Every kind answers the same question the same way: something confirmed this
recently, or it did not. Making the rule per-kind would mean each new kind has
to define health before it can appear, for no gain. If a kind ever needs its own
window - a weekly backup that should not be judged on a 3-day rule - add a
threshold override on BackupKind and have the rule prefer it; the
skip-the-unmeasurable contract stays as it is.
Where the code lives
plugins/backups/services/staleness.py-staledays()(the shared threshold),latestperchain()(newest revision per chain),stalechains()(the verdict)plugins/backups/api/routes.py-dashboard_stale, a thin route over that rule so it stays testable without an auth layer in the wayplugins/backups/plugin.py- thebackups-staledashboard widget declaration, including thebackups_staledayssetting
Adding a kind is BACKUP-KINDS.md; backing up the ShopDB database itself is BACKUP-RESTORE.md, a different subject.