Development

This commit is contained in:
Matthew Grotke 2026-06-09 20:52:50 -04:00
parent 6f0dac01b0
commit e9166d8a6a
3 changed files with 159 additions and 159 deletions

View file

@ -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', '')

View file

@ -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%"
}
]
}
]
}

View file

@ -49,7 +49,7 @@ def _dns_providers_table(servers):
)
return (
'<table class="data-table" style="margin-top:0.75rem"><thead><tr>'
'<th class="table-header">Server</th>'
'<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>'
@ -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 = '<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():
@ -118,28 +201,19 @@ 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', {})
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)
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 '-'
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_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']
@ -149,4 +223,9 @@ def collect_tokens(cfg):
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