diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue
index f4ac9019..5461fe39 100644
--- a/neode-ui/src/components/CompanionIntroOverlay.vue
+++ b/neode-ui/src/components/CompanionIntroOverlay.vue
@@ -161,6 +161,14 @@
{{ wgError }}
+
@@ -318,7 +326,15 @@ const POST_INTRO_GRACE_MS = 2000
let calmTicker: ReturnType | null = null
+// Running inside the companion app's own WebView (it injects this JS bridge).
+// The "get the companion app" pitch is nonsense there — the user is already in
+// it — and the WG steps mid-pairing race the phone's changing network (the
+// "Failed to fetch" dead-end QR). Server/tunnel management for connected
+// companions lives in the NESMenu instead.
+const IN_COMPANION_APP = typeof (window as { ArchipelagoNative?: unknown }).ArchipelagoNative !== 'undefined'
+
onMounted(() => {
+ if (IN_COMPANION_APP) return
try {
if (localStorage.getItem(STORAGE_KEY) !== '1') {
setTimeout(maybeShow, BASE_DELAY_MS)
@@ -481,12 +497,28 @@ async function loadWgPeer() {
try {
const listed = await rpcClient
.call<{ peers: { name: string }[] }>({ method: 'vpn.list-peers' })
- .catch(() => ({ peers: [] as { name: string }[] }))
- const exists = (listed.peers || []).some((p) => p.name === WG_PEER_NAME)
- const res = await rpcClient.call<{ config: string; peer_ip: string }>({
- method: exists ? 'vpn.peer-config' : 'vpn.create-peer',
- params: { name: WG_PEER_NAME },
- })
+ .catch(() => null)
+ // list-peers unreachable → don't guess "doesn't exist": create-peer on an
+ // existing name would fail. Try create first, fall back to peer-config.
+ const exists = listed === null
+ ? null
+ : (listed.peers || []).some((p) => p.name === WG_PEER_NAME)
+ let res: { config: string; peer_ip: string }
+ if (exists === true) {
+ res = await rpcClient.call({ method: 'vpn.peer-config', params: { name: WG_PEER_NAME } })
+ } else {
+ try {
+ res = await rpcClient.call({ method: 'vpn.create-peer', params: { name: WG_PEER_NAME } })
+ } catch (e) {
+ // Peer already provisioned on a previous visit (list failed or raced).
+ const msg = e instanceof Error ? e.message : ''
+ if (/exist|duplicate/i.test(msg)) {
+ res = await rpcClient.call({ method: 'vpn.peer-config', params: { name: WG_PEER_NAME } })
+ } else {
+ throw e
+ }
+ }
+ }
wgConfig.value = res.config
wgQrDataUrl.value = await QRCode.toDataURL(res.config, {
width: 512,
@@ -498,12 +530,24 @@ async function loadWgPeer() {
},
})
} catch (e) {
- wgError.value = e instanceof Error ? e.message : 'Failed to generate the tunnel config'
+ // fetch()'s raw "Failed to fetch" means the node itself was unreachable —
+ // on this screen that's usually the phone's network mid-change (WiFi drop,
+ // or a half-configured tunnel already routing 10.44.0.0/16). Say so, and
+ // leave a Retry path instead of a dead end.
+ const raw = e instanceof Error ? e.message : ''
+ wgError.value = /failed to fetch|networkerror|load failed|abort/i.test(raw)
+ ? "Can't reach your node. Check the phone is on the same network as the node (and any half-set-up tunnel is switched off), then tap Try again."
+ : raw || 'Failed to generate the tunnel config'
} finally {
wgLoading.value = false
}
}
+function retryWgPeer() {
+ wgError.value = ''
+ void loadWgPeer()
+}
+
// Same-device path: hand the config to the WireGuard app as an importable
// .conf file (a phone can't scan its own screen).
function downloadWgConfig() {