88ab5b917e
Renames the Python package directory, CLI command, env var prefix, docker-compose service/container/image, Postgres role/db, and all visible branding. Marketing form is "Fabled Steward". Clean break from the previous rebrand: drops the fabledscryer→roundtable import shim in __init__.py and the FABLEDSCRYER_* env var fallback in config.py and migrations/env.py. Env vars are now STEWARD_* only. Heads-up for existing deployments: - Postgres user/db renamed fabledscryer → steward in docker-compose.yml. Existing volumes need the role/db renamed inside Postgres, or override POSTGRES_USER/POSTGRES_DB to keep the old names. - Host-agent systemd unit is now steward-agent.service. Existing agents keep running under the old name; reinstall to switch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
174 lines
6.2 KiB
Markdown
174 lines
6.2 KiB
Markdown
# Plugin System Overview
|
|
|
|
Plugins extend Steward with new data sources, UI pages, scheduled tasks, and dashboard widgets. Only enabled plugins are imported — disabled or unlisted plugins have zero runtime overhead.
|
|
|
|
---
|
|
|
|
## How Plugins Are Loaded
|
|
|
|
Plugin loading happens in step 9 of `create_app()`, after all core blueprints and tasks are registered. The entrypoint is `load_plugins(app)` in `steward/core/plugin_manager.py`.
|
|
|
|
For each plugin listed as `enabled: true` in the `PLUGINS` config:
|
|
|
|
1. **Directory check** — `plugins/<name>/` must exist
|
|
2. **`plugin.yaml` check** — file must exist and be valid YAML
|
|
3. **Name validation** — `plugin.yaml` `name` field must match the directory name exactly
|
|
4. **Version check** — if `min_app_version` is set, the running app version must be >= that value (uses SemVer comparison)
|
|
5. **Config merge** — `plugin.yaml` `config` defaults are merged with user overrides from app settings; result stored in `app.config["PLUGINS"][name]`
|
|
6. **Import** — `importlib.import_module(name)` imports the plugin package (the plugins directory is prepended to `sys.path`)
|
|
7. **Export validation** — plugin must export `setup` and `get_scheduled_tasks`
|
|
8. **`setup(app)`** called
|
|
9. **Blueprint registration** — if plugin exports `get_blueprint()`, the returned blueprint is registered at `/plugins/<name>/`
|
|
10. **Task registration** — `get_scheduled_tasks()` results are appended to `app._task_registry`
|
|
|
|
If any step fails, the plugin is skipped with an error log and startup continues.
|
|
|
|
---
|
|
|
|
## Plugin Directory Structure
|
|
|
|
```
|
|
plugins/
|
|
└── myplugin/
|
|
├── __init__.py # Required: setup(), get_scheduled_tasks(), optionally get_blueprint()
|
|
├── plugin.yaml # Required: name, version, description
|
|
├── models.py # SQLAlchemy models (optional if plugin has no DB tables)
|
|
├── routes.py # Quart Blueprint (optional if plugin has no UI)
|
|
├── scheduler.py # Task logic (optional if plugin has no background tasks)
|
|
├── migrations/ # Alembic migrations for plugin DB tables
|
|
│ ├── env.py
|
|
│ ├── script.py.mako
|
|
│ └── versions/
|
|
└── templates/
|
|
└── myplugin/ # Jinja2 templates (namespaced under plugin name)
|
|
├── index.html
|
|
└── widget.html
|
|
```
|
|
|
|
---
|
|
|
|
## plugin.yaml Schema
|
|
|
|
```yaml
|
|
# Required
|
|
name: myplugin # Must match directory name exactly
|
|
version: "1.0.0" # SemVer
|
|
description: "Does a thing"
|
|
|
|
# Optional
|
|
author: "Your Name"
|
|
min_app_version: "0.1.0" # Minimum Steward version required
|
|
|
|
# Default config — merged with user overrides at runtime
|
|
# Access at runtime via: app.config["PLUGINS"]["myplugin"]["my_setting"]
|
|
config:
|
|
my_setting: "default_value"
|
|
poll_interval_seconds: 60
|
|
```
|
|
|
|
---
|
|
|
|
## Required Python Exports
|
|
|
|
Every plugin's `__init__.py` must export:
|
|
|
|
### `setup(app: Quart) -> None`
|
|
|
|
Called once during app startup. Use it to store the `app` reference for use in scheduled tasks, and to import models so they register with SQLAlchemy metadata.
|
|
|
|
```python
|
|
_app = None
|
|
|
|
def setup(app):
|
|
global _app
|
|
_app = app
|
|
from .models import MyModel # noqa: registers model with Base.metadata
|
|
```
|
|
|
|
### `get_scheduled_tasks() -> list[ScheduledTask]`
|
|
|
|
Returns a list of `ScheduledTask` objects. Return `[]` if the plugin has no background tasks. Called after `setup()`, so any app references set in `setup()` are available.
|
|
|
|
```python
|
|
from steward.core.scheduler import ScheduledTask
|
|
|
|
def get_scheduled_tasks():
|
|
app = _app
|
|
|
|
async def my_task():
|
|
async with app.db_sessionmaker() as session:
|
|
async with session.begin():
|
|
# do work
|
|
pass
|
|
|
|
return [
|
|
ScheduledTask(
|
|
name="myplugin_task",
|
|
coro_factory=my_task,
|
|
interval_seconds=app.config["PLUGINS"]["myplugin"]["poll_interval_seconds"],
|
|
run_on_startup=True,
|
|
)
|
|
]
|
|
```
|
|
|
|
### `get_blueprint() -> Blueprint` (optional)
|
|
|
|
Return a Quart `Blueprint`. The plugin manager mounts it automatically at `/plugins/<name>/`. Do not set `url_prefix` on the blueprint itself.
|
|
|
|
```python
|
|
def get_blueprint():
|
|
from .routes import my_bp
|
|
return my_bp
|
|
```
|
|
|
|
---
|
|
|
|
## Plugin Migrations
|
|
|
|
Plugins manage their own Alembic migrations in `migrations/versions/`. Plugin migrations run after core migrations, so the core schema (including `app_settings`) is always available.
|
|
|
|
Each plugin's initial revision declares a dependency on the core migration head using `depends_on` (not `down_revision`), keeping the plugin on a separate migration branch:
|
|
|
|
```python
|
|
# In the plugin's first migration file:
|
|
depends_on = ("0004_core_head_revision_id",)
|
|
down_revision = None
|
|
branch_labels = ("myplugin",)
|
|
```
|
|
|
|
Revision IDs should be prefixed with the plugin name to avoid collisions:
|
|
|
|
```
|
|
myplugin_001_initial.py
|
|
myplugin_002_add_column.py
|
|
```
|
|
|
|
If a plugin is disabled after its migrations have been applied, its tables remain in the database. Re-enabling it skips already-applied migrations and resumes normally.
|
|
|
|
---
|
|
|
|
## Dashboard Widgets
|
|
|
|
A plugin can contribute a dashboard widget by:
|
|
|
|
1. Adding a `GET /widget` route to its blueprint that returns an HTMX HTML fragment
|
|
2. The dashboard template polling that endpoint with HTMX
|
|
|
|
The dashboard (`steward/templates/dashboard/index.html`) currently includes the Traefik widget conditionally based on `traefik_enabled`. To add a new plugin widget, the dashboard route and template both need to be updated to detect and render the new widget. See the Traefik widget implementation as a reference.
|
|
|
|
---
|
|
|
|
## Available Context in Plugins
|
|
|
|
Inside scheduled tasks and request handlers, the following is available via `app` or `current_app`:
|
|
|
|
| Attribute | Type | Description |
|
|
|---|---|---|
|
|
| `app.config["PLUGINS"]["myplugin"]` | dict | Merged plugin config (defaults + user overrides) |
|
|
| `app.db_sessionmaker` | async_sessionmaker | Open DB sessions |
|
|
| `app.logger` | Logger | Standard Python logger |
|
|
| `app.config["SMTP"]` | dict | Email config |
|
|
| `app.config["WEBHOOK"]` | dict | Webhook config |
|
|
|
|
During `setup(app)`, do not open DB sessions — the event loop is not yet running. Store the `app` reference and open sessions inside coroutines only.
|