diff --git a/Plugin/unraid/include/fallback.php b/Plugin/unraid/include/fallback.php index c45671c..4d23936 100644 --- a/Plugin/unraid/include/fallback.php +++ b/Plugin/unraid/include/fallback.php @@ -167,8 +167,10 @@ function vv_fb_all(): array { $nodes = []; foreach ($hosts as $slot => $hostname) { $isMe = ($slot === $currentHost || $currentHost === 'unknown'); - $tsLabel = strtolower($hostname); - $ts = $tsPeers[$tsLabel] ?? ['online' => null, 'active' => false, 'ip' => null]; + // Exact-key only, until now — see vv_pt_peer_lookup(). This mesh's conf name and tailnet + // name differ by one character, so HOST2 missed every lookup and the page rendered a + // partner that was up the whole time as state=UNREACHABLE. + $ts = vv_pt_peer_lookup($tsPeers, $hostname); $ip = $ts['ip'] ?? null; // State diff --git a/Plugin/unraid/include/partnership.php b/Plugin/unraid/include/partnership.php index c6ee6a1..5533cff 100644 --- a/Plugin/unraid/include/partnership.php +++ b/Plugin/unraid/include/partnership.php @@ -187,6 +187,29 @@ function vv_pt_ts_peers(): array { return $peers; } +// Resolve one host's peer record out of vv_pt_ts_peers(). +// +// The tailnet name and the OS hostname are not the same string, and nothing keeps them in step: +// this mesh has master.conf saying "unRAID-Jayred36" while the tailnet device is +// "unraid-jayred365". An exact-key lookup finds nothing, and every consumer that did one showed +// a live partner as dark — the Partnership cards until that was fixed inline, and the Fallback +// page for as long as it has existed, where the miss became state=UNREACHABLE. +// +// Exact key first, then a single unambiguous prefix match in either direction. One candidate or +// none — server1 must never resolve to server10 because it happens to share a prefix, and this +// is never similarity scoring. Same rule as vv_resolve_tailscale_ip(), which applies it to +// `tailscale status` text rather than to the parsed peer array. +function vv_pt_peer_lookup(array $tsPeers, string $hostname): array { + $label = strtolower($hostname); + if (isset($tsPeers[$label])) return $tsPeers[$label]; + + $cand = []; + foreach ($tsPeers as $peerName => $peer) { + if (str_starts_with($peerName, $label) || str_starts_with($label, $peerName)) $cand[] = $peer; + } + return count($cand) === 1 ? $cand[0] : ['online' => null, 'active' => false, 'ip' => null]; +} + // ── SSH helper — run a single command on a remote host ──────────────────────── // Multiplexed, because this page makes several of these per render — a state read per partner, @@ -457,18 +480,7 @@ function vv_pt_nodes(): array { // // Same rule as vv_resolve_tailscale_ip(), and the same refusal: one candidate or none. // server1 must never resolve to server10 because it happens to share a prefix. - $tsLabel = strtolower($hostname); - $ts = $tsPeers[$tsLabel] ?? null; - if ($ts === null) { - $cand = []; - foreach ($tsPeers as $peerName => $peer) { - if (str_starts_with($peerName, $tsLabel) || str_starts_with($tsLabel, $peerName)) { - $cand[] = $peer; - } - } - if (count($cand) === 1) $ts = $cand[0]; - } - if ($ts === null) $ts = ['online' => null, 'active' => false, 'ip' => null]; + $ts = vv_pt_peer_lookup($tsPeers, $hostname); // Fallback state $fbState = 'UNKNOWN';