diff --git a/scripts/build.ts b/scripts/build.ts index 985408b..7a6fa9c 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -79,9 +79,9 @@ const swSource = await swBuild.outputs[0]!.text(); const precachePaths = precachePathsFromOutputs( shellBuild.outputs.map((output) => relative(DIST, output.path)), [ - "/inference.worker.js", - "/manifest.webmanifest", - ...ICON_SIZES.map((size) => `/icons/icon-${size}.png`), + "inference.worker.js", + "manifest.webmanifest", + ...ICON_SIZES.map((size) => `icons/icon-${size}.png`), ], ); const version = Bun.hash( diff --git a/src/App.tsx b/src/App.tsx index 70a47fc..342c08b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -120,7 +120,7 @@ export function App() { // main thread (Architecture.md §5). The worker script is bundled by the // server at startup and served at a stable URL (see src/index.ts) because // Bun's bundler does not follow `new Worker(new URL(...))` graphs yet. - const worker = new Worker("/inference.worker.js", { type: "module" }); + const worker = new Worker("inference.worker.js", { type: "module" }); worker.addEventListener("message", (event: MessageEvent) => { const message = event.data; if (!isWorkerToUiMessage(message)) return; diff --git a/src/lib/authClient.ts b/src/lib/authClient.ts index 49276ad..122c9d7 100644 --- a/src/lib/authClient.ts +++ b/src/lib/authClient.ts @@ -122,7 +122,7 @@ export function login( password: string, fetchFn: FetchLike = defaultFetch, ): Promise { - return requestUser(fetchFn, "/api/auth/login", { username, password }); + return requestUser(fetchFn, "api/auth/login", { username, password }); } /** POST /api/auth/register → user (auto signed in), or throws (400/409). */ @@ -131,7 +131,7 @@ export function register( password: string, fetchFn: FetchLike = defaultFetch, ): Promise { - return requestUser(fetchFn, "/api/auth/register", { username, password }); + return requestUser(fetchFn, "api/auth/register", { username, password }); } /** Result of the startup /me probe (fetchAuthStatus). */ @@ -158,7 +158,7 @@ export async function fetchAuthStatus( fetchFn: FetchLike = defaultFetch, ): Promise { try { - const response = await fetchFn("/api/auth/me", { + const response = await fetchFn("api/auth/me", { method: "GET", credentials: "same-origin", }); @@ -179,7 +179,7 @@ export async function fetchAuthStatus( */ export async function logout(fetchFn: FetchLike = defaultFetch): Promise { try { - await fetchFn("/api/auth/logout", { + await fetchFn("api/auth/logout", { method: "POST", credentials: "same-origin", }); diff --git a/src/lib/engine.ts b/src/lib/engine.ts index 79c387d..5e6228f 100644 --- a/src/lib/engine.ts +++ b/src/lib/engine.ts @@ -263,7 +263,7 @@ export class ServerEngine implements Engine { let response: Response; try { - response = await this.fetchFn(`/api/transcribe?${params}`, { + response = await this.fetchFn(`api/transcribe?${params}`, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/octet-stream" }, @@ -327,7 +327,7 @@ export async function fetchServerEngineStatus( fetchFn: FetchLike = defaultFetch, ): Promise { try { - const response = await fetchFn("/api/transcribe/status", { + const response = await fetchFn("api/transcribe/status", { method: "GET", credentials: "same-origin", }); diff --git a/src/pwa/manifest.webmanifest b/src/pwa/manifest.webmanifest index 6669a4b..2805eaf 100644 --- a/src/pwa/manifest.webmanifest +++ b/src/pwa/manifest.webmanifest @@ -2,21 +2,21 @@ "name": "Whisper Web — on-device speech-to-text", "short_name": "Whisper Web", "description": "Speech-to-text that runs entirely in your browser via OpenAI Whisper. Works offline once installed with a downloaded model.", - "id": "/", - "start_url": "/", - "scope": "/", + "id": "./", + "start_url": "./", + "scope": "./", "display": "standalone", "theme_color": "#16181d", "background_color": "#16181d", "icons": [ { - "src": "/icons/icon-192.png", + "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/icons/icon-512.png", + "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" diff --git a/src/pwa/precache.ts b/src/pwa/precache.ts index de498b9..bbc8d7b 100644 --- a/src/pwa/precache.ts +++ b/src/pwa/precache.ts @@ -27,7 +27,7 @@ export function precachePathsFromOutputs( for (const outputPath of outputPaths) { const normalized = outputPath.replaceAll("\\", "/").replace(/^\.?\//, ""); if (normalized.endsWith(".map")) continue; - paths.add(normalized === "index.html" ? "/" : `/${normalized}`); + paths.add(normalized === "index.html" ? "./" : normalized); } for (const extra of extraPaths) paths.add(extra); return [...paths].sort(); diff --git a/src/pwa/register.ts b/src/pwa/register.ts index a4f278d..579ed39 100644 --- a/src/pwa/register.ts +++ b/src/pwa/register.ts @@ -31,7 +31,7 @@ export function registerServiceWorker(options: RegisterOptions): void { if (container.controller) options.onControlled?.(); container - .register("/sw.js") + .register("sw.js") .then((registration) => { const notify = (worker: ServiceWorker) => { options.onUpdateReady(() => { diff --git a/src/pwa/sw.ts b/src/pwa/sw.ts index 8a5c951..79e5c70 100644 --- a/src/pwa/sw.ts +++ b/src/pwa/sw.ts @@ -18,6 +18,7 @@ // In dev src/index.ts injects an empty list + "dev"; in production // scripts/build.ts injects the hashed dist asset list + a content hash. const PRECACHE: string[] = JSON.parse("__PRECACHE_MANIFEST__") as string[]; +const PRECACHE_URLS = PRECACHE.map(path => new URL(path, sw.location.href).pathname); const VERSION = "__CACHE_VERSION__"; const SHELL_CACHE = `whisper-shell-${VERSION}`; @@ -118,19 +119,19 @@ sw.addEventListener("fetch", (event) => { if (url.origin !== sw.location.origin) return; // API is network-only (D-007) — auth and the server engine must never be // served stale from cache. - if (url.pathname.startsWith("/api/")) return; + if (url.pathname.includes("/api/")) return; - if (url.pathname.startsWith("/ort/")) { + if (url.pathname.includes("/ort/")) { event.respondWith(cacheFirst(RUNTIME_CACHE, request)); return; } - if (PRECACHE.includes(url.pathname)) { + if (PRECACHE_URLS.includes(url.pathname)) { event.respondWith(cacheFirst(SHELL_CACHE, request)); return; } // Anything else (dev's unhashed shell, navigations to deep links): // network-first with cache fallback; navigations fall back to the shell. - event.respondWith(networkFirst(request, request.mode === "navigate" ? "/" : null)); + event.respondWith(networkFirst(request, request.mode === "navigate" ? "./" : null)); }); export {}; diff --git a/src/worker/inference.worker.ts b/src/worker/inference.worker.ts index 4bf6eb1..be843dd 100644 --- a/src/worker/inference.worker.ts +++ b/src/worker/inference.worker.ts @@ -31,7 +31,7 @@ import { // them at /ort/* (src/index.ts; copied into dist/ort by scripts/build.ts) // and the service worker runtime-caches them cache-first (src/pwa/sw.ts). if (env.backends.onnx.wasm) { - env.backends.onnx.wasm.wasmPaths = "/ort/"; + env.backends.onnx.wasm.wasmPaths = "ort/"; } // `lib: ["DOM"]` types `self` as Window; give it the worker-scope shape we use. diff --git a/tests/auth-client.test.ts b/tests/auth-client.test.ts index 76bd902..2aba72e 100644 --- a/tests/auth-client.test.ts +++ b/tests/auth-client.test.ts @@ -69,7 +69,7 @@ describe("fetchAuthStatus (fail-silent /me)", () => { const { fetch, calls } = mockFetch(jsonResponse(200, { user: USER })); expect(await fetchAuthStatus(fetch)).toEqual({ enabled: true, user: USER }); // The cookie is HttpOnly — it must ride along via same-origin credentials. - expect(calls[0]?.input).toBe("/api/auth/me"); + expect(calls[0]?.input).toBe("api/auth/me"); expect(calls[0]?.init?.credentials).toBe("same-origin"); }); @@ -105,7 +105,7 @@ describe("login", () => { expect(await login("alice", "hunter22", fetch)).toEqual(USER); const call = calls[0]; - expect(call?.input).toBe("/api/auth/login"); + expect(call?.input).toBe("api/auth/login"); expect(call?.init?.method).toBe("POST"); expect(call?.init?.credentials).toBe("same-origin"); expect( @@ -145,7 +145,7 @@ describe("register", () => { test("returns the user on 201", async () => { const { fetch, calls } = mockFetch(jsonResponse(201, { user: USER })); expect(await register("alice", "hunter22", fetch)).toEqual(USER); - expect(calls[0]?.input).toBe("/api/auth/register"); + expect(calls[0]?.input).toBe("api/auth/register"); }); test("surfaces {error} on 409 (username taken)", async () => { @@ -172,7 +172,7 @@ describe("logout", () => { test("POSTs to /api/auth/logout with credentials", async () => { const { fetch, calls } = mockFetch(new Response(null, { status: 204 })); await logout(fetch); - expect(calls[0]?.input).toBe("/api/auth/logout"); + expect(calls[0]?.input).toBe("api/auth/logout"); expect(calls[0]?.init?.method).toBe("POST"); expect(calls[0]?.init?.credentials).toBe("same-origin"); }); diff --git a/tests/precache.test.ts b/tests/precache.test.ts index 19e6cdc..079201a 100644 --- a/tests/precache.test.ts +++ b/tests/precache.test.ts @@ -11,15 +11,15 @@ describe("precachePathsFromOutputs", () => { expect( precachePathsFromOutputs( ["index.html", "chunk-abc123.js", "chunk-abc123.js.map", "./index-def456.css"], - ["/inference.worker.js"], + ["inference.worker.js"], ), - ).toEqual(["/", "/chunk-abc123.js", "/index-def456.css", "/inference.worker.js"]); + ).toEqual(["./", "chunk-abc123.js", "index-def456.css", "inference.worker.js"]); }); test("deduplicates and sorts", () => { - expect(precachePathsFromOutputs(["a.js", "a.js"], ["/a.js", "/"])).toEqual([ - "/", - "/a.js", + expect(precachePathsFromOutputs(["a.js", "a.js"], ["a.js", "./"])).toEqual([ + "./", + "a.js", ]); }); });