Files
shopdb-flask/docs/PLUGIN-QUICKSTART.md
cproudlock f77f0a8d90
Some checks failed
CI / naming (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / migrations-mysql (push) Has been cancelled
CI / backend (push) Has been cancelled
Add GitHub Actions CI + Windows notes on the developer docs
GitHub had no CI, so naming/tests/build were unenforced on the public
mirror. Add .github/workflows/ci.yml mirroring the internal pipeline:
backend pytest, the naming gate, frontend vitest+build, and the
migrations-mysql job that proves a fresh flask db upgrade + every
plugin chain on utf8mb4 MySQL 8 is idempotent. Flip the dev-setup CI
note to reflect it. Add an identical Windows/VS Code convention note to
the four developer docs (venv\Scripts vs venv/bin, $env: vs export,
pointer to DEVELOPMENT-SETUP).
2026-07-17 15:12:36 -04:00

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

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.

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

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_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:

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' }
  }
]
  1. 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)).

  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.

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.