# 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`. It also writes the frontend starting points, and they live **inside the plugin**: `plugins/cameras/frontend/routes.js` and `plugins/cameras/frontend/views/CamerasList.vue`, `CamerasDetail.vue`, `CamerasForm.vue`. Nothing is written into `frontend/src/`. The plugin owns its frontend because that is what makes it prunable: `scripts/stage-frontend.mjs` copies each plugin's `frontend/` into `frontend/src/.plugins-staged//` and aggregates the route modules into `routes.gen.js`, so a plugin left out of `SITE_PLUGINS` simply is not copied and its frontend leaves the build with it (ADR-013). A plugin developed in its own repo gets exactly this same layout, and its frontend starts working the moment the directory is dropped into a host repo's `plugins/`. 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 First add `plugins/cameras/migrations/` with a per-plugin Alembic chain that creates the plugin's tables, and register those tables in `PLUGIN_TABLE_OWNERS` (per ADR-008; the plugin chain owns plugin schema, never the core chain). Then: ```bash flask plugin install cameras flask plugin upgrade-all ``` `install` runs the plugin's `on_install` hook (which seeds the AssetType row), registers it in the plugin registry, and runs the plugin's own migration chain. `flask db migrate`/`flask db upgrade` is reserved for core tables and must not be used for plugin schema. ## 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/` | 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 `plugins/cameras/frontend/views/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. **Imports have one rule, and staging is the reason.** Anything from the CORE tree uses the `@` alias (`@/api`, `@/components/PaginationBar.vue`, `@/stores/auth`); anything inside your own plugin is relative (`./views/...`). A relative path into the core tree such as `../../api` is correct where the file sits in your plugin and wrong once staged into `.plugins-staged/cameras/`, and the failure is a build error rather than anything subtle. 2. **Route file** - the scaffold created `plugins/cameras/frontend/routes.js` exporting a route array plus a `toplevel` array. `stage-frontend.mjs` aggregates both into `routes.gen.js`, which the router spreads in, 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/CamerasList.vue'), meta: { plugin: 'cameras' } }, { path: 'cameras/:id/edit', name: 'cameras-edit', component: () => import('./views/CamerasForm.vue'), meta: { requiresAuth: true, plugin: 'cameras' } } ] // Full-screen routes that render OUTSIDE AppLayout (kiosk, print pages). // Export it even when empty: routes.gen.js reads `.toplevel` off every staged // module, and a missing export is a Rollup warning under strict ESM. export const toplevel = [] ``` 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//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 `/plugins//`. 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).