The on-disk-version test hardcoded '1.0.0' and broke on the 1.1.0 bump. Compare
against agent.AGENT_VERSION so future bumps don't require touching the test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agent now detects its own primary non-loopback IP (UDP-socket trick, no
packets sent) and sends it in the metadata bag; bumps AGENT_VERSION 1.0.0->1.1.0
and re-detects on SIGHUP. The server persists it on HostAgentRegistration.host_ip
and mirrors it into Host.address ONLY when that field is blank — an admin-typed
address is never overwritten. The reported IP shows in the settings table so
admins can see drift regardless.
- agent.py: detect_primary_ip(); host_ip in collect_metadata(); refresh on reload
- routes.py: pure pick_host_address() helper; ingest persists host_ip + mirrors
- models.py + migration host_agent_002_host_ip: new String(45) column
- settings_list.html: Reported IP column
- plugin.yaml: 1.0.0 -> 1.1.0
- tests: pick_host_address (fill-when-blank / never-clobber / no-op),
detect_primary_ip (never raises, non-loopback), metadata carries host_ip
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fabledscryer->steward rename had only ever reached host_agent. The other
five bundled plugins (http, snmp, traefik, unifi, docker) still imported
`from fabledscryer.*` (package no longer exists) and read FABLEDSCRYER_* env
vars — so every one of them was broken at import since the original rebrand.
CI stayed green only because none are enabled by default and migrations don't
import plugin modules. Now that they version in-tree, complete the rename:
- fabledscryer.* -> steward.* imports across all five plugins
- FABLEDSCRYER_* -> STEWARD_* in plugin migration env.py files
- author/repository/homepage + user-facing 'Fabled Scryer' strings -> Steward
- snmp/scheduler.py: also drop dead `now`/datetime; record_metric from steward
Adds tests/test_no_legacy_names.py — fails if 'scryer'/'roundtable' ever
reappear in shipped code (the drift bit twice; this stops a third time).
Also clears pre-existing ruff lint debt (unused imports, semicolon statements,
mid-file import) surfaced by the new lint lane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-repo CI now that plugins are bundled in-tree. Three lanes on push to
dev/main, modeled on FabledCurator's canonical ci.yml (minus frontend/Redis):
- lint: ruff check steward/ plugins/ tests/ (no dep install)
- unit: pytest -m 'not integration' — whole current suite (testing=True mocks DB)
- integration: postgres:16-alpine service via socket-discovered bridge IP; a
boot-and-migrate test creates the real app, running core + all bundled-plugin
migrations, then round-trips the DB — guards the folded-in migration graph.
Registers the 'integration' pytest marker; adds tests/integration/test_boot.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First-party plugins (host_agent, http, snmp, traefik, unifi, docker) are now
tracked under plugins/ and baked into the image, so they version atomically
with core — ending the cross-repo import drift the roundtable->steward rename
exposed. History for these files is preserved in the archived Roundtable-plugins
repo.
Plugin discovery becomes multi-root: PLUGIN_DIR (single) -> PLUGIN_DIRS
(bundled first, then external) + PLUGIN_INSTALL_DIR. Bundled ships in the image;
third-party plugins still mount at runtime into the external root
(STEWARD_PLUGIN_DIR, default /data/plugins) and downloads/installs land there.
Bundled shadows external on a name collision.
- config.py: load_bootstrap returns plugin_dirs + plugin_install_dir
- app.py: iterate PLUGIN_DIRS at the migration + load sites
- migration_runner.py: discover_all_in() unions every plugin root
- plugin_manager.py: resolve_plugin_path() (pure, first-root-wins); load /
install / hot-reload span all roots; installs target the external root
- settings/routes.py: _discover_plugins scans all roots, dedup bundled-first
- Dockerfile: COPY plugins/ ; docker-compose: drop host bind, document external
- tests/test_plugin_dirs.py: resolution, multi-root discovery, bootstrap split
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Add read_config/ConfigError unit tests covering flat key-value parsing,
blank lines/comments, list fields, defaults, missing required keys, and
malformed lines. Also add tests/__init__.py to fix pytest namespace
collision with tests/plugins/ shadowing the root plugins/ package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records the mid-execution pivot: test harness uses testing=True which
mocks the DB, so plugin tests stick to pure-Python unit tests (agent +
metric expander). Server routes are verified manually.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Design for a push-based Python agent that collects CPU/memory/storage/
load/uptime from remote Linux hosts and POSTs to a new host_agent
plugin. Uses per-host bearer tokens, systemd install via curl|sh,
in-memory ring buffer with exponential backoff, and writes to the
existing PluginMetric bus plus a plugin-private registrations table.
No core schema changes — plugin stays strictly isolated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The UPS plugin's default driver was snmp-ups, so NUT was just translating
network SNMP back into its own protocol — no value over polling the UPS
directly through the existing snmp plugin. The managed-NUT entrypoint path
also broke container startup when config was incomplete.
Deletes nut_setup.py, the NUT block from entrypoint.sh, NUT packages from
the Dockerfile, the ups entry from the plugin catalog example, and the
ups wiring in alerts METRIC_CATALOG, widgets, settings routes, and the
rules form. The plugins/ups source tree (untracked) is also removed from
the working copy. Existing ups_* DB tables are orphaned but harmless in
dev; a drop migration isn't needed.
Follow-up: rebuild "on battery → Ansible shutdown" automation on top of
the alerts system as a new action type that runs a playbook.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Updates nav brand, page <title> blocks across all templates, and the
read-only share view footer. Removes duplicate Roundtable heading from
login card now that the nav wordmark reads correctly.
Service, container_name, image, and env var name flipped to roundtable.
DB credentials kept as fabledscryer to preserve the existing pgdata
volume — a separate migration can rename the role/db later if desired.
Reads ROUNDTABLE_<NAME> first, falls back to FABLEDSCRYER_<NAME> so
existing deployments keep working through the rebrand transition.
Covers DATABASE_URL, DATABASE__URL, SECRET_KEY, PLUGIN_DIR, CONFIG.
Keeps the image buildable after the package rename. Image name, container
name, compose service, and env vars still flip in PR 4.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Meta path finder redirects fabledscryer[.*] imports to roundtable[.*],
keeping unmodified plugin repos working during the rebrand transition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Plugin settings UI:
- Settings → Plugins now shows a clean list (status dot, name, enabled badge)
with a Settings button per plugin linking to its own detail page
- Per-plugin detail page at /settings/plugins/<name>/ shows enable toggle,
all config fields, load status, and failure notice if the plugin errored
- Config saving now scoped per-plugin via POST /settings/plugins/<name>/;
removes the monolithic save-all-plugins form
Plugin repositories:
- Replace single Plugin Index URL field with a managed list of repositories
(HTMX add/edit/remove per entry, same pattern as Ansible sources)
- plugin_index.py: fetch_catalog() now accepts a list of URLs, caches per-URL,
and merges results deduplicating by plugin name (first repo wins)
- Legacy plugins.index_url is auto-migrated to the new list format on first
access so existing installs don't lose their configured URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plugin failures:
- Track load-time failures in _FAILED_PLUGINS dict with human-readable reasons
- Inject plugin_failures into all templates via context processor
- Show admin-only warning banner in base.html when any plugin fails to load
- Show inline failure notice per plugin card in Settings → Plugins
NUT managed mode:
- entrypoint.sh: make nut_setup.py failure non-fatal so the container starts
even if nut_ups_host isn't set yet (allows fixing config via UI)
- save_plugins: when nut_managed is enabled, auto-set nut_host=localhost so
the plugin connects to the managed NUT instance without extra manual config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace the raw JSON textarea in Settings → Ansible with a proper
per-source card UI: add, inline-edit, remove, and git-sync each
source independently via HTMX without a page reload
- Each source card shows type badge (git/local), URL or path, branch,
and pull interval; git sources get a Sync button that triggers
git pull and returns inline status
- Add source form uses a type-aware select that shows URL/branch/interval
fields for git or path field for local, collapsed in a details accordion
- Add new settings routes: sources/add, sources/<idx>/remove,
sources/<idx>/save, sources/<idx>/sync
- Update browse.html and run_detail.html to use CSS vars throughout
(removed hardcoded hex colors); link empty state to Settings page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace up/down arrow reorder buttons on dashboard edit page with
SortableJS drag handles; add POST /d/<id>/edit/reorder endpoint
accepting ordered widget ID array
- Add {% block extra_scripts %} hook to base.html for page-specific JS
- Add dynamic alert rule field loading via HTMX partial (_rule_fields)
so metric/threshold fields update when source is changed
- Add docs/plugins/index.yaml.example showing catalog index format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add managed NUT mode: configure NUT daemon from Settings → Plugins
(UPS plugin), writes /data/nut_managed.json read by entrypoint on
restart — no env vars or USB passthrough required
- entrypoint.sh: start NUT from /data/nut_managed.json or NUT_MANAGED=1
env var; drop to app user via gosu after NUT daemons start
- nut_setup.py: support --from-file <json> in addition to env vars
- Dockerfile: add nut, nut-client, gosu packages and entrypoint
- docker-compose.yml: document optional NUT_MANAGED env var block
- Fix plugin hot-reload migration failure: pass all plugin migration
dirs to Alembic so previously-stamped revisions from other plugins
remain resolvable (fixes UPS and any plugin with depends_on)
- Fix plugin list-type config (e.g. SNMP devices) rendering as broken
text input — now shows a read-only note to edit plugin.yaml instead
- Fix _sync_nut_managed_config: only write JSON when nut_ups_host is
non-empty, preventing NUT startup loop on container restart
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
plugins/ is now tracked at bvandeusen/fabledscryer-plugins.
Removed tracked traefik files from this repo's index (files remain on disk).
Added plugins/ to .gitignore so the directory is ignored going forward.
PLUGIN_DIR in config.yaml still points to plugins/ for local dev and Docker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace base.html style block with full CSS design system using custom properties (dark theme, cards, tables, badges, dots, widgets)
- Add DNS blueprint (fablednetmon/dns/) with /dns/ and /dns/rows routes + templates
- Add Traefik /widget route and widget.html fragment for dashboard
- Update dashboard: DNS + Traefik widgets, traefik_enabled config detection
- Update all templates to use new CSS classes (page-title, section-title, card-flush, table, badge-*, dot-*, btn-*)
- Register dns_bp in app.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace individual volume mounts (playbook_cache, plugins) with single app_data:/data mount
- Change FABLEDNETMON_DATABASE__URL to FABLEDNETMON_DATABASE_URL (underscore to hyphen)
- Simplify config.example.yaml to focus on required database URL setting
- Document that other settings are now managed via the Settings UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>