Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions scripts/verify-offline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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 ./');
Expand All @@ -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');
}
Expand Down
63 changes: 38 additions & 25 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -440,16 +443,26 @@ function updateUILanguage() {
document.getElementById('export-confirm').textContent = t('exportConfirm');
document.getElementById('export-pdf-label').innerHTML = `PDF <small>${t('exportPdfHint')}</small>`;

// 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
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -834,7 +847,7 @@ function applyBackup(backup) {
}
}

document.getElementById('backup-modal').classList.remove('active');
document.getElementById('backup-modal')?.classList.remove('active');
alert(t('backupRestored'));
}

Expand Down Expand Up @@ -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);
});
Expand Down
5 changes: 2 additions & 3 deletions src/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -49,7 +50,6 @@ self.addEventListener('install', event => {
caches
.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE_ASSETS.map(scopedUrl)))
.then(() => self.skipWaiting())
);
});

Expand All @@ -64,7 +64,6 @@ self.addEventListener('activate', event => {
.map(key => caches.delete(key))
)
)
.then(() => self.clients.claim())
);
});

Expand Down
Loading