feat: Als PWA installierbar — Manifest, Offline-Worker (network-first), Dateihandling (D73)
Manifest mit Icons aus der Marke und Standalone-Fenster; ein bewusst dummer Service Worker beantwortet nur die App-Navigation network-first und hält die zuletzt gesehene Fassung für den Offline-Fall — die Update-Prüfung (D45) bleibt dadurch unverändert wahr, der skipWaiting-Lebenszyklus entfällt. Die installierte App registriert sich für .werkbaum-Dateien (file_handlers + launchQueue -> adoptFile). Beide Deploy-Wege kopieren die App-Hülle mit; .htaccess liefert .webmanifest mit MIME-Typ aus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f6785e3138
commit
19053eba1d
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Werkbaum",
|
||||
"short_name": "Werkbaum",
|
||||
"description": "A textual notation for work breakdown structures with and/or decomposition, and a live diagram editor.",
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#243447",
|
||||
"icons": [
|
||||
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
],
|
||||
"file_handlers": [
|
||||
{ "action": "./", "accept": { "text/plain": [".werkbaum", ".txt"] } }
|
||||
],
|
||||
"launch_handler": { "client_mode": "focus-existing" }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* Werkbaum — Service Worker (D73).
|
||||
*
|
||||
* Bewusst ein dummer Offline-Mantel, kein App-Verwalter: Navigationen gehen
|
||||
* NETWORK-FIRST (der Server ist die Quelle der Wahrheit, wie ohne Worker),
|
||||
* der Cache hält nur die zuletzt gesehene Fassung der einen self-contained
|
||||
* Datei (D19) für den Offline-Fall bereit. Drei Folgen, alle Absicht:
|
||||
*
|
||||
* - Die Update-Prüfung (D45) bleibt wahr: ihr fetch() ist keine Navigation
|
||||
* und läuft unangefasst ans Netz; „Jetzt laden" ist eine Navigation und
|
||||
* bekommt network-first die frische Fassung. Kein skipWaiting-Tanz,
|
||||
* keine zweite Update-Logik.
|
||||
* - ?sourceUrl=- und Pad-Abrufe (D23/D31) werden nie abgefangen — der
|
||||
* Worker fasst ausschließlich die Navigation zur App-Wurzel an; llms.md,
|
||||
* llms.txt und alles Fremde gehen unverändert durch.
|
||||
* - Diese Datei ändert sich praktisch nie: Die App kommt vom Server, nicht
|
||||
* aus dem Worker — es gibt keine Versionsnummer, die hier gepflegt werden
|
||||
* müsste.
|
||||
*/
|
||||
const CACHE = 'werkbaum-shell';
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
/* Die Shell sofort vorhalten, damit Offline schon nach dem ersten Besuch
|
||||
funktioniert (die erste Navigation lief noch ohne Worker). {cache:
|
||||
'reload'} umgeht den HTTP-Cache — vorgehalten wird, was der Server
|
||||
JETZT sagt. Scheitert das (Installation offline), bleibt der Cache
|
||||
leer und die nächste erfolgreiche Navigation füllt ihn. */
|
||||
e.waitUntil(
|
||||
caches.open(CACHE)
|
||||
.then(c => fetch('./', { cache: 'reload' })
|
||||
.then(r => { if (r.ok) return c.put('./', r); }))
|
||||
.catch(() => {})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', e => {
|
||||
e.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
/* Nur die Navigation zur App selbst; alles andere geht unangefasst durch. */
|
||||
if (e.request.mode !== 'navigate') return;
|
||||
const path = new URL(e.request.url).pathname;
|
||||
const scopePath = new URL(self.registration.scope).pathname;
|
||||
if (path !== scopePath && path !== scopePath + 'index.html') return;
|
||||
|
||||
e.respondWith((async () => {
|
||||
try {
|
||||
const r = await fetch(e.request);
|
||||
if (r.ok) {
|
||||
const copy = r.clone();
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.put('./', copy)));
|
||||
}
|
||||
return r;
|
||||
} catch (_) {
|
||||
const m = await caches.match('./');
|
||||
return m || Response.error();
|
||||
}
|
||||
})());
|
||||
});
|
||||
Reference in New Issue
Block a user