From 072aece5eaf80696c65d64d01073b93bfe2319fb Mon Sep 17 00:00:00 2001 From: Ali Orhun Akkirman Date: Mon, 28 Sep 2026 14:36:54 +0300 Subject: [PATCH 1/2] Fix example loading and stale PWA assets --- README.md | 4 +++ index.html | 5 ++-- scripts/verify-offline.mjs | 15 ++++++++++ src/app.js | 17 ++++++++++++ src/sw.js | 56 +++++++++++++++++++++++++++++++------- 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6b06864..03990f5 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ Sağ üstteki **Export** düğmesiyle mevcut sekme içeriği şu biçimlerde dı ### Dil Desteği Sağ üstteki **EN / TR** geçişiyle arayüz dili değiştirilebilir. Tercih `localStorage`'a kaydedilir. +### Örnekleri yeniden yükleme +Üst bardaki **Örnekler** düğmesi güncel dildeki örnek içeriği yeni bir notebook olarak açar. Mevcut notebook'ları veya `localStorage` verisini silmez; böylece daha önce kullanıcı verisi kaydedilmiş olsa bile örnekler tekrar yüklenebilir. + ### Offline durum, yedekleme ve kayıt - Alt durum çubuğu tarayıcının çevrimiçi / çevrimdışı durumunu anlık gösterir. - **Yedek** düğmesi tüm notebook'ları, aktif sekmeyi ve temel görünüm tercihlerini tek bir sürümlü JSON dosyasında dışa aktarır. @@ -153,6 +156,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 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. diff --git a/index.html b/index.html index a5aa175..334bae5 100644 --- a/index.html +++ b/index.html @@ -66,7 +66,7 @@ border-radius: 6px; padding: 2px; } -#export-btn, #backup-btn, #info-btn { +#export-btn, #backup-btn, #examples-btn, #info-btn { padding: 4px 10px; border: 1px solid #ced4da; background: #fff; @@ -82,7 +82,7 @@ gap: 4px; white-space: nowrap; } -#export-btn:hover, #backup-btn:hover, #info-btn:hover { +#export-btn:hover, #backup-btn:hover, #examples-btn:hover, #info-btn:hover { background: #f1f3f5; border-color: #1A71C2; color: #1A71C2; @@ -968,6 +968,7 @@
+
diff --git a/scripts/verify-offline.mjs b/scripts/verify-offline.mjs index 97d3e4e..934a822 100644 --- a/scripts/verify-offline.mjs +++ b/scripts/verify-offline.mjs @@ -60,11 +60,19 @@ if (!html.includes('rel="manifest" href="./manifest.webmanifest"')) { violations.push('index.html: manifest link missing or not relative'); } +if (!html.includes('id="examples-btn"')) { + violations.push('index.html: examples button missing'); +} + const app = await readFile(new URL('app.js', root), 'utf8'); if (!app.includes("register('./sw.js'") && !app.includes('register("./sw.js"')) { violations.push('app.js: relative service worker registration missing'); } +if (!app.includes("getElementById('examples-btn')") || !app.includes('openExamplesNotebook')) { + violations.push('app.js: examples loader wiring missing'); +} + 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 ./'); @@ -81,6 +89,13 @@ if (sw.includes('__CACHE_NAME__') || sw.includes('__PRECACHE_ASSETS__')) { if (!/mathnote-[a-f0-9]{12}/.test(sw)) { violations.push('sw.js: content-derived cache version missing'); } + +if (!sw.includes('NETWORK_FIRST_PATHS') || !sw.includes('networkFirst(request')) { + violations.push('sw.js: mutable core assets are not network-first'); +} +if (sw.includes('caches.match(')) { + violations.push('sw.js: cross-cache lookup can serve stale assets'); +} for (const asset of ['./index.html', './app.js', './app.css', './manifest.webmanifest', './icons/icon-192.png', './icons/icon-512.png']) { if (!sw.includes(asset)) violations.push(`sw.js: precache entry missing for ${asset}`); } diff --git a/src/app.js b/src/app.js index 0133cc3..19e3100 100644 --- a/src/app.js +++ b/src/app.js @@ -19,6 +19,9 @@ const translations = { en: { // Top bar newFileBtn: 'New file', + examplesBtn: 'Examples', + examplesTitle: 'Load examples into a new notebook', + examplesFileName: 'examples.math', // Modal modalTitle: 'Create New File', @@ -208,6 +211,9 @@ const translations = { tr: { // Top bar newFileBtn: 'Yeni dosya', + examplesBtn: 'Örnekler', + examplesTitle: 'Örnekleri yeni bir not defterine yükle', + examplesFileName: 'ornekler.math', // Modal modalTitle: 'Yeni Dosya Oluştur', @@ -418,6 +424,8 @@ 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'); document.getElementById('zoom-in').title = t('zoomIn'); document.getElementById('zoom-out').title = t('zoomOut'); @@ -658,6 +666,15 @@ function newFile(initialContent) { input.select(); } +function openExamplesNotebook() { + newFile(getDefaultContent()); + const input = document.getElementById('new-file-input'); + input.value = t('examplesFileName'); + input.select(); +} + +document.getElementById('examples-btn').addEventListener('click', openExamplesNotebook); + function confirmNewFile() { const input = document.getElementById('new-file-input'); const name = input.value.trim(); diff --git a/src/sw.js b/src/sw.js index f3e3a55..d8de343 100644 --- a/src/sw.js +++ b/src/sw.js @@ -4,6 +4,45 @@ const PRECACHE_ASSETS = __PRECACHE_ASSETS__; const APP_SHELL = new URL('./index.html', self.registration.scope).toString(); const scopedUrl = path => new URL(path, self.registration.scope).toString(); +const NETWORK_FIRST_PATHS = new Set( + ['./app.js', './app.css', './manifest.webmanifest'].map( + path => new URL(path, self.registration.scope).pathname + ) +); + +async function networkFirst(request, fallbackUrl = null) { + try { + const response = await fetch(request); + if (!response || !response.ok) throw new Error('Network response was not OK'); + + const cache = await caches.open(CACHE_NAME); + await cache.put(request, response.clone()); + return response; + } catch { + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request, { ignoreSearch: true }); + if (cached) return cached; + + if (fallbackUrl) { + const fallback = await cache.match(fallbackUrl); + if (fallback) return fallback; + } + + return Response.error(); + } +} + +async function cacheFirst(request) { + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request, { ignoreSearch: true }); + if (cached) return cached; + + const response = await fetch(request); + if (response && response.ok) { + await cache.put(request, response.clone()); + } + return response; +} self.addEventListener('install', event => { event.waitUntil( @@ -37,17 +76,14 @@ self.addEventListener('fetch', event => { if (url.origin !== self.location.origin) return; if (request.mode === 'navigate') { - event.respondWith( - fetch(request) - .then(response => response) - .catch(() => caches.match(APP_SHELL)) - ); + event.respondWith(networkFirst(request, APP_SHELL)); return; } - event.respondWith( - caches - .match(request, { ignoreSearch: true }) - .then(cached => cached || fetch(request)) - ); + if (NETWORK_FIRST_PATHS.has(url.pathname)) { + event.respondWith(networkFirst(request)); + return; + } + + event.respondWith(cacheFirst(request)); }); From a6f3bf9ba3072e8f037f9799d479a5388798757e Mon Sep 17 00:00:00 2001 From: Ali Orhun Akkirman Date: Mon, 28 Sep 2026 14:39:54 +0300 Subject: [PATCH 2/2] Make examples bundle verification robust --- scripts/verify-offline.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-offline.mjs b/scripts/verify-offline.mjs index 934a822..15fcdd4 100644 --- a/scripts/verify-offline.mjs +++ b/scripts/verify-offline.mjs @@ -69,7 +69,7 @@ if (!app.includes("register('./sw.js'") && !app.includes('register("./sw.js"')) violations.push('app.js: relative service worker registration missing'); } -if (!app.includes("getElementById('examples-btn')") || !app.includes('openExamplesNotebook')) { +if (!app.includes('examples-btn') || !app.includes('getDefaultContent')) { violations.push('app.js: examples loader wiring missing'); }