All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
Eddy: „so dass es gut aussieht", zu rein weißen Symbolen dann „viel zu schlicht". Emojis erscheinen je nach Handy als Kästchen. - icon() in app.js (Material-Symbole), Farbe je Symbol in app.css (.ikon-<name>): große Knöpfe = 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 = farbiges Symbol (Teilen blau, Löschen rot, Stift/Blitz/ Hilfe gelb, Download/Telefon grün). - Untere Leiste: farbiges Symbol über dem Wort, Heute als Sonne, Zahnrad heißt „Einstellungen". - Titel und Überschriften ohne Emoji; Telefonnummer lesbar. - .icon-btn[hidden] bleibt versteckt (inline-flex hätte hidden ausgehebelt). Im Android-Emulator angesehen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
342 lines
27 KiB
Markdown
342 lines
27 KiB
Markdown
# ROADMAP — Baustelle PWA
|
||
|
||
Stand: 2026-10-19 · Gegenstück: `data/bericht` (Modul-Version 1.8.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] ✅ 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:<neu> 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-<name>`: `--ikon-farbe`, `--ikon-grund`).
|
||
- [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.
|
||
- [ ] Offen, nicht angefasst: Die untere Leiste steht seit v1.0.0 am **Seitenende** statt fest unten (`#app` nur
|
||
`min-height: 100dvh`, `#main` wächst mit) — bei langen Listen erst nach Scrollen erreichbar. Wenn gewünscht:
|
||
Leiste fest unten, dabei FAB und „Geteiltes ablegen"-Leiste (Punkt 8) mit hochrücken.
|
||
|
||
## 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/<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-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 |
|