feat(dashboard): merge top summary strip + Status widget into one Overview
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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 %} <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 %} <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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user