From def89ffe59275a12cd6d50e3d8b2ea6616b14c75 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())