feat: dashboard top hosts/types, problems 24h, drill-down (d2-2)

- Extend /dashboards/summary; Events filters from query (hostname, severity)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 10:01:07 +10:00
parent 249894d8e1
commit c8f357007f
7 changed files with 271 additions and 6 deletions
+53
View File
@@ -15,6 +15,17 @@ from app.services.host_health import DAILY_REPORT_SSH, HEARTBEAT_TYPE, count_sta
router = APIRouter(prefix="/dashboards", tags=["dashboards"])
class TopHostItem(BaseModel):
host_id: int
hostname: str
count: int
class TopTypeItem(BaseModel):
type: str
count: int
class DashboardSummary(BaseModel):
events_last_24h: int
hosts_total: int
@@ -22,7 +33,11 @@ class DashboardSummary(BaseModel):
heartbeats_24h: int
daily_reports_24h: int
problems_open: int
problems_opened_24h: int
problems_resolved_24h: int
severity_24h: dict[str, int]
top_hosts: list[TopHostItem]
top_event_types: list[TopTypeItem]
recent_events: list[EventSummary]
@@ -52,6 +67,40 @@ def dashboard_summary(
problems_open = (
db.scalar(select(func.count()).select_from(Problem).where(Problem.status == "open")) or 0
)
problems_opened_24h = (
db.scalar(select(func.count()).select_from(Problem).where(Problem.created_at >= since)) or 0
)
problems_resolved_24h = (
db.scalar(
select(func.count()).select_from(Problem).where(
Problem.status == "resolved",
Problem.updated_at >= since,
)
)
or 0
)
top_host_rows = db.execute(
select(Host.id, Host.hostname, func.count())
.select_from(Event)
.join(Host, Event.host_id == Host.id)
.where(Event.received_at >= since)
.group_by(Host.id, Host.hostname)
.order_by(func.count().desc())
.limit(10)
).all()
top_hosts = [
TopHostItem(host_id=row[0], hostname=row[1], count=row[2]) for row in top_host_rows
]
top_type_rows = db.execute(
select(Event.type, func.count())
.where(Event.received_at >= since)
.group_by(Event.type)
.order_by(func.count().desc())
.limit(10)
).all()
top_event_types = [TopTypeItem(type=row[0], count=row[1]) for row in top_type_rows]
severity_rows = db.execute(
select(Event.severity, func.count())
@@ -92,6 +141,10 @@ def dashboard_summary(
heartbeats_24h=heartbeats_24h,
daily_reports_24h=daily_reports_24h,
problems_open=problems_open,
problems_opened_24h=problems_opened_24h,
problems_resolved_24h=problems_resolved_24h,
severity_24h=severity_24h,
top_hosts=top_hosts,
top_event_types=top_event_types,
recent_events=recent_events,
)
+92
View File
@@ -0,0 +1,92 @@
"""Dashboard summary API."""
import uuid
from datetime import datetime, timedelta, timezone
from app.models import Event, Host, Problem
def _host(db, name: str) -> Host:
h = Host(
hostname=name,
os_family="linux",
product="ssh-monitor",
last_seen_at=datetime.now(timezone.utc),
)
db.add(h)
db.flush()
return h
def _event(db, host: Host, etype: str, *, received_at: datetime | None = None) -> Event:
now = received_at or datetime.now(timezone.utc)
e = Event(
event_id=str(uuid.uuid4()),
host_id=host.id,
occurred_at=now,
received_at=now,
category="security",
type=etype,
severity="info",
title=f"t {etype}",
summary="s",
payload={},
)
db.add(e)
db.flush()
return e
def test_dashboard_summary_top_and_problems_24h(client, db_session, jwt_headers):
h1 = _host(db_session, "web-01")
h2 = _host(db_session, "db-01")
now = datetime.now(timezone.utc)
for _ in range(3):
_event(db_session, h1, "ssh.login.failed")
_event(db_session, h2, "agent.heartbeat")
_event(db_session, h2, "agent.heartbeat")
db_session.add(
Problem(
host_id=h1.id,
title="open now",
summary="s",
severity="high",
status="open",
rule_id="rule:test",
fingerprint="fp1",
event_count=1,
last_seen_at=now,
created_at=now - timedelta(hours=2),
updated_at=now - timedelta(hours=2),
)
)
db_session.add(
Problem(
host_id=h2.id,
title="resolved today",
summary="s",
severity="warning",
status="resolved",
rule_id="rule:test",
fingerprint="fp2",
event_count=1,
last_seen_at=now,
created_at=now - timedelta(hours=5),
updated_at=now - timedelta(hours=1),
)
)
db_session.commit()
r = client.get("/api/v1/dashboards/summary", headers=jwt_headers)
assert r.status_code == 200
body = r.json()
assert body["events_last_24h"] == 5
assert body["problems_open"] == 1
assert body["problems_opened_24h"] == 2
assert body["problems_resolved_24h"] == 1
assert body["top_hosts"][0]["hostname"] == "web-01"
assert body["top_hosts"][0]["count"] == 3
types = {row["type"]: row["count"] for row in body["top_event_types"]}
assert types["ssh.login.failed"] == 3
assert types["agent.heartbeat"] == 2