Audited all 40 docs/ against the live codebase; fixed factual staleness in 23, 14 were clean. Highlights (all verified against code): - equipment -> machines (ADR-011 rename) in INSTALL/DEPLOY-WINDOWS-IIS, PLUGIN-GUIDE, GE-ENFORCE, ROADMAP. - Versions refreshed: contract 0.10.0 -> 0.13.0, product 0.5.0 -> 0.7.0, plus plugin example core_version pins. - Bundled set corrected to the current 13 (PLUGINS.md 7 -> 13 rows; DEPLOY eleven -> thirteen). - Per-plugin Alembic chain workflow (ADR-008) replacing stale core-chain steps in PLUGIN-QUICKSTART / BACKUP-RESTORE; deploy adds plugin upgrade-all. - Frontend plugin staging (ADR-010) replacing 'no frontend plugin system yet' in PLUGIN-GUIDE; view/route paths repointed to plugins/<name>/frontend/. - Corrected file paths (MapView.vue, manifest_schema.json), CLI (shelf-list), API gating (GET /api/plugins is optional-jwt), WJF 15 -> 16 stages, and retired Collector/PC-Types settings pages (ADR-012). - ge-enforce proposal marked ACCEPTED/built.
9.7 KiB
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.
Windows / VS Code: command examples below use the Linux venv path
venv/bin/python; on Windows usevenv\Scripts\pythonand$env:FLASK_APP="shopdb"(notexport). Full Windows onboarding: DEVELOPMENT-SETUP.
For the architectural decisions behind the contract, see docs/adr/.
Step 1: Generate the skeleton
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:
pytest plugins/cameras/tests/
Step 2: Edit the model
Open plugins/cameras/models/cameras.py. Replace the examplefield placeholder with your domain fields:
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.
Step 3: Add routes
Open plugins/cameras/api/routes.py. The scaffold provides list and detail endpoints. Add CRUD as needed:
@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:
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:
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
flask plugin list
You should see cameras [Enabled].
Run the plugin's tests:
pytest plugins/cameras/tests/
Hit the API:
curl http://localhost:5001/api/cameras
Step 6: Add hooks (optional)
Override hooks on the plugin class as needed. See 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:
-
View files - the scaffold created
frontend/src/views/cameras/CamerasList.vue,CamerasDetail.vue,CamerasForm.vuefrom the example model. Replace theexamplefieldcolumns and inputs with your domain fields. Keep the global.filters/.form-control/.cardstyles; do not invent per-page input styling. -
Route file - the scaffold created
frontend/src/router/routes/cameras.jsexporting a route array. The router auto-discovers every file inroutes/viaimport.meta.glob, so no registration edit is needed. Every route is already tagged withmeta: { plugin: 'cameras' }- the ADR-009 guard redirects to the dashboard when the backend plugin is disabled - and the form routes already carryrequiresAuth: true:
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' }
}
]
-
API client - the scaffolded views ship with an inline
camerasApiclient so they run standalone. To graduate to the shared module, paste the generatedplugins/cameras/frontend-api-snippet.jsblock intofrontend/src/api/index.js, then delete the inline const in each view andimport { camerasApi } from '../../api'instead. The snippet already matches the existing blocks' shape (list(params),get(id),create(data),update(id, data),remove(id)). -
Sidebar entry - implement
get_navigation_itemson the plugin class. No frontend edit: the sidebar builds itself from/api/dashboard/navigation. -
Report cards (if any) - implement
get_reportson the plugin class. No frontend edit: the Reports hub builds itself from/api/reports. Useroutefor a dedicated page (add it to your route file), orendpointfor inline rendering. -
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 fromfrontend/src/views/settings/. -
Verify -
npm run buildmust 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 - the full narrative walkthrough of building the
measuringtoolsplugin 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 for the full hook reference
- CONTRIBUTING.md for naming conventions
- docs/adr/ADR-001-asset-as-platform-contract.md for what your plugin can rely on
- 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.
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. For what you can rely on staying stable before contract 1.0, see CONTRACT-STABILITY.md.