diff --git a/dashboard/README.md b/dashboard/README.md index c11d23d..3822d22 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -41,6 +41,10 @@ address, not the address to put in a browser. Confirm the in-app status reads **Validated sample aggregates**, then stop the server with `Ctrl-C` after the demo. +If the host is on a Tailscale tailnet, the server also prints a **Tailscale +(tunnel)** URL (a `100.x.y.z` address). Open that URL on any other device signed +into the same tailnet to reach the dashboard remotely, without sharing a LAN. + All data-contract and checksum validation remains enabled over LAN HTTP. When a remote browser does not expose the SubtleCrypto digest API to the plain-HTTP page, the dashboard uses its dependency-free SHA-256 implementation and still diff --git a/dashboard/server.mjs b/dashboard/server.mjs index 22ad198..4a0e177 100644 --- a/dashboard/server.mjs +++ b/dashboard/server.mjs @@ -52,12 +52,13 @@ function safeFile(requestUrl) { } } -// Tailscale and similar VPN tunnels show up as extra "LAN" interfaces but are not -// useful for an in-room demo, so keep them out of the printed URL list. -function isTunnelInterface(interfaceName, address) { - if (/^(utun|tailscale|tun|ppp|ipsec)/i.test(interfaceName)) return true; +// Tailscale advertises the host over its tailnet on a 100.64.0.0/10 address, so +// label that URL clearly for remote (off-LAN) demos over the tunnel. +function interfaceLabel(interfaceName, address) { const [first, second] = address.split(".").map(Number); - return first === 100 && second >= 64 && second <= 127; // 100.64.0.0/10 (CGNAT/tailnet) + const isTailnet = + /tailscale/i.test(interfaceName) || (first === 100 && second >= 64 && second <= 127); + return isTailnet ? "Tailscale (tunnel)" : `Other devices (${interfaceName})`; } function displayUrls(host, port) { @@ -72,10 +73,9 @@ function displayUrls(host, port) { for (const [interfaceName, addresses] of Object.entries(networkInterfaces())) { for (const address of addresses || []) { if (address.family !== "IPv4" || address.internal || seen.has(address.address)) continue; - if (isTunnelInterface(interfaceName, address.address)) continue; seen.add(address.address); entries.push({ - label: `Other devices (${interfaceName})`, + label: interfaceLabel(interfaceName, address.address), url: `http://${address.address}:${port}`, }); }