dashboard: show LAN status in Local Services panel header

- Add lan_enabled to ServerCtx
- Add lan field to /stats API (enabled, peer count)
- Dashboard shows "LAN off" (dim) or "LAN on · N peers" (green)
- Tooltip shows enable command or mDNS service type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-23 11:16:52 +02:00
parent 607470472d
commit 4748a4a4bb
4 changed files with 31 additions and 1 deletions

View File

@@ -580,10 +580,11 @@ body {
<!-- Local services --> <!-- Local services -->
<div class="panel"> <div class="panel">
<div class="panel-header"> <div class="panel-header">
<div> <div style="flex:1;">
<span class="panel-title">Local Services</span> <span class="panel-title">Local Services</span>
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:0.15rem;">Give localhost apps clean .numa URLs. Persistent, with HTTP proxy.</div> <div style="font-size:0.68rem;color:var(--text-dim);margin-top:0.15rem;">Give localhost apps clean .numa URLs. Persistent, with HTTP proxy.</div>
</div> </div>
<span id="lanToggle" style="font-family:var(--font-mono);font-size:0.68rem;cursor:default;user-select:none;" title=""></span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form class="override-form" id="serviceForm" onsubmit="return addService(event)"> <form class="override-form" id="serviceForm" onsubmit="return addService(event)">
@@ -874,6 +875,22 @@ async function refresh() {
document.getElementById('uptime').textContent = formatUptime(stats.uptime_secs); document.getElementById('uptime').textContent = formatUptime(stats.uptime_secs);
document.getElementById('uptimeSub').textContent = formatUptimeSub(stats.uptime_secs); document.getElementById('uptimeSub').textContent = formatUptimeSub(stats.uptime_secs);
document.getElementById('footerUpstream').textContent = stats.upstream || ''; document.getElementById('footerUpstream').textContent = stats.upstream || '';
// LAN status indicator
const lanEl = document.getElementById('lanToggle');
if (stats.lan) {
if (!stats.lan.enabled) {
lanEl.style.color = 'var(--text-dim)';
lanEl.textContent = 'LAN off';
lanEl.title = 'Enable with: numa lan on';
} else {
const pc = stats.lan.peers || 0;
lanEl.style.color = pc > 0 ? 'var(--emerald)' : 'var(--teal)';
lanEl.textContent = `LAN on · ${pc} peer${pc !== 1 ? 's' : ''}`;
lanEl.title = 'mDNS discovery active (_numa._tcp.local)';
}
}
document.getElementById('overrideCount').textContent = stats.overrides.active; document.getElementById('overrideCount').textContent = stats.overrides.active;
document.getElementById('blockedCount').textContent = formatNumber(q.blocked); document.getElementById('blockedCount').textContent = formatNumber(q.blocked);
const bl = stats.blocking; const bl = stats.blocking;

View File

@@ -134,6 +134,13 @@ struct StatsResponse {
cache: CacheStats, cache: CacheStats,
overrides: OverrideStats, overrides: OverrideStats,
blocking: BlockingStatsResponse, blocking: BlockingStatsResponse,
lan: LanStatsResponse,
}
#[derive(Serialize)]
struct LanStatsResponse {
enabled: bool,
peers: usize,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -466,6 +473,10 @@ async fn stats(State(ctx): State<Arc<ServerCtx>>) -> Json<StatsResponse> {
domains_loaded: bl_stats.domains_loaded, domains_loaded: bl_stats.domains_loaded,
allowlist_size: bl_stats.allowlist_size, allowlist_size: bl_stats.allowlist_size,
}, },
lan: LanStatsResponse {
enabled: ctx.lan_enabled,
peers: ctx.lan_peers.lock().unwrap().list().len(),
},
}) })
} }

View File

@@ -39,6 +39,7 @@ pub struct ServerCtx {
pub timeout: Duration, pub timeout: Duration,
pub proxy_tld: String, pub proxy_tld: String,
pub proxy_tld_suffix: String, // pre-computed ".{tld}" to avoid per-query allocation pub proxy_tld_suffix: String, // pre-computed ".{tld}" to avoid per-query allocation
pub lan_enabled: bool,
} }
pub async fn handle_query( pub async fn handle_query(

View File

@@ -159,6 +159,7 @@ async fn main() -> numa::Result<()> {
format!(".{}", config.proxy.tld) format!(".{}", config.proxy.tld)
}, },
proxy_tld: config.proxy.tld.clone(), proxy_tld: config.proxy.tld.clone(),
lan_enabled: config.lan.enabled,
}); });
let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum(); let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum();