diff --git a/docker/routlin-dash/app/factory.py b/docker/routlin-dash/app/factory.py index c96b2a3..cfb905c 100644 --- a/docker/routlin-dash/app/factory.py +++ b/docker/routlin-dash/app/factory.py @@ -1,4 +1,4 @@ -# factory.py — JSON content-type renderer +# factory.py: JSON content-type renderer # Converts content.json item trees into HTML strings. # Pure type processing: no data loading, no routing, no layout. from flask import session @@ -46,7 +46,7 @@ def _prefix_to_dotted(n): def apply_tokens(text, tokens): - """Substitute %TOKEN% placeholders. Values are NOT auto-escaped — callers + """Substitute %TOKEN% placeholders. Values are NOT auto-escaped. Callers that use results in HTML attribute or text context must call e() themselves.""" return re.sub(r'%([A-Z_]+)%', lambda m: str(tokens.get(m.group(1), m.group(0))), text) diff --git a/docker/routlin-dash/app/pages/dhcpleases/content.json b/docker/routlin-dash/app/pages/dhcpleases/content.json index fe5525b..4cf7e03 100644 --- a/docker/routlin-dash/app/pages/dhcpleases/content.json +++ b/docker/routlin-dash/app/pages/dhcpleases/content.json @@ -32,7 +32,8 @@ "columns": [ { "label": "Hostname", - "field": "hostname" + "field": "hostname", + "render": "raw_html" }, { "label": "IP Address", diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index b72e904..166171b 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -177,11 +177,17 @@ def _dnsmasq_start_time(vlan_name): def live_dhcp_leases(): rows = [] now = int(datetime.now(tz=timezone.utc).timestamp()) - vlans = load_config().get('vlans', []) + cfg = load_config() + vlans = cfg.get('vlans', []) vlan_lease_secs = { - v['name']: _parse_lease_secs(v.get('dhcp', {}).get('lease_time', '')) + v['name']: _parse_lease_secs(v.get('dhcp_information', {}).get('lease_time', '')) for v in vlans if v.get('name') } + mac_to_res = { + r['mac'].lower(): r['hostname'] + for r in cfg.get('dhcp_reservations', []) + if r.get('mac') and r.get('hostname') + } for leases_file in glob.glob('/var/lib/misc/dnsmasq-routlin-*.leases'): stem = os.path.basename(leases_file) vlan_name = stem[len('dnsmasq-routlin-'):-len('.leases')] @@ -200,8 +206,19 @@ def live_dhcp_leases(): obtained = relative_time(obtained_ts) if obtained_ts else '-' recent = (obtained_ts is not None and restart_time is not None and obtained_ts >= restart_time) + mac_norm = parts[1].lower() + device_h = parts[3] if parts[3] != '*' else None + res_h = mac_to_res.get(mac_norm) + if res_h and device_h and device_h.lower() != res_h.lower(): + hostname_html = f'{e(res_h)}
({e(device_h)})' + elif res_h: + hostname_html = f'{e(res_h)}' + elif device_h: + hostname_html = e(device_h) + else: + hostname_html = '-' rows.append({ - 'hostname': parts[3] if parts[3] != '*' else '-', + 'hostname': hostname_html, 'ip_address': parts[2], 'mac_address': parts[1], 'vlan_name': vlan_name,