From 66fd1217482aadf168fb7182d36afcc3db5c5a0a Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 16 Jul 2026 07:05:56 -0400 Subject: [PATCH] fix(kiosk): no reload on first service-worker claim + patient boot health check Two causes of the fresh-install 'loads, refreshes, loads again' jank: - The PWA autoUpdate service worker claims the page right after the very first load, firing controllerchange, which unconditionally reloaded the page. Now only a *replaced* controller (a real update) triggers reload. - A single 2s RPC echo decided server-down on first boot, flashing the BootScreen and then hard-reloading seconds later. A second, 6s attempt now runs before concluding the backend is down. Co-Authored-By: Claude Fable 5 --- neode-ui/src/components/PWAUpdatePrompt.vue | 12 +++++++++++- neode-ui/src/views/RootRedirect.vue | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/neode-ui/src/components/PWAUpdatePrompt.vue b/neode-ui/src/components/PWAUpdatePrompt.vue index 85c943b4..e970c142 100644 --- a/neode-ui/src/components/PWAUpdatePrompt.vue +++ b/neode-ui/src/components/PWAUpdatePrompt.vue @@ -32,8 +32,18 @@ let updateCallback: (() => Promise) | null = null onMounted(() => { // Listen for service worker updates if ('serviceWorker' in navigator) { + // On the very first visit the page loads with no controlling SW; the + // freshly-installed worker then activates and claims the page + // (autoUpdate → clientsClaim), firing controllerchange. Reloading on + // that first claim caused the fresh-install "loads, then reloads" + // jank (worst on kiosk). Only reload when an existing controller is + // REPLACED — i.e. a genuine update. + let hadController = !!navigator.serviceWorker.controller navigator.serviceWorker.addEventListener('controllerchange', () => { - // Service worker updated, reload the page + if (!hadController) { + hadController = true + return + } window.location.reload() }) diff --git a/neode-ui/src/views/RootRedirect.vue b/neode-ui/src/views/RootRedirect.vue index 740f85fe..d43d96ef 100644 --- a/neode-ui/src/views/RootRedirect.vue +++ b/neode-ui/src/views/RootRedirect.vue @@ -40,10 +40,10 @@ function log(msg: string, data?: unknown) { sessionStorage.setItem('archipelago_boot_log', prev + entry + '\n') } -async function quickHealthCheck(): Promise { +async function quickHealthCheck(timeoutMs = 2000): Promise { try { const ac = new AbortController() - const t = setTimeout(() => ac.abort(), 2000) + const t = setTimeout(() => ac.abort(), timeoutMs) const res = await fetch('/rpc/v1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -132,7 +132,16 @@ onMounted(async () => { return } - const isUp = await quickHealthCheck() + // First-boot backends can be up but slow (image loads, first-run podman + // work), so a single 2s echo timing out used to flash the BootScreen and + // then hard-reload seconds later ("loads, refreshes, loads again" on + // kiosk). Give it a second, more patient attempt before concluding the + // server is down. + let isUp = await quickHealthCheck() + if (!isUp) { + log('healthCheck retry with longer timeout') + isUp = await quickHealthCheck(6000) + } log('production flow', { isUp }) if (isUp) {