From 791af2d58f09408e2e169e158237d751a5631346 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 18 Mar 2026 23:09:49 -0400 Subject: [PATCH] feat: ping/DNS status and latency in hosts list and dashboard summary --- fablednetmon/dashboard/routes.py | 55 ++++++++++++++++- fablednetmon/hosts/routes.py | 41 ++++++++++++- fablednetmon/templates/dashboard/index.html | 65 +++++++++++++++------ fablednetmon/templates/hosts/list.html | 47 +++++++++++++-- 4 files changed, 181 insertions(+), 27 deletions(-) diff --git a/fablednetmon/dashboard/routes.py b/fablednetmon/dashboard/routes.py index 7085aef..95b5472 100644 --- a/fablednetmon/dashboard/routes.py +++ b/fablednetmon/dashboard/routes.py @@ -1,6 +1,10 @@ -from quart import Blueprint, render_template +from __future__ import annotations +from quart import Blueprint, render_template, current_app +from sqlalchemy import select, func from fablednetmon.auth.middleware import require_role from fablednetmon.models.users import UserRole +from fablednetmon.models.hosts import Host +from fablednetmon.models.monitors import PingResult, DnsResult dashboard_bp = Blueprint("dashboard", __name__) @@ -8,4 +12,51 @@ dashboard_bp = Blueprint("dashboard", __name__) @dashboard_bp.get("/") @require_role(UserRole.viewer) async def index(): - return await render_template("dashboard/index.html") + async with current_app.db_sessionmaker() as db: + # Latest ping per host + 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), + ) + ) + latest_pings = list(pr.scalars()) + ping_up = sum(1 for p in latest_pings if p.status.value == "up") + ping_down = sum(1 for p in latest_pings if p.status.value == "down") + + # Latest DNS per host + 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), + ) + ) + latest_dns = list(dr.scalars()) + dns_ok = sum(1 for d in latest_dns if d.status.value == "resolved") + dns_fail = sum(1 for d in latest_dns if d.status.value != "resolved") + + total_hosts = (await db.execute(select(func.count()).select_from(Host))).scalar() + + poll_interval = current_app.config.get("MONITORS_POLL_INTERVAL", 60) + + return await render_template( + "dashboard/index.html", + ping_up=ping_up, + ping_down=ping_down, + dns_ok=dns_ok, + dns_fail=dns_fail, + total_hosts=total_hosts, + poll_interval=poll_interval, + ) diff --git a/fablednetmon/hosts/routes.py b/fablednetmon/hosts/routes.py index 9c816fe..3eccfbb 100644 --- a/fablednetmon/hosts/routes.py +++ b/fablednetmon/hosts/routes.py @@ -1,8 +1,9 @@ from __future__ import annotations from quart import Blueprint, render_template, request, redirect, url_for, current_app -from sqlalchemy import select +from sqlalchemy import select, func from fablednetmon.auth.middleware import require_role from fablednetmon.models.hosts import Host, ProbeType +from fablednetmon.models.monitors import PingResult, DnsResult from fablednetmon.models.users import UserRole hosts_bp = Blueprint("hosts", __name__, url_prefix="/hosts") @@ -14,7 +15,43 @@ async def list_hosts(): async with current_app.db_sessionmaker() as db: result = await db.execute(select(Host).order_by(Host.name)) hosts = result.scalars().all() - return await render_template("hosts/list.html", hosts=hosts) + + # Latest ping result per host + 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), + ) + ) + latest_pings = {r.host_id: r for r in pr.scalars()} + + # Latest DNS result per host + 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), + ) + ) + latest_dns = {r.host_id: r for r in dr.scalars()} + + return await render_template( + "hosts/list.html", + hosts=hosts, + latest_pings=latest_pings, + latest_dns=latest_dns, + ) @hosts_bp.get("/new") diff --git a/fablednetmon/templates/dashboard/index.html b/fablednetmon/templates/dashboard/index.html index 255b8f8..3987105 100644 --- a/fablednetmon/templates/dashboard/index.html +++ b/fablednetmon/templates/dashboard/index.html @@ -2,31 +2,60 @@ {% block title %}Dashboard — FabledNetMon{% endblock %} {% block content %}

Dashboard

-
+
+
-

Ping Monitor

-

- Manage hosts to begin monitoring. -

+

Ping

+ {% if ping_up + ping_down == 0 %} +

Add hosts to begin monitoring.

+ {% else %} +
+
+ {{ ping_up }} + up +
+ {% if ping_down > 0 %} +
+ {{ ping_down }} + down +
+ {% endif %} +
+

every {{ poll_interval }}s — view all

+ {% endif %}
+
-

DNS Monitor

-

- Enable DNS monitoring on a host. -

+

DNS

+ {% if dns_ok + dns_fail == 0 %} +

Enable DNS monitoring on a host.

+ {% else %} +
+
+ {{ dns_ok }} + ok +
+ {% if dns_fail > 0 %} +
+ {{ dns_fail }} + failed +
+ {% endif %} +
+ {% endif %}
+
-

Alerts

-

- Configure alert rules against any metric. -

+

Hosts

+ {{ total_hosts }} + configured +

Manage →

+
-

Ansible

-

- Browse playbooks or view - run history. -

+

Alerts

+

Configure alert rules against any metric.

+
{% endblock %} diff --git a/fablednetmon/templates/hosts/list.html b/fablednetmon/templates/hosts/list.html index a74aacc..8589442 100644 --- a/fablednetmon/templates/hosts/list.html +++ b/fablednetmon/templates/hosts/list.html @@ -13,19 +13,56 @@ Name Address Probe - Monitors + Ping + Latency + DNS {% for host in hosts %} + {% set ping = latest_pings.get(host.id) %} + {% set dns = latest_dns.get(host.id) %} {{ host.name }} {{ host.address }} - {{ host.probe_type.value | upper }}:{{ host.probe_port }} - - {% if host.ping_enabled %}ping{% endif %} - {% if host.dns_enabled %}dns{% endif %} + + {{ host.probe_type.value | upper }}{% if host.probe_type.value == 'tcp' %}:{{ host.probe_port }}{% endif %} + + + {% if not host.ping_enabled %} + off + {% elif ping is none %} + + {% elif ping.status.value == 'up' %} + + {% else %} + + {% endif %} + + + {% if ping and ping.response_time_ms is not none %} + {% if ping.response_time_ms < 10 %} + {{ ping.response_time_ms | round(1) }} ms + {% elif ping.response_time_ms < 100 %} + {{ ping.response_time_ms | round(1) }} ms + {% else %} + {{ ping.response_time_ms | round(1) }} ms + {% endif %} + {% else %} + + {% endif %} + + + {% if not host.dns_enabled %} + off + {% elif dns is none %} + + {% elif dns.status.value == 'resolved' %} + + {% else %} + + {% endif %} Edit