Ship plugin framework shore-up: frontend scaffold, sister-site adoption kit
All checks were successful
CI / backend (push) Successful in 24s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

- flask plugin new now scaffolds the frontend too: List/Detail/Form
  views on the global styles, a gated route module (ADR-009), and an
  api-client snippet emitted into the plugin dir. Views are written
  before the route file so a partially generated plugin cannot 500 the
  dev server.
- docs/PLUGIN-EXTERNAL-REPO.md + scripts/test-external-plugin.sh: how a
  sister site develops a plugin in its own repo and runs the framework
  contract tests in CI against a pinned framework ref (script verified
  to fail on a broken core_version pin).
- docs/CONTRACT-STABILITY.md: settled vs churning contract surface and
  the provisional 1.0 criteria.
- CLAUDE.md active-state refresh (contract 0.6.0, 11 plugins, 340
  tests, measuringtools done).

Known limitation documented: Path.rglob does not descend symlinks, so
the import-surface contract test skips symlinked external plugins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-11 10:30:03 -04:00
parent 94f852a1c8
commit 529b9f2fed
13 changed files with 1547 additions and 14 deletions

112
docs/CONTRACT-STABILITY.md Normal file
View File

@@ -0,0 +1,112 @@
# Path to contract 1.0
This page is an honest read of how stable the plugin contract is today, for a
sister site deciding how much to build on it. It is derived from the ADRs and
the live code, not aspiration. The authoritative hook reference is
[PLUGIN-HOOKS.md](PLUGIN-HOOKS.md); the versioning rules are in
[ADR-002](adr/ADR-002-plugin-versioning.md).
## Current version
The plugin contract is at **0.6.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.
The product release version (`__version__`, currently 0.5.0) is a separate
series with its own bump rules; see [ADR-007](adr/ADR-007-product-versioning-and-releases.md).
Do not pin against it for compatibility - pin against `__contract_version__`.
### 0.x history
Recorded in the comment block in `shopdb/__init__.py`:
| Version | Change | Kind |
|---------|--------|------|
| 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) |
The source comment block documents 0.3.0, 0.4.0, and 0.6.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.
## Settled surface
These are unlikely to break before 1.0. A plugin can depend on them with
reasonable confidence; a breaking change to any would be a major bump and would
land with a new or amended ADR.
| Surface | What it is |
|---------|-----------|
| `meta` / manifest schema | `PluginMeta` fields and `manifest.json` (ADR-002 single source of truth) |
| `get_blueprint` | Flask Blueprint registration at the manifest `api_prefix` |
| `get_models` | SQLAlchemy model classes the plugin owns |
| `init_app` | Custom init after blueprint + models are known |
| `get_cli_commands` | Click commands added to the Flask CLI |
| `get_services` | Named service classes, consumed by `plugin_manager.get_service` |
| Lifecycle hooks | `on_install`, `on_uninstall`, `on_enable`, `on_disable` |
| `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) |
| 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 |
| `get_config_schema` | Setup-wizard config field declarations |
| `shopdb.api` import surface | The only core module plugins may import (plus `shopdb.plugins.base`); adding a name is minor, removing one is major |
The asset model itself - `Asset`, `AssetType`, `AssetStatus`,
`AssetRelationship`, and the shared reference models - is the platform contract
locked in [ADR-001](adr/ADR-001-asset-as-platform-contract.md).
## Expected churn before 1.0
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. | project charter / this doc |
| 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) |
## Bump rules
From [ADR-002](adr/ADR-002-plugin-versioning.md), applied to `__contract_version__`:
| Bump | Trigger |
|------|---------|
| major | Breaking change to the `BasePlugin` ABC, the `PluginMeta` schema, or any model in the platform contract (`Asset`, `AssetType`, `AssetStatus`, `AssetRelationship`, `Vendor`, `Location`, `BusinessUnit`, `Model`, `OperatingSystem`). Removing a name from `shopdb.api` is major. |
| minor | Additive change: a new optional hook, a new field on a contract model with a default, a new name added to `shopdb.api`. |
| patch | Bug fix with no change to the contract surface. |
### Deprecation policy
ADR-002 defines the bump classification above but is **silent** on any
deprecation window or notice period for pre-1.0 removals. In practice a removal
is simply a major (or, pre-1.0, a breaking minor) bump: the hook or name is
gone, and the loader fails loud in dev or excludes the mismatched plugin in prod
(see the load-time table in [PLUGIN-EXTERNAL-REPO.md](PLUGIN-EXTERNAL-REPO.md)).
Because the ADR says nothing about a grace period, this document proposes none;
the honest guidance for sister sites is the mitigation that already exists - pin
a tight `core_version` range and re-test before widening it.
## Criteria for declaring 1.0
Provisional. This is the maintainer's working list, not a committed checklist,
and it will move. ADR-002's own open question ("When does the framework declare
1.0.0?") ties 1.0 to the `Machine` retirement from ADR-001 and the framework
being "ready for sister sites"; the items below expand that intent.
- Frontend hook contract defined via its own ADR (asset-detail panels, map
markers, search results), closing the biggest churn item above.
- `Machine` / legacy-model retirement complete per ADR-001, so the asset model
is the only contract.
- At least two external plugins running in production at a second site - the
bar ADR-003 already sets before pip distribution and sister-site readiness
are considered justified.
- The per-plugin migration pattern (ADR-008) proven across real upgrades, not
just fresh installs.
- No contract bump needed for N consecutive product releases (N to be fixed
when the list is firmed up), showing the surface has actually settled.
Until then: pre-1.0, pin tight, re-test on every framework bump.

View File

@@ -0,0 +1,340 @@
# Developing a plugin in its own repo
This guide is for a sister GE Aerospace site (or any third party) building a
shopdb-flask plugin in a git repo it owns, outside the framework tree. It is
the "external plugin" path in [ADR-003](adr/ADR-003-plugin-distribution.md):
the framework ships a bundled set, and you drop your own plugin into
`<framework>/plugins/<name>/` by clone, submodule, or symlink. No pip packaging
is required for v1 (pip distribution is deferred to v2 per ADR-003).
If you have not written a plugin before, start with
[PLUGIN-QUICKSTART.md](PLUGIN-QUICKSTART.md) and the hook reference in
[PLUGIN-HOOKS.md](PLUGIN-HOOKS.md). This document only covers the parts that are
different when the plugin lives in its own repo.
## Recommended repo layout
Your repo root holds the plugin directory contents directly, so the whole repo
can be dropped in at `<framework>/plugins/<name>/`. A plugin named `shipping`
in a repo named `wjsf-shipping` looks like this:
```
wjsf-shipping/ # your git repo root == the plugin directory
manifest.json # required: name, version, description, core_version
plugin.py # required: your BasePlugin subclass
__init__.py
models/
__init__.py # exports every model
shipping.py
api/
__init__.py
routes.py # Flask Blueprint returned by get_blueprint()
migrations/ # per-plugin Alembic chain (ADR-008), if you own tables
env.py
script.py.mako
versions/
0001_shipping_baseline.py
tests/ # your own tests (the CI harness runs these)
test_shipping.py
README.md # what it tracks, who maintains it, where to file issues
```
Notes:
- `manifest.json` is the single source of truth for metadata (ADR-002). The
`name` field must match the directory name the site installs it under and
follows the framework naming convention (lowercase concatenated, no
underscores or dashes). Prefix site-specific names with the site code when a
collision across sites is possible, e.g. `wjsf-shipping`
(see [PLUGINS.md](PLUGINS.md) naming policy and ADR-003).
- `migrations/` is only needed if your plugin owns tables. A plugin built
outside the tree never had its tables created by the framework's core chain,
so its `0001` is a REAL baseline that creates them, not a stamp-only anchor.
This is the same "baseline vs anchor" distinction ADR-008 draws for
post-cutover plugins; see [ADR-008](adr/ADR-008-plugin-migration-ownership.md).
- Import core code ONLY through `shopdb.api` (plus `shopdb.plugins.base` for the
ABC). Deep imports of `shopdb.core.*`, `shopdb.extensions`, or `shopdb.utils.*`
are contract violations. See PLUGIN-HOOKS.md for the exposed surface.
## Local dev workflow
Symlinking lets you edit in your own repo while the framework loads the plugin
live. The loader discovers a symlinked directory the same as a real one.
```bash
# 1. Clone the framework and your plugin repo side by side.
git clone https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git
git clone https://gitea.proudtech.net/wjsf/wjsf-shipping.git
# 2. Symlink your repo into the framework's plugins/ directory.
# The link name is the plugin name from your manifest.json.
cd shopdb-flask
ln -s ../../wjsf-shipping plugins/shipping
# (use an absolute path if you prefer: ln -s "$(pwd)/../wjsf-shipping" plugins/shipping)
# 3. Set up the framework as usual.
python3 -m venv venv
venv/bin/pip install -r requirements.txt
# 4. Install (enable) your plugin.
venv/bin/flask plugin install shipping
# 5. If your plugin owns tables, apply its migrations.
venv/bin/flask db upgrade # core chain
venv/bin/flask plugin upgrade-all # plugin chains, including yours
# 6. Run tests.
venv/bin/python -m pytest tests/test_plugin_contract.py
venv/bin/python -m pytest ../wjsf-shipping/tests
```
Edits in `../wjsf-shipping` are picked up on the next framework restart, because
the symlink points back at your working tree.
## core_version pinning for sister sites
The framework is pre-1.0. Under semver, any 0.x minor bump is allowed to break
the contract, and this project uses that latitude (see the history in
[CONTRACT-STABILITY.md](CONTRACT-STABILITY.md)). So pin a TIGHT range that
admits only the contract minor you tested against, not the whole 0.x line.
The current contract version is declared in `shopdb/__init__.py`:
```python
__contract_version__ = '0.6.0'
```
Recommended pin in your `manifest.json`, per ADR-002 (pip-style `>=,<` ranges):
```json
{
"name": "shipping",
"version": "1.0.0",
"description": "Tracks shipping-station scanners and label printers",
"core_version": ">=0.6.0,<0.7.0",
"dependencies": []
}
```
Do NOT pin the loose `>=0.2.0,<1.0.0` default that `PluginMeta` falls back to.
That default exists so bundled plugins keep loading across minor bumps; an
external plugin should be deliberate and re-test before widening its range.
What happens at load time on a mismatch (ADR-002):
| Environment | Behavior on core_version mismatch |
|-------------|-----------------------------------|
| dev / test | The loader re-raises. Startup fails loud so you notice immediately. |
| production | The loader logs an error, marks the plugin incompatible, and excludes it from registration. The rest of the app still starts. |
When you move a site to a newer framework, bump your `core_version` upper bound
only after the harness below passes against the new ref.
## CI recipe
`scripts/test-external-plugin.sh` (in the framework repo) stands up a throwaway
framework at a pinned ref, drops your plugin in as a symlink, and runs the
framework contract tests plus your own `tests/`. It has two modes:
- **CI / remote** (default): clones the framework at `FRAMEWORK_REF` and builds
a fresh venv. Needs network access to git and PyPI.
- **Local / offline**: set `LOCAL_FRAMEWORK` to a framework checkout on disk.
The script exports that checkout at HEAD with `git archive` and reuses its
existing venv, so it runs with no internet. Useful for air-gapped verification.
```bash
# CI: test the plugin in the current repo against a pinned tag
PLUGIN_DIR=. FRAMEWORK_REF=v0.5.0 scripts/test-external-plugin.sh
# Offline: test against a framework checkout already on disk
LOCAL_FRAMEWORK=/opt/shopdb-flask PLUGIN_DIR=. scripts/test-external-plugin.sh
```
The full script:
```bash
#!/usr/bin/env bash
#
# test-external-plugin.sh
#
# Verify an out-of-tree shopdb-flask plugin against a pinned framework build.
# Stand up a throwaway copy of the framework, drop the plugin into
# plugins/<name>/ as a symlink (the mechanism ADR-003 documents for sister
# sites), and run the framework contract tests plus the plugin's own tests.
# Nonzero exit means the plugin is not compatible with that framework ref.
#
# Two modes:
#
# CI / remote (default): clone the framework at FRAMEWORK_REF from
# FRAMEWORK_URL, build a fresh venv, pip install requirements. Needs
# network access to git and PyPI.
#
# Local / offline: set LOCAL_FRAMEWORK to a framework checkout on disk.
# The script exports that checkout at HEAD with `git archive` (no network)
# and reuses the checkout's existing venv, so it runs with no internet.
#
# Inputs (env var, or positional):
# PLUGIN_DIR ($1) path to the plugin directory (holds manifest.json). Required.
# FRAMEWORK_REF ($2) git ref to test against in CI mode. Default: main.
# FRAMEWORK_URL framework git URL for CI mode.
# Default: https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git
# LOCAL_FRAMEWORK path to an existing framework checkout. Set it to run offline.
set -eu
PLUGIN_DIR="${PLUGIN_DIR:-${1:-}}"
FRAMEWORK_REF="${FRAMEWORK_REF:-${2:-main}}"
FRAMEWORK_URL="${FRAMEWORK_URL:-https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git}"
LOCAL_FRAMEWORK="${LOCAL_FRAMEWORK:-}"
if [ -z "$PLUGIN_DIR" ]; then
echo "ERROR: PLUGIN_DIR is required (env var or first argument)." >&2
exit 2
fi
if [ ! -f "$PLUGIN_DIR/manifest.json" ]; then
echo "ERROR: $PLUGIN_DIR has no manifest.json - not a plugin directory." >&2
exit 2
fi
PLUGIN_ABS="$(cd "$PLUGIN_DIR" && pwd)"
PLUGIN_NAME="$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PLUGIN_ABS/manifest.json" | head -n1)"
if [ -z "$PLUGIN_NAME" ]; then
PLUGIN_NAME="$(basename "$PLUGIN_ABS")"
fi
WORKDIR="$(mktemp -d)"
cleanup() { rm -rf "$WORKDIR"; }
trap cleanup EXIT
FRAMEWORK="$WORKDIR/framework"
mkdir -p "$FRAMEWORK"
if [ -n "$LOCAL_FRAMEWORK" ]; then
echo "==> Local mode: exporting framework from $LOCAL_FRAMEWORK (HEAD)"
LOCAL_ABS="$(cd "$LOCAL_FRAMEWORK" && pwd)"
git -C "$LOCAL_ABS" archive HEAD | tar -x -C "$FRAMEWORK"
if [ -x "$LOCAL_ABS/venv/bin/python" ]; then
PYTHON="$LOCAL_ABS/venv/bin/python"
elif [ -x "$LOCAL_ABS/.venv/bin/python" ]; then
PYTHON="$LOCAL_ABS/.venv/bin/python"
else
echo "ERROR: no venv found under $LOCAL_ABS (looked for venv/ and .venv/)." >&2
exit 2
fi
else
echo "==> CI mode: cloning $FRAMEWORK_URL @ $FRAMEWORK_REF"
git clone --depth 1 --branch "$FRAMEWORK_REF" "$FRAMEWORK_URL" "$FRAMEWORK"
echo "==> Creating venv and installing requirements"
python3 -m venv "$WORKDIR/venv"
PYTHON="$WORKDIR/venv/bin/python"
"$PYTHON" -m pip install --upgrade pip >/dev/null
"$PYTHON" -m pip install -r "$FRAMEWORK/requirements.txt"
fi
echo "==> Linking plugin '$PLUGIN_NAME' into framework plugins/"
rm -rf "$FRAMEWORK/plugins/$PLUGIN_NAME"
ln -s "$PLUGIN_ABS" "$FRAMEWORK/plugins/$PLUGIN_NAME"
rm -f "$FRAMEWORK/instance/plugins.json"
RC=0
echo "==> Running framework contract tests"
( cd "$FRAMEWORK" && "$PYTHON" -m pytest tests/test_plugin_contract.py -q ) || RC=1
if [ -d "$PLUGIN_ABS/tests" ]; then
echo "==> Running plugin's own tests"
( cd "$FRAMEWORK" && "$PYTHON" -m pytest "$PLUGIN_ABS/tests" -q ) || RC=1
else
echo "==> Plugin has no tests/ directory - skipping plugin test step"
fi
if [ "$RC" -eq 0 ]; then
echo "==> PASS: plugin '$PLUGIN_NAME' is compatible with framework ref '$FRAMEWORK_REF'"
else
echo "==> FAIL: plugin '$PLUGIN_NAME' - see output above" >&2
fi
exit "$RC"
```
### What the harness does and does not check
The symlinked plugin is discovered and loaded by the plugin loader when the app
starts under test. That validates the things that break a real install: the
manifest parses, the `core_version` range admits the framework's
`__contract_version__` (an out-of-range plugin makes startup fail loud, so the
contract test run errors and the script exits nonzero), models expose
`__tablename__`, and hooks return the right shapes.
One gap to know about: the framework's `test_plugins_only_import_contract_surface`
scans `plugins/` with `Path.rglob`, which does not descend symlinked directories
on CPython 3.12. So that particular sub-test does not see a symlinked external
plugin's source. Keep an equivalent import-surface assertion in your own
`tests/` so your CI still enforces "core imports only via `shopdb.api`". A
minimal version:
```python
# tests/test_import_surface.py
import re
from pathlib import Path
ALLOWED = ('shopdb.api', 'shopdb.plugins.base')
IMPORT_RE = re.compile(r'^\s*(?:from (shopdb[\w.]*) import|import (shopdb[\w.]*))', re.MULTILINE)
def test_only_contract_surface_imports():
root = Path(__file__).resolve().parent.parent
bad = []
for path in root.rglob('*.py'):
if '__pycache__' in path.parts or 'migrations' in path.parts:
continue
for m in IMPORT_RE.finditer(path.read_text()):
mod = m.group(1) or m.group(2)
if mod and not any(mod == a or mod.startswith(a + '.') for a in ALLOWED):
bad.append(f'{path.name}: {mod}')
assert not bad, 'core imports outside shopdb.api / shopdb.plugins.base: ' + '; '.join(bad)
```
## GitHub Actions example
For a plugin repo hosted on GitHub, a workflow calling the harness against a
pinned framework tag (config only, adjust URLs and ref to your setup):
```yaml
name: plugin-contract
on:
push:
pull_request:
jobs:
contract:
runs-on: ubuntu-latest
env:
FRAMEWORK_REF: v0.5.0
FRAMEWORK_URL: https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git
steps:
- name: Check out the plugin
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Fetch the harness from the framework
run: |
git clone --depth 1 --branch "$FRAMEWORK_REF" "$FRAMEWORK_URL" /tmp/framework
- name: Run the contract harness against this plugin
run: |
PLUGIN_DIR="$GITHUB_WORKSPACE" \
FRAMEWORK_REF="$FRAMEWORK_REF" \
FRAMEWORK_URL="$FRAMEWORK_URL" \
bash /tmp/framework/scripts/test-external-plugin.sh
```
## See also
- [PLUGIN-QUICKSTART.md](PLUGIN-QUICKSTART.md) - generate and install a plugin fast
- [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md) - the full hook and import-surface reference
- [CONTRACT-STABILITY.md](CONTRACT-STABILITY.md) - what is settled vs still churning before 1.0
- [ADR-002](adr/ADR-002-plugin-versioning.md) - contract versioning and core_version ranges
- [ADR-003](adr/ADR-003-plugin-distribution.md) - the bundled vs external distribution model
- [ADR-008](adr/ADR-008-plugin-migration-ownership.md) - per-plugin migration ownership

View File

@@ -11,7 +11,7 @@ For the architectural decisions behind the contract, see [docs/adr/](../docs/adr
flask plugin new cameras --description "Tracks shop-floor surveillance cameras"
```
Output: `plugins/cameras/` with manifest, plugin class, example model, example routes, schemas stub, tests, and a README.
Output: `plugins/cameras/` with manifest, plugin class, example model, example routes, schemas stub, tests, a README, and a paste-in `frontend-api-snippet.js`. When a `frontend/src/` tree is present, it also writes the frontend starting points: `frontend/src/views/cameras/CamerasList.vue`, `CamerasDetail.vue`, `CamerasForm.vue`, and the auto-discovered route file `frontend/src/router/routes/cameras.js`. (A plugin developed in its own repo, with no frontend tree, gets the backend skeleton plus the snippet only.)
The generated plugin already passes the framework's contract tests. Verify before editing:
@@ -126,13 +126,13 @@ Override hooks on the plugin class as needed. See [PLUGIN-HOOKS.md](PLUGIN-HOOKS
Each hook has a default that does nothing. Override only what your plugin needs.
## Step 7: Frontend (manual checklist)
## Step 7: Frontend (finish the generated starting points)
Backend scaffolding is automated. The frontend is a manual checklist until a frontend scaffold ships. Copy from the closest bundled plugin (`network` is the cleanest) and work through these in order:
The scaffold generates the frontend starting points too: three views, a route file, and a paste-in api-client snippet (see Step 1). They build and run out of the box against the example model, so `npm run build` is green immediately after scaffolding. The list below is what you finish by hand once the views exist. Copy patterns from the closest bundled plugin (`network` is the cleanest) as you flesh them out, and work through these in order:
1. **View files** - create `frontend/src/views/cameras/CamerasList.vue`, `CameraDetail.vue`, `CameraForm.vue`. Copy from `frontend/src/views/network/` and rename. Use the global `.filters` / `.form-control` / `.card` styles; do not invent per-page input styling.
1. **View files** - the scaffold created `frontend/src/views/cameras/CamerasList.vue`, `CamerasDetail.vue`, `CamerasForm.vue` from the example model. Replace the `examplefield` columns and inputs with your domain fields. Keep the global `.filters` / `.form-control` / `.card` styles; do not invent per-page input styling.
2. **Route file** - create `frontend/src/router/routes/cameras.js` exporting a route array. The router auto-discovers every file in `routes/` via `import.meta.glob`, so no registration edit is needed. Tag EVERY route with `meta: { plugin: 'cameras' }` - the ADR-009 guard redirects to the dashboard when the backend plugin is disabled. Add `requiresAuth: true` on form routes:
2. **Route file** - the scaffold created `frontend/src/router/routes/cameras.js` exporting a route array. The router auto-discovers every file in `routes/` via `import.meta.glob`, so no registration edit is needed. Every route is already tagged with `meta: { plugin: 'cameras' }` - the ADR-009 guard redirects to the dashboard when the backend plugin is disabled - and the form routes already carry `requiresAuth: true`:
```js
export default [
@@ -144,14 +144,14 @@ export default [
},
{
path: 'cameras/:id/edit',
name: 'camera-edit',
component: () => import('../../views/cameras/CameraForm.vue'),
name: 'cameras-edit',
component: () => import('../../views/cameras/CamerasForm.vue'),
meta: { requiresAuth: true, plugin: 'cameras' }
}
]
```
3. **API client** - add a `camerasApi` block to `frontend/src/api/index.js` wrapping your endpoints. Match an existing block's shape (`list(params)`, `get(id)`, `create(data)`, `update(id, data)`).
3. **API client** - the scaffolded views ship with an inline `camerasApi` client so they run standalone. To graduate to the shared module, paste the generated `plugins/cameras/frontend-api-snippet.js` block into `frontend/src/api/index.js`, then delete the inline const in each view and `import { camerasApi } from '../../api'` instead. The snippet already matches the existing blocks' shape (`list(params)`, `get(id)`, `create(data)`, `update(id, data)`, `remove(id)`).
4. **Sidebar entry** - implement `get_navigation_items` on the plugin class. No frontend edit: the sidebar builds itself from `/api/dashboard/navigation`.
@@ -182,3 +182,5 @@ export default [
## Distribution
If you are building a plugin for a specific GE Aerospace site (sister-site adoption), ship it as its own git repo. The site running shopdb-flask clones or symlinks your plugin into `<repo>/plugins/<name>/`. See [ADR-003](../docs/adr/ADR-003-plugin-distribution.md).
For the full own-repo workflow (layout, symlink dev loop, CI recipe with `scripts/test-external-plugin.sh`, version pinning), see [PLUGIN-EXTERNAL-REPO.md](PLUGIN-EXTERNAL-REPO.md). For what you can rely on staying stable before contract 1.0, see [CONTRACT-STABILITY.md](CONTRACT-STABILITY.md).

View File

@@ -18,10 +18,12 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin
## Building your own
Two guides:
Guides:
- [PLUGIN-QUICKSTART.md](PLUGIN-QUICKSTART.md) - generate, customize, install, and test a plugin in 30 minutes using `flask plugin new`.
- [PLUGIN-GUIDE.md](PLUGIN-GUIDE.md) - the full narrative walkthrough of building the `measuringtools` plugin, the exemplar that exercises every current framework feature (models, per-plugin migrations, authz, hooks, frontend integration, tests).
- [PLUGIN-EXTERNAL-REPO.md](PLUGIN-EXTERNAL-REPO.md) - developing a plugin in its own repo per ADR-003: repo layout, symlink dev workflow, `core_version` pinning, and a runnable CI harness (`scripts/test-external-plugin.sh`) that tests the plugin against a pinned framework ref.
- [CONTRACT-STABILITY.md](CONTRACT-STABILITY.md) - path to contract 1.0: what is settled vs still churning, the bump rules, and how much a sister site can safely build on today.
The contract is locked in [ADR-001](adr/ADR-001-asset-as-platform-contract.md) and versioned per [ADR-002](adr/ADR-002-plugin-versioning.md).