# 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: 1. [Adding a new backup kind](#adding-a-new-backup-kind) 2. [How the Backup badge decides good vs stale](#how-the-backup-badge-decides-good-vs-stale) --- ## 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: ```python 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 itself - `docs/geenforce-api-cutover.md` - the fleet reporting path these fields ride on