import json import os from datetime import datetime import config_utils import factory from pages.ddns.view import public_ip_info from pages.dhcpleases.view import live_dhcp_leases METRICS_DB = config_utils.DNS_METRICS_DB def _fmt_since(since_str): try: dt = datetime.strptime(since_str, '%Y-%m-%d') now = datetime.now() rel = config_utils.relative_time(int(dt.timestamp()), int(now.timestamp())) if dt.date() == now.date(): return 'Today' return f'{dt.strftime("%Y-%m-%d")} ({rel} ago)' except Exception: return since_str def _fmt_updated(updated_ts): try: now = datetime.now() rel = config_utils.relative_time(int(updated_ts), int(now.timestamp())) return f'{rel} ago' except Exception: return '-' 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) + '
Upstream DNS ProviderQueries SentRetriedFailedNXDOMAINAvg Latency
' ) def load_dns_metrics(period=0): import sqlite3 empty = { 'queries': '-', 'hits': '-', 'hit_rate': '-', 'forwarded': '-', 'auth': '-', 'tcp_peak': '-', 'cache_evictions': '-', 'updated': '-', 'since': '-', 'servers': [], } try: where = ( f"WHERE date >= date('now','localtime','-{period - 1} days')" if period and period > 0 else '' ) con = sqlite3.connect(METRICS_DB, timeout=5) con.execute('PRAGMA journal_mode=WAL') row = con.execute(f''' SELECT MIN(date), MAX(last_updated), SUM(queries_forwarded), SUM(queries_answered_locally), SUM(queries_authoritative), SUM(cache_reused), MAX(tcp_hwm) FROM daily_totals {where} ''').fetchone() srv_rows = con.execute(f''' SELECT ds.address, SUM(ds.queries_sent), SUM(ds.retried), SUM(ds.failed), SUM(ds.nxdomain), (SELECT avg_latency_ms FROM daily_servers d2 WHERE d2.address = ds.address AND d2.avg_latency_ms > 0 ORDER BY d2.date DESC LIMIT 1) FROM daily_servers ds {where} GROUP BY ds.address ORDER BY SUM(ds.queries_sent) DESC ''').fetchall() con.close() if not row or row[0] is None: return empty since_raw, updated_raw, fwd, hits, auth, reused, tcp_hwm = row fwd = fwd or 0 hits = hits or 0 total = fwd + hits servers = [ { 'address': r[0], 'queries_sent': r[1] or 0, 'retried': r[2] or 0, 'failed': r[3] or 0, 'nxdomain': r[4] or 0, 'avg_latency_ms': r[5] or 0, } for r in srv_rows ] 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'{(auth or 0):,}', 'tcp_peak': str(tcp_hwm or 0), 'cache_evictions': f'{(reused or 0):,}', 'updated': _fmt_updated(updated_raw), 'since': _fmt_since(since_raw), 'servers': servers, } except Exception: return empty DNS_QUERIES_DB = config_utils.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'{factory.e(r[0])}' f'{r[1]:,}' for r in rows ) return ( '' '' '' '' + 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):,} ({pct}%)' except Exception: return '-' def count_blocked_domains(): try: total = sum( int(factory.run(f'wc -l < "{config_utils.BLOCKLISTS_DIR}/{f}"') or 0) for f in os.listdir(config_utils.BLOCKLISTS_DIR) if f.endswith('.con') ) return str(total) except Exception: return '-' def bl_last_update(): try: mtime = max( os.path.getmtime(f'{config_utils.BLOCKLISTS_DIR}/{f}') for f in os.listdir(config_utils.BLOCKLISTS_DIR) if f.endswith('.con') ) return config_utils.fmt_timestamp(int(mtime)) except Exception: return '-' def collect_tokens(cfg): tokens = config_utils.collect_layout_tokens(cfg) 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, _ = public_ip_info(ddns) lease_count = len(live_dhcp_leases()) tokens['STAT_LEASE_COUNT'] = str(lease_count) 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_CACHE_EVICTIONS'] = dns_stats['cache_evictions'] tokens['DNS_METRICS_SINCE'] = dns_stats['since'] 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