8af297670eb611fc245ecf733ea9ae2e41979101
30 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8af297670e |
feat(metrics): roll plugin_metrics up to hourly to bound storage
plugin_metrics grows by (sources × resources × ~30s cadence); keeping 90d of raw
is a large table. Add a raw→hourly rollup (mirroring the Docker plugin) so only a
short raw window is kept at full resolution, with hourly averages archived longer.
- PluginMetricHourly model + core migration 0024 (plugin_metrics_hourly: avg/max/
count per source/resource/metric/hour, unique bucket constraint + lookup index).
- steward/core/metrics_retention.rollup_plugin_metrics: date_trunc('hour') agg of
raw older than the hour-aligned raw window, idempotent pg upsert into hourly,
delete the rolled raw, prune hourly beyond the rollup window.
- cleanup.py: plugin_metrics is no longer blanket-deleted at data.retention_days;
_run_metrics_retention drives the rollup with windows read live from settings.
- Settings: metrics.retention.raw_days (7) + rollup_days (90), tunable on the
Thresholds & Retention page (new "Host metrics retention" card).
- Chart read: _history_for_host merges the hourly rollup (older part of the range)
with raw date_bin (recent part, capped ≤1h), so 30d charts keep working —
recent at full resolution, older at hourly. Route passes raw_days from settings.
- Tests: unit (cutoff helpers) + integration (rollup aggregates/prunes; history
merges hourly + raw) against Postgres.
Speed was already handled by the indexes + SQL aggregation; this is the storage
lever (raw window ~10x smaller).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
|
||
|
|
b0d3e83bdd |
feat(hosts): true live vitals strip; agent panel becomes management-only
Realizes the chosen host-summary layout: a thin live vitals bar at the very top, separate from the agent management panel (no more duplicated CPU/MEM/DISK/LOAD). - New fragment _host_vitals.html + route /plugins/host_agent/vitals/<id>: compact CPU / Memory / Disk(/) / Load-per-core (threshold-coloured + sparkline) + Pressure + live/stale·version·last-seen. Polled every 15s; renders nothing until the agent reports. - The metric computation (latest snapshot + 6h sparkline query + load/core + PSI) moves from host_panel into host_vitals. host_panel slims to management only (reg/target/reporting/stale/ansible) and no longer queries metrics; panel.html drops the gauge row + pressure block, keeping status + lifecycle actions. - hosts/detail.html: vitals strip on top (full width, live), then a 2-col [Monitors | Agent] grid, Ansible full width below, docker fragment last. UI only. Templates parse; plugin-template parse test covers the new fragment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC |
||
|
|
9e4f1983f8 |
feat(host_agent): lazy-load full-metrics charts + live-poll current state
The full-metrics page rendered once server-side with the history query inline, so the charts blocked first paint and nothing refreshed without a reload (it reads the latest snapshot the server holds — the agent pushes ~every 30s). Split it into a shell + two HTMX fragments: - Shell (host_detail.html): header + shared time-range toggle + two containers; paints instantly. - Current state (/<id>/metrics → _host_metrics.html): identity + gauges + per-core + filesystems + interfaces/disks + temps, from the DISTINCT ON latest query. hx-trigger "load, every 15s" → live numbers at ~the agent cadence. - History charts (/<id>/charts → _host_charts.html): the 3 charts + Chart.js, from the date_bin history query. hx-trigger "load, every 60s, rangeChange" so they lazy-load (never block paint), refresh slowly, and follow the range selector. Leak-safe: previous Chart instances are destroyed before re-render. - Range toggle switched from full-reload links to the shared _time_range.html (setTimeRange + rangeChange), so only the fragments refetch. - routes: host_detail (shell) + host_detail_metrics + host_detail_charts, with a _split_host_metrics helper. - tests/test_templates_parse.py now also parses plugin templates (these fragments aren't rendered in the unit lane). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC |
||
|
|
a78af23793 |
refactor(host_agent): extract metric-query helpers to a model-free module
The previous commit's integration test failed at collection-time import:
importing plugins.host_agent.routes (which imports the host_agent ORM models at
top level) double-registers host_agent_registrations against the app-loaded
plugin's metadata ("Table already defined").
Move the two pure read helpers (_latest_metrics_for_host, _history_for_host)
plus SOURCE_MODULE / HISTORY_METRICS into plugins/host_agent/metrics_query.py,
which imports only the core PluginMetric model — no plugin models. routes.py
imports them back. The integration test now imports from metrics_query and no
longer trips the loader's registration guard. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
|
||
|
|
aff0c36d37 |
perf(host_agent): aggregate metric history in SQL; DISTINCT ON for latest
Follow-on to the plugin_metrics indexes. plugin_metrics is already retention- bounded (core cleanup prunes > data.retention_days, default 90d) and charts top out at 30d, so the cost wasn't growth — it was the read path shipping raw rows to Python. - _history_for_host: bucket + average in SQL via date_bin (epoch-aligned, ~120 buckets) instead of fetching every raw sample (a 30d range was hundreds of thousands of rows) and downsampling in Python. Uses the new (source_module, resource_name, recorded_at) index. - _latest_metrics_for_host: DISTINCT ON (resource_name, metric_name) ORDER BY recorded_at DESC — newest row per group in one index-ordered pass, replacing the GROUP-BY-max subquery self-joined back to the whole history. - Integration test validates both against Postgres. Deliberately not a materialized raw→hourly rollup: these query-side changes deliver the speed; a rollup would additionally cut storage and remains a future option if scale demands it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC |
||
|
|
a840d6f823 |
feat(docker): collect + persist /system/df image/disk usage (agent 1.6.0)
Backend for the image/disk panel (milestone 77 #942). Agent gains collect_disk_usage() — one /system/df call (gated on containers existing, so Docker-less hosts pay nothing), surfacing reclaimable bytes (image size held by unreferenced images), layers/containers/volumes/build-cache sizes, and the top 50 images by size. Emitted as sample["docker_disk"]; host_agent ingest tracks the newest sample's copy and hands it to the docker capability as a 5th arg. New current-state tables docker_disk_usage (one row/host) + docker_images (per-host image rows), docker_007 migration; ingest upserts the summary and replaces the image set per host (stale images pruned). Unit tests for the df parsing/reclaimable math + build_sample gating; integration test for persistence + image-set replacement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC |
||
|
|
578cc33cc0 |
feat(docker): ingest swarm topology + lifecycle events + health/restart alerts
Wires the agent's enriched + swarm payloads through the docker.persist_host_
samples capability:
* Swarm topology — persist sample["swarm"] into docker_swarm_services /
docker_swarm_nodes (upsert + prune stale, host-scoped so two managers don't
clobber). Migration docker_005 adds services.placement_json for the
task→node placement the agent now reports.
* Lifecycle events — _derive_events (pure, unit-tested) diffs the newest
snapshot against stored per-container state: start / stop / die (non-zero
exit) / oom / health_change → docker_events rows. Skipped on a host's first
snapshot so the baseline doesn't emit a start per existing container.
* Alerts — record restart_count (always) and is_healthy (1.0/0.0, only when a
HEALTHCHECK exists) alongside cpu/mem, under host-scoped resource names;
METRIC_CATALOG[docker] gains restart_count + is_healthy so they're alertable.
host_agent ingest captures the newest sample's swarm object and threads it to
the capability (now persist_host_docker(session, host, snapshots, swarm=None));
invoked when containers OR swarm are present, under the same SAVEPOINT. Unit
tests cover the event-diff matrix; integration tests cover event derivation
across two snapshots and swarm topology round-trip (incl. placement).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
|
||
|
|
7b80552a7d |
feat(docker): per-host collection via the host agent; drop central scrape
Docker collection moves off the central single-socket scrape onto the host
agent, giving Docker a real per-host dimension. The Steward host now reports
its own containers like any other host, and same-named containers on different
hosts no longer collide.
- agent: stdlib UDS Docker client (AF_UNIX HTTP/1.1, Connection: close,
chunked-aware), collect_docker() ports the cpu%/mem math; sample["docker"]
added best-effort (silent-skip on absent/unreadable socket). AGENT_VERSION
1.2.0 → 1.3.0; optional docker_socket config key.
- ingest: host_agent ingest hands per-host container snapshots to the docker
plugin via a new "docker.persist_host_samples" capability (no hard import,
no-op when docker disabled), inside a SAVEPOINT so a docker failure never
sinks the host metrics. Resource names are host-scoped ("<host>/<name>").
- schema: docker_containers re-keyed (host_id, name); docker_metrics gains
host_id; docker_002 migration DROP+recreates (dev-only, rule 122).
- ui: Docker page + widgets grouped by host with host links; new per-host
Docker panel embedded on the Hosts hub (gated on docker enabled via a new
enabled_plugins template context). Replaces the SQLite-only strftime
bucketing with DB-agnostic Python bucketing.
- provisioning: install/provision playbooks add steward-agent to the docker
group (best-effort) so the agent can read the socket.
- removed central scrape: docker scheduler.py + scraper.py deleted; plugin.yaml
socket_path/scrape_interval_seconds/include_stopped dropped (plugin 2.0.0).
- tests: agent docker collector units (math, chunked decode, silent-skip,
sample shape, config) + integration (host-scoped schema + persistence).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
|
||
|
|
e7b96fbfa7 |
feat(host_agent): surface network, disk I/O, and temperature in fleet widget
The agent already collects + ingests net throughput, disk I/O, and temps; add them to the fleet-glance rows (fmt_bps + two-line io cells + temp with 70/85C threshold color), each shown only when the host reports it so VMs/ containers without sensors don't show blank cells. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
10808c1c5d |
fix(dashboard): inline cpu sparkline, history graph host label + stable y-axis
- Hosts-Overview: move the CPU sparkline inline next to the cpu % value it represents (was floated far right on the name line, reading as unrelated). - Host Agent history widget: caption the chart with the host it represents (the panel title is generic) — links to the host hub. - History widget: snap the y-axis to a stable 0..next-10%-band ceiling instead of auto-scaling to the exact peak, so the 'zoom' no longer jumps each poll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fed9973899 |
fix(dashboard): downsample history graphs + readable tooltip time
The agent reports every few seconds, so multi-hour history series were hundreds– thousands of points — a dense, noisy line (esp. CPU). Bucket-average server-side to ~120 points (keeps the shape, drops the noise) for both the history-graph widget and the host-detail charts. Also fix the chart tooltip title showing the raw epoch-ms (e.g. 1,781,720,471,459) — format it as HH:MM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cb47b5e977 |
feat(dashboard): per-metric sparklines in the Host Agent — Resources widget
Operator ask: show the graphs next to their fields in the fleet widget, like the host page's AGENT panel. Each row now renders cpu/mem/disk/load as a value with a trend sparkline beneath it (1h window), instead of bare numbers. - _fleet_rows fetches a per-host recent series (cpu/mem/load host-level, disk from the root mount) in one 1h query and attaches a sparkline per metric to each row. - widget_table.html lays out a metric cell (label + threshold-coloured value + sparkline) per field, mirroring panel.html. Threshold colour is computed in the loop and passed into the cell macro (keeps Jinja macro scope clean). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
988c13d51f |
feat(dashboard): phase C richer panels — host time-series graph + cpu sparklines
Milestone 72 phase C — bring the host-view graphs onto the dashboard:
- host_resource_history widget reworked into a real host-view chart: epoch-ms
linear axis (no Chart.js date adapter), themed like the host-detail charts,
maintainAspectRatio:false so it fills the resized panel, unique canvas per
widget instance (wid), and empty states ("pick a host" / "no metrics yet").
Was previously unusable — it had a broken time axis and no way to choose a host.
- Add a "host" param type: the edit form renders a live dropdown of hosts
(dashboard routes now pass the host list to the editor); the chosen host_id is
stored in config and fed to the widget.
- Hosts-overview widget gains a per-row CPU sparkline (last hour) via the shared
sparkline_svg helper — the host-view at-a-glance trend, on the main widget.
Charts/sparklines render only in the browser, so CI can't exercise them — needs
an operator visual check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e0253fba48 |
fix(ansible): surface run failure reason; widget audit cleanup
Run UX: when start_run throws before/around launch (e.g. ENOSPC creating the temp dir — the box is out of disk), the run was marked "failed" with empty output. Now the exception is broadcast + written to the run output/results so the run view shows e.g. "[run error] OSError: [Errno 28] No space left on device" instead of a blank failure. Widget audit follow-ups (no broken links were found; these are consistency): - host_resource_history widget now charts root (/) disk, consistent with the host panel (was the opaque "disk worst"). - host_resources widget: tooltip on the health dot explaining it warns on the worst mount while the number shows root. - status_overview widget detail_url /status → /status/ (avoid redirect). - Normalize ad-hoc widget empty-states to the shared .empty style (wording, which distinguishes "configured" vs "data yet", preserved). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a0d1c5f07c |
feat(host_agent): sparklines + load/core + PSI on the host panel
- Each at-a-glance metric (CPU, Memory, Disk /, Load) now shows a 6h sparkline (reused core.status.sparkline_svg + a recent-series query; disk uses the root mount sub-resource) so trend/consistency is visible, not just the instant. - Load is now normalized: "Load /core" = 1m load ÷ CPU cores as % (100% = run queue matches capacity), comparable across different hardware. Cores derived from the per-core CPU metrics already collected — no agent change. Raw load in the tooltip. - Added a "Pressure 10s" line: PSI cpu/mem/io (some, avg10), the hardware-independent saturation signal already collected by the agent. Scribe #898. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
36212dc58b |
feat(host_agent): at-a-glance disk = root (/) not "worst"; metric tooltips
The "Disk (worst)" number was opaque at a glance. Show the root filesystem (/) usage instead — what people actually care about — on the host panel and the dashboard widget. "Worst" is kept only for the widget's health dot (so a full /var or /data still warns) and on the full-metrics page (per-mount + worst trend). Added hover tooltips defining CPU / Memory / Disk / Load. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7e6e63521b |
fix(host_agent): provision/deploy vars shadowed by play-var precedence
The assertion failed ("Pass steward_url/token/pubkey") because those were
injected as inventory HOST vars, but the playbooks declared them in the play
`vars:` block — and play vars OUTRANK inventory host vars, so the empty
defaults won and the injected values never reached the play.
- Pass globals (steward_url, steward_pubkey, steward_user, agent_interval) as
extra-vars via the JSON -e @file (highest precedence, space-safe). Keep only
the per-host steward_token as an inventory host var.
- provision.yml / install.yml: drop steward_token from `vars:` so the host var
isn't shadowed; assertions use `| default('')` for the un-defaulted token.
This was the next layer under the inventory-format fix — first the inventory
wouldn't parse, now the injected vars actually apply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bb90411f00 |
fix(host_agent): gate "deployed" on agent check-in; fail no-op runs
A failed provision looked successful in two ways: 1. The host panel showed the agent as deployed (metrics + Update/Rotate/Remove) because provision/deploy mint the registration row BEFORE the playbook runs and the panel keyed "installed" on that row. Now gated on the agent actually checking in (reg.last_seen_at). Three states: reporting (metrics + lifecycle), pending (token minted but no check-in → "no metrics yet, deploy may be running/failed" banner + retry + Clear pending registration), and none (install path). 2. The run reported success though nothing ran — ansible-playbook exits 0 on "no hosts matched"/empty inventory. The executor now treats an empty PLAY RECAP (returncode 0 but no hosts executed) as failed, with a clear failure note. Non-zero exits and recap failed/unreachable were already caught. Scribe issue #887. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a92d1995d5 |
fix(ansible): write DB inventory as static YAML, not dynamic --list JSON
generate_inventory() emits Ansible's dynamic --list shape (all.hosts is a
LIST, vars under _meta) — valid only as an executable inventory script's
stdout. We were writing it to a static file and passing -i, so Ansible's yaml
plugin rejected it ("Invalid 'hosts' entry for 'all' group, requires a
dictionary, found ...list...") and fell back to implicit localhost → "no hosts
matched". Affected every steward:* scope run; surfaced on the first real
provision.
- New inventory_to_yaml(inv): convert the --list dict → a valid static YAML
inventory (all.hosts dict keyed by host, groups under all.children, group
vars preserved, injected per-host vars like steward_token retained).
- Wire it into runner.trigger_run, host_agent deploy + provision.
- executor writes the file as inventory.yml so the yaml plugin's extension
check reliably claims it.
- generate_inventory unchanged (still the --list dict); conversion happens at
write time. Unit tests added.
Scribe issue #885.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
cfe6b4c25f |
fix(host_agent): deploy/provision 500 — double-begin on session
fetch_scope_targets() runs a SELECT that autobegins the session transaction (SQLAlchemy 2.0), so the following `async with db.begin()` raised "A transaction is already begun on this Session" → 500 on both /plugins/host_agent/deploy and /provision. Mint registrations directly into the autobegun transaction, build the inventory while the ORM objects are still live, then await db.commit(). Escaped CI because unit tests use a mock db_sessionmaker; no integration test exercises these routes (follow-up noted in Scribe issue #884). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
17c9c875e4 |
refactor(hosts): remove legacy host_agent redirects/paths (no back-compat)
Dev-only instance, no bookmarks — per family rule 22, fully remove old paths instead of shimming them. - Delete the /plugins/host_agent/ (index) and /plugins/host_agent/settings/ redirect routes; delete the now-dead host_list.html fleet template. - Move the remaining management POST routes off /settings/ to /fleet/ (add-host, rotate-token, delete) — single canonical prefix. - Repoint real callers to canonical URLs: dashboard widgets (host resources → /hosts/, history → /plugins/host_agent/fleet/), the full-metrics page breadcrumb + back link (→ the host hub), Settings→Ansible link, and the agent panel's curl-install link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f29255039d |
feat(hosts): Phase 2+3 — agent column, fold fleet + management into Hosts
- Hosts list: new Agent column (latest CPU/mem read from the generic
PluginMetric table — no host_agent import) + an admin "Agent fleet" button.
- /plugins/host_agent/ (old fleet page) now redirects to /hosts/ (folded into
the hub; kept as a redirect so widgets/links don't 404).
- Agent management moved off the "settings" URL: the management page is now
/plugins/host_agent/fleet/ ("Agent fleet" — bulk provision/install/update +
registrations + curl install), reachable from the Hosts list. Old
/plugins/host_agent/settings/ redirects there. Per-host management lives on
the host detail page; this page is now explicitly the bulk/fleet view.
Milestone 70 phases 2-3. Phase 4 (plugins capability/integration split + nav
cleanup) next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8bdf07f709 |
feat(hosts): Phase 1 — host detail hub page (unify host IA)
Make Hosts the front-and-center hub. A host now has a real detail page at /hosts/<id> that pulls its facets into one view, instead of management being scattered across a nav-less Host-Agents area and the edit form. - hosts: new GET /hosts/<id> detail route + hosts/detail.html. Shows the monitors summary (ping/DNS status + latency + uptime 24h/7d/30d), an Ansible section (linked target, link/create, run-playbook), and an embedded Agent panel. Hosts list name links here; ansible-link redirects here. - host_agent: GET /plugins/host_agent/panel/<host_id> — a self-contained HTMX fragment embedded into the core hub across the plugin boundary (core never imports plugin models). Shows live agent metrics + Update/Rotate/Remove when installed, or the provisioning path when not: inline "generate managed key" warning, a prompt to link an Ansible target first, then Provision (bootstrap password) / Install (managed key) tied to the host's target scope. Part of milestone 70 (Hosts hub). Phase 2+ will enrich the list, redirect the old fleet/settings pages, and re-taxonomize plugins into capabilities vs integrations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6a8146b544 |
fix(ansible): replace steward key on reprovision + distinct agent update path
Provisioning review corrections + the matching frontend, plus breadcrumb header integration. - provision.yml: authorize the managed pubkey with a regexp match on the ' steward-managed' comment so rotating the key REPLACES the host's steward key in place instead of stacking a second authorized entry. Hand-added keys (other comments) are untouched. - update.yml (new): refresh agent.py + restart only. Does NOT rotate the token or rewrite /etc/steward-agent.conf — the host keeps its identity. Asserts the agent is already installed and fails clearly otherwise. - host_agent /update route: runs update.yml as the managed steward user (no token minting). Token rotation stays a deliberate action. - settings/ansible/generate-key honors a safe relative `next` redirect, so an inline trigger elsewhere returns to its page. - Host Agents settings: reworked into a clear lifecycle — an intro card that explains it runs Ansible to deploy the agent (+ inline "generate managed key" warning/trigger when none exists), then three labelled cards: 1 Provision, 2 Install/enroll, 3 Update. Each explains what it does. - base.html: breadcrumb now renders as a kicker line directly above the page title (moved below alerts, tightened margin) so nested and top-level views share one consistent header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0318f6423f |
feat(ansible): host provisioning via steward managed SSH identity
Turn the agent-install playbook into a full provisioning + maintenance path. Solves the bootstrap chicken-and-egg: first contact uses an operator-supplied password (one run, never stored), which creates a dedicated `steward` login account with NOPASSWD sudo + Steward's managed public key. Every run thereafter connects as `steward` with the managed key — fully unattended (scheduled prune, agent updates). - core/crypto: generate_ssh_keypair() — ed25519, OpenSSH formats. - settings: ansible.ssh_public_key (non-secret, displayed) + ansible.ssh_user (default steward); to_ansible_cfg extended. - settings UI + route: "Generate managed key" (private encrypted, public shown to copy) + SSH-user field. - executor: build_bootstrap() writes a 0600 vars file (-e @file) for the per-run user/password — never argv, never DB, never logged; drops the managed key when a bootstrap password is given; --user floor from the global ssh_user when no override. - runner.trigger_run: pass-through `connection` kwarg, deliberately NOT persisted on AnsibleRun.params (password stays out of the DB). - bundled/host_agent/provision.yml: create steward user + authorized_keys + /etc/sudoers.d/steward (visudo-validated) + agent install. - host_agent: /provision route + "Provision a fresh host" card (bootstrap user/password; injects pubkey + user + token as space-safe JSON hostvars). - Dockerfile: add sshpass (Ansible shells out to it for password SSH). - tests: keypair generation + build_bootstrap (secret stays off argv). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f3e919892d |
feat(plugins): plugin capability registry + host_agent→Ansible deploy synergy
Implements #253's framework: a small core capability registry (steward/core/capabilities.py) where a module/plugin publishes a named, role-gated action and a consumer discovers it via has_capability() and runs it via invoke_capability() — no hard import, graceful degradation, permission propagation (actor role checked against the capability's required_role). Core publishes "ansible.run_playbook" (operator) wrapping ansible.runner. trigger_run (extended to accept a caller-built inventory). First consumer: the host_agent plugin gains "Deploy via Ansible" on its settings page — pick an inventory target/group and it installs/updates the agent via the bundled host_agent/install.yml, minting a fresh token per host and injecting it as an inventory hostvar (turning per-host curl|sh into one run). Exposed role ordering as middleware.role_meets. Unit tests for the registry + role checks. Task #253 (milestone #37). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bca1b92cc6 |
feat(host_agent): Netdata-style fleet overview + per-host detail view
Surfaces the metrics the agent now collects (task #868). Adds a viewer-facing fleet page (/plugins/host_agent/) with per-host cards (CPU/mem/disk/load/temp, stale flag) and a per-host detail page (/plugins/host_agent/<id>/) — the link the fleet widget already pointed at but had no route for. Detail page shows current gauges (CPU, memory incl. swap/cache, load, network rx/tx, disk I/O, temp max, memory/cpu/io PSI), per-core CPU bars, per-mount filesystem bars, and per-interface/disk/sensor breakdowns, plus history charts (utilization %, throughput B/s, load & pressure) over a selectable range. Charts use a linear epoch-ms x-axis so no Chart.js date adapter is needed. Stale state uses the plugin's stale_after_seconds threshold. Repoints the host_resources widget detail link from admin settings to the new fleet page. Completes milestone #68 (task #867). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a9e7baee6a |
feat(host_agent): collect network, disk I/O, per-core CPU, temps, memory PSI
Extends the push agent (v1.2.0) with the Netdata-style signals the operator wants: per-core CPU, per-interface network throughput and per-disk I/O (rates derived in-agent from monotonic /proc counter deltas, with counter-reset clamping), hardware temperatures (/sys/class/hwmon), memory-pressure PSI (/proc/pressure), and cached/buffers memory breakdown. All stdlib-only. Server side needs no migration — these land as additional rows in the shared PluginMetric table via _expand_sample_to_metrics. Per-resource series use a ':' in resource_name (host:net:eth0, host:core0, host:temp:Package) so the existing fleet widget's ':' filter ignores them; host-level totals/max are emitted at the bare host resource. New sample keys are optional, so older agents keep ingesting unchanged. Unit tests for the new parsers, rate/reset logic, and the expander contract. Data foundation for the Netdata-style host view (task #867). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3b2146bc7a |
feat(host_agent): agent reports its primary IP; mirror into Host.address (task 274)
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> |
||
|
|
a7a281cb11 |
feat(plugins): fold first-party plugins in-tree; bundled + external roots
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> |