Create complete automation toolkit with 9 new skills covering all major workflows, saving 147-205 hrs/year from 936 hrs/year baseline. ## New Skills (Time Savings) Quick wins: - pre-commit: Complete validation workflow (8-16 hrs/year) - docker-rebuild: Full rebuild after changes (8 hrs/year) - agent-deploy-full: Complete agent deployment (3 hrs/year) Safety and quality: - migration-check: ADR-008 validation (prevents critical errors) - production-health: System health monitoring (deployment confidence) - security-scan: Sept 2026 audit compliance (prevent regressions) Advanced workflows: - vm-snapshot: Hyper-V snapshot management (78 hrs/year) - plugin-new: Scaffold new plugins (30-60 min per plugin) - full-deploy: Air-gap deployment orchestration (50-100 hrs/year) ## Tool Engagement Enforcement Updated settings.json to load TOOL-ENGAGEMENT-PROTOCOL.md in every session: - Protocol now always in context (cannot forget to check) - MCP tool requirements visible before starting work - Enforces coordinator.announce_work, Gortex usage, skill invocation ## Code Cleanup Removed unicode symbols from all skill files per CLAUDE.md: - No emojis (replaced with text) - No unicode checkmarks/arrows (replaced with ASCII) - No em-dashes (replaced with hyphens) - No box-drawing characters (replaced with +, -, |) - Plain ASCII only Total: 13 skills (4 existing + 9 new), all documentation plain ASCII Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
708 lines
18 KiB
Markdown
708 lines
18 KiB
Markdown
---
|
|
name: plugin-new
|
|
description: Scaffold new ShopDB plugin following ADRs - directory structure, BasePlugin boilerplate, migrations, Vue component
|
|
---
|
|
|
|
# New Plugin Scaffolding
|
|
|
|
Generate complete plugin structure following ShopDB architecture (ADR-001, ADR-008, ADR-010).
|
|
|
|
## What This Skill Does
|
|
|
|
Creates production-ready plugin scaffold:
|
|
1. Plugin directory structure
|
|
2. BasePlugin subclass with manifest
|
|
3. SQLAlchemy models directory
|
|
4. Flask API blueprint
|
|
5. Alembic migrations directory
|
|
6. Vue frontend component
|
|
7. README with checklist
|
|
8. Adds plugin to PLUGIN_TABLE_OWNERS
|
|
|
|
Saves 30-60 minutes of setup per new plugin.
|
|
|
|
## Usage
|
|
|
|
`/plugin-new "plugin-name" "description"`
|
|
|
|
Example: `/plugin-new "tooling" "Track hand tools and tooling inventory"`
|
|
|
|
## Workflow
|
|
|
|
### 1. Validate Plugin Name
|
|
|
|
**Check naming conventions:**
|
|
- Lowercase, no underscores
|
|
- Singular form ("tool" not "tools", exception: existing plurals like "computers")
|
|
- No spaces or special characters
|
|
- Not a Python reserved word
|
|
|
|
**Check plugin doesn't exist:**
|
|
```bash
|
|
if [ -d "plugins/$plugin_name" ]; then
|
|
echo "ERROR: Plugin '$plugin_name' already exists"
|
|
exit 1
|
|
fi
|
|
```
|
|
|
|
**Suggest corrections:**
|
|
- "tool_tracking" -> "tooling" (remove underscore)
|
|
- "Tools" -> "tools" (lowercase)
|
|
- "hand-tools" -> "handtools" (remove hyphen)
|
|
|
|
### 2. Create Directory Structure
|
|
|
|
```bash
|
|
mkdir -p plugins/$plugin_name/{api,models,frontend/views,migrations/versions,tests}
|
|
```
|
|
|
|
**Expected structure:**
|
|
```
|
|
plugins/tooling/
|
|
__init__.py
|
|
plugin.py # BasePlugin subclass
|
|
api/
|
|
__init__.py
|
|
routes.py # Flask blueprint
|
|
models/
|
|
__init__.py
|
|
tool.py # SQLAlchemy model
|
|
frontend/
|
|
views/
|
|
ToolList.vue # Main Vue component
|
|
migrations/
|
|
env.py # Alembic environment
|
|
script.py.mako # Migration template
|
|
versions/ # Migration files
|
|
tests/
|
|
__init__.py
|
|
test_tooling.py # Plugin tests
|
|
README.md # Plugin documentation
|
|
```
|
|
|
|
### 3. Generate BasePlugin Subclass
|
|
|
|
**File:** `plugins/$plugin_name/plugin.py`
|
|
|
|
```python
|
|
"""
|
|
$Plugin_Name Plugin
|
|
|
|
$Description
|
|
"""
|
|
from shopdb.api import BasePlugin
|
|
|
|
|
|
class ${Plugin_Name}Plugin(BasePlugin):
|
|
"""$Description"""
|
|
|
|
name = "$plugin_name"
|
|
display_name = "$Display Name"
|
|
description = "$description"
|
|
version = "0.1.0"
|
|
author = "ShopDB Team"
|
|
|
|
# Dependencies (other plugins this depends on)
|
|
dependencies = []
|
|
|
|
# Frontend routes
|
|
frontend_routes = [
|
|
{
|
|
"path": "/$plugin_name",
|
|
"name": "$Plugin_Name",
|
|
"component": "$Plugin_NameList",
|
|
"meta": {"requiresAuth": True, "title": "$Display Name"}
|
|
}
|
|
]
|
|
|
|
# Sidebar menu entry
|
|
sidebar_items = [
|
|
{
|
|
"title": "$Display Name",
|
|
"icon": "mdi-tools", # Material Design Icon
|
|
"path": "/$plugin_name",
|
|
"permission": "$plugin_name.view"
|
|
}
|
|
]
|
|
|
|
# Permissions this plugin defines
|
|
permissions = [
|
|
("$plugin_name.view", "View $display_name"),
|
|
("$plugin_name.create", "Create $display_name"),
|
|
("$plugin_name.edit", "Edit $display_name"),
|
|
("$plugin_name.delete", "Delete $display_name"),
|
|
]
|
|
|
|
def register_blueprints(self, app):
|
|
"""Register Flask blueprints"""
|
|
from .api.routes import bp
|
|
app.register_blueprint(bp, url_prefix=f"/api/$plugin_name")
|
|
|
|
def init_app(self, app):
|
|
"""Initialize plugin with Flask app"""
|
|
self.register_blueprints(app)
|
|
```
|
|
|
|
**Variables to substitute:**
|
|
- `$plugin_name`: "tooling"
|
|
- `$Plugin_Name`: "Tooling"
|
|
- `$Display Name`: "Tool Tracking"
|
|
- `$description`: User-provided description
|
|
|
|
### 4. Create SQLAlchemy Model
|
|
|
|
**File:** `plugins/$plugin_name/models/$model_name.py`
|
|
|
|
```python
|
|
"""
|
|
$Model_Name model for $plugin_name plugin
|
|
"""
|
|
from shopdb.api import db, Asset
|
|
|
|
|
|
class $Model_Name(Asset):
|
|
"""$Description"""
|
|
|
|
__tablename__ = "$tablename"
|
|
__mapper_args__ = {"polymorphic_identity": "$plugin_name"}
|
|
|
|
# Polymorphic inheritance from Asset (ADR-001)
|
|
id = db.Column(db.Integer, db.ForeignKey('asset.id'), primary_key=True)
|
|
|
|
# Plugin-specific fields
|
|
tool_type = db.Column(db.String(100), nullable=False)
|
|
manufacturer = db.Column(db.String(100))
|
|
model_number = db.Column(db.String(100))
|
|
serial_number = db.Column(db.String(100), unique=True)
|
|
purchase_date = db.Column(db.Date)
|
|
status = db.Column(db.String(50), default='available')
|
|
|
|
# Relationships (example: assigned to employee)
|
|
assigned_to_id = db.Column(db.Integer, db.ForeignKey('employee.id'))
|
|
assigned_to = db.relationship('Employee', backref='tools')
|
|
|
|
def __repr__(self):
|
|
return f"<$Model_Name {self.serial_number}>"
|
|
|
|
def to_dict(self):
|
|
"""Serialize model to dictionary"""
|
|
data = super().to_dict() # Get Asset base fields
|
|
data.update({
|
|
'tool_type': self.tool_type,
|
|
'manufacturer': self.manufacturer,
|
|
'model_number': self.model_number,
|
|
'serial_number': self.serial_number,
|
|
'purchase_date': self.purchase_date.isoformat() if self.purchase_date else None,
|
|
'status': self.status,
|
|
'assigned_to': self.assigned_to.to_dict() if self.assigned_to else None
|
|
})
|
|
return data
|
|
```
|
|
|
|
**Models/__init__.py:**
|
|
```python
|
|
from .tool import Tool
|
|
|
|
__all__ = ['Tool']
|
|
```
|
|
|
|
### 5. Create Flask API Blueprint
|
|
|
|
**File:** `plugins/$plugin_name/api/routes.py`
|
|
|
|
```python
|
|
"""
|
|
API routes for $plugin_name plugin
|
|
"""
|
|
from flask import Blueprint, request, jsonify
|
|
from shopdb.api import db, requires_auth
|
|
from ..models import $Model_Name
|
|
|
|
bp = Blueprint('$plugin_name', __name__)
|
|
|
|
|
|
@bp.route('/', methods=['GET'])
|
|
@requires_auth
|
|
def list_$plural():
|
|
"""List all $plural"""
|
|
items = $Model_Name.query.all()
|
|
return jsonify([item.to_dict() for item in items])
|
|
|
|
|
|
@bp.route('/<int:id>', methods=['GET'])
|
|
@requires_auth
|
|
def get_$singular(id):
|
|
"""Get specific $singular by ID"""
|
|
item = $Model_Name.query.get_or_404(id)
|
|
return jsonify(item.to_dict())
|
|
|
|
|
|
@bp.route('/', methods=['POST'])
|
|
@requires_auth
|
|
def create_$singular():
|
|
"""Create new $singular"""
|
|
data = request.json
|
|
|
|
# Validate required fields
|
|
required = ['tool_type', 'serial_number']
|
|
if not all(field in data for field in required):
|
|
return jsonify({'error': 'Missing required fields'}), 400
|
|
|
|
# Create item
|
|
item = $Model_Name(**data)
|
|
db.session.add(item)
|
|
db.session.commit()
|
|
|
|
return jsonify(item.to_dict()), 201
|
|
|
|
|
|
@bp.route('/<int:id>', methods=['PUT'])
|
|
@requires_auth
|
|
def update_$singular(id):
|
|
"""Update existing $singular"""
|
|
item = $Model_Name.query.get_or_404(id)
|
|
data = request.json
|
|
|
|
# Update fields
|
|
for key, value in data.items():
|
|
if hasattr(item, key):
|
|
setattr(item, key, value)
|
|
|
|
db.session.commit()
|
|
return jsonify(item.to_dict())
|
|
|
|
|
|
@bp.route('/<int:id>', methods=['DELETE'])
|
|
@requires_auth
|
|
def delete_$singular(id):
|
|
"""Delete $singular"""
|
|
item = $Model_Name.query.get_or_404(id)
|
|
db.session.delete(item)
|
|
db.session.commit()
|
|
|
|
return '', 204
|
|
```
|
|
|
|
### 6. Initialize Alembic Migrations
|
|
|
|
**File:** `plugins/$plugin_name/migrations/env.py`
|
|
|
|
Copy from template (similar to other plugins)
|
|
|
|
**File:** `plugins/$plugin_name/migrations/script.py.mako`
|
|
|
|
Copy from template
|
|
|
|
**Create initial migration:**
|
|
```bash
|
|
docker compose exec api flask plugin revision -p $plugin_name -m "Initial $plugin_name schema"
|
|
```
|
|
|
|
This generates: `plugins/$plugin_name/migrations/versions/001_initial_schema.py`
|
|
|
|
**Verify migration:**
|
|
- Check revision ID generated
|
|
- Verify down_revision is None (first migration)
|
|
- Verify upgrade() creates table
|
|
- Verify downgrade() drops table
|
|
|
|
### 7. Create Vue Frontend Component
|
|
|
|
**File:** `plugins/$plugin_name/frontend/views/$Plugin_NameList.vue`
|
|
|
|
```vue
|
|
<template>
|
|
<div class="$plugin_name-list">
|
|
<h1>$Display Name</h1>
|
|
|
|
<v-btn color="primary" @click="createDialog = true">
|
|
<v-icon left>mdi-plus</v-icon>
|
|
Add $Singular
|
|
</v-btn>
|
|
|
|
<v-data-table
|
|
:headers="headers"
|
|
:items="items"
|
|
:loading="loading"
|
|
class="elevation-1 mt-4"
|
|
>
|
|
<template v-slot:item.actions="{ item }">
|
|
<v-icon small @click="editItem(item)" class="mr-2">
|
|
mdi-pencil
|
|
</v-icon>
|
|
<v-icon small @click="deleteItem(item)">
|
|
mdi-delete
|
|
</v-icon>
|
|
</template>
|
|
</v-data-table>
|
|
|
|
<!-- Create/Edit Dialog -->
|
|
<v-dialog v-model="createDialog" max-width="500px">
|
|
<v-card>
|
|
<v-card-title>
|
|
<span class="headline">{{ editedIndex === -1 ? 'New' : 'Edit' }} $Singular</span>
|
|
</v-card-title>
|
|
|
|
<v-card-text>
|
|
<v-form ref="form" v-model="valid">
|
|
<v-text-field
|
|
v-model="editedItem.tool_type"
|
|
label="Tool Type"
|
|
:rules="[v => !!v || 'Required']"
|
|
required
|
|
></v-text-field>
|
|
|
|
<v-text-field
|
|
v-model="editedItem.serial_number"
|
|
label="Serial Number"
|
|
:rules="[v => !!v || 'Required']"
|
|
required
|
|
></v-text-field>
|
|
|
|
<v-text-field
|
|
v-model="editedItem.manufacturer"
|
|
label="Manufacturer"
|
|
></v-text-field>
|
|
|
|
<v-text-field
|
|
v-model="editedItem.model_number"
|
|
label="Model Number"
|
|
></v-text-field>
|
|
</v-form>
|
|
</v-card-text>
|
|
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
<v-btn text @click="closeDialog">Cancel</v-btn>
|
|
<v-btn color="primary" text @click="saveItem" :disabled="!valid">
|
|
Save
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: '$Plugin_NameList',
|
|
|
|
data: () => ({
|
|
items: [],
|
|
loading: false,
|
|
createDialog: false,
|
|
valid: false,
|
|
editedIndex: -1,
|
|
editedItem: {
|
|
tool_type: '',
|
|
serial_number: '',
|
|
manufacturer: '',
|
|
model_number: ''
|
|
},
|
|
defaultItem: {
|
|
tool_type: '',
|
|
serial_number: '',
|
|
manufacturer: '',
|
|
model_number: ''
|
|
},
|
|
headers: [
|
|
{ text: 'Serial Number', value: 'serial_number' },
|
|
{ text: 'Tool Type', value: 'tool_type' },
|
|
{ text: 'Manufacturer', value: 'manufacturer' },
|
|
{ text: 'Model', value: 'model_number' },
|
|
{ text: 'Status', value: 'status' },
|
|
{ text: 'Actions', value: 'actions', sortable: false }
|
|
]
|
|
}),
|
|
|
|
mounted() {
|
|
this.fetchItems()
|
|
},
|
|
|
|
methods: {
|
|
async fetchItems() {
|
|
this.loading = true
|
|
try {
|
|
const response = await this.$http.get('/api/$plugin_name/')
|
|
this.items = response.data
|
|
} catch (error) {
|
|
console.error('Failed to fetch $plural:', error)
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
editItem(item) {
|
|
this.editedIndex = this.items.indexOf(item)
|
|
this.editedItem = Object.assign({}, item)
|
|
this.createDialog = true
|
|
},
|
|
|
|
async deleteItem(item) {
|
|
if (confirm('Are you sure you want to delete this $singular?')) {
|
|
try {
|
|
await this.$http.delete(`/api/$plugin_name/${item.id}`)
|
|
this.fetchItems()
|
|
} catch (error) {
|
|
console.error('Failed to delete $singular:', error)
|
|
}
|
|
}
|
|
},
|
|
|
|
closeDialog() {
|
|
this.createDialog = false
|
|
this.$refs.form.reset()
|
|
this.editedItem = Object.assign({}, this.defaultItem)
|
|
this.editedIndex = -1
|
|
},
|
|
|
|
async saveItem() {
|
|
if (!this.$refs.form.validate()) return
|
|
|
|
try {
|
|
if (this.editedIndex > -1) {
|
|
// Update
|
|
await this.$http.put(`/api/$plugin_name/${this.editedItem.id}`, this.editedItem)
|
|
} else {
|
|
// Create
|
|
await this.$http.post('/api/$plugin_name/', this.editedItem)
|
|
}
|
|
this.fetchItems()
|
|
this.closeDialog()
|
|
} catch (error) {
|
|
console.error('Failed to save $singular:', error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### 8. Add to PLUGIN_TABLE_OWNERS
|
|
|
|
**File:** `shopdb/config.py`
|
|
|
|
```python
|
|
PLUGIN_TABLE_OWNERS = [
|
|
'employees', # Core dependency (if using Employee FK)
|
|
'assets', # Core dependency (Asset polymorphism)
|
|
# ... existing plugins ...
|
|
'$plugin_name', # ADD NEW PLUGIN HERE
|
|
]
|
|
```
|
|
|
|
**Placement considerations:**
|
|
- After plugins you reference (foreign keys)
|
|
- Before plugins that reference you
|
|
- Alphabetical within same dependency level
|
|
|
|
### 9. Create Plugin Tests
|
|
|
|
**File:** `plugins/$plugin_name/tests/test_tooling.py`
|
|
|
|
```python
|
|
"""
|
|
Tests for $plugin_name plugin
|
|
"""
|
|
import pytest
|
|
from shopdb.api import db
|
|
from plugins.$plugin_name.models import $Model_Name
|
|
|
|
|
|
def test_create_$singular(client, auth_header):
|
|
"""Test creating $singular via API"""
|
|
response = client.post('/api/$plugin_name/', json={
|
|
'tool_type': 'Wrench',
|
|
'serial_number': 'WR-001',
|
|
'manufacturer': 'Craftsman'
|
|
}, headers=auth_header)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json
|
|
assert data['tool_type'] == 'Wrench'
|
|
assert data['serial_number'] == 'WR-001'
|
|
|
|
|
|
def test_list_$plural(client, auth_header):
|
|
"""Test listing all $plural"""
|
|
response = client.get('/api/$plugin_name/', headers=auth_header)
|
|
|
|
assert response.status_code == 200
|
|
assert isinstance(response.json, list)
|
|
|
|
|
|
def test_get_$singular(client, auth_header, test_$singular):
|
|
"""Test getting specific $singular"""
|
|
response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json
|
|
assert data['id'] == test_$singular.id
|
|
|
|
|
|
def test_update_$singular(client, auth_header, test_$singular):
|
|
"""Test updating $singular"""
|
|
response = client.put(f'/api/$plugin_name/{test_$singular.id}', json={
|
|
'status': 'in_use'
|
|
}, headers=auth_header)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json
|
|
assert data['status'] == 'in_use'
|
|
|
|
|
|
def test_delete_$singular(client, auth_header, test_$singular):
|
|
"""Test deleting $singular"""
|
|
response = client.delete(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
|
|
|
|
assert response.status_code == 204
|
|
|
|
# Verify deleted
|
|
response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.fixture
|
|
def test_$singular(db_session):
|
|
"""Create test $singular"""
|
|
$singular = $Model_Name(
|
|
tool_type='Hammer',
|
|
serial_number='TEST-001',
|
|
manufacturer='TestCo'
|
|
)
|
|
db_session.add($singular)
|
|
db_session.commit()
|
|
return $singular
|
|
```
|
|
|
|
### 10. Create Plugin README
|
|
|
|
**File:** `plugins/$plugin_name/README.md`
|
|
|
|
```markdown
|
|
# $Display Name Plugin
|
|
|
|
$description
|
|
|
|
## Features
|
|
|
|
- Track $plural with serial numbers
|
|
- Manage tool assignments
|
|
- Monitor tool status (available, in_use, maintenance, retired)
|
|
|
|
## Models
|
|
|
|
### $Model_Name
|
|
|
|
Inherits from `Asset` (ADR-001: Asset as platform contract)
|
|
|
|
**Fields:**
|
|
- `tool_type` (str): Type of tool
|
|
- `manufacturer` (str): Manufacturer name
|
|
- `model_number` (str): Model number
|
|
- `serial_number` (str, unique): Serial number
|
|
- `status` (str): Tool status
|
|
|
|
## API Endpoints
|
|
|
|
- `GET /api/$plugin_name/` - List all $plural
|
|
- `GET /api/$plugin_name/<id>` - Get specific $singular
|
|
- `POST /api/$plugin_name/` - Create new $singular
|
|
- `PUT /api/$plugin_name/<id>` - Update $singular
|
|
- `DELETE /api/$plugin_name/<id>` - Delete $singular
|
|
|
|
## Frontend
|
|
|
|
Main component: `$Plugin_NameList.vue`
|
|
Route: `/$plugin_name`
|
|
|
|
## Development Checklist
|
|
|
|
- [ ] Models defined with proper relationships
|
|
- [ ] Initial migration created and tested
|
|
- [ ] API endpoints implemented with authentication
|
|
- [ ] Frontend component renders list and detail views
|
|
- [ ] Tests written and passing
|
|
- [ ] Added to PLUGIN_TABLE_OWNERS in correct order
|
|
- [ ] Documentation updated
|
|
- [ ] Permissions defined
|
|
- [ ] Sidebar menu item configured
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
pytest plugins/$plugin_name/tests/ -v
|
|
```
|
|
|
|
## Migration
|
|
|
|
```bash
|
|
# Create new migration
|
|
docker compose exec api flask plugin revision -p $plugin_name -m "description"
|
|
|
|
# Apply migration
|
|
docker compose exec api flask plugin upgrade $plugin_name
|
|
```
|
|
```
|
|
|
|
### 11. Summary Report
|
|
|
|
```
|
|
Plugin scaffolding complete: $plugin_name
|
|
|
|
Created files:
|
|
[X] plugins/$plugin_name/plugin.py (BasePlugin subclass)
|
|
[X] plugins/$plugin_name/models/$model_name.py (SQLAlchemy model)
|
|
[X] plugins/$plugin_name/api/routes.py (Flask blueprint, 5 endpoints)
|
|
[X] plugins/$plugin_name/frontend/views/$Plugin_NameList.vue (Vue component)
|
|
[X] plugins/$plugin_name/migrations/ (Alembic initialized)
|
|
[X] plugins/$plugin_name/tests/test_$plugin_name.py (pytest tests)
|
|
[X] plugins/$plugin_name/README.md (documentation)
|
|
|
|
Updated files:
|
|
[X] shopdb/config.py (added to PLUGIN_TABLE_OWNERS)
|
|
|
|
Next steps:
|
|
1. Review generated code and customize as needed
|
|
2. Create initial migration: docker compose exec api flask plugin revision -p $plugin_name -m "Initial schema"
|
|
3. Apply migration: docker compose exec api flask plugin upgrade $plugin_name
|
|
4. Run tests: pytest plugins/$plugin_name/tests/ -v
|
|
5. Start dev server and test frontend: npm run dev
|
|
6. Commit changes: git add plugins/$plugin_name shopdb/config.py
|
|
|
|
Time saved: ~45 minutes of manual scaffolding
|
|
```
|
|
|
|
## Customization
|
|
|
|
After scaffolding, customize:
|
|
|
|
**Models:**
|
|
- Add plugin-specific fields
|
|
- Define relationships to other plugins
|
|
- Add custom methods
|
|
|
|
**API:**
|
|
- Add custom endpoints (search, filters, bulk operations)
|
|
- Implement business logic
|
|
- Add validation schemas
|
|
|
|
**Frontend:**
|
|
- Customize table columns
|
|
- Add filters and search
|
|
- Implement detail views
|
|
- Add charts/visualizations
|
|
|
|
**Tests:**
|
|
- Add tests for custom logic
|
|
- Test edge cases
|
|
- Add integration tests
|
|
|
|
## Related
|
|
|
|
- ADR-001: Asset as platform contract
|
|
- ADR-008: Plugin migration ownership
|
|
- ADR-010: Frontend plugin hooks
|
|
- `CLAUDE.md` - Plugin development guide
|
|
- `/pre-commit` - Test before committing
|