Files
shopdb-flask/docs/BACKUP-STALENESS.md
cproudlock 534f179ee6 Make main the lean product a site receives
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.
2026-09-19 11:24:40 -04:00

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 way
  • plugins/backups/plugin.py - the backups-stale dashboard widget declaration, including the backups_staledays setting

Adding a kind is BACKUP-KINDS.md; backing up the ShopDB database itself is BACKUP-RESTORE.md, a different subject.