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>
390 lines
23 KiB
Markdown
390 lines
23 KiB
Markdown
# 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()` ruft `api.getPhotoBlobUrl(rel, 'small')` auf → `photo.php?size=small`
|
||
- `photo.php` liefert nur dann ein Thumbnail, wenn Dolibarr eines unter
|
||
`thumbs/<name>_small.<ext>` **vorgefertigt** hat
|
||
- Die PWA lädt ihre Fotos aber per `orders.php?action=upload_photo` hoch, und dort wird
|
||
**nie `vignette()` 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.
|
||
|
||
- [x] **[API]** `photo.php` um `size=thumb&w=<px>` erweitern → `bericht_attachment_thumb()`,
|
||
mit ETag aus mtime und `Cache-Control: private, max-age=…, immutable`
|
||
- [x] `loadThumbs()` umbauen: `<img src="…api/photo.php?…&size=thumb">` **direkt** setzen statt
|
||
fetch → Blob → ObjectURL. Damit greifen HTTP-Cache und `loading="lazy"` wirklich; heute
|
||
sind beide wirkungslos, weil der Blob schon vorher komplett geladen wird.
|
||
- [x] 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.
|
||
- [x] Doppel-Ladung entfernen: schlägt `size=small` fehl, 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`.
|
||
|
||
- [x] **[API]** `taken_at` je Datei ausgeben
|
||
- [x] 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.
|
||
|
||
- [x] **[API]** `'title' => $p->title` in `reports.php` ergä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_path` nur **eines von bis zu sechs** Bildern
|
||
- Bei `title_only` gibt es gar keine Quelle → ❌-Kachel
|
||
|
||
- [x] **[API]** `composite_path` in `reports.php` mit ausgeben
|
||
- [x] PWA zeigt `composite_path`, wenn vorhanden, sonst `source_path`
|
||
- [x] 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 |
|
||
|
||
- [x] 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.
|
||
|
||
- [x] Eigener Runtime-Cache `baustelle-media` für `photo.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.
|
||
- [x] 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.
|
||
|
||
- [x] „📝 Notizen (n)" am Auftrag, direkt unter der Sprachnotiz
|
||
- [x] Liste mit Betreff, zweizeiliger Vorschau und Zeitstempel; Antippen öffnet zum Lesen
|
||
und Ändern, 🗑 löscht (mit Rückfrage)
|
||
- [x] Editor: Betreff-Feld + Textfeld über die volle Höhe, Speichern über ✓ in der Kopfzeile,
|
||
Rückfrage beim Verlassen mit ungespeicherten Änderungen
|
||
- [x] Ablage als `notiz_<betreff>_<datum>.txt` im Auftragsordner (`api/note.php`) — genau der
|
||
Weg der Sprachnotiz. Damit auch im Dolibarr-Auftrag und in der Anhänge-Spalte des
|
||
Bericht-Editors sichtbar.
|
||
- [x] 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.html` fragte `api.getToken()` — ein JWT in IndexedDB, das es **seit der SSO-Umstellung
|
||
gar nicht mehr gibt** (`api.login()` legt nur noch `user` ab, die Sitzung steckt im
|
||
HttpOnly-Cookie `awl_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".
|
||
|
||
- [x] Anmeldung über `api/verify.php` (Cookie) statt über das nicht mehr existierende Token
|
||
- [x] Anmelden **in** der Teilen-Seite (Passwort + Passkey) — das Geteilte bleibt dabei gesichert
|
||
- [x] Service Worker sichert `title`, `text` und `url` mit; Weiterleitung auch ohne Datei
|
||
- [x] Auswahl-Seite: Bildvorschau, Betreff + Text zum Nachbessern, Auftragssuche
|
||
(ohne Suchbegriff die offenen Aufträge, mit Suchbegriff alle)
|
||
- [x] 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
|
||
- [x] Banner in der App, wenn noch etwas Geteiltes unabgelegt bereitliegt
|
||
- [x] **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 im `catch` stumpf `e.message` anzeigten.
|
||
- Warum es beim Teilen nicht auffiel: seit `eaad796` merkt sich `share.html` die letzten
|
||
Aufträge lokal und kommt ohne Netz aus. Die Hauptapp hatte für die Listen
|
||
**keinerlei Offline-Fallback** — `lib/offline.js` puffert nur Foto-Uploads.
|
||
|
||
### Umgesetzt (Muster aus KB #1037 / #1041, dort im Stundenzettel verifiziert)
|
||
|
||
- [x] **Netzfehler kenntlich machen** — `lib/api.js` wandelt den `TypeError` aus `fetch()`
|
||
in einen eigenen Fehler mit `offline = true` und deutschem Text
|
||
(„Keine Verbindung zum Server" / „Zeitüberschreitung"). Ein 4xx/5xx trägt die
|
||
Markierung **nicht** und wird weiterhin als Serverfehler gemeldet.
|
||
- [x] **`isTransient()` in `lib/offline.js` nachgezogen** — die Funktion erkannte Netzfehler
|
||
an `err 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.
|
||
- [x] **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).
|
||
- [x] **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.
|
||
- [x] **Suche ohne Netz** filtert im gespiegelten Stand statt ins Leere zu laufen —
|
||
mit einmaligem Hinweis, dass nur der gespeicherte Stand durchsucht wird.
|
||
- [x] **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).
|
||
- [x] **`sw.js`: echter Fallback.** Der Same-Origin-Zweig endete auf
|
||
`caches.match(e.request)`; fand der Cache nichts, wurde `respondWith(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.
|
||
- [x] **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.
|
||
- [x] **Precache nach Deploy** — der Cache trägt nach jedem Deploy einen neuen Namen und
|
||
startet leer, `app.js`/`app.css` inklusive. 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>`)
|
||
- `fetch` auf 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 liegen `app.js`, `app.css` und alle
|
||
`lib/*.js` mit 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) **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/<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)
|
||
|
||
- [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/<id>')` 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.
|
||
|
||
### Offen (beobachtet, nicht Teil dieser Runde)
|
||
|
||
- [ ] `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.
|
||
|
||
## Erledigt
|
||
|
||
### 2026-08-22
|
||
Abschnitte 1, 2 und 3 vollständig. Dazu nebenbei behoben:
|
||
|
||
- `.modal-header`, `.close-btn` und `.modal-body` hatten **ü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_MEDIA` an 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.
|