Four data-only hooks on BasePlugin (get_settings_cards, get_asset_panels, get_map_overlays, get_asset_presentation) with a GET-only /api/pluginui consumer surface copying the dashboard-widgets semantics. Pilots: warranty declares its asset panel; measuringtools supplies its settings card, presentation, and calibration overlay - the last hardcoded settings-nav entry is now hook-sourced. Generic renderers for panels/overlays/presentation deferred per the ADR's incremental adoption plan (documented in CONTRACT-STABILITY.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
109 lines
4.0 KiB
Python
109 lines
4.0 KiB
Python
"""Plugin frontend-contribution API endpoints (ADR-010).
|
|
|
|
Consumers for the four data-only presentation hooks on BasePlugin:
|
|
get_settings_cards, get_asset_panels, get_map_overlays, get_asset_presentation.
|
|
|
|
Every endpoint copies the dashboard-widgets consumer semantics: skip disabled
|
|
plugins, tag each contributed dict with its originating plugin name, fail loud
|
|
in dev/test, isolate a broken plugin in prod. All routes are GET + jwt-optional,
|
|
matching the other read-only core endpoints; they expose data, not mutations.
|
|
"""
|
|
|
|
from flask import Blueprint, request, current_app
|
|
from flask_jwt_extended import jwt_required
|
|
|
|
from shopdb.extensions import db
|
|
from shopdb.core.models import Asset
|
|
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
|
|
|
pluginui_bp = Blueprint('pluginui', __name__)
|
|
|
|
|
|
def _collect(hookname):
|
|
"""Aggregate a data-only hook across enabled plugins.
|
|
|
|
Same access pattern as dashboard.get_widgets: skip disabled plugins, inject
|
|
the plugin name on every returned dict, re-raise in dev/test, log-and-isolate
|
|
a broken plugin in prod. Returns the merged list (unsorted).
|
|
"""
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
if not pm:
|
|
return []
|
|
|
|
merged = []
|
|
for name, plugin in pm.get_all_plugins().items():
|
|
if not pm.registry.is_enabled(name):
|
|
continue
|
|
try:
|
|
for entry in getattr(plugin, hookname)() or []:
|
|
entry['plugin'] = name
|
|
merged.append(entry)
|
|
except Exception:
|
|
# fail loud in dev/test, isolate broke plugin in prod
|
|
if current_app.config.get('DEBUG') or current_app.config.get('TESTING'):
|
|
raise
|
|
current_app.logger.exception('Plugin %s %s failed', name, hookname)
|
|
return merged
|
|
|
|
|
|
@pluginui_bp.route('/settings-cards', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def settings_cards():
|
|
"""Merge enabled plugins' settings-catalog cards (get_settings_cards hook).
|
|
|
|
The frontend merges these into the core settingsNav catalog and renders
|
|
them in the settings rail + landing overview.
|
|
"""
|
|
cards = _collect('get_settings_cards')
|
|
cards.sort(key=lambda card: card.get('position', 99))
|
|
return success_response(cards)
|
|
|
|
|
|
@pluginui_bp.route('/asset-panels', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def asset_panels():
|
|
"""Return the asset-detail panels matching one asset's type (get_asset_panels).
|
|
|
|
Query param assetid is required. A panel matches when its assettypes list
|
|
contains the asset's type key or the wildcard '*'.
|
|
"""
|
|
assetid = request.args.get('assetid', type=int)
|
|
if not assetid:
|
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'assetid is required')
|
|
|
|
asset = db.session.get(Asset, assetid)
|
|
if not asset:
|
|
return error_response(ErrorCodes.NOT_FOUND, 'Asset not found', http_code=404)
|
|
assettype = asset.assettype.assettype if asset.assettype else None
|
|
|
|
matched = []
|
|
for panel in _collect('get_asset_panels'):
|
|
assettypes = panel.get('assettypes') or []
|
|
if '*' in assettypes or (assettype and assettype in assettypes):
|
|
matched.append(panel)
|
|
matched.sort(key=lambda panel: panel.get('position', 99))
|
|
return success_response(matched)
|
|
|
|
|
|
@pluginui_bp.route('/map-overlays', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def map_overlays():
|
|
"""Merge enabled plugins' map overlay declarations (get_map_overlays hook).
|
|
|
|
The map fetches each overlay endpoint and decorates already-placed markers.
|
|
"""
|
|
overlays = _collect('get_map_overlays')
|
|
overlays.sort(key=lambda overlay: overlay.get('position', 99))
|
|
return success_response(overlays)
|
|
|
|
|
|
@pluginui_bp.route('/asset-presentation', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def asset_presentation():
|
|
"""Merge enabled plugins' asset-type presentation entries (hook).
|
|
|
|
Search rows and cross-links use these to pick a type's icon and detail route.
|
|
"""
|
|
entries = _collect('get_asset_presentation')
|
|
return success_response(entries)
|