Development

This commit is contained in:
Matthew Grotke 2026-05-25 19:59:42 -04:00
parent d0cfffac52
commit adcfe55c7c
24 changed files with 405 additions and 359 deletions

View file

@ -4,7 +4,7 @@ import json, re, subprocess, os, sys, html as html_mod
import sanitize
import validation as validate
from datetime import datetime, timezone
from config_utils import core_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR
from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR
bp = Blueprint('view_page', __name__)
@ -44,8 +44,8 @@ def _load_json(path):
print(f'[view_page] ERROR loading {path}: {ex}', file=sys.stderr)
return {}
def _load_core(): return _load_json(f'{CONFIGS_DIR}/core.json')
def _load_ddns(): return _load_core().get('ddns', {})
def _load_config(): return _load_json(f'{CONFIGS_DIR}/config.json')
def _load_ddns(): return _load_config().get('ddns', {})
def _load_accounts(): return _load_json(f'{DATA_DIR}/authorized_accounts.json')
def _load_css():
@ -149,17 +149,17 @@ def _iface_status(iface):
return 'INVALID'
def _resolve_iface(vlan, core):
def _resolve_iface(vlan, cfg):
"""Compute interface name from is_vpn + derived vlan_id + general.lan_interface."""
if vlan.get('is_vpn'):
wg_vlans = [v for v in core.get('vlans', []) if v.get('is_vpn')]
wg_vlans = [v for v in cfg.get('vlans', []) if v.get('is_vpn')]
wg_sorted = sorted(wg_vlans, key=lambda v: (
validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) is None,
validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) or 0,
))
idx = next((i for i, v in enumerate(wg_sorted) if v is vlan), 0)
return f'wg{idx}'
lan = core.get('network_interfaces', {}).get('lan_interface', 'eth0')
lan = cfg.get('network_interfaces', {}).get('lan_interface', 'eth0')
vid = validate.derive_vlan_id(vlan.get('subnet', ''), vlan.get('subnet_mask', 24)) or 1
return lan if vid == 1 else f'{lan}.{vid}'
@ -187,7 +187,7 @@ def _live_dhcp_leases():
def _vlan_name_for_ip(ip):
import ipaddress
for vlan in _load_core().get('vlans', []):
for vlan in _load_config().get('vlans', []):
subnet = vlan.get('subnet', '')
mask = vlan.get('subnet_mask', 24)
if not subnet:
@ -254,11 +254,11 @@ def _fmt_bytes(n):
# Config data loaders ===============================================
def _config_datasource(name):
core = _load_core()
vlans = core.get('vlans', [])
cfg = _load_config()
vlans = cfg.get('vlans', [])
if name == 'interfaces':
gen = core.get('network_interfaces', {})
gen = cfg.get('network_interfaces', {})
wan = gen.get('wan_interface', '')
lan = gen.get('lan_interface', '')
return [
@ -267,14 +267,14 @@ def _config_datasource(name):
]
if name == 'banned_ips':
return core.get('banned_ips', [])
return cfg.get('banned_ips', [])
if name == 'host_overrides':
return core.get('host_overrides', [])
return cfg.get('host_overrides', [])
if name == 'blocklists':
rows = []
for bl in core.get('dns_blocking', {}).get('blocklists', []):
for bl in cfg.get('dns_blocking', {}).get('blocklists', []):
row = dict(bl)
bl_path = f'{CONFIGS_DIR}/blocklists/{bl.get("save_as", "")}'
try:
@ -288,12 +288,12 @@ def _config_datasource(name):
return rows
if name == 'vlans':
bl_desc = {b['name']: b.get('description', b['name']) for b in core.get('dns_blocking', {}).get('blocklists', []) if 'name' in b}
bl_desc = {b['name']: b.get('description', b['name']) for b in cfg.get('dns_blocking', {}).get('blocklists', []) if 'name' in b}
rows = []
for v in sorted(vlans, key=lambda x: validate.derive_vlan_id(x.get('subnet', ''), x.get('subnet_mask', 24)) or 0):
row = {k: v.get(k) for k in ('name', 'subnet', 'subnet_mask', 'radius_default', 'mdns_reflection', 'is_vpn', 'dnsmasq_log_queries')}
row['vlan_id'] = validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24))
row['interface'] = _resolve_iface(v, core)
row['interface'] = _resolve_iface(v, cfg)
row['use_blocklists'] = json.dumps([
{'n': bl, 'd': bl_desc.get(bl, bl)} for bl in v.get('use_blocklists', [])
])
@ -301,10 +301,10 @@ def _config_datasource(name):
return rows
if name == 'inter_vlan_exceptions':
return core.get('inter_vlan_exceptions', [])
return cfg.get('inter_vlan_exceptions', [])
if name == 'port_forwarding':
return core.get('port_forwarding', [])
return cfg.get('port_forwarding', [])
if name == 'dhcp_reservations':
rows = []
@ -418,10 +418,10 @@ def _bl_last_update():
except Exception:
return '-'
def _blocklist_stats_html(core):
def _blocklist_stats_html(cfg):
bl_dir = f'{CONFIGS_DIR}/blocklists'
rows = ''
for bl in core.get('dns_blocking', {}).get('blocklists', []):
for bl in cfg.get('dns_blocking', {}).get('blocklists', []):
name = e(bl.get('name', ''))
save_as = bl.get('save_as', '')
bl_path = f'{bl_dir}/{save_as}' if save_as else ''
@ -546,7 +546,7 @@ def _ddns_last_checked():
return 'Last checked: ---'
def _vpn_info():
for vlan in _load_core().get('vlans', []):
for vlan in _load_config().get('vlans', []):
if 'vpn_information' in vlan:
return vlan['vpn_information']
return {}
@ -556,11 +556,11 @@ def _vpn_info():
def collect_tokens():
tokens = {}
core = _load_core()
net = core.get('network_interfaces', {})
dns_blk_gen = core.get('dns_blocking', {}).get('general', {})
dns = core.get('upstream_dns', {})
vlans = core.get('vlans', [])
cfg = _load_config()
net = cfg.get('network_interfaces', {})
dns_blk_gen = cfg.get('dns_blocking', {}).get('general', {})
dns = cfg.get('upstream_dns', {})
vlans = cfg.get('vlans', [])
tokens['GENERAL_WAN_INTERFACE'] = str(net.get('wan_interface', '-'))
tokens['GENERAL_LAN_INTERFACE'] = str(net.get('lan_interface', '-'))
tokens['GENERAL_WAN_STATUS'] = _iface_status(net.get('wan_interface', ''))
@ -693,10 +693,10 @@ def collect_tokens():
tokens['VPN_VLAN_COUNT'] = str(sum(1 for v in vlans if v.get('is_vpn')))
tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([validate.derive_vlan_id(v.get('subnet', ''), v.get('subnet_mask', 24)) for v in vlans])
tokens['EXISTING_VLAN_NAMES_JSON'] = json.dumps([v.get('name', '') for v in vlans])
tokens['EXISTING_VLAN_INTERFACES_JSON'] = json.dumps([_resolve_iface(v, core) for v in vlans])
tokens['STAT_BANNED_IP_COUNT'] = str(sum(1 for b in core.get('banned_ips', []) if b.get('enabled', True)))
tokens['STAT_BLOCKLIST_COUNT'] = str(len(core.get('dns_blocking', {}).get('blocklists', [])))
tokens['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(core)
tokens['EXISTING_VLAN_INTERFACES_JSON'] = json.dumps([_resolve_iface(v, cfg) for v in vlans])
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['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(cfg)
ddns = _load_ddns()
ddns_gen = ddns.get('general', {})
@ -795,7 +795,7 @@ def collect_tokens():
tokens['BLOCKLIST_NAME_OPTIONS'] = json.dumps([
{'value': bl.get('name', ''), 'label': bl.get('description', bl.get('name', ''))}
for bl in core.get('dns_blocking', {}).get('blocklists', [])
for bl in cfg.get('dns_blocking', {}).get('blocklists', [])
])
tokens['ACCOUNT_LEVEL_OPTIONS'] = json.dumps([
@ -984,7 +984,7 @@ def _render_item(item, tokens, inherited_req=None):
f'<button type="button" class="btn btn-ghost btn-sm stat-card-edit-btn">Edit</button>'
f'</div>'
f'<form class="stat-card-edit-form" style="display:none" action="{e(edit_action)}" method="post">'
f'<input type="hidden" name="config_hash" value="{e(core_hash())}"/>'
f'<input type="hidden" name="config_hash" value="{e(config_hash())}"/>'
f'{input_wrap}'
f'<div style="margin-top:0.5em;display:flex;gap:0.5em">'
f'<button type="submit" class="btn btn-primary btn-sm" disabled>Save</button>'
@ -1066,7 +1066,7 @@ def _render_item(item, tokens, inherited_req=None):
action = e(apply_tokens(item.get('action', ''), tokens))
method = e(item.get('method', 'post'))
inner = render_items(item.get('items', []), tokens, req)
hash_field = f'<input type="hidden" name="config_hash" value="{e(core_hash())}"/>'
hash_field = f'<input type="hidden" name="config_hash" value="{e(config_hash())}"/>'
originals = _collect_form_originals(item.get('items', []), tokens)
orig_field = (f'<input type="hidden" name="original_values" value="{e(json.dumps(originals))}"/>'
if originals else '')
@ -1406,7 +1406,7 @@ def _render_table(item, tokens, inherited_req=None):
rows = _load_datasource(item.get('datasource', ''))
empty = e(item.get('empty_message', 'No data.'))
row_actions = item.get('row_actions', [])
hash_val = core_hash()
hash_val = config_hash()
toolbar_html = ''
toolbar = item.get('toolbar')
@ -1594,7 +1594,7 @@ def render_layout(view_id, content_html, tokens):
navbar_html = _render_navbar(view_id, level, tokens)
footer_html = f'<footer class="footer">{WEB_APP_DISPLAY_NAME}</footer>'
page_hash = core_hash()
page_hash = config_hash()
lan_iface = e(tokens.get('GENERAL_LAN_INTERFACE', ''))
vpn_count = tokens.get('VPN_VLAN_COUNT', '0')
existing_ids = tokens.get('EXISTING_VLAN_IDS_JSON', '[]')