geenforce: the fleet table links where it says, and judges backups instead of dating them
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.
This commit is contained in:
118
docs/BACKUP-KINDS.md
Normal file
118
docs/BACKUP-KINDS.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user