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)