dashboard: PCs not reporting, and the card styling standard it broke
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

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.
This commit is contained in:
cproudlock
2026-08-11 13:40:41 -04:00
parent 05c150c663
commit 1ca8a9b8e8
6 changed files with 229 additions and 13 deletions

View File

@@ -934,3 +934,63 @@ def dashboard_summary():
'shopfloor': shopfloor_count,
'nonshopfloor': total - shopfloor_count
})
@computers_bp.route('/dashboard/quiet', methods=['GET'])
@jwt_required()
@require_permission('computers.view')
def dashboard_quiet():
"""PCs that have stopped reporting, and PCs that never started.
Two populations, deliberately distinguished rather than merged into one
count. A PC that reported and went quiet is probably switched off, moved or
broken. A PC that has NEVER reported is worse: it is either 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
is failing loudly - and it is exactly the shape of the bay that carried a
wrong machine number for weeks.
Silence is the only signal available here. There is no heartbeat separate
from the report, so 'stopped reporting' is measured against the collector's
own last write.
"""
from datetime import datetime, timedelta, timezone
hours = 24
setting = Setting.query.filter_by(key='computers_quietreporthours').first()
if setting and (setting.value or '').strip():
try:
hours = int(setting.value)
except (TypeError, ValueError):
pass
cutoff = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=hours)
query = (db.session.query(Computer, Asset)
.join(Asset, Asset.assetid == Computer.assetid)
.filter(Asset.isactive.is_(True)))
rows = []
for comp, asset in query.all():
last = comp.lastreporteddate
if last is None:
rows.append({
'computerid': comp.computerid,
'hostname': comp.hostname,
'state': 'never reported',
'lastreported': None,
'quietdays': None,
})
elif last < cutoff:
rows.append({
'computerid': comp.computerid,
'hostname': comp.hostname,
'state': 'stopped reporting',
'lastreported': last.isoformat() + 'Z',
'quietdays': (datetime.now(timezone.utc).replace(tzinfo=None)
- last).days,
})
# Never-reported first, then longest silence: the order someone should work
# down the list, not the order the rows came out of the table.
rows.sort(key=lambda r: (r['quietdays'] is not None, -(r['quietdays'] or 0)))
return success_response(rows[:50])

View File

@@ -163,6 +163,14 @@ class ComputersPlugin(BasePlugin):
site that installed an earlier one.
"""
return [
{
'key': 'computers_quietreporthours',
'value': '24',
'valuetype': 'integer',
'category': 'computers',
'description': 'Hours without a collector report before a PC '
'is listed as not reporting on the dashboard.',
},
{
'key': 'computers_machinelink_alerts',
'value': 'false',
@@ -1247,14 +1255,30 @@ class ComputersPlugin(BasePlugin):
return [computerscli]
def get_dashboard_widgets(self) -> List[Dict]:
"""Return dashboard widget definitions."""
"""Dashboard cards this plugin contributes.
REPLACES a declaration that named a Vue component which was never
written - as did four other plugins - so nothing rendered and nobody
noticed, because the frontend never called the widgets endpoint either.
The contract now carries data and a renderer; core draws it.
"""
return [
{
'name': 'Computer Status',
'component': 'ComputerStatusWidget',
'endpoint': '/api/computers/dashboard/summary',
'size': 'medium',
'position': 6,
'id': 'computers-quiet',
'title': 'PCs not reporting',
'endpoint': '/api/computers/dashboard/quiet',
'render': 'exceptions',
'severity': 'warning',
'permission': 'computers.view',
'empty': 'hide',
'position': 20,
'map': {
'title': 'hostname',
'detail': 'state',
'meta': [{'key': 'quietdays', 'label': 'quiet for',
'suffix': ' days'}],
'link': '/pcs/{computerid}',
},
},
]