From 19053eba1d6da68e8c3b1ecc5fad8c456f3252e6 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Tue, 25 Aug 2026 15:03:22 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Als=20PWA=20installierbar=20=E2=80=94?= =?UTF-8?q?=20Manifest,=20Offline-Worker=20(network-first),=20Dateihandlin?= =?UTF-8?q?g=20(D73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/pages.yml | 5 ++ docs/CHANGELOG.md | 3 + docs/DECISIONS.md | 87 ++++++++++++++++++++++++++ docs/examples/werkbaum.werkbaum | 35 ++++++----- frontend/CLAUDE.md | 13 ++++ frontend/index.html | 7 +++ frontend/public/icon-192.png | Bin 0 -> 3324 bytes frontend/public/icon-512.png | Bin 0 -> 9061 bytes frontend/public/icon-maskable-512.png | Bin 0 -> 7545 bytes frontend/public/manifest.webmanifest | 19 ++++++ frontend/public/sw.js | 60 ++++++++++++++++++ frontend/src/app.js | 31 +++++++++ scripts/deploy-prod.sh | 5 ++ scripts/prod.htaccess | 3 + 14 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 frontend/public/icon-192.png create mode 100644 frontend/public/icon-512.png create mode 100644 frontend/public/icon-maskable-512.png create mode 100644 frontend/public/manifest.webmanifest create mode 100644 frontend/public/sw.js diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 51e0bf1..4cd5827 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -88,6 +88,11 @@ jobs: cp frontend/dist/llms.md site/llms.md # Wegweiser der llms.txt-Konvention (D43-Nachtrag 2) cp frontend/dist/llms.txt site/llms.txt + # PWA-Hülle (D73): Manifest + Icons + Service Worker — public/-Assets + # neben der einen Datei + cp frontend/dist/manifest.webmanifest site/manifest.webmanifest + cp frontend/dist/icon-192.png frontend/dist/icon-512.png frontend/dist/icon-maskable-512.png site/ + cp frontend/dist/sw.js site/sw.js - name: Pages-Konfiguration uses: actions/configure-pages@v6 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5aef216..974bbd3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,9 @@ reverse. ## 2026-08-25 +- The installed app registers for `.werkbaum` files — a double-click in the file manager opens them straight into the editor +- A service worker starts the editor offline; navigations stay network-first, so the update notice keeps telling the truth +- Werkbaum installs as an app: a web app manifest with standalone display and icons rendered from the brand mark - PWA file handling recorded in the plan — an installed app would open `.werkbaum` files on double-click and save without any dialog - In Chromium browsers, saving writes back to the opened file, and the same file reopens into the same document - Open a local `.werkbaum` file and save the document back as a file, from the document menu diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 66ecfff..e8d57ba 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -5389,3 +5389,90 @@ IndexedDB-Rundreise über einen Neustart: Stub-Handles überleben den Structured Clone nicht (`DataCloneError`, planmäßig geschluckt) — echte Handles sind gerade dafür klonbar; dieser eine Pfad ist Code-Review statt Messung. 480 Tests. + +## D73 — PWA: Manifest und ein bewusst dummer Offline-Worker, network-first +Werkbaum ist als App installierbar (`#bld.pwa`): Manifest mit Icons und +Standalone-Fenster, ein Service Worker für den Offline-Start, und die +installierte App registriert sich für `.werkbaum`-Dateien. Die tragende +Entscheidung ist die Rolle des Workers — und sie hat den gefürchteten Teil +des Features aufgelöst. + +**Der Worker ist ein Offline-Mantel, kein App-Verwalter.** Er fasst +ausschließlich die **Navigation zur App-Wurzel** an und beantwortet sie +**network-first**: Der Server bleibt die Quelle der Wahrheit, genau wie ohne +Worker; der Cache hält nur die zuletzt gesehene Fassung der einen +self-contained Datei (D19) für den Offline-Fall und wird bei jeder +erfolgreichen Navigation aufgefrischt. Alles andere — `?sourceUrl=`- und +Pad-Abrufe (D23/D31), `llms.md`/`llms.txt`, jeder `fetch()` — geht unangefasst +durch. + +**Damit blieb die geplante D45-Migration aus — und das ist ein Befund, keine +Abkürzung.** Der Plan-Knoten hieß „The reload notice moves into the worker", +in der Annahme, ein Worker entscheide, was ausgeliefert wird, und der +Vergleich „laufender Build gegen den, den der Server sendet" (D45) verliere +seine Grundlage. Das gilt für einen **cache-first** Worker — und genau +deshalb ist der verworfen: Er hätte den skipWaiting-/updatefound-Lebenszyklus +gebraucht (die Stelle, an der PWAs erfahrungsgemäß Fehler sammeln, und D45 +hat seine eigene Fehlergeschichte), eine zweite Update-Logik neben der +bestehenden, und jeden Nutzer bis dahin auf der zuerst installierten Fassung +festgenagelt. Network-first braucht nichts davon: Der Prüf-`fetch()` ist +keine Navigation und läuft ans echte Netz; „Jetzt laden" ist eine Navigation +und bekommt die frische Fassung. Beides gemessen, nicht angenommen (unten). +Der Knoten heißt jetzt „The reload notice stays truthful under the worker" — +die Arbeit war der Nachweis, nicht der Umzug. Der Preis von network-first ist +benannt: Der Start kostet online weiterhin einen Netz-Abruf (wie bisher auch) +statt sofort aus dem Cache zu kommen — für ein Produkt, das laufend deployt, +der richtige Tausch. + +**`sw.js` ändert sich praktisch nie.** Weil die App vom Server kommt und +nicht aus dem Worker, gibt es keine Versionsnummer, die dort gepflegt oder +von den Deploy-Skripten eingespritzt werden müsste — kein zweiter +`sed`-Stempel neben der Footer-Version (D16). + +**Aus „eine Datei" wird ehrlich „eine Datei plus App-Hülle".** Manifest, +Icons und Worker sind **nicht inlinebar** — der Browser holt sie per URL; +ein Worker braucht seine eigene Adresse. Sie liegen als `frontend/public/`- +Assets neben der Datei (der `llms.md`-Weg, D43) und werden von **beiden** +Deploy-Wegen mitkopiert (Pages-Workflow und `deploy-prod.sh` stellen die Site +je von Hand zusammen — dieselbe Doppelpflege wie bei den `sed`-Regeln, D16). +Die `file://`-Tauglichkeit der einen Datei bleibt: Ohne die Hülle fehlt nur +die Installierbarkeit, nicht die App. Die `.htaccess` bekommt den MIME-Typ +für `.webmanifest` — dieselbe Apache-Falle wie bei `.md` (D43-Nachtrag 2): +unbekannte Endung, kein Content-Type, Chromium verwirft das Manifest still. + +**Icons aus der Marke, eingecheckt.** Die Raster-Größen (192/512 plus eine +Maskable-Variante mit Schutzzone: Marke auf 60 % statt 78 % der Fläche) sind +einmalig per Inkscape aus `docs/brand/favicon.svg` gerendert und eingecheckt — +der Fonts-Präzedenzfall (D20): Assets im Repo, kein Werkzeug im Build. + +**Nicht im Dev-Server registriert.** Dort würde der Worker die HMR-Seite +cachen; der Zweig hängt an `!import.meta.env.DEV` und fällt im Dev als toter +Code weg. Auf `file://` und http ohne Secure Context gibt es keinen nutzbaren +`serviceWorker` — das Scheitern ist geschluckt, die App läuft ohne. + +**Dateihandling (`#bld.pwa.files`):** `file_handlers` im Manifest +(`.werkbaum`/`.txt`, dieselben Endungen wie `FILE_TYPES`, D72) plus ein +`launchQueue`-Empfänger, der das gereichte Handle an das vorhandene +`adoptFile()` gibt — dieselbe Datei landet damit im selben Dokument, und das +gemerkte Handle macht „Als Datei speichern" dialogfrei (D72-Nachtrag). +`launch_handler: focus-existing`, damit der Doppelklick ein offenes Fenster +wiederverwendet, statt Instanzen zu stapeln. Chromium only — Firefox +installiert auf dem Desktop nicht, Safari kennt `file_handlers` nicht; dort +ändert sich nichts. + +**Nachgemessen** am gebauten Stand (`vite preview`, dist auf localhost:8138): +Worker aktiv und `controller` gesetzt, Cache hält genau `./`. Dann ein Marker +in `dist/index.html` geschrieben: Der D45-artige `fetch(…, no-store)` **sieht +ihn sofort** (läuft also am Worker vorbei ans Netz), während die laufende +Seite ihn nicht hat; ein Reload **lädt ihn** (Navigation network-first) und +frischt den Cache mit auf. Server gestoppt, Reload: Die Seite kommt +vollständig aus dem Cache (18 Knoten gerendert, Marker enthalten). Manifest +parst mit `file_handlers` und `launch_handler`; `launchQueue` existiert und +der Consumer registriert sich fehlerfrei. Aufgeräumt per `unregister()` + +`caches.delete()`. 480 Tests unverändert grün. + +**Werkzeuggrenzen, wie bei D72 benannt:** Installieren, der OS-Doppelklick +auf eine `.werkbaum`-Datei und die persistente Schreibberechtigung der +installierten App sind Betriebssystem-Dialoge und bleiben ein Handtest auf +echter Hardware; gemessen ist alles bis an diese Kante (Manifest gültig, +Consumer registriert, `adoptFile()`-Weg seit D72 geprüft). diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index 507a525..4e46077 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -99,11 +99,11 @@ - [^] #bld.tests: Unit tests (S) - [^] #bld.pages: Published on every push (S) - [^] #bld.update: Says when a new build is out (S) %% compares against the running one, see D45 - - [ ] #bld.pwa: Installable as a progressive web app (M) - - [ ] #bld.pwa.manifest: A manifest with name, icons and standalone display (XS) - - [ ] #bld.pwa.sw: A service worker that serves the app offline (S) %% one file to cache - - [ ] #bld.pwa.update: The reload notice moves into the worker (S) %% see D45 - - [ ] #bld.pwa.files: Double-click a .werkbaum file, save without any dialog (S) :#ed.files.inplace %% file_handlers + launchQueue + - [x] #bld.pwa: Installable as a progressive web app (M) + - [x] #bld.pwa.manifest: A manifest with name, icons and standalone display (XS) + - [x] #bld.pwa.sw: A service worker that serves the app offline (S) %% network-first, cache as fallback + - [x] #bld.pwa.update: The reload notice stays truthful under the worker (S) %% see D45/D73 + - [x] #bld.pwa.files: Double-click a .werkbaum file, save without any dialog (S) :#ed.files.inplace %% file_handlers + launchQueue - [-] #bld.ghpages: Deploy through a gh-pages branch (S) %% GitHub Actions instead - [-] #bld.gfonts: Fonts from Google (XS) %% self-hosted, no third-party request - [ ] #be: Backend (XL) @@ -667,31 +667,32 @@ #bld.pwa Installable from the browser and startable like an app, in its own window - and with its own icon. Everything it needs is already in one file, so what - is missing is mostly the declaration that says so. + and with its own icon. Everything it needs was already in one file — what + was missing was mostly the declaration that says so. #bld.pwa.manifest Name, icons, theme colour and a standalone display mode — the declaration a - browser reads before it offers to install anything. The brand mark exists - and needs the raster sizes an installer asks for. + browser reads before it offers to install anything. The raster icons are + rendered from the brand mark, with a padded maskable variant. #bld.pwa.sw - A worker answering from the cache lets the editor start without a network. - There is exactly one object to cache, because the build inlines fonts, - styles and modules; documents live in the browser anyway. Texts pulled from - a URL or a pad stay online by their nature. + A worker lets the editor start without a network. It is deliberately dumb: + navigations go network-first, and the cache only keeps the last version it + has seen of the one self-contained file for the offline case. Texts pulled from a URL + or a pad pass through untouched and stay online by their nature. #bld.pwa.update - With a worker in front of the page, the reload notice has to come from it: - it decides what gets served, so "the running build against the one the - server sends" is no longer the same comparison. + The feared migration turned out unnecessary: because the worker only + touches navigations and answers them network-first, the update check still + fetches the real server and a reload really loads the new build. The work + here was proving that, not moving code. #bld.pwa.files The installed app registers for the .werkbaum extension: double-clicking a file in the OS opens it here, with a ready file handle from the launch queue. Together with the persistent write permission an installed app can hold, editing becomes open, type, save — no dialog at all. The saving - mechanics already exist; what is missing is the manifest entry and the + mechanics existed already; this added the manifest entry and the launch-queue receiver. Chromium only. #bld.ghpages diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index ad3a41f..f2f82f7 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -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/` 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 diff --git a/frontend/index.html b/frontend/index.html index c31c4e7..2dc86d5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,6 +6,13 @@ Werkbaum – PSP & Lean Pathfinding Editor (Work Breakdown Structure) + + + diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..8feb6e6a24b2db0e357625c55a27a8924f17674b GIT binary patch literal 3324 zcmb_fXHb*t68-{05CaHGkuF6PDN>{c1nEdGq7+3V0@8a81P&647!>q?N|$2j=m886 zO6VOa0YXQl84^Hx$&F|3k2`bkuRG_*zWbJEo|)a*-FG+M%tVigk(&_!044){Z40Vy z`YjCf)YW3j9ZXecz4h<=0f2L2vT*9iy;3X*d5bn}Bf z@^+H)@^yZ{0p$h&NW6izrd99<;&h0=)e>L(rn4spTa~7MZ#P1Sf%UvZ_L&$#K|5_9 zT5T4q=T%ndT+kMtELc zQ)5Ew$yf&mbDcuNu55JkqeCy+IRe04_?B`eeBKM)Q8AJ@p#Cz9#8nUqS!xBFnFTYz z1k#@u6kHk2yC*Ioak1E!BiHUv6SSVW;qhk;>4r?K+Rho|^VS|u`(brg10R0T0*tZIeIo{H_OLPf0r%YFVvWjqA*V*CxQEf);WCV87NlDL z`KghGTCA*Te=tK%PL(w4WfcJE#7dr9B=P5mLVApA#dw5cr*i-H9RNgz`f_j7`PHt; zNXU8i>O)oC&}%a8n#VgJsogH;ulH)7D4S$STB{{ovq&j#DY=w#BXqwMtyjrJPri^D zOYc5eeIdjS+p@V}d9}Q#C8S(&E?^+60nvz@oY#os*q2^DZ>;rx|E>$k2v=G&6+;b; z^ZfiAWK7>#+KSFOnMU2suHlI3F>2II-6W+!SwY7(WpNF1SIR5P8d7QrHUR z$e?~k=--L(CHs&kyR3{q$=LYTQBWkMR%DTiw5oH9*d;y6$%%Ub$!uhaUb~+iKb+OP zn-OM>(){z)L>+j>LH6MeSHz;0;D6+ot(n@nk2ZKIK5NvV1n>TC+#0LVusbcokbKGg zP0X6iyXXCiN%G%YfxuRzhRC}rJjk#|Jh@I;;E0hWrzDq6G=Iq(KXN)-{%J4xc&a(7 z+BGU`PfmL{d9Z+;?ZU#V*Qs&6>S{wZHY|jU2d%>+F@1V0!X)zL55Cs9twdedBtml5 z)NGatZBt{Nfp#K?zEND`Pmcv5;a6-t$l}M3ePEaiI5~`v@)dcIGrNeNhJ2E(?Si(NTaU$lcDo9@$1b!JY%rw=98k$Bjg_qO1#X+=+X;v zU`RXBp$<3ieXpj*8We8neO_9^J|VhA>#n;SU;z&5(zPCcqn*}I_7akV9+cK2DK z=nn6fo0rwIX0fXc=>WTo*71bJ;wnieuhRV1ulqiEFa^@0i53>qVk+}Pv;sh z)vF6UjrhVAki=9_;$;Z*+_E2bAE+dus5UGdS)OK`ofK$$!`?YjIa;w3-IxB1^Q|Yr zy?ZhiY-U4hu41RihfZXF8y}CUfk5fYf56Y0%E9UT9!5^5B_6ix;_x@22+4yw=>v0R z*Rf@fn?GB)vp6`oZkT%)ql??xg%%%b$sYH4hrHh|Us<=UNzn1PN^KtI%KP|*Ht4( z1%-MakZdsHyD&W&j~%d*x%tK(-d$Y9eV;d0{mfPux7|o8tu%D4-LvwW{a#X)+nYm4}Hgolxu8S@=PgmN8tKGYZVcZ z$1cl|Mz2}#Q)sta5z#!H0rRGR-uEwW=U`_jiDud>Q2sydwK|Z5bt5I8)QM*SnVwf< zp;Ln=OiY3~N!Wp)h`;Iti~0pH&Xk&>VO9-`TW?1||mg%1B z!xz}p*Yk~_$FO6{VDM1o<9h5*xUk+0<2S|Jem@#>*4@}HOrELtvM9k)$YNt9x=zHU z^1q&PMji|-W2Cl*Fiv-HrhAX~<11AHT#yc7=T5LEJYJCqhcFgLm%CTk0#4~GZ^JBw zUvg3qZ`{VzB~;_ok4l9BUz&SV4@Ljxd=9gdP4-Li)AuI9TkLw@4L<^pt|_&?vR)WT0(?zgJD z5sBjjkvtUhWHFbp1@i+mOnu$*peMFplUaq% zik*%1iF1k{^LeTtRQM?Irl~FjT8=4*BBU}@=5`;oPlKZ4aq~?-I|{iDX*K+mZUnWu z+>sXa5QBy7-bQ_?E-3MsyxzQ>8FDd0LKa`i1OxUwt~P6}$M-)Atl>7?;qFydWkl z4kg=+M8JfTmkMmqTG>$hS) z$pvJo*-BfMU!>$c3u4NAx6Qa!Z9lP;@>3#LN9pM;1-iZ26pvMGPoR6V0KW9*Qi~5d zsdpt-vBRX|yA|o}8FC)+A*neUKhxfS4lprnm($Xn>)#kt>-{Vb%(XbBR!5#NDY#EYP4ykb+Us_TQz@A6LG8&= zaG7wjWc61}U(k}%KK``fo=vSlUE{5sb!*tD#iH$_C%QH-Fj!Uk2ZXLIH{N^vO>N=o z0^Qx$eX76kiaITkgMS{ZL#3qXZA4pzMX#}%OA_I9Z@E!mv0`zbF$WKVVw+CPb0K8~ z=B`Q~{e+k1hri0c0dZD~VlDqvBmIwe=Kn=VjQnbs2;}O^uGY> C-YmWV literal 0 HcmV?d00001 diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..efdad7f12a953e3f6527aa7bd40dd033684350c5 GIT binary patch literal 9061 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 OA#HU7welN|_x}Tm7y)Vk literal 0 HcmV?d00001 diff --git a/frontend/public/icon-maskable-512.png b/frontend/public/icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..3303e5e636f9bf7dcbf27948dc9d2ee74d4a32dd GIT binary patch literal 7545 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 literal 0 HcmV?d00001 diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..3c7125c --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -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" } +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..85b0e52 --- /dev/null +++ b/frontend/public/sw.js @@ -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(); + } + })()); +}); diff --git a/frontend/src/app.js b/frontend/src/app.js index e41c028..61e8689 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -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 diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh index 14017c3..1475cef 100755 --- a/scripts/deploy-prod.sh +++ b/scripts/deploy-prod.sh @@ -138,6 +138,11 @@ cp frontend/dist/llms.md "$STAGE/llms.md" # llms.md, SPEC und Repo zeigt. Nur er liegt an der Adresse, die Agenten von # selbst probieren. cp frontend/dist/llms.txt "$STAGE/llms.txt" +# PWA-Hülle (D73): Manifest + Icons + Service Worker — public/-Assets neben +# der einen Datei +cp frontend/dist/manifest.webmanifest "$STAGE/manifest.webmanifest" +cp frontend/dist/icon-192.png frontend/dist/icon-512.png frontend/dist/icon-maskable-512.png "$STAGE/" +cp frontend/dist/sw.js "$STAGE/sw.js" # Ohne diese Zuordnung liefert Apache `.md` ohne Content-Type aus und der # Browser rät windows-1252 (D43-Nachtrag 2). Pages braucht sie nicht. cp scripts/prod.htaccess "$STAGE/.htaccess" diff --git a/scripts/prod.htaccess b/scripts/prod.htaccess index a31633d..d1a7436 100644 --- a/scripts/prod.htaccess +++ b/scripts/prod.htaccess @@ -20,4 +20,7 @@ AddType text/markdown;charset=utf-8 .md AddType text/plain;charset=utf-8 .txt AddType text/plain;charset=utf-8 .werkbaum + # PWA-Manifest (D73): ohne Zuordnung liefert Apache die unbekannte Endung + # ohne Content-Type aus, und Chromium verwirft das Manifest dann still. + AddType application/manifest+json .webmanifest