From b5ea7112a9d10227cc2b97bc1b969e8976e447a1 Mon Sep 17 00:00:00 2001 From: Ark0N Date: Mon, 8 Jun 2026 05:03:59 +0200 Subject: [PATCH] fix(web): cache-bust same-origin module scripts + stylesheets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static assets are served Cache-Control: max-age=1y, immutable, but the script and link tags in index.html carried no version — so any edit to a frontend module (panels-ui.js, styles.css, …) stayed cached until a manual hard refresh. renderIndexHtml now appends ?v= to every same-origin .js/.css ref (generalizing the existing gesture-bundle cache-bust), re-stat'd per render so a changed file is picked up with no server restart. External URLs, already- versioned refs, and refs with no file on disk are left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/web/server.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/web/server.ts b/src/web/server.ts index f777b4a4..22885c8b 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -997,6 +997,9 @@ export class WebServer extends EventEmitter { 'Codeman', `${escapeHtmlText(this.windowTitle)}` ); + // Cache-bust same-origin module scripts + stylesheets so a normal reload + // always serves the latest (static assets carry a 1-year immutable cache). + html = this.cacheBustAssets(html); // Detached single-session ("solo") window: inject the target session id so // the client can enter solo mode even if a (network-first) service worker // later serves a cached shell. The client primarily detects solo mode from @@ -1032,6 +1035,26 @@ export class WebServer extends EventEmitter { } } + /** Append ?v= to every same-origin .js/.css reference in the page so a + * normal reload always serves the latest. Codeman's static assets are sent + * with `Cache-Control: max-age=1y, immutable` and the script/link tags carry + * no version, so without this an edited module (panels-ui.js, styles.css, …) + * stays cached until a manual hard refresh. mtime is re-stat'd per render, so + * a changed file is picked up with no server restart. External URLs (have a + * `:` scheme), already-versioned refs (have a `?`), and refs with no matching + * file on disk are left untouched. */ + private cacheBustAssets(html: string): string { + const publicDir = join(__dirname, 'public'); + return html.replace(/(\s(?:src|href)=")([^"?:]+\.(?:js|css))(")/g, (full, pre, ref, post) => { + try { + const v = Math.floor(statSync(join(publicDir, ref)).mtimeMs); + return `${pre}${ref}?v=${v}${post}`; + } catch { + return full; + } + }); + } + private async setupSessionListeners(session: Session): Promise { // Create run summary tracker for this session const summaryTracker = new RunSummaryTracker(session.id, session.name);