Commit Graph
22 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 4195b9171a docs(prometheus): writing a widget template, every block, and the format reference (#5694)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m16s
CI / unit (push) Successful in 1m30s
CI / publish (push) Successful in 22s
docs/plugins/prometheus-widgets.md covers:
- where templates live and how to copy one in, duplicate, export and share;
- a first template, walked through;
- each block (states with its deadlines, values, times, table, info) with an
  example;
- the format reference: keys, units, spans, limits, what the checker refuses;
- the Fabled Archives template, block by block, with what it leaves out and
  why.

Linked from the README, the plugin guide, the editor and template_format.
tests/plugins/prometheus/test_widget_guide.py checks every YAML example in the
guide against the checker, so the guide can't drift from the format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:51:56 -04:00
bvandeusenandClaude Opus 5.5 bd762307e0 feat(prometheus): the widget template library: list, edit with live preview, paste in, upload, export, duplicate (#5692)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m16s
CI / unit (push) Failing after 1m29s
CI / publish (push) Skipped
Prometheus gains a "Widget templates" tab beside Targets. It lists the
built-in templates and an admin's own, any that no longer pass the checker
first. A template opens as its YAML beside a live preview drawn over a
chosen target exactly as the dashboard widget draws it (routes.draw, now
shared by both). An admin writes or pastes a new one, reads a .yaml upload
into the editor to look over, duplicates any template under a free name,
edits and deletes their own; anyone can export the YAML. Built-in
templates are read-only.

Custom templates are rows in prometheus_widget_templates (migration
prometheus_002_templates), kept in backups, keyed "custom:<id>" so a
rename keeps its widgets. Saving needs a template that passes the checker
and a name no other template has. Create, update and delete are audited
and link to the template.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:22:31 -04:00
bvandeusenandClaude Opus 5.5 5ca13f9278 feat(prometheus): the Prometheus dashboard widget, target + template (#5691)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m14s
CI / unit (push) Successful in 1m29s
CI / publish (push) Successful in 1m12s
#5691 "Generic Prometheus dashboard widget: pick a target and a template",
step 3 of milestone 508 "Prometheus widget templates".

- /plugins/prometheus/widget draws one target with one template in widget
  shape: the verdict (led by "isn't answering" when the target is down),
  then each block with problems first, figures beside a share bar or the
  last 24h of bars, and the healthy rest folding to "+N more healthy" past
  six. A missing target or template, or one that no longer checks, says so.
- library.py: bundled templates from plugins/prometheus/widgets/*.yaml,
  the picker's target and template lists, and the default title.
- widgets/fabled_archives.yaml: the first bundled template, against the
  series Archives reports (refined in #5693).
- Dashboard: a widget setting may take `options_from` (read when the picker
  opens) with an `empty` sentence; `default_title`; a Details link may name
  settings ("/plugins/prometheus/targets/{target}"). Prometheus is grouped
  with the integrations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:05:15 -04:00
bvandeusenandClaude Opus 5.5 ec3e5e7e0d feat(prometheus): fold state sets on the target page; forget series an endpoint drops (#5689)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m20s
CI / unit (push) Successful in 1m28s
CI / publish (push) Successful in 20s
#5689 "Prometheus target page: fold state sets, show epoch times, size
bytes", step 1 of milestone 508 "Prometheus widget templates".

- A state set (name ending in _state, a state label, every series 0 or 1;
  or the OpenMetrics form, the label named after the metric) reads as one
  row per thing it describes, naming the current state, the others on
  hover. fabledarchives_jobs{state} is a count and stays as it is.
- label_pairs / state_label / fold_states in service.py, for the widget
  templates to reuse.
- After a good scrape, series it no longer has lose their newest value
  (forget_gone): Archives' old build_info row read as current beside the
  new one. History stays; a failed scrape forgets nothing; alert rules keep
  their own last-sample time.
- Epoch times and _bytes sizes were already read by shown_value /
  metric_value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:11:24 -04:00
bvandeusenandClaude Opus 5.5 002181253e fix(prometheus): the list page is a section top; the scrape test builds its text without format(); docs (#5668, #5670)
- tests/test_breadcrumbs.py: the Prometheus target list is its section's
  own top page, like SNMP's, so it carries no trail.
- The integration test's exposition has label braces, which str.format read
  as fields: the fake scrape raised, the target was recorded down and no
  series landed. It substitutes the counter with replace() now.
- docs/plugins/prometheus.md: adding a target, filtering, how series are
  named and stored, alerting, and the Fabled Archives recipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:06:31 -04:00
bvandeusenandClaude Opus 5.5 400e91a0d8 fix(host_agent): a post that reached Steward counts as the agent seen, even if storing it failed (#5514)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m14s
CI / unit (push) Successful in 1m28s
CI / publish (push) Successful in 1m16s
At 20:25-20:39Z the NFS storage under steward_db slowed badly (checkpoint
sync 4.6 s, then 70.7 s with one file at 21.9 s, against the usual 0.002 s).
Ingest commits stalled past the agent's 10 s timeout and were dropped, and
last_seen_at, written in that same transaction, went with them. Agents
posting every few seconds read as silent for 3 minutes, and "Agent offline"
fired and emailed for all three hosts.

Ingest now notes each agent's arrival in memory right after the (read-only)
registration lookup, before any write; seconds_since_seen counts from the
later of last_seen_at and that arrival.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:48:19 -04:00
bvandeusenandClaude Opus 5.5 f67678998c refactor(plugins): no plugin catalog; plugins ship in the image or are mounted (#5374)
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 1m12s
CI / unit (push) Successful in 1m25s
CI / publish (push) Skipped
First-party plugins have shipped inside the image since a7a281c, and the
catalog's only repository (Steward-plugins) answers 404, so Steward polled a
dead URL every minute for nothing. The operator chose to remove the catalog
entirely (#5214 "Review: readable and fast on the live instance").

Removed:
- core/plugin_index.py and the plugin_catalog_refresh startup task;
- the plugins.index_url default, and migration 0035 deletes the stored
  plugins.index_url / plugins.repositories rows;
- the Settings -> Plugins repository editor, catalog, refresh, install and
  reload routes and their templates;
- the download/checksum/extract install path in plugin_manager, and the
  PLUGIN_INSTALL_DIR config it wrote to;
- plugins/index.yaml, docs/plugins/index.yaml.example and the guide's
  "Publishing to the Catalog" section (now "Shipping a Plugin").

Kept: enabling and configuring plugins, hot-reload on enable, restart, orphan
cleanup, and the external root (/data/plugins) for operator-mounted plugins.
tests/test_no_plugin_catalog.py guards the routes, default and task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:15:55 -04:00
bvandeusenandClaude Opus 5.5 9ca2e38fa1 feat(snmp): devices you add, test and read on the SNMP page (milestone 491)
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 55s
CI / unit (push) Successful in 1m24s
CI / publish (push) Skipped
The operator, reviewing SNMP on #5214: "this system looks incomplete like
there's something missing from it". The one device shown was plugin.yaml's
example (core-switch 192.168.1.1, community public), polled every minute;
devices could only be defined by editing YAML in the image; a failed poll was
logged at DEBUG and stored nowhere, so the page said "waiting for the next
poll" forever; v3 ran as v2c; counters showed as lifetime totals.

- Devices are rows (snmp_devices, migration snmp_001): address, port,
  v1/v2c community or a v3 user with auth/priv protocols and keys (secrets
  encrypted), presets plus custom readings, an optional host binding, and
  the newest poll's outcome. The YAML device list and its example are gone.
- The SNMP page adds, edits and deletes them (admin, audited, with
  get_audit_entities so the audit log names and links a device). Readings
  come from presets (system uptime, interface traffic by number, UPS-MIB,
  Linux net-snmp) and custom "OID name [unit]" lines.
- One GET per device with every OID, devices polled at once. The outcome is
  written to the row in words ("No reply from … within 5 s. Check the address
  and port, that SNMP is on, and the community"), with readings it lacks.
  v1's whole-request error drops that OID and asks again. SNMPv3 signs in
  with USM (MD5/SHA-1/SHA-2, DES/3DES/AES).
- Test asks the unsaved form's device for its name, description, uptime and
  interfaces by number. It is an admin's explicit action; every page reads
  only stored rows (System 178's ruling).
- Each reading has a unit; a counter is stored as a per-second rate (bytes as
  bit/s), a reset or wrap skipped. The page, widget and host panel say values
  in units and why a device is silent; the device page draws a chart per
  reading on a dated axis.

Tests: readings and presets, the form and its secrets, v3 checks, rates,
the poller against a stand-in pysnmp (one GET, v1 retry, timeout wording,
USM), the pages, and an integration test of the poll writing outcomes and
rates on Postgres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:54:11 -04:00
bvandeusenandClaude Opus 5.5 5492336f1e feat(dashboard): a curated default layout, merged duplicate widgets, a labelled Overview (#5213)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 50s
CI / unit (push) Failing after 1m16s
CI / publish (push) Skipped
- New installs start from widgets.RECOMMENDED, packed row by row for the
  enabled plugins; the edit drawer can reset a dashboard to it.
- Host Agent — Resources folds into Hosts — Overview (which gains load 1m),
  UniFi — Devices into UniFi — Overview; migration 0032 moves existing
  dashboards onto the survivors.
- The Overview labels what it counts (hosts, monitors up/down/pending,
  alerts firing) and lists firing alerts and down monitors with since-times.
- Uptime polls every 5 minutes (poll_seconds) instead of every cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:42:04 -04:00
bvandeusenandClaude Opus 5.5 41c7509366 feat(mcp): diagnostic tools — overview, hosts, monitors, alerts, metrics, audit
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 36s
CI / unit (push) Successful in 58s
CI / publish (push) Successful in 1m2s
MCP-4 of milestone 461 "MCP server: Steward as an LLM-inspectable,
LLM-operable hub". Twelve read-only viewer tools:

- get_overview: what is wrong now, ranked critical-then-oldest. Failing
  monitors with their error and when the run of failures began, firing,
  pending and still-breaching acknowledged alerts, TLS certificates expiring
  within 14 days, agents that stopped reporting, hosts over the configured
  CPU/memory/disk thresholds (Settings > Thresholds), failed Ansible runs in
  the last 24h, and findings from enabled plugins through a new optional
  `mcp_overview(db)` plugin hook. Each finding names the call that shows
  its evidence.
- list_hosts, get_host, get_host_metrics
- list_monitors, get_monitor (HTTP header values and URL passwords are
  redacted)
- list_alerts, get_alert_history, list_alert_rules, list_maintenance_windows
- query_metrics: the whole metric store, narrowed one argument at a time
  (sources > resources > latest values > a series summary)
- list_audit_events

Shared shaping in steward/mcp/tools/_shape.py: UTC ISO times with age_s,
bounded lists that say when and how they were cut, series as
min/avg/max/p95/last plus a ≤60-point series, lookup by id or
case-insensitive name with a did-you-mean, and a range argument that
refuses an unknown key instead of reading it as 24h.

New core reads in steward/core/metric_series.py (sources and resources
via loose index scans, latest values via DISTINCT ON, series across the
raw/hourly-rollup seam). monitors/service.py gains monitor_states,
down_since and recent_failures; agent_overview_by_host now carries the
fullest disk and the last sample time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 07:39:41 -04:00
bvandeusenandClaude Opus 5.5 3984f639e8 feat(mcp): /mcp endpoint with bearer auth and role-gated tools
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 35s
CI / unit (push) Failing after 1m0s
CI / publish (push) Skipped
Second step of milestone 461. An MCP client (Claude Code, Claude Desktop
via mcp-remote, Open WebUI) connects to /mcp with a personal API token.

- mcp>=2.2 (SDK v2 MCPServer). Mounted by wrapping app.asgi_app, so /mcp
  never enters Quart's pipeline; stateless streamable HTTP so a redeploy
  never strands a client on a session id the new process didn't issue.
- Auth: Authorization: Bearer stw_… resolves to a Caller (user +
  effective role) in a ContextVar; 401 with WWW-Authenticate otherwise.
  The web pipeline's database gate runs first, so a DB restart answers
  503 instead of failing inside a tool.
- Registry, default-deny: register_tool(fn, role=, read_only=) is the only
  door; role and read_only are required. Tools above the caller's role,
  or from a disabled plugin, are neither listed nor callable. Undeclared
  arguments are refused (not silently dropped); a tool's ValueError
  reaches the agent as its reason; a DB failure mid-tool says so.
- Plugins contribute tools via an optional register_mcp_tools(register)
  export, all-or-nothing, replaced on hot-reload. Documented in
  docs/plugins/writing-a-plugin.md.
- First tool: whoami.
- Tests drive app.asgi_app directly (Quart's test client never reaches
  /mcp); the integration test mints a real token against Postgres and
  runs initialize → tools/list → whoami → revoke → 401.

Task #4966 "MCP-2 — /mcp transport".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 07:24:00 -04:00
bvandeusenandClaude Opus 4.8 8c2d994e4c docs(host-agent): document container-log collection [M79 step 6]
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 42s
CI / integration (push) Successful in 2m21s
CI / publish (push) Successful in 5s
Design-spec updates for m79: the docker_logs_enabled / docker_log_exclude config
keys, the collect_docker_logs collector + the drop-logs-on-backoff note in the
agent internals, and a wire-format design point on docker_logs riding in the same
push (with server-side toggle/exclude enforcement + the per-container ring).

The end-to-end chain (store → rotate → query + ingest enforcement) is already
covered by the integration tests added across steps 2–5, so no capstone test is
added. AGENT_VERSION bump (1.7.0) landed in step 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CAGR73DUowdVFVvYzLXC5C
2026-07-19 19:07:53 -04:00
bvandeusenandClaude Opus 4.7 88ab5b917e chore: rename project Roundtable → Steward
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>
2026-04-25 16:20:14 -04:00
bvandeusenandClaude Opus 4.6 44cc2f2fce docs(plugins): catalog entry for host_agent
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 00:31:27 -04:00
bvandeusenandClaude Opus 4.6 b1cfc377da docs(plugins): host_agent plan — path 1 note (no DB tests)
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>
2026-04-14 18:13:41 -04:00
bvandeusenandClaude Opus 4.6 98003f3458 docs(plugins): host_agent implementation plan
15-task TDD plan covering plugin scaffolding, agent collectors, ingest
route, install script, settings UI, widgets, detail page, scheduler,
and integration test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:46:37 -04:00
bvandeusenandClaude Opus 4.6 e243e0e0e1 docs(plugins): host-agent plugin design spec
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>
2026-04-13 23:35:12 -04:00
bvandeusenandClaude Opus 4.6 d6e094bbb5 refactor: remove broken NUT/UPS plugin and managed-NUT startup
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>
2026-04-13 22:41:08 -04:00
bvandeusen 0596f1d9c1 docs: roundtable rename sweep
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.
2026-04-13 20:16:25 -04:00
bvandeusenandClaude Sonnet 4.6 edc9b752d2 feat: drag-and-drop dashboard editing, alert dynamic fields
- 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>
2026-03-23 08:16:04 -04:00
bvandeusenandClaude Sonnet 4.6 3f437141fb chore: update plugin catalog URL to git.fabledsword.com
Default plugins.index_url and all documentation URLs now point to
https://git.fabledsword.com/bvandeusen/FabledScryer-plugins.
Anchor /plugins/ in .gitignore so it only matches the root directory
(not docs/plugins/).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:36:09 -04:00
bvandeusenandClaude Sonnet 4.6 230b542015 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>
2026-03-22 18:27:56 -04:00