dashboard: convert the last dead widgets, and delete the one that had nothing
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

Three plugins still declared widgets naming Vue components nobody wrote.
Converting them honestly meant three different answers, not one.

notifications gets a real card: the active notifications themselves, not a
count. "4 active" tells an admin nothing; knowing WHICH message the shop is
looking at is the point, and it is how a stale one gets noticed and taken down.

machines gets machines out of service - anything not In Use, excluding
Inventory, because a spare on a shelf is stock rather than a problem. Someone is
supposed to be chasing each of those and today they are visible only to whoever
thinks to filter the list by status.

network gets NOTHING, and its declaration is deleted rather than converted.
Network devices carry no live status - no polling, no reachability check,
nothing that can be wrong - so the only possible card is a count of how many
exist, which is precisely the always-true number this dashboard exists to get
away from. A comment records that, so the next person does not re-add it. If
reachability is ever collected, that is the card.

Also adds a contract test over every declared card: no component names, a valid
renderer and severity, and - the one that matters - the endpoint must be a REAL
route. A declaration pointing at a route nobody wrote is exactly how the old
widgets rotted unnoticed for months, and now it fails the build instead.
This commit is contained in:
cproudlock
2026-08-11 14:27:29 -04:00
parent 7151b68bdd
commit 5eb84873e8
7 changed files with 174 additions and 53 deletions

View File

@@ -558,3 +558,35 @@ def dashboard_summary():
'bytype': [{'type': t, 'count': c} for t, c in by_type],
'bystatus': [{'status': s, 'count': c} for s, c in by_status]
})
@machines_bp.route('/dashboard/outofservice', methods=['GET'])
@jwt_required()
@require_permission('machines.view')
def dashboard_outofservice():
"""Machines whose status says they are not running.
Anything that is not In Use: In Repair, Lost, Returned, and so on. Someone
is supposed to be chasing each of these, and today they are visible only to
whoever thinks to filter the machines list by status.
Inventory is excluded - a spare on a shelf is not a problem, it is stock.
"""
from shopdb.api import AssetStatus
ignored = ('In Use', 'Inventory')
rows = (db.session.query(Machine, Asset, AssetStatus)
.join(Asset, Asset.assetid == Machine.assetid)
.join(AssetStatus, AssetStatus.statusid == Asset.statusid)
.filter(Asset.isactive.is_(True),
AssetStatus.status.notin_(ignored))
.order_by(Asset.assetnumber)
.limit(50).all())
return success_response([{
'assetid': asset.assetid,
'assetnumber': asset.assetnumber or str(asset.assetid),
'name': asset.name,
'status': status.status,
} for _machine, asset, status in rows])

View File

@@ -196,14 +196,30 @@ class MachinesPlugin(BasePlugin):
return [machinescli]
def get_dashboard_widgets(self) -> List[Dict]:
"""Return dashboard widget definitions."""
"""Dashboard card: machines not in service.
A machine sitting in Repair or marked Lost is a thing someone is
supposed to be chasing, and nothing surfaces it today - it is visible
only to whoever thinks to filter the machines list by status.
Replaces a declaration naming a component nobody wrote.
"""
return [
{
'name': 'Machine Status',
'component': 'MachineStatusWidget',
'endpoint': '/api/machines/dashboard/summary',
'size': 'medium',
'position': 5,
'id': 'machines-outofservice',
'title': 'Machines out of service',
'endpoint': '/api/machines/dashboard/outofservice',
'render': 'exceptions',
'severity': 'warning',
'permission': 'machines.view',
'empty': 'hide',
'position': 45,
'map': {
'title': 'assetnumber',
'detail': 'status',
'meta': [{'key': 'name'}],
'link': '/machines/{assetid}',
},
},
]