diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index ae5eb198..a3c3aaa3 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -39,7 +39,7 @@ "title": "LND", "version": "0.18.4", "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.", - "icon": "/assets/img/app-icons/lnd.svg", + "icon": "/assets/img/app-icons/lnd.png", "author": "Lightning Labs", "category": "money", "tier": "core", diff --git a/core/archipelago/src/api/rpc/mesh/status.rs b/core/archipelago/src/api/rpc/mesh/status.rs index 7118c4a9..71fba9b9 100644 --- a/core/archipelago/src/api/rpc/mesh/status.rs +++ b/core/archipelago/src/api/rpc/mesh/status.rs @@ -37,6 +37,21 @@ impl RpcHandler { "receive_block_headers".into(), config.receive_block_headers.into(), ); + // Raw serial-device presence, in BOTH branches. MeshStatus has no + // such field, so while the service was running the UI couldn't + // tell "no radio plugged in" from "radio present but the session + // can't open it yet" — both looked like device_connected=false. + if !obj.contains_key("detected_devices") { + let devices = mesh::detect_devices().await; + obj.insert("device_present".into(), (!devices.is_empty()).into()); + obj.insert("detected_devices".into(), devices.into()); + } else { + let present = obj + .get("detected_devices") + .and_then(|v| v.as_array()) + .is_some_and(|a| !a.is_empty()); + obj.insert("device_present".into(), present.into()); + } } Ok(value) } diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index b1484e20..b0bc972f 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -1180,6 +1180,7 @@ impl RpcHandler { // the fresh pull instead of showing stale bytes from a prior // partial attempt. self.set_install_progress(package_id, 0, 0).await; + let started = std::time::Instant::now(); match self .pull_one_url_with_progress(url, *tls_verify, package_id, &user_tmp) .await? @@ -1190,6 +1191,24 @@ impl RpcHandler { break; } false => { + // A fast failure (DNS miss / TCP reset while a fresh + // node's network is still warming up) is worth one quick + // retry — observed on first-boot installs where the + // second click succeeded. A slow failure already spent + // the 300s budget; retrying it just doubles the wait. + if started.elapsed() < std::time::Duration::from_secs(30) { + tracing::info!("Fast pull failure on {}, retrying once in 5s", url); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + self.set_install_progress(package_id, 0, 0).await; + if self + .pull_one_url_with_progress(url, *tls_verify, package_id, &user_tmp) + .await? + { + tracing::info!("Pulled {} from {} (retry)", docker_image, url); + pulled_url = Some(url.clone()); + break; + } + } tracing::debug!( "Pull attempt {}/{} failed for {}, trying next mirror", i + 1, diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index c909c80a..dbd0751c 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -1569,6 +1569,13 @@ chmod +x "$WORK_DIR/first-boot-secrets.sh" cp "$WORK_DIR/first-boot-secrets.sh" "$ARCH_DIR/scripts/" cp "$WORK_DIR/archipelago-first-boot-secrets.service" "$ARCH_DIR/scripts/" +# Ship the mesh-radio udev rule at the media root — the embedded installer +# searches "$BOOT_MEDIA/99-mesh-radio.rules" first, but nothing ever staged +# the rule into the ISO tree, so installed nodes never got /dev/mesh-radio +if [ -f "$SCRIPT_DIR/../configs/99-mesh-radio.rules" ]; then + cp "$SCRIPT_DIR/../configs/99-mesh-radio.rules" "$INSTALLER_ISO/99-mesh-radio.rules" +fi + # Tor setup: copy torrc and create first-boot setup script mkdir -p "$ARCH_DIR/scripts/tor" if [ -f "$SCRIPT_DIR/../../scripts/tor/torrc.template" ]; then diff --git a/image-recipe/archipelago-scripts/install-to-disk.sh b/image-recipe/archipelago-scripts/install-to-disk.sh index 9fdc26d4..25b13901 100755 --- a/image-recipe/archipelago-scripts/install-to-disk.sh +++ b/image-recipe/archipelago-scripts/install-to-disk.sh @@ -278,6 +278,17 @@ if [ -n "$BOOT_MEDIA" ]; then mkdir -p /mnt/archipelago/etc/archipelago cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/archipelago/etc/archipelago/ fi + + # Mesh radio udev rule: stable /dev/mesh-radio symlink + dialout perms + # for known LoRa USB-serial chips (this installer never shipped it) + for p in "$BOOT_MEDIA/99-mesh-radio.rules" "$BOOT_MEDIA/archipelago/configs/99-mesh-radio.rules"; do + if [ -f "$p" ]; then + mkdir -p /mnt/archipelago/etc/udev/rules.d + cp "$p" /mnt/archipelago/etc/udev/rules.d/99-mesh-radio.rules + chmod 644 /mnt/archipelago/etc/udev/rules.d/99-mesh-radio.rules + break + fi + done # Create profile.d script for welcome message on login mkdir -p /mnt/archipelago/etc/profile.d diff --git a/image-recipe/configs/archipelago-kiosk.service b/image-recipe/configs/archipelago-kiosk.service index b388c4f5..2b53a8e0 100644 --- a/image-recipe/configs/archipelago-kiosk.service +++ b/image-recipe/configs/archipelago-kiosk.service @@ -15,6 +15,13 @@ Type=simple # slow-but-functional hardware enough headroom; TimeoutStartSec is # bumped in lockstep so systemd doesn't kill us mid-wait. ExecStartPre=/bin/bash -c 'for i in $(seq 1 150); do curl -sf http://localhost/health >/dev/null 2>&1 && break; sleep 2; done' +# Also wait for the web-ui asset swap to finish. The first-boot rsync into +# /opt/archipelago/web-ui is non-atomic and writes the large bg-*.webp images +# last — a kiosk launched mid-swap rendered the UI with blank backgrounds +# (CSS background-image 404s are never refetched). A representative large +# asset answering 200 means the swap is effectively done. Bounded 60s so a +# renamed asset can never block kiosk startup. +ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf -o /dev/null http://localhost/assets/img/bg-home.webp && break; sleep 2; done; exit 0' ExecStart=/usr/local/bin/archipelago-kiosk-launcher TimeoutStartSec=360 Restart=always diff --git a/neode-ui/src/components/PWAUpdatePrompt.vue b/neode-ui/src/components/PWAUpdatePrompt.vue index 4e0356a1..85c943b4 100644 --- a/neode-ui/src/components/PWAUpdatePrompt.vue +++ b/neode-ui/src/components/PWAUpdatePrompt.vue @@ -63,14 +63,20 @@ onMounted(() => { if (newWorker) { newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { - // New service worker installed, show update prompt - showUpdatePrompt.value = true updateCallback = async () => { if (newWorker.state === 'installed' && registration.waiting) { // Skip waiting and activate the new service worker registration.waiting.postMessage({ type: 'SKIP_WAITING' }) } } + // The kiosk is unattended — nobody can click "Update Now", + // so it kept running stale bundles forever (stale FIPS state, + // old assets). Auto-apply there; prompt everywhere else. + if (localStorage.getItem('kiosk')) { + updateCallback() + } else { + showUpdatePrompt.value = true + } } }) } diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index ce346560..e9d547ce 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -16,6 +16,8 @@ export interface MeshStatus { messages_sent: number messages_received: number detected_devices?: string[] + /** A serial radio is physically present, whether or not a session opened it. */ + device_present?: boolean /** Bitcoin block-header send/receive prefs (issue #28). */ announce_block_headers?: boolean receive_block_headers?: boolean diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index 259907a8..f039ae62 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -1767,6 +1767,7 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {

