From 9a3de2f23165a2c07e7fd1476c322a313c461d9e Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sun, 22 Mar 2026 06:35:12 +0200 Subject: [PATCH] add LAN accessibility indicator for services Show whether each service is reachable from the network or bound to localhost only. Dashboard displays green "LAN" or amber "local only" badge next to each healthy service. Unified TCP check function, concurrent health+LAN probes. Co-Authored-By: Claude Opus 4.6 (1M context) --- site/dashboard.html | 21 +++++++++++++++--- src/api.rs | 54 +++++++++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/site/dashboard.html b/site/dashboard.html index ccbb4b5..41f9ce3 100644 --- a/site/dashboard.html +++ b/site/dashboard.html @@ -382,6 +382,15 @@ body { } .health-dot.up { background: var(--emerald); } .health-dot.down { background: var(--rose); } +.lan-badge { + font-family: var(--font-mono); + font-size: 0.58rem; + padding: 1px 5px; + border-radius: 3px; + margin-left: 0.3rem; +} +.lan-badge.shared { background: rgba(82, 122, 82, 0.12); color: var(--emerald); } +.lan-badge.local-only { background: rgba(192, 98, 58, 0.12); color: var(--amber-dim); } /* Override form */ .override-form { @@ -1082,16 +1091,22 @@ function renderServices(entries) { el.innerHTML = '
No services configured
'; return; } - el.innerHTML = entries.map(e => ` + el.innerHTML = entries.map(e => { + const lanBadge = e.healthy + ? (e.lan_accessible + ? 'LAN' + : 'local only') + : ''; + return `
- +
${e.name}.numa${lanBadge}
localhost:${e.target_port} → proxied
${e.name === 'numa' ? '' : ``}
- `).join(''); + `}).join(''); } async function addService(event) { diff --git a/src/api.rs b/src/api.rs index 9c25377..a9bd7ab 100644 --- a/src/api.rs +++ b/src/api.rs @@ -590,6 +590,7 @@ struct ServiceResponse { target_port: u16, url: String, healthy: bool, + lan_accessible: bool, } #[derive(Deserialize)] @@ -609,22 +610,38 @@ async fn list_services(State(ctx): State>) -> Json = entries + let lan_ip = crate::lan::detect_lan_ip(); + + let check_futures: Vec<_> = entries .iter() - .map(|(_, port)| check_health(*port)) + .map(|(_, port)| { + let port = *port; + let localhost = std::net::SocketAddr::from(([127, 0, 0, 1], port)); + let lan_addr = lan_ip.map(|ip| std::net::SocketAddr::new(ip.into(), port)); + async move { + let healthy = check_tcp(localhost).await; + let lan_accessible = match lan_addr { + Some(addr) => check_tcp(addr).await, + None => false, + }; + (healthy, lan_accessible) + } + }) .collect(); - let health_results = futures::future::join_all(health_futures).await; + let check_results = futures::future::join_all(check_futures).await; let results: Vec<_> = entries .into_iter() - .zip(health_results) - .map(|((name, port), healthy)| ServiceResponse { - url: format!("http://{}.{}", name, tld), - name, - target_port: port, - healthy, - }) + .zip(check_results) + .map( + |((name, port), (healthy, lan_accessible))| ServiceResponse { + url: format!("http://{}.{}", name, tld), + name, + target_port: port, + healthy, + lan_accessible, + }, + ) .collect(); Json(results) } @@ -655,7 +672,15 @@ async fn create_service( let tld = &ctx.proxy_tld; ctx.services.lock().unwrap().insert(&name, req.target_port); - let healthy = check_health(req.target_port).await; + let localhost = std::net::SocketAddr::from(([127, 0, 0, 1], req.target_port)); + let lan_addr = + crate::lan::detect_lan_ip().map(|ip| std::net::SocketAddr::new(ip.into(), req.target_port)); + let (healthy, lan_accessible) = tokio::join!(check_tcp(localhost), async { + match lan_addr { + Some(a) => check_tcp(a).await, + None => false, + } + }); Ok(( StatusCode::CREATED, Json(ServiceResponse { @@ -663,6 +688,7 @@ async fn create_service( name, target_port: req.target_port, healthy, + lan_accessible, }), )) } @@ -679,10 +705,10 @@ async fn remove_service(State(ctx): State>, Path(name): Path bool { +async fn check_tcp(addr: std::net::SocketAddr) -> bool { tokio::time::timeout( std::time::Duration::from_millis(100), - tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)), + tokio::net::TcpStream::connect(addr), ) .await .map(|r| r.is_ok())