248 lines
18 KiB
Markdown
248 lines
18 KiB
Markdown
# ROADMAP — Baustelle PWA
|
||
|
||
Stand: 2026-10-19 · 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, Background Sync bei ausgeschaltetem
|
||
Display, und der Zwei-Finger-Zoom im PDF-Betrachter (lokal nur per CDP-Touch simuliert). 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).
|
||
|
||
---
|
||
|
||
## 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: single-shot`, sonst `continuous`;
|
||
ohne Punktwahl nur neu scharfstellen), Ring an der Stelle. Bei `single-shot` bleibt der Fokus fest, oben steht
|
||
„🎯 Fokus fest · zurück auf Auto" (Tippen → `continuous`). 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), „Fokus fest" → continuous; ohne Fähigkeiten kein Knopf, keine Wirkung.
|
||
- [ ] **Am Handy gegenprüfen** (am Schreibtisch nicht nachstellbar): blitzt „An" wirklich, geht „Licht" an,
|
||
wird nach Antippen die angetippte Stelle scharf.
|
||
Versuch 19.10.2026 am Rechner Buero: Android-Emulator startet nicht (`SVM disabled (by BIOS)`, außerdem fehlen
|
||
nach dem Kernel-Update die Module des laufenden Kernels 7.2.9 → erst nach Neustart + SVM im BIOS). Test mit
|
||
der Webcam Razer Kiyo Pro in Chrome: Die Kamera hing schon beim Starten des Bildstroms
|
||
(`uvcvideo: Failed to set UVC commit control: -110`) und fiel danach vom USB-Bus (`error -71`) — also vor
|
||
Blitz/Fokus. Ergebnis daraus nur: Chrome wirft bei nicht unterstützten Fokus-Vorgaben `OverconstrainedError`,
|
||
den die App abfängt.
|
||
|
||
## 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 |
|