# ROADMAP — Baustelle PWA Stand: 2026-10-21 · Gegenstück: `data/bericht` (Modul-Version 1.9.0) 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 - [x] ✅ 21.10.2026 **[API]** Auftragsbeschreibung in der App bearbeiten (Eddy: Merkzettel lieber in der Beschreibung als als extra Notiz-Datei). Bericht 1.9.0 `set_beschreibung` mit Konfliktschutz (409). Steht auch auf dem Stundenzettel-PDF — Eddy hat das so gewählt. Erst Bericht ausliefern, dann die App. - [x] ✅ 19.10.2026 (ohne Schema-Änderung): `idb.queueAll()` lud **alle** Foto-Daten in den Speicher, weil sie als ArrayBuffer gesichert waren. Neue Einträge tragen den Blob selbst — Chrome legt ihn als Datei ab und liest ihn erst beim Hochladen; `getAll()` liefert nur den Verweis. Alte ArrayBuffer-Einträge laufen wie bisher ab. - [x] ✅ 19.10.2026 **im Android-Emulator nachgeholt** (echter Chrome/Camera2-Weg, siehe Punkt 10): Kamera + „Fertig" im Funkloch, Hochladen bei ausgeschaltetem Display, Zwei-Finger-Zoom im PDF-Betrachter. Was nur das echte Gerät zeigt (Blitzhelligkeit, Fokus-Treiber, Video-Auflösung), steht bei Punkt 9/11. Wenn etwas komisch aussieht: Tag + ungefähre Uhrzeit reichen, der Rest steht im Prod-Zugriffslog (Lesart KB #208). **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). --- ## 10. Kamera vernünftig + offene Handy-Prüfungen im Emulator (Eddy 19.10.2026: „mach das vernünftig") Anlass: Nach der Blitz/Fokus-Gegenprobe (Punkt 9) meldet Chrome außerdem Zoom 1–10× und Belichtungskorrektur ±3. Der Emulator läuft jetzt (SVM an) — die bisher „nur am Handy" prüfbaren Punkte gehen dort mit echtem Android-Chrome. - [x] ✅ 19.10.2026 **Zoom mit zwei Fingern im Sucher** (`applyConstraints({advanced:[{zoom}]})`, nur wenn `caps.zoom` gemeldet) + Anzeige „1×"/„2,5×" unten im Sucher (gelb, sobald gezoomt), Antippen der Anzeige → zurück auf 1×. Während der Geste immer nur ein Aufruf an die Kamera, Zwischenwerte verfallen; Ende der Geste löst keinen Fokus-Tipp aus. Im Emulator mit **echter Zwei-Finger-Geste** (Emulator-Konsole → Touchscreen, siehe unten) geprüft: Fingerabstand ×2,87 → 2,9×, zusammen → 1×, kein Fokus-Aufruf/Klick während der Gesten; Foto bei 2,5× zeigt genau den Sucher-Ausschnitt. Chrome 153 zoomt über `control.zoomRatio` (nicht Ausschnitt) — die Fokus-/Belichtungs- bereiche gelten dann laut AOSP-Definition relativ zum gezoomten Bild, die Tippstelle passt also auch im Zoom. - [x] ✅ Belichtung: **kein eigener Regler** — Antippen setzt bei Chrome Fokus-, Belichtungs- und Weißabgleich-Bereich gemeinsam; im Emulator gemessen: `aeRegions` = `afRegions` = `awbRegions` = Tippstelle → Display/Typenschild antippen = dort scharf UND richtig belichtet. - [x] ✅ 19.10.2026 Emulator, **echte Kamera + „Fertig" im Funkloch** (Flugmodus + `adb reverse --remove-all`, Server nachweislich unerreichbar): 3 Fotos → Warteschlange 3, nach „Fertig" bleibt die App im Auftrag (Fehler aus Punkt 6 bestätigt behoben), Band „Kein Netz — Stand von 23:20 Uhr", 🔴 3, Kasten „Warten auf Upload (3)". - [x] ✅ 19.10.2026 Emulator, **Hochladen bei ausgeschaltetem Display**: Display aus (`keyevent 223`), dann Netz an 23:23:08 → Server bekam die 3 Fotos 23:26:29–30, Display schlief noch (`mWakefulness=Asleep`). Genau +3, keine Dubletten. Chrome weckt sich dafür über einen Android-Hintergrundjob — **gut 3 Minuten Verzögerung sind normal**. - [x] ✅ 19.10.2026 Emulator, **10 Fotos à 8,5 MB (81 MB) auf einmal aus der Galerie** (Android-Fotoauswahl): alle 10 angekommen, Server genau +10, kein „permission"-Fehler (Fix aus Punkt 7 bestätigt). - [x] ✅ 19.10.2026 Emulator, **Zwei-Finger-Zoom im PDF-Betrachter** mit echter Mehrfinger-Geste (Konsole, s. u.): Fingerabstand 200 → 500 px ergibt 100 % → 250 %, Text scharf neu gezeichnet, die Seite selbst zoomt nicht mit. **Echte Zwei-Finger-Geste im Emulator** (ohne Root, Betriebssystem-Ebene): Emulator-Konsole `telnet localhost 5554`, `auth <~/.emulator_console_auth_token>`, dann `event send EV_KEY:BTN_TOUCH:1 EV_ABS:ABS_MT_SLOT:0 EV_ABS:ABS_MT_TRACKING_ID: EV_ABS:ABS_MT_POSITION_X:<0..32767> … EV_SYN:0:0` (EV_SYN nur als Zahl; `BTN_TOUCH` und frische Tracking-IDs sind Pflicht, sonst meldet Android keine Berührung bzw. alte Finger „hängen"). Skript: KB-Eintrag zur Baustellen-Kamera. ## 12. Farbige Symbole statt Emojis (Eddy 19./20.10.2026: „so dass es gut aussieht", dann zu reinem Weiß: „viel zu schlicht") Anlass: der 🔄-Wechselknopf erschien als blaues Kästchen („den hässlichen Wechselbutton lässt du da doch nicht"). - [x] ✅ 20.10.2026: Alle Knöpfe mit Symbol nutzen `icon()` (app.js, Material Icons, `fill=currentColor`): Kopfzeilen der Ansichten (Teilen, Zeichnen, Löschen, Download), Auswahlleiste (Alle, Teilen), Kopfzeile oben (Installieren, Hilfe statt rotem ❓), Blitz-Knopf (Aus/Auto/An/Licht als Blitz-Symbole), die großen Knöpfe am Auftrag (Kamera, Galerie, Mikrofon, Notizen, Liste, Bericht, Lieferung), Einstellungen, Unterschrift, Warteschlange, Telefonnummer (jetzt lesbar blau statt dunkelblau unterstrichen). - [x] ✅ 20.10.2026 nach „viel zu schlicht": **Farbe zurück, ohne Emojis** — große Knöpfe tragen ein weißes Symbol im farbigen runden Feld (Kamera blau, Galerie grün, Sprachnotiz rot, Notizen gelb, Material orange, Bericht lila, Lieferungen türkis …), kleine Knöpfe ein farbiges Symbol (Teilen blau, Löschen rot, Stift/Blitz/Hilfe gelb, Download/Telefon grün). Farben je Symbol in `app.css` (`.ikon-`: `--ikon-farbe`, `--ikon-grund`). - [x] ✅ 20.10.2026 (Eddy: „die Symbole alle linksbündig"): große Knöpfe mit Symbol **linksbündig** — die farbigen Kreise stehen in einer Spalte, der Text beginnt immer an derselben Stelle (liest sich wie eine Liste). Gilt für alle `.btn` mit Symbol; nur der kleine „Auswählen"-Knopf bleibt mittig. - [x] ✅ Untere Leiste wie in Handy-Apps: Symbol über dem Wort, farbig (Heute als Sonne), aktive Seite blau; das Zahnrad heißt jetzt „Einstellungen". - [x] ✅ Titel der Vollbild-Ansichten und Abschnitts-Überschriften ohne Emoji (reiner Text). - Bewusst gelassen: Zustandsanzeige 🟢/🟡/🔴 (Farbe ist dort die Aussage), Stempel im Skizzen-Editor (werden so ins Bild gezeichnet), Bilder in leeren Listen, Emojis in Meldungen. - [x] ✅ 20.10.2026 (Eddy: „ja mach die Leiste unten mal fest"): Die untere Leiste stand seit v1.0.0 am **Seitenende** (`#app` nur `min-height`, die Seite wächst mit) — in einem Auftrag unter allen Fotos und Dokumenten, Eddy kannte sie gar nicht. Jetzt `position: sticky; bottom: 0` wie die Kopfzeile oben, feste Höhe `--nav-h: 56px`; „Geteiltes ablegen" klebt genau darüber (`bottom: var(--nav-h)`), der FAB (fixed 68 px) darüber. Im Emulator gemessen: Leiste 729–786 bei 785 px Fensterhöhe, oben wie gescrollt; Banner 688–730, FAB 618–674 — nichts überlappt. Banner mit Teilen-Symbol statt 📤 und „2 Fotos und 1 Video" statt „3 Bilder". ## 11. Videos aufnehmen (gewünscht 2026-10-19) Eddy: *„Bei Fotos aufnehmen auch Videos unterstützen bitte"*. - [x] ✅ 19.10.2026 **Kamera:** Umschalter „Foto | Video" über dem Auslöser, roter Aufnahmeknopf (läuft: rotes Quadrat), Laufzeit oben „● 0:12 / 5:00", Ton mit (Mikrofon erst beim ersten Video angefragt; verweigert → stumm), Licht (Blitz-Knopf zeigt beim Filmen nur Aus/Licht), Zoom und Antippen-Fokus auch beim Filmen. MP4 (H.264/AAC), sonst WebM; 5 Mbit/s ≈ 38 MB/min; Höchstlänge 5 min, kürzer, wenn der Server weniger annimmt (`config.php upload_max`: lokal 100 MB → 2:19). Schließen (✕/Fertig/Zurück) oder App-Wechsel während der Aufnahme **sichert** das Video. - [x] ✅ Warteschlange: Videos laufen wie Fotos (Persist-First, `type` bleibt `'photo'`, sonst löschten ältere Fassungen den Eintrag). Gesichert wird jetzt der **Blob** statt eines ArrayBuffers (auch bei Fotos) — damit erledigt sich auch „`queueAll()` lädt alle Blobs" für neue Einträge. Upload-Zeitlimit und Lease wachsen mit der Größe (45 s + 1 s je 100 KB; gleiche Formel in `api.js` und `sw.js`, Lease steht im Eintrag `lease_ms`). 413 (zu groß) → sofort Quarantäne, auch im Service Worker. - [x] ✅ **[API]** Bericht 1.8.0: `orders.php` nimmt mp4/m4v/mov/webm (Inhalt geprüft: `ftyp` ohne Bildmarke bzw. EBML), Name `video_…`, `taken_at` → Dateidatum; Aufnahmezeit sonst aus dem MP4 (`mvhd`); `photo.php` mit Range (206); `config.php` meldet `upload_max`; zu groß → 413 mit Meldung. Bericht-Editor: 🎬-Kachel, Übernahme in einen Bericht lehnt `add_attachment.php` ab (nur Bild/PDF) — das PDF kann daran nicht scheitern. - [x] ✅ Auftrag: eigener Abschnitt „🎬 Videos (n)" mit erstem Bild, Datum, Größe; Antippen = Player (spult dank Range), 📤 Teilen, 🗑 Löschen; Mehrfachauswahl/Teilen schließt Videos ein. „Warten auf Upload" zeigt Videos mit erstem Bild aus dem Gerätespeicher und zählt „2 Fotos und 1 Video". - [x] ✅ Galerie und „Teilen" nehmen Videos; ein Video über `upload_max` wird gleich mit Meldung abgelehnt. - [x] ✅ Im Emulator geprüft: Aufnahme mit Ton, Zoom und Licht → MP4 H.264/AAC auf dem Server; im Funkloch filmen + Kamera mitten in der Aufnahme schließen → gesichert, nach Netz-Rückkehr hochgeladen, nach Aufnahmezeit einsortiert; Galerie: 1,9-MB-Video hoch, 124-MB-Video mit Meldung abgelehnt; Player spielt (206), Löschen geht. - [ ] **Am Handy beim nächsten Einsatz:** Auflösung/Bildrate eines echten Videos (der Sucher fordert bis 4K an; bei 4K verteilt sich die 5-Mbit/s-Rate auf mehr Pixel) — Wert an der ersten Datei auf Prod ablesen (ffprobe). ## 8. Knopf „Geteiltes ablegen" bleibt dauerhaft stehen (gemeldet 2026-10-19) Eddy: *„bei der App Baustelle ist permanent ein Button relativ weit unten angezeigt über den aufgelisteten Aufträgen: Geteiltes ablegen"*. - Ursache: `checkPendingShare()` zeigte den Knopf, sobald IRGENDEIN `shared_payload` in IndexedDB lag — auch ein leeres (Teilen abgebrochen, nichts mitgekommen). `share.html` meldete dann „Es kam nichts an" und ließ den Rest liegen → Knopf für immer. Wer etwas Geteiltes nicht ablegen wollte, kam ebenfalls nicht davon los. - [x] ✅ 19.10.2026: Leere Pakete räumt die App selbst weg (kein Knopf); „Es kam nichts an" löscht den Rest; Knopf zeigt „(Notiz)", wenn nur Text geteilt wurde; `share.html` hat „Verwerfen" mit Rückfrage in der Seite. Auf der Testinstanz durchgespielt (leer → kein Knopf; Notiz → Knopf → Verwerfen → weg). - [x] ✅ 19.10.2026 (Eddy: „unten am Rand, nicht auf den Aufträgen"): Knopf ist jetzt eine Leiste in der App-Spalte direkt über der Navigation (`position: sticky; bottom: 0`) statt fest schwebend; der FAB rückt eine Leistenhöhe hoch. Auf 412 × 860 gemessen: kurze Liste → Leiste über der Navigation, nichts verdeckt; lange Liste → Leiste klebt am unteren Rand, am Ende lässt sich alles darüber scrollen. - [ ] Am Handy gegenprüfen: Knopf ist nach dem Update weg (beim ersten Laden räumt die App den leeren Rest ab). ### Befunde aus der AVIF-Prüfung in Bericht (19.10.2026) - [x] ✅ 19.10.2026: Ist eine Maximalgröße für Fotos gesetzt (`photo_maxside` > 0), behielt das im Browser verkleinerte JPEG seinen Originalnamen — ein `.avif`/`.webp` lehnte der Server ab. Jetzt zieht `offline.replaceQueuedBlob()` die Endung auf `.jpg` nach (gilt für Galerie, Teilen und Kamera). - [x] ✅ 19.10.2026: Beim Verkleinern im Browser (Canvas → JPEG) wurde Transparenz schwarz — jetzt weißer Grund. Lokal geprüft: Queue-Eintrag „bild.avif" (PNG mit Transparenz) → „bild.jpg", Ecke weiß. ## 9. Kamera: Blitz und Fokus einstellbar (gemeldet 2026-10-19) Eddy: *„beim Fotografieren kann man den Blitz nicht einstellen und den Fokus auch nicht"*. - [x] ✅ 19.10.2026: Blitz-Knopf in der Kopfzeile, schaltet reihum durch, was das Gerät meldet: Aus → Auto → An (`ImageCapture.getPhotoCapabilities().fillLightMode`, wirkt beim Auslösen über `takePhoto({fillLightMode})`; klappt das nicht, wird ohne Einstellung ausgelöst) → 🔦 Licht (Dauerlicht, `track.getCapabilities().torch`, nach dem Auslösen erneut gesetzt). Meldet das Gerät nichts davon, gibt es keinen Knopf. Auswahl bleibt gemerkt (`localStorage baustelle.kamera.blitz`). - [x] ✅ 19.10.2026: Antippen stellt dort scharf (`pointsOfInterest` + `focusMode`), Ring an der Stelle, einmaliger Hinweis „Antippen stellt dort scharf". - [x] Lokal geprüft mit nachgebildeter Kamera (Canvas-Strom + gemeldete Fähigkeiten): Reihenfolge der Modi, Licht an/aus, `takePhoto({fillLightMode:"flash"})`, Antippstelle richtig umgerechnet (eingepasste Vorschau, 0,803 statt 0,75 bei seitlichem Rand); ohne Fähigkeiten kein Knopf, keine Wirkung. - [x] ✅ 19.10.2026 abends: **Gegenprobe im Android-Emulator** (AVD `fluke`, Chrome 153, emulierte Rückkamera mit Blitz + AF-Bereich) — echter Camera2-Weg, gemessen per `adb shell dumpsys media.camera` („Last request sent"): | Einstellung | was bei der Kamera ankommt | Ergebnis | |---|---|---| | ⚡ Aus | `aeMode ON`, `flash.mode OFF` | ✓ | | ⚡ Auto, Auslösen | Standbild `aeMode ON_AUTO_FLASH` | ✓ Kamera entscheidet | | ⚡ An, Auslösen | Standbild `aeMode ON_ALWAYS_FLASH`, `flash.mode SINGLE` | ✓ wie Kamera-Apps | | 🔦 Licht | `flash.mode TORCH`, bleibt nach dem Auslösen an | ✓ | | Antippen (alt) | `afRegions` an falscher Stelle, `afMode` bleibt `CONTINUOUS_PICTURE` | ✗ zwei Fehler | **Fehler 1 — Tippstelle ungedreht:** Chrome rechnet `pointsOfInterest` direkt auf die Sensorfläche um (`VideoCaptureCamera2.java`, keine Drehung), das Sucherbild kommt aber gedreht an (Sensor quer, Bild hochkant). Tipp bei 67 % / 82 % → Bereich bei 67 % / 82 % des quer liegenden Sensors statt 82 % / 33 %. Jetzt dreht `sensorPunkt()` zurück (Rückkamera: Sensor 90° − Bildschirmwinkel; Nothing A059 hat 90°, siehe takePhoto- Drehmarke 6 in KB #1494). Gemessen: hochkant ✓ (zwei Stellen), quer 90° ✓ (1:1), quer 270° ✓ (180°). **Fehler 2 — „Fokus fest" log:** `single-shot` nimmt Chrome ohne Fehler an (auch mit `exact`), setzt auf Android aber nichts um — `AF_TRIGGER_START` kommt im Quelltext nicht vor, der Modus bleibt `CONTINUOUS_PICTURE`, und die gefundene Entfernung meldet Chrome nicht zurück (nur die eigene Vorgabe). Ein echtes Festhalten geht über die Web-Schnittstelle also nicht. Jetzt: Antippen = Dauer-Autofokus **auf die Tippstelle**, der Hinweis „🎯 Fokus fest" ist entfernt. - [x] ✅ 19.10.2026 (Eddy: „den hässlichen Wechselbutton lässt du da doch nicht"): 🔄-Emoji (erschien als blaues Kästchen) → weißes Material-Symbol wie ✕; Knopf nur, wenn `enumerateDevices()` mindestens zwei Kameras meldet. - [ ] **Am Handy beim nächsten Einsatz ansehen** — der Weg bis zur Kamera ist belegt, offen ist nur das Gerät selbst: blitzt das Nothing bei „An" sichtbar, und springt der Fokus nach dem Antippen sofort auf die Stelle (ohne AF-Auslöser hängt das vom Kamera-Treiber ab). Wenn nicht: Tag + Uhrzeit reichen. ## 7. Galerie-Auswahl: „permission"-Fehler ab dem 3. Foto (gemeldet 2026-09-30) Befund (Prod-Zugriffslog 30.09., Auftrag 114 / SO2609-0088, JHB Bio): erster Anlauf 19:40:21 — **nur 2** `upload_photo`-POSTs, danach rote Toasts mit „permission". Der Server hat die übrigen Fotos nie gesehen; zweiter Anlauf 19:41:48 lief mit 8 POSTs durch. Ursache: `handleFiles()` las jedes Foto erst, nachdem das vorige hochgeladen war. Chrome/Android gibt die Galerie-Dateien nur kurz frei (bzw. prüft, ob sie seit der Auswahl verändert wurden) → `NotReadableError` („could not be read, typically due to permission problems…"). Die betroffenen Fotos landeten nicht in der Queue. - [x] ✅ Fix live seit 30.09.2026 (Commit b824e95): erst **alle** Dateien sofort in den Speicher lesen (`readFileNow`, ein Wiederholversuch nach 500 ms), dann hochladen. Unlesbare Datei → klare Meldung „bitte nochmal auswählen". - [ ] Am Handy gegenprüfen: 8–10 Fotos aus der Galerie auf einmal auswählen. ## 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-10-15 | Zurück-Knopf mit Pfeil und Wort; **ein** PDF-Betrachter mit Zoom (Knöpfe, Zwei-Finger, Doppeltipp, Strg+Rad) für Bericht, Lieferschein und Dokumente — der alte Dokumenten-Betrachter hatte keinen Zoom | `55b9a4f`, `a6cdc7b` | #478 | | 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 |