diff --git a/neode-ui/src/types/api.ts b/neode-ui/src/types/api.ts index 714e060a..0d5db132 100644 --- a/neode-ui/src/types/api.ts +++ b/neode-ui/src/types/api.ts @@ -25,6 +25,9 @@ export interface ServerInfo { 'status-info': StatusInfo 'lan-address': string | null 'tor-address': string | null + /** Live probe of the Tor daemon. NOT the same as `tor-address`, which is read + * from the hidden-service hostname file and outlives a dead daemon. */ + 'tor-running': boolean 'node-address'?: string unread: number 'wifi-ssids': string[] diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index bd2f9668..88b40ec8 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -424,10 +424,13 @@ function marketplaceDescription(app: MarketplaceApp) { } // Network card data -const torConnected = computed(() => { - const torAddr = store.data?.['server-info']?.['tor-address'] - return !!torAddr && torAddr.length > 0 -}) +// Tor liveness, NOT "was an onion ever provisioned". This used to return +// `!!server-info['tor-address']`, but that address is read from the +// hidden-service hostname file on disk and outlives the daemon — so this card +// showed "Connected" on three fleet nodes whose Tor had been dead for days +// (2026-08-09), which is precisely why nobody noticed. `tor-running` is a live +// probe of 127.0.0.1:9050 performed server-side. +const torConnected = computed(() => store.data?.['server-info']?.['tor-running'] === true) const vpnConnected = computed(() => homeStatus.vpnStatus.connected === true || (!!packages.value['tailscale'] && packages.value['tailscale'].state === PackageState.Running)) const vpnDotClass = computed(() => { if (vpnConnected.value) return 'bg-orange-400' diff --git a/neode-ui/src/views/Server.vue b/neode-ui/src/views/Server.vue index e85c0144..2e9e6784 100644 --- a/neode-ui/src/views/Server.vue +++ b/neode-ui/src/views/Server.vue @@ -995,8 +995,14 @@ async function checkTorStatus() { checkingTor.value = true; torStatusLabel.value = 'checking' try { await torServicesRes.refresh() + // Report the DAEMON, not whether onion addresses exist. A .onion address is + // a config artifact — the hidden-service hostname file stays on disk when + // Tor is dead — so `some(s => s.onion_address)` reported "running" on three + // fleet nodes whose Tor had been down for days (2026-08-09). The backend + // already returns the truth: tor_running comes from an actual connect to + // 127.0.0.1:9050. if (torServicesRes.error.value) torStatusLabel.value = 'stopped' - else torStatusLabel.value = torServices.value.some(s => s.onion_address) ? 'running' : 'stopped' + else torStatusLabel.value = torDaemonRunning.value ? 'running' : 'stopped' } finally { checkingTor.value = false } }