Skip to content
This repository was archived by the owner on Oct 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/web.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ npx expo export -p web # static site in dist/

Web Bluetooth only works in a secure context, so serve the exported site over HTTPS. `http://localhost` also counts as secure for development.

## Installing as an app

The web app is a progressive web app, so Chrome and Edge offer to install it to the home screen or desktop, where it opens in its own window.

- `public/manifest.json` sets the name, start URL, standalone display and colours. Its icons are in `public/icons/`: 192 and 512 pixel versions of the app icon, plus a maskable icon made from the Android adaptive foreground.
- `src/app/+html.tsx` links the manifest and registers `public/sw.js`. Registration only happens in production exports, so `npm run web` never caches Metro's development bundles.
- 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.

## Supported browsers

Web Bluetooth is available in Chrome and Edge on Android, Windows, macOS and ChromeOS. Safari, Firefox and every iOS browser lack it. In those browsers the PCs screen shows that Bluetooth is unavailable and names the supported browsers.
Expand Down
Binary file added public/icons/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"id": "/",
"name": "Switchify Remote",
"short_name": "Switchify Remote",
"description": "Control a Windows PC or Mac running Switchify PC over Bluetooth.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#050505",
"theme_color": "#050505",
"categories": ["accessibility", "utilities"],
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
54 changes: 54 additions & 0 deletions public/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Switchify Remote service worker. Pages come from the network first so a new
// deployment is never stuck behind the cache; content-hashed bundles and assets
// are cached once and reused, which lets the app open offline after a visit.
const CACHE = 'switchify-remote-v1';
const SHELL = ['/', '/manifest.json', '/icons/icon-192.png', '/icons/icon-512.png'];
const IMMUTABLE = ['/_expo/static/', '/assets/'];

self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)).then(() => self.skipWaiting()));
Comment on lines +5 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Installation misses required app code

Installation saves the root HTML but none of its required JavaScript bundles. registerServiceWorker runs after load, when the first visit's bundles have already loaded without the worker. Until another controlled visit, offline startup depends on the browser's separate HTTP cache. If that cache is gone, the installed app cannot open offline. This is a non-blocking offline reliability gap: save the required bundles during installation as well.

Artifacts

Evidence from the check

  • This executed script serves the prepared build, records both launches and asserts bundle failures and preserved CacheStorage, reproducing the missing-code installation defect.

Evidence from the check

  • This executed runner captures the reproduction command, working directory, exit code and actual output into the execution log, making the run reproducible.

Command output from the check

  • The captured successful command output includes browser network events, DOM state and CacheStorage entries for both launches, supporting the reported defect.

▶ Recording of the check

  • Chromium records the fresh production visit and holds on the rendered onboarding UI for eight seconds, establishing the working baseline.

Screenshot of the first online visit

  • The screenshot captures the first-visit page after live DOM assertions detected onboarding text and two buttons, documenting the working baseline.

▶ Recording of the check

  • Chromium records the offline launch and holds its nonfunctional result for ten seconds after the required bundle fails, demonstrating the installation gap.

Screenshot of the nonfunctional offline launch

  • The screenshot captures the offline page whose live DOM contains only “Switchify Remote” and no buttons, documenting the failed app render.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/sw.js
Line: 5-9

Comment:
**Installation misses required app code**

Installation saves the root HTML but none of its required JavaScript bundles. `registerServiceWorker` runs after `load`, when the first visit's bundles have already loaded without the worker. Until another controlled visit, offline startup depends on the browser's separate HTTP cache. If that cache is gone, the installed app cannot open offline. This is a non-blocking offline reliability gap: save the required bundles during installation as well.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE).map((key) => caches.delete(key))))
.then(() => self.clients.claim()),
);
});

async function networkFirst(request) {
const cache = await caches.open(CACHE);
try {
const response = await fetch(request);
if (response.ok) await cache.put(request, response.clone());
return response;
Comment on lines +23 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Full storage breaks online loading

If browser storage is full, cache.put can fail after a successful download. networkFirst then returns an old cached page, while cacheFirst rejects the bundle request instead of returning the downloaded code. Users can get an old build or an app that fails to load even while online. Catch cache-write failures separately in both functions and still return the downloaded response. This loading failure must be fixed before merging.

Artifacts

Evidence from the check

  • This executed harness serves the unchanged worker and runs isolated baseline, healthy-cache, and injected-failure cases, making the reproduction inspectable.

Evidence from the check

  • This executed runner records each command, working directory, exit code, and observed output, making the verification reproducible.

Command output from the check

  • The command captured the commit, repository status, worker hash, full worker source, and its absence at base, identifying exactly what was tested.

Command output from the check

  • Chromium loaded the same HTTP fixtures without service-worker registration and received fresh HTML plus an executable bundle, establishing the no-worker baseline.

Command output from the check

  • Chromium executed the unchanged worker with normal Cache.put behavior and received fresh HTML plus an executable bundle, confirming the healthy control.

Command output from the check

  • Chromium executed the unchanged worker with injected Cache.put rejections and observed stale HTML plus failed bundle loading despite HTTP 200 fetches, supporting candidate 0.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/sw.js
Line: 23-25

Comment:
**Full storage breaks online loading**

If browser storage is full, `cache.put` can fail after a successful download. `networkFirst` then returns an old cached page, while `cacheFirst` rejects the bundle request instead of returning the downloaded code. Users can get an old build or an app that fails to load even while online. Catch cache-write failures separately in both functions and still return the downloaded response. This loading failure must be fixed before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

} catch (error) {
const cached = (await cache.match(request, { ignoreSearch: true })) ?? (await cache.match('/'));
if (cached) return cached;
throw error;
}
}

async function cacheFirst(request) {
const cache = await caches.open(CACHE);
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) await cache.put(request, response.clone());
return response;
}

self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
if (request.mode === 'navigate') {
event.respondWith(networkFirst(request));
} else if (IMMUTABLE.some((prefix) => url.pathname.startsWith(prefix))) {
event.respondWith(cacheFirst(request));
} else {
event.respondWith(networkFirst(request));
}
});
31 changes: 31 additions & 0 deletions src/app/+html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ScrollViewStyleReset } from 'expo-router/html';
import { type PropsWithChildren } from 'react';

// Runs only in Node during static rendering, so it decides here whether the page
// registers the service worker. Development builds never do, which keeps Metro's
// live bundles out of the cache.
const registerServiceWorker = `
if ('serviceWorker' in navigator && window.isSecureContext) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js').catch(function () {});
});
}`;

export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover" />
<meta name="theme-color" content="#050505" />
<meta name="description" content="Control a Windows PC or Mac running Switchify PC over Bluetooth." />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<ScrollViewStyleReset />
{process.env.NODE_ENV === 'production' ? <script dangerouslySetInnerHTML={{ __html: registerServiceWorker }} /> : null}
</head>
<body>{children}</body>
</html>
);
}
19 changes: 18 additions & 1 deletion vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,22 @@
"buildCommand": "npx expo export -p web",
"outputDirectory": "dist",
"cleanUrls": true,
"framework": null
"framework": null,
"headers": [
{
"source": "/sw.js",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
},
{
"source": "/manifest.json",
"headers": [
{ "key": "Cache-Control", "value": "no-cache" },
{ "key": "Content-Type", "value": "application/manifest+json" }
]
},
{
"source": "/_expo/static/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]
}
Loading