diff --git a/core/archipelago/src/api/rpc/vpn.rs b/core/archipelago/src/api/rpc/vpn.rs index cc09c28a..d07ce3f0 100644 --- a/core/archipelago/src/api/rpc/vpn.rs +++ b/core/archipelago/src/api/rpc/vpn.rs @@ -46,14 +46,37 @@ impl RpcHandler { let wg_pubkey = tokio::fs::read_to_string("/var/lib/archipelago/wireguard/public.key") .await.ok().map(|s| s.trim().to_string()); - // Don't report NostrVPN ip_address if it's the same as WireGuard (means tunnel not up) - let nvpn_ip = status.ip_address.as_ref().and_then(|ip| { - let clean = ip.split('/').next().unwrap_or(ip); - if wg_ip.as_deref() == Some(clean) { None } else { Some(ip.clone()) } + // Check if nvpn0 tunnel interface actually exists and has an IP + let nvpn0_ip = tokio::process::Command::new("ip") + .args(["-4", "addr", "show", "nvpn0"]) + .output().await + .ok() + .and_then(|o| { + let out = String::from_utf8_lossy(&o.stdout).to_string(); + out.lines() + .find(|l| l.contains("inet ")) + .and_then(|l| l.split_whitespace().nth(1)) + .map(|s| s.split('/').next().unwrap_or(s).to_string()) + }); + + // NostrVPN IP: only report if nvpn0 tunnel is actually up with its own IP, + // and that IP is distinct from the standalone WireGuard IP + let nvpn_ip = nvpn0_ip.as_ref().and_then(|ip| { + if wg_ip.as_deref() == Some(ip.as_str()) { None } else { Some(ip.clone()) } }); + // NostrVPN is connected only if its dedicated tunnel (nvpn0) has a distinct IP + let nvpn_connected = status.provider.as_deref() == Some("nostr-vpn") && nvpn_ip.is_some(); + + // connected = NostrVPN tunnel is up OR another VPN provider is active OR standalone WireGuard is up + let is_connected = if status.provider.as_deref() == Some("nostr-vpn") { + nvpn_connected || wg_ip.is_some() + } else { + status.connected || wg_ip.is_some() + }; + Ok(serde_json::json!({ - "connected": status.connected || wg_ip.is_some(), + "connected": is_connected, "provider": status.provider, "interface": status.interface, "ip_address": nvpn_ip, diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 19dff038..9371378a 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -1076,6 +1076,8 @@ if [ "$WEBUI_CAPTURED" = "0" ]; then echo " ⚠️ Could not capture from live server, building from source..." fi cd "$SCRIPT_DIR/../neode-ui" + echo " Installing frontend dependencies..." + npm ci --prefer-offline 2>&1 | tail -3 if npm run build 2>&1 | tail -5; then if [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then echo " Including web UI from web/dist/neode-ui..." @@ -1153,6 +1155,8 @@ if [ "$UNBUNDLED" = "1" ]; then # Marker file: first-boot-containers.sh checks this to skip app creation touch "$ARCH_DIR/.unbundled" IMAGES_DIR="$ARCH_DIR/container-images" + # Clean stale images from previous builds (e.g. bundled build tars leaking into unbundled) + rm -rf "$IMAGES_DIR" mkdir -p "$IMAGES_DIR" # FileBrowser is a core dependency (powers the Cloud file manager) — always bundle it CORE_IMAGE="${FILEBROWSER_IMAGE}" diff --git a/image-recipe/configs/archipelago-wg.service b/image-recipe/configs/archipelago-wg.service index 00c5a6bc..71cf6541 100644 --- a/image-recipe/configs/archipelago-wg.service +++ b/image-recipe/configs/archipelago-wg.service @@ -1,7 +1,6 @@ [Unit] Description=Archipelago Standalone WireGuard (wg0) -After=network-online.target -Wants=network-online.target +After=network.target ConditionPathExists=/var/lib/archipelago/wireguard/private.key [Service] diff --git a/neode-ui/src/App.vue b/neode-ui/src/App.vue index d84c3351..d781a0a4 100644 --- a/neode-ui/src/App.vue +++ b/neode-ui/src/App.vue @@ -104,7 +104,8 @@ watch(() => appStore.isAuthenticated, (authenticated) => { screensaverStore.resetInactivityTimer() // Don't start relay on kiosk — kiosk gets input via xdotool (system-level), // relay would duplicate every keystroke/click as DOM events - const isKiosk = window.location.pathname.startsWith('/kiosk') + const isKiosk = localStorage.getItem('kiosk') === 'true' + || new URLSearchParams(window.location.search).has('kiosk') if (!isKiosk) { startRemoteRelay() } diff --git a/neode-ui/src/components/PWAInstallPrompt.vue b/neode-ui/src/components/PWAInstallPrompt.vue index c744c209..fa71db40 100644 --- a/neode-ui/src/components/PWAInstallPrompt.vue +++ b/neode-ui/src/components/PWAInstallPrompt.vue @@ -44,7 +44,7 @@ const DISMISS_KEY = 'archipelago_pwa_install_dismissed' onMounted(() => { // Don't show in kiosk mode, if already dismissed, or if already installed - if (window.location.pathname.startsWith('/kiosk')) return + if (localStorage.getItem('kiosk') === 'true') return if (sessionStorage.getItem(DISMISS_KEY) === '1') return if (window.matchMedia('(display-mode: standalone)').matches) return if ((window.navigator as Navigator & { standalone?: boolean }).standalone) return diff --git a/neode-ui/src/router/index.ts b/neode-ui/src/router/index.ts index 2733425a..42a211d4 100644 --- a/neode-ui/src/router/index.ts +++ b/neode-ui/src/router/index.ts @@ -87,6 +87,11 @@ const router = createRouter({ path: '/kiosk', name: 'kiosk', redirect: '/', + beforeEnter: () => { + // Persist kiosk mode before redirect so App.vue can skip the remote relay + // (relay duplicates xdotool input on the kiosk display) + localStorage.setItem('kiosk', 'true') + }, }, { path: '/dashboard', diff --git a/neode-ui/src/views/ContainerApps.vue b/neode-ui/src/views/ContainerApps.vue index 4337f2a5..9f80d175 100644 --- a/neode-ui/src/views/ContainerApps.vue +++ b/neode-ui/src/views/ContainerApps.vue @@ -210,7 +210,10 @@ const store = useContainerStore() const appLauncherStore = useAppLauncherStore() // Use enriched bundled apps with runtime data (like lan_address) -const bundledApps = computed(() => store.enrichedBundledApps) +// Only show apps that actually have a container (hides pre-defined apps on unbundled installs) +const bundledApps = computed(() => store.enrichedBundledApps.filter( + app => store.getAppState(app.id) !== 'not-installed' +)) // Get current host for launch URLs const currentHost = computed(() => window.location.hostname) diff --git a/scripts/fix-indeedhub-containers.sh b/scripts/fix-indeedhub-containers.sh index 31c978f9..c3416cec 100755 --- a/scripts/fix-indeedhub-containers.sh +++ b/scripts/fix-indeedhub-containers.sh @@ -173,7 +173,7 @@ echo "Creating indeedhub frontend..." podman run -d --name indeedhub \ --restart unless-stopped \ --network "$NETWORK" \ - -p 7777:7777 \ + -p 7778:7777 \ --label "com.archipelago.app=indeedhub" \ --label "com.archipelago.title=IndeedHub" \ --label "com.archipelago.version=0.1.0" \ @@ -200,6 +200,11 @@ if podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then rm -f /tmp/ih-nginx.conf fi + # Fix X-Forwarded-Prefix for NIP-98 URL reconstruction in iframe context + # The outer Archipelago nginx sets X-Forwarded-Prefix to /app/indeedhub; + # the inner nginx must pass it through (appending /api) instead of hardcoding /api + podman exec indeedhub sed -i 's|proxy_set_header X-Forwarded-Prefix /api;|proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix/api;|' /etc/nginx/conf.d/default.conf 2>/dev/null || true + # Replace DNS-based upstream resolution with hardcoded container IPs # (podman DNS resolver 127.0.0.11 is unreliable, causing 502 errors) API_IP=$(podman inspect indeedhub-build_api_1 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)