diff --git a/search-app/server.js b/search-app/server.js index 7ad5c33..1ffc8ca 100644 --- a/search-app/server.js +++ b/search-app/server.js @@ -13,15 +13,35 @@ const UPLOAD_TIMEOUT = 10 * 60 * 1000; // 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 INJECT_VERSION = crypto +const CUSTOM_JS_HASH = crypto .createHash('sha256') .update(customJS) .digest('hex') .slice(0, 12); -const SCRIPT_TAG = - `` + - ``; -console.log(`Injected script version: ${INJECT_VERSION}`); + +// `currentVersion` combines our custom script with a fingerprint of Immich's +// served HTML so that any upstream update (chunk-hash rotation, etc.) also +// bumps the version. Stuck PWAs polling /api/custom/inject-version will see +// the change and self-reload. Initialized with just the custom hash until the +// first HTML response lands. +let currentVersion = CUSTOM_JS_HASH; +console.log(`Injected script hash: ${CUSTOM_JS_HASH} (initial version: ${currentVersion})`); + +function makeScriptTag(version) { + return ( + `` + + `` + ); +} + +// Extract just the chunk/manifest references from Immich's HTML (the parts +// that actually change on upgrades). Hashing the full HTML would include +// server-rendered state (CSRF tokens, session user id) that change per-user +// per-request and would cause spurious version churn. +function extractShellFingerprint(html) { + const matches = html.match(/\/_app\/immutable\/[^"'\s]+/g) || []; + return matches.sort().join('|'); +} // Helper: make an internal request to Immich (for small JSON requests only) function immichRequest(method, apiPath, headers, body) { @@ -57,7 +77,7 @@ const server = http.createServer((req, res) => { 'Content-Type': 'application/json', 'Cache-Control': 'no-store, no-cache, must-revalidate' }); - res.end(JSON.stringify({ version: INJECT_VERSION })); + res.end(JSON.stringify({ version: currentVersion })); return; } @@ -129,7 +149,20 @@ const server = http.createServer((req, res) => { proxyRes.on('data', chunk => htmlChunks.push(chunk)); proxyRes.on('end', () => { let html = Buffer.concat(htmlChunks).toString('utf8'); - html = html.replace('', SCRIPT_TAG + ''); + + // Compute a per-response version: custom script hash XOR'd + // with the upstream HTML's chunk/manifest fingerprint. This + // way an Immich upgrade that rotates chunk hashes bumps our + // version and stuck PWAs will self-heal on the next poll. + const shellHash = crypto + .createHash('sha256') + .update(customJS) + .update(extractShellFingerprint(html)) + .digest('hex') + .slice(0, 12); + currentVersion = shellHash; + + html = html.replace('', makeScriptTag(shellHash) + ''); const resHeaders = { ...proxyRes.headers }; delete resHeaders['content-length'];