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:
mhoennig
2026-08-25 15:03:22 +02:00
co-authored by Claude Fable 5
parent f6785e3138
commit 19053eba1d
14 changed files with 251 additions and 17 deletions
+13
View File
@@ -554,6 +554,19 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Hash-Rückfall, der Marker-Pfad ist nur zu prüfen, wenn man einen echten
`commit/<sha>` in `index.html` einspritzt (danach zurücknehmen!) und
`window.fetch` überschreibt.
- **PWA (D73):** `public/sw.js` fasst NUR die Navigation zur App-Wurzel an
und beantwortet sie **network-first** — der Cache ist reiner
Offline-Rückfall. Wer daraus cache-first macht, bricht die Update-Prüfung
(D45): deren `fetch()` läuft nur deshalb ans echte Netz, und „Jetzt laden"
bekommt nur deshalb die frische Fassung. Keine Registrierung im Dev-Server
(`!import.meta.env.DEV` — sonst cacht der Worker die HMR-Seite). Manifest,
Icons und `sw.js` sind public/-Assets neben der einen Datei; **jedes neue
public/-Asset muss in BEIDE Deploy-Wege** (pages.yml und deploy-prod.sh
stellen die Site je von Hand zusammen) und ggf. in `scripts/prod.htaccess`
(Apache kennt `.webmanifest` nicht, D43-Falle). Der `launchQueue`-Empfänger
(Dateidoppelklick der installierten App) reicht Handles an `adoptFile()`
(D72) weiter — Installieren/Doppelklick sind nur auf echter Hardware
prüfbar.
- **Neuigkeiten (D58):** `NEWS` kommt aus dem virtuellen Modul
`virtual:werkbaum-news` (Vite-Plugin in `vite.config.js`), gefüllt zur
**Bauzeit** aus `docs/CHANGELOG.md` (die Notizen) und der git-Historie des
+7
View File
@@ -6,6 +6,13 @@
<title>Werkbaum PSP &amp; Lean Pathfinding Editor (Work Breakdown Structure)</title>
<meta name="description" content="Werkbaum ist ein textbasierter Editor für PSP/Lean Pathfinding und Projektstrukturpläne (WBS): Schreibe die Struktur als Text, das And/Or-Diagramm entsteht live. Auch für Feature-Trees &amp; Requirements.">
<link rel="icon" type="image/svg+xml" href="../docs/brand/favicon.svg">
<!-- PWA (D73): Manifest + Icons liegen als public/-Assets NEBEN der einen
self-contained Datei (nicht inlinebar — der Browser holt das Manifest
per URL). Relativer Pfad, damit Pages unter /werkbaum/ und die
Wurzel-Installation denselben Verweis nutzen; per file:// bleibt der
Link wirkungslos, die Datei selbst läuft unverändert. -->
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#243447">
</head>
<body>
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

+19
View File
@@ -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" }
}
+60
View File
@@ -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();
}
})());
});
+31
View File
@@ -4568,9 +4568,40 @@ try{ startLang = localStorage.getItem('werkbaum-lang') || detectLang(); }catch(_
applyLang(I18N[startLang] ? startLang : 'de'); /* setzt Texte + rendert */
initDocs(); /* Dokumente laden + aktiven Text in den Editor (nach Sprache) */
if(hasFsAccess) idbLoadHandles(); /* gemerkte Datei-Handles zurückholen (D72, Stufe 2) */
/* PWA-Dateihandling (D73): Die installierte App registriert sich über das
Manifest (`file_handlers`) für .werkbaum/.txt; ein Doppelklick im
Dateimanager reicht die Datei als Handle über die launchQueue herein.
`adoptFile()` (D72) übernimmt dieselbe Datei landet im selben Dokument,
und das Handle macht Als Datei speichern" dialogfrei. Außerhalb einer
installierten Chromium-App gibt es die launchQueue nicht: dann ist der
Block ein No-op. */
if('launchQueue' in window){
window.launchQueue.setConsumer(async params => {
for(const h of (params && params.files) || []){
try{
const text = await (await h.getFile()).text();
await adoptFile(h, h.name, text);
}catch(_){ /* nicht lesbar — still, wie beim Picker-Abbruch (D72) */ }
}
});
}
applyMobile(); /* Mobil-Verhalten (nach Sprache/Restore) anwenden */
loadRemoteSource(); /* ?sourceUrl= / ?etherpad= nachladen (asynchron, D23/D31) */
/* ---------- PWA: Service Worker (D73) ----------
Ein reiner Offline-Mantel (public/sw.js): Navigationen network-first, der
Cache hält nur die zuletzt gesehene Fassung die Update-Prüfung (D45)
bleibt dadurch unverändert wahr. NICHT im Dev-Server registrieren: dort
würde der Worker die HMR-Seite cachen; der Zweig fällt im Dev als toter
Code weg. Relativer Pfad, damit der Scope dem Auslieferungsort folgt
(Pages unter /werkbaum/, prod an der Wurzel). Auf file:// und http gibt es
keinen (nutzbaren) serviceWorker das Scheitern ist geschluckt, die App
läuft ohne. */
if(!import.meta.env.DEV && 'serviceWorker' in navigator){
navigator.serviceWorker.register('sw.js').catch(() => {});
}
/* ---------- Build-Hinweis (Vorschau/Dev + latest build") ----------
Kennzeichnet einen nicht-produktiven Build mit einem kleinen Symbol samt
Tooltip hinter dem Titel. Gesteuert per Vite-Env `VITE_BUILD_BADGE`; ohne