diff --git a/app/server/runtime.cpp b/app/server/runtime.cpp index 9793affd..a67a4ebd 100644 --- a/app/server/runtime.cpp +++ b/app/server/runtime.cpp @@ -215,6 +215,35 @@ const char * backend_name(engine::core::BackendType type) { return "unknown"; } +HttpResponse ui_service_worker_retirement_response() { + HttpResponse response; + response.status = 200; + response.content_type = "application/javascript; charset=utf-8"; + response.body = R"JS( +// Retire service workers left by applications that previously used this origin. +self.addEventListener("install", (event) => { + event.waitUntil(self.skipWaiting()); +}); +self.addEventListener("activate", (event) => { + event.waitUntil((async () => { + const cacheNames = await caches.keys(); + await Promise.all(cacheNames.map((name) => caches.delete(name))); + await self.clients.claim(); + const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); + await self.registration.unregister(); + await Promise.all(windows.map((client) => client.navigate(client.url))); + })()); +}); +)JS"; + response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"; + response.headers["Clear-Site-Data"] = "\"cache\""; + response.headers["Expires"] = "0"; + response.headers["Pragma"] = "no-cache"; + response.headers["Service-Worker-Allowed"] = "/"; + response.headers["X-Content-Type-Options"] = "nosniff"; + return response; +} + std::unordered_map options_from_object(const Value * value) { return minitts::cli::json_options_map(value); } @@ -876,6 +905,11 @@ HttpResponse ServerState::handle(const HttpRequest & request) { else if (request.method == "GET" && (request.path == "/" || request.path == "/index.html")) { response = handle_ui_asset(); } + else if ( + request.method == "GET" && config_.ui_enabled && + (request.path == "/sw.js" || request.path == "/service-worker.js")) { + response = ui_service_worker_retirement_response(); + } else if (request.method == "GET" && request.path == "/favicon.ico") { response.status = 204; response.content_type = "image/x-icon"; @@ -1399,7 +1433,15 @@ HttpResponse ServerState::handle_ui_asset() const { response.content_type = "text/html; charset=utf-8"; const auto html = embedded_ui_html(); response.body.assign(html.data(), html.size()); - response.headers["Cache-Control"] = "no-cache"; + // The WebUI shares the server origin (usually localhost:8080) with any app + // that previously occupied that port. Never let an old shell survive a + // server upgrade, and ask the browser to discard only cached resources. + // Deliberately omit the Clear-Site-Data "storage" directive: saved voices, + // the selected models folder, and UI preferences live in local storage. + response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"; + response.headers["Clear-Site-Data"] = "\"cache\""; + response.headers["Expires"] = "0"; + response.headers["Pragma"] = "no-cache"; response.headers["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline' blob: data:; connect-src 'self'; media-src 'self' blob: data:"; response.headers["X-Content-Type-Options"] = "nosniff"; diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index 22d18e38..b09b7037 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -6,27 +6,27 @@ -
diff --git a/webui/native/src/app.css b/webui/native/src/app.css index 3e18e3a0..4153f2a5 100644 --- a/webui/native/src/app.css +++ b/webui/native/src/app.css @@ -182,7 +182,6 @@ pre { background: #071220; border: 1px solid var(--line); border-radius: 8px; pa .model-actions button { padding: 7px; font-size: 11px; } .package-buttons { display: grid; grid-template-columns: repeat(3, minmax(76px, 1fr)); gap: 5px; } .package-buttons.wide-package-set { grid-template-columns: repeat(2, minmax(0, 1fr)); } -.package-buttons.wide-package-set .package-choice, .package-buttons.wide-package-set .package-install { height: 68px; } .package-buttons.wide-package-set .package-size { line-height: 1.15; } .package-buttons:has(> .package-choice:only-child) { grid-template-columns: minmax(0, 1fr); } .package-buttons:has(> .package-choice:nth-child(2):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); } diff --git a/webui/native/src/routes/+page.svelte b/webui/native/src/routes/+page.svelte index fb4c5088..1fdb5b4e 100644 --- a/webui/native/src/routes/+page.svelte +++ b/webui/native/src/routes/+page.svelte @@ -132,6 +132,27 @@ document.documentElement.lang = uiLanguage; } + async function clearLegacyUiCaches() { + // This server commonly reuses localhost:8080. Remove workers and Cache + // Storage left by an older application on that origin before Native Studio + // starts making requests. Do not clear localStorage or IndexedDB: they hold + // saved voices, model-folder selection, and UI preferences. + try { + if ('serviceWorker' in navigator) { + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map((registration) => registration.unregister())); + } + if ('caches' in window) { + const cacheNames = await window.caches.keys(); + await Promise.all(cacheNames.map((name) => window.caches.delete(name))); + } + } catch (error) { + // Cache cleanup must not prevent an offline/local UI from starting when a + // browser restricts either API. The no-store response headers still apply. + console.warn('Unable to clear legacy WebUI caches:', error); + } + } + function workflowLabel(id: string, fallback: string, translate = tr) { const translationId = id === 'conversion' ? 'vc' : id === 'separation' ? 'sep' : id; return translate(`workflow.${translationId}`, {}, fallback); @@ -1707,6 +1728,7 @@ } onMount(async () => { + await clearLegacyUiCaches(); const savedLanguage = localStorage.getItem('audiocpp.ui.language'); uiLanguage = resolveUiLanguage(savedLanguage ? [savedLanguage] : navigator.languages); document.documentElement.lang = uiLanguage;