Draft ADR-010 frontend hook contract; relabel Equipment to Machines
ADR-010 (PROPOSED): four data-only frontend hooks following the get_reports precedent - get_settings_cards, get_asset_panels, get_map_overlays, get_asset_presentation - batched as a future contract 0.7.0. Warranty proves asset panels first, measuringtools the rest; accept only after both run on the hooks. Display-label swap Equipment -> Machines across nav, list, form, detail, search, map editor, and settings copy (incl the API-failure fallback nav). Identifiers unchanged: plugin name, tables, /api/equipment, asset type, and routes all stay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,7 @@ Known-unstable areas. Building on these means expecting rework.
|
|||||||
|
|
||||||
| Area | Status | Reference |
|
| 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. | project charter / this doc |
|
| 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) |
|
||||||
| 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) |
|
| 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) |
|
| 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) |
|
||||||
|
|
||||||
|
|||||||
319
docs/adr/ADR-010-frontend-plugin-hooks.md
Normal file
319
docs/adr/ADR-010-frontend-plugin-hooks.md
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# ADR-010: Frontend plugin hook contract
|
||||||
|
|
||||||
|
- **Status:** PROPOSED
|
||||||
|
- **Date:** 2026-07-11
|
||||||
|
- **Deciders:** cproudlock
|
||||||
|
- **Supersedes:** none
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The backend plugin contract is settled (ADR-002 surface, ADR-006 collector,
|
||||||
|
ADR-008 migrations, ADR-009 route gating). The one undefined piece before 1.0 is
|
||||||
|
the frontend: how a plugin contributes UI without hand-editing core Vue files.
|
||||||
|
CONTRACT-STABILITY.md names this the single biggest churn item ("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").
|
||||||
|
|
||||||
|
The `measuringtools` plugin (bundled 2026-07-11, whose construction is narrated
|
||||||
|
in docs/PLUGIN-GUIDE.md) is the fresh evidence. Integrating it needed exactly
|
||||||
|
**two hand edits to core frontend files**:
|
||||||
|
|
||||||
|
1. `frontend/src/api/index.js` - an api-client block appended after
|
||||||
|
`warrantyApi` (PLUGIN-GUIDE.md section 9). The scaffolder now emits a
|
||||||
|
paste-in snippet, but it is still a hand edit to a shared, churn-heavy file.
|
||||||
|
2. `frontend/src/views/settings/settingsNav.js` - a card entry in the hardcoded
|
||||||
|
`settingsGroups` catalog (the "Measuring Tools" group, ~line 51), consumed by
|
||||||
|
`SettingsLayout` (left rail) and `SettingsIndex` (landing overview).
|
||||||
|
|
||||||
|
Everything else integrated with **zero** core edits, because a mechanism already
|
||||||
|
existed for it:
|
||||||
|
|
||||||
|
| Capability | Existing mechanism |
|
||||||
|
|---|---|
|
||||||
|
| Views / routes | Auto-discovery: `frontend/src/router/index.js` globs `router/routes/*.js` via `import.meta.glob`; a route starting `settings/` auto-nests under the settings shell. |
|
||||||
|
| Disabled-plugin gating | `meta: { plugin: '<name>' }` per ADR-009; router guard redirects when the backend plugin is off. |
|
||||||
|
| Sidebar / dashboard / reports | Declarative backend hooks rendered by core: `get_navigation_items`, `get_dashboard_widgets`, `get_reports` on `BasePlugin` (shopdb/plugins/base.py), merged by `shopdb/core/api/dashboard.py` and `shopdb/core/api/reports.py`. |
|
||||||
|
| Map placement | Data-driven off asset types + resolved positions (ADR-001); a typed, positioned asset appears on the map with its type color and no plugin-side map code. |
|
||||||
|
|
||||||
|
Capabilities a plugin **cannot have at all today** - no hook exists, and adding
|
||||||
|
one would mean forking a core view:
|
||||||
|
|
||||||
|
3. **Asset-detail extension panels.** A warranty-coverage section on a PC or
|
||||||
|
printer detail page, a calibration-status card on a measuring tool. Today
|
||||||
|
`WarrantyPanel.vue` is composed in by hand-editing each detail view.
|
||||||
|
4. **Map marker / overlay contributions.** A calibration-due badge on the
|
||||||
|
shop-floor map (`frontend/src/views/map/MapView.vue`). The map draws type
|
||||||
|
colors but has no plugin decoration path.
|
||||||
|
5. **Search-result rendering / routing for plugin asset types.** Global search
|
||||||
|
returns assets, but core hardcodes how each type renders and where its detail
|
||||||
|
link points; a plugin asset type has no way to declare its icon or route.
|
||||||
|
|
||||||
|
There is also an unsolved distribution wrinkle from ADR-003: external plugins
|
||||||
|
symlink into `plugins/<name>/` backend-only, but any frontend file they carry
|
||||||
|
must be physically copied into `frontend/src/`, because Vite compiles the tree
|
||||||
|
at build time and cannot reach outside it.
|
||||||
|
|
||||||
|
## Options considered
|
||||||
|
|
||||||
|
### A. Runtime dynamic component registration
|
||||||
|
|
||||||
|
Plugins ship real Vue components that core loads and mounts at runtime into
|
||||||
|
named extension points (an `iconMap` registration, a panel registry, a marker
|
||||||
|
renderer registry). This is the richest model and the one ADR-009's "Future
|
||||||
|
direction" sketched.
|
||||||
|
|
||||||
|
- Pro: a plugin can render anything; no core generic-renderer ceiling.
|
||||||
|
- Con: requires runtime module loading of plugin-authored code (dynamic import
|
||||||
|
of built chunks), a versioned shared-component surface that becomes an
|
||||||
|
accidental contract the moment it leaks, and it does nothing about the
|
||||||
|
build-time-only reach of Vite for external plugins. It is the most code for
|
||||||
|
the least near-term payoff. ADR-009 already priced this and deferred it.
|
||||||
|
|
||||||
|
### B. Declarative data-only hooks rendered by core generic components
|
||||||
|
|
||||||
|
Plugins return plain dicts from new `BasePlugin` hooks; a generic core component
|
||||||
|
renders them. This is the exact precedent already proven three times:
|
||||||
|
`get_navigation_items`, `get_dashboard_widgets`, and `get_reports` (0.6.0) all
|
||||||
|
return dicts that a core consumer endpoint merges and a core Vue component
|
||||||
|
renders. No plugin ships frontend code.
|
||||||
|
|
||||||
|
- Pro: additive, minor-bump changes under ADR-002; identical access pattern to
|
||||||
|
the existing consumers (skip disabled, fail-loud in dev, isolate in prod);
|
||||||
|
works untouched for external symlink-only plugins, because the data crosses
|
||||||
|
the wire and core owns the renderer.
|
||||||
|
- Con: bounded to what a generic renderer can draw. A panel that needs a bespoke
|
||||||
|
chart or a custom-interaction map overlay does not fit.
|
||||||
|
|
||||||
|
### C. Build-time file-convention discovery
|
||||||
|
|
||||||
|
Extend the `import.meta.glob` precedent from `./routes/*.js` to a plugin-owned
|
||||||
|
frontend tree (`plugins/*/frontend/`), so a plugin's real components (full
|
||||||
|
list/detail/form views, its api-client module) live with the plugin and the
|
||||||
|
build picks them up by convention. This is ADR-009 "Future direction" steps 1-2.
|
||||||
|
|
||||||
|
- Pro: true self-containment for genuine components; the natural home for the
|
||||||
|
api-client block (friction 1) and full views.
|
||||||
|
- Con: significant build-system work (per-plugin Vite entry discovery,
|
||||||
|
code-splitting, dev-server HMR across the tree) and it does not by itself
|
||||||
|
solve the external-plugin wrinkle - a symlinked out-of-tree `frontend/` is
|
||||||
|
still outside Vite's compiled root. Only pays off once out-of-tree plugins
|
||||||
|
with frontends are a real requirement.
|
||||||
|
|
||||||
|
### Hybrid
|
||||||
|
|
||||||
|
The evidence splits cleanly. The five friction points fall into two buckets:
|
||||||
|
surfaces where a generic renderer fed by plugin data is sufficient (2, 3, 4, 5),
|
||||||
|
and surfaces where a real component is genuinely unavoidable (full views, and
|
||||||
|
the api-client module in friction 1). Option B fits the first bucket exactly and
|
||||||
|
is cheap and additive. Option C is the right long-term answer for the second but
|
||||||
|
is expensive and, per ADR-009, gated on external-plugin demand. Option A buys
|
||||||
|
nothing B does not, at the highest cost. The decision is B now, C deferred, A
|
||||||
|
not pursued.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**PROPOSED:** 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
|
||||||
|
component registration (Option A).
|
||||||
|
|
||||||
|
### New hooks (Option B, data-only, additive)
|
||||||
|
|
||||||
|
Each is a new optional `BasePlugin` method returning a list of dicts, merged by
|
||||||
|
a core consumer endpoint using the same pattern as `get_reports` (inject
|
||||||
|
`plugin` name, skip disabled, re-raise in dev/test, log-and-isolate in prod),
|
||||||
|
and rendered by a generic core component. Icon values are string keys mapped to
|
||||||
|
Lucide components core-side, exactly like `get_navigation_items`.
|
||||||
|
|
||||||
|
**`get_settings_cards`** - resolves friction point 2 (settingsNav.js hand edit).
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Consumer: a new `GET /api/settings/cards` merges enabled plugins' cards into the
|
||||||
|
core `settingsGroups` catalog; `SettingsLayout` and `SettingsIndex` read the
|
||||||
|
merged catalog instead of the hardcoded JS array. The catalog's own core groups
|
||||||
|
stay in `settingsNav.js`; plugin groups are appended.
|
||||||
|
|
||||||
|
**`get_asset_panels`** - resolves friction point 3 (asset-detail panels).
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'id': 'calibration',
|
||||||
|
'title': 'Calibration',
|
||||||
|
'assettypes': ['measuring_tool'], # detail pages it appears on; ['*'] = all
|
||||||
|
'endpoint': '/api/measuringtools/{assetid}/calibration-panel',
|
||||||
|
'render': 'keyvalue', # 'keyvalue' | 'table' | 'badge'
|
||||||
|
'position': 20,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Consumer: `GET /api/assets/{assetid}/panels` returns the panels whose
|
||||||
|
`assettypes` match that asset's type; a generic `AssetPanel` component on the
|
||||||
|
detail page fetches each `endpoint` and renders it in the declared style. This
|
||||||
|
covers the warranty-coverage and calibration-status cases. A panel that needs
|
||||||
|
bespoke UI (a chart) is out of scope for the data-only hook and falls to the
|
||||||
|
deferred component mechanism below - stated honestly, not hidden.
|
||||||
|
|
||||||
|
**`get_map_overlays`** - resolves friction point 4 (map marker/overlay badges).
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'id': 'calibration-due',
|
||||||
|
'label': 'Calibration due', # legend label
|
||||||
|
'endpoint': '/api/measuringtools/map-overlay', # -> [{assetid, color, label}]
|
||||||
|
'style': 'badge', # 'badge' | 'ring'
|
||||||
|
'legend': True,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Consumer: `MapView.vue` fetches enabled plugins' overlay endpoints and decorates
|
||||||
|
the already-placed markers; legend entries append to the existing legend. The
|
||||||
|
map stays data-driven; plugins add decoration data, not map code.
|
||||||
|
|
||||||
|
**`get_asset_presentation`** - resolves friction point 5 (search rendering /
|
||||||
|
routing) and, as a bonus, removes the `AppLayout.vue` iconMap hand edit called
|
||||||
|
out in PLUGIN-GUIDE.md section 9.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'assettype': 'measuring_tool', # AssetType.assettype key the plugin owns
|
||||||
|
'icon': 'ruler',
|
||||||
|
'label': 'Measuring Tool',
|
||||||
|
'route': '/measuringtools/{assetid}', # detail-route pattern
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Consumer: `GET /api/assets/presentation` returns the type-to-presentation map;
|
||||||
|
global-search result rows and any asset cross-link use it to pick the icon and
|
||||||
|
build the detail link, so core never hardcodes a plugin's route or icon.
|
||||||
|
|
||||||
|
### Friction map
|
||||||
|
|
||||||
|
| Friction point | Mechanism | Hook / change |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. api-client block in api/index.js | C (deferred) | plugin-owned `frontend/` tree, glob-discovered; scaffolder snippet is the near-term mitigation |
|
||||||
|
| 2. settingsNav.js card entry | B (now) | `get_settings_cards` |
|
||||||
|
| 3. asset-detail panels | B (now) | `get_asset_panels` (bespoke panels -> C, deferred) |
|
||||||
|
| 4. map markers / overlays | B (now) | `get_map_overlays` (bespoke overlays -> C, deferred) |
|
||||||
|
| 5. search-result rendering / routing | B (now) | `get_asset_presentation` |
|
||||||
|
| views (full list/detail/form) | C (deferred) | already glob-discovered under core `routes/*.js`; long-term move to `plugins/*/frontend/` per ADR-009 |
|
||||||
|
|
||||||
|
### Deferred: file-convention frontend tree (Option C)
|
||||||
|
|
||||||
|
Extending `import.meta.glob` from `./routes/*.js` to `plugins/*/frontend/` (so
|
||||||
|
views and the api-client module live with the plugin) is ADR-009 "Future
|
||||||
|
direction" steps 1-2. It is deferred for the same reasons ADR-009 gave: it is
|
||||||
|
build-system-heavy and the payoff only lands once out-of-tree plugins with their
|
||||||
|
own frontends are a real requirement. Until then, friction 1 stays mitigated by
|
||||||
|
the scaffolder snippet, and full views keep shipping in the core bundle and
|
||||||
|
gated by ADR-009 `meta.plugin`.
|
||||||
|
|
||||||
|
This is also the honest limit on the external-plugin wrinkle. The four data-only
|
||||||
|
hooks need **zero** frontend files from a plugin, so an external symlink-only
|
||||||
|
(backend) plugin gets settings cards, detail panels, map overlays, and search
|
||||||
|
presentation with no copy-into-`frontend/src/` step at all. The wrinkle survives
|
||||||
|
only for the residual component-backed cases, which is exactly the deferred
|
||||||
|
Option C work; pushing external-repo frontend distribution to that later ADR
|
||||||
|
matches ADR-003's posture of deferring out-of-tree packaging until two sites run
|
||||||
|
their own plugins.
|
||||||
|
|
||||||
|
### Contract-version impact (ADR-002)
|
||||||
|
|
||||||
|
Each of the four hooks is a new optional `BasePlugin` method - an additive
|
||||||
|
change, so a **minor** bump per ADR-002, the same classification `get_reports`
|
||||||
|
took at 0.6.0. Landing all four together is a single minor bump (proposed
|
||||||
|
**0.7.0**); landing them incrementally is one minor bump each. Adding the core
|
||||||
|
consumer endpoints and the generic renderer components is core-internal and does
|
||||||
|
not itself move `__contract_version__`. The deferred Option C introduces a
|
||||||
|
separate, versioned **frontend** contract (ADR-009 step 4), tracked apart from
|
||||||
|
the backend `__contract_version__`; it is not part of this proposal's bumps.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Closes the CONTRACT-STABILITY.md "single biggest gap" for the four surfaces a
|
||||||
|
generic renderer can serve, using the already-proven declarative pattern - low
|
||||||
|
risk, low cost, additive-only.
|
||||||
|
- External symlink-only plugins get four presentation surfaces with no
|
||||||
|
copy-into-core step, shrinking (not yet eliminating) the ADR-003 wrinkle.
|
||||||
|
- Removes three of the two-plus hand edits the exemplar needed (settingsNav
|
||||||
|
card, plus the AppLayout iconMap edit from section 9), moving them to data.
|
||||||
|
- Docs-drift guard forces documentation: `tests/test_docs_contract.py` fails if
|
||||||
|
any new public `BasePlugin` hook is missing from `docs/PLUGIN-HOOKS.md`, so the
|
||||||
|
hooks cannot ship undocumented.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- Four more hooks and four more core consumer endpoints to maintain, each with
|
||||||
|
the skip-disabled / fail-loud-in-dev / isolate-in-prod discipline.
|
||||||
|
- The generic renderers set a ceiling: bespoke panels and overlays still have no
|
||||||
|
home until Option C lands, so the contract is honest-but-partial, not total.
|
||||||
|
- One extra request per surface on the pages that use it (a detail page fetches
|
||||||
|
its panels; the map fetches overlays), on top of ADR-009's enabled-list fetch.
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
|
||||||
|
- Bundled plugins keep their current in-core views; migrating them to the hooks
|
||||||
|
is opt-in and incremental (see Adoption plan), not a flag-day rewrite.
|
||||||
|
- `meta.plugin` route gating (ADR-009) is unchanged and still gates the views;
|
||||||
|
these hooks add presentation, not routing.
|
||||||
|
- Contract tests (`tests/test_plugin_contract.py`) already assert every public
|
||||||
|
hook is exercised; the new hooks slot into that harness.
|
||||||
|
|
||||||
|
## Adoption plan
|
||||||
|
|
||||||
|
Prove the contract on bundled plugins before declaring it settled for sister
|
||||||
|
sites.
|
||||||
|
|
||||||
|
1. **`get_asset_panels` first, via `warranty`.** Warranty is asset-general and
|
||||||
|
already composes `WarrantyPanel.vue` onto multiple detail pages by hand, with
|
||||||
|
a clean per-asset endpoint behind it. Migrating it to a declarative
|
||||||
|
`get_asset_panels` entry rendered by the generic `AssetPanel` is the
|
||||||
|
lowest-risk proof and immediately removes hand edits from every detail view
|
||||||
|
that shows warranty. This is the recommended first migration.
|
||||||
|
2. **`get_settings_cards`, `get_map_overlays`, `get_asset_presentation` via
|
||||||
|
`measuringtools`.** The guide exemplar already needs a settings card, a
|
||||||
|
calibration-due map badge, and search routing for its `measuring_tool` type,
|
||||||
|
so it exercises all three at once and its PLUGIN-GUIDE.md walkthrough becomes
|
||||||
|
the reference for the new hooks.
|
||||||
|
3. Only after both plugins run on the hooks in a real build: bump
|
||||||
|
`__contract_version__` to 0.7.0, document the hooks in
|
||||||
|
`docs/PLUGIN-HOOKS.md`, and mark this ADR ACCEPTED.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should `get_asset_panels` `render` styles stay a small closed set
|
||||||
|
(`keyvalue` / `table` / `badge`), or grow? A closed set keeps the renderer
|
||||||
|
generic; growth pressure is the signal that a case actually needs Option C.
|
||||||
|
- Should the four consumer endpoints collapse into one bundled
|
||||||
|
`GET /api/plugins/frontend-contributions` call to save round-trips, or stay
|
||||||
|
separate per surface for cache locality? Defer until the per-surface request
|
||||||
|
cost is measured.
|
||||||
|
- When Option C lands, does the api-client module move under
|
||||||
|
`plugins/*/frontend/` or get replaced entirely by a generated client from the
|
||||||
|
backend blueprint? Out of scope here; belongs to the ADR-009 frontend-contract
|
||||||
|
follow-up.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ADR-001 (asset model as the map/search data source)
|
||||||
|
- ADR-002 (bump classification for the new hooks)
|
||||||
|
- ADR-003 (external-plugin distribution posture)
|
||||||
|
- ADR-009 (route gating; "Future direction" is the deferred Option C)
|
||||||
|
- docs/PLUGIN-GUIDE.md sections 9-10 (the measuringtools frontend hand edits)
|
||||||
|
- docs/CONTRACT-STABILITY.md (the expected-churn line this ADR answers)
|
||||||
|
- shopdb/plugins/base.py (existing declarative hooks this pattern extends)
|
||||||
|
- shopdb/core/api/dashboard.py, shopdb/core/api/reports.py (consumer precedent)
|
||||||
|
</content>
|
||||||
|
</invoke>
|
||||||
@@ -22,6 +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 |
|
| [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 |
|
| [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 |
|
| [009](ADR-009-frontend-plugin-gating.md) | Frontend plugin route gating | ACCEPTED |
|
||||||
|
| [010](ADR-010-frontend-plugin-hooks.md) | Frontend plugin hook contract | PROPOSED |
|
||||||
|
|
||||||
## Authoring
|
## Authoring
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const defaultNav = [
|
|||||||
{ name: 'Notifications', icon: 'bell', route: '/notifications', position: 5 },
|
{ name: 'Notifications', icon: 'bell', route: '/notifications', position: 5 },
|
||||||
{ name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6 },
|
{ name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6 },
|
||||||
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
|
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
|
||||||
{ name: 'Equipment', icon: 'cog', route: '/machines', position: 10 },
|
{ name: 'Machines', icon: 'cog', route: '/machines', position: 10 },
|
||||||
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
|
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
|
||||||
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
|
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
|
||||||
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
|
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<h3>Assets</h3>
|
<h3>Assets</h3>
|
||||||
<select v-model="filterType" class="form-control">
|
<select v-model="filterType" class="form-control">
|
||||||
<option value="">All Types</option>
|
<option value="">All Types</option>
|
||||||
<option value="equipment">Equipment</option>
|
<option value="equipment">Machines</option>
|
||||||
<option value="computer">Computers</option>
|
<option value="computer">Computers</option>
|
||||||
<option value="printer">Printers</option>
|
<option value="printer">Printers</option>
|
||||||
<option value="network_device">Network Devices</option>
|
<option value="network_device">Network Devices</option>
|
||||||
|
|||||||
@@ -109,8 +109,8 @@ const highlightId = ref(null)
|
|||||||
const copiedId = ref(null)
|
const copiedId = ref(null)
|
||||||
|
|
||||||
const typeLabels = {
|
const typeLabels = {
|
||||||
machine: 'Equipment',
|
machine: 'Machine',
|
||||||
equipment: 'Equipment',
|
equipment: 'Machine',
|
||||||
pc: 'PC',
|
pc: 'PC',
|
||||||
computer: 'PC',
|
computer: 'PC',
|
||||||
application: 'App',
|
application: 'App',
|
||||||
@@ -136,7 +136,7 @@ const filterTypeMap = {
|
|||||||
|
|
||||||
const filterList = [
|
const filterList = [
|
||||||
{ key: 'all', label: 'All' },
|
{ key: 'all', label: 'All' },
|
||||||
{ key: 'equipment', label: 'Equipment' },
|
{ key: 'equipment', label: 'Machines' },
|
||||||
{ key: 'computers', label: 'PCs' },
|
{ key: 'computers', label: 'PCs' },
|
||||||
{ key: 'printers', label: 'Printers' },
|
{ key: 'printers', label: 'Printers' },
|
||||||
{ key: 'network', label: 'Network' },
|
{ key: 'network', label: 'Network' },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="detail-page">
|
<div class="detail-page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Equipment Details</h2>
|
<h2>Machine Details</h2>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<router-link :to="`/print/equipment-badge/${equipment?.equipment?.equipmentid}`" class="btn btn-secondary" v-if="equipment" target="_blank">
|
<router-link :to="`/print/equipment-badge/${equipment?.equipment?.equipmentid}`" class="btn btn-secondary" v-if="equipment" target="_blank">
|
||||||
Print Badge
|
Print Badge
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="badge badge-lg badge-primary">
|
<span class="badge badge-lg badge-primary">
|
||||||
{{ equipment.assettypename || 'Equipment' }}
|
{{ equipment.assettypename || 'Machine' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
|
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
|
||||||
{{ equipment.statusname || 'Unknown' }}
|
{{ equipment.statusname || 'Unknown' }}
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="card">
|
<div v-else class="card">
|
||||||
<p style="text-align: center; color: var(--text-light);">Equipment not found</p>
|
<p style="text-align: center; color: var(--text-light);">Machine not found</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>{{ isEdit ? 'Edit Equipment' : 'New Equipment' }}</h2>
|
<h2>{{ isEdit ? 'Edit Machine' : 'New Machine' }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Equipment Hardware Section -->
|
<!-- Equipment Hardware Section -->
|
||||||
<h3 class="form-section-title">Equipment Hardware</h3>
|
<h3 class="form-section-title">Machine Hardware</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="equipmenttypeid">Equipment Type</label>
|
<label for="equipmenttypeid">Equipment Type</label>
|
||||||
@@ -335,7 +335,7 @@
|
|||||||
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
||||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||||
{{ saving ? 'Saving...' : 'Save Equipment' }}
|
{{ saving ? 'Saving...' : 'Save Machine' }}
|
||||||
</button>
|
</button>
|
||||||
<router-link to="/machines" class="btn btn-secondary">Cancel</router-link>
|
<router-link to="/machines" class="btn btn-secondary">Cancel</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Equipment</h2>
|
<h2>Machines</h2>
|
||||||
<router-link to="/machines/new" class="btn btn-primary">Add Equipment</router-link>
|
<router-link to="/machines/new" class="btn btn-primary">Add Machine</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
v-model="search"
|
v-model="search"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Search equipment..."
|
placeholder="Search machines..."
|
||||||
@input="debouncedSearch"
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ export const settingsGroups = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Equipment',
|
title: 'Machines',
|
||||||
cards: [
|
cards: [
|
||||||
{ to: '/settings/equipmenttypes', icon: Wrench, title: 'Equipment Types', description: 'Manage equipment subtypes + map colors' },
|
{ to: '/settings/equipmenttypes', icon: Wrench, title: 'Equipment Types', description: 'Manage machine subtypes + map colors' },
|
||||||
{ to: '/settings/machinetypes', icon: Monitor, title: 'Machine Types', description: 'Manage machine type categories' },
|
{ to: '/settings/machinetypes', icon: Monitor, title: 'Machine Types', description: 'Manage machine type categories' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
"""Return dashboard widget definitions."""
|
"""Return dashboard widget definitions."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'name': 'Equipment Status',
|
'name': 'Machine Status',
|
||||||
'component': 'EquipmentStatusWidget',
|
'component': 'EquipmentStatusWidget',
|
||||||
'endpoint': '/api/equipment/dashboard/summary',
|
'endpoint': '/api/equipment/dashboard/summary',
|
||||||
'size': 'medium',
|
'size': 'medium',
|
||||||
@@ -211,7 +211,7 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
"""Return navigation menu items."""
|
"""Return navigation menu items."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'name': 'Equipment',
|
'name': 'Machines',
|
||||||
'icon': 'cog',
|
'icon': 'cog',
|
||||||
'route': '/machines',
|
'route': '/machines',
|
||||||
'position': 10,
|
'position': 10,
|
||||||
|
|||||||
Reference in New Issue
Block a user