{{ mesh.status?.peer_count ?? 0 }} peer{{ (mesh.status?.peer_count ?? 0) !== 1 ? 's' : '' }} Live + Mesh device detected

Status - - {{ mesh.status.device_connected ? 'Broadcasting' : mesh.status.enabled ? 'Connecting...' : 'Disabled' }} + + {{ mesh.status.device_connected ? 'Broadcasting' : mesh.status.device_present ? 'Device detected — connecting' : mesh.status.enabled ? 'Connecting...' : 'Disabled' }}
diff --git a/neode-ui/src/views/marketplace/MarketplaceAppCard.vue b/neode-ui/src/views/marketplace/MarketplaceAppCard.vue index 3e3c01d3..ebd06de9 100644 --- a/neode-ui/src/views/marketplace/MarketplaceAppCard.vue +++ b/neode-ui/src/views/marketplace/MarketplaceAppCard.vue @@ -185,6 +185,15 @@ const installProgressMessage = computed(() => { function handleImageError(event: Event) { const img = event.target as HTMLImageElement + // Catalog icon extensions drift (.svg vs .png on disk) — try the sibling + // extension once before giving up on the placeholder + if (!img.dataset.extRetry && /\.(svg|png)$/.test(img.src)) { + img.dataset.extRetry = '1' + img.src = img.src.endsWith('.svg') + ? img.src.replace(/\.svg$/, '.png') + : img.src.replace(/\.png$/, '.svg') + return + } if (!img.src.includes(DEFAULT_APP_ICON)) { img.src = DEFAULT_APP_ICON img.dataset.defaultIcon = '1' diff --git a/neode-ui/src/views/server/FipsNetworkCard.vue b/neode-ui/src/views/server/FipsNetworkCard.vue index fe3dc7ea..2257d142 100644 --- a/neode-ui/src/views/server/FipsNetworkCard.vue +++ b/neode-ui/src/views/server/FipsNetworkCard.vue @@ -119,7 +119,7 @@