From 7dba1afde013b007c46e49eb54b092bf0ff2b1b3 Mon Sep 17 00:00:00 2001 From: Ali Orhun Akkirman Date: Mon, 28 Sep 2026 14:49:56 +0300 Subject: [PATCH 1/4] Guard optional UI during PWA updates --- src/app.js | 63 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/app.js b/src/app.js index 19e3100..eb7f0e1 100644 --- a/src/app.js +++ b/src/app.js @@ -424,8 +424,11 @@ function setLanguage(lang) { function updateUILanguage() { // Update tooltips document.getElementById('new-tab-btn').title = t('newFileBtn'); - document.getElementById('examples-btn').textContent = t('examplesBtn'); - document.getElementById('examples-btn').title = t('examplesTitle'); + const examplesBtn = document.getElementById('examples-btn'); + if (examplesBtn) { + examplesBtn.textContent = t('examplesBtn'); + examplesBtn.title = t('examplesTitle'); + } document.getElementById('zoom-in').title = t('zoomIn'); document.getElementById('zoom-out').title = t('zoomOut'); @@ -440,16 +443,26 @@ function updateUILanguage() { document.getElementById('export-confirm').textContent = t('exportConfirm'); document.getElementById('export-pdf-label').innerHTML = `PDF ${t('exportPdfHint')}`; - // Update backup / restore UI - document.getElementById('backup-btn').textContent = t('backupBtn'); - document.getElementById('backup-btn').title = t('backupTitle'); - document.getElementById('backup-modal-title').textContent = t('backupTitle'); - document.getElementById('backup-modal-desc').textContent = t('backupDesc'); - document.getElementById('backup-export-title').textContent = t('backupExportTitle'); - document.getElementById('backup-export-desc').textContent = t('backupExportDesc'); - document.getElementById('backup-import-title').textContent = t('backupImportTitle'); - document.getElementById('backup-import-desc').textContent = t('backupImportDesc'); - document.getElementById('backup-close').textContent = t('backupClose'); + // Update backup / restore UI. These controls are optional so an older cached + // HTML shell can still run safely while a newer JS bundle is being activated. + const backupBtn = document.getElementById('backup-btn'); + if (backupBtn) { + backupBtn.textContent = t('backupBtn'); + backupBtn.title = t('backupTitle'); + } + const backupText = { + 'backup-modal-title': t('backupTitle'), + 'backup-modal-desc': t('backupDesc'), + 'backup-export-title': t('backupExportTitle'), + 'backup-export-desc': t('backupExportDesc'), + 'backup-import-title': t('backupImportTitle'), + 'backup-import-desc': t('backupImportDesc'), + 'backup-close': t('backupClose') + }; + for (const [id, value] of Object.entries(backupText)) { + const el = document.getElementById(id); + if (el) el.textContent = value; + } updateConnectivityStatus(); // Update insert modal @@ -673,7 +686,7 @@ function openExamplesNotebook() { input.select(); } -document.getElementById('examples-btn').addEventListener('click', openExamplesNotebook); +document.getElementById('examples-btn')?.addEventListener('click', openExamplesNotebook); function confirmNewFile() { const input = document.getElementById('new-file-input'); @@ -777,7 +790,7 @@ function exportFullBackup() { const date = payload.exportedAt.slice(0, 10); const filename = `${t('backupFileName')}-${date}.json`; downloadBlob(JSON.stringify(payload, null, 2), filename, 'application/json;charset=utf-8'); - document.getElementById('backup-modal').classList.remove('active'); + document.getElementById('backup-modal')?.classList.remove('active'); } function normalizeBackup(raw) { @@ -834,7 +847,7 @@ function applyBackup(backup) { } } - document.getElementById('backup-modal').classList.remove('active'); + document.getElementById('backup-modal')?.classList.remove('active'); alert(t('backupRestored')); } @@ -863,22 +876,22 @@ function importFullBackup(file) { reader.readAsText(file); } -document.getElementById('backup-btn').addEventListener('click', () => { - document.getElementById('backup-modal').classList.add('active'); +document.getElementById('backup-btn')?.addEventListener('click', () => { + document.getElementById('backup-modal')?.classList.add('active'); }); -document.getElementById('backup-close').addEventListener('click', () => { - document.getElementById('backup-modal').classList.remove('active'); +document.getElementById('backup-close')?.addEventListener('click', () => { + document.getElementById('backup-modal')?.classList.remove('active'); }); -document.getElementById('backup-modal').addEventListener('click', event => { +document.getElementById('backup-modal')?.addEventListener('click', event => { if (event.target === document.getElementById('backup-modal')) { - document.getElementById('backup-modal').classList.remove('active'); + document.getElementById('backup-modal')?.classList.remove('active'); } }); -document.getElementById('backup-export-btn').addEventListener('click', exportFullBackup); -document.getElementById('backup-import-btn').addEventListener('click', () => { - document.getElementById('backup-file-input').click(); +document.getElementById('backup-export-btn')?.addEventListener('click', exportFullBackup); +document.getElementById('backup-import-btn')?.addEventListener('click', () => { + document.getElementById('backup-file-input')?.click(); }); -document.getElementById('backup-file-input').addEventListener('change', event => { +document.getElementById('backup-file-input')?.addEventListener('change', event => { const [file] = event.target.files || []; if (file) importFullBackup(file); }); From b3ff266fc2b6e9fe1695a866a216054dfb3dcb1a Mon Sep 17 00:00:00 2001 From: Ali Orhun Akkirman Date: Mon, 28 Sep 2026 14:49:58 +0300 Subject: [PATCH 2/4] Avoid mixed PWA versions during activation --- src/sw.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sw.js b/src/sw.js index d8de343..633e9b4 100644 --- a/src/sw.js +++ b/src/sw.js @@ -12,7 +12,8 @@ const NETWORK_FIRST_PATHS = new Set( async function networkFirst(request, fallbackUrl = null) { try { - const response = await fetch(request); + const freshRequest = new Request(request, { cache: 'no-store' }); + const response = await fetch(freshRequest); if (!response || !response.ok) throw new Error('Network response was not OK'); const cache = await caches.open(CACHE_NAME); @@ -49,7 +50,6 @@ self.addEventListener('install', event => { caches .open(CACHE_NAME) .then(cache => cache.addAll(PRECACHE_ASSETS.map(scopedUrl))) - .then(() => self.skipWaiting()) ); }); @@ -64,7 +64,6 @@ self.addEventListener('activate', event => { .map(key => caches.delete(key)) ) ) - .then(() => self.clients.claim()) ); }); From d273aefcab8bbe3164be108b81b57a39ba689307 Mon Sep 17 00:00:00 2001 From: Ali Orhun Akkirman Date: Mon, 28 Sep 2026 14:50:01 +0300 Subject: [PATCH 3/4] Verify safe PWA update lifecycle --- scripts/verify-offline.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/verify-offline.mjs b/scripts/verify-offline.mjs index 15fcdd4..915e042 100644 --- a/scripts/verify-offline.mjs +++ b/scripts/verify-offline.mjs @@ -65,6 +65,7 @@ if (!html.includes('id="examples-btn"')) { } const app = await readFile(new URL('app.js', root), 'utf8'); +const sourceApp = await readFile(new URL('../src/app.js', import.meta.url), 'utf8'); if (!app.includes("register('./sw.js'") && !app.includes('register("./sw.js"')) { violations.push('app.js: relative service worker registration missing'); } @@ -73,6 +74,13 @@ if (!app.includes('examples-btn') || !app.includes('getDefaultContent')) { violations.push('app.js: examples loader wiring missing'); } +for (const id of ['examples-btn', 'backup-btn', 'backup-close', 'backup-modal', 'backup-export-btn', 'backup-import-btn', 'backup-file-input']) { + const unsafe = `getElementById('${id}').addEventListener`; + if (sourceApp.includes(unsafe)) { + violations.push(`src/app.js: optional UI binding for ${id} is not null-safe`); + } +} + const manifest = JSON.parse(await readFile(new URL('manifest.webmanifest', root), 'utf8')); if (manifest.start_url !== './') violations.push('manifest: start_url must be ./'); if (manifest.scope !== './') violations.push('manifest: scope must be ./'); @@ -93,6 +101,13 @@ if (!/mathnote-[a-f0-9]{12}/.test(sw)) { if (!sw.includes('NETWORK_FIRST_PATHS') || !sw.includes('networkFirst(request')) { violations.push('sw.js: mutable core assets are not network-first'); } + +if (sw.includes('skipWaiting()') || sw.includes('clients.claim()')) { + violations.push('sw.js: eager service-worker takeover can mix HTML and JS versions'); +} +if (!sw.includes("cache: 'no-store'")) { + violations.push('sw.js: network-first core fetches should bypass the HTTP cache'); +} if (sw.includes('caches.match(')) { violations.push('sw.js: cross-cache lookup can serve stale assets'); } From 1ef94d7851584a8a8f363d5177682fcc6fc58e22 Mon Sep 17 00:00:00 2001 From: Ali Orhun Akkirman Date: Mon, 28 Sep 2026 14:50:03 +0300 Subject: [PATCH 4/4] Document safe PWA updates --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 03990f5..b6bd5ae 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ GitHub Pages sürümü installable PWA olarak yayınlanır. Manifest, uygulama i - `start_url` ve `scope` relative (`./`) tutulduğu için proje `/mathnote/` gibi GitHub Pages alt yollarında çalışır. - İlk başarılı ziyarette uygulama shell'i, JavaScript/CSS, KaTeX fontları, manifest ve ikonlar precache edilir. - `app.js`, `app.css` ve manifest gibi değişebilir çekirdek dosyalar ağ varken **network-first**, çevrimdışıyken cache fallback ile yüklenir. Bu, yeni deploy sonrasında eski JavaScript'in yanlışlıkla servis edilmesini önler. +- Yeni service worker çalışan sayfayı zorla devralmaz; bekleyen service worker eski sekmeler kapandıktan sonra aktive olur. Böylece eski HTML ile yeni JavaScript aynı sayfada karışmaz. - Yeni build'de dosya içeriğinden yeni bir cache sürümü üretilir; service worker aktive olduğunda eski MathNote cache'leri temizlenir. - Service worker yalnızca HTTP/HTTPS üzerinde kaydolur. Bu nedenle `dist/index.html` dosyasını doğrudan `file://` ile açma desteği korunur. - Destekleyen tarayıcılar MathNote'u masaüstüne veya ana ekrana bağımsız uygulama olarak kurabilir.