Ansible schedule form: structured playbook-variable fields + first-item dropdown defaults #2
@@ -43,7 +43,10 @@
|
||||
elements: { point: { radius: 0 } },
|
||||
scales: {
|
||||
x: { type: "linear", ticks: { callback: function (v) { return fmtTime(v); }, maxTicksLimit: 6, color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } },
|
||||
y: { beginAtZero: true, max: 100, ticks: { color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } },
|
||||
// Auto-scale to the data (with a little headroom) so the lines fill the
|
||||
// panel instead of hugging the bottom of a fixed 0–100 axis. Axis labels
|
||||
// still show the real %, so the values stay honest.
|
||||
y: { beginAtZero: true, grace: "8%", ticks: { color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } },
|
||||
},
|
||||
plugins: { legend: { labels: { color: "#b8b8b0", boxWidth: 10, font: { size: 11 } } } },
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{# host_agent widget — fleet glance: per-host row with a sparkline beside each
|
||||
metric (cpu/mem/disk/load), mirroring the host-page AGENT panel. #}
|
||||
{# host_agent widget — fleet glance. Per host: name on its own line (never
|
||||
truncated), then a row of metric cells (cpu/mem/disk/load), each value with a
|
||||
trend sparkline beneath it — the host-page AGENT panel, on the dashboard. #}
|
||||
{% from "_macros.html" import metric_style %}
|
||||
{% if rows %}
|
||||
{% macro metric_cell(label, value, fmt, svg, style="", title="") %}
|
||||
<div style="min-width:74px;flex-shrink:0;" title="{{ title }}">
|
||||
<div style="min-width:74px;flex:0 0 auto;" title="{{ title }}">
|
||||
<div style="font-size:0.66rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.03em;">{{ label }}</div>
|
||||
<div style="font-weight:600;font-size:0.8rem;font-variant-numeric:tabular-nums;{{ style }}">
|
||||
{% if value is not none %}{{ fmt|format(value) }}{% else %}—{% endif %}
|
||||
@@ -11,27 +12,31 @@
|
||||
<div style="line-height:0;height:16px;">{{ svg | safe }}</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<div style="display:grid;gap:0.1rem;">
|
||||
<div class="host-blocks">
|
||||
{% for r in rows %}
|
||||
{% set stale = r.stale %}
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;font-size:0.82rem;padding:0.45rem 0;border-bottom:1px solid var(--border);">
|
||||
<span class="dot {% if stale %}dot-dim{% elif (r.cpu_pct or 0) >= 90 or (r.mem_used_pct or 0) >= 90 or (r.disk_worst or 0) >= 90 %}dot-warn{% else %}dot-up{% endif %}"
|
||||
title="{% if stale %}stale / no recent data{% else %}warns at CPU/memory ≥90% or any disk mount ≥90% (the 'disk /' figure shows root only){% endif %}"></span>
|
||||
{% set name_style = "flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;" %}
|
||||
{% if session.user_id %}
|
||||
<a href="/hosts/{{ r.host.id }}" title="{{ r.host.name }}" style="{{ name_style }}">{{ r.host.name }}</a>
|
||||
{% else %}
|
||||
<span title="{{ r.host.name }}" style="{{ name_style }}">{{ r.host.name }}</span>
|
||||
{% endif %}
|
||||
<div style="display:flex;gap:1.1rem;flex-shrink:0;align-items:flex-start;">
|
||||
<div style="padding:0.55rem 0;border-bottom:1px solid var(--border);">
|
||||
{# Line 1 — health dot + full host name (own line) + last seen #}
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||
<span class="dot {% if stale %}dot-dim{% elif (r.cpu_pct or 0) >= 90 or (r.mem_used_pct or 0) >= 90 or (r.disk_worst or 0) >= 90 %}dot-warn{% else %}dot-up{% endif %}"
|
||||
style="flex-shrink:0;"
|
||||
title="{% if stale %}stale / no recent data{% else %}warns at CPU/memory ≥90% or any disk mount ≥90% (the 'disk /' figure shows root only){% endif %}"></span>
|
||||
{% if session.user_id %}
|
||||
<a href="/hosts/{{ r.host.id }}" style="font-weight:600;font-size:0.85rem;overflow-wrap:anywhere;">{{ r.host.name }}</a>
|
||||
{% else %}
|
||||
<span style="font-weight:600;font-size:0.85rem;overflow-wrap:anywhere;">{{ r.host.name }}</span>
|
||||
{% endif %}
|
||||
<span style="margin-left:auto;font-size:0.72rem;color:var(--text-dim);flex-shrink:0;">
|
||||
{{ r.reg.last_seen_at.strftime("%H:%M:%S") if r.reg.last_seen_at else "never" }}
|
||||
</span>
|
||||
</div>
|
||||
{# Line 2 — metric cells with value + trend; wraps instead of truncating #}
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.4rem 1.1rem;margin-top:0.35rem;padding-left:1.1rem;">
|
||||
{{ metric_cell("cpu", r.cpu_pct, "%.0f%%", r.sparks.cpu, metric_style(r.cpu_pct), "Average CPU utilization") }}
|
||||
{{ metric_cell("mem", r.mem_used_pct, "%.0f%%", r.sparks.mem, metric_style(r.mem_used_pct), "Memory in use") }}
|
||||
{{ metric_cell("disk /", r.disk_root, "%.0f%%", r.sparks.disk, metric_style(r.disk_root), "Root filesystem (/) usage") }}
|
||||
{{ metric_cell("load", r.load_1m, "%.2f", r.sparks.load, "", "Load average (1m)") }}
|
||||
</div>
|
||||
<span style="min-width:58px;text-align:right;font-size:0.72rem;color:var(--text-dim);flex-shrink:0;">
|
||||
{{ r.reg.last_seen_at.strftime("%H:%M:%S") if r.reg.last_seen_at else "never" }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -163,7 +163,7 @@ textarea { resize: vertical; }
|
||||
.ping-row { display:flex; align-items:center; gap:1rem; padding:0.45rem 0; border-bottom:1px solid var(--border); }
|
||||
.ping-row:last-child { border-bottom:none; }
|
||||
.ping-meta { min-width:120px; max-width:180px; flex-shrink:1; overflow:hidden; }
|
||||
.ping-name { font-size:0.875rem; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block; }
|
||||
.ping-name { font-size:0.875rem; font-weight:500; display:block; overflow-wrap:anywhere; }
|
||||
a.ping-name:hover { text-decoration:underline; }
|
||||
/* Dashboard widget grid — Gridstack-rendered. Static (positioned but inert) in
|
||||
view mode; the dashboard itself becomes the edit surface when Edit is toggled,
|
||||
@@ -174,6 +174,14 @@ a.ping-name:hover { text-decoration:underline; }
|
||||
padding:0.8rem 1rem; display:flex; flex-direction:column; overflow:hidden;
|
||||
}
|
||||
.grid-stack-item-content .dash-cell-body { flex:1; min-height:0; overflow:auto; }
|
||||
/* The widget body is a query container, so fragments can restyle to their OWN
|
||||
panel width (not the viewport) — widgets adapt as you resize them. */
|
||||
.dash-cell-body { container-type:inline-size; }
|
||||
/* Host-list widgets flow into columns as the panel widens: use the width, drop
|
||||
the vertical deadspace; stay single-column (and untruncated) when narrow. */
|
||||
.host-blocks { display:grid; grid-template-columns:1fr; gap:0 1.5rem; }
|
||||
@container (min-width:520px) { .host-blocks { grid-template-columns:1fr 1fr; } }
|
||||
@container (min-width:900px) { .host-blocks { grid-template-columns:1fr 1fr 1fr; } }
|
||||
.panel-chrome { display:none; }
|
||||
.panel-drag { cursor:grab; color:var(--text-dim); user-select:none; }
|
||||
.panel-drag:active { cursor:grabbing; }
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
.ping-row { display:flex; align-items:center; gap:1rem; padding:0.45rem 0; border-bottom:1px solid var(--border); }
|
||||
.ping-row:last-child { border-bottom:none; }
|
||||
.ping-meta { min-width:120px; max-width:180px; flex-shrink:1; overflow:hidden; }
|
||||
.ping-name { font-size:0.875rem; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block; }
|
||||
.ping-name { font-size:0.875rem; font-weight:500; display:block; overflow-wrap:anywhere; }
|
||||
.ping-addr { display:block; color:var(--text-muted); font-size:0.75rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.ping-pills { display:flex; gap:2px; flex:1; min-width:0; overflow:hidden; align-items:center; }
|
||||
.pill { display:inline-block; width:7px; height:22px; border-radius:2px; }
|
||||
@@ -59,6 +59,10 @@
|
||||
.dash-grid { display:grid; grid-template-columns:repeat(12,1fr); grid-auto-rows:70px; gap:1rem; }
|
||||
.dash-grid > .dash-cell { margin-bottom:0; min-width:0; overflow:hidden; display:flex; flex-direction:column; }
|
||||
.dash-cell .dash-cell-body { flex:1; min-height:0; overflow:auto; }
|
||||
.dash-cell-body { container-type:inline-size; }
|
||||
.host-blocks { display:grid; grid-template-columns:1fr; gap:0 1.5rem; }
|
||||
@container (min-width:520px) { .host-blocks { grid-template-columns:1fr 1fr; } }
|
||||
@container (min-width:900px) { .host-blocks { grid-template-columns:1fr 1fr 1fr; } }
|
||||
@media (max-width:820px) {
|
||||
.dash-grid { grid-template-columns:1fr; grid-auto-rows:auto; }
|
||||
.dash-grid > .dash-cell { grid-column:1 / -1 !important; grid-row:auto !important; }
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
{# Unified Hosts widget — monitor status + agent glance per host, links to the hub. #}
|
||||
{# Unified Hosts widget — monitor status + agent glance per host, links to the hub.
|
||||
Name sits on its own line (never truncated); its data wraps beneath it, grouped
|
||||
under the host. Prefer vertical growth over cramming a row. #}
|
||||
{% from "_macros.html" import metric_style %}
|
||||
{% if hosts %}
|
||||
<div style="display:grid;gap:0.1rem;">
|
||||
<div class="host-blocks">
|
||||
{% for host in hosts %}
|
||||
{% set ping = latest_pings.get(host.id) %}
|
||||
{% set dns = latest_dns.get(host.id) %}
|
||||
{% set ut = uptime.get(host.id, {}) %}
|
||||
{% set a = agent.get(host.name, {}) %}
|
||||
{% set down = host.ping_enabled and ping and ping.status.value != 'up' %}
|
||||
<div style="display:flex;align-items:center;gap:0.6rem;font-size:0.82rem;padding:0.3rem 0;border-bottom:1px solid var(--border);">
|
||||
<span class="dot {% if down %}dot-down{% elif host.ping_enabled and ping %}dot-up{% else %}dot-dim{% endif %}"
|
||||
title="{% if not host.ping_enabled %}ping off{% elif ping %}{{ ping.status.value }}{% else %}no data{% endif %}"></span>
|
||||
{% set name_style = "flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;" %}
|
||||
{% if session.user_id %}
|
||||
<a href="/hosts/{{ host.id }}" title="{{ host.name }}" style="{{ name_style }}">{{ host.name }}</a>
|
||||
{% else %}
|
||||
<span title="{{ host.name }}" style="{{ name_style }}">{{ host.name }}</span>
|
||||
{% endif %}
|
||||
{% set spark = cpu_sparks.get(host.name) %}
|
||||
{% if spark %}<span title="CPU — last hour" style="flex-shrink:0;line-height:0;opacity:0.85;">{{ spark | safe }}</span>{% endif %}
|
||||
<span style="display:flex;gap:0.75rem;font-variant-numeric:tabular-nums;color:var(--text-muted);font-size:0.75rem;flex-shrink:0;">
|
||||
{# Monitors #}
|
||||
{% set spark = cpu_sparks.get(host.name) %}
|
||||
<div style="padding:0.5rem 0;border-bottom:1px solid var(--border);">
|
||||
{# Line 1 — status dot + full host name (own line) + cpu trend #}
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||
<span class="dot {% if down %}dot-down{% elif host.ping_enabled and ping %}dot-up{% else %}dot-dim{% endif %}"
|
||||
style="flex-shrink:0;"
|
||||
title="{% if not host.ping_enabled %}ping off{% elif ping %}{{ ping.status.value }}{% else %}no data{% endif %}"></span>
|
||||
{% if session.user_id %}
|
||||
<a href="/hosts/{{ host.id }}" style="font-weight:600;font-size:0.85rem;overflow-wrap:anywhere;">{{ host.name }}</a>
|
||||
{% else %}
|
||||
<span style="font-weight:600;font-size:0.85rem;overflow-wrap:anywhere;">{{ host.name }}</span>
|
||||
{% endif %}
|
||||
{% if spark %}<span title="CPU — last hour" style="margin-left:auto;line-height:0;opacity:0.85;flex-shrink:0;">{{ spark | safe }}</span>{% endif %}
|
||||
</div>
|
||||
{# Line 2 — monitors + agent metrics; wraps instead of truncating #}
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.25rem 0.9rem;margin-top:0.25rem;padding-left:1.1rem;
|
||||
font-variant-numeric:tabular-nums;color:var(--text-muted);font-size:0.75rem;">
|
||||
{% if host.ping_enabled and ping and ping.response_time_ms is not none %}
|
||||
<span title="Ping latency"><span style="color:var(--text-dim);">ping</span> {{ "%.0f"|format(ping.response_time_ms) }}ms</span>
|
||||
{% endif %}
|
||||
{% if ut.get('24h') is not none %}
|
||||
<span title="Uptime 24h"><span style="color:var(--text-dim);">24h</span> {{ "%.1f"|format(ut['24h']) }}%</span>
|
||||
{% endif %}
|
||||
{# Agent glance — failing metric coloured amber/red (≥80/≥90) #}
|
||||
{% if a %}
|
||||
{% if a.get('cpu_pct') is not none %}<span title="CPU" style="{{ metric_style(a.cpu_pct) }}"><span style="color:var(--text-dim);">cpu</span> {{ "%.0f"|format(a.cpu_pct) }}%</span>{% endif %}
|
||||
{% if a.get('mem_used_pct') is not none %}<span title="Memory" style="{{ metric_style(a.mem_used_pct) }}"><span style="color:var(--text-dim);">mem</span> {{ "%.0f"|format(a.mem_used_pct) }}%</span>{% endif %}
|
||||
@@ -36,7 +41,7 @@
|
||||
{% else %}
|
||||
<span style="color:var(--text-dim);" title="No agent reporting">no agent</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<div style="display:grid;grid-template-columns:1fr 52px 52px 52px;gap:0.35rem;align-items:center;padding:0.15rem 0;font-size:0.82rem;">
|
||||
{% if session.user_id %}
|
||||
<a href="/hosts/{{ host.id }}" title="{{ host.name }}"
|
||||
style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:inherit;text-decoration:none;">{{ host.name }}</a>
|
||||
style="overflow-wrap:anywhere;color:inherit;text-decoration:none;">{{ host.name }}</a>
|
||||
{% else %}
|
||||
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="{{ host.name }}">{{ host.name }}</span>
|
||||
<span style="overflow-wrap:anywhere;" title="{{ host.name }}">{{ host.name }}</span>
|
||||
{% endif %}
|
||||
{% for window in ["24h", "7d", "30d"] %}
|
||||
{% set pct = ut.get(window) %}
|
||||
|
||||
Reference in New Issue
Block a user