Files
shopdb-flask/docs/PLUGIN-QUICKSTART.md
cproudlock 94f852a1c8
All checks were successful
CI / backend (push) Successful in 24s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
Add frontend integration checklist and docs-drift guard
PLUGIN-QUICKSTART Step 7 is now a concrete 7-item checklist (view
conventions, route auto-discovery + ADR-009 gating meta, api client
shape, nav/report hooks, settings auto-nesting, verification).

New tests/test_docs_contract.py introspects BasePlugin and fails CI when
a public hook is missing from PLUGIN-HOOKS.md or the documented contract
version drifts - it immediately caught two undocumented hooks
(get_provisioning_note, get_config_schema), now documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:14:25 -04:00

7.4 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. 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, and a README.

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

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

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_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 (manual checklist)

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:

  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.

  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:

export default [
  {
    path: 'cameras',
    name: 'cameras',
    component: () => import('../../views/cameras/CamerasList.vue'),
    meta: { plugin: 'cameras' }
  },
  {
    path: 'cameras/:id/edit',
    name: 'camera-edit',
    component: () => import('../../views/cameras/CameraForm.vue'),
    meta: { requiresAuth: true, plugin: 'cameras' }
  }
]
  1. 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)).

  2. Sidebar entry - implement get_navigation_items on the plugin class. No frontend edit: the sidebar builds itself from /api/dashboard/navigation.

  3. 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.

  4. 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/.

  5. 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

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.