Sweeps README, docs/, example configs, code docstrings, report and notification subject lines, plugin catalog URL default, and stray comments in Dockerfile/entrypoint.sh/.gitignore. Adds a legacy note to docs/core/configuration.md explaining the FABLEDSCRYER_* fallback. docker-compose DB credentials intentionally left as fabledscryer to preserve the existing pgdata volume.
6.2 KiB
Plugin System Overview
Plugins extend Roundtable 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 roundtable/core/plugin_manager.py.
For each plugin listed as enabled: true in the PLUGINS config:
- Directory check —
plugins/<name>/must exist plugin.yamlcheck — file must exist and be valid YAML- Name validation —
plugin.yamlnamefield must match the directory name exactly - Version check — if
min_app_versionis set, the running app version must be >= that value (uses SemVer comparison) - Config merge —
plugin.yamlconfigdefaults are merged with user overrides from app settings; result stored inapp.config["PLUGINS"][name] - Import —
importlib.import_module(name)imports the plugin package (the plugins directory is prepended tosys.path) - Export validation — plugin must export
setupandget_scheduled_tasks setup(app)called- Blueprint registration — if plugin exports
get_blueprint(), the returned blueprint is registered at/plugins/<name>/ - Task registration —
get_scheduled_tasks()results are appended toapp._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
# 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 Roundtable 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.
_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.
from roundtable.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.
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:
# 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:
- Adding a
GET /widgetroute to its blueprint that returns an HTMX HTML fragment - The dashboard template polling that endpoint with HTMX
The dashboard (roundtable/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.