feat: rename to FabledScryer, multi-dashboard system, plugin management, branding
- Rename package fablednetmon → fabledscryer throughout - Multi-dashboard: ownership, per-user defaults, HTMX edit (add/remove/reorder) - Read-only share tokens scoped to individual dashboards - Dashboard edit is HTMX-driven (no page reloads) - Plugin management system: remote catalog, download/install, hot-reload, in-app restart - plugin_index.py: fetch/cache remote index.yaml; default URL → bvandeusen/fabledscryer-plugins - plugin_manager.py: download_and_install_plugin, hot_reload_plugin, restart_app - ZIP extraction handles GitHub archive formats (name-v1.0.0/, name-main/) - Settings split into tabbed sections: General, Notifications, Ansible, Plugins - Plugins tab: catalog browser (HTMX), install/activate/update/restart actions - UI/branding: dark palette (#07071a), crystal ball SVG logo, animated star field, Libertinus Serif applied to headings, nav, labels, and section titles - Widget registry (core/widgets.py) for dashboard plugin integration - UPS widget.html (dashboard card) and settings/_tabs.html include - Migrations 0005–0008: dashboards, is_default, ownership, share tokens - docs/plugins/: writing-a-plugin.md updated with publishing guide, index.yaml.example template for fabledscryer-plugins repo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# Plugin System Overview
|
||||
|
||||
Plugins extend Fabled Scryer 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 `fabledscryer/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 Fabled Scryer 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 fabledscryer.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 (`fabledscryer/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.
|
||||
Reference in New Issue
Block a user