From 47245616c3c4fd290b147cd1c91276374e0a4c63 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Fri, 9 Oct 2026 15:43:54 +0100 Subject: [PATCH] fix(web): recover when the web app fails to start - If the app has not started 10 seconds after the page loads, an inline script in the root HTML shows "Switchify Remote didn't start" with a focused Reload button - Reload unregisters the service worker and clears its caches first, so a broken or stale copy cannot leave anyone on the static loading spinner - The root layout marks the app as started, removing the message if a slow connection made it appear; it only runs in production exports and native is unaffected Co-Authored-By: Claude Opus 5.5 --- docs/web.md | 2 ++ src/app/+html.tsx | 43 ++++++++++++++++++++++++ src/app/_layout.tsx | 4 ++- src/platform/installPlatform.ts | 2 ++ src/platform/installPlatform.web.test.ts | 11 +++++- src/platform/installPlatform.web.ts | 6 ++++ 6 files changed, 66 insertions(+), 2 deletions(-) diff --git a/docs/web.md b/docs/web.md index c2ac091..ddcc76b 100644 --- a/docs/web.md +++ b/docs/web.md @@ -20,6 +20,8 @@ The web app is a progressive web app, so Chrome and Edge offer to install it to - The service worker fetches pages from the network first and falls back to the cache when offline, so a new deployment is never stuck behind an old copy. Content-hashed bundles and assets under `/_expo/static/` and `/assets/` are cached once and reused. Bluetooth still needs the PC nearby; offline only means the app opens. - `vercel.json` serves `sw.js` and the manifest with `no-cache`, and the hashed bundles as immutable. To change caching behaviour, change the `CACHE` name in `sw.js` so old caches are cleared on activation. +If the app has not started 10 seconds after the page loads, an inline script in `src/app/+html.tsx` shows "Switchify Remote didn't start" with a Reload button. Reload unregisters the service worker and clears its caches first, so a broken or stale copy cannot leave anyone on the loading screen. The root layout reports that the app started, which removes the message if a slow connection made it appear. It only runs in production exports. + ## Switch scanning Switch users can operate Remote with keyboard-style switch interfaces, which send key presses such as Space or Enter. Scanning uses [@switchify/scanning](https://github.com/switchifyapp/switchify-scanning), the TypeScript port of Switchify PC's item scanner, so timing, passes, groups and hold actions behave as they do on the PC. diff --git a/src/app/+html.tsx b/src/app/+html.tsx index 05b0d2d..16b9077 100644 --- a/src/app/+html.tsx +++ b/src/app/+html.tsx @@ -11,6 +11,48 @@ if ('serviceWorker' in navigator && window.isSecureContext) { }); }`; +/** + * If the app has not started 10 seconds after the page loads, offer a reload that + * also drops the service worker and its caches, so a broken or stale copy can never + * leave someone on the static loading screen. The app removes it once it starts. + */ +const startupWatchdog = ` +(function () { + var started = false; + window.__switchifyStarted = function () { + started = true; + var shown = document.getElementById('switchify-start-failed'); + if (shown) shown.remove(); + }; + function reload() { + var work = []; + if (navigator.serviceWorker) work.push(navigator.serviceWorker.getRegistrations().then(function (all) { return Promise.all(all.map(function (r) { return r.unregister(); })); })); + if (window.caches) work.push(caches.keys().then(function (keys) { return Promise.all(keys.map(function (k) { return caches.delete(k); })); })); + Promise.all(work).catch(function () {}).then(function () { location.reload(); }); + } + setTimeout(function () { + if (started || !document.body) return; + var box = document.createElement('div'); + box.id = 'switchify-start-failed'; + box.setAttribute('role', 'alert'); + box.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:24px;background:#0B0B0D;color:#F7F7F8;font:16px/1.5 system-ui,sans-serif;text-align:center'; + var title = document.createElement('p'); + title.style.cssText = 'margin:0;font-size:22px;font-weight:600'; + title.textContent = 'Switchify Remote didn’t start'; + var body = document.createElement('p'); + body.style.cssText = 'margin:0;max-width:28em;color:#B4B4BE'; + body.textContent = 'Reload to fetch a fresh copy. Your saved PCs and settings are kept.'; + var button = document.createElement('button'); + button.type = 'button'; + button.textContent = 'Reload'; + button.style.cssText = 'font:inherit;font-weight:600;min-height:52px;padding:0 32px;border:0;border-radius:12px;background:#D90429;color:#fff'; + button.addEventListener('click', reload); + box.append(title, body, button); + document.body.appendChild(box); + button.focus(); + }, 10000); +})();`; + export default function Root({ children }: PropsWithChildren) { return ( @@ -23,6 +65,7 @@ export default function Root({ children }: PropsWithChildren) { + {process.env.NODE_ENV === 'production' ?