- 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>
4.2 KiB
Ansible Integration
Fabled Scryer can browse, trigger, and stream output from Ansible playbooks directly from the web UI. Runs execute as asyncio tasks inside the same process — no Celery, no external workers.
Playbook Sources
Sources are configured at /settings/ under Ansible. Two source types are supported:
Local Filesystem
Points to a directory on the host that already contains playbooks.
# In app_settings (configured via UI)
ansible.sources:
- name: "homelab"
type: local
path: "/opt/playbooks"
Git Repository
The app clones or pulls the repo into a local cache directory on a configurable schedule. All execution uses the local cache.
ansible.sources:
- name: "infra-repo"
type: git
url: "https://github.com/example/infra.git"
branch: "main"
pull_interval_seconds: 300
cache_path: "/data/playbook_cache/infra-repo"
Git sources register a ScheduledTask named ansible_git_pull_<name> that runs git pull on the configured interval.
Inventory Discovery
The app discovers inventory files within the root of the playbook source directory (non-recursive). It looks for files named:
hostsinventoryinventory.ymlinventory.ini
A manual relative path can also be entered in the UI (e.g. inventories/production/hosts) for inventories in subdirectories.
Triggering a Run
From the UI at /ansible/, you can:
- Browse available playbooks across all sources
- View playbook contents before running
- Select an inventory file
- Trigger a run (requires
operatororviewerrole — execution is restricted tooperator/admin)
The run flow:
- UI submits
POST /ansible/runswithplaybook_path,source_name, andinventory_path - An
AnsibleRunrow is created withstatus = running - Playbook execution starts as
asyncio.create_task() - The response returns the
run_idand an HTMX partial that wires the SSE subscription - The browser connects to
GET /ansible/runs/<run_id>/streamto receive live output
SSE Streaming
Run output is streamed to the browser via Server-Sent Events (SSE) at:
GET /ansible/runs/<run_id>/stream
Each output line is sent as:
event: output
data: <line of stdout/stderr>
Run completion:
event: done
data: success|failed|interrupted
Output is flushed to the ansible_runs.output DB column every 50 lines or every 5 seconds (whichever comes first) and always on completion. This means partial output survives a process crash.
If a client connects after the run has already completed, the endpoint immediately sends event: done with the final status and closes the stream. To view stored output, use the run history view at GET /ansible/runs/<run_id>.
Run Lifecycle
| Status | Description |
|---|---|
running |
Execution in progress |
success |
Playbook exited 0 |
failed |
Playbook exited non-zero |
interrupted |
App restarted while run was in progress |
On startup, the app marks any runs still in running state as interrupted (see _mark_interrupted_runs() in app.py).
Output stored in the DB is capped at 1 MB. If truncated, [output truncated] is appended to the DB column, but the live SSE stream continues unaffected.
Data Model
ansible_runs table (defined in fabledscryer/models/ansible.py):
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
playbook_path |
str | Relative path to playbook |
inventory_path |
str | Inventory path used |
source_name |
str | Name of the playbook source |
triggered_by |
FK → users | |
status |
enum | running, success, failed, interrupted |
started_at |
timestamp UTC | |
finished_at |
timestamp UTC | Null if still running |
output |
text | Captured stdout/stderr (capped at 1 MB) |
Runs older than data.retention_days (default 90) are pruned by the data_cleanup scheduled task.
Source Files
| File | Purpose |
|---|---|
fabledscryer/ansible/sources.py |
Source discovery, git pull logic |
fabledscryer/ansible/executor.py |
Subprocess execution and output streaming |
fabledscryer/ansible/routes.py |
HTTP routes (browse, trigger, stream, history) |
fabledscryer/models/ansible.py |
AnsibleRun model |