geenforce: first dashboard card, and the widget contract it proves
Wave one of the dashboard proposal, built as a vertical slice so the contract is proven by something real before the other five cards follow. GET /api/geenforce/dashboard/failures lists entries that FAILED on their PC's most recent enforcement cycle. Per ENTRY, not per report: "three PCs failed" is a number, while "Install OpenText failed with exit 1603 on WJSF1234" is something a person can act on. Only current reports count, so a failure that has since been fixed clears itself instead of needing dismissing. Hostnames resolve to computerids in one query so each row links to the PC, and a PC shopdb does not know still appears - the failure is real even when the inventory is behind, and that is the bay most likely to be misconfigured. The data has been there all along. The only way to see any of it was to open one PC's report modal, one PC at a time. The widget declaration is the contract change. The old shape named a Vue component per widget, which cannot survive a lean build where a plugin's component may never be staged into the bundle - which is exactly why five plugins declare widgets pointing at components nobody ever wrote. This declares data, a generic renderer, a permission and a link template, the way ADR-010 already does for asset panels. A test asserts no 'component' key, so the old shape cannot creep back. empty: hide is part of the contract, not decoration. A card reporting "nothing wrong" daily teaches people to stop reading the page, which is how a fleet log reached 3,234 lines with 17 that mattered. Frontend rendering comes next; the endpoint and declaration stand alone and change nothing that exists.
This commit is contained in:
@@ -26,7 +26,8 @@ SHAREROOT_SETTING = 'geenforce_share_root'
|
||||
|
||||
from ..models import (
|
||||
ManifestScope, ManifestEntry, ManifestPublishedVersion,
|
||||
ManifestEnforcementReport, ManifestPayload, ManifestBlob, ENTRY_TYPES, PHASES,
|
||||
ManifestEnforcementReport, ManifestEnforcementResult, ManifestPayload,
|
||||
ManifestBlob, ENTRY_TYPES, PHASES,
|
||||
)
|
||||
from ..serializer import scope_to_manifest, entry_to_dict
|
||||
from ..importer import build_entry, populate_entry
|
||||
@@ -954,3 +955,57 @@ def get_report(reportid):
|
||||
'message': r.message,
|
||||
} for r in report.results],
|
||||
})
|
||||
|
||||
|
||||
@geenforce_bp.route('/dashboard/failures', methods=['GET'])
|
||||
@jwt_required()
|
||||
@require_permission('geenforce.manage')
|
||||
def dashboard_failures():
|
||||
"""Entries that FAILED on their PC's most recent enforcement cycle.
|
||||
|
||||
The dashboard card behind this answers a question nothing else does: what is
|
||||
broken on the floor right now. The reports table has held it all along -
|
||||
status, the failing entry, its exit code and the engine's message - and the
|
||||
only way to see any of it was to open one PC's report modal, one PC at a
|
||||
time. A bay returned 500 to every collector report for a day and a half
|
||||
before anyone looked.
|
||||
|
||||
Per ENTRY, not per report: "three PCs failed" is a number, while "Install
|
||||
OpenText failed with exit 1603 on WJSF1234" is something a person can act
|
||||
on. Only current reports (iscurrent) are considered, so a failure that has
|
||||
since been fixed disappears on its own rather than needing dismissing.
|
||||
"""
|
||||
rows = (db.session.query(ManifestEnforcementResult, ManifestEnforcementReport)
|
||||
.join(ManifestEnforcementReport,
|
||||
ManifestEnforcementResult.reportid ==
|
||||
ManifestEnforcementReport.reportid)
|
||||
.filter(ManifestEnforcementReport.iscurrent.is_(True),
|
||||
ManifestEnforcementResult.action == 'failed')
|
||||
.order_by(ManifestEnforcementReport.receivedat.desc())
|
||||
.limit(50)
|
||||
.all())
|
||||
|
||||
# Resolve hostnames to computerids in ONE query so the card can link to the
|
||||
# PC rather than just naming it. A row whose PC is unknown to shopdb still
|
||||
# shows - the failure is real even when the inventory is behind.
|
||||
hostnames = {report.hostname for _result, report in rows}
|
||||
idbyhost = {}
|
||||
if hostnames:
|
||||
try:
|
||||
from plugins.computers.models import Computer
|
||||
for computer in Computer.query.filter(
|
||||
Computer.hostname.in_(hostnames)).all():
|
||||
idbyhost[(computer.hostname or '').lower()] = computer.computerid
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return success_response([{
|
||||
'hostname': report.hostname,
|
||||
'computerid': idbyhost.get((report.hostname or '').lower()),
|
||||
'scopename': report.scopename,
|
||||
'entryname': result.entryname,
|
||||
'exitcode': result.exitcode,
|
||||
'message': (result.message or '').strip()[:200],
|
||||
'receivedat': (report.receivedat.isoformat() + 'Z'
|
||||
if report.receivedat else None),
|
||||
} for result, report in rows])
|
||||
|
||||
Reference in New Issue
Block a user