# 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/<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-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).
