All checks were successful
Deploy baustelle-pwa / deploy (push) Has been skipped
README: - "Verkleinerung auf 2000px" war veraltet - seit Bericht 1.7.0 im Admin einstellbar, Vorgabe 1:1 - fehlten: Background Sync / Periodic Sync, volle Kameraaufloesung ueber ImageCapture, Wake Lock, idempotenter Wiederhol-Upload (duplicate:true) - Verzeichnisstruktur: Rollen von idb.js/offline.js/router.js/sw.js, icon.svg, Workflow, Hinweis auf Symlink der lokalen Testinstanz - API-Liste geprueft: alle 18 Endpunkte stimmen ROADMAP: Offenes nach oben, Abschnitt 6 (18.09.) bleibt ausfuehrlich, Abschnitte 1-5 (alle erledigt) zu einer Tabelle mit Commit + KB-Verweis gekuerzt. Bewusst-nicht-gebaut-Entscheidungen bleiben erhalten. Gegenstueck Bericht 1.5.1 -> 1.7.1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
186 lines
13 KiB
Markdown
186 lines
13 KiB
Markdown
# 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/<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.
|
||
|
||
---
|
||
|
||
## 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 |
|