From e9166d8a6aa90c2dc674e10b0c715ba99cded04d Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Tue, 9 Jun 2026 20:52:50 -0400 Subject: [PATCH] Development --- docker/routlin-dash/app/factory.py | 6 +- .../app/pages/overview/content.json | 167 +++++------------- .../routlin-dash/app/pages/overview/view.py | 145 +++++++++++---- 3 files changed, 159 insertions(+), 159 deletions(-) diff --git a/docker/routlin-dash/app/factory.py b/docker/routlin-dash/app/factory.py index 7524eda..e744d21 100644 --- a/docker/routlin-dash/app/factory.py +++ b/docker/routlin-dash/app/factory.py @@ -1131,6 +1131,9 @@ def build_items(items, tokens, inherited_req=None): req = item.get('client_requirement', inherited_req) if not passes(req, level): continue + data_req = item.get('data_requirement') + if data_req and not tokens.get(data_req): + continue parts.append(build_item(item, tokens, req)) return ''.join(parts) @@ -1209,7 +1212,8 @@ def build_item(item, tokens, inherited_req=None): label = e(apply_tokens(item.get('label', ''), tokens)) raw_value = apply_tokens(item.get('value', ''), tokens) value = e(raw_value) - sub = e(apply_tokens(item.get('sub', ''), tokens)) + sub_raw = apply_tokens(item.get('sub', ''), tokens) + sub = sub_raw if sub_raw.startswith('<') else e(sub_raw) variant = item.get('variant', '') cls = f'stat-card{(" stat-card-" + variant) if variant else ""}' edit_action = item.get('edit_action', '') diff --git a/docker/routlin-dash/app/pages/overview/content.json b/docker/routlin-dash/app/pages/overview/content.json index 16a163e..4029ccd 100644 --- a/docker/routlin-dash/app/pages/overview/content.json +++ b/docker/routlin-dash/app/pages/overview/content.json @@ -50,15 +50,28 @@ "type": "stat_card", "label": "DHCP Leases", "value": "%STAT_LEASE_COUNT%", - "sub": "active leases", + "sub": "%STAT_LEASES_LINK%", "variant": "accent" }, + { + "type": "stat_card", + "label": "DNS Queries", + "value": "%DNS_STAT_QUERIES%", + "sub": "since %DNS_METRICS_SINCE%" + }, + { + "type": "stat_card", + "label": "DNS Cache", + "value": "%DNS_STAT_HITS% (%DNS_STAT_HIT_RATE% hit rate)", + "sub": "cache size: %DNS_CACHE_SIZE%, evictions: %DNS_STAT_CACHE_EVICTIONS%" + }, { "type": "stat_card", "label": "Queries Blocked", - "value": "%STAT_BLOCKED_TODAY%", - "sub": "in last 24h", - "variant": "warning" + "value": "%STAT_BLOCKED_ALLTIME%", + "sub": "all time", + "variant": "warning", + "data_requirement": "HAS_QUERY_LOGGING" }, { "type": "stat_card", @@ -68,126 +81,6 @@ } ] }, - { - "type": "card", - "label": "Network", - "client_requirement": "client_is_viewer+", - "items": [ - { - "type": "grid", - "rows": [ - { - "cells": [ - { - "type": "grid_label", - "text": "WAN Interface" - }, - { - "type": "grid_value", - "text": "%GENERAL_WAN_INTERFACE%" - } - ] - }, - { - "cells": [ - { - "type": "grid_label", - "text": "VLANs" - }, - { - "type": "grid_value", - "text": "%OVERVIEW_VLAN_NAMES%" - } - ] - }, - { - "cells": [ - { - "type": "grid_label", - "text": "Firewall" - }, - { - "type": "grid_value", - "text": "%STAT_NFTABLES_STATUS%" - } - ] - }, - { - "cells": [ - { - "type": "grid_label", - "text": "System Uptime" - }, - { - "type": "grid_value", - "text": "%STAT_UPTIME%" - } - ] - } - ] - } - ] - }, - { - "type": "card", - "label": "DNS Blocking", - "client_requirement": "client_is_viewer+", - "items": [ - { - "type": "grid", - "rows": [ - { - "cells": [ - { - "type": "grid_label", - "text": "Blocked Domains" - }, - { - "type": "grid_value", - "text": "%STAT_BLOCKED_DOMAINS%" - } - ] - }, - { - "cells": [ - { - "type": "grid_label", - "text": "Active Blocklists" - }, - { - "type": "grid_value", - "text": "%STAT_BLOCKLIST_COUNT% lists" - } - ] - }, - { - "cells": [ - { - "type": "grid_label", - "text": "Last Refreshed" - }, - { - "type": "grid_value", - "text": "%STAT_BL_LAST_UPDATE%" - } - ] - }, - { - "cells": [ - { - "type": "grid_label", - "text": "Active IP Bans" - }, - { - "type": "grid_value", - "text": "%STAT_BANNED_IP_COUNT% rules" - } - ] - } - ] - } - ] - }, { "type": "card", "label": "DNS Statistics", @@ -245,6 +138,30 @@ "html": "%DNS_PROVIDERS_TABLE%" } ] + }, + { + "type": "card", + "label": "Blocked Domains", + "client_requirement": "client_is_viewer+", + "data_requirement": "HAS_QUERY_LOGGING", + "items": [ + { + "type": "raw_html", + "html": "%BLOCKED_DOMAINS_TABLE%" + } + ] + }, + { + "type": "card", + "label": "Client Activity", + "client_requirement": "client_is_viewer+", + "data_requirement": "HAS_QUERY_LOGGING", + "items": [ + { + "type": "raw_html", + "html": "%CLIENT_ACTIVITY_TABLE%" + } + ] } ] -} \ No newline at end of file +} diff --git a/docker/routlin-dash/app/pages/overview/view.py b/docker/routlin-dash/app/pages/overview/view.py index a7c963e..57708c3 100644 --- a/docker/routlin-dash/app/pages/overview/view.py +++ b/docker/routlin-dash/app/pages/overview/view.py @@ -49,7 +49,7 @@ def _dns_providers_table(servers): ) return ( '' - '' + '' '' '' '' @@ -89,9 +89,92 @@ def load_dns_metrics(): return empty -def count_blocked_today(): - out = factory.run("journalctl -u 'dnsmasq-routlin-*' --since '24 hours ago' --no-pager 2>/dev/null | grep -c ' is 0\\.0\\.0\\.0'") - return out.strip() or '0' +DNS_QUERIES_DB = f'{config_utils.CONFIGS_DIR}/dns-queries.db' + + +def has_query_logging(cfg): + return any(v.get('dnsmasq_log_queries') for v in cfg.get('vlans', [])) + + +def blocked_domains_table(): + no_data = '

No query data collected yet.

' + try: + import sqlite3 + if not os.path.exists(DNS_QUERIES_DB): + return no_data + con = sqlite3.connect(DNS_QUERIES_DB) + rows = con.execute( + 'SELECT domain, COUNT(*) as cnt FROM dns_queries WHERE blocked=1 ' + 'GROUP BY domain ORDER BY cnt DESC LIMIT 10' + ).fetchall() + con.close() + if not rows: + return no_data + trs = ''.join( + f'' + f'' + for r in rows + ) + return ( + '
ServerUpstream DNS ProviderQueries SentRetriedFailed
{factory.e(r[0])}{r[1]:,}
' + '' + '' + '' + trs + '
DomainTimes Blocked
' + ) + except Exception: + return no_data + + +def client_activity_table(): + no_data = '

No query data collected yet.

' + try: + import sqlite3 + if not os.path.exists(DNS_QUERIES_DB): + return no_data + con = sqlite3.connect(DNS_QUERIES_DB) + rows = con.execute( + 'SELECT client_ip, COUNT(*) as total, SUM(blocked) as blocked ' + 'FROM dns_queries GROUP BY client_ip ORDER BY total DESC LIMIT 10' + ).fetchall() + con.close() + if not rows: + return no_data + trs = [] + for client_ip, total, blocked in rows: + pct = f'{blocked / total * 100:.0f}%' if total else '0%' + trs.append( + f'{factory.e(client_ip)}' + f'{total:,}' + f'{int(blocked):,} ({pct})' + ) + return ( + '' + '' + '' + '' + '' + ''.join(trs) + '
ClientTotal QueriesBlocked
' + ) + except Exception: + return no_data + + +def all_time_blocked_display(): + try: + import sqlite3 + if not os.path.exists(DNS_QUERIES_DB): + return '-' + con = sqlite3.connect(DNS_QUERIES_DB) + row = con.execute( + 'SELECT SUM(blocked), COUNT(*) FROM dns_queries' + ).fetchone() + con.close() + blocked, total = row + if not blocked: + return '-' + pct = f'{blocked / total * 100:.0f}' if total else '0' + return f'{int(blocked):,} blocked ({pct}%)' + except Exception: + return '-' def count_blocked_domains(): @@ -118,35 +201,31 @@ def bl_last_update(): def collect_tokens(cfg): tokens = config_utils.collect_layout_tokens(cfg) - vlans = cfg.get('vlans', []) - non_vpn_vlans = [v for v in vlans if not v.get('is_vpn')] - vlan_names = [v.get('name', '') for v in vlans] - net = cfg.get('network_interfaces', {}) - dns = cfg.get('upstream_dns', {}) + non_vpn_vlans = [v for v in cfg.get('vlans', []) if not v.get('is_vpn')] + dns = cfg.get('upstream_dns', {}) dns_stats = load_dns_metrics() - ddns = factory.load_ddns() - ip_str, domains_sub, last_obtained = public_ip_info(ddns) + ddns = factory.load_ddns() + ip_str, domains_sub, _ = public_ip_info(ddns) - tokens['GENERAL_WAN_INTERFACE'] = str(net.get('wan_interface', '-')) - tokens['OVERVIEW_VLAN_NAMES'] = ', '.join(vlan_names) or '-' - tokens['STAT_VLAN_COUNT'] = str(len(non_vpn_vlans)) - tokens['STAT_LEASE_COUNT'] = str(len(live_dhcp_leases())) - tokens['STAT_BANNED_IP_COUNT'] = str(sum(1 for b in cfg.get('banned_ips', []) if b.get('enabled', True))) - tokens['STAT_BLOCKLIST_COUNT'] = str(len(cfg.get('dns_blocking', {}).get('blocklists', []))) - tokens['STAT_BLOCKED_TODAY'] = count_blocked_today() - tokens['STAT_BLOCKED_DOMAINS'] = count_blocked_domains() - tokens['STAT_BL_LAST_UPDATE'] = bl_last_update() - tokens['STAT_UPTIME'] = factory.run('uptime -p') or '-' - tokens['STAT_NFTABLES_STATUS'] = 'Active' if factory.run('nft list tables 2>/dev/null').strip() else 'Inactive' - tokens['STAT_PUBLIC_IP'] = ip_str - tokens['STAT_DDNS_HOSTNAME'] = domains_sub - tokens['DNS_CACHE_SIZE'] = str(dns.get('cache_size', '-')) - 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_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']) + lease_count = len(live_dhcp_leases()) + tokens['STAT_LEASE_COUNT'] = str(lease_count) + tokens['STAT_LEASES_LINK'] = f'{lease_count} active lease{"s" if lease_count != 1 else ""}' + tokens['STAT_VLAN_COUNT'] = str(len(non_vpn_vlans)) + tokens['STAT_PUBLIC_IP'] = ip_str + tokens['STAT_DDNS_HOSTNAME'] = domains_sub + + tokens['DNS_CACHE_SIZE'] = str(dns.get('cache_size', '-')) + 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_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']) + + tokens['STAT_BLOCKED_ALLTIME'] = all_time_blocked_display() + tokens['HAS_QUERY_LOGGING'] = '1' if has_query_logging(cfg) else '' + tokens['BLOCKED_DOMAINS_TABLE'] = blocked_domains_table() + tokens['CLIENT_ACTIVITY_TABLE'] = client_activity_table() return tokens