Tab the PC detail page and add panel groups to the ADR-010 contract

This commit is contained in:
2026-08-31 14:25:53 -04:00
parent 73a91ef916
commit 0d49341a2a
8 changed files with 315 additions and 20 deletions

View File

@@ -104,7 +104,7 @@ from .plugins import plugin_manager
# 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 names on the surface, minor bump.
__contract_version__ = '0.28.0'
__contract_version__ = '0.29.0'
# Product release version (see ADR-007). The product version and the
# plugin-contract version above are distinct series with independent

View File

@@ -19,6 +19,14 @@ from shopdb.utils.authz import require_permission
pluginui_bp = Blueprint('pluginui', __name__)
# The tabs an asset detail page offers, and so the only values a panel's
# 'group' may take. Kept here rather than in the frontend because the server
# resolves the group before the client sees it - see asset_panels below.
#
# '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', 'actions', 'activity')
def _collect(hookname):
"""Aggregate a data-only hook across enabled plugins.
@@ -67,6 +75,12 @@ def asset_panels():
Query param assetid is required. A panel matches when its assettypes list
contains the asset's type key or the wildcard '*'.
Every panel comes back carrying a 'group', naming the detail-page tab it
belongs to. It is resolved HERE rather than in the client so a plugin that
predates tabs, or names a group that does not exist, still lands somewhere a
person can see instead of vanishing from a page that only renders known
groups. Unknown and absent both mean 'details'.
"""
assetid = request.args.get('assetid', type=int)
if not assetid:
@@ -81,6 +95,9 @@ def asset_panels():
for panel in _collect('get_asset_panels'):
assettypes = panel.get('assettypes') or []
if '*' in assettypes or (assettype and assettype in assettypes):
panel = dict(panel)
group = str(panel.get('group') or '').strip().lower()
panel['group'] = group if group in ASSET_PANEL_GROUPS else 'details'
matched.append(panel)
matched.sort(key=lambda panel: panel.get('position', 99))
return success_response(matched)

View File

@@ -370,8 +370,23 @@ class BasePlugin(ABC):
# ('list' takes a 'map' of title/badge/meta
# keys, rendered generically - see warranty)
'position': int, # order among panels
'group': str, # OPTIONAL. Which tab on the detail page:
# 'details' | 'software' | 'metrics'
# | 'actions' | 'activity'. Omitted means
# 'details'.
}
ON 'group'. The detail pages are tabbed, and a panel says which tab it
belongs to rather than the page listing panels by id - a page that knew
its plugins' panels by name would defeat the point of the hook.
Omitting it is the correct answer for most panels and keeps every
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, controls that act on the machine,
or a history feed.
Consumed by GET /api/pluginui/asset-panels?assetid=<id>, which returns
the panels matching that asset's type. Disabled plugins
are skipped; a broken plugin is isolated in prod, re-raised in dev/test.