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) {