Files
shopdb-flask/docs/PLUGIN-QUICKSTART.md
cproudlock a515c28e3b Decouple core from plugins; wire widgets hook; drop dead search hook
Architectural pass from the skill review ("plugin is the product" boundary).

Core no longer imports plugin models at module load (was a hard import-time
dependency that broke core if the computers plugin was absent/disabled):
- collector.py, applications.py, reports.py: lazy + guarded imports of the
  computers plugin models. Endpoints that need install-tracking now return 503
  when the plugin is absent instead of failing at import.

Search honors runtime enable/disable:
- search.py: _require_enabled(name) raises ImportError for a disabled plugin,
  so each plugin-scoped block skips it (a disabled plugin's rows leave search).
- Replace hardcoded root/rootpassword employee-DB connection in _search_employees
  with the shared env-backed employee_connection helper.

Plugin hooks (integrating-plugin-hooks: every hook needs a consumer):
- get_dashboard_widgets: add the consumer GET /api/dashboard/widgets (5 plugins
  already implemented the hook; it had none). Skips disabled, isolates in prod.
- get_searchable_fields: REMOVED. Zero plugins implemented it and there was no
  consumer; global search is a core concern over the asset model. Contract
  reduction, __contract_version__ 0.3.0 -> 0.4.0.

Docs/contract: PLUGIN-HOOKS.md (widgets consumer note, searchable-fields removal,
0.4.0), PLUGIN-QUICKSTART.md, ADR-001 hook list. Tests: widgets endpoint
aggregate + disabled-skip; contract tests for the removed/added hooks.

151 tests pass, naming/style green, app boots all 6 plugins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:00:10 -04:00

5.3 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_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 for now)

Backend scaffolding is automated. Frontend is manual until the frontend scaffolding skill ships. Convention:

  • frontend/src/views/cameras/CamerasList.vue
  • frontend/src/views/cameras/CameraDetail.vue
  • frontend/src/views/cameras/CameraForm.vue

Copy from an existing plugin's view files (e.g., frontend/src/views/network/) as a starting point. Update the API client in frontend/src/api/index.js to add cameras endpoints.

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.