2026-04-11 20:00:05 +01:00
|
|
|
<template>
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
<Transition name="overlay-fade">
|
|
|
|
|
<div
|
|
|
|
|
v-if="visible"
|
|
|
|
|
class="fixed inset-0 flex items-end sm:items-center justify-center p-4 z-[3000]"
|
|
|
|
|
@click.self="dismiss"
|
|
|
|
|
>
|
|
|
|
|
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" />
|
|
|
|
|
<div
|
2026-07-16 20:07:56 -04:00
|
|
|
class="glass-card p-5 w-full max-w-sm relative z-10 mb-20 sm:mb-0 overflow-hidden"
|
2026-04-11 20:00:05 +01:00
|
|
|
@click.stop
|
|
|
|
|
>
|
2026-06-11 00:24:40 -04:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-07-16 20:07:56 -04:00
|
|
|
class="absolute right-3 top-3 h-8 w-8 rounded-full bg-white/5 border border-white/10 text-white/60 hover:text-white hover:bg-white/10 transition-colors z-10"
|
2026-06-11 00:24:40 -04:00
|
|
|
aria-label="Close companion modal"
|
|
|
|
|
@click="dismiss"
|
|
|
|
|
>
|
|
|
|
|
×
|
|
|
|
|
</button>
|
|
|
|
|
|
2026-07-16 20:07:56 -04:00
|
|
|
<Transition :name="slideName" mode="out-in">
|
|
|
|
|
<!-- Screen 1: get the app -->
|
|
|
|
|
<div v-if="step === 'download'" key="download">
|
|
|
|
|
<div class="flex items-start gap-4 mb-4">
|
|
|
|
|
<div class="w-12 h-12 rounded-xl bg-orange-500/15 border border-orange-500/30 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
<svg class="w-7 h-7 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
|
<rect x="3" y="7" width="18" height="11" rx="3" stroke-width="1.5" />
|
|
|
|
|
<rect x="7.5" y="10" width="2" height="5" rx="0.5" fill="currentColor" />
|
|
|
|
|
<rect x="6" y="11.5" width="5" height="2" rx="0.5" fill="currentColor" />
|
|
|
|
|
<circle cx="16" cy="11" r="1.2" fill="currentColor" />
|
|
|
|
|
<circle cx="14" cy="13.5" r="1.2" fill="currentColor" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
<h3 class="text-lg font-semibold text-white mb-1">Remote Companion</h3>
|
|
|
|
|
<p class="text-sm text-white/60 leading-relaxed">
|
|
|
|
|
Install the Archipelago companion app on your phone, scan the code, and connect to the same node.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="hidden md:flex justify-center mb-4">
|
|
|
|
|
<div class="w-[128px] rounded-2xl border border-white/10 bg-white/[0.03] p-1 overflow-hidden">
|
|
|
|
|
<img
|
|
|
|
|
v-if="qrDataUrl"
|
|
|
|
|
:src="qrDataUrl"
|
|
|
|
|
alt="Companion app download QR code"
|
|
|
|
|
class="block w-full max-w-full h-auto rounded-lg bg-white"
|
|
|
|
|
/>
|
|
|
|
|
<div v-else class="w-full aspect-square rounded-lg bg-white/5"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<a
|
|
|
|
|
:href="companionDownloadUrl"
|
|
|
|
|
class="flex-1 inline-flex items-center justify-center rounded-lg bg-orange-500/20 border border-orange-500/30 px-3 py-2.5 text-sm font-medium text-orange-400 hover:bg-orange-500/30 transition-colors text-center"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
2026-07-16 20:46:08 -04:00
|
|
|
download
|
2026-07-16 20:07:56 -04:00
|
|
|
>
|
|
|
|
|
Download app
|
|
|
|
|
</a>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="flex-1 rounded-lg bg-white/5 border border-white/15 px-3 py-2.5 text-sm font-medium text-white/80 hover:bg-white/10 hover:text-white transition-colors"
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
@click="showPairScreen"
|
2026-07-16 20:07:56 -04:00
|
|
|
>
|
|
|
|
|
I've installed it
|
|
|
|
|
</button>
|
2026-06-11 00:24:40 -04:00
|
|
|
</div>
|
2026-04-11 20:00:05 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-07-16 20:07:56 -04:00
|
|
|
<!-- Screen 2: pair the app with this node -->
|
|
|
|
|
<div v-else key="pair">
|
|
|
|
|
<div class="flex items-start gap-4 mb-4">
|
|
|
|
|
<div class="w-12 h-12 rounded-xl bg-orange-500/15 border border-orange-500/30 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
<svg class="w-7 h-7 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
|
<rect x="4" y="4" width="7" height="7" rx="1.5" stroke-width="1.5" />
|
|
|
|
|
<rect x="13" y="4" width="7" height="7" rx="1.5" stroke-width="1.5" />
|
|
|
|
|
<rect x="4" y="13" width="7" height="7" rx="1.5" stroke-width="1.5" />
|
|
|
|
|
<path d="M13 13h3v3h-3zM17 17h3v3h-3z" fill="currentColor" stroke="none" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
<h3 class="text-lg font-semibold text-white mb-1">Connect your app</h3>
|
|
|
|
|
<p class="text-sm text-white/60 leading-relaxed">
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
In the companion app, choose “Scan Node's QR” and point your phone here — it connects instantly and sets up secure remote access over the mesh.
|
2026-07-16 20:07:56 -04:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex justify-center mb-4">
|
2026-07-17 02:03:11 +01:00
|
|
|
<!-- Bigger than the download QR: this one is scanned by the
|
|
|
|
|
companion app camera, and physical size drives decode. -->
|
|
|
|
|
<div class="w-[192px] rounded-2xl border border-white/10 bg-white/[0.03] p-1 overflow-hidden">
|
2026-07-16 20:07:56 -04:00
|
|
|
<img
|
|
|
|
|
v-if="pairQrDataUrl"
|
|
|
|
|
:src="pairQrDataUrl"
|
|
|
|
|
alt="Companion app pairing QR code"
|
|
|
|
|
class="block w-full max-w-full h-auto rounded-lg bg-white"
|
|
|
|
|
/>
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
<div v-else class="w-full aspect-square rounded-lg bg-white/5 flex items-center justify-center">
|
|
|
|
|
<svg v-if="pairLoading" class="w-6 h-6 animate-spin text-white/40" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg>
|
|
|
|
|
</div>
|
2026-07-16 20:07:56 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Same-device path: a QR on the phone's own screen can't be
|
|
|
|
|
scanned, so offer the deep link directly on small screens. -->
|
|
|
|
|
<a
|
|
|
|
|
v-if="pairingUrl"
|
|
|
|
|
:href="pairingUrl"
|
|
|
|
|
class="md:hidden inline-flex w-full items-center justify-center rounded-lg bg-orange-500/20 border border-orange-500/30 px-4 py-2.5 text-sm font-medium text-orange-400 hover:bg-orange-500/30 transition-colors mb-2"
|
|
|
|
|
>
|
|
|
|
|
Open in companion app
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="w-full py-2.5 rounded-lg bg-white/5 border border-white/15 text-white/80 text-sm font-medium hover:bg-white/10 hover:text-white transition-colors"
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
@click="showDownloadScreen"
|
2026-07-16 20:07:56 -04:00
|
|
|
>
|
|
|
|
|
Back
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
2026-04-11 20:00:05 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Transition>
|
|
|
|
|
</Teleport>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-16 17:42:57 -04:00
|
|
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
2026-06-11 00:24:40 -04:00
|
|
|
import * as QRCode from 'qrcode'
|
2026-07-16 20:07:56 -04:00
|
|
|
import { IS_DEMO, DEMO_PASSWORD } from '@/composables/useDemoIntro'
|
2026-07-16 20:46:08 -04:00
|
|
|
import { companionIntroRequested } from '@/composables/useCompanionIntro'
|
2026-07-16 06:57:42 -04:00
|
|
|
import { useLoginTransitionStore } from '@/stores/loginTransition'
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
import { useServerStore } from '@/stores/server'
|
2026-07-16 20:07:56 -04:00
|
|
|
import { rpcClient } from '@/api/rpc-client'
|
2026-04-11 20:00:05 +01:00
|
|
|
|
|
|
|
|
const STORAGE_KEY = 'neode_companion_intro_seen'
|
2026-06-19 09:50:10 -04:00
|
|
|
// Absolute URL so the QR works when scanned by a phone (a relative path has no
|
|
|
|
|
// host to resolve). Points at the companion APK hosted on the 146 release server
|
|
|
|
|
// (publicly reachable) rather than the local node's /packages copy.
|
2026-07-14 11:38:51 -04:00
|
|
|
// The demo serves the APK from its own public origin instead, so the QR never
|
|
|
|
|
// exposes the release-server address.
|
|
|
|
|
const DEFAULT_DOWNLOAD_URL = IS_DEMO
|
|
|
|
|
? `${window.location.origin}/packages/archipelago-companion.apk`
|
|
|
|
|
: 'http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/neode-ui/public/packages/archipelago-companion.apk'
|
2026-04-11 20:00:05 +01:00
|
|
|
|
2026-07-16 20:07:56 -04:00
|
|
|
// Deep-link scheme the companion app registers; carries the server entry the
|
|
|
|
|
// app should create (see docs/companion-pairing-qr.md for the contract).
|
|
|
|
|
const PAIR_SCHEME = 'archipelago://pair'
|
|
|
|
|
const DEMO_SERVER_URL = 'https://demo.archipelago-foundation.org'
|
|
|
|
|
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
// Fallback display name when the node still has the factory server name.
|
|
|
|
|
const DEFAULT_PAIR_NAME = 'My Archipelago'
|
|
|
|
|
|
|
|
|
|
// Device-token entry name; re-minting replaces the previous token so
|
|
|
|
|
// re-showing this screen never piles up credentials server-side.
|
|
|
|
|
const DEVICE_TOKEN_NAME = 'companion'
|
2026-07-17 02:32:02 +01:00
|
|
|
|
2026-04-11 20:00:05 +01:00
|
|
|
const visible = ref(false)
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
const step = ref<'download' | 'pair'>('download')
|
2026-07-16 20:07:56 -04:00
|
|
|
const slideName = ref('slide-forward')
|
2026-06-11 00:24:40 -04:00
|
|
|
const qrDataUrl = ref('')
|
2026-07-16 20:07:56 -04:00
|
|
|
const pairQrDataUrl = ref('')
|
|
|
|
|
const pairingUrl = ref('')
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
const pairLoading = ref(false)
|
2026-06-11 00:24:40 -04:00
|
|
|
const companionDownloadUrl = import.meta.env.VITE_COMPANION_APK_URL || DEFAULT_DOWNLOAD_URL
|
2026-04-11 20:00:05 +01:00
|
|
|
|
2026-07-16 06:57:42 -04:00
|
|
|
const loginTransition = useLoginTransitionStore()
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
const serverStore = useServerStore()
|
2026-07-16 06:57:42 -04:00
|
|
|
|
|
|
|
|
// Base delay before the popup may appear, and extra breathing room after the
|
|
|
|
|
// dashboard entrance cinematic ends so the popup never cuts into the reveal.
|
|
|
|
|
const BASE_DELAY_MS = 5000
|
|
|
|
|
const POST_INTRO_GRACE_MS = 2000
|
|
|
|
|
|
2026-07-16 17:42:57 -04:00
|
|
|
let calmTicker: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
|
2026-07-20 06:07:35 +01:00
|
|
|
// 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
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
// it. Server management for connected companions lives in the NESMenu instead.
|
2026-07-20 06:07:35 +01:00
|
|
|
const IN_COMPANION_APP = typeof (window as { ArchipelagoNative?: unknown }).ArchipelagoNative !== 'undefined'
|
|
|
|
|
|
2026-04-11 20:00:05 +01:00
|
|
|
onMounted(() => {
|
2026-07-20 06:07:35 +01:00
|
|
|
if (IN_COMPANION_APP) return
|
2026-04-11 20:00:05 +01:00
|
|
|
try {
|
|
|
|
|
if (localStorage.getItem(STORAGE_KEY) !== '1') {
|
2026-07-16 06:57:42 -04:00
|
|
|
setTimeout(maybeShow, BASE_DELAY_MS)
|
2026-04-11 20:00:05 +01:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// localStorage unavailable
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-16 17:42:57 -04:00
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (calmTicker) clearInterval(calmTicker)
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-16 06:57:42 -04:00
|
|
|
function maybeShow() {
|
2026-07-16 17:42:57 -04:00
|
|
|
// Show only after the scene has been CONTINUOUSLY calm (no reveal
|
|
|
|
|
// cinematic) for the full grace window. The previous point-in-time check
|
|
|
|
|
// raced a slow-starting reveal — on a cold cache the entrance video can
|
|
|
|
|
// begin buffering after the 5s base delay, so the flag was still false
|
|
|
|
|
// when sampled and the popup cut straight into the cinematic.
|
|
|
|
|
let calmSince = loginTransition.introCinematicPlaying ? null : Date.now()
|
|
|
|
|
calmTicker = setInterval(() => {
|
|
|
|
|
if (loginTransition.introCinematicPlaying) {
|
|
|
|
|
calmSince = null
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (calmSince === null) calmSince = Date.now()
|
|
|
|
|
if (Date.now() - calmSince >= POST_INTRO_GRACE_MS) {
|
|
|
|
|
if (calmTicker) clearInterval(calmTicker)
|
|
|
|
|
calmTicker = null
|
|
|
|
|
visible.value = true
|
|
|
|
|
}
|
|
|
|
|
}, 250)
|
2026-07-16 06:57:42 -04:00
|
|
|
}
|
|
|
|
|
|
2026-07-16 20:46:08 -04:00
|
|
|
// Manual open (App Store banner etc.) — ignores the once-per-browser gate.
|
|
|
|
|
watch(companionIntroRequested, (requested) => {
|
|
|
|
|
if (!requested) return
|
|
|
|
|
companionIntroRequested.value = false
|
|
|
|
|
if (calmTicker) {
|
|
|
|
|
clearInterval(calmTicker)
|
|
|
|
|
calmTicker = null
|
|
|
|
|
}
|
|
|
|
|
step.value = 'download'
|
|
|
|
|
visible.value = true
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-11 00:24:40 -04:00
|
|
|
watch(visible, async (isVisible) => {
|
|
|
|
|
if (!isVisible) return
|
2026-07-17 02:03:11 +01:00
|
|
|
// Generate large and let CSS scale down — at 112px source a ~45-module QR
|
|
|
|
|
// is 2.5px/module, which camera decoders (the companion app included)
|
|
|
|
|
// routinely fail on. 512px keeps every module crisp.
|
2026-06-11 00:24:40 -04:00
|
|
|
qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, {
|
2026-07-17 02:03:11 +01:00
|
|
|
width: 512,
|
|
|
|
|
margin: 2,
|
2026-06-11 00:24:40 -04:00
|
|
|
errorCorrectionLevel: 'M',
|
|
|
|
|
color: {
|
|
|
|
|
dark: '#111111',
|
|
|
|
|
light: '#ffffff',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}, { immediate: true, flush: 'post' })
|
|
|
|
|
|
2026-07-22 19:08:17 -04:00
|
|
|
/** Tailscale/CGNAT range 100.64.0.0/10 — reachable only inside the tailnet. */
|
|
|
|
|
function isTailnetIp(host: string): boolean {
|
|
|
|
|
const m = host.match(/^100\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/)
|
|
|
|
|
return !!m && Number(m[1]) >= 64 && Number(m[1]) <= 127
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 20:07:56 -04:00
|
|
|
/**
|
|
|
|
|
* The server URL the companion app should connect to. The demo advertises its
|
2026-07-22 19:08:17 -04:00
|
|
|
* public https origin; a real node advertises the browser's own origin ONLY
|
|
|
|
|
* when a phone could plausibly reach it too. Two origins that a phone on the
|
|
|
|
|
* LAN can never dial get substituted with the node's real LAN address:
|
|
|
|
|
* - localhost/127.0.0.1 (the kiosk browses itself)
|
|
|
|
|
* - a tailnet 100.x address (operator browsing over Tailscale — a scanned
|
|
|
|
|
* QR carried one of these on 2026-07-22 and the companion sat there
|
|
|
|
|
* dialing an IP the phone had no route to)
|
2026-07-16 20:07:56 -04:00
|
|
|
*/
|
|
|
|
|
async function resolveServerUrl(): Promise<string> {
|
|
|
|
|
if (IS_DEMO) return DEMO_SERVER_URL
|
|
|
|
|
const { hostname, origin } = window.location
|
2026-07-22 19:08:17 -04:00
|
|
|
const phoneUnreachable =
|
|
|
|
|
hostname === 'localhost' || hostname === '127.0.0.1' || isTailnetIp(hostname)
|
|
|
|
|
if (!phoneUnreachable) return origin
|
2026-07-16 20:07:56 -04:00
|
|
|
try {
|
2026-07-22 19:08:17 -04:00
|
|
|
const res = await rpcClient.call<{ mdns_hostname?: string; lan_ip?: string | null }>({
|
|
|
|
|
method: 'system.get-hostname',
|
|
|
|
|
})
|
|
|
|
|
// Prefer the LAN IP (phones resolve it without mDNS support — Android
|
|
|
|
|
// notoriously lacks .local); fall back to the mDNS name.
|
|
|
|
|
if (res?.lan_ip) return `http://${res.lan_ip}`
|
2026-07-16 20:07:56 -04:00
|
|
|
if (res?.mdns_hostname) return `http://${res.mdns_hostname}`
|
|
|
|
|
} catch {
|
2026-07-22 19:08:17 -04:00
|
|
|
// RPC unavailable — fall through to the (unreachable) origin; the app
|
|
|
|
|
// still lets the user edit the address by hand.
|
2026-07-16 20:07:56 -04:00
|
|
|
}
|
|
|
|
|
return origin
|
|
|
|
|
}
|
|
|
|
|
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
/**
|
|
|
|
|
* Pairing payload the companion app consumes (docs/companion-pairing-qr.md):
|
|
|
|
|
* v contract version (1)
|
|
|
|
|
* url where the app connects right now (LAN origin / mDNS)
|
|
|
|
|
* name what the server is called in the app
|
|
|
|
|
* tok device token — the app logs in with it instantly (real nodes)
|
|
|
|
|
* pw shared demo password (demo only)
|
|
|
|
|
* fnpub node's FIPS mesh identity — the phone's embedded FIPS dials it
|
|
|
|
|
* fip node's fips0 ULA — where the UI stays reachable once meshed
|
|
|
|
|
* fhost host the phone dials for the mesh (same host `url` resolved to)
|
|
|
|
|
* fudp / ftcp mesh transport ports on fhost
|
|
|
|
|
*
|
|
|
|
|
* Token and mesh params are best-effort: without them the QR still pairs the
|
|
|
|
|
* old way (manual password, LAN only), so a mid-onboarding backend hiccup
|
|
|
|
|
* never produces a dead QR.
|
|
|
|
|
*/
|
2026-07-16 20:07:56 -04:00
|
|
|
async function buildPairingUrl(): Promise<string> {
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
const serverUrl = await resolveServerUrl()
|
|
|
|
|
const params = new URLSearchParams({ v: '1', url: serverUrl })
|
|
|
|
|
|
|
|
|
|
const name = serverStore.serverName
|
|
|
|
|
params.set('name', !name || name === 'Archipelago' ? DEFAULT_PAIR_NAME : name)
|
|
|
|
|
|
|
|
|
|
if (IS_DEMO) {
|
|
|
|
|
// Only the shared demo password ever rides in the QR; real node passwords
|
|
|
|
|
// are never available to the frontend.
|
|
|
|
|
params.set('pw', DEMO_PASSWORD)
|
|
|
|
|
return `${PAIR_SCHEME}?${params.toString()}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ token?: string }>({
|
|
|
|
|
method: 'auth.createDeviceToken',
|
|
|
|
|
params: { name: DEVICE_TOKEN_NAME },
|
|
|
|
|
})
|
|
|
|
|
if (res?.token) params.set('tok', res.token)
|
|
|
|
|
} catch {
|
|
|
|
|
// Older backend or transient failure — app falls back to password entry.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const info = await rpcClient.call<{
|
|
|
|
|
npub?: string
|
|
|
|
|
ula?: string | null
|
|
|
|
|
udp_port?: number
|
|
|
|
|
tcp_port?: number
|
2026-07-22 21:44:39 +01:00
|
|
|
anchors?: { npub: string; addr: string; transport: string }[]
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
}>({ method: 'fips.pair-info' })
|
|
|
|
|
if (info?.npub) {
|
|
|
|
|
params.set('fnpub', info.npub)
|
|
|
|
|
if (info.ula) params.set('fip', info.ula)
|
|
|
|
|
try {
|
|
|
|
|
params.set('fhost', new URL(serverUrl).hostname)
|
|
|
|
|
} catch {
|
|
|
|
|
params.set('fhost', window.location.hostname)
|
|
|
|
|
}
|
|
|
|
|
if (info.udp_port) params.set('fudp', String(info.udp_port))
|
|
|
|
|
if (info.tcp_port) params.set('ftcp', String(info.tcp_port))
|
2026-07-22 21:44:39 +01:00
|
|
|
// Rendezvous anchors (compact: npub@addr/transport, comma-joined).
|
2026-07-23 15:30:40 -04:00
|
|
|
// FIRST entry is the paired node ITSELF: the phone peers with it by
|
|
|
|
|
// npub (the fhost addr is only a dial hint), so LAN contact is direct
|
|
|
|
|
// p2p over FIPS and survives DHCP renumbering; the node's public
|
|
|
|
|
// anchors follow for reaching it away from home. Cap keeps the QR at a
|
|
|
|
|
// camera-friendly density; the node lists its most reachable anchors
|
|
|
|
|
// first.
|
|
|
|
|
const selfAnchor = info.tcp_port
|
|
|
|
|
? [{ npub: info.npub, addr: `${params.get('fhost')}:${info.tcp_port}`, transport: 'tcp' }]
|
|
|
|
|
: []
|
2026-07-23 16:46:51 -04:00
|
|
|
// 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)
|
2026-07-22 21:44:39 +01:00
|
|
|
if (anchors.length) {
|
|
|
|
|
params.set(
|
|
|
|
|
'fanchors',
|
|
|
|
|
anchors.map((a) => `${a.npub}@${a.addr}/${a.transport}`).join(','),
|
|
|
|
|
)
|
|
|
|
|
}
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// FIPS not provisioned yet — LAN pairing still works; the app can mesh
|
|
|
|
|
// later from a re-scan.
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 20:07:56 -04:00
|
|
|
return `${PAIR_SCHEME}?${params.toString()}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function showPairScreen() {
|
|
|
|
|
slideName.value = 'slide-forward'
|
|
|
|
|
step.value = 'pair'
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
if (pairQrDataUrl.value || pairLoading.value) return
|
|
|
|
|
pairLoading.value = true
|
|
|
|
|
try {
|
2026-07-16 20:07:56 -04:00
|
|
|
pairingUrl.value = await buildPairingUrl()
|
2026-07-17 02:03:11 +01:00
|
|
|
// 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).
|
2026-07-23 16:46:51 -04:00
|
|
|
// 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.
|
2026-07-16 20:07:56 -04:00
|
|
|
pairQrDataUrl.value = await QRCode.toDataURL(pairingUrl.value, {
|
2026-07-23 16:46:51 -04:00
|
|
|
width: 768,
|
2026-07-17 02:03:11 +01:00
|
|
|
margin: 3,
|
2026-07-23 16:46:51 -04:00
|
|
|
errorCorrectionLevel: 'L',
|
2026-07-16 20:07:56 -04:00
|
|
|
color: {
|
|
|
|
|
dark: '#111111',
|
|
|
|
|
light: '#ffffff',
|
|
|
|
|
},
|
|
|
|
|
})
|
feat: instant companion pairing — device tokens, named QR, FIPS pair-info
- auth.createDeviceToken / listDeviceTokens / revokeDeviceToken RPCs; only
SHA-256 hashes persist in data_dir/device-tokens.json
- auth.login accepts {token} (same rate limiter, skips TOTP like remember-me)
- pairing QR now carries name (server name, fallback "My Archipelago"),
tok (instant login), and FIPS mesh params from new fips.pair-info RPC
(npub, fips0 ULA, transport ports)
- companion onboarding drops the WireGuard install/tunnel screens — remote
access moves to the FIPS mesh embedded in the companion app
- fix: fips daemon UDP bind now 2121, matching the published container port
and fleet rosters (was upstream's 8668 — inbound UDP was dead on bridged
installs, mesh silently rode TCP 8443)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:16:27 +01:00
|
|
|
} finally {
|
|
|
|
|
pairLoading.value = false
|
2026-07-16 20:07:56 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showDownloadScreen() {
|
|
|
|
|
slideName.value = 'slide-back'
|
|
|
|
|
step.value = 'download'
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 20:00:05 +01:00
|
|
|
function dismiss() {
|
|
|
|
|
visible.value = false
|
2026-07-16 20:07:56 -04:00
|
|
|
step.value = 'download'
|
2026-04-11 20:00:05 +01:00
|
|
|
try {
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, '1')
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.overlay-fade-enter-active { transition: opacity 0.3s ease; }
|
|
|
|
|
.overlay-fade-leave-active { transition: opacity 0.2s ease; }
|
|
|
|
|
.overlay-fade-enter-from,
|
|
|
|
|
.overlay-fade-leave-to { opacity: 0; }
|
|
|
|
|
.overlay-fade-enter-active .glass-card { transition: transform 0.3s ease; }
|
|
|
|
|
.overlay-fade-enter-from .glass-card { transform: translateY(20px); }
|
2026-07-16 20:07:56 -04:00
|
|
|
|
|
|
|
|
/* Horizontal slide between the download and pairing screens */
|
|
|
|
|
.slide-forward-enter-active,
|
|
|
|
|
.slide-forward-leave-active,
|
|
|
|
|
.slide-back-enter-active,
|
|
|
|
|
.slide-back-leave-active { transition: transform 0.25s ease, opacity 0.25s ease; }
|
|
|
|
|
.slide-forward-enter-from { transform: translateX(40px); opacity: 0; }
|
|
|
|
|
.slide-forward-leave-to { transform: translateX(-40px); opacity: 0; }
|
|
|
|
|
.slide-back-enter-from { transform: translateX(-40px); opacity: 0; }
|
|
|
|
|
.slide-back-leave-to { transform: translateX(40px); opacity: 0; }
|
2026-04-11 20:00:05 +01:00
|
|
|
</style>
|