From b71fd0306fed2ab168794ed906ca88b063082d4f Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Tue, 9 Jun 2026 19:31:27 -0400 Subject: [PATCH] Development --- .../app/pages/overview/content.json | 44 +++++----- .../routlin-dash/app/pages/overview/view.py | 80 ++++++++++++++++--- 2 files changed, 85 insertions(+), 39 deletions(-) diff --git a/docker/routlin-dash/app/pages/overview/content.json b/docker/routlin-dash/app/pages/overview/content.json index bbd75e8..16a163e 100644 --- a/docker/routlin-dash/app/pages/overview/content.json +++ b/docker/routlin-dash/app/pages/overview/content.json @@ -196,6 +196,18 @@ { "type": "grid", "rows": [ + { + "cells": [ + {"type": "grid_label", "text": "Tracking Since"}, + {"type": "grid_value", "text": "%DNS_METRICS_SINCE%"} + ] + }, + { + "cells": [ + {"type": "grid_label", "text": "Last Updated"}, + {"type": "grid_value", "text": "%DNS_METRICS_UPDATED%"} + ] + }, { "cells": [ {"type": "grid_label", "text": "Total Queries"}, @@ -214,18 +226,6 @@ {"type": "grid_value", "text": "%DNS_STAT_FORWARDED%"} ] }, - { - "cells": [ - {"type": "grid_label", "text": "Authoritative Answers"}, - {"type": "grid_value", "text": "%DNS_STAT_AUTH%"} - ] - }, - { - "cells": [ - {"type": "grid_label", "text": "TCP Peak"}, - {"type": "grid_value", "text": "%DNS_STAT_TCP_PEAK%"} - ] - }, { "cells": [ {"type": "grid_label", "text": "Cache Capacity"}, @@ -234,23 +234,15 @@ }, { "cells": [ - {"type": "grid_label", "text": "DNS Providers"}, - {"type": "grid_value", "text": "%OVERVIEW_UPSTREAM_SERVERS%"} - ] - }, - { - "cells": [ - {"type": "grid_label", "text": "Tracking Since"}, - {"type": "grid_value", "text": "%DNS_METRICS_SINCE%"} - ] - }, - { - "cells": [ - {"type": "grid_label", "text": "Last Updated"}, - {"type": "grid_value", "text": "%DNS_METRICS_UPDATED%"} + {"type": "grid_label", "text": "Cache Evictions"}, + {"type": "grid_value", "text": "%DNS_STAT_CACHE_EVICTIONS%"} ] } ] + }, + { + "type": "raw_html", + "html": "%DNS_PROVIDERS_TABLE%" } ] } diff --git a/docker/routlin-dash/app/pages/overview/view.py b/docker/routlin-dash/app/pages/overview/view.py index c13da0d..a7c963e 100644 --- a/docker/routlin-dash/app/pages/overview/view.py +++ b/docker/routlin-dash/app/pages/overview/view.py @@ -1,5 +1,6 @@ import json import os +from datetime import datetime import config_utils import factory from pages.ddns.view import public_ip_info @@ -8,9 +9,61 @@ from pages.dhcpleases.view import live_dhcp_leases METRICS_FILE = f'{config_utils.CONFIGS_DIR}/.dns-metrics' +def _fmt_since(since_str): + try: + dt = datetime.strptime(since_str, '%Y-%m-%d %H:%M:%S') + now = datetime.now() + rel = config_utils.relative_time(int(dt.timestamp()), int(now.timestamp())) + if dt.date() == now.date(): + return f'Today at {dt.strftime("%H:%M")} ({rel} ago)' + return f'{dt.strftime("%Y-%m-%d")} ({rel} ago)' + except Exception: + return since_str + + +def _fmt_updated(updated_str): + try: + dt = datetime.strptime(updated_str, '%Y-%m-%d %H:%M:%S') + now = datetime.now() + rel = config_utils.relative_time(int(dt.timestamp()), int(now.timestamp())) + return f'{rel} ago' + except Exception: + return updated_str + + +def _dns_providers_table(servers): + if not servers: + return '

