The admin banner counts every plugin load failure, but settings/plugins.html only rendered failure reasons inside the plugin card macro -- which iterates discovered plugins. A plugin that failed BECAUSE it has no directory was therefore counted in the banner and shown nowhere on the page the banner links to. That dead end is what the operator hit: "1 plugin failed to load" on the dashboard, nothing wrong on the details page, and no way to clean it up. Folds the reason into the "Configured but not installed" rows rather than adding a competing section, and drives that list from the UNION of stored plugin.* settings and recorded load failures instead of stored settings alone. A failure with no stored row -- a plugin enabled by a DEFAULTS entry, which is exactly what plugin.http was -- would otherwise still be counted and still be invisible. Rows carry `removable`: only a plugin with a real stored row can be cleaned up, so a default-declared one shows the reason and an explanation instead of a Remove button that would delete nothing while the default reasserts it. Rows with a reason read as an error (red, "Failed to load"); rows that merely have leftover config stay a warning. A discovered plugin that failed is excluded -- its own card already shows the reason. Replaces find_orphaned_plugin_names outright rather than adding a second overlapping helper; it had one caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Steward
A self-hosted network monitoring and infrastructure management hub for home servers. Steward gives you a single pane of glass over your hosts, services, and automation — with live-updating dashboards, alerting, and Ansible integration.
What It Does
- Ping monitoring — TCP/ICMP probes with live latency history and configurable thresholds
- DNS monitoring — resolution checks with optional expected-IP validation
- Ansible — browse playbooks, trigger runs, stream output live
- Alerting — threshold-based rules against any monitored metric, with email and webhook (Discord-compatible) notifications
- Plugins — extend with additional data sources; Traefik metrics included out of the box
- Dashboard widgets — all monitors and plugins contribute live-updating widgets
No JavaScript framework. No build step. No external workers or message brokers.
Quick Start — Docker
cp .env.example .env
# Set STEWARD_DATABASE_URL in .env
docker compose up -d
Open http://localhost:5000. On first run you'll be prompted to create the admin account.
This compose file builds the image locally and bind-mounts the source for live editing. To run a persistent instance off the published CI image instead, see Remote dev instance below.
Remote dev instance
For a long-lived instance on a remote server that tracks the dev CI line, use compose.deploy.yml. It pulls the published image (git.fabledsword.com/bvandeusen/steward:dev) rather than building, runs no source bind-mounts, and persists data in named volumes.
One-time prerequisite — registry push token (on the Git host). CI publishes the image, so the FabledSteward repo needs a push credential. The injected GITHUB_TOKEN lacks write:package, so create a Forgejo token scoped read:package + write:package and add it as the repo Actions secret REGISTRY_TOKEN. Until this exists, the publish CI job 401s and no :dev image is produced.
Standup (on the server):
# 1. Authenticate to the registry (read:package token is enough to pull)
docker login git.fabledsword.com -u <user>
# 2. Configure secrets
cp .env.example .env
# Set POSTGRES_PASSWORD. Leave STEWARD_SECRET_KEY unset to let the app
# generate + persist one on the /data volume.
# 3. Pull + boot
docker compose -f compose.deploy.yml up -d
Open http://<server>:5000 and create the admin account on first run.
Update to the latest dev build:
docker compose -f compose.deploy.yml pull
docker compose -f compose.deploy.yml up -d
Every push to dev that goes CI-green publishes a fresh :dev (and an immutable :<commit-sha> for rollback). To pin a specific commit, change the image: tag in compose.deploy.yml to :<sha>.
Quick Start — Bare Metal
Requires Python 3.11+ and PostgreSQL.
python -m venv .venv && source .venv/bin/activate
pip install -e .
cp config.example.yaml config.yaml
# Edit config.yaml — set database.url at minimum
steward --host 0.0.0.0 --port 5000
ICMP ping requires CAP_NET_RAW or the setuid ping binary. TCP mode (the default) needs no elevated privileges.
Configuration
Only two things must be set to run the app:
| What | How |
|---|---|
| Database URL | STEWARD_DATABASE_URL env var or database.url in config.yaml |
| Secret key | Auto-generated on first run and saved to /data/secret.key |
Everything else — SMTP, webhooks, monitor intervals, Ansible sources, plugin settings — is configured through the web UI at /settings/.
Plugins
Drop a directory into plugins/ and enable it via the Settings UI. The Traefik plugin is included:
plugins/
└── traefik/ ← included; enable in Settings
See docs/plugins/ for a full plugin development guide.
Documentation
| Document | Contents |
|---|---|
docs/architecture.md |
How the app works: startup sequence, routing, scheduler, DB pattern |
docs/core/configuration.md |
Bootstrap config, DB-backed settings, all setting keys and defaults |
docs/core/monitors.md |
Ping and DNS monitors: probe logic, metrics emitted, data models |
docs/core/alerting.md |
Alert rules, state machine, notification channels, template variables |
docs/core/ansible.md |
Playbook sources, run lifecycle, SSE streaming |
docs/plugins/overview.md |
Plugin system: how loading works, plugin.yaml schema, required exports |
docs/plugins/writing-a-plugin.md |
Step-by-step plugin development guide |
docs/plugins/traefik.md |
Traefik plugin: config, metrics, alert rule examples |
docs/reference/code-map.md |
Where every key function, model, and route lives in the codebase |