docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop. - Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht. Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben. /books/ mit X-Server-Now, /api/sync/ mit server_now. - Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert. Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat. Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle". - Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste Stelle, Liste und Snapshot). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
3.5 KiB
JavaScript
106 lines
3.5 KiB
JavaScript
/**
|
|
* diora service worker — caches the app shell for offline use.
|
|
*/
|
|
|
|
const CACHE = 'diora-v100';
|
|
const SHELL = [
|
|
'/static/css/app.css',
|
|
'/static/js/app.js',
|
|
'/static/js/podcasts-web.js',
|
|
'/static/js/home.js',
|
|
'/static/js/feeds.js',
|
|
'/static/podcasts/native-podcast.js',
|
|
'/static/podcasts/podcast-ui.css',
|
|
'/static/podcasts/podcasts-web.css',
|
|
'/static/manifest.json',
|
|
'/static/ambient/index.html',
|
|
'/static/fonts/SourceSerif4-normal-latin.woff2',
|
|
'/static/fonts/SourceSerif4-normal-latin-ext.woff2',
|
|
'/static/fonts/SourceSerif4-italic-latin.woff2',
|
|
'/static/fonts/SourceSerif4-italic-latin-ext.woff2',
|
|
];
|
|
|
|
// Install: pre-cache the app shell
|
|
self.addEventListener('install', function (event) {
|
|
event.waitUntil(
|
|
caches.open(CACHE).then(function (cache) {
|
|
return cache.addAll(SHELL);
|
|
})
|
|
);
|
|
// Activate immediately without waiting for old tabs to close
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate: remove stale caches from previous versions (also the old
|
|
// 'diora-podcast-v1' of episodes the web app used to download)
|
|
self.addEventListener('activate', function (event) {
|
|
event.waitUntil(
|
|
caches.keys().then(function (keys) {
|
|
return Promise.all(
|
|
keys.filter(function (key) { return key !== CACHE; })
|
|
.map(function (key) { return caches.delete(key); })
|
|
);
|
|
}).then(function () {
|
|
return self.clients.claim();
|
|
}).then(function () {
|
|
// Tell all open tabs to reload so they get the latest cached assets
|
|
return self.clients.matchAll({type: 'window'}).then(function (clients) {
|
|
clients.forEach(function (client) {
|
|
client.postMessage({type: 'SW_ACTIVATED'});
|
|
});
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Fetch: serve from cache, fall back to network
|
|
self.addEventListener('fetch', function (event) {
|
|
// Only handle GET requests; let POST/SSE etc. pass through
|
|
if (event.request.method !== 'GET') return;
|
|
|
|
const url = new URL(event.request.url);
|
|
|
|
// Bypass for API/mutation endpoints
|
|
if (url.pathname.startsWith('/radio/sse/') ||
|
|
url.pathname.startsWith('/radio/record/') ||
|
|
url.pathname.startsWith('/radio/affiliate/') ||
|
|
url.pathname.startsWith('/tts/') ||
|
|
url.pathname.startsWith('/admin/') ||
|
|
url.pathname.startsWith('/podcasts/progress/') ||
|
|
url.pathname.startsWith('/podcasts/queue/') ||
|
|
url.pathname.startsWith('/podcasts/player/') ||
|
|
url.pathname === '/podcasts/feeds/add' ||
|
|
url.pathname.startsWith('/podcasts/feeds/add') ||
|
|
url.pathname.includes('/remove') ||
|
|
url.pathname.startsWith('/podcasts/feeds/refresh')) {
|
|
return;
|
|
}
|
|
|
|
// Cache-first for pre-defined shell assets
|
|
const isShell = SHELL.some(function (s) { return url.pathname === s; });
|
|
if (isShell) {
|
|
event.respondWith(
|
|
// ignoreSearch: the Ambient frame loads with ?embed=1&bg=…
|
|
caches.match(event.request, {ignoreSearch: true}).then(function (cached) {
|
|
return cached || fetch(event.request);
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Navigation requests (the HTML page itself): network-first, fall back to
|
|
// last cached version so the app opens offline after being visited once.
|
|
if (event.request.mode === 'navigate') {
|
|
event.respondWith(
|
|
fetch(event.request).then(function (response) {
|
|
if (response.ok) {
|
|
var clone = response.clone();
|
|
caches.open(CACHE).then(function (cache) { cache.put(event.request, clone); });
|
|
}
|
|
return response;
|
|
}).catch(function () {
|
|
return caches.match(event.request);
|
|
})
|
|
);
|
|
}
|
|
});
|