feat: PWA auto-update detection for injected script

server.js: hash live-search.js into INJECT_VERSION, expose
/api/custom/inject-version, set no-store on HTML so inline version
tag stays fresh.

live-search.js: compare baked window.__LS_VERSION vs server version
every 2 min + on visibility change; show banner, auto-unregister
service worker, clear caches, and reload when a new version ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-17 12:26:04 -04:00
parent 40dd22e3ad
commit 2418298d77
2 changed files with 99 additions and 2 deletions

View File

@ -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 =
'<span>New version available</span>' +
'<button id="ls-update-btn">Refresh</button>';
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);
})();
})();

View File

@ -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 = `<script>${customJS}</script>`;
const INJECT_VERSION = crypto
.createHash('sha256')
.update(customJS)
.digest('hex')
.slice(0, 12);
const SCRIPT_TAG =
`<script>window.__LS_VERSION=${JSON.stringify(INJECT_VERSION)};</script>` +
`<script>${customJS}</script>`;
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);