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
+3
View File
@@ -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
+87
View File
@@ -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).
+18 -17
View File
@@ -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