No upstream server data recorded yet.

' + rows = [] + for s in servers: + latency = f'{s["avg_latency_ms"]} ms' if s.get("avg_latency_ms") else '-' + rows.append( + f'' + f'{factory.e(s.get("address", "-"))}' + f'{s.get("queries_sent", 0):,}' + f'{s.get("retried", 0):,}' + f'{s.get("failed", 0):,}' + f'{s.get("nxdomain", 0):,}' + f'{latency}' + f'' + ) + return ( + '' + '' + '' + '' + '' + '' + '' + '' + + ''.join(rows) + + '
ServerQueries SentRetriedFailedNXDOMAINAvg Latency
' + ) + + def load_dns_metrics(): empty = {'queries': '-', 'hits': '-', 'hit_rate': '-', 'forwarded': '-', - 'auth': '-', 'tcp_peak': '-', 'updated': '-', 'since': '-', 'servers': []} + 'tcp_peak': '-', 'cache_evictions': '-', 'updated': '-', 'since': '-', 'servers': []} try: with open(METRICS_FILE) as f: data = json.load(f) @@ -19,16 +72,18 @@ def load_dns_metrics(): fwd = t.get('queries_forwarded', 0) hits = t.get('queries_answered_locally', 0) total = fwd + hits + since_raw = meta.get('first_recorded', '-') + updated_raw = meta.get('last_recorded', '-') return { - 'queries': f'{total:,}' if total else '-', - 'hits': f'{hits:,}' if hits else '-', - 'hit_rate': f'{hits / total * 100:.0f}%' if total > 0 else '-', - 'forwarded': f'{fwd:,}' if fwd else '-', - 'auth': f'{t.get("queries_authoritative", 0):,}', - 'tcp_peak': str(t.get('tcp_hwm', 0)), - 'updated': meta.get('last_recorded', '-'), - 'since': meta.get('first_recorded', '-'), - 'servers': t.get('servers', []), + 'queries': f'{total:,}' if total else '-', + 'hits': f'{hits:,}' if hits else '-', + 'hit_rate': f'{hits / total * 100:.0f}%' if total > 0 else '-', + 'forwarded': f'{fwd:,}' if fwd else '-', + 'tcp_peak': str(t.get('tcp_hwm', 0)), + 'cache_evictions': f'{t.get("cache_reused", 0):,}', + 'updated': _fmt_updated(updated_raw), + 'since': _fmt_since(since_raw), + 'servers': t.get('servers', []), } except Exception: return empty @@ -86,13 +141,12 @@ def collect_tokens(cfg): tokens['STAT_PUBLIC_IP'] = ip_str tokens['STAT_DDNS_HOSTNAME'] = domains_sub tokens['DNS_CACHE_SIZE'] = str(dns.get('cache_size', '-')) - tokens['OVERVIEW_UPSTREAM_SERVERS'] = ', '.join(dns.get('upstream_servers', [])) or '-' tokens['DNS_STAT_QUERIES'] = dns_stats['queries'] tokens['DNS_STAT_HITS'] = dns_stats['hits'] tokens['DNS_STAT_HIT_RATE'] = dns_stats['hit_rate'] tokens['DNS_STAT_FORWARDED'] = dns_stats['forwarded'] - tokens['DNS_STAT_AUTH'] = dns_stats['auth'] - tokens['DNS_STAT_TCP_PEAK'] = dns_stats['tcp_peak'] + tokens['DNS_STAT_CACHE_EVICTIONS'] = dns_stats['cache_evictions'] tokens['DNS_METRICS_UPDATED'] = dns_stats['updated'] tokens['DNS_METRICS_SINCE'] = dns_stats['since'] + tokens['DNS_PROVIDERS_TABLE'] = _dns_providers_table(dns_stats['servers']) return tokens