From 8edc6956da701efcc87fd54c17292f6157352e3d Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Sat, 5 Sep 2026 06:25:55 -0400 Subject: [PATCH] Add the disk health panel Its own card rather than folded into Storage: Storage is per volume and this is per physical device, and one drive can back several volumes or none the operator sees, so the two do not share a key. Rows sort failure-first, then unknown, then OK. A failing drive must not sit below the fold on a multi-disk box, and a drive that stopped answering is a weaker claim of health than one that said OK. An unrecognised verdict sorts as unknown rather than last, so a future verdict string cannot bury itself under the healthy drives. Both agent sources are shown rather than merged, matching the mapped-drive card: when two sources disagree about one drive, that disagreement is the thing worth looking at. --- plugins/endpoints/api/routes.py | 60 +++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/plugins/endpoints/api/routes.py b/plugins/endpoints/api/routes.py index 53afb2b0..9a63a92d 100644 --- a/plugins/endpoints/api/routes.py +++ b/plugins/endpoints/api/routes.py @@ -640,6 +640,66 @@ def panel_storage(asset_id: int): return success_response(rows) +def _diskhealth_rows(drives): + """Turn the agent's disk health list into display rows, worst first. + + Both agent sources are kept rather than merged. MSFT_PhysicalDisk is the + only source that reports NVMe at all, and Win32_DiskDrive covers hardware + and hosts where the Storage namespace is unavailable. When both answer for + the same hardware, two rows that disagree is the useful signal; collapsing + them would hide exactly the case worth looking at. + """ + if not isinstance(drives, list): + return [] + + rows = [] + for drive in drives: + if not isinstance(drive, dict): + continue + predictfailure = drive.get('predictfailure') + if predictfailure is None: + verdict = 'Unknown' + elif predictfailure: + verdict = 'Failure predicted' + else: + verdict = 'OK' + rows.append({ + 'deviceid': drive.get('deviceid'), + 'model': drive.get('model') or '', + 'mediatype': drive.get('mediatype') or '', + 'bustype': drive.get('bustype') or '', + 'healthstatus': drive.get('healthstatus') or 'Unknown', + 'verdict': verdict, + 'source': drive.get('source') or '', + }) + + # Failure first, then unknown, then OK. Unknown outranks OK because a drive + # that stopped answering is a weaker claim of health than one that said OK. + # An unrecognised verdict sorts as unknown rather than last, so a future + # verdict string cannot bury itself under the healthy drives. + order = {'Failure predicted': 0, 'Unknown': 1, 'OK': 2} + rows.sort(key=lambda row: (order.get(row['verdict'], 1), + row['deviceid'] or '')) + return rows + + +@endpoints_bp.route('/asset//diskhealth', methods=['GET']) +@require_permission('endpoints.view') +def panel_diskhealth(asset_id: int): + """Table panel: physical drive health. + + Separate from the storage panel because the two do not share a key. Storage + is per volume (C:, D:), this is per physical device, and one physical drive + can back several volumes or none the operator can see. + + Rows are ordered so anything predicting failure sorts first: a failing + drive must not be below the fold on a machine with several disks. + """ + state = EndpointState.query.filter_by(assetid=asset_id).first() + drives = _state_json(state, 'diskhealthjson') if state else None + return success_response(_diskhealth_rows(drives)) + + @endpoints_bp.route('/asset//listeners', methods=['GET']) @require_permission('endpoints.view') def panel_listeners(asset_id: int):