Two fixes to the same table, in the same regions of the same files. ASSET LINK POINTED AT THE WRONG RECORD. The Asset chip linked /machines/<assetid>, but /machines/:id keys on machineid - the plugin extension id - as MachineDetail itself does everywhere. So the link landed on whichever machine happened to carry that number: a wrong page that looks right, which is worse than a 404. Same for /measuringtools/. The API now returns machinepluginid / toolpluginid beside the asset ids and the view links on those. Both lookups are import-guarded, and with no plugin id the number renders as plain text rather than a link that misleads. AssetRelationships already resolved this correctly; this brings the reports table in line. BACKUP COLUMN READ AS NEGLECT. It showed a raw date, and a revision is only written when the config CHANGES - dedup means a machine stable for months has a months-old newest revision and is perfectly healthy. The column already used lastseenat, the last time the collector CONFIRMED the config, but a bare timestamp says "nothing has happened since", which at the default 24h collection interval IS the healthy steady state. It made a working system look stalled and made the reader do arithmetic against a setting they would have to go and find. It now returns backupok and shows a badge naming the kind, green when confirmed recently, red when not, with the date and an explanation in the hover. backupok is tri-state on purpose: null means no revision at all, and renders as NO badge rather than a green one, because "never seen" must not read as healthy. The threshold is the backups plugin's own backups_staledays, read through its service so there is one definition of stale rather than a second drifting here. Nothing in the badge is kind-specific, so a backup kind added later inherits it by existing. docs/BACKUP-KINDS.md records that, the BackupKind contract, and why the rule is time-based rather than per-kind.
5.7 KiB
Fleet config backups: adding a kind, and how health is judged
This is about the backups plugin - the per-PC CONFIG backups the collector
takes off shopfloor machines (NTLARS settings, part-marker configs). It is not
about backing up the ShopDB database itself; that is docs/BACKUP-RESTORE.md.
Two things this covers:
Adding a new backup kind
A kind is one class in plugins/backups/services/registry.py, added to
REGISTRY at the bottom of that file:
REGISTRY = {k.key: k for k in (NtlarsKind(), PartMarkerKind())}
Subclass BackupKind and override what applies. NtlarsKind is the fullest
example (parseable, renderable, has an info panel); PartMarkerKind is the
lean one.
| Member | What it is |
|---|---|
key |
Wire value. This is what lands in backuprevisions.backupkind and what the fleet table shows on the badge. Keep it short and lowercase. |
displayname |
Human label for the UI. |
storagebackend |
'shopdb' (bytes live in the DB, deduped on a semantic hash) or 'share' (file lives on the SMB share, deduped on a raw byte hash). |
assettypes |
Which core asset types this kind can attach to; ['*'] for any. |
emptytext |
Text when an asset has no revisions of this kind. None HIDES the panel, which is the right default - a kind applies to an asset TYPE, but whether a given machine ever has that backup is per-machine. A part-marker panel on all 144 machines is noise. |
parse(raw) |
Opaque kinds return None; parseable kinds return a projection dict. |
formats() / render(...) |
Download formats, for shopdb kinds. |
resolveassetid(payload) |
Map a collector payload to the asset the backup belongs to. |
infopanel() / buildinfo(...) |
Optional at-a-glance card (ADR-010). Declared by the KIND, not hardcoded in the plugin, so a successor technology ships its own card by adding a class. |
sharedir(...) |
Conventional UNC directory for a share kind. Advisory - the authoritative path is whatever the collector reported, because the PC is what actually wrote the file. |
What you do NOT have to touch
The GE-Enforce Enforcement Reports Backup column needs no change. It reads
backuprevisions generically: newest revision per host, whatever the kind, and
shows backupkind on the badge. A new kind inherits the badge, the colour and
the tooltip with no work.
Same for the staleness rule below - it is time-based and kind-agnostic.
How the Backup badge decides good vs stale
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 fleet table 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.
That was shown as a raw date at first, and it 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. The date made a working system
look like a stalled one, and made the reader do arithmetic against a setting
they would have to go and look up.
What it does now
GET /api/geenforce/reports returns, per host:
| Field | Meaning |
|---|---|
backupkind |
Which kind was most recently confirmed. null = no backup at all. |
backuplastseen |
When it was last CONFIRMED (ISO). Tooltip only. |
backupok |
true good, false stale, null nothing to judge. |
backupstaleafterdays |
The threshold in force, so the UI can explain itself. |
backupok is deliberately tri-state. null means there is no revision for
that host, or the check is disabled - and it renders as no badge, never green.
"Never seen" must not read as healthy.
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 check.
The import is guarded, so a lean site build without the backups plugin returns
null instead of failing.
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 show a badge, 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 _backup_stale_cutoff prefer
it; the tri-state contract stays as it is.
Where the code lives
plugins/geenforce/api/routes.py-_attach_backup_state(newest revision per host),_backup_stale_cutoff(threshold),_backup_ok(verdict)plugins/geenforce/frontend/views/EnforcementReports.vue-backupClass(green / red / none),backupTitle(the hover text)plugins/backups/services/staleness.py- the shared threshold, also behind the dashboard's stale-backups card
Tests: tests/test_plugins/test_geenforce_reporting.py, the backup-verdict
block - recent-is-good, older-than-threshold-is-stale, and none-is-not-green.
See also
docs/BACKUP-RESTORE.md- backing up the ShopDB database itselfdocs/geenforce-api-cutover.md- the fleet reporting path these fields ride on