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": {
|
"status_overview": {
|
||||||
"key": "status_overview",
|
"key": "status_overview",
|
||||||
"label": "Status — Overview",
|
"label": "Overview",
|
||||||
"description": "Unified up/down/pending summary across ping, DNS, and HTTP monitors",
|
"description": "At-a-glance summary — host count, monitor up/down/pending, and alerts",
|
||||||
"hx_url": "/status/widget",
|
"hx_url": "/status/widget",
|
||||||
"detail_url": "/status/",
|
"detail_url": "/status/",
|
||||||
"plugin": None,
|
"plugin": None,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from sqlalchemy import select, func, update
|
|||||||
from steward.auth.middleware import require_role, current_user_id
|
from steward.auth.middleware import require_role, current_user_id
|
||||||
from steward.models.users import UserRole
|
from steward.models.users import UserRole
|
||||||
from steward.models.hosts import Host
|
from steward.models.hosts import Host
|
||||||
from steward.models.monitors import PingResult, DnsResult
|
|
||||||
from steward.models.dashboard import Dashboard, DashboardWidget, DashboardShareToken
|
from steward.models.dashboard import Dashboard, DashboardWidget, DashboardShareToken
|
||||||
from steward.core.settings import public_base_url
|
from steward.core.settings import public_base_url
|
||||||
from steward.core.widgets import get_available_widgets, WIDGET_REGISTRY
|
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
|
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]:
|
def _resolve_widgets(widgets: list[DashboardWidget]) -> list[dict]:
|
||||||
plugins_cfg = current_app.config.get("PLUGINS", {})
|
plugins_cfg = current_app.config.get("PLUGINS", {})
|
||||||
out = []
|
out = []
|
||||||
@@ -199,7 +162,6 @@ async def view(dash_id: int):
|
|||||||
if dash is None or not _can_view(dash, user_id, user_role):
|
if dash is None or not _can_view(dash, user_id, user_role):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
stats = await _get_summary_stats(db)
|
|
||||||
widgets = await _get_widgets(db, dash_id)
|
widgets = await _get_widgets(db, dash_id)
|
||||||
accessible = await _accessible_dashboards(db, user_id, user_role)
|
accessible = await _accessible_dashboards(db, user_id, user_role)
|
||||||
can_edit = _can_edit(dash, 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,
|
can_edit=can_edit,
|
||||||
addable=addable,
|
addable=addable,
|
||||||
hosts=hosts,
|
hosts=hosts,
|
||||||
**stats,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from quart import Blueprint, current_app, render_template, request
|
from quart import Blueprint, current_app, render_template, request
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from steward.auth.middleware import require_role
|
from steward.auth.middleware import require_role
|
||||||
from steward.core.status import collect_status, sparkline_svg
|
from steward.core.status import collect_status, sparkline_svg
|
||||||
|
from steward.models.hosts import Host
|
||||||
from steward.models.users import UserRole
|
from steward.models.users import UserRole
|
||||||
|
|
||||||
status_bp = Blueprint("status", __name__, url_prefix="/status")
|
status_bp = Blueprint("status", __name__, url_prefix="/status")
|
||||||
@@ -40,12 +42,17 @@ async def rows():
|
|||||||
@status_bp.get("/widget")
|
@status_bp.get("/widget")
|
||||||
@require_role(UserRole.viewer)
|
@require_role(UserRole.viewer)
|
||||||
async def widget():
|
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:
|
async with current_app.db_sessionmaker() as db:
|
||||||
entries = await collect_status(db)
|
entries = await collect_status(db)
|
||||||
|
total_hosts = (await db.execute(
|
||||||
|
select(func.count()).select_from(Host))).scalar()
|
||||||
up, down, pending = _summarize(entries)
|
up, down, pending = _summarize(entries)
|
||||||
return await render_template(
|
return await render_template(
|
||||||
"status/widget.html",
|
"status/widget.html",
|
||||||
entries=entries, up=up, down=down, pending=pending,
|
entries=entries, up=up, down=down, pending=pending,
|
||||||
|
total_hosts=total_hosts,
|
||||||
widget_id=request.args.get("wid", "0"),
|
widget_id=request.args.get("wid", "0"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,37 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ── Summary strip ─────────────────────────────────────────────────────────── #}
|
{# The old fixed summary strip (Hosts/Ping/DNS/Alerts) was merged into the single
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:0.75rem;margin-bottom:1.5rem;">
|
'Overview' widget (status_overview) so there's one canonical summary, not two. #}
|
||||||
<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>
|
|
||||||
|
|
||||||
{# ── Widget grid (Gridstack; static until Edit is toggled) ──────────────────── #}
|
{# ── Widget grid (Gridstack; static until Edit is toggled) ──────────────────── #}
|
||||||
{% if not widgets %}
|
{% if not widgets %}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
{# status/widget.html — compact dashboard summary + problem list #}
|
{# status/widget.html — the unified Overview: host count + monitor health
|
||||||
<div style="display:flex;gap:1.25rem;align-items:baseline;margin-bottom:0.6rem;">
|
(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: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 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>
|
<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>
|
</div>
|
||||||
|
|
||||||
{% set problems = entries | rejectattr("status", "equalto", "up") | list %}
|
{% set problems = entries | rejectattr("status", "equalto", "up") | list %}
|
||||||
|
|||||||
Reference in New Issue
Block a user