From 30918e4f4aaa3d1021438d871e8ee4e9165700eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 28 Sep 2026 23:40:24 +0300 Subject: [PATCH] fix: no stale stylesheet from caches (v3.6.31) Even with the rebuilt style.css, three caching paths could still serve the old file: - the server sent Cache-Control: no-cache only for index.html/sw.js, so browsers could heuristically cache style.css; - the SW install precached with addAll(), which re-uses HTTP-cached copies, so a new SW version could precache the stale file; - the fetch handler is cache-first (fine once the cache is fresh). Now every static shell file is served with Cache-Control: no-cache plus Last-Modified and cheap 304 revalidation, and the SW install fetches each precache URL with cache: 'reload' (CACHE simple-v304). A test pins the install behaviour. 661 frontend / 504 Python green; version 3.6.31. Note: the public demo simple.atroshin.ru is still v3.6.20 with the old stylesheet - it needs a deploy of the current frontend/. --- frontend/index.html | 2 +- frontend/sw.js | 11 +++++++++-- frontend/tests/sw.test.js | 17 +++++++++++++++++ pyproject.toml | 2 +- pysim_simple_server/server.py | 23 ++++++++++++++++++++--- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index dc6ea9e..1fd1453 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1713,7 +1713,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.6.30'; +const SIMPLE_VERSION = '3.6.31'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== diff --git a/frontend/sw.js b/frontend/sw.js index 028b3f4..faef1a0 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v303'; +const CACHE = 'simple-v304'; const URLS = [ 'index.html', 'help.html', @@ -18,8 +18,15 @@ const URLS = [ ]; self.addEventListener('install', e => { + // Fetch the shell with `cache: 'reload'`: a plain addAll() would re-use + // HTTP-cached copies, so a new SW version could precache a stale + // style.css/index.html (the stale-style trap, v3.6.31). e.waitUntil( - caches.open(CACHE).then(c => c.addAll(URLS)) + caches.open(CACHE).then(c => + Promise.all(URLS.map(u => fetch(u, { cache: 'reload' }).then(r => { + if (r && r.ok) return c.put(u, r); + }))) + ) ); self.skipWaiting(); }); diff --git a/frontend/tests/sw.test.js b/frontend/tests/sw.test.js index 3b8e7c5..975702e 100644 --- a/frontend/tests/sw.test.js +++ b/frontend/tests/sw.test.js @@ -55,6 +55,23 @@ function navigateEvent(url) { return event; } +test('install precaches every URL with cache: reload (no stale HTTP copies)', async () => { + const calls = []; + const { listeners, puts } = loadSW({ + fetchImpl: async (url, init) => { + calls.push([String(url), init && init.cache]); + return { ok: true, status: 200 }; + }, + cacheMatch: async () => undefined, + }); + const event = { waitUntil: p => { event.promise = p; } }; + listeners.install(event); + await event.promise; + assert.ok(calls.length >= 10, String(calls.length)); + assert.ok(calls.every(c => c[1] === 'reload'), JSON.stringify(calls[0])); + assert.strictEqual(puts.length, calls.length); +}); + test('offline navigation falls back to the cached index.html', async () => { const index = new FakeResponse('html'); const { listeners } = loadSW({ diff --git a/pyproject.toml b/pyproject.toml index 44eab9b..e65f739 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.6.30" +version = "3.6.31" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 4c28ecc..294f026 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.6.30' +VERSION = '3.6.31' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -5050,11 +5050,28 @@ class PysimHandler(BaseHTTPRequestHandler): with open(fs_path, 'rb') as f: data = f.read() content_type = _STATIC_MIME.get(os.path.splitext(rel)[1].lower(), 'application/octet-stream') + try: + mtime = int(os.path.getmtime(fs_path)) + except OSError: + mtime = 0 + last_modified = (time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(mtime)) + if mtime else '') + ims = self.headers.get('If-Modified-Since') if self.headers else None + if last_modified and ims == last_modified: + self.send_response(304) + self.send_header('Cache-Control', 'no-cache') + self.send_header('Last-Modified', last_modified) + self.end_headers() + return self.send_response(200) self.send_header('Content-Type', content_type) self.send_header('Content-Length', str(len(data))) - if rel in ('index.html', 'sw.js'): - self.send_header('Cache-Control', 'no-cache') + # Every shell file revalidates - index.html/sw.js always did, but a + # stale style.css silently drops newly added styles (v3.6.31). The + # revalidation is cheap thanks to Last-Modified/304. + self.send_header('Cache-Control', 'no-cache') + if last_modified: + self.send_header('Last-Modified', last_modified) self.end_headers() self.wfile.write(data)