Ansible schedule form: structured playbook-variable fields + first-item dropdown defaults #2
Reference in New Issue
Block a user
Delete Branch "dev"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two related Ansible run-form improvements from
dev.Structured playbook-variable fields on the schedule form (issue #1217)
The Schedules form only had a free-form extra-vars textarea, unlike the browse/host run forms which auto-populate a field per declared playbook variable (
vars:/vars_prompt:) plus the playbook description.<select>loads/ansible/playbook-vars?schedule=1into a#sch-playbook-varscontainer (mirrors the host detail form)._playbook_vars.htmlgains optionalvalues(prefill) +scheduleflags: in the schedule context secret vars render disabled ("secrets can't be scheduled" — they're dropped downstream) and the destructive-confirm gate is shown but notrequired.Dropdowns default to the first item (task #1219)
Source/Playbook
<select>s opened on a "— choose … —" placeholder, forcing an extra click._playbook_options).schedules()defaults the source to the first (or edited) source and pre-renders that source's playbooks (create mode no longer shows the all-sources union).hx-on::after-settlere-triggers the playbook select to refresh its variable fields.Backend already parsed
var__*fields for schedules, so this is primarily a front-end/partial parity fix. CI green on both commits (runs 1805, 1807 — lint/unit/integration).Note: the alert-rule form's opt-in Ansible-action source dropdown intentionally keeps its placeholder (different pattern — free-text playbook, checkbox-gated).
🤖 Generated with Claude Code
The alertoperator Postgres enum (migration 0002) was created with the operator VALUES as labels ('>', '<', …), but the model's Enum(AlertOperator) defaulted to persisting enum-member NAMES ('gt', 'lt', …) — so inserting any AlertRule against real Postgres raised 'invalid input value for enum alertoperator: "gt"'. Never caught because unit tests mock the DB and no integration test inserted a rule until task 250's. Add values_callable so the ORM round-trips the values. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Plain ${POSTGRES_PASSWORD} in compose; default 'steward' lives in .env.example where it's visible and editable rather than buried in compose YAML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>A fresh install enabled zero plugins — settings.py DEFAULTS had no plugin.* keys, so to_plugins_cfg returned {} and every plugin had to be flipped on by hand. Seed docker, host_agent, http, snmp as default-on (generic, non- vendor-specific); traefik and unifi stay opt-in. Stored choices override the default, so disabling persists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Adds a breadcrumb trail to nested views for orientation. Mechanism: a {% block breadcrumb %} slot in base.html (+ styling) and a shared crumbs() macro in templates/_macros.html; each nested page fills the block with its trail (root→current, last item is the current page). Pages without the block render no bar, so top-level nav roots stay clean. Applied to: Ansible (browse, schedules, playbook editor, run detail) + inventory (targets/groups + detail), host_agent (fleet, host detail, settings), hosts form, settings tabs (ansible/auth/notifications/plugins/ reports + plugin detail), dashboard (list, edit), and alerts (rule form, maintenance + new). Dynamic labels (host/target/run/dashboard names) come from the page context — no route changes. All 60 templates Jinja-compile. Task #873. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>- 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>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>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>New maintenance/system_update.yml: cross-distro OS package upgrade (apt + dnf/yum) with two run-form flags: - restart_services: restart every service that needs it after the upgrade (Debian via needrestart -r a, installed if missing; RHEL via needs-restarting -s → systemctl try-restart). - reboot_if_required: reboot the host only when a reboot is actually pending (Debian /var/run/reboot-required; RHEL needs-restarting -r). Never reboots otherwise. Tagged steward:category=maintenance and steward:confirm=true (significant / reboot-capable), so it shows in the run UI with the confirm gate and both flags as fill-in fields. No code changes — auto-discovered from the builtin source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>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>Collapse the three former check types into a single core `Monitor` entity with one management surface (/monitors), one result table (monitor_results), and a single scheduled task. Every type can now watch a free-standing custom destination (optional host_id) — not just a registered Host. - models: Monitor + MonitorResult replace PingResult/DnsResult; Host loses its ping/dns facet columns (now Monitor rows linked by host_id). - checks: monitors/{ping,dns,http}.py pure probes + runner.run_monitor dispatcher; one monitor_check scheduler with a per-monitor due-filter. - status: single monitor_status_source replaces the three sources. - UI: /monitors blueprint (type-aware add/edit/list/widget); host hub shows a host's linked monitors + "add monitor for this host"; nav + widget registry + alert metric catalog rewired. http plugin folded into core and removed. - migration 0022 merges the http branch, data-migrates host facets + http_monitors + all three result histories, drops the old tables/columns. Resolves the per-host ping/dns auto-attach issue (#275): monitors are now explicit, never auto-added to every host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMCDocker 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_016Jg27rgypiW2efULXJDtMCFirst slice of milestone 77 (Docker monitoring depth). Surfaces real per-container stats beyond basic state, all read-only on the existing push model. - agent (→1.4.0): collect_docker now inspects each container (health, restart count, exit code, OOM) and reads net + block I/O from the stats payload; pulls compose project + swarm service/task/node from container labels. Per-container inspect+stats calls run over a small bounded ThreadPool so the ~1s-per-stats blocking doesn't stretch the sample on a busy host. - schema (docker_003): additive columns on docker_containers — health, exit_code, oom_killed, compose_project, service_name, task_id, node_id, and BigInteger net/blk byte counters. - ingest: persists the enrichment + restart_count (.get keeps older agents working). - ui: Docker page rows now show health badge, uptime ("up 3d 4h"), restart count, exit code (+OOM) for stopped containers, and compose/service grouping label. - tests: agent helpers (grouping, inspect fields, net/IO sum) + collect_docker assembly incl. inspect; integration asserts enrichment round-trips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMCAdds the milestone-77 storage that doesn't fit on the per-container row: * docker_events — lifecycle (start/stop/die/oom/health_change), to be derived by diffing consecutive host snapshots; host-scoped, indexed for timeline lookups (host_id, container_name, at) and retention pruning (at). * docker_swarm_services / docker_swarm_nodes — manager-reported Swarm topology (desired-vs-running replicas, node role/availability/status). Migration docker_004 extends the docker branch (down_revision docker_003); purely additive, no DROP+recreate. event/mode/role are plain strings (no CHECK whitelist), matching how docker_containers models status. Integration guard asserts the three new host-scoped tables exist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMCAdds collect_swarm(socket_path) to the host agent. Self-detects a Swarm manager via /info (Swarm.ControlAvailable) — workers and non-swarm daemons return None and never touch the manager-only endpoints (one cheap /info call, no 503s). On a manager it queries /services, /tasks, /nodes and emits sample["swarm"] = {services, nodes}: * services roll desired-vs-running replicas up from the task list (replica health isn't on the service object), handle replicated + global mode, strip the @sha256 image digest, and carry cross-node task→node placement. * nodes normalise role / availability / status + the manager leader flag. build_sample omits the swarm key entirely off managers, same silent contract as collect_docker. Unit tests cover manager detection, replica roll-up + placement (replicated & global), node normalisation, worker silent-skip, and build_sample wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMCWires 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_016Jg27rgypiW2efULXJDtMCThe page-top chrome was inconsistent: most nested views used the breadcrumb kicker + page-title pattern, but plugin sub-pages used ad-hoc "← back" links, ~11 pages stacked a breadcrumb AND a redundant ancestor back button, and two (monitors/edit, hosts/uptime) had a back button but no breadcrumb. The settings/plugin_detail page stacked all of it at once. Unify on the breadcrumb-led model: - settings/_tabs.html: drop the hardcoded "Settings" h1; the breadcrumb ("Settings › …") plus the tab strip is the header. - settings/plugin_detail: drop the "← Plugins" back button. - docker container_detail/swarm/disk + snmp/device: replace ad-hoc back links with the standard crumbs() breadcrumb. - host_agent, ansible/*, alerts/maintenance: remove redundant ancestor back buttons (the breadcrumb's parent crumbs already link there); keep lateral shortcuts (Inventory/Schedules/Browse/Targets/Groups/New). - monitors/edit, hosts/uptime: add the missing breadcrumb, drop the back link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMCThe 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_016Jg27rgypiW2efULXJDtMCThe live refresh re-created the Chart.js charts each poll, which blanked the canvases and re-ran the grow-in animation — a jarring flash/jump every cycle. Make the canvases persistent in the page shell and poll only the data: - The 3 chart canvases + their Chart instances are created once in the shell, with animation disabled. - /charts is now a data-only fragment swapped into a hidden div; it calls window.applyHostSeries(series, range), which sets each dataset's data and calls chart.update("none") — in-place, no re-create, no animation, fixed height. Range labels update via .hm-chart-range spans. - Current-state fragment keeps its atomic innerHTML poll into fixed-height cards, so numbers update without a layout jump. Net: live updates morph smoothly with no flicker or layout shift. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMCSwarm services/nodes are cluster-global (every manager's API returns the same list), but each manager reports them independently (rows keyed by host_id) and the Swarm page grouped by reporting manager — so two managers in one cluster listed every service and node twice. Group the reporting managers into swarms in the swarm() view (managers sharing any node_id are the same cluster, via union-find over node-set intersection), then dedup within each: one service per name and one node per node_id, keeping the freshest. Render one section per swarm ("reported by N managers · names"). node_id is globally unique so node dedup is always safe; grouping by node overlap also keeps it correct if two separate swarms are ever monitored. View-layer only — no schema/agent change. The main per-host container page is unaffected (those are genuinely distinct per-node tasks). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMCplugin_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_016Jg27rgypiW2efULXJDtMCThe Ansible Schedules form only offered a free-form extra-vars textarea, unlike the browse/host run forms which auto-populate a field per declared playbook variable (vars:/vars_prompt:) plus the playbook description. Wire the schedule form to the same shared `_playbook_vars.html` partial: - Playbook <select> now loads /ansible/playbook-vars?schedule=1 on change into a #sch-playbook-vars container (mirrors the host detail form). - Edit mode pre-renders the saved playbook's variables server-side with their stored values; the extra-vars textarea keeps only leftover (non-declared) vars. - Partial gains optional `values` (prefill) and `schedule` flags. In the schedule context secret vars render disabled ("secrets can't be scheduled") since they're dropped downstream, and the destructive-confirm gate is shown but not required. Backend already parsed var__* fields for schedules, so this is the front-end/partial parity fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>