add mapped drives card

Mapped drives existed only as a comma-joined line in the user context card, which is fine for three drives and unreadable for a dozen. They now get their own list card in Monitors: one row per letter, remote path as meta, health as a badge. Reuses the statecolor/levels vocabulary the tracked-services cards already declare rather than new CSS. Only unhealthy drives are coloured. Green on every working drive makes the broken one just another badge in a row of badges, and the card exists to make the exception findable at a glance. Unhealthy rows sort first for the same reason - a user with a dozen mappings should not hunt for the broken one - and the sort is on health alone, so drive-letter order survives within each group and the list does not reshuffle between refreshes. The merge logic is untouched. _mapped_drive_rows now returns structured rows and the user-context summary line is built from them, so the card and the line cannot drift apart about a drive's status. The two silent-failure paths are mutation-tested: forcing every badge green failed 7 tests, and the first sort test passed a mutation that added a secondary key because every broken drive in it said the same word - it now uses statuses that would reorder under a text sort. Also corrects a stale warning claiming the merge is untested; it has been covered since Phase 1.
This commit is contained in:
benjamin.baker
2026-09-05 07:12:21 -04:00
parent e00bb44d6b
commit 0c9bd1ed32
5 changed files with 230 additions and 20 deletions

View File

@@ -864,8 +864,8 @@ def _worse_drive_status(left, right):
)
def _mapped_drives(context):
"""Merge the two mapped-drive views into one row per drive letter.
def _mapped_drive_rows(context):
"""Merge the two mapped-drive views into one structured row per letter.
Windows exposes mapped drives through Win32_NetworkConnection (the 'net use'
table) and through Get-SmbMapping, and they do not always agree - a share can
@@ -875,8 +875,9 @@ def _mapped_drives(context):
A drive present in only one source is still a drive: it is reported with the
status that source gave it, rather than dropped for lack of a second opinion.
WARNING - this function is not covered by tests. Three behaviours here are
load-bearing and easy to break while "simplifying":
WARNING - three behaviours here are load-bearing and easy to break while
"simplifying". They are covered by tests/test_plugins/test_endpoints_mapped
_drives.py; if you touch this, read those first:
1. Worse-status-wins. Never report the healthier of the two sources. A
drive that either source calls Unavailable IS unavailable to the
@@ -888,8 +889,7 @@ def _mapped_drives(context):
string must never be optimistically treated as healthy.
Changing any of these makes a broken drive look fine, which is a silent
failure - the panel stays green and nobody is told. If you touch this,
add the tests first.
failure - the card stays green and nobody is told.
"""
drives = {}
@@ -928,20 +928,64 @@ def _mapped_drives(context):
rows = []
for letter in sorted(drives):
drive = drives[letter]
status = (drive.get('status') or '').strip()
remote = (drive.get('remote') or '').strip()
label = '{} {}'.format(letter, remote).strip()
# A drive neither source gave a status for is Unknown, never blank. An
# unannotated row reads as healthy, so a failed check would render as a
# working drive - the one outcome this card must never produce.
if not status:
status = 'Unknown'
status = (drive.get('status') or '').strip() or 'Unknown'
healthy = status.lower() in ('ok', 'connected')
rows.append({
'drive': letter,
'remote': (drive.get('remote') or '').strip(),
'statuslabel': status,
# Only unhealthy drives are coloured. Green on every working drive
# makes the broken one just another badge in a row of badges; the
# card's job is to make the exception findable at a glance.
'statecolor': 'green' if healthy else 'red',
})
return rows
def _mapped_drives(context):
"""The mapped drives as one-line labels, for the user context card.
Built on _mapped_drive_rows so the summary line and the card can never
disagree about a drive's status.
"""
labels = []
for row in _mapped_drive_rows(context):
label = '{} {}'.format(row['drive'], row['remote']).strip()
# Only the problem states are annotated. Tagging every healthy drive
# with "(OK)" would bury the one that is not.
if status.lower() not in ('ok', 'connected'):
label = '{} ({})'.format(label, status)
rows.append(label)
return rows
if row['statecolor'] != 'green':
label = '{} ({})'.format(label, row['statuslabel'])
labels.append(label)
return labels
def _mapped_drives_unhealthy_first(rows):
"""Order drive rows so anything not healthy sorts first.
A user with a dozen mappings should not have to hunt for the broken one.
Sorting only on health keeps it stable: drive-letter order survives within
each group, so the list does not reshuffle between refreshes.
"""
return sorted(rows, key=lambda row: row['statecolor'] == 'green')
@endpoints_bp.route('/asset/<int:asset_id>/mappeddrives', methods=['GET'])
@require_permission('endpoints.view')
def panel_mappeddrives(asset_id: int):
"""List panel: mapped network drives with a health badge each.
Session-scoped: mapped drives do not exist in the agent's session 0, so
these come from the interactive user probe. Nothing reported when nobody
is signed in, which is a valid state and not a failure.
"""
state = EndpointState.query.filter_by(assetid=asset_id).first()
context = _state_json(state, 'usercontextjson') if state else None
if not isinstance(context, dict) or not context:
return success_response([])
return success_response(_mapped_drives_unhealthy_first(_mapped_drive_rows(context)))
@endpoints_bp.route('/asset/<int:asset_id>/usercontext', methods=['GET'])