Dashboard cards, backup last-seen

This commit is contained in:
2026-08-11 14:01:57 -04:00
parent c30195a92b
commit 0ed8be75d9
18 changed files with 1167 additions and 9 deletions

View File

@@ -13,6 +13,7 @@ Two layers:
"""
import base64
from datetime import datetime, timedelta
import json
import pytest
@@ -862,3 +863,100 @@ def test_the_collection_interval_is_readable_without_a_login(bk_app):
BackupsPlugin().get_settings_defaults()}
assert declared['backups_intervalhours'].get('public') is True
assert not declared['backups_shareroot'].get('public')
# =============================================================================
# Dashboard: backups that have STOPPED, which is not the same as unchanged
# =============================================================================
def test_an_unchanged_config_records_the_check(bk_app, bk_plugin):
"""The fix that makes the card possible at all. Dedup writes no revision on
a no-op, so without recording the check there is no evidence a backup still
runs - a config stable for six months looks identical to a backup that died
six months ago."""
from plugins.backups.models import BackupRevision
with bk_app.app_context():
first = bk_plugin.apply_collector_payload(_payload())
revision = _db.session.get(BackupRevision, first['backuprevisionid'])
_db.session.query(BackupRevision).update(
{'lastseenat': datetime(2026, 1, 1)})
_db.session.commit()
second = bk_plugin.apply_collector_payload(_payload())
assert second['action'] == 'noop'
_db.session.refresh(revision)
assert revision.lastseenat > datetime(2026, 1, 2)
def test_a_stable_machine_is_not_called_stale(bk_app, bk_plugin):
"""A machine whose settings have not changed in months is HEALTHY. Keying
the card on revision age would have flagged most of the fleet."""
from plugins.backups.models import BackupRevision
with bk_app.app_context():
bk_plugin.apply_collector_payload(_payload())
# Changed long ago, checked just now: the normal steady state.
_db.session.query(BackupRevision).update({
'collectedat': datetime.utcnow() - timedelta(days=180),
'lastseenat': datetime.utcnow(),
})
_db.session.commit()
from plugins.backups.services.staleness import stalechains
assert stalechains() == []
def test_a_machine_that_stopped_checking_in_is_listed(bk_app, bk_plugin):
from plugins.backups.models import BackupRevision
with bk_app.app_context():
bk_plugin.apply_collector_payload(_payload())
_db.session.query(BackupRevision).update(
{'lastseenat': datetime.utcnow() - timedelta(days=10)})
_db.session.commit()
from plugins.backups.services.staleness import stalechains
[row] = stalechains()
assert row['quietdays'] == 10
assert row['backupkind'] == 'ntlars'
def test_one_marker_stopping_is_not_hidden_by_its_healthy_sibling(bk_app,
bk_plugin):
"""Per CHAIN, not per asset. Two part markers share one machine number, and
a per-asset view would report the machine as fine while one device has been
silent for a week."""
from plugins.backups.models import BackupRevision
with bk_app.app_context():
bk_plugin.apply_collector_payload(
_payload(reg=_markerreg('COM3'), sourcehostname='MARKERA'))
bk_plugin.apply_collector_payload(
_payload(reg=_markerreg('COM4'), sourcehostname='MARKERB'))
(_db.session.query(BackupRevision)
.filter_by(sourcehostname='MARKERA')
.update({'lastseenat': datetime.utcnow() - timedelta(days=9)}))
(_db.session.query(BackupRevision)
.filter_by(sourcehostname='MARKERB')
.update({'lastseenat': datetime.utcnow()}))
_db.session.commit()
from plugins.backups.services.staleness import stalechains
rows = stalechains()
assert [r['sourcehostname'] for r in rows] == ['MARKERA']
def test_the_threshold_is_a_setting_and_zero_disables_the_card(bk_app, bk_plugin):
from shopdb.core.models import Setting
from plugins.backups.models import BackupRevision
with bk_app.app_context():
bk_plugin.apply_collector_payload(_payload())
_db.session.query(BackupRevision).update(
{'lastseenat': datetime.utcnow() - timedelta(days=2)})
_db.session.add(Setting(key='backups_staledays', value='1'))
_db.session.commit()
from plugins.backups.services.staleness import stalechains
assert len(stalechains()) == 1
_db.session.query(Setting).filter_by(key='backups_staledays').update(
{'value': '0'})
_db.session.commit()
assert stalechains() == []

View File

@@ -0,0 +1,120 @@
"""Dashboard card: PCs that stopped reporting, and PCs that never started.
Two populations, deliberately not merged. A PC that reported and went quiet is
probably off, moved or broken. A PC that has NEVER reported is worse - not
enrolled, or enrolled against the wrong pc-type, so nothing enforces anything on
it and no backup of it exists. That one hides indefinitely because nothing about
it fails loudly.
"""
from datetime import datetime, timedelta, timezone
from shopdb.core.models import Asset, AssetType, Setting
URL = '/api/computers/dashboard/quiet'
def _now():
return datetime.now(timezone.utc).replace(tzinfo=None)
def _pc(db, hostname, lastreported='now', isactive=True):
from plugins.computers.models import Computer
atype = AssetType.query.filter_by(assettype='computer').first()
if not atype:
atype = AssetType(assettype='computer')
db.session.add(atype)
db.session.flush()
asset = Asset(assetnumber=hostname, assettypeid=atype.assettypeid,
statusid=1, isactive=isactive)
db.session.add(asset)
db.session.flush()
comp = Computer(assetid=asset.assetid, hostname=hostname)
if lastreported == 'now':
comp.lastreporteddate = _now()
elif lastreported is None:
comp.lastreporteddate = None
else:
comp.lastreporteddate = _now() - timedelta(hours=lastreported)
db.session.add(comp)
db.session.commit()
return comp
def test_a_reporting_pc_is_not_listed(client, db, auth_headers):
_pc(db, 'BUSYPC')
assert client.get(URL, headers=auth_headers).get_json()['data'] == []
def test_a_pc_gone_quiet_is_listed_with_how_long(client, db, auth_headers):
_pc(db, 'QUIETPC', lastreported=72)
[row] = client.get(URL, headers=auth_headers).get_json()['data']
assert row['hostname'] == 'QUIETPC'
assert row['state'] == 'stopped reporting'
assert row['quietdays'] == 3
def test_a_pc_that_never_reported_is_called_out_separately(client, db,
auth_headers):
"""Never-reported is a different fault from gone-quiet: nothing is
enforcing anything on that PC and no backup of it exists."""
_pc(db, 'NEVERPC', lastreported=None)
[row] = client.get(URL, headers=auth_headers).get_json()['data']
assert row['state'] == 'never reported'
assert row['lastreported'] is None
def test_never_reported_sorts_above_the_merely_quiet(client, db, auth_headers):
"""The order someone should work down the list."""
_pc(db, 'QUIETPC', lastreported=48)
_pc(db, 'NEVERPC', lastreported=None)
rows = client.get(URL, headers=auth_headers).get_json()['data']
assert [r['hostname'] for r in rows] == ['NEVERPC', 'QUIETPC']
def test_the_longest_silence_comes_first(client, db, auth_headers):
_pc(db, 'DAY2', lastreported=48)
_pc(db, 'DAY9', lastreported=216)
rows = client.get(URL, headers=auth_headers).get_json()['data']
assert [r['hostname'] for r in rows] == ['DAY9', 'DAY2']
def test_the_window_is_a_setting_not_a_hardcode(client, db, auth_headers):
"""Every site will disagree with 24 hours (ADR-015)."""
_pc(db, 'OVERNIGHT', lastreported=10)
assert client.get(URL, headers=auth_headers).get_json()['data'] == []
db.session.add(Setting(key='computers_quietreporthours', value='4'))
db.session.commit()
rows = client.get(URL, headers=auth_headers).get_json()['data']
assert [r['hostname'] for r in rows] == ['OVERNIGHT']
def test_a_bad_setting_falls_back_rather_than_failing_the_card(client, db,
auth_headers):
db.session.add(Setting(key='computers_quietreporthours', value='soon'))
db.session.commit()
_pc(db, 'QUIETPC', lastreported=72)
assert client.get(URL, headers=auth_headers).status_code == 200
def test_a_retired_pc_is_not_nagged_about(client, db, auth_headers):
"""A decommissioned PC is silent on purpose. Listing it would train people
to ignore the card, which is the failure mode this whole board avoids."""
_pc(db, 'RETIRED', lastreported=None, isactive=False)
assert client.get(URL, headers=auth_headers).get_json()['data'] == []
def test_the_card_is_permission_gated(client, db, member_headers):
assert client.get(URL, headers=member_headers).status_code == 403
def test_the_widget_uses_the_new_contract(app):
from plugins.computers.plugin import ComputersPlugin
widget = ComputersPlugin().get_dashboard_widgets()[0]
assert 'component' not in widget
assert widget['render'] == 'exceptions'
assert widget['permission'] == 'computers.view'
assert widget['empty'] == 'hide'

View File

@@ -0,0 +1,123 @@
"""GE-Enforce dashboard card: what is broken on the floor right now.
The reports table has always held enforcement failures - the failing entry, its
exit code, the engine's message - and the only way to see any of it was to open
one PC's report modal, one PC at a time. A bay returned 500 to every collector
report for a day and a half before anyone looked. This card is the fix, so
these tests pin the behaviour that makes it useful rather than decorative.
"""
from datetime import datetime
from plugins.geenforce.models import (ManifestEnforcementReport,
ManifestEnforcementResult)
URL = '/api/geenforce/dashboard/failures'
def _report(db, hostname, iscurrent=True, results=(), scopename='gea-shopfloor-cmm',
receivedat=None):
report = ManifestEnforcementReport(
hostname=hostname, scopename=scopename, phase='runtime',
status='failed', iscurrent=iscurrent,
receivedat=receivedat or datetime(2026, 8, 11, 12, 0, 0))
db.session.add(report)
db.session.flush()
for entryname, action, exitcode, message in results:
db.session.add(ManifestEnforcementResult(
reportid=report.reportid, entryname=entryname, action=action,
exitcode=exitcode, message=message))
db.session.commit()
return report
def test_a_failed_entry_is_listed_with_what_a_person_needs(client, db, auth_headers):
"""Per ENTRY, not per report. "Three PCs failed" is a number; "Install
OpenText failed with exit 1603 on WJSF1234" is something to act on."""
_report(db, 'WJSF1234', results=[
('Install OpenText', 'failed', 1603, 'Fatal error during installation')])
rows = client.get(URL, headers=auth_headers).get_json()['data']
assert len(rows) == 1
assert rows[0]['hostname'] == 'WJSF1234'
assert rows[0]['entryname'] == 'Install OpenText'
assert rows[0]['exitcode'] == 1603
assert 'Fatal error' in rows[0]['message']
def test_only_failures_appear(client, db, auth_headers):
"""A cycle installs, skips and filters far more than it fails. Listing
anything but failures is how a card becomes wallpaper."""
_report(db, 'WJSF1234', results=[
('Install OpenText', 'failed', 1603, 'boom'),
('Install Acrobat', 'installed', 0, ''),
('Set FMS host', 'skipped', 0, ''),
('CMM settings', 'filtered', 0, 'PCTypes filter')])
rows = client.get(URL, headers=auth_headers).get_json()['data']
assert [r['entryname'] for r in rows] == ['Install OpenText']
def test_a_fixed_failure_disappears_on_its_own(client, db, auth_headers):
"""Superseded reports are not current. A failure that has since been fixed
must clear itself rather than needing someone to dismiss it - otherwise the
card accumulates history and stops meaning "right now"."""
_report(db, 'WJSF1234', iscurrent=False, results=[
('Install OpenText', 'failed', 1603, 'boom')])
_report(db, 'WJSF1234', iscurrent=True, results=[
('Install OpenText', 'installed', 0, '')])
assert client.get(URL, headers=auth_headers).get_json()['data'] == []
def test_the_row_links_to_the_pc_when_shopdb_knows_it(client, db, auth_headers):
"""A card that names a PC without linking to it is a worse report."""
from plugins.computers.models import Computer
from shopdb.core.models import Asset, AssetType
atype = AssetType.query.filter_by(assettype='computer').first()
if not atype:
atype = AssetType(assettype='computer')
db.session.add(atype)
db.session.flush()
asset = Asset(assetnumber='WJSF1234',
assettypeid=atype.assettypeid, statusid=1)
db.session.add(asset)
db.session.flush()
comp = Computer(assetid=asset.assetid, hostname='WJSF1234')
db.session.add(comp)
db.session.commit()
_report(db, 'WJSF1234', results=[('Install OpenText', 'failed', 1603, 'x')])
rows = client.get(URL, headers=auth_headers).get_json()['data']
assert rows[0]['computerid'] == comp.computerid
def test_a_pc_shopdb_does_not_know_still_reports_its_failure(client, db,
auth_headers):
"""The failure is real even when the inventory is behind. Dropping the row
would hide exactly the bay most likely to be misconfigured."""
_report(db, 'GHOSTPC', results=[('Install OpenText', 'failed', 1603, 'x')])
rows = client.get(URL, headers=auth_headers).get_json()['data']
assert len(rows) == 1
assert rows[0]['computerid'] is None
def test_the_card_is_permission_gated(client, db, member_headers):
"""A dashboard card must not become a way around RBAC."""
assert client.get(URL, headers=member_headers).status_code == 403
def test_the_widget_declares_data_and_a_renderer_not_a_component(app):
"""The old contract named a Vue component per widget, which cannot survive
a lean build - which is why five plugins declared widgets pointing at
components nobody wrote. A plugin declares data and shape; core renders."""
from plugins.geenforce.plugin import GeEnforcePlugin
widget = GeEnforcePlugin().get_dashboard_widgets()[0]
assert 'component' not in widget
assert widget['render'] == 'exceptions'
assert widget['permission'] == 'geenforce.manage'
# Empty cards must shrink, or the board becomes wallpaper.
assert widget['empty'] == 'hide'
assert widget['map']['link'] == '/pcs/{computerid}'