diff --git a/docs/CONTRACT-STABILITY.md b/docs/CONTRACT-STABILITY.md index ec7d7a31..07e26fe1 100644 --- a/docs/CONTRACT-STABILITY.md +++ b/docs/CONTRACT-STABILITY.md @@ -47,7 +47,7 @@ Recorded in the comment block in `shopdb/__init__.py`: | 0.25.0 | Added `caller_holds(permission)`, which answers whether the current caller holds a permission WITHOUT refusing the request - the complement to `require_permission` for a route that serves everyone but should only offer what the caller can act on. The endpoints services panel is the case: it emitted Start/Stop row actions to every reader, because the emission never consulted the caller. Clicking them was always refused server-side, so nothing was exploitable, but offering a read-only viewer controls over a shop-floor PC is the wrong signal, and it becomes a real problem once a site grants that panel to guests. Guests are included in the answer, since a signed-out visitor holds whatever the guest role was granted | additive surface (minor) | | 0.26.0 | Dashboard cards may declare `shortname`. The dashboard merged its per-source cards into one worst-first queue, and a card's `title` is written as a heading ("Endpoints with crashes or shutdowns") which as a row badge ran wider than the hostname it qualified. Optional, defaults to the title, so a plugin that does not declare one is unaffected | additive field (minor) | | 0.27.0 | Added `Script` so a plugin can read the ShopDB-hosted script library that backs endpoint `run_script` delivery without importing core model internals. The endpoints plugin resolves a delivered script by name and serves its body by digest to the agent | additive surface (minor) | -| 0.29.0 | Added an optional `group` to `get_asset_panels`, naming which detail-page tab a panel belongs to: `details` (the default) / `software` / `metrics` / `actions` / `activity`. The detail pages are tabbed, and a page that placed panels by knowing their ids would defeat the hook. Additive: an absent or unrecognised group resolves to `details` server-side, so every existing plugin keeps working with no change | additive surface (minor) | +| 0.29.0 | Added an optional `group` to `get_asset_panels`, naming which detail-page tab a panel belongs to: `details` (the default) / `monitors` / `software` / `activity`. The detail pages are tabbed, and a page that placed panels by knowing their ids would defeat the hook. Additive: an absent or unrecognised group resolves to `details` server-side, so every existing plugin keeps working with no change | additive surface (minor) | | 0.28.0 | Added `client_ip` and `strip_port` so a plugin does not hand-roll caller-IP extraction. Two plugins had, and both read the first `X-Forwarded-For` hop, which the caller supplies - so a rate limit keyed on it was defeated by rotating the header. The helper resolves from `remote_addr`, which ProxyFix has already filled from the header when `TRUSTED_PROXY_COUNT` says a proxy is really in front | additive surface (minor) | The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, 0.10.0, diff --git a/frontend/src/components/PluginAssetPanels.vue b/frontend/src/components/PluginAssetPanels.vue index 58155e5d..9782f2af 100644 --- a/frontend/src/components/PluginAssetPanels.vue +++ b/frontend/src/components/PluginAssetPanels.vue @@ -200,8 +200,9 @@ const panels = ref([]) // has since been renamed or removed silently disappears. That is what happened // to tracked services when it moved out of 'actions' - the backend still said // 'actions', this build had no such tab, and the card was simply gone with -// nothing logged. Fall back the same way the server does rather than drop it. -const RENDERED_GROUPS = ['details', 'software', 'metrics', 'activity'] +// nothing logged. The same applies to the 'metrics' to 'monitors' rename. +// Fall back the same way the server does rather than drop it. +const RENDERED_GROUPS = ['details', 'monitors', 'software', 'activity'] function groupOf(panel) { return RENDERED_GROUPS.includes(panel.group) ? panel.group : 'details' } diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue index 9970e092..044691dc 100644 --- a/plugins/computers/frontend/views/PCDetail.vue +++ b/plugins/computers/frontend/views/PCDetail.vue @@ -302,13 +302,14 @@ - -
- + Monitors keeps the two-column flow because it is short cards. --> +
+
@@ -384,10 +385,12 @@ const detailtabs = computed(() => { const filled = assetPanels.value?.populatedGroups ?? new Set() const has = (key) => filled.has ? filled.has(key) : false const tabs = [{ key: 'details', label: 'Details' }] + // Monitors sits second: what is being watched on this box is the question + // asked right after "what is it", and well before its software inventory. + if (has('monitors')) tabs.push({ key: 'monitors', label: 'Monitors' }) if (has('software') || installedApps.value.length > 0) { tabs.push({ key: 'software', label: 'Software', count: installedApps.value.length || null }) } - if (has('metrics')) tabs.push({ key: 'metrics', label: 'Metrics' }) if (has('activity')) tabs.push({ key: 'activity', label: 'Activity' }) return tabs }) diff --git a/plugins/endpoints/plugin.py b/plugins/endpoints/plugin.py index 506806dc..c02a3f1e 100644 --- a/plugins/endpoints/plugin.py +++ b/plugins/endpoints/plugin.py @@ -351,9 +351,9 @@ class EndpointsPlugin(BasePlugin): 'permission': 'endpoints.command', 'empty': 'No monitored services reported.', 'position': 12, - # Service state is telemetry first; the Start/Stop controls - # belong beside it rather than in a tab of their own. - 'group': 'metrics', + # Service state is what is being watched; the Start/Stop + # controls belong beside it, not in a tab of their own. + 'group': 'monitors', }, { 'id': 'endpoints-tracked-processes', @@ -393,7 +393,7 @@ class EndpointsPlugin(BasePlugin): 'empty': 'No monitored processes reported.', 'position': 13, # observed process state, not something you act on here - 'group': 'metrics', + 'group': 'monitors', }, { # One feed instead of two cards. Events and commands answer the @@ -458,8 +458,8 @@ class EndpointsPlugin(BasePlugin): ], 'empty': 'No network interfaces reported.', 'position': 16, - # adapter state is telemetry, not something you act on - 'group': 'metrics', + # adapter state is watched, not something you act on + 'group': 'monitors', }, { # Installed-software (SBOM) inventory. Raw discovered list for diff --git a/shopdb/core/api/pluginui.py b/shopdb/core/api/pluginui.py index 1b917885..20cf1508 100644 --- a/shopdb/core/api/pluginui.py +++ b/shopdb/core/api/pluginui.py @@ -25,7 +25,7 @@ pluginui_bp = Blueprint('pluginui', __name__) # # 'details' is first and is the fallback: it holds what an asset IS, which is # what a panel declaring nothing almost always contributes. -ASSET_PANEL_GROUPS = ('details', 'software', 'metrics', 'activity') +ASSET_PANEL_GROUPS = ('details', 'monitors', 'software', 'activity') def _collect(hookname): diff --git a/shopdb/plugins/base.py b/shopdb/plugins/base.py index cebdf581..523d48f6 100644 --- a/shopdb/plugins/base.py +++ b/shopdb/plugins/base.py @@ -371,7 +371,7 @@ class BasePlugin(ABC): # keys, rendered generically - see warranty) 'position': int, # order among panels 'group': str, # OPTIONAL. Which tab on the detail page: - # 'details' | 'software' | 'metrics' + # 'details' | 'monitors' | 'software' # | 'activity'. Omitted means 'details'. } @@ -383,12 +383,13 @@ class BasePlugin(ABC): existing plugin working untouched: an unknown or absent group resolves to 'details', which is the tab that holds what an asset IS. Reach for another group only when the panel is the thing that tab exists for - - an inventory listing, live telemetry, or a history feed. + what is being watched on the box, an inventory listing, or a history + feed. There is deliberately no group for controls. A panel that acts on the machine belongs beside the state it acts on: tracked services read as - telemetry first and a Start/Stop second, so they live in 'metrics' with - the rest of what the endpoint reports. Splitting the buttons away from + state first and a Start/Stop second, so they live in 'monitors' with + the rest of what is being watched. Splitting the buttons away from the state they change made a tab that held one card and answered no question on its own. diff --git a/tests/test_core/test_pluginui.py b/tests/test_core/test_pluginui.py index 8b672ad2..708cd428 100644 --- a/tests/test_core/test_pluginui.py +++ b/tests/test_core/test_pluginui.py @@ -198,7 +198,7 @@ def test_a_group_nobody_defined_falls_back_rather_than_disappearing( monkeypatch.setattr(plugin, 'get_asset_panels', lambda: [{ 'id': 'typo', 'title': 'Typo', 'assettypes': ['*'], 'endpoint': '/api/whatever', 'render': 'keyvalue', 'position': 1, - 'group': 'metrcis', + 'group': 'montiors', }]) assetid = _make_asset(db) @@ -210,18 +210,18 @@ def test_a_group_nobody_defined_falls_back_rather_than_disappearing( def test_group_matching_is_not_case_or_space_sensitive(app, client, db, auth_headers, monkeypatch): - """'Metrics ' is obviously meant to be metrics; do not send it to details.""" + """'Monitors ' is obviously meant to be monitors; do not send it to details.""" pm = app.extensions['plugin_manager'] monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: True) plugin = next(iter(pm.get_all_plugins().values())) monkeypatch.setattr(plugin, 'get_asset_panels', lambda: [{ 'id': 'shouty', 'title': 'Shouty', 'assettypes': ['*'], 'endpoint': '/api/whatever', 'render': 'keyvalue', 'position': 1, - 'group': ' Metrics ', + 'group': ' Monitors ', }]) assetid = _make_asset(db) panels = _data(client, '/api/pluginui/asset-panels?assetid=%d' % assetid, auth_headers) panel = next(p for p in panels if p['id'] == 'shouty') - assert panel['group'] == 'metrics' + assert panel['group'] == 'monitors'