Files
shopdb-flask/docs/PLUGIN-QUICKSTART.md
cproudlock efb879d44a
Some checks failed
CI / backend (push) Successful in 1m45s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s
Docs audit fixes: kiosk code drift, PowerShell chains, broken links, leaks
From the Fable/Opus documentation audit (8 confirmed + verified
lab-drift the run's session limit had cut short):
- HIGH: the lab's kiosk _kiosk_find_item block showed the pre-stage-17
  row-id resolver as current; replace with the shipped gagelabtag /
  numeric-tail resolver, fix the stale 'resolved by row id' prose and
  the 'stage-7 code is corrected' note.
- MED: the badge _external_lookup block used dict-only row access that
  breaks on a tuple cursor; use the tuple-or-dict form shipped. Split
  '&&' command chains (fail in PowerShell 5.1) in the lab.
- LOW/link: the Windows note's [DEVELOPMENT-SETUP] link dropped the .md
  and 404'd in four docs; fix. Correct the stage-6a->16a comment and
  the lab-stage tag range (..16 -> ..17).
- Leaks: drop /home/camp path from ADR-006, the internal gitea host
  from PLUGINS.md.
- Windows: add an mklink junction note for the external-plugin symlink
  dev loop.
- CI: prime root to mysql_native_password so pymysql connects to the
  MySQL 8 service without the cryptography package (and its kit wheel).
2026-07-17 18:05:47 -04:00

198 lines
9.4 KiB
Markdown

# Plugin Quickstart
Build a working shopdb-flask plugin in 30 minutes. This walks through generating, customizing, installing, and testing a plugin from scratch.
For the full hook reference, see [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md).
> **Windows / VS Code:** command examples below use the Linux venv path
> `venv/bin/python`; on Windows use `venv\Scripts\python` and
> `$env:FLASK_APP="shopdb"` (not `export`). Full Windows onboarding:
> [DEVELOPMENT-SETUP](DEVELOPMENT-SETUP.md).
For the architectural decisions behind the contract, see [docs/adr/](../docs/adr/).
## Step 1: Generate the skeleton
```bash
flask plugin new cameras --description "Tracks shop-floor surveillance cameras"
```
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:
```bash
pytest plugins/cameras/tests/
```
## Step 2: Edit the model
Open `plugins/cameras/models/cameras.py`. Replace the `examplefield` placeholder with your domain fields:
```python
class Cameras(BaseModel):
__tablename__ = 'cameras'
assetid = db.Column(
db.Integer,
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
primary_key=True,
)
streamurl = db.Column(db.String(255), nullable=False)
resolution = db.Column(db.String(20))
fps = db.Column(db.Integer)
poeport = db.Column(db.String(50))
asset = db.relationship('Asset', backref=db.backref('cameras', uselist=False))
def to_dict(self):
return {
'assetid': self.assetid,
'streamurl': self.streamurl,
'resolution': self.resolution,
'fps': self.fps,
'poeport': self.poeport,
}
```
Note the naming convention: lowercase concatenated, no underscores (`streamurl`, not `stream_url`). See [CONTRIBUTING.md](../CONTRIBUTING.md).
## Step 3: Add routes
Open `plugins/cameras/api/routes.py`. The scaffold provides list and detail endpoints. Add CRUD as needed:
```python
@cameras_bp.route('', methods=['POST'])
@jwt_required()
def create_camera():
data = request.get_json()
asset = Asset(assetnumber=data['assetnumber'], name=data['name'], ...)
db.session.add(asset)
db.session.flush()
camera = Cameras(
assetid=asset.assetid,
streamurl=data['streamurl'],
resolution=data.get('resolution'),
)
db.session.add(camera)
db.session.commit()
return success_response(camera.to_dict(), http_code=201)
```
For audit logging, use the public helper:
```python
from shopdb.api import audit_log
audit_log(action='created', entitytype='Camera', entityid=asset.assetid, entityname=asset.name)
```
## Step 4: Install the plugin
```bash
flask plugin install cameras
flask db migrate -m "Add cameras plugin tables"
flask db upgrade
```
`install` runs the plugin's `on_install` hook (which seeds the AssetType row), registers it in the plugin registry, and runs migrations.
## Step 5: Verify it works
```bash
flask plugin list
```
You should see `cameras [Enabled]`.
Run the plugin's tests:
```bash
pytest plugins/cameras/tests/
```
Hit the API:
```bash
curl http://localhost:5001/api/cameras
```
## Step 6: Add hooks (optional)
Override hooks on the plugin class as needed. See [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md) for the full list. Common ones:
| Hook | Adds |
|------|------|
| `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_permissions` | Plugin's RBAC permissions join the catalog, seeding, role grid, and token scopes |
| `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/<name>` |
Each hook has a default that does nothing. Override only what your plugin needs.
## Step 7: Frontend (finish the generated starting points)
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** - 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** - 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 [
{
path: 'cameras',
name: 'cameras',
component: () => import('../../views/cameras/CamerasList.vue'),
meta: { plugin: 'cameras' }
},
{
path: 'cameras/:id/edit',
name: 'cameras-edit',
component: () => import('../../views/cameras/CamerasForm.vue'),
meta: { requiresAuth: true, plugin: 'cameras' }
}
]
```
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`.
5. **Report cards** (if any) - implement `get_reports` on the plugin class. No frontend edit: the Reports hub builds itself from `/api/reports`. Use `route` for a dedicated page (add it to your route file), or `endpoint` for inline rendering.
6. **Settings page** (if the plugin has subtypes) - add a route whose path starts with `settings/` (e.g. `settings/cameratypes`) to your route file; the router automatically nests it under the settings shell. Copy a types-list view from `frontend/src/views/settings/`.
7. **Verify** - `npm run build` must pass, then screenshot your pages against the dev servers: `venv/bin/python tools/shot.py /cameras`.
## Common errors
| Symptom | Cause | Fix |
|---------|-------|-----|
| `PluginNotFoundError: manifest.json` | Manifest deleted or moved | Restore `plugins/<name>/manifest.json` |
| `PluginContractError: missing required field` | manifest.json incomplete | Re-add `name`, `version`, `description` |
| `PluginVersionError: requires core_version X but framework is Y` | Framework upgraded past your range | Update `core_version` in manifest |
| `Table 'cameras' is already defined` | Two models declared the same `__tablename__` | Pick a unique table name |
| Index name collision | Two indexes share the same name (SQLite enforces global uniqueness) | Prefix index names with table: `idx_cameras_streamurl` |
## Next steps
- [PLUGIN-GUIDE.md](PLUGIN-GUIDE.md) - the full narrative walkthrough of building the `measuringtools` plugin end to end (models, per-plugin migration baseline, authz, hooks, frontend integration, tests). Read this after the quickstart when you want the exemplar that exercises every framework feature.
- [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md) for the full hook reference
- [CONTRIBUTING.md](../CONTRIBUTING.md) for naming conventions
- [docs/adr/ADR-001-asset-as-platform-contract.md](../docs/adr/ADR-001-asset-as-platform-contract.md) for what your plugin can rely on
- [docs/adr/ADR-006-collector-contract.md](../docs/adr/ADR-006-collector-contract.md) for accepting external collector input
## 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).