Framework: - Per-plugin Alembic migration chains (ADR-008): every bundled plugin carries its own chain with a stamp-only anchor at the ownership cutover; new plugin schema lands in plugins/<name>/migrations/, never the core chain. Deploys add flask plugin upgrade-all. Fixed a latent bug in the shared alembic template (engine URL resolution) and taught the metadata filter to include FK-referenced core tables. - Frontend plugin route gating (ADR-009): plugin routes carry meta.plugin; a disabled plugin's pages redirect to the dashboard via a cached, fail-open check against the new public GET /api/plugins/enabled. - get_reports() plugin hook (contract 0.5.0 -> 0.6.0): plugins contribute report cards; warranty and toner cards moved off the hardcoded list. Reports: - Hub grouped by category with search; inline reports render at the top, are URL-backed (?report=id, back-button and deep links work), expose their server-side filter params as controls, and export CSV. Warranty and Toner pages gained CSV export. - Deleted the dead legacy Warranty Status report (always-zero buckets from a retired column). Theming and fonts: - Inter (variable) bundled locally via @fontsource, replacing the Google Fonts Roboto import - air-gapped installs now render correctly; tables use tabular numerals. - Optional brand_primary_dark_color, brand_accent_color, brand_sidebar_color settings applied to CSS vars at bootstrap. USB frontend repair (views were reading a dead legacy shape): - List/detail/form and the employee profile USB panels remapped to the real API shape (device_id/device_desc/checkinoutlog); employee panels now use /usb/checkouts endpoints; external-mode /usb/checkouts/active honors the badge filter; dead client methods pruned. Also: warranties list page no longer requires login (matches app convention); collector doc rewritten with a GE-Enforce integration guide and paste-ready PowerShell reporter; ADR index and CHANGELOG updated. Verified: 323 tests pass, naming/style green, frontend builds, plugin migration dry-run green on scratch MySQL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
160 lines
5.6 KiB
Markdown
160 lines
5.6 KiB
Markdown
# 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_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 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
|
|
|
|
- [PLUGIN-GUIDE.md](PLUGIN-GUIDE.md) - the full narrative walkthrough of building the `measuringtools` plugin 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](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 `<repo>/plugins/<name>/`. See [ADR-003](../docs/adr/ADR-003-plugin-distribution.md).
|