feat(dashboard): merge top summary strip + Status widget into one Overview
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m15s
CI / publish (push) Successful in 51s

Operator chose a single canonical summary. Repurpose the status_overview
widget into 'Overview' (host count + monitor up/down/pending + Alerts link;
key kept so existing dashboards upgrade in place) and remove the redundant
fixed top strip from the dashboard view. Drops _get_summary_stats and its
now-unused PingResult/DnsResult imports (rule 22).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 20:40:26 -04:00
parent 10808c1c5d
commit ae03f09234
5 changed files with 22 additions and 75 deletions
+2 -2
View File
@@ -24,8 +24,8 @@ WIDGET_REGISTRY: dict[str, dict] = {
},
"status_overview": {
"key": "status_overview",
"label": "Status — Overview",
"description": "Unified up/down/pending summary across ping, DNS, and HTTP monitors",
"label": "Overview",
"description": "At-a-glance summary — host count, monitor up/down/pending, and alerts",
"hx_url": "/status/widget",
"detail_url": "/status/",
"plugin": None,
-39
View File
@@ -9,7 +9,6 @@ from sqlalchemy import select, func, update
from steward.auth.middleware import require_role, current_user_id
from steward.models.users import UserRole
from steward.models.hosts import Host
from steward.models.monitors import PingResult, DnsResult
from steward.models.dashboard import Dashboard, DashboardWidget, DashboardShareToken
from steward.core.settings import public_base_url
from steward.core.widgets import get_available_widgets, WIDGET_REGISTRY
@@ -103,42 +102,6 @@ async def _get_or_create_system_default(db) -> Dashboard:
return dash
async def _get_summary_stats(db) -> dict:
latest_ping_subq = (
select(PingResult.host_id, func.max(PingResult.probed_at).label("max_at"))
.group_by(PingResult.host_id).subquery()
)
pr = await db.execute(
select(PingResult).join(
latest_ping_subq,
(PingResult.host_id == latest_ping_subq.c.host_id)
& (PingResult.probed_at == latest_ping_subq.c.max_at),
)
)
pings = list(pr.scalars())
ping_up = sum(1 for p in pings if p.status.value == "up")
ping_down = sum(1 for p in pings if p.status.value == "down")
latest_dns_subq = (
select(DnsResult.host_id, func.max(DnsResult.resolved_at).label("max_at"))
.group_by(DnsResult.host_id).subquery()
)
dr = await db.execute(
select(DnsResult).join(
latest_dns_subq,
(DnsResult.host_id == latest_dns_subq.c.host_id)
& (DnsResult.resolved_at == latest_dns_subq.c.max_at),
)
)
dns = list(dr.scalars())
dns_ok = sum(1 for d in dns if d.status.value == "resolved")
dns_fail = sum(1 for d in dns if d.status.value != "resolved")
total = (await db.execute(select(func.count()).select_from(Host))).scalar()
return dict(ping_up=ping_up, ping_down=ping_down,
dns_ok=dns_ok, dns_fail=dns_fail, total_hosts=total)
def _resolve_widgets(widgets: list[DashboardWidget]) -> list[dict]:
plugins_cfg = current_app.config.get("PLUGINS", {})
out = []
@@ -199,7 +162,6 @@ async def view(dash_id: int):
if dash is None or not _can_view(dash, user_id, user_role):
abort(404)
stats = await _get_summary_stats(db)
widgets = await _get_widgets(db, dash_id)
accessible = await _accessible_dashboards(db, user_id, user_role)
can_edit = _can_edit(dash, user_id, user_role)
@@ -224,7 +186,6 @@ async def view(dash_id: int):
can_edit=can_edit,
addable=addable,
hosts=hosts,
**stats,
)
+8 -1
View File
@@ -1,8 +1,10 @@
from __future__ import annotations
from quart import Blueprint, current_app, render_template, request
from sqlalchemy import func, select
from steward.auth.middleware import require_role
from steward.core.status import collect_status, sparkline_svg
from steward.models.hosts import Host
from steward.models.users import UserRole
status_bp = Blueprint("status", __name__, url_prefix="/status")
@@ -40,12 +42,17 @@ async def rows():
@status_bp.get("/widget")
@require_role(UserRole.viewer)
async def widget():
"""HTMX dashboard widget — compact up/down/pending summary + problem list."""
"""HTMX dashboard 'Overview' widget — the single at-a-glance summary that
replaced the old fixed top strip: host count + monitor up/down/pending +
problem list + an alerts link."""
async with current_app.db_sessionmaker() as db:
entries = await collect_status(db)
total_hosts = (await db.execute(
select(func.count()).select_from(Host))).scalar()
up, down, pending = _summarize(entries)
return await render_template(
"status/widget.html",
entries=entries, up=up, down=down, pending=pending,
total_hosts=total_hosts,
widget_id=request.args.get("wid", "0"),
)
+2 -31
View File
@@ -33,37 +33,8 @@
</div>
</div>
{# ── Summary strip ─────────────────────────────────────────────────────────── #}
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:0.75rem;margin-bottom:1.5rem;">
<div class="card" style="margin-bottom:0;">
<div class="section-title" style="margin-bottom:0.5rem;">Hosts</div>
<span class="stat-val">{{ total_hosts }}</span>
<span class="stat-label">total</span>
<div style="margin-top:0.5rem;"><a href="/hosts/" style="font-size:0.8rem;">Manage →</a></div>
</div>
<div class="card" style="margin-bottom:0;">
<div class="section-title" style="margin-bottom:0.5rem;">Ping</div>
{% if ping_up + ping_down == 0 %}
<span style="color:var(--text-muted);font-size:0.85rem;">No hosts</span>
{% else %}
<span class="stat-val" style="color:var(--green);">{{ ping_up }}</span><span class="stat-label">up</span>
{% if ping_down %}&nbsp;<span class="stat-val" style="color:var(--red);">{{ ping_down }}</span><span class="stat-label">down</span>{% endif %}
{% endif %}
</div>
<div class="card" style="margin-bottom:0;">
<div class="section-title" style="margin-bottom:0.5rem;">DNS</div>
{% if dns_ok + dns_fail == 0 %}
<span style="color:var(--text-muted);font-size:0.85rem;">No hosts</span>
{% else %}
<span class="stat-val" style="color:var(--green);">{{ dns_ok }}</span><span class="stat-label">ok</span>
{% if dns_fail %}&nbsp;<span class="stat-val" style="color:var(--red);">{{ dns_fail }}</span><span class="stat-label">failed</span>{% endif %}
{% endif %}
</div>
<div class="card" style="margin-bottom:0;">
<div class="section-title" style="margin-bottom:0.5rem;">Alerts</div>
<a href="/alerts/" style="font-size:0.85rem;">View rules →</a>
</div>
</div>
{# The old fixed summary strip (Hosts/Ping/DNS/Alerts) was merged into the single
'Overview' widget (status_overview) so there's one canonical summary, not two. #}
{# ── Widget grid (Gridstack; static until Edit is toggled) ──────────────────── #}
{% if not widgets %}
+10 -2
View File
@@ -1,8 +1,16 @@
{# status/widget.html — compact dashboard summary + problem list #}
<div style="display:flex;gap:1.25rem;align-items:baseline;margin-bottom:0.6rem;">
{# status/widget.html — the unified Overview: host count + monitor health
(up/down/pending) + alerts link + problem list. Replaced the old fixed
top summary strip. #}
<div style="display:flex;flex-wrap:wrap;gap:1.25rem;align-items:baseline;margin-bottom:0.6rem;">
<span>
{% if session.user_id %}<a href="/hosts/" style="color:inherit;text-decoration:none;">{% endif %}
<span class="stat-val" style="font-size:1.5rem;">{{ total_hosts }}</span><span class="stat-label">host{{ '' if total_hosts == 1 else 's' }}</span>
{% if session.user_id %}</a>{% endif %}
</span>
<span><span class="stat-val" style="font-size:1.5rem;color:var(--green);">{{ up }}</span><span class="stat-label">up</span></span>
<span><span class="stat-val" style="font-size:1.5rem;color:{% if down %}var(--red){% else %}var(--text-dim){% endif %};">{{ down }}</span><span class="stat-label">down</span></span>
<span><span class="stat-val" style="font-size:1.5rem;color:{% if pending %}var(--yellow){% else %}var(--text-dim){% endif %};">{{ pending }}</span><span class="stat-label">pending</span></span>
{% if session.user_id %}<a href="/alerts/" style="margin-left:auto;align-self:center;font-size:0.8rem;">Alerts →</a>{% endif %}
</div>
{% set problems = entries | rejectattr("status", "equalto", "up") | list %}