# ROADMAP — Baustelle PWA Stand: 2026-09-18 · Gegenstück: `data/bericht` (Modul-Version 1.7.1) Die PWA hat kein eigenes Backend — alles läuft über `custom/bericht/api/`. Punkte, die eine API-Änderung brauchen, sind mit **[API]** markiert und stehen zusätzlich in der ROADMAP des Bericht-Moduls. --- ## Offen - [ ] `idb.queueAll()` lädt **alle** Foto-Blobs in den Speicher — auch `updateBadge()`, das nur zählen will. Bei 20 Originalen à 8 MB sind das 160 MB je Badge-Update. Braucht ein Zähl-Verfahren ohne Werte (Schlüssel + kleiner Meta-Store); Schema-Änderung, weil `sw.js`/`share.html` die DB mit fester Version 1 öffnen. - [ ] **Am Handy gegenprüfen** (geht nur auf dem Gerät, am Schreibtisch nicht nachstellbar): echte Kamera-Hardware mit „Fertig" im Funkloch, und Background Sync bei ausgeschaltetem Display. Beides lief lokal nur mit nachgebildeter Kamera (`canvas.captureStream()`). Wenn etwas komisch aussieht: Tag + ungefähre Uhrzeit reichen, der Rest steht im Prod-Zugriffslog (Lesart siehe KB #208, Nachtrag 18.09.2026). **Bewusst nicht gebaut** (Entscheidungen, nicht vergessen): - Keine Schreib-Warteschlange für Auftragsdaten — bräuchte Idempotenz und Konfliktbehandlung (zwei Geräte am selben Auftrag). Lesen geht ohne Netz, ändern nur mit Netz; Fotos sind die Ausnahme (Persist-First-Queue). - Keine Zeichenfläche für echte Handschrift — am Handy ohne Stift unbrauchbar. Notizen werden getippt. - Kein Web Lock (`navigator.locks`) für die Foto-Queue — ein eingefrorener Tab hielte ihn ewig; deshalb Lease mit Ablauf in IndexedDB (Abschnitt 6.1). --- ## 6. Rauswurf aus dem Auftrag + unsichtbare Warteschlange (gemeldet 2026-09-18) Gemeldet von Eddy: am 17.09.2026 gegen 18 Uhr mehrere Fotos in einem Auftrag gemacht, „Fertig" getippt — die App stand danach auf der **Auftragsliste** statt im Auftrag. Wieder in den Auftrag gegangen: **keine Fotos zu sehen**, kein Hinweis, dass sie noch warten. ### Befund (Prod-Zugriffslog 17.09., Auftrag 111 — kein Raten) - 17:30:56 `POST orders.php?action=create` — Auftrag 111 über den ➕-Knopf angelegt. In derselben Sekunde laufen **zwei** Routen gleichzeitig: `orders.php` (Liste) **und** `orders.php?id=111` (Detail). Das ist die Signatur des Fehlers. - 17:40:45 `verify.php` + `orders.php` (Liste) — der Rauswurf. **Kein** `index.php` davor, also kein Reload, kein SW-Update, kein Logout (kein 401), kein Absturz. - 17:40:47 Auftrag 111 erneut geöffnet, `action=photos` liefert eine **leere** Liste; die Uploads kommen erst 17:40:54 und 17:57:25–17:58:22 an. **Ursache Rauswurf — History-Race in `openNewOrderModal`:** ```js closeModal(modal); // -> history.back() (asynchron!) router.go('#/orders/' + res.order.id); // -> location.hash = … (sofort) ``` `history.back()` wird erst nach dem Hash-Wechsel ausgeführt und springt dann **hinter** den neuen Eintrag zurück. Ergebnis: Auf dem Bildschirm steht der Auftrag (seine Route wurde als letzte fertig), in der Adresszeile steht aber `#/orders`. Alles, was danach `router.navigate()` ruft — z. B. „Fertig" in der Kamera — zeichnet die **Auftragsliste**. Nachgestellt in Chromium (18.09.2026): ohne Abwarten endet der Ablauf auf `#/orders`, mit abgewartetem `popstate` auf `#/orders/111`. Dieselbe Falle an zwei weiteren Stellen: „Neuen Bericht anlegen" (`#/reports/`) und Lieferschein-Unterschrift (`#/shipments/`). Betroffen ist nur, wer den Auftrag **in derselben Sitzung frisch angelegt** hat — deshalb fiel es nicht früher auf. **Ursache „keine Fotos, keine Meldung":** Die Auftragsseite zeigt ausschließlich, was der Server schon hat. Fotos in der Warteschlange erscheinen nirgends außer als Zahl im kleinen Badge oben rechts (🔴 3 / 🟡 3), und die Seite zeichnet sich nach einem im Hintergrund fertig gewordenen Upload nicht neu. Verloren ging nichts — alle Fotos kamen an. ### Umsetzung (2026-09-18) - [x] **Race zentral behoben:** `closeModal()` zählt selbst ausgelöste `history.back()` (`_pendingBacks`, Zähler statt Boolean, KB #1209 — der Boolean verschluckt bei zwei schnell nacheinander geschlossenen Dialogen ein `popstate`), `router.go()` wartet über `window.historySettled()`, bis alle durch sind, und wechselt erst dann den Hash (Schutzzeit 1,5 s, falls ein `popstate` ausbleibt). Alle Aufrufstellen bleiben wie sie sind. `closeModal()` nimmt den History-Eintrag jetzt **vor** dem Cleanup zurück, damit ein navigierender Cleanup den Rücksprung sieht; `pushModal()` wartet ebenfalls. - [x] **Sicherheitsnetz in Kamera und Galerie-Auswahl:** nach „Fertig" ausdrücklich `router.go('#/orders/')` statt `router.navigate()` — gezeichnet wird der Auftrag, für den fotografiert wurde, nicht „was gerade in der Adresszeile steht". - [x] **Wartende Fotos am Auftrag:** Block „⏳ Warten auf Upload (n)" über „Hochgeladene Fotos", mit Vorschaubildern direkt aus der Warteschlange (max. 12, danach „+n") und Klartext („📴 Kein Netz — 3 Fotos sind auf dem Gerät gesichert und werden gesendet, sobald wieder Netz da ist."), fehlgeschlagene rot markiert; Tipp öffnet die Warteschlange. - [x] **Auftragsseite zieht nach:** bei `photo-uploaded` für diesen Auftrag wird nach 2,5 s Ruhe neu gezeichnet (Scrollposition bleibt) — nicht, solange ein Modal offen ist, die Mehrfachauswahl läuft oder eine Sprachnotiz abgespielt wird. - [x] **Hinweis beim Schließen der Kamera:** „📴 Kein Netz — n Fotos auf dem Gerät gesichert, Upload folgt automatisch" bzw. „⏳ n Fotos warten noch auf den Upload". ### Verifiziert (2026-09-18, lokale Testinstanz, Chromium, Auftrag 62) - Ablauf „Modal zu + `router.go`": endet auf `#/orders/62` (vorher `#/orders`) - **Echtes Funkloch** (`context.setOffline(true)`): 3 Fotos gesichert, „Fertig" → bleibt im Auftrag, Offline-Band, Block mit 3 Vorschaubildern und Klartext, Badge 🔴 3 - Netz zurück: Upload läuft an, Block verschwindet von selbst, „Hochgeladene Fotos" 2 → 5, Badge 🟢, Warteschlange leer. Testfotos danach wieder entfernt. - **Nicht** getestet: die echte Kamera (`getUserMedia`) am Handy — der Test hat den Kamera-Cleanup nachgestellt, nicht die Kamera selbst bedient. ### 6.1 Doppel-Uploads: Seite und Service Worker laden dasselbe Foto hoch Log 17.09.: mehrere Antworten mit `duplicate:true`. `syncQueue()` (Seite) und `drainQueue()` (Service Worker, Background Sync) lesen dieselbe IndexedDB-Queue und wissen nichts voneinander. Dank md5-Abgleich (Bericht 1.6.0) folgenlos — kostet aber im Mobilfunk das doppelte Datenvolumen, bei 8-MB-Originalen spürbar. **Schlimmer, bisher unbemerkt:** Ist im Admin ein Verkleinern eingestellt, sichert die Kamera erst das Original und ersetzt es danach durch die kleine Fassung. Greift der Service Worker dazwischen zu, lädt er das **Original** hoch, die Seite danach die **kleine Fassung** — unterschiedlicher Inhalt, der md5-Abgleich greift nicht, es liegen **zwei Dateien** im Auftrag. Umgesetzt (2026-09-18) — Lease je Item in IndexedDB (kein Web Lock: ein eingefrorener Tab hielte den ewig, und genau für den eingefrorenen Tab gibt es den Background Sync): - [x] `idb.queueKeys()` / `queuePatch(id, fn)` / `queueClaim(id, owner, leaseMs)` / `queueRelease()` — Lesen-Ändern-Schreiben in **einer** readwrite-Transaktion (über Kontexte hinweg atomar). Gegenstück in `sw.js` (`queueClaim`/`queueRelease`) — **beide Stellen zusammen ändern**, der Worker kann `lib/idb.js` nicht laden (`window`). - [x] `syncQueue()` und `drainQueue()` holen sich jedes Item per Claim; ist es frisch vom anderen belegt, wird es übersprungen (im Worker zählt es als „offen", damit der Browser nachfasst). Lease 90 s (> längster Upload-Timeout 60 s), danach darf der andere übernehmen. Besitzer je Seiten-/Worker-Instanz eindeutig (`page-…` / `sw-…`). Nebeneffekt: es liegt nur noch **ein** Foto gleichzeitig im Speicher statt aller. - [x] Fehlschlag gibt die Lease sofort frei; Versuchszähler/Quarantäne per Patch statt „ganzes Item zurückschreiben" (überschrieb bisher eine parallele Änderung). Ebenso `retryFailed()`. - [x] `enqueuePhoto(…, {hold:true})` + `releaseHold()`: soll verkleinert werden, kommt das Foto **mit** Lease (`page-…:resize`) in die Queue; freigegeben wird im `finally` nach dem Verkleinern. Vergisst es jemand oder stirbt die Seite: Ablauf nach 90 s. - [x] `replaceQueuedBlob()` ersetzt per Patch ### 6.2 SW-Update lädt mitten in der Arbeit neu Beim Test beobachtet: `controllerchange` → `location.reload()` (index.php, Muster KB #201) feuerte, während drei Fotos in der Warteschlange lagen; der Browser zeigte dazu seinen eigenen „Seite verlassen?"-Dialog (`beforeunload`). War am 17.09. **nicht** die Ursache (kein Deploy, kein `index.php` im Log), trifft aber jeden, der nach einem Deploy arbeitet: offene Kamera weg, getippte Notiz weg, halbe Unterschrift weg. - [x] `window.appBusy()` in app.js: Modal offen, Mehrfachauswahl, laufender Upload (`offline.isSyncing()`), Eingabefeld im Fokus, laufende Sprachnotiz. **Nicht** beschäftigt: PIN-Sperre — dort ist der beste Moment zum Neuladen, sonst kommt die PIN-Abfrage zweimal. Suchfelder zählen nicht (behalten am Handy den Fokus, der Reload käme nie). - [x] index.php: Reload nur, wenn nicht beschäftigt; sonst alle 3 s und bei jedem Sichtbarkeitswechsel erneut prüfen - [x] Eigener Reload setzt `window.__selfReload` → `beforeunload` warnt dann nicht (die Fotos liegen in IndexedDB, ein Reload verliert nichts). Die Warnung beim echten Schließen bleibt. ### Verifiziert 6.1 + 6.2 (2026-09-18, lokale Testinstanz, Chromium, Auftrag 62) - **Echte Kamera-Oberfläche** mit nachgebildetem Kamerabild (`canvas.captureStream()` statt Hardware), offline: 3× Auslöser, „Fertig" → bleibt im Auftrag, Toast „📴 Kein Netz — 3 Fotos auf dem Gerät gesichert, Upload folgt automatisch", Block mit 3 Vorschaubildern, Badge 🔴 3 - **Gleichzeitiger Zugriff:** Netz an, Seite und Service Worker greifen parallel zu — der Worker lud Foto 4 und 6, die Seite Foto 5. Serverlog: **genau 3** `upload_photo`-POSTs für 3 Fotos, keine Antwort mit `duplicate:true` - **Verkleinern:** während des Verkleinerns ist das Item von `page-…:resize` belegt; auf dem Server liegt **eine** Datei mit 41.832 Bytes (die kleine Fassung), nicht das 208-KB-Original - **Fremde Lease:** frisch → wird respektiert (kein Upload, bleibt in der Queue); künstlich abgelaufen → wird übernommen und hochgeladen - **Reload:** `controllerchange` bei offenem Notiz-Dialog → 4,5 s kein Reload; Dialog zu → Reload nach 0,7 s, gleicher Auftrag, **kein Browser-Dialog**, obwohl ein Foto in der Warteschlange lag. Konsole ohne Fehler. Testdaten danach wieder entfernt. - **Nicht** getestet: echte Kamera-Hardware und echter Background Sync bei ausgeschaltetem Display am Handy — beides geht nur auf dem Gerät. --- ## Erledigt (gekürzt — Einzelheiten stehen im Commit und in der KB) | Wann | Was | Commit | KB | |---|---|---|---| | 2026-09-18 | Abschnitt 6 oben: Rauswurf nach „Fertig", wartende Fotos am Auftrag, keine Doppel-Uploads, Reload erst wenn nichts offen ist | `64ebbf7`, `30202f8` (deployt `292c251`) | #1444, #1445, #1446 | | 2026-08-31 | Offline lesbar statt „Failed to fetch": Netzfehler als `offline = true`, Datenspiegel in IndexedDB (Listen + 30 Details), Band „📴 Kein Netz — Stand von HH:MM", Suche im Spiegel, Spiegel weg beim Abmelden, SW liefert immer eine echte Response, nur vollständige Antworten cachen, Precache per `PRECACHE`-Nachricht nach Deploy | `a108664` | #1261, #1037, #1041 | | 2026-08-30 | Teilen aus WhatsApp: Anmeldung über `verify.php` statt totem JWT, Anmelden in der Teilen-Seite, Text/Titel/Link werden mitgesichert, Bilder über die Queue, Text als Notiz, Banner für Unabgelegtes, „Zuletzt benutzt" + 30-Minuten-Vorauswahl | `7076388`, `eaad796` | — | | 2026-08-28 | Foto-Serie übersteht Display-Aus: Persist-First streng (erst sichern, dann verkleinern), Schutzzeit im Verkleinern, Wake Lock, Background Sync, volle Kameraauflösung über `ImageCapture` | `65bc09d`, `130d9d1`, `c2bf4e7` | #840 | | 2026-08-22 | Aus Bericht 1.5.x nachgezogen **[API]**: echte Thumbnails (`photo.php?size=thumb`), Sortierung nach Aufnahmezeit (`taken_at`), Seiten-Titel, `composite_path` + Layout-Kennzeichen | `4a53ab0` | #1142, #1133 | | 2026-08-22 | Zehn Browser-Dialoge durch `confirmDialog`/`inputDialog`/`alertDialog` ersetzt; Bild-Cache `baustelle-media` (cache-first, LRU 300, überlebt Deploys, leer beim Abmelden); Textnotizen (`api/note.php`, `notiz_*.txt`) | `4a53ab0` | — | | 2026-08-22 | Nebenbei: `.modal-header`/`.close-btn`/`.modal-body` hatten gar kein CSS; Thumbnail-Laden überschrieb Auswahl-Häkchen und Seitennummer der Kachel | `4a53ab0` | — | | davor | SSO-Migration auf awlauth, WebAuthn-Login, Neuer-Auftrag-Formular, Live-Kamera mit Persist-First-Queue, Lieferschein-Unterschrift | siehe `git log` | #208, #841, #855 |