From 165a202ba4c61e993113d3ed6f7d187f5bc56b76 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 18 Mar 2026 23:37:46 -0400 Subject: [PATCH] feat: UI redesign (CSS system, DNS widget, Traefik dashboard widget) - Replace base.html style block with full CSS design system using custom properties (dark theme, cards, tables, badges, dots, widgets) - Add DNS blueprint (fablednetmon/dns/) with /dns/ and /dns/rows routes + templates - Add Traefik /widget route and widget.html fragment for dashboard - Update dashboard: DNS + Traefik widgets, traefik_enabled config detection - Update all templates to use new CSS classes (page-title, section-title, card-flush, table, badge-*, dot-*, btn-*) - Register dns_bp in app.py Co-Authored-By: Claude Sonnet 4.6 --- fablednetmon/app.py | 2 + fablednetmon/dashboard/routes.py | 15 +- fablednetmon/dns/__init__.py | 0 fablednetmon/dns/routes.py | 52 ++++++ fablednetmon/templates/alerts/list.html | 78 ++++----- fablednetmon/templates/alerts/rules_form.html | 8 +- fablednetmon/templates/ansible/browse.html | 37 ++-- fablednetmon/templates/ansible/index.html | 36 ++-- .../templates/ansible/run_detail.html | 2 +- fablednetmon/templates/auth/login.html | 4 +- fablednetmon/templates/auth/setup.html | 6 +- fablednetmon/templates/base.html | 163 ++++++++++++++---- fablednetmon/templates/dashboard/index.html | 99 +++++++---- fablednetmon/templates/dns/index.html | 16 ++ fablednetmon/templates/dns/rows.html | 29 ++++ fablednetmon/templates/hosts/form.html | 8 +- fablednetmon/templates/hosts/list.html | 68 ++++---- fablednetmon/templates/ping/index.html | 10 +- fablednetmon/templates/settings/index.html | 18 +- plugins/traefik/routes.py | 23 +++ plugins/traefik/templates/traefik/index.html | 28 +-- plugins/traefik/templates/traefik/widget.html | 29 ++++ 22 files changed, 495 insertions(+), 236 deletions(-) create mode 100644 fablednetmon/dns/__init__.py create mode 100644 fablednetmon/dns/routes.py create mode 100644 fablednetmon/templates/dns/index.html create mode 100644 fablednetmon/templates/dns/rows.html create mode 100644 plugins/traefik/templates/traefik/widget.html diff --git a/fablednetmon/app.py b/fablednetmon/app.py index ca2770b..9007194 100644 --- a/fablednetmon/app.py +++ b/fablednetmon/app.py @@ -87,6 +87,7 @@ def create_app( from .dashboard.routes import dashboard_bp from .hosts.routes import hosts_bp from .ping.routes import ping_bp + from .dns.routes import dns_bp from .alerts.routes import alerts_bp from .ansible.routes import ansible_bp from .settings.routes import settings_bp @@ -95,6 +96,7 @@ def create_app( app.register_blueprint(dashboard_bp) app.register_blueprint(hosts_bp) app.register_blueprint(ping_bp) + app.register_blueprint(dns_bp) app.register_blueprint(alerts_bp) app.register_blueprint(ansible_bp) app.register_blueprint(settings_bp) diff --git a/fablednetmon/dashboard/routes.py b/fablednetmon/dashboard/routes.py index 95b5472..dc1cd63 100644 --- a/fablednetmon/dashboard/routes.py +++ b/fablednetmon/dashboard/routes.py @@ -16,8 +16,7 @@ async def index(): # 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() + .group_by(PingResult.host_id).subquery() ) pr = await db.execute( select(PingResult).join( @@ -33,8 +32,7 @@ async def index(): # 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() + .group_by(DnsResult.host_id).subquery() ) dr = await db.execute( select(DnsResult).join( @@ -49,14 +47,15 @@ async def index(): total_hosts = (await db.execute(select(func.count()).select_from(Host))).scalar() + plugins = current_app.config.get("PLUGINS", {}) + traefik_enabled = plugins.get("traefik", {}).get("enabled", False) 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, + ping_up=ping_up, ping_down=ping_down, + dns_ok=dns_ok, dns_fail=dns_fail, total_hosts=total_hosts, + traefik_enabled=traefik_enabled, poll_interval=poll_interval, ) diff --git a/fablednetmon/dns/__init__.py b/fablednetmon/dns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fablednetmon/dns/routes.py b/fablednetmon/dns/routes.py new file mode 100644 index 0000000..8fbcd25 --- /dev/null +++ b/fablednetmon/dns/routes.py @@ -0,0 +1,52 @@ +from __future__ import annotations +from quart import Blueprint, current_app, render_template +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 DnsResult + +dns_bp = Blueprint("dns", __name__, url_prefix="/dns") + + +async def _latest_dns(db, host_ids: list[str]) -> dict: + if not host_ids: + return {} + subq = ( + select(DnsResult.host_id, func.max(DnsResult.resolved_at).label("max_at")) + .where(DnsResult.host_id.in_(host_ids)) + .group_by(DnsResult.host_id) + .subquery() + ) + pr = await db.execute( + select(DnsResult).join( + subq, + (DnsResult.host_id == subq.c.host_id) & (DnsResult.resolved_at == subq.c.max_at), + ) + ) + return {r.host_id: r for r in pr.scalars()} + + +@dns_bp.get("/") +@require_role(UserRole.viewer) +async def index(): + async with current_app.db_sessionmaker() as db: + result = await db.execute( + select(Host).where(Host.dns_enabled.is_(True)).order_by(Host.name) + ) + hosts = result.scalars().all() + latest = await _latest_dns(db, [h.id for h in hosts]) + poll_interval = current_app.config.get("MONITORS_POLL_INTERVAL", 60) + return await render_template("dns/index.html", hosts=hosts, latest=latest, poll_interval=poll_interval) + + +@dns_bp.get("/rows") +@require_role(UserRole.viewer) +async def rows(): + async with current_app.db_sessionmaker() as db: + result = await db.execute( + select(Host).where(Host.dns_enabled.is_(True)).order_by(Host.name) + ) + hosts = result.scalars().all() + latest = await _latest_dns(db, [h.id for h in hosts]) + return await render_template("dns/rows.html", hosts=hosts, latest=latest) diff --git a/fablednetmon/templates/alerts/list.html b/fablednetmon/templates/alerts/list.html index 4824701..be43f26 100644 --- a/fablednetmon/templates/alerts/list.html +++ b/fablednetmon/templates/alerts/list.html @@ -1,40 +1,40 @@ {% extends "base.html" %} {% block title %}Alerts — FabledNetMon{% endblock %} {% block content %} -

Alerts

+

Alerts

{% if active %} -

Active

-
- +

Active

+
+
- - - - - - + + + + + + {% for state, rule in active %} - - + - - - - + + + @@ -44,40 +44,40 @@
StateRuleResourceMetric
StateRuleResourceMetric
+
{% if state.state.value == 'firing' %} - FIRING + FIRING {% elif state.state.value == 'acknowledged' %} - ACK + ACK {% else %} - PENDING + PENDING {% endif %} {{ rule.name }}{{ rule.resource_name }}{{ rule.metric_name }} {{ rule.operator.value }} {{ rule.threshold }} + {{ rule.name }}{{ rule.resource_name }}{{ rule.metric_name }} {{ rule.operator.value }} {{ rule.threshold }} {% if state.state.value == 'firing' %}
- +
{% endif %}
{% else %} -

No active alerts.

+

No active alerts.

{% endif %}
-

Rules

+

Rules

New Rule
{% if rules %} -
- +
+
- - - - - - + + + + + + {% for rule in rules %} - - - + + - - + - @@ -87,6 +87,6 @@
NameConditionConsec.Enabled
NameConditionConsec.Enabled
{{ rule.name }} +
{{ rule.name }} {{ rule.source_module }}/{{ rule.resource_name }}:{{ rule.metric_name }} {{ rule.operator.value }} {{ rule.threshold }} {{ rule.consecutive_failures_required }} - {% if rule.enabled %}yes{% else %}no{% endif %} + {{ rule.consecutive_failures_required }} + {% if rule.enabled %}yes{% else %}no{% endif %} +
-
{% else %} -

No alert rules configured.

+

No alert rules configured.

{% endif %} {% endblock %} diff --git a/fablednetmon/templates/alerts/rules_form.html b/fablednetmon/templates/alerts/rules_form.html index 6a7bbcf..6c5d23f 100644 --- a/fablednetmon/templates/alerts/rules_form.html +++ b/fablednetmon/templates/alerts/rules_form.html @@ -2,7 +2,7 @@ {% block title %}New Alert Rule — FabledNetMon{% endblock %} {% block content %}
-

New Alert Rule

+

New Alert Rule

@@ -24,7 +24,7 @@
- {% for val, label in operators %} {% endfor %} @@ -40,8 +40,8 @@
- - Cancel + + Cancel
diff --git a/fablednetmon/templates/ansible/browse.html b/fablednetmon/templates/ansible/browse.html index 19f5e2b..4c66689 100644 --- a/fablednetmon/templates/ansible/browse.html +++ b/fablednetmon/templates/ansible/browse.html @@ -2,14 +2,14 @@ {% block title %}Browse Playbooks — FabledNetMon{% endblock %} {% block content %}
-

Browse Playbooks

+

Browse Playbooks

← Run History
{% if view_contents is defined %}
-

{{ view_path }}

+

{{ view_path }}

← Back to browse
{{ view_contents }}
@@ -26,7 +26,7 @@ {% for sd in source_data %}
-

{{ sd.source.name }}

+

{{ sd.source.name }}

{{ sd.source.type }} {{ sd.source.path }}
@@ -34,25 +34,25 @@ {% if not sd.playbooks %}

No playbooks found at this path.

{% else %} - +
- - - + + + {% for pb in sd.playbooks %} - - - + + {% endfor %} @@ -60,19 +60,19 @@
Playbook
Playbook
{{ pb }} - View +
{{ pb }} + View Run + class="btn btn-sm btn-ghost">Run
{# Run trigger form for this source #} -