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:
Dorian
2026-07-13 18:41:00 +01:00
co-authored by Claude Fable 5
parent ca89cb4196
commit 00a70c6193
11 changed files with 93 additions and 7 deletions
+8 -2
View File
@@ -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
}
}
})
}
+2
View File
@@ -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
+3 -2
View File
@@ -1767,6 +1767,7 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
<p class="mesh-subtitle">
{{ 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-else-if="mesh.status?.device_present" class="mesh-subtitle-badge">Mesh device detected</span>
</p>
</div>
<a
@@ -1801,8 +1802,8 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
<div v-else-if="mesh.status" class="mesh-status-grid">
<div class="mesh-stat">
<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'">
{{ mesh.status.device_connected ? 'Broadcasting' : mesh.status.enabled ? 'Connecting...' : 'Disabled' }}
<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.device_present ? 'Device detected — connecting' : mesh.status.enabled ? 'Connecting...' : 'Disabled' }}
</span>
</div>
<div class="mesh-stat">
@@ -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'
+11 -2
View File
@@ -119,7 +119,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { rpcClient } from '@/api/rpc-client'
import { safeClipboardWrite } from '@/views/web5/utils'
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>