diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue index d324d542..a8f4526f 100644 --- a/neode-ui/src/components/CompanionIntroOverlay.vue +++ b/neode-ui/src/components/CompanionIntroOverlay.vue @@ -357,10 +357,15 @@ async function buildPairingUrl(): Promise { const selfAnchor = info.tcp_port ? [{ npub: info.npub, addr: `${params.get('fhost')}:${info.tcp_port}`, transport: 'tcp' }] : [] - const anchors = [ - ...selfAnchor, - ...(info.anchors || []).filter((a) => a.npub !== info.npub), - ].slice(0, 4) + // HARD CAP at 2 anchors: every extra npub adds ~100 URL-encoded chars + // and pushed the QR past what phone cameras decode off a screen + // (3 anchors ≈ 600 chars ≈ QR v23 — observed unscannable 2026-07-23). + // Self + one public rendezvous is all the phone needs; prefer the + // Archipelago-operated vps2 anchor as the public one. + const others = (info.anchors || []).filter((a) => a.npub !== info.npub) + const publicAnchor = + others.find((a) => a.addr.startsWith('146.59.87.168')) ?? others[0] + const anchors = [...selfAnchor, ...(publicAnchor ? [publicAnchor] : [])].slice(0, 2) if (anchors.length) { params.set( 'fanchors', @@ -385,10 +390,13 @@ async function showPairScreen() { pairingUrl.value = await buildPairingUrl() // Large source + a real quiet zone; this QR is scanned by the companion // app's camera, so give it every advantage (see download QR note above). + // EC level L: the payload is long (token + npub + anchors) and screen + // scans don't suffer the damage EC-M protects against — L drops the + // module count a full version tier, which is what makes it scannable. pairQrDataUrl.value = await QRCode.toDataURL(pairingUrl.value, { - width: 512, + width: 768, margin: 3, - errorCorrectionLevel: 'M', + errorCorrectionLevel: 'L', color: { dark: '#111111', light: '#ffffff',