From 294ddbb38e22c9c909e509213892ce9a071c2e71 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Tue, 11 Aug 2026 15:00:25 -0400 Subject: [PATCH] backups: show that a check happened, not just that a change did lastseenat already recorded it and the API already returned it; nothing displayed it, so from the UI a healthy machine still looked abandoned - one revision from last spring and no sign anything had looked at it since. The history page gains a Last verified column beside Captured, and the asset panel a Verified field. Only the CURRENT revision carries one: an older revision was superseded, so saying it was verified today would be false - what was verified is the configuration the PC holds now. A current revision with no check yet says "not yet checked" rather than showing a blank or borrowing the captured date. That state is real and temporary: the column is new, so every chain reports it until its PC next posts. --- CHANGELOG.md | 7 +++++++ .../backups/frontend/views/BackupHistory.vue | 17 +++++++++++++++ plugins/backups/plugin.py | 6 ++++++ tests/test_plugins/test_backups.py | 21 +++++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e24df33..e879fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,13 @@ ADR-007 and ADR-002. ### Added +- Backup history distinguishes CAPTURED from LAST VERIFIED. Captured is when a + configuration was written; verified is when a PC last posted it and the hash + still matched. They differ by design, because an unchanged config writes no + new revision - so a machine checked daily for a year shows one revision from + last spring, and previously read as abandoned. Shown on the history page and + on the asset panel; only the current revision carries it, since an older one + is not what the PC holds now. - A backup now records that it was CHECKED, not only that it changed. Dedup means an unchanged configuration writes no revision, so the stored timestamps moved only on a change: a machine whose settings had been stable for six diff --git a/plugins/backups/frontend/views/BackupHistory.vue b/plugins/backups/frontend/views/BackupHistory.vue index 625e689..6ab4fec 100644 --- a/plugins/backups/frontend/views/BackupHistory.vue +++ b/plugins/backups/frontend/views/BackupHistory.vue @@ -20,6 +20,7 @@ Captured + Last verified Kind From PC Hash @@ -34,6 +35,22 @@ {{ formatWhen(rev.collectedat || rev.createdat) }} current + + + + not yet checked + - + {{ rev.backupkind }} {{ rev.sourcehostname || '-' }} {{ rev.shorthash }} diff --git a/plugins/backups/plugin.py b/plugins/backups/plugin.py index 0e8b5b7..e26567e 100644 --- a/plugins/backups/plugin.py +++ b/plugins/backups/plugin.py @@ -101,6 +101,12 @@ class BackupsPlugin(BasePlugin): 'title': 'label', 'meta': [ {'key': 'collectedat', 'label': 'Captured', 'format': 'date'}, + # Captured is when this config was WRITTEN; verified is + # when a PC last posted it and the hash still matched. + # An unchanged config writes no revision, so without + # this a machine checked daily for a year shows one + # entry from last spring and reads as abandoned. + {'key': 'lastseenat', 'label': 'Verified', 'format': 'date'}, {'key': 'sourcehostname', 'label': 'From'}, {'key': 'shorthash', 'label': 'Hash', 'mono': True}, ], diff --git a/tests/test_plugins/test_backups.py b/tests/test_plugins/test_backups.py index acfa92a..6b08680 100644 --- a/tests/test_plugins/test_backups.py +++ b/tests/test_plugins/test_backups.py @@ -960,3 +960,24 @@ def test_the_threshold_is_a_setting_and_zero_disables_the_card(bk_app, bk_plugin {'value': '0'}) _db.session.commit() assert stalechains() == [] + + +def test_the_history_reports_when_a_config_was_last_verified(bk_app, bk_plugin): + """A machine checked daily for a year still shows one revision, because an + unchanged config writes none. The API has to say the check happened or that + machine reads as abandoned.""" + from plugins.backups.models import BackupRevision + with bk_app.app_context(): + result = bk_plugin.apply_collector_payload(_payload()) + revision = _db.session.get(BackupRevision, result['backuprevisionid']) + data = revision.to_dict() + assert data['lastseenat'] is not None + assert data['lastseenat'].endswith('Z') + + +def test_the_panel_shows_verified_beside_captured(bk_app, bk_plugin): + with bk_app.app_context(): + panels = bk_plugin.get_asset_panels() + listpanel = next(p for p in panels if p.get('render') == 'list') + labels = [m['label'] for m in listpanel['map']['meta']] + assert 'Captured' in labels and 'Verified' in labels