From e00bb44d6bae4f12a29e8ef34fc4a772b2ae5e5f Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Sat, 5 Sep 2026 07:05:30 -0400 Subject: [PATCH] merge storage and disk health into one card with two sections Volume capacity and physical drive health were two adjacent cards, and the operator reading a full disk is usually the operator reading a dying disk in the same minute. They are now one Storage card with two sections. Visual grouping only - the two tables share no key, storage being per volume and health per physical device, and one drive can back several volumes or none the operator sees. Nothing is reconciled across the sections; this is not the mapped-drive merge where two sources described the same drive and the worse status won. The tabs render type was the only sections-in-one-card mechanism and emitted label/value fields with no table path, so it now takes table sections too. That is the renderer every plugin uses, so both behaviour changes in it are mutation-tested. Two things only surfaced on contact: the empty-section filter had to grow a table branch, because a table section declares columns up front and dropping it when it has no rows takes its tab with it and reads as no disks rather than nothing reported; and full-width span was keyed off the declared render type, but a tabs panel holding a six-column table needs the same span and that is only knowable from fetched data. The diskhealth route stays. It no longer backs a panel, but it is a stable read for callers outside the renderer and removing it for a UI-only change would break them. --- .../notes/endpoint-telemetry-and-ui-phases.md | 37 +++---- frontend/src/components/PluginAssetPanels.vue | 38 ++++++- frontend/src/components/pluginAssetPanels.js | 45 +++++++-- .../src/components/tabSectionsTable.spec.js | 99 +++++++++++++++++++ plugins/endpoints/api/routes.py | 75 +++++++++++--- plugins/endpoints/plugin.py | 40 ++------ scripts/backup/_probe_manifest.py | 0 scripts/backup/backup.sh.tmp | 0 scripts/backup/watch.sh.tmp | 0 scripts/site_imports/map_from_grid.py | 0 .../test_endpoints_panel_groups.py | 9 +- tests/test_plugins/test_endpoints_plugin.py | 4 +- .../test_endpoints_storage_sections.py | 51 ++++++++++ 13 files changed, 321 insertions(+), 77 deletions(-) create mode 100644 frontend/src/components/tabSectionsTable.spec.js create mode 100644 scripts/backup/_probe_manifest.py create mode 100644 scripts/backup/backup.sh.tmp create mode 100644 scripts/backup/watch.sh.tmp create mode 100644 scripts/site_imports/map_from_grid.py create mode 100644 tests/test_plugins/test_endpoints_storage_sections.py diff --git a/docs/notes/endpoint-telemetry-and-ui-phases.md b/docs/notes/endpoint-telemetry-and-ui-phases.md index 5537f957..699d48a9 100644 --- a/docs/notes/endpoint-telemetry-and-ui-phases.md +++ b/docs/notes/endpoint-telemetry-and-ui-phases.md @@ -242,24 +242,25 @@ Depends on Phase 2 data existing. until the collection question is settled. - Boot type placement to be decided when the data is visible. - **Storage + Disk health into one card with two collapsible sections** - - AGREED, deferred to its own phase. VISUAL GROUPING ONLY. These two tables - cannot be merged into shared rows: Storage is per volume, Disk health is per - physical device, and one drive can back several volumes or none the operator - sees. This is emphatically NOT the Phase 1 mapped-drive treatment, where two - sources described the same drive and the worse status won. Nothing here gets - reconciled. - Cost, measured rather than guessed: the `tabs` render type is the only - existing sections-in-one-card mechanism and it emits label/value fields only - (PluginAssetPanels.vue), with no table path. Both cards here are `table` - renders with different columns AND different keys, so nothing existing - carries them. Preferred approach is extending `tabs` to support table - sections (~40 lines in the shared renderer plus a helper), then collapsing - the two routes into one returning `sections`. A new `sections` render type is - the alternative but is a larger addition to that fixed set. - Deferred deliberately: this modifies the renderer EVERY plugin uses, so it - needs its own regression tests and must not be bundled with the Phase 2 - telemetry commits - otherwise a UI regression and a telemetry regression - become one commit to bisect. + DONE. VISUAL GROUPING ONLY. These two tables are not merged into shared rows: + Storage is per volume, Disk health is per physical device, and one drive can + back several volumes or none the operator sees. This is emphatically NOT the + Phase 1 mapped-drive treatment, where two sources described the same drive and + the worse status won. Nothing here is reconciled. + Built as predicted: `tabs` was extended to support table sections (columns/ + rows/empty per section) in the shared renderer, and the two routes collapsed + into one `/storage` returning `sections`. `/diskhealth` is kept as a route - + it is a stable read for non-panel callers, and removing it for a UI-only + change would break them. + Two things only found by building it: + - The empty-section filter had to grow a table branch. A table section + declares its columns up front and so must survive having no rows; dropping + it takes its TAB with it, and a missing tab reads as "this machine has no + disks" rather than "nothing was reported". + - Full-width span was keyed off `render === 'table'`. A `tabs` panel holding a + six-column table needs the same span, and whether it does is only knowable + from FETCHED DATA, not the declaration - hence `spansFullWidth()`. + Both are mutation-tested in `tabSectionsTable.spec.js`. Checked and cleared: the "inline label/value pairs, not nested cards" density rule in Phase 5 does NOT block this. That rule is scoped to the PC Details HEADER block, where the constraint is adding zero height so the tab strip is diff --git a/frontend/src/components/PluginAssetPanels.vue b/frontend/src/components/PluginAssetPanels.vue index 922f1509..9966ad9d 100644 --- a/frontend/src/components/PluginAssetPanels.vue +++ b/frontend/src/components/PluginAssetPanels.vue @@ -3,7 +3,7 @@ a panel as JSON (title, endpoint, render mode, field map); this renders it on any asset detail page, so a plugin adds detail-page UI with no Vue. -->