linuxrouter/docker/routlin-dash/app/pages/overview/view.py
2026-06-09 20:52:50 -04:00

231 lines
8.4 KiB
Python

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_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 '<p class="text-muted" style="margin:0.5rem 0 0">No upstream server data recorded yet.</p>'
rows = []
for s in servers:
latency = f'{s["avg_latency_ms"]} ms' if s.get("avg_latency_ms") else '-'
rows.append(
f'<tr>'
f'<td class="table-cell">{factory.e(s.get("address", "-"))}</td>'
f'<td class="table-cell">{s.get("queries_sent", 0):,}</td>'
f'<td class="table-cell">{s.get("retried", 0):,}</td>'
f'<td class="table-cell">{s.get("failed", 0):,}</td>'
f'<td class="table-cell">{s.get("nxdomain", 0):,}</td>'
f'<td class="table-cell">{latency}</td>'
f'</tr>'
)
return (
'<table class="data-table" style="margin-top:0.75rem"><thead><tr>'
'<th class="table-header">Upstream DNS Provider</th>'
'<th class="table-header">Queries Sent</th>'
'<th class="table-header">Retried</th>'
'<th class="table-header">Failed</th>'
'<th class="table-header">NXDOMAIN</th>'
'<th class="table-header">Avg Latency</th>'
'</tr></thead><tbody>'
+ ''.join(rows)
+ '</tbody></table>'
)
def load_dns_metrics():
empty = {'queries': '-', 'hits': '-', 'hit_rate': '-', 'forwarded': '-',
'tcp_peak': '-', 'cache_evictions': '-', 'updated': '-', 'since': '-', 'servers': []}
try:
with open(METRICS_FILE) as f:
data = json.load(f)
t = data.get('totals', {})
meta = data.get('metadata', {})
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 '-',
'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
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 = '<p class="text-muted" style="margin:0.5rem 0 0">No query data collected yet.</p>'
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'<tr><td class="table-cell">{factory.e(r[0])}</td>'
f'<td class="table-cell">{r[1]:,}</td></tr>'
for r in rows
)
return (
'<table class="data-table"><thead><tr>'
'<th class="table-header">Domain</th>'
'<th class="table-header">Times Blocked</th>'
'</tr></thead><tbody>' + trs + '</tbody></table>'
)
except Exception:
return no_data
def client_activity_table():
no_data = '<p class="text-muted" style="margin:0.5rem 0 0">No query data collected yet.</p>'
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'<tr><td class="table-cell">{factory.e(client_ip)}</td>'
f'<td class="table-cell">{total:,}</td>'
f'<td class="table-cell">{int(blocked):,} ({pct})</td></tr>'
)
return (
'<table class="data-table"><thead><tr>'
'<th class="table-header">Client</th>'
'<th class="table-header">Total Queries</th>'
'<th class="table-header">Blocked</th>'
'</tr></thead><tbody>' + ''.join(trs) + '</tbody></table>'
)
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():
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_LEASES_LINK'] = f'<a href="/dhcpleases">{lease_count} active lease{"s" if lease_count != 1 else ""}</a>'
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