Files
shopdb-flask/tests/test_plugins/test_computers_dashboard.py
cproudlock 1ca8a9b8e8
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
dashboard: PCs not reporting, and the card styling standard it broke
Second wave-one card. GET /api/computers/dashboard/quiet lists two populations
and deliberately does not merge them into one count. 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 - the same shape as the bay that carried a wrong
machine number for weeks.

Never-reported sorts above the merely quiet, then longest silence first: the
order someone should work down the list, not the order rows left the table. A
soft-deleted PC is excluded - a decommissioned machine is silent on purpose,
and listing it would train people to ignore the card, which is the failure this
whole board exists to avoid.

The window is computers_quietreporthours, default 24, because every site will
disagree with any number picked here (ADR-015). A malformed value falls back
rather than failing the card.

This also replaces the computers plugin's old widget declaration, which named a
component nobody ever wrote. Four such declarations remain and will convert as
their cards arrive.

Two fixes to the renderer found while wiring this up. Meta specs now support a
trailing unit, so a row reads 'quiet for 3 days' rather than 'quiet for 3'. And
the card styles hardcoded hex colours against the frontend standard, including
a var(--card-bg) that DOES NOT EXIST - the variable is --bg-card - so the
fallback would have painted every card white and broken dark mode entirely.
Now --bg-card, --border, --danger, --warning, --primary and --link throughout.
2026-08-11 13:40:41 -04:00

121 lines
4.6 KiB
Python

"""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'