Doppel-Uploads (Prod-Log 17.09.: reihenweise duplicate:true): Seite
(syncQueue) und Service Worker (Background Sync) arbeiteten dieselbe
IndexedDB-Queue ab, ohne voneinander zu wissen. Dank md5-Abgleich
folgenlos, aber doppeltes Datenvolumen. Mit eingestelltem Verkleinern
sogar zwei verschiedene Dateien im Auftrag: der Worker lud das
Original, die Seite danach die kleine Fassung.
- idb.js: queueKeys/queuePatch/queueClaim/queueRelease - Lesen-Aendern-
Schreiben in einer readwrite-Transaktion, ueber Kontexte atomar
- syncQueue (Seite) und drainQueue (Worker) belegen jedes Foto vor dem
Upload (Lease 90 s > laengster Upload-Timeout); belegt = ueberspringen,
abgelaufen = uebernehmen. Nur noch ein Foto gleichzeitig im Speicher
- enqueuePhoto({hold:true}) + releaseHold: waehrend des Verkleinerns
bleibt das Foto belegt
- Versuchszaehler, retryFailed und replaceQueuedBlob per Patch statt
ganzes Item zurueckschreiben
Reload nach App-Update: controllerchange lud sofort neu - offene Kamera,
getippte Notiz, halbe Unterschrift weg; mit Fotos in der Warteschlange
kam dazu der Browserdialog "Seite verlassen?".
- window.appBusy(): Modal, Auswahl, laufender Upload, Eingabe im Fokus,
laufende Sprachnotiz. PIN-Sperre zaehlt bewusst nicht
- index.php holt den Reload nach, sobald nichts mehr offen ist; eigener
Reload setzt __selfReload, beforeunload warnt dann nicht
Verifiziert lokal in Chromium: echte Kamera-Oberflaeche mit Canvas-
Stream offline, paralleler Zugriff Seite+Worker (3 Fotos = 3 POSTs im
Serverlog, kein duplicate), Verkleinern (eine Datei, kleine Fassung),
fremde Lease frisch/abgelaufen, aufgeschobener Reload ohne Dialog.
Nicht getestet: Kamera-Hardware und Background Sync bei Display aus am
Handy. Kein Deploy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
23 KiB
ROADMAP — Baustelle PWA
Stand: 2026-09-18 · Gegenstück: data/bericht (Modul-Version 1.5.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.
1. Nachgezogen aus Bericht 1.5.0 ✅ (2026-08-22, Modul 1.5.1)
Das Modul hatte mit 1.5.0 Infrastruktur bekommen, von der die PWA nichts hatte. Die Beschreibungen unten schildern jeweils den Zustand davor.
1.1 Echte Thumbnails statt Vollbilder ✅ [API]
Bis dahin lud die PWA für jede Kachel das komplette Foto:
loadThumbs()ruftapi.getPhotoBlobUrl(rel, 'small')auf →photo.php?size=smallphoto.phpliefert nur dann ein Thumbnail, wenn Dolibarr eines unterthumbs/<name>_small.<ext>vorgefertigt hat- Die PWA lädt ihre Fotos aber per
orders.php?action=upload_photohoch, und dort wird nievignette()aufgerufen → es existiert nie ein Thumb → es kommt immer das Original - Bei 30 Baustellenfotos sind das im Mobilfunknetz 30 × 0,5–1 MB (bei über die Dolibarr-Oberfläche hochgeladenen Originalen entsprechend mehr)
Das Modul kann das seit 1.5.0 längst: bericht_attachment_thumb($full, $maxside) in
lib/bericht.lib.php (GD, Cache unter bericht/thumbs/, EXIF-Rotation, Speicher-Notbremse).
api/photo.php lädt diese lib bereits.
- [API]
photo.phpumsize=thumb&w=<px>erweitern →bericht_attachment_thumb(), mit ETag aus mtime undCache-Control: private, max-age=…, immutable loadThumbs()umbauen:<img src="…api/photo.php?…&size=thumb">direkt setzen statt fetch → Blob → ObjectURL. Damit greifen HTTP-Cache undloading="lazy"wirklich; heute sind beide wirkungslos, weil der Blob schon vorher komplett geladen wird.- Sequenzielle Schleife auflösen —
for (const t of thumbs) { await … }lädt Foto für Foto nacheinander. Beim<img>-Weg erledigt der Browser das parallel von selbst. - Doppel-Ladung entfernen: schlägt
size=smallfehl, wird das Original noch einmal geladen.
1.2 Aufnahmezeit statt Dateidatum ✅ [API]
1.5.0 hat einen eigenen EXIF-Parser bekommen (bericht_jpeg_taken_at() /
bericht_file_taken_at()), weil im Prod-Container das exif-Modul fehlt. Der Editor sortiert
Anhänge damit nach Aufnahmezeit. orders.php?action=photos lieferte nur filemtime.
- [API]
taken_atje Datei ausgeben - Fotoliste nach Aufnahmezeit sortieren (Auswahl gemerkt, wie im Editor)
1.3 Seiten-Titel kommt nie an ✅ [API]
api/pages.php kann title einer Seite setzen, die PWA rendert ihn (page-title-badge,
openPageActionsModal(…, title)) — aber api/reports.php gibt title im pages-Array
nicht aus. Ein im Editor gesetzter Titel ist in der PWA also unsichtbar, und ein in der PWA
gesetzter Titel scheint nach dem Neuladen verschwunden.
- [API]
'title' => $p->titleinreports.phpergänzen (eine Zeile)
1.4 Bearbeitete Seiten und Raster-Layouts ✅ [API]
Seit 1.5.0 hat eine Seite composite_path (das im Editor gebaute Seitenbild) und layout
(single, grid_2, grid_4, before_after, title_only, …). Die PWA zeigt stur
source_path:
-
Nach Bearbeitung im Desktop-Editor zeigt die PWA weiter das Rohbild ohne Anmerkungen
-
Bei einer Rasterseite ist
source_pathnur eines von bis zu sechs Bildern -
Bei
title_onlygibt es gar keine Quelle → ❌-Kachel -
[API]
composite_pathinreports.phpmit ausgeben -
PWA zeigt
composite_path, wenn vorhanden, sonstsource_path -
Layout-Kennzeichen an der Kachel (z. B. „⊞ 4er-Raster"), damit klar ist, warum dort mehr drauf ist als das eine Foto
2. Verbesserungen unabhängig vom Modul
2.1 Browser-Dialoge raus (10 Stück) ✅
confirm() / alert() / prompt() widersprechen der Hausregel und sehen in der
Standalone-PWA auf Android wie ein Systemfehler aus. Ein sauberes Muster liegt schon vor:
das „Bericht löschen"-Modal in router.on('/reports/:id').
| Stelle | Zeile (vor dem Umbau) |
|---|---|
| PIN stimmt nicht überein | alert 295 |
| Bericht finalisieren | confirm 1111 |
| PIN-Schutz deaktivieren | confirm 1264 |
| Material-Eintrag löschen | confirm 1432 |
| Seite aus Bericht entfernen | confirm 1809 |
| Foto löschen | confirm 2701 |
| Nicht hochgeladenes Foto löschen | confirm 3019 |
| Mess-Skala: Länge eingeben | prompt 3409 |
| Ungültiges Längenformat | alert 3419 |
| „Zuerst kalibrieren" | alert 3440 |
- Eine
confirmDialog({title, text, danger})-Hilfsfunktion (Promise) +inputDialog()für die Längeneingabe, alle zehn Stellen darauf umstellen
2.2 Fotos offline sichtbar halten ✅
Der Service Worker reichte alles unter /custom/bericht/api/ ungecacht durch. Auf der Baustelle
ohne Netz ist damit kein einziges schon angesehenes Foto verfügbar.
- Eigener Runtime-Cache
baustelle-mediafürphoto.php-GETs, cache-first mit Größenbegrenzung (LRU, z. B. 200 Einträge). Die Thumb-URLs aus 1.1 sind dafür der richtige Kandidat — klein und unveränderlich. - Vom App-Shell-Cache getrennt halten, damit ein Deploy die Medien nicht wegräumt
3. Textnotizen ✅ (2026-08-22)
Getippte Notiz zum Auftrag — für alles, was man lieber schreibt als diktiert: Zählernummern, Absprachen, Maße.
- „📝 Notizen (n)" am Auftrag, direkt unter der Sprachnotiz
- Liste mit Betreff, zweizeiliger Vorschau und Zeitstempel; Antippen öffnet zum Lesen und Ändern, 🗑 löscht (mit Rückfrage)
- Editor: Betreff-Feld + Textfeld über die volle Höhe, Speichern über ✓ in der Kopfzeile, Rückfrage beim Verlassen mit ungespeicherten Änderungen
- Ablage als
notiz_<betreff>_<datum>.txtim Auftragsordner (api/note.php) — genau der Weg der Sprachnotiz. Damit auch im Dolibarr-Auftrag und in der Anhänge-Spalte des Bericht-Editors sichtbar. - Notizen sind aus „Weitere Dokumente" ausgefiltert — sie haben ihre eigene Sektion und stünden dort sonst doppelt
Nicht umgesetzt (und bewusst verworfen): eine Zeichenfläche für echte Handschrift. Am Handy ohne Stift ist das unbrauchbar.
4. Teilen aus WhatsApp ✅ (2026-08-30)
Bilder und Beschreibungstexte, die die Kundin per WhatsApp schickt, landen direkt im Auftrag. Vorher endete jeder Teilen-Vorgang in einer Sackgasse:
-
share.htmlfragteapi.getToken()— ein JWT in IndexedDB, das es seit der SSO-Umstellung gar nicht mehr gibt (api.login()legt nur nochuserab, die Sitzung steckt im HttpOnly-Cookieawl_sso). Ergebnis: immer „Bitte zuerst anmelden", und der Weg „Zur App" verwarf das Geteilte. -
Der Service Worker sicherte nur Dateien. Ein Text-Share (oder Bild + Bildunterschrift) schrieb nichts in IndexedDB → „Keine geteilten Fotos gefunden".
-
Anmeldung über
api/verify.php(Cookie) statt über das nicht mehr existierende Token -
Anmelden in der Teilen-Seite (Passwort + Passkey) — das Geteilte bleibt dabei gesichert
-
Service Worker sichert
title,textundurlmit; Weiterleitung auch ohne Datei -
Auswahl-Seite: Bildvorschau, Betreff + Text zum Nachbessern, Auftragssuche (ohne Suchbegriff die offenen Aufträge, mit Suchbegriff alle)
-
Ablegen: Bilder über die normale Upload-Queue (Persist-First), Text als Notiz über
api/note.php. Schlägt die Notiz fehl (kein Netz), bleibt der Text gesichert und der Vorgang ist wiederholbar -
Banner in der App, wenn noch etwas Geteiltes unabgelegt bereitliegt
-
Mehrere Nachrichten hintereinander: die letzten drei Aufträge stehen unter „Zuletzt benutzt" oben; innerhalb von 30 Minuten ist der letzte vorausgewählt (danach nur noch Schnellzugriff — eine Dauer-Vorauswahl würde irgendwann im falschen Auftrag landen)
5. Offline lesbar statt „Failed to fetch" ✅ (2026-08-31)
Gemeldet von Eddy am 2026-08-31: Die App zeigte beim normalen Öffnen nur „⚠️ Failed to fetch", während das Teilen aus WhatsApp weiter funktionierte.
Befund
- Prod-Deploy vollständig, alle Dateien liefern 200, die API antwortet korrekt
(403 ohne
X-Requested-With, 401 ohne Anmeldung) — kein Server- oder Deploy-Fehler. - Im frischen Desktop-Chromium lief der ganze Weg sauber durch
(
auth.php→verify.php→orders.php?open=1, alle 200). - „Failed to fetch" ist die rohe Browsermeldung eines
fetch(), das gar nicht bis zum Server kam. Sie stand 1:1 im UI, weil die Routen imcatchstumpfe.messageanzeigten. - Warum es beim Teilen nicht auffiel: seit
eaad796merkt sichshare.htmldie letzten Aufträge lokal und kommt ohne Netz aus. Die Hauptapp hatte für die Listen keinerlei Offline-Fallback —lib/offline.jspuffert nur Foto-Uploads.
Umgesetzt (Muster aus KB #1037 / #1041, dort im Stundenzettel verifiziert)
- Netzfehler kenntlich machen —
lib/api.jswandelt denTypeErrorausfetch()in einen eigenen Fehler mitoffline = trueund deutschem Text („Keine Verbindung zum Server" / „Zeitüberschreitung"). Ein 4xx/5xx trägt die Markierung nicht und wird weiterhin als Serverfehler gemeldet. isTransient()inlib/offline.jsnachgezogen — die Funktion erkannte Netzfehler anerr instanceof TypeError. Ohne Anpassung hätte ein Foto aus dem Funkloch als dauerhaft fehlgeschlagen gegolten und wäre nach sechs Versuchen in der Quarantäne gelandet, statt hochgeladen zu werden.- Datenspiegel (
offline.mirrorSave/mirrorLoad/mirrorClear) — jede erfolgreiche Antwort wird in IndexedDB gespiegelt: Auftragsliste, Kundenliste, Berichte sowie Auftrags- und Kundendetails (letztere auf 30 Einträge begrenzt, der gerade geschriebene Schlüssel ist beim Aufräumen geschützt — KB #1037). - Sichtbares Band „📴 Kein Netz — angezeigt wird der Stand von HH:MM Uhr". Ohne den Hinweis hält man alte Zahlen für aktuell. Es gehört zur Ansicht und wird vom Router bei jedem Routenwechsel zurückgesetzt.
- Suche ohne Netz filtert im gespiegelten Stand statt ins Leere zu laufen — mit einmaligem Hinweis, dass nur der gespeicherte Stand durchsucht wird.
- Beim Abmelden wird der Spiegel gelöscht — sonst sähe der nächste Benutzer am selben Gerät offline die Aufträge, Kunden und Preise des vorherigen (KB #1041).
sw.js: echter Fallback. Der Same-Origin-Zweig endete aufcaches.match(e.request); fand der Cache nichts, wurderespondWith(undefined)aufgerufen — und die Anfrage scheiterte erneut mit genau der Meldung, die der Fallback verhindern sollte. Jetzt kommt immer eine echte Response: Seite → App-Hülle aus dem Cache, sonst eine erklärende Seite; API → 503 mit JSON.- Nur vollständige Antworten cachen (
ok && status === 200 && type !== 'opaque') — eine 206 (Teilinhalt beim Audio-Seek) liefert der Worker später als kaputte Datei aus. - Precache nach Deploy — der Cache trägt nach jedem Deploy einen neuen Namen und
startet leer,
app.js/app.cssinklusive. Eine feste Liste hilft nicht, weil die URLs?v=<mtime>tragen. Die Seite meldet dem Worker nach dem Laden, was sie tatsächlich geholt hat (PRECACHE-Nachricht), er cacht es einzeln nach (KB #1041).
Verifiziert (2026-08-31, lokale Testinstanz, Chromium)
- Spiegel wird geschrieben (
mirror:orders:open,:customers,:reports,:detail:order:<id>,:detail:customer:<id>) fetchauf Fehler gesetzt: Auftragsliste zeigt 6 Aufträge aus dem Spiegel + Band „Stand von 19:03 Uhr"; Kundenliste ohne Spiegel zeigt die verständliche Meldung statt „Failed to fetch"- Suche „Rolfs" ohne Netz → 2 Treffer aus dem Spiegel + Hinweis-Toast
- Band verschwindet auf Routen ohne Spiegel und nach Rückkehr des Netzes
- Echtes Funkloch (
context.setOffline(true)+reload()): App startet aus dem Cache, zeigt Band und 6 Aufträge; im Cache liegenapp.js,app.cssund allelib/*.jsmit ihrem?v=— vorher wären sie nach einem Deploy nicht dagewesen - Konsole beim Offline-Start: nur Netzfehler, keine JS-Fehler
Bewusst nicht gebaut
Keine Schreib-Warteschlange für Auftragsdaten. Die bräuchte Idempotenz und Konfliktbehandlung (zwei Geräte am selben Auftrag). Für Fotos existiert sie längst (Persist-First-Queue), für alles andere gilt: lesen ja, ändern nur mit Netz.
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) undorders.php?id=111(Detail). Das ist die Signatur des Fehlers. - 17:40:45
verify.php+orders.php(Liste) — der Rauswurf. Keinindex.phpdavor, also kein Reload, kein SW-Update, kein Logout (kein 401), kein Absturz. - 17:40:47 Auftrag 111 erneut geöffnet,
action=photosliefert eine leere Liste; die Uploads kommen erst 17:40:54 und 17:57:25–17:58:22 an.
Ursache Rauswurf — History-Race in openNewOrderModal:
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/<id>) und
Lieferschein-Unterschrift (#/shipments/<id>). 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)
- Race zentral behoben:
closeModal()zählt selbst ausgelöstehistory.back()(_pendingBacks, Zähler statt Boolean, KB #1209 — der Boolean verschluckt bei zwei schnell nacheinander geschlossenen Dialogen einpopstate),router.go()wartet überwindow.historySettled(), bis alle durch sind, und wechselt erst dann den Hash (Schutzzeit 1,5 s, falls einpopstateausbleibt). 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. - Sicherheitsnetz in Kamera und Galerie-Auswahl: nach „Fertig" ausdrücklich
router.go('#/orders/<id>')stattrouter.navigate()— gezeichnet wird der Auftrag, für den fotografiert wurde, nicht „was gerade in der Adresszeile steht". - 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.
- Auftragsseite zieht nach: bei
photo-uploadedfü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. - 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):
idb.queueKeys()/queuePatch(id, fn)/queueClaim(id, owner, leaseMs)/queueRelease()— Lesen-Ändern-Schreiben in einer readwrite-Transaktion (über Kontexte hinweg atomar). Gegenstück insw.js(queueClaim/queueRelease) — beide Stellen zusammen ändern, der Worker kannlib/idb.jsnicht laden (window).syncQueue()unddrainQueue()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.- Fehlschlag gibt die Lease sofort frei; Versuchszähler/Quarantäne per Patch statt
„ganzes Item zurückschreiben" (überschrieb bisher eine parallele Änderung). Ebenso
retryFailed(). enqueuePhoto(…, {hold:true})+releaseHold(): soll verkleinert werden, kommt das Foto mit Lease (page-…:resize) in die Queue; freigegeben wird imfinallynach dem Verkleinern. Vergisst es jemand oder stirbt die Seite: Ablauf nach 90 s.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.
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).- index.php: Reload nur, wenn nicht beschäftigt; sonst alle 3 s und bei jedem Sichtbarkeitswechsel erneut prüfen
- Eigener Reload setzt
window.__selfReload→beforeunloadwarnt 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 mitduplicate:true - Verkleinern: während des Verkleinerns ist das Item von
page-…:resizebelegt; 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:
controllerchangebei 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.
Offen (beobachtet, nicht Teil dieser Runde)
idb.queueAll()lädt alle Foto-Blobs in den Speicher — auchupdateBadge(), 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, weilsw.js/share.htmldie DB mit fester Version 1 öffnen.
Erledigt
2026-08-22
Abschnitte 1, 2 und 3 vollständig. Dazu nebenbei behoben:
.modal-header,.close-btnund.modal-bodyhatten überhaupt kein CSS, obwohl das Markup sie seit Längerem nutzt (u. a. beim Löschen eines Berichts) — der Schließen-Knopf saß dadurch unter der Überschrift statt daneben.- Das Laden eines Thumbnails überschrieb den kompletten Kachel-Inhalt (
t.innerHTML = …) und nahm die Overlays mit: Auswahl-Häkchen in der Mehrfachauswahl und Seitennummer der Bericht-Seiten waren weg, sobald das Bild da war. - Beim Abmelden bleiben keine Kundenfotos mehr auf dem Gerät (
CLEAR_MEDIAan den Service Worker +clearPhotoCache()).
Davor
Siehe README (Feature-Liste) und die Commit-Historie: SSO-Migration auf awlauth, WebAuthn-Login, Neuer-Auftrag-Formular, Live-Kamera mit Persist-First-Queue, Lieferschein-Unterschrift.