fix: RC feedback round 2 — kiosk staleness, LND icon+retry, mesh detection surfacing
Kiosk (backgrounds + FIPS mismatch, one root cause each): - kiosk service now also waits for the web-ui asset swap (probes a large bg asset, bounded 60s) — it launched mid-rsync and CSS background 404s are never refetched, hence blank backgrounds on first boot - FipsNetworkCard polls every 15s instead of fetching once on mount - PWA updates auto-apply on the kiosk (localStorage kiosk flag) — an unattended kiosk could never click 'Update Now' and ran stale bundles LND App Store: - catalog icon pointed at lnd.svg which doesn't exist (lnd.png does); store card also retries the sibling extension before the placeholder - image pulls retry once after fast transient failures (<30s) — fresh first-boot networks reset connections; slow failures aren't retried so the UI never waits longer than before Mesh detection: - mesh.status now always reports device_present + detected_devices (previously only when the service wasn't running, so the UI couldn't tell 'no radio' from 'radio present, session connecting') - Mesh page shows 'Mesh device detected' badge + 'Device detected — connecting' status for present-but-not-connected radios - the 99-mesh-radio.rules udev rule was never staged into the ISO tree (installer searched for it and found nothing) — ship it at the media root and install it from install-to-disk.sh too Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ca89cb4196
commit
00a70c6193
@ -39,7 +39,7 @@
|
|||||||
"title": "LND",
|
"title": "LND",
|
||||||
"version": "0.18.4",
|
"version": "0.18.4",
|
||||||
"description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
|
"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",
|
"author": "Lightning Labs",
|
||||||
"category": "money",
|
"category": "money",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
|
|||||||
@ -37,6 +37,21 @@ impl RpcHandler {
|
|||||||
"receive_block_headers".into(),
|
"receive_block_headers".into(),
|
||||||
config.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)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1180,6 +1180,7 @@ impl RpcHandler {
|
|||||||
// the fresh pull instead of showing stale bytes from a prior
|
// the fresh pull instead of showing stale bytes from a prior
|
||||||
// partial attempt.
|
// partial attempt.
|
||||||
self.set_install_progress(package_id, 0, 0).await;
|
self.set_install_progress(package_id, 0, 0).await;
|
||||||
|
let started = std::time::Instant::now();
|
||||||
match self
|
match self
|
||||||
.pull_one_url_with_progress(url, *tls_verify, package_id, &user_tmp)
|
.pull_one_url_with_progress(url, *tls_verify, package_id, &user_tmp)
|
||||||
.await?
|
.await?
|
||||||
@ -1190,6 +1191,24 @@ impl RpcHandler {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
false => {
|
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!(
|
tracing::debug!(
|
||||||
"Pull attempt {}/{} failed for {}, trying next mirror",
|
"Pull attempt {}/{} failed for {}, trying next mirror",
|
||||||
i + 1,
|
i + 1,
|
||||||
|
|||||||
@ -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/first-boot-secrets.sh" "$ARCH_DIR/scripts/"
|
||||||
cp "$WORK_DIR/archipelago-first-boot-secrets.service" "$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
|
# Tor setup: copy torrc and create first-boot setup script
|
||||||
mkdir -p "$ARCH_DIR/scripts/tor"
|
mkdir -p "$ARCH_DIR/scripts/tor"
|
||||||
if [ -f "$SCRIPT_DIR/../../scripts/tor/torrc.template" ]; then
|
if [ -f "$SCRIPT_DIR/../../scripts/tor/torrc.template" ]; then
|
||||||
|
|||||||
@ -278,6 +278,17 @@ if [ -n "$BOOT_MEDIA" ]; then
|
|||||||
mkdir -p /mnt/archipelago/etc/archipelago
|
mkdir -p /mnt/archipelago/etc/archipelago
|
||||||
cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/archipelago/etc/archipelago/
|
cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/archipelago/etc/archipelago/
|
||||||
fi
|
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
|
# Create profile.d script for welcome message on login
|
||||||
mkdir -p /mnt/archipelago/etc/profile.d
|
mkdir -p /mnt/archipelago/etc/profile.d
|
||||||
|
|||||||
@ -15,6 +15,13 @@ Type=simple
|
|||||||
# slow-but-functional hardware enough headroom; TimeoutStartSec is
|
# slow-but-functional hardware enough headroom; TimeoutStartSec is
|
||||||
# bumped in lockstep so systemd doesn't kill us mid-wait.
|
# 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'
|
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
|
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
|
||||||
TimeoutStartSec=360
|
TimeoutStartSec=360
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
@ -63,14 +63,20 @@ onMounted(() => {
|
|||||||
if (newWorker) {
|
if (newWorker) {
|
||||||
newWorker.addEventListener('statechange', () => {
|
newWorker.addEventListener('statechange', () => {
|
||||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||||
// New service worker installed, show update prompt
|
|
||||||
showUpdatePrompt.value = true
|
|
||||||
updateCallback = async () => {
|
updateCallback = async () => {
|
||||||
if (newWorker.state === 'installed' && registration.waiting) {
|
if (newWorker.state === 'installed' && registration.waiting) {
|
||||||
// Skip waiting and activate the new service worker
|
// Skip waiting and activate the new service worker
|
||||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export interface MeshStatus {
|
|||||||
messages_sent: number
|
messages_sent: number
|
||||||
messages_received: number
|
messages_received: number
|
||||||
detected_devices?: string[]
|
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). */
|
/** Bitcoin block-header send/receive prefs (issue #28). */
|
||||||
announce_block_headers?: boolean
|
announce_block_headers?: boolean
|
||||||
receive_block_headers?: boolean
|
receive_block_headers?: boolean
|
||||||
|
|||||||
@ -1767,6 +1767,7 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
|||||||
<p class="mesh-subtitle">
|
<p class="mesh-subtitle">
|
||||||
{{ mesh.status?.peer_count ?? 0 }} peer{{ (mesh.status?.peer_count ?? 0) !== 1 ? 's' : '' }}
|
{{ mesh.status?.peer_count ?? 0 }} peer{{ (mesh.status?.peer_count ?? 0) !== 1 ? 's' : '' }}
|
||||||
<span v-if="mesh.status?.device_connected" class="mesh-subtitle-badge">Live</span>
|
<span v-if="mesh.status?.device_connected" class="mesh-subtitle-badge">Live</span>
|
||||||
|
<span v-else-if="mesh.status?.device_present" class="mesh-subtitle-badge">Mesh device detected</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@ -1801,8 +1802,8 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
|||||||
<div v-else-if="mesh.status" class="mesh-status-grid">
|
<div v-else-if="mesh.status" class="mesh-status-grid">
|
||||||
<div class="mesh-stat">
|
<div class="mesh-stat">
|
||||||
<span class="mesh-stat-label">Status</span>
|
<span class="mesh-stat-label">Status</span>
|
||||||
<span class="mesh-stat-value" :class="mesh.status.device_connected ? 'text-green' : mesh.status.enabled ? 'text-orange' : 'text-muted'">
|
<span class="mesh-stat-value" :class="mesh.status.device_connected ? 'text-green' : mesh.status.device_present ? 'text-orange' : mesh.status.enabled ? 'text-orange' : 'text-muted'">
|
||||||
{{ 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' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mesh-stat">
|
<div class="mesh-stat">
|
||||||
|
|||||||
@ -185,6 +185,15 @@ const installProgressMessage = computed(() => {
|
|||||||
|
|
||||||
function handleImageError(event: Event) {
|
function handleImageError(event: Event) {
|
||||||
const img = event.target as HTMLImageElement
|
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)) {
|
if (!img.src.includes(DEFAULT_APP_ICON)) {
|
||||||
img.src = DEFAULT_APP_ICON
|
img.src = DEFAULT_APP_ICON
|
||||||
img.dataset.defaultIcon = '1'
|
img.dataset.defaultIcon = '1'
|
||||||
|
|||||||
@ -119,7 +119,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
import { safeClipboardWrite } from '@/views/web5/utils'
|
import { safeClipboardWrite } from '@/views/web5/utils'
|
||||||
import FipsSeedAnchorsCard from './FipsSeedAnchorsCard.vue'
|
import FipsSeedAnchorsCard from './FipsSeedAnchorsCard.vue'
|
||||||
@ -254,5 +254,14 @@ async function reconnectAnchor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadStatus)
|
// Poll instead of one-shot: a long-lived page (the kiosk especially) was
|
||||||
|
// stuck showing whatever anchor state existed at mount time forever.
|
||||||
|
let statusInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
onMounted(() => {
|
||||||
|
loadStatus()
|
||||||
|
statusInterval = setInterval(loadStatus, 15000)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (statusInterval) clearInterval(statusInterval)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user