From e0253fba4826555f77f92be14b2d562cc436b654 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 17 Jun 2026 12:46:08 -0400 Subject: [PATCH] fix(ansible): surface run failure reason; widget audit cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- plugins/docker/templates/docker/widget.html | 2 +- plugins/host_agent/routes.py | 15 ++++++++++----- plugins/host_agent/templates/widget_history.html | 6 +++--- plugins/host_agent/templates/widget_table.html | 3 ++- plugins/unifi/templates/unifi/widget_clients.html | 2 +- steward/ansible/executor.py | 9 ++++++++- steward/core/widgets.py | 2 +- steward/templates/status/widget.html | 2 +- 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/plugins/docker/templates/docker/widget.html b/plugins/docker/templates/docker/widget.html index 427018f..061df4b 100644 --- a/plugins/docker/templates/docker/widget.html +++ b/plugins/docker/templates/docker/widget.html @@ -1,6 +1,6 @@ {# docker/widget.html — dashboard widget: container status overview #} {% if not containers and running_count == 0 and stopped_count == 0 %} -
No containers found.
+

No containers found.

{% else %}
diff --git a/plugins/host_agent/routes.py b/plugins/host_agent/routes.py index 199c552..57c92bc 100644 --- a/plugins/host_agent/routes.py +++ b/plugins/host_agent/routes.py @@ -398,16 +398,21 @@ async def widget_history(): points = (await session.execute( select(PluginMetric).where( PluginMetric.source_module == SOURCE_MODULE, - PluginMetric.resource_name == host.name, - PluginMetric.metric_name.in_( - ("cpu_pct", "mem_used_pct", "disk_used_pct_worst")), PluginMetric.recorded_at >= cutoff, + or_( + and_(PluginMetric.resource_name == host.name, + PluginMetric.metric_name.in_(("cpu_pct", "mem_used_pct"))), + and_(PluginMetric.resource_name == host.name + ":/", + PluginMetric.metric_name == "disk_used_pct"), + ), ).order_by(PluginMetric.recorded_at) )).scalars().all() - series: dict[str, list[dict]] = {"cpu_pct": [], "mem_used_pct": [], "disk_used_pct_worst": []} + # Disk = root (/), consistent with the host panel — not the opaque "worst". + series: dict[str, list[dict]] = {"cpu_pct": [], "mem_used_pct": [], "disk_root": []} for p in points: - series[p.metric_name].append({"t": p.recorded_at.isoformat(), "v": p.value}) + key = "disk_root" if p.resource_name != host.name else p.metric_name + series[key].append({"t": p.recorded_at.isoformat(), "v": p.value}) return await render_template("widget_history.html", host=host, series=series, hours=hours) diff --git a/plugins/host_agent/templates/widget_history.html b/plugins/host_agent/templates/widget_history.html index 35e363f..0d5b567 100644 --- a/plugins/host_agent/templates/widget_history.html +++ b/plugins/host_agent/templates/widget_history.html @@ -9,9 +9,9 @@ type: "line", data: { datasets: [ - { label: "CPU %", data: series.cpu_pct.map(p => ({x: p.t, y: p.v})) }, - { label: "Mem %", data: series.mem_used_pct.map(p => ({x: p.t, y: p.v})) }, - { label: "Disk % worst", data: series.disk_used_pct_worst.map(p => ({x: p.t, y: p.v})) }, + { label: "CPU %", data: series.cpu_pct.map(p => ({x: p.t, y: p.v})) }, + { label: "Mem %", data: series.mem_used_pct.map(p => ({x: p.t, y: p.v})) }, + { label: "Disk / %", data: series.disk_root.map(p => ({x: p.t, y: p.v})) }, ], }, options: { diff --git a/plugins/host_agent/templates/widget_table.html b/plugins/host_agent/templates/widget_table.html index 65bb418..14a5bbd 100644 --- a/plugins/host_agent/templates/widget_table.html +++ b/plugins/host_agent/templates/widget_table.html @@ -4,7 +4,8 @@ {% for r in rows %} {% set stale = r.stale %}
- + {{ r.host.name }} diff --git a/plugins/unifi/templates/unifi/widget_clients.html b/plugins/unifi/templates/unifi/widget_clients.html index 99a5892..56059a9 100644 --- a/plugins/unifi/templates/unifi/widget_clients.html +++ b/plugins/unifi/templates/unifi/widget_clients.html @@ -1,6 +1,6 @@ {# unifi/widget_clients.html — dashboard widget: top active clients #} {% if not snapshot %} -
No client data yet.
+

No client data yet.

{% else %}
diff --git a/steward/ansible/executor.py b/steward/ansible/executor.py index 3f51975..e061930 100644 --- a/steward/ansible/executor.py +++ b/steward/ansible/executor.py @@ -462,9 +462,16 @@ async def start_run( except _Aborted: final_status = "cancelled" - except Exception: + except Exception as exc: logger.exception("Ansible run %s failed with exception", run_id) final_status = "cancelled" if run_id in _cancelled else "failed" + # Surface WHY in the run output instead of an empty "failed" (e.g. ENOSPC + # when the temp dir can't be created — the run never reaches Ansible). + msg = f"[run error] {type(exc).__name__}: {exc}" + _broadcast(run_id, msg) + if len(failures) < _FAILURE_CAP: + failures.append(msg[:500]) + db_output = (db_output + "\n" + msg) if db_output else msg finally: if logf is not None: logf.close() diff --git a/steward/core/widgets.py b/steward/core/widgets.py index 5f92858..7ea8671 100644 --- a/steward/core/widgets.py +++ b/steward/core/widgets.py @@ -27,7 +27,7 @@ WIDGET_REGISTRY: dict[str, dict] = { "label": "Status — Overview", "description": "Unified up/down/pending summary across ping, DNS, and HTTP monitors", "hx_url": "/status/widget", - "detail_url": "/status", + "detail_url": "/status/", "plugin": None, "poll": True, "params": [], diff --git a/steward/templates/status/widget.html b/steward/templates/status/widget.html index eef40b8..2acc92f 100644 --- a/steward/templates/status/widget.html +++ b/steward/templates/status/widget.html @@ -7,7 +7,7 @@ {% set problems = entries | rejectattr("status", "equalto", "up") | list %} {% if not entries %} -
No monitors configured.
+

No monitors configured.

{% elif not problems %}
✓ All {{ up }} monitors are up.
{% else %}