diff --git a/search-app/live-search.js b/search-app/live-search.js
index b0fe736..9eacb7d 100644
--- a/search-app/live-search.js
+++ b/search-app/live-search.js
@@ -741,4 +741,77 @@
});
console.log('[live-search] Immich live search with map view loaded');
+
+ // --- PWA auto-update detection ---
+ // The proxy bakes a version hash into window.__LS_VERSION on each HTML
+ // response. We poll /api/custom/inject-version and, if the server reports a
+ // newer hash, we nuke caches + unregister the service worker + reload so the
+ // PWA picks up the new HTML (with the new inline script).
+ (function setupAutoUpdate() {
+ const myVersion = window.__LS_VERSION;
+ if (!myVersion) return; // older HTML without version — skip
+ console.log('[live-search] version:', myVersion);
+
+ let updating = false;
+ async function forceUpdate() {
+ if (updating) return;
+ updating = true;
+ try {
+ if ('serviceWorker' in navigator) {
+ const regs = await navigator.serviceWorker.getRegistrations();
+ await Promise.all(regs.map(r => r.unregister().catch(() => null)));
+ }
+ if (window.caches) {
+ const keys = await caches.keys();
+ await Promise.all(keys.map(k => caches.delete(k).catch(() => null)));
+ }
+ } catch (e) {
+ console.warn('[live-search] cache clear failed', e);
+ }
+ location.reload();
+ }
+
+ function showBanner() {
+ if (document.getElementById('ls-update-banner')) return;
+ const b = document.createElement('div');
+ b.id = 'ls-update-banner';
+ b.innerHTML =
+ 'New version available' +
+ '';
+ b.style.cssText = [
+ 'position:fixed', 'bottom:16px', 'left:50%',
+ 'transform:translateX(-50%)', 'z-index:100000',
+ 'background:#e94560', 'color:#fff', 'padding:10px 14px',
+ 'border-radius:8px', 'box-shadow:0 6px 20px rgba(0,0,0,0.4)',
+ 'display:flex', 'gap:10px', 'align-items:center',
+ 'font-size:14px', 'font-family:system-ui,sans-serif'
+ ].join(';');
+ b.querySelector('#ls-update-btn').style.cssText =
+ 'background:#fff;color:#e94560;border:none;border-radius:6px;' +
+ 'padding:5px 10px;font-weight:600;cursor:pointer;';
+ b.querySelector('#ls-update-btn').addEventListener('click', forceUpdate);
+ document.body.appendChild(b);
+ // Auto-apply after 8s if user ignores banner
+ setTimeout(() => { if (document.body.contains(b)) forceUpdate(); }, 8000);
+ }
+
+ async function checkVersion() {
+ try {
+ const r = await fetch('/api/custom/inject-version', { cache: 'no-store' });
+ if (!r.ok) return;
+ const { version } = await r.json();
+ if (version && version !== myVersion) {
+ console.log('[live-search] new version detected:', version);
+ showBanner();
+ }
+ } catch {}
+ }
+
+ // Check on load, on visibility change, and every 2 minutes
+ checkVersion();
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') checkVersion();
+ });
+ setInterval(checkVersion, 2 * 60 * 1000);
+ })();
})();
diff --git a/search-app/server.js b/search-app/server.js
index d1486b6..0eb6023 100644
--- a/search-app/server.js
+++ b/search-app/server.js
@@ -2,6 +2,7 @@ const http = require('http');
const net = require('net');
const fs = require('fs');
const path = require('path');
+const crypto = require('crypto');
const PORT = process.env.PORT || 3000;
const IMMICH_URL = process.env.IMMICH_URL || 'http://immich-server:2283';
@@ -10,9 +11,17 @@ const immichParsed = new URL(IMMICH_URL);
// Timeouts for large uploads (10 minutes)
const UPLOAD_TIMEOUT = 10 * 60 * 1000;
-// Load the custom JS to inject
+// Load the custom JS to inject. Version hash lets PWAs detect stale caches.
const customJS = fs.readFileSync(path.join(__dirname, 'live-search.js'), 'utf8');
-const SCRIPT_TAG = ``;
+const INJECT_VERSION = crypto
+ .createHash('sha256')
+ .update(customJS)
+ .digest('hex')
+ .slice(0, 12);
+const SCRIPT_TAG =
+ `` +
+ ``;
+console.log(`Injected script version: ${INJECT_VERSION}`);
// Helper: make an internal request to Immich (for small JSON requests only)
function immichRequest(method, apiPath, headers, body) {
@@ -42,6 +51,16 @@ function immichRequest(method, apiPath, headers, body) {
}
const server = http.createServer((req, res) => {
+ // --- Custom endpoint: report current injected-script version ---
+ if (req.url === '/api/custom/inject-version' && req.method === 'GET') {
+ res.writeHead(200, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store, no-cache, must-revalidate'
+ });
+ res.end(JSON.stringify({ version: INJECT_VERSION }));
+ return;
+ }
+
// --- Custom endpoint: batch fetch exif (needs buffered body) ---
if (req.url === '/api/custom/batch-exif' && req.method === 'POST') {
let bodyChunks = [];
@@ -116,6 +135,11 @@ const server = http.createServer((req, res) => {
delete resHeaders['content-length'];
delete resHeaders['content-encoding'];
resHeaders['content-type'] = 'text/html; charset=utf-8';
+ // Prevent browser + CDN from caching our modified HTML so the
+ // inline script version check stays fresh on every navigation.
+ resHeaders['cache-control'] = 'no-store, no-cache, must-revalidate';
+ delete resHeaders['etag'];
+ delete resHeaders['last-modified'];
res.writeHead(proxyRes.statusCode, resHeaders);
res.end(html);