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.
121 lines
4.6 KiB
Python
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'
|