# 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). 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, and a README. 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 ```bash 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 ```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_searchable_fields` | Plugin contributes to the global search endpoint | | `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` | Plugin accepts external pushes at `/api/collector/` | 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//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-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).