From 1a29d500d30623cb5b544b11ad36c3221261d627 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:00:36 -0400 Subject: [PATCH] Link the app's code with a build number so a proxy cache cannot serve an old copy The People screens did not appear after the redeploy even after a hard refresh: a caching layer (for example Nginx Proxy Manager's "Cache Assets") was still serving the old app.js. The pages now reference /app.js?v=, /style.css?v= etc., where the build changes with every release, so a stale cached file at the old address is never used. The service worker no longer precaches unversioned app code. Co-Authored-By: Claude Sonnet 5.5 --- public/sw.js | 4 ++-- server.js | 5 ++++- tools/smoke-test.js | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/public/sw.js b/public/sw.js index c6d6e56..40c5bc5 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,6 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v5'; -const SHELL = ['/style.css', '/app.js', '/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png']; +const CACHE = 'hub-v6'; +const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png']; self.addEventListener('install', e => { e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL))); diff --git a/server.js b/server.js index 016b1ba..d09812c 100644 --- a/server.js +++ b/server.js @@ -75,6 +75,9 @@ function serveStatic(req, res) { if (!file.startsWith(PUBLIC)) { res.writeHead(403); return res.end(); } fs.readFile(file, (err, data) => { if (err) { res.writeHead(404); return res.end('Not found'); } + // The pages link to /app.js?v= etc. A new release changes the address, so no proxy cache ("Cache Assets" in + // Nginx Proxy Manager, a CDN, the browser) can hand out an old copy. + if (p === '/index.html' || p === '/login.html') data = Buffer.from(String(data).replace(/(src|href)="(\/(?:app|webauthn|login)\.js|\/style\.css)"/g, `$1="$2?v=${BUILD}"`)); const headers = { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream' }; if (p.startsWith('/icons/')) headers['cache-control'] = 'public, max-age=604800'; // icons and splash screens rarely change else { // everything else: the browser may keep a copy but must check with us first, so a new version is never missed @@ -88,7 +91,7 @@ function serveStatic(req, res) { } // Changes whenever the app's files change, so an open page can tell that a newer version is available. -const BUILD = (() => { try { return crypto.createHash('sha1').update(['app.js', 'index.html', 'style.css'].map(f => fs.readFileSync(path.join(PUBLIC, f))).join('')).digest('hex').slice(0, 12); } catch { return String(Date.now()); } })(); +const BUILD = (() => { try { return crypto.createHash('sha1').update(['app.js', 'index.html', 'style.css', 'webauthn.js', 'login.js', 'login.html', 'sw.js'].map(f => fs.readFileSync(path.join(PUBLIC, f))).join('')).digest('hex').slice(0, 12); } catch { return String(Date.now()); } })(); // ---- OnlyOffice in-app editing (JWT-signed editor config + save-back callback to Seafile) const JWT_SECRET = process.env.ONLYOFFICE_JWT_SECRET || ''; diff --git a/tools/smoke-test.js b/tools/smoke-test.js index cc38809..b4f9ad9 100644 --- a/tools/smoke-test.js +++ b/tools/smoke-test.js @@ -56,6 +56,10 @@ const wait = ms => new Promise(r => setTimeout(r, ms)); ok('and an unchanged file is answered with a cheap 304', a2.status === 304, a2.status); const i1 = await fetch(base + '/icons/icon-192.png'); await i1.arrayBuffer(); ok('icons are cached for a week', /max-age=604800/.test(i1.headers.get('cache-control') || ''), i1.headers.get('cache-control')); const cfg = await (await fetch(base + '/api/config', { headers: { cookie } })).json(); ok('the server reports a build stamp so open pages can tell a newer version exists', /^[0-9a-f]{12}$/.test(cfg.build || ''), cfg.build); + const home = await (await fetch(base + '/', { headers: { cookie } })).text(), cfg2 = await (await fetch(base + '/api/config', { headers: { cookie } })).json(); + ok('the app page links its code with the current build number, so a cached old copy can never be used', home.includes(`/app.js?v=${cfg2.build}`) && home.includes(`/style.css?v=${cfg2.build}`), home.match(/app\.js[^"]*/)); + const login = await (await fetch(base + '/login')).text(); ok('so does the sign-in page', login.includes(`/login.js?v=${cfg2.build}`), login.match(/login\.js[^"]*/)); + const va = await fetch(base + `/app.js?v=${cfg2.build}`, { headers: { cookie } }); ok('the versioned address serves the file', va.status === 200 && (await va.text()).includes('addPersonSheet'), va.status); ok('the server is still running after all of that', alive); child.kill(); fs.rmSync(dir, { recursive: true, force: true }); if (failed) { console.log('\n--- server log ---\n' + log.split('\n').filter(l => /error|Error|not defined/.test(l)).slice(0, 12).join('\n')); }