diff --git a/docs/CONTRACT-STABILITY.md b/docs/CONTRACT-STABILITY.md index 111da12..d4cf4da 100644 --- a/docs/CONTRACT-STABILITY.md +++ b/docs/CONTRACT-STABILITY.md @@ -8,7 +8,7 @@ the live code, not aspiration. The authoritative hook reference is ## Current version -The plugin contract is at **0.6.0**, declared in `shopdb/__init__.py` as +The plugin contract is at **0.7.0**, declared in `shopdb/__init__.py` as `__contract_version__`. It is pre-1.0, which under semver means any 0.x minor bump is allowed to break the contract, and this project has used that latitude. @@ -25,8 +25,9 @@ Recorded in the comment block in `shopdb/__init__.py`: | 0.3.0 | `shopdb.api` expanded to the full plugin import surface (db, cache, model bases, core models, response + pagination helpers, `employee_connection`) so plugins stop importing internal core paths | additive (minor) | | 0.4.0 | Removed the never-implemented `get_searchable_fields` hook (search is a core concern over the asset model) and wired `get_dashboard_widgets` to a real consumer (`/api/dashboard/widgets`) | pre-1.0 contract reduction | | 0.6.0 | Added the `get_reports` hook, consumed by `GET /api/reports` to merge plugin report cards into the Reports hub | additive optional hook (minor) | +| 0.7.0 | Added the four ADR-010 frontend-contribution hooks (`get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`), consumed by the `GET /api/pluginui/*` endpoints | additive optional hooks (minor) | -The source comment block documents 0.3.0, 0.4.0, and 0.6.0. Earlier points +The source comment block documents 0.3.0, 0.4.0, 0.6.0, and 0.7.0. Earlier points (0.1.x / 0.2.x) predate that recorded rationale; `PluginMeta`'s fallback `core_version` default of `>=0.2.0,<1.0.0` is the only remaining trace of the 0.2 baseline. @@ -49,6 +50,7 @@ land with a new or amended ADR. | `get_navigation_items` | Sidebar menu entries | | `get_dashboard_widgets` | Dashboard widgets, consumed by `/api/dashboard/widgets` | | `get_reports` | Report cards, consumed by `/api/reports` (added 0.6.0) | +| Frontend-contribution hooks | `get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`, consumed by `/api/pluginui/*` (added 0.7.0, [ADR-010](adr/ADR-010-frontend-plugin-hooks.md)) | | Collector pair | `get_collector_schema` + `apply_collector_payload` per [ADR-006](adr/ADR-006-collector-contract.md) | | Settings helpers | `get_setting` / `set_setting`, namespaced to the plugin | | `get_provisioning_note` | Setup-wizard transparency note for extra tables | @@ -65,7 +67,7 @@ Known-unstable areas. Building on these means expecting rework. | Area | Status | Reference | |------|--------|-----------| -| Frontend hook contract | Not defined yet. There is no server-side hook for asset-detail panels, map markers, or search-result rendering. A plugin that needs custom UI still hand-edits the Vue frontend. This is the single biggest gap. [ADR-010](adr/ADR-010-frontend-plugin-hooks.md) proposes the path: data-only declarative hooks (`get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`) rendered by generic core components, with build-time glob discovery deferred for real components. | [ADR-010](adr/ADR-010-frontend-plugin-hooks.md) (PROPOSED) | +| Frontend renderers (residual) | The four data-only hooks and their `/api/pluginui/*` consumers are settled (0.7.0). The generic core renderers are landing incrementally: the settings-cards rail/landing renderer ships with 0.7.0; the asset-panel, map-overlay, and search-presentation renderers are wired opt-in per the ADR adoption plan. Real component-backed panels (bespoke charts, custom overlays) remain deferred to Option C (build-time glob discovery). | [ADR-010](adr/ADR-010-frontend-plugin-hooks.md) (ACCEPTED) | | Per-plugin migrations | Brand new. The per-plugin Alembic engine exists and every bundled plugin now carries a chain, but the pattern has one release of production mileage, not years. | [ADR-008](adr/ADR-008-plugin-migration-ownership.md) (2026-07-10) | | Pip distribution | Deferred to v2. External plugins install by clone / submodule / symlink; there is no entry-point discovery and no automatic update path yet. | [ADR-003](adr/ADR-003-plugin-distribution.md) | diff --git a/docs/PLUGIN-HOOKS.md b/docs/PLUGIN-HOOKS.md index a2611d6..995f758 100644 --- a/docs/PLUGIN-HOOKS.md +++ b/docs/PLUGIN-HOOKS.md @@ -9,7 +9,7 @@ The contract is locked in [ADR-001](../docs/adr/ADR-001-asset-as-platform-contra The framework declares its contract version in `shopdb/__init__.py`: ```python -__contract_version__ = '0.6.0' +__contract_version__ = '0.7.0' ``` Each plugin's `manifest.json` declares the range of contract versions it supports: @@ -216,6 +216,98 @@ Consumed by `GET /api/reports`, which merges plugin cards after the static core reports sorted into category groups by the frontend (disabled plugins are skipped; a broken plugin is isolated in prod, re-raised in dev/test). +### `get_settings_cards() -> List[Dict]` + +Returns settings-catalog card definitions. Added in contract 0.7.0 (ADR-010). +Each card is merged into the settings rail and landing overview without the +plugin hand-editing the core `settingsNav.js` catalog. `icon` is a string key +mapped to a Lucide component core-side, exactly like `get_navigation_items`. + +```python +class MeasuringToolsPlugin(BasePlugin): + def get_settings_cards(self): + return [{ + 'group': 'Measuring Tools', # rail group title (created if new) + 'to': '/settings/measuringtooltypes', + 'icon': 'ruler', # string key, mapped core-side + 'title': 'Measuring Tool Types', + 'description': 'Manage measuring-tool subtypes + map colors', + 'position': 22, # order within the group + }] +``` + +Consumed by `GET /api/pluginui/settings-cards`, which merges enabled plugins' +cards into the core catalog (disabled plugins are skipped; a broken plugin is +isolated in prod, re-raised in dev/test). + +### `get_asset_panels() -> List[Dict]` + +Returns asset-detail extension-panel definitions. Added in contract 0.7.0 +(ADR-010). A generic core `AssetPanel` component renders each panel on the +matching detail pages, fetching the panel's `endpoint`. This replaces +hand-composing a plugin panel component into each detail view. + +```python +class WarrantyPlugin(BasePlugin): + def get_asset_panels(self): + return [{ + 'id': 'warranty', + 'title': 'Warranty', + 'assettypes': ['*'], # detail pages it appears on; ['*'] = all + 'endpoint': '/api/warranty/asset/{assetid}', + 'render': 'table', # 'keyvalue' | 'table' | 'badge' + 'position': 30, + }] +``` + +Consumed by `GET /api/pluginui/asset-panels?assetid=`, which returns the +panels whose `assettypes` match that asset's type (disabled plugins skipped; +broken plugin isolated in prod, re-raised in dev/test). A panel that needs +bespoke UI (a chart) is out of scope for this data-only hook. + +### `get_map_overlays() -> List[Dict]` + +Returns shop-floor map overlay/decoration definitions. Added in contract 0.7.0 +(ADR-010). The map stays data-driven off asset types + positions; an overlay +adds decoration data (a badge or ring) plus an optional legend entry, with no +plugin-side map code. + +```python +class MeasuringToolsPlugin(BasePlugin): + def get_map_overlays(self): + return [{ + 'id': 'calibration-due', + 'label': 'Calibration due', # legend label + 'endpoint': '/api/measuringtools/map-overlay', # -> [{assetid, color, label}] + 'style': 'badge', # 'badge' | 'ring' + 'legend': True, + }] +``` + +Consumed by `GET /api/pluginui/map-overlays` (disabled plugins skipped; broken +plugin isolated in prod, re-raised in dev/test). + +### `get_asset_presentation() -> List[Dict]` + +Returns asset-type presentation/routing definitions. Added in contract 0.7.0 +(ADR-010). Declares how a plugin-owned asset type renders in global-search rows +and cross-links (which icon, which detail route), so core never hardcodes a +plugin's route or icon. + +```python +class MeasuringToolsPlugin(BasePlugin): + def get_asset_presentation(self): + return [{ + 'assettype': 'measuring_tool', # AssetType.assettype key the plugin owns + 'icon': 'ruler', + 'label': 'Measuring Tool', + 'route': '/measuringtools/{assetid}', + }] +``` + +Consumed by `GET /api/pluginui/asset-presentation` (disabled plugins skipped; +broken plugin isolated in prod, re-raised in dev/test). + ### `get_provisioning_note() -> Optional[Dict]` Transparency note the setup wizard shows the moment a site checks this plugin diff --git a/docs/PLUGIN-QUICKSTART.md b/docs/PLUGIN-QUICKSTART.md index f5d76ed..07e1a3a 100644 --- a/docs/PLUGIN-QUICKSTART.md +++ b/docs/PLUGIN-QUICKSTART.md @@ -122,6 +122,10 @@ Override hooks on the plugin class as needed. See [PLUGIN-HOOKS.md](PLUGIN-HOOKS | `get_navigation_items` | Plugin shows up in the sidebar nav | | `get_dashboard_widgets` | Plugin's dashboard widget appears on the home page | | `get_reports` | Plugin's report cards appear on the Reports hub | +| `get_settings_cards` | Plugin's card joins the settings rail + landing (no `settingsNav.js` edit) | +| `get_asset_panels` | Plugin panel renders on matching asset-detail pages | +| `get_map_overlays` | Plugin decorates shop-floor map markers + adds a legend entry | +| `get_asset_presentation` | Plugin declares its asset type's search icon + detail route | | `get_collector_schema` + `apply_collector_payload` | Plugin accepts external pushes at `/api/collector/` | Each hook has a default that does nothing. Override only what your plugin needs. diff --git a/docs/adr/ADR-010-frontend-plugin-hooks.md b/docs/adr/ADR-010-frontend-plugin-hooks.md index a8e60f3..70fd542 100644 --- a/docs/adr/ADR-010-frontend-plugin-hooks.md +++ b/docs/adr/ADR-010-frontend-plugin-hooks.md @@ -1,7 +1,8 @@ # ADR-010: Frontend plugin hook contract -- **Status:** PROPOSED +- **Status:** ACCEPTED - **Date:** 2026-07-11 +- **Accepted:** 2026-07-11 - **Deciders:** cproudlock - **Supersedes:** none @@ -112,7 +113,7 @@ not pursued. ## Decision -**PROPOSED:** adopt a hybrid. Add **data-only declarative hooks** (Option B) for +**DECISION:** adopt a hybrid. Add **data-only declarative hooks** (Option B) for the four presentation surfaces a generic core renderer can serve, and keep **file-convention glob discovery** (Option C) as the deferred mechanism for the residual cases where a real component is unavoidable. Do not pursue runtime diff --git a/docs/adr/README.md b/docs/adr/README.md index 610d41f..7b20fbe 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -22,7 +22,7 @@ Each ADR captures a single architectural decision: the context, the decision its | [007](ADR-007-product-versioning-and-releases.md) | Product versioning and releases | ACCEPTED | | [008](ADR-008-plugin-migration-ownership.md) | Plugin migration ownership (per-plugin chains) | ACCEPTED | | [009](ADR-009-frontend-plugin-gating.md) | Frontend plugin route gating | ACCEPTED | -| [010](ADR-010-frontend-plugin-hooks.md) | Frontend plugin hook contract | PROPOSED | +| [010](ADR-010-frontend-plugin-hooks.md) | Frontend plugin hook contract | ACCEPTED | | [011](ADR-011-machines-rename.md) | Machines rename + modeltypes retyping | ACCEPTED | ## Authoring diff --git a/frontend/src/composables/settingsCatalog.js b/frontend/src/composables/settingsCatalog.js new file mode 100644 index 0000000..81015ff --- /dev/null +++ b/frontend/src/composables/settingsCatalog.js @@ -0,0 +1,86 @@ +// Settings catalog: the static core groups (settingsNav.js) merged with the +// plugin-contributed cards from the ADR-010 get_settings_cards hook, served by +// GET /api/pluginui/settings-cards. Both SettingsLayout (rail) and SettingsIndex +// (landing) read the merged catalog so plugins add settings cards with no core +// edit. Fetched once into a module-level ref and shared across callers. +import { ref } from 'vue' +import { Ruler, Wrench, Cog, Package, Settings, Puzzle, SlidersHorizontal, Palette, Bell, Network, Printer, Droplets } from 'lucide-vue-next' +import api from '../api' +import { settingsGroups } from '../views/settings/settingsNav' + +// Icon string keys (as returned by the hook) mapped to Lucide components, +// same idea as the sidebar iconMap. Unknown keys fall back to a puzzle piece. +const iconMap = { + ruler: Ruler, + wrench: Wrench, + cog: Cog, + package: Package, + settings: Settings, + puzzle: Puzzle, + sliders: SlidersHorizontal, + palette: Palette, + bell: Bell, + network: Network, + printer: Printer, + droplets: Droplets, +} + +function resolveIcon(key) { + return iconMap[key] || Puzzle +} + +// Merge plugin cards into a fresh copy of the core groups. A card joins the +// group whose title matches its `group`; a new group is appended at the end. +function mergeCards(baseGroups, cards) { + const merged = baseGroups.map(group => ({ + title: group.title, + cards: [...group.cards], + })) + const byTitle = new Map(merged.map(group => [group.title, group])) + + for (const card of cards) { + const entry = { + to: card.to, + icon: resolveIcon(card.icon), + title: card.title, + description: card.description || '', + position: card.position ?? 99, + } + let group = byTitle.get(card.group) + if (!group) { + group = { title: card.group, cards: [] } + byTitle.set(card.group, group) + merged.push(group) + } + group.cards.push(entry) + } + + // Order plugin cards within a group by position; core cards keep their order. + for (const group of merged) { + group.cards.sort((a, b) => (a.position ?? 0) - (b.position ?? 0)) + } + return merged +} + +// Shared across component instances: seeded with the core groups, replaced with +// the merged catalog once the hook endpoint answers. +const groups = ref(settingsGroups) +let loaded = false + +async function loadCatalog() { + try { + const response = await api.get('/pluginui/settings-cards') + groups.value = mergeCards(settingsGroups, response.data.data || []) + } catch (error) { + // Degrade to the core-only catalog; the rail still works without plugins. + groups.value = settingsGroups + } +} + +export function useSettingsCatalog() { + if (!loaded) { + loaded = true + loadCatalog() + } + return { groups } +} diff --git a/frontend/src/views/settings/SettingsIndex.vue b/frontend/src/views/settings/SettingsIndex.vue index 1a66d94..d3d9c8e 100644 --- a/frontend/src/views/settings/SettingsIndex.vue +++ b/frontend/src/views/settings/SettingsIndex.vue @@ -25,7 +25,10 @@