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 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 23:37:46 -04:00
parent d29c5b3eab
commit 165a202ba4
22 changed files with 495 additions and 236 deletions
+23
View File
@@ -68,3 +68,26 @@ async def index():
})
return await render_template("traefik/index.html", router_data=router_data)
@traefik_bp.get("/widget")
@require_role(UserRole.viewer)
async def widget():
"""HTMX dashboard widget fragment."""
async with current_app.db_sessionmaker() as db:
result = await db.execute(
select(TraefikMetric.router_name).distinct().order_by(TraefikMetric.router_name)
)
routers = [row[0] for row in result.all()]
router_data = []
for router in routers:
result = await db.execute(
select(TraefikMetric)
.where(TraefikMetric.router_name == router)
.order_by(TraefikMetric.scraped_at.desc())
.limit(1)
)
latest = result.scalar_one_or_none()
if latest:
router_data.append({"name": router, "latest": latest})
return await render_template("traefik/widget.html", router_data=router_data)
+14 -14
View File
@@ -2,7 +2,7 @@
{% extends "base.html" %}
{% block title %}Traefik — FabledNetMon{% endblock %}
{% block content %}
<h1 style="color:#c0c0ff;margin-bottom:1.5rem;">Traefik Routers</h1>
<h1 class="page-title">Traefik Routers</h1>
{% if not router_data %}
<div class="card">
<p style="color:#606080;">No Traefik metrics yet. Configure <code>metrics_url</code> in <code>config.yaml</code> under <code>plugins.traefik</code>.</p>
@@ -11,39 +11,39 @@
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1rem;">
{% for r in router_data %}
<div class="card">
<h3 style="color:#8080ff;margin-bottom:0.75rem;font-size:0.95rem;word-break:break-all;">{{ r.name }}</h3>
<table style="width:100%;border-collapse:collapse;font-size:0.85rem;">
<h3 class="section-title" style="margin-bottom:0.75rem;word-break:break-all;">{{ r.name }}</h3>
<table class="table" style="font-size:0.85rem;">
<tr>
<td style="color:#606080;padding:0.2rem 0;">Req/s</td>
<td style="color:#c0c0e0;padding:0.2rem 0.5rem;">{{ "%.2f"|format(r.latest.request_rate) }}</td>
<td style="color:var(--text-muted);padding:0.2rem 0;">Req/s</td>
<td style="padding:0.2rem 0.5rem;">{{ "%.2f"|format(r.latest.request_rate) }}</td>
<td style="padding:0.2rem 0;">{{ r.sparkline_req | safe }}</td>
</tr>
<tr>
<td style="color:#606080;padding:0.2rem 0;">p95 ms</td>
<td style="color:#c0c0e0;padding:0.2rem 0.5rem;">{{ "%.1f"|format(r.latest.latency_p95_ms) }}</td>
<td style="color:var(--text-muted);padding:0.2rem 0;">p95 ms</td>
<td style="padding:0.2rem 0.5rem;">{{ "%.1f"|format(r.latest.latency_p95_ms) }}</td>
<td style="padding:0.2rem 0;">{{ r.sparkline_p95 | safe }}</td>
</tr>
<tr>
<td style="color:#606080;padding:0.2rem 0;">p99 ms</td>
<td style="color:#e0e0e0;padding:0.2rem 0.5rem;">{{ "%.1f"|format(r.latest.latency_p99_ms) }}</td>
<td style="color:var(--text-muted);padding:0.2rem 0;">p99 ms</td>
<td style="padding:0.2rem 0.5rem;">{{ "%.1f"|format(r.latest.latency_p99_ms) }}</td>
<td style="padding:0.2rem 0;"></td>
</tr>
<tr>
<td style="color:#606080;padding:0.2rem 0;">4xx %</td>
<td style="color:{% if r.latest.error_rate_4xx_pct > 1 %}#c0a000{% else %}#40a040{% endif %};padding:0.2rem 0.5rem;">
<td style="color:var(--text-muted);padding:0.2rem 0;">4xx %</td>
<td style="color:{% if r.latest.error_rate_4xx_pct > 1 %}var(--yellow){% else %}var(--green){% endif %};padding:0.2rem 0.5rem;">
{{ "%.1f"|format(r.latest.error_rate_4xx_pct) }}%
</td>
<td style="padding:0.2rem 0;"></td>
</tr>
<tr>
<td style="color:#606080;padding:0.2rem 0;">5xx %</td>
<td style="color:{% if r.latest.error_rate_5xx_pct > 0.1 %}#a04040{% else %}#40a040{% endif %};padding:0.2rem 0.5rem;">
<td style="color:var(--text-muted);padding:0.2rem 0;">5xx %</td>
<td style="color:{% if r.latest.error_rate_5xx_pct > 0.1 %}var(--red){% else %}var(--green){% endif %};padding:0.2rem 0.5rem;">
{{ "%.1f"|format(r.latest.error_rate_5xx_pct) }}%
</td>
<td style="padding:0.2rem 0;">{{ r.sparkline_5xx | safe }}</td>
</tr>
</table>
<div style="color:#404060;font-size:0.75rem;margin-top:0.5rem;">
<div style="color:var(--text-dim);font-size:0.75rem;margin-top:0.5rem;">
Last scraped {{ r.latest.scraped_at.strftime("%H:%M:%S") }} UTC
</div>
</div>
@@ -0,0 +1,29 @@
{% if router_data %}
{% for r in router_data %}
<div class="ping-row">
<div class="ping-meta">
<span class="ping-name" style="font-size:0.8rem;word-break:break-all;">{{ r.name }}</span>
</div>
<div style="flex:1;display:flex;gap:1.5rem;font-size:0.82rem;font-variant-numeric:tabular-nums;">
<span title="Requests/s">
<span style="color:var(--text-muted);">req/s</span>
<span style="margin-left:0.3rem;">{{ "%.2f"|format(r.latest.request_rate) }}</span>
</span>
<span title="p95 latency">
<span style="color:var(--text-muted);">p95</span>
<span style="margin-left:0.3rem;
{% if r.latest.latency_p95_ms > 500 %}color:var(--red){% elif r.latest.latency_p95_ms > 200 %}color:var(--yellow){% else %}color:var(--green){% endif %};">
{{ "%.0f"|format(r.latest.latency_p95_ms) }}ms
</span>
</span>
{% if r.latest.error_rate_5xx_pct > 0 %}
<span title="5xx error rate">
<span style="color:var(--red);">{{ "%.1f"|format(r.latest.error_rate_5xx_pct) }}% 5xx</span>
</span>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<p class="empty" style="padding:0.5rem 0;font-size:0.85rem;">No Traefik data yet.</p>
{% endif %}