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,