Dashboard cards, backup last-seen
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])
|
||||
|
||||
@@ -66,6 +66,42 @@ class GeEnforcePlugin(BasePlugin):
|
||||
ManifestEnforcementReport, ManifestEnforcementResult, PcTypeAlias,
|
||||
]
|
||||
|
||||
def get_dashboard_widgets(self) -> List[Dict]:
|
||||
"""Dashboard cards this plugin contributes.
|
||||
|
||||
DATA AND SHAPE, not a component name. The older widget contract named a
|
||||
Vue component per widget, which cannot survive a lean build - a
|
||||
plugin's component may never be staged into the frontend bundle - and
|
||||
is why five plugins declared widgets pointing at components nobody ever
|
||||
wrote. Core owns a small set of generic renderers; a plugin says what
|
||||
to show and how to link it. Same lesson ADR-010 already applied to
|
||||
asset panels.
|
||||
|
||||
`empty: hide` matters as much as the data. A card that reports "nothing
|
||||
wrong" every day teaches people to stop reading the page, which is
|
||||
exactly how a fleet log reached 3,234 lines with 17 that mattered.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'id': 'geenforce-failures',
|
||||
'title': 'Enforcement failures',
|
||||
'endpoint': '/api/geenforce/dashboard/failures',
|
||||
'render': 'exceptions',
|
||||
'severity': 'critical',
|
||||
'permission': 'geenforce.manage',
|
||||
'empty': 'hide',
|
||||
'position': 10,
|
||||
'map': {
|
||||
'title': 'hostname',
|
||||
'detail': 'entryname',
|
||||
'meta': [{'key': 'message'}, {'key': 'exitcode',
|
||||
'label': 'exit'}],
|
||||
'link': '/pcs/{computerid}',
|
||||
'timestamp': 'receivedat',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
def get_permissions(self) -> List:
|
||||
"""RBAC permissions this plugin owns (edit vs ship are split)."""
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user