dashboard: printer supplies, expiring warranties, mis-numbered bays
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

Wave one complete. Three cards, no new data and no migrations.

Printer supplies reuses the existing low-supplies query and its five-minute
cache; a Zabbix round-trip per printer on every dashboard load would make this
the slowest page in the app. One row per printer listing every depleted
cartridge, criticals first - a row per cartridge would report one printer three
times and read as three problems, and showing only the worst class would hide a
low cartridge behind a critical one on the same machine when whoever walks out
there wants to carry both.

While there: the low-supplies REPORT itself was including healthy cartridges. A
printer with one empty black and three full colour ones listed all four, so the
reader had to find the problem inside the row. It now lists only what needs
replacing, and the test that asserted the old behaviour now asserts the new.

Expiring warranties keeps already-expired entries on the list rather than
dropping them the day they lapse, which is how they get missed. Horizon is
warranty_expiringdays, default 90, because that suits a site budgeting
quarterly and nobody else.

Mis-numbered bays promotes check-shared-machines out of a CLI command nobody
will remember to run - it found seven bays that had been wrong for weeks. It
reports only numbers with NO child assets, so part markers legitimately sharing
an operation stay silent: that distinction is the whole card, and without it it
would list correct data beside faults and be ignored.

Printers also loses its dead component-named widget; notifications, network and
machines still have theirs.
This commit is contained in:
cproudlock
2026-08-11 14:13:26 -04:00
parent 6c975a107c
commit 7151b68bdd
9 changed files with 339 additions and 13 deletions

View File

@@ -118,3 +118,81 @@ def test_the_widget_uses_the_new_contract(app):
assert widget['render'] == 'exceptions'
assert widget['permission'] == 'computers.view'
assert widget['empty'] == 'hide'
# =============================================================================
# Machine numbers claimed by two PCs - a fault, unless the devices are modelled
# =============================================================================
SHAREDURL = '/api/computers/dashboard/sharedmachines'
def _machine(db, assetnumber):
mt = AssetType.query.filter_by(assettype='machine').first()
if not mt:
mt = AssetType(assettype='machine')
db.session.add(mt)
db.session.flush()
asset = Asset(assetnumber=assetnumber, assettypeid=mt.assettypeid,
statusid=1)
db.session.add(asset)
db.session.commit()
return asset
def _controls(db, pchostname, machineasset, label='collector:machine'):
from shopdb.core.models import AssetRelationship, RelationshipType
controls = RelationshipType.query.filter_by(relationshiptype='controls').first()
if not controls:
controls = RelationshipType(relationshiptype='controls', isdirectional=True)
db.session.add(controls)
db.session.flush()
comp = _pc(db, pchostname)
db.session.add(AssetRelationship(
sourceassetid=comp.assetid, targetassetid=machineasset.assetid,
relationshiptypeid=controls.relationshiptypeid, label=label,
isactive=True))
db.session.commit()
return comp
def test_one_pc_per_machine_is_not_a_finding(client, db, auth_headers):
machine = _machine(db, '3015')
_controls(db, 'ONLYPC', machine)
assert client.get(SHAREDURL, headers=auth_headers).get_json()['data'] == []
def test_two_pcs_on_one_number_is_reported(client, db, auth_headers):
machine = _machine(db, '2026')
_controls(db, 'PCONE', machine)
_controls(db, 'PCTWO', machine)
[row] = client.get(SHAREDURL, headers=auth_headers).get_json()['data']
assert row['machinenumber'] == '2026'
assert row['pccount'] == 2
assert 'PCONE' in row['pcs'] and 'PCTWO' in row['pcs']
def test_a_modelled_shared_number_is_not_a_finding(client, db, auth_headers):
"""Part markers legitimately share an operation number. Modelled correctly,
each device is its own asset filed partof the operation - so the operation
has child assets, and that is what tells a real fault from correct data."""
from shopdb.core.models import AssetRelationship, RelationshipType
operation = _machine(db, '0615')
_controls(db, 'MARKERPCA', operation)
_controls(db, 'MARKERPCB', operation)
partof = RelationshipType(relationshiptype='partof', isdirectional=True)
db.session.add(partof)
db.session.flush()
marker = _machine(db, 'MARKERPCA-PARTMARKER')
db.session.add(AssetRelationship(
sourceassetid=marker.assetid, targetassetid=operation.assetid,
relationshiptypeid=partof.relationshiptypeid,
label='collector:partmarker', isactive=True))
db.session.commit()
assert client.get(SHAREDURL, headers=auth_headers).get_json()['data'] == []
def test_shared_machines_is_permission_gated(client, db, member_headers):
assert client.get(SHAREDURL, headers=member_headers).status_code == 403

View File

@@ -138,5 +138,7 @@ def test_low_supplies_rollup_flags_waste_and_toner(app, db, mock_zabbix):
assert statuses['Black Toner Level'] == 'critical'
# 97%-full waste (HP, non-inverted) -> 3% remaining -> critical
assert statuses['Waste Cartridge Level'] == 'critical'
# 60% cyan is fine
assert statuses['Cyan Toner Level'] == 'ok'
# 60% cyan is FINE, so it is not listed at all. The report answers
# "what needs replacing"; including healthy cartridges made the reader
# hunt for the problem inside the row.
assert 'Cyan Toner Level' not in statuses