baustelle-pwa/README.md
Eddy aaed48682e
All checks were successful
Deploy baustelle-pwa / deploy (push) Has been skipped
Doku: README auf Code-Stand, ROADMAP entruempelt
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>
2026-09-18 23:57:47 +02:00

220 lines
16 KiB
Markdown

# Baustelle PWA
Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und Textnotizen, Skizzen, offline-fähig. Spricht die REST-API des Dolibarr-Bericht-Moduls.
## Stack
- **Vanilla JavaScript** — kein Build, keine Framework-Abhängigkeit
- **Service Worker** für App-Shell-Cache (Workbox-frei, eigene Implementierung)
- **IndexedDB** für die Offline-Upload-Queue (Auth-Session steckt im HttpOnly-Cookie, nicht in IndexedDB)
- **REST API** des `bericht`-Dolibarr-Moduls unter `/custom/bericht/api/`
## Features
**Aufträge**
- ✅ Login mit Dolibarr-Credentials → zentrales SSO (HttpOnly-Cookie `awl_sso`, same-origin; Single-Sign-On/-Logout über alle AWL-Apps). WebAuthn/Passkey-Login möglich
-**„Heute"-Ansicht**: offene Aufträge des Tages, gefiltert aus der normalen Auftragsliste, mit Route
für alle Termine auf einen Blick (öffnet Google Maps mit allen Adressen als Wegpunkten)
- ✅ Auftragsliste mit Suche, gefiltert auf eigene Aufträge (Multi-User)
- ✅ Toggle "Auch abgeschlossene" (Filter in localStorage persistiert)
- ✅ Auftragsdetail mit Kunde, Adresse, Telefon (Click-to-Call)
-**Neuer Auftrag** (FAB): Kunde per Suche/Zuletzt-verwendet wählen, dann **Ihr Zeichen** (Pflicht), **Auftragsbeschreibung** (optional), **Geplanter Liefertermin** (Datum + Uhrzeit) und Checkbox **Auftrag direkt freigeben**. Kunden-Defaults (Zahlungsbedingung/Zahlart, Ansprechpartner) werden übernommen. Bei „direkt freigeben" wird der Auftrag **ohne Position** validiert — die Leistungen kommen aus dem Stundenzettel. (Bis Bericht 1.3.1 hängte das Backend hier eine Std-Lohn-Zeile mit Menge 1 an; die verfälschte den Auftragswert, stand als offene Restmenge in der Lieferauflistung und im Kunden-PDF.)
**Foto-Upload**
- ✅ Live-Kamera-Modal (getUserMedia): bleibt für Serien-Aufnahmen offen — Auslöser + Filmstreifen mit Upload-Status je Foto (💾 gesichert → ⏳ lädt hoch → ✓ fertig), Front-/Rückkamera-Umschaltung, Fehlschuss löschbar
-**Volle Kameraauflösung**: ausgelöst wird über `ImageCapture.takePhoto()` (native JPEG der Kamera inkl. EXIF) — ein Standbild aus dem Video-Stream hat nur Stream-Auflösung und ist nur noch die Rückfallebene (iOS/Safari, ältere Browser)
- ✅ Alternativ Aufnahme aus der Galerie
-**Fotoqualität im Admin einstellbar** (Bericht-Modul → Fotoqualität, `api/config.php`): Vorgabe ist **1:1, gar nicht verkleinern**; optional längste Seite + JPEG-Qualität. Gesichert wird immer zuerst das Original, die kleine Fassung ersetzt es danach — hängt das Verkleinern (Display aus), bleibt das Original
-**Wake Lock** während Foto-Serie und Upload: schaltet sich das Display ab, friert der Browser laufende Uploads und das Verkleinern ein
-**Persist-First / Datenverlust-Schutz**: jedes Foto wird beim Auslösen ZUERST in IndexedDB gesichert, Upload erst danach; Queue-Item wird nur nach bestätigtem Upload (HTTP 2xx mit gültigem `relpath`) gelöscht — überlebt fehlendes/schwaches Netz, hängende Uploads (45s-Timeout) und App-Kill. Erkennt 2xx-HTML (abgelaufene Session/Proxy-Loginseite) als Fehler statt als Erfolg
- ✅ Auto-Sync bei "online", periodisch (15s) und bei App-Fokus; Status-Badge (🟢 alles gesichert / 🟡 lädt hoch / 🔴 offline / ⚠️ fehlgeschlagen)
-**Background Sync**: der Service Worker lädt die Warteschlange auch dann hoch, wenn die App gar nicht offen ist (Display aus, weggewischt) — Tag `photo-queue`, dazu Periodic Background Sync (15 min), wo der Browser ihn gewährt. Anmeldung läuft über das `awl_sso`-Cookie mit
-**Wiederholter Upload ist idempotent**: geht die Antwort nach dem Speichern verloren, wird erneut gesendet — der Server vergleicht den Inhalt (md5, seit Bericht 1.6.0), legt nichts doppelt ab und antwortet mit `duplicate: true`
- ✅ Recovery: Tipp auf das Status-Badge öffnet die Warteschlange → erneut senden / teilen; Warnung beim Schließen mit noch ungesicherten Fotos
-**Seite und Service Worker laden nichts doppelt hoch**: wer ein Foto sendet, belegt es in IndexedDB (Lease, 90 s); der andere überspringt es und übernimmt erst nach Ablauf. Beim Verkleinern bleibt das Foto belegt, bis die kleine Fassung in der Queue liegt — sonst gingen Original **und** kleine Fassung raus. Logik steht zweimal (`lib/idb.js` + `sw.js`), immer zusammen ändern
-**App-Update lädt nicht mitten in der Arbeit neu**: nach einem Deploy wartet der Reload, bis kein Dialog (Kamera, Notiz, Unterschrift) offen ist, nichts getippt wird und kein Upload läuft (`window.appBusy()`); der eigene Reload löst keinen Browser-Dialog aus
-**Wartende Fotos stehen am Auftrag**: Block „⏳ Warten auf Upload (n)" mit Vorschaubildern aus der Warteschlange und Klartext (gesichert auf dem Gerät, geht raus sobald Netz da ist); nach dem Upload zieht die Auftragsseite von selbst nach. Hinweis-Toast beim Schließen der Kamera, wenn noch etwas wartet
- ✅ Kacheln laden **serverseitige Thumbnails** (`photo.php?size=thumb`) statt der Originale — rund 10 KB statt mehrerer hundert KB je Kachel, parallel und mit `loading="lazy"`, Wiederholaufrufe enden mit 304
- ✅ Foto-Viewer mit Zoom + Swipe
- ✅ Foto-Skizze: Annotationen mit Pfeilen, Kreisen, Rechtecken, Text
**Notizen**
- ✅ Getippte Notiz zum Auftrag: Betreff + Text, Liste mit Vorschau und Zeitstempel, jederzeit wieder les- und änderbar
- ✅ Ablage als `notiz_<betreff>_<datum>.txt` im Auftragsordner — damit auch im Dolibarr-Auftrag und in der Anhänge-Spalte des Bericht-Editors sichtbar
- ✅ Löschen mit Rückfrage; in „Weitere Dokumente" tauchen Notizen nicht doppelt auf
**Sprachnotizen**
- ✅ Sprachaufnahme via MediaRecorder (webm)
- ✅ Whisper-Transkription (serverseitig, API-Aufruf)
**Berichte**
- ✅ Berichte pro Auftrag anzeigen
- ✅ Seitenkacheln zeigen das im Editor gebaute Seitenbild (`composite_path`) samt Anmerkungen — nicht mehr nur das Rohbild; Raster-Layouts sind mit „⊞ 4" gekennzeichnet
- ✅ Bericht erstellen mit Template-Auswahl
- ✅ Seiten umordnen per Drag&Drop
- ✅ Seite löschen, Notiz bearbeiten
- ✅ Touch-Unterschrift abnehmen (mit GPS + Name)
- ✅ Bericht finalisieren → PDF-Vorschau
- ✅ PDF-Download
**Lieferungen + Kundenunterschrift**
- ✅ Lieferungen-Liste pro Auftrag mit Status + Signed-Badge
- ✅ Lieferschein-PDF inline (PDF.js, alle Seiten)
- ✅ Vollbild-Querformat-Unterschrift mit HiDPI-Canvas, transparent (kein Hintergrund)
- ✅ Trimmt Canvas automatisch auf bemalte Fläche
- ✅ Name des Unterzeichners aus Kundendaten vorausgefüllt
- ✅ GPS-Koordinaten (optional, timeout 3s)
- ✅ Unterschrift wird via ODT-Hook in das Lieferschein-PDF gestempelt
- ✅ Expedition wird nach Unterschrift automatisch validiert + geschlossen
- ✅ Signed-PDF direkt nach Bestätigung als Vorschau
**Materialliste**
- ✅ Material pro Auftrag erfassen (Label, Menge, Einheit, Notiz)
- ✅ Material löschen
**PWA**
- ✅ Service Worker für Offline-Start
- ✅ Installierbar als PWA (Home-Screen-Icon)
-**Web Share Target** — aus WhatsApp & Co. Bilder **und** Beschreibungstext an „Baustelle" teilen:
anmelden (falls nötig, direkt in der Teilen-Seite), Auftrag suchen und wählen, ablegen. Bilder gehen
über die normale Upload-Queue, der Text wird als Notiz (`notiz_*.txt`) im Auftrag gespeichert
-**Serie ohne Sucherei**: die Teilen-Seite merkt sich die letzten drei benutzten Aufträge und zeigt
sie unter „Zuletzt benutzt" ganz oben. Wurde einer davon in den letzten 30 Minuten benutzt, ist er
**vorausgewählt** — die zweite und dritte Nachricht der Kundin kosten dann nur noch einen Tipp.
Danach steht er nur noch als Schnellzugriff da, damit nichts versehentlich im Auftrag von vorgestern landet
- ✅ Nativer Android-Share-Sheet für Fotos, PDFs und Dokumente (`navigator.share`) — direkt an WhatsApp / Signal / Mail
- ✅ Mehrfachauswahl in Foto- und Dokument-Liste mit Batch-Teilen
- ✅ Android-Hardware-Back-Button schließt Modale/Ansichten sauber (History-Modal-Stack), bricht Mehrfachauswahl ab statt App zu verlassen
- ✅ „Nochmal drücken zum Beenden"-Toast auf Top-Level-Routen
- ✅ PIN-Schutz (optional)
-**Push-Benachrichtigungen via ntfy** (statt klassischem Web-Push): eigener Server/Topic in den
Einstellungen, Browser-Notification-Permission, Reconnect bei Verbindungsabbruch
-**Bild-Cache** `baustelle-media` (cache-first, LRU 300): schon angesehene Fotos sind auch ohne Netz da; überlebt Deploys, wird beim Abmelden geleert
-**Keine Browser-Dialoge**`confirm`/`alert`/`prompt` sind durch eigene Modale ersetzt (`confirmDialog`, `alertDialog`, `inputDialog`); der Hardware-Zurück-Button schließt sie als Abbruch
**Ohne Netz** (Keller, Neubau, Funkloch — auf der Baustelle der Normalfall)
-**Datenspiegel**: Auftragsliste, Kundenliste, Berichte sowie geöffnete Auftrags- und
Kundendetails werden nach jedem erfolgreichen Abruf in IndexedDB gespiegelt und ohne Netz
von dort angezeigt (Details: die letzten 30). Zusammen mit dem Bild-Cache steht ein einmal
geöffneter Auftrag samt Fotos auch im Funkloch
-**Sichtbares Band** „📴 Kein Netz — angezeigt wird der Stand von HH:MM Uhr" — ohne den
Hinweis hält man alte Zahlen für aktuell. Kommt das Netz zurück, lädt die Ansicht neu und
das Band verschwindet
-**Suche greift auf den gespiegelten Stand zu**, statt ins Leere zu laufen (mit Hinweis)
-**Verständliche Meldungen**: ein `fetch()`, das nicht bis zum Server kommt, wird zu
„Keine Verbindung zum Server" — nicht mehr die rohe Browsermeldung „Failed to fetch".
Ein echter Serverfehler (401/403/500) wird weiterhin als solcher gemeldet
-**Start ohne Netz**: der Service Worker liefert die App-Hülle aus dem Cache; die Seite
meldet ihm nach dem Laden ihre tatsächlichen Asset-URLs zum Nachcachen, sonst wäre der
Cache direkt nach einem Deploy leer (`?v=<mtime>` verhindert eine feste Dateiliste)
-**Beim Abmelden wird der Spiegel gelöscht** — am geteilten Gerät sieht sonst der nächste
Benutzer offline die Kunden und Preise des vorherigen
-**Ändern braucht Netz** — bewusst keine Schreib-Warteschlange für Auftragsdaten
(bräuchte Idempotenz + Konfliktbehandlung). Fotos sind die Ausnahme: die gehen über die
Persist-First-Queue und werden nachgeliefert
## Hosting
Deploy per Forgejo-Pipeline (Commit-Tag `[deploy]`) nach `/mnt/appdata/firma/dolibarr-202509/modules/baustelle/`. Erreichbar unter `https://awl.data-it-solution.de/custom/baustelle/`. Cache-Busting automatisch via `filemtime()` in `index.php` (`?v=<mtime>` an JS/CSS) — kein manuelles Versions-Hochzählen nötig, die PWA aktualisiert sich beim nächsten Laden.
## Verzeichnis-Struktur
```
baustelle-pwa/
├── index.php App-Shell (Cache-Busting via filemtime → ?v=<mtime>)
├── share.html Web-Share-Target-Empfänger
├── manifest.webmanifest PWA Manifest
├── sw.js Service Worker (Network-First für Assets, API pass-through,
│ cache-first für photo.php?size=thumb → Cache baustelle-media,
│ Share-Target, Background Sync der Foto-Queue inkl. Lease)
├── app.js Hauptlogik (Routes, Views, Dialoge, Notizen, Modal-Stack, appBusy)
├── app.css Mobile-first Styling
├── lib/
│ ├── idb.js IndexedDB-Wrapper: kv-Store (Benutzer, Datenspiegel) + Foto-Queue
│ │ (queuePatch/queueClaim/queueRelease — atomar, Gegenstück in sw.js)
│ ├── api.js REST-API-Client (SSO-Cookie same-origin, JSON-Guard, Timeout,
│ │ Netzfehler → `offline = true`)
│ ├── offline.js Offline-Queue + Sync (Persist-First, Lease), Wake Lock, Datenspiegel
│ ├── router.js Hash-Router (wartet vor dem Hash-Wechsel auf ausstehende history.back())
│ ├── pdf.min.js PDF.js (Lieferschein-Vorschau)
│ └── pdf.worker.min.js PDF.js Worker
├── icons/
│ ├── icon.svg
│ ├── icon-192.png
│ └── icon-512.png
├── .forgejo/workflows/ deploy.yml — rsync nach Prod bei `[deploy]`, meldet per Ntfy
├── ROADMAP.md Offene Punkte + Befunde/Verifikation der letzten Runden
└── README.md
```
**Lokal testen:** `/var/www/dolibarr/custom/baustelle` ist ein Symlink auf dieses Repo — Änderungen sind sofort unter `http://localhost:8080/custom/baustelle/` live. Nach Code-Änderungen die Seite wirklich neu laden (nicht nur den Hash wechseln), sonst läuft der alte Stand.
## API
Die App spricht die Bericht-API unter `/custom/bericht/api/`:
**Auth**
- `POST /auth.php` — Login mit Dolibarr-Credentials → setzt HttpOnly-Cookie `awl_sso` (zentrales SSO via awlauth). Folge-Requests authentisieren same-origin über das Cookie (kein Bearer-Token)
- `GET /verify.php` — prüft das Cookie und verlängert die Session (sliding renewal); läuft bei jedem Routenwechsel (`ensureAuth()`) und ist der Weg, wie `share.html` prüft, ob eine Anmeldung nötig ist
- `POST /logout.php` — löscht das `awl_sso`-Cookie (Single-Logout über alle AWL-Apps)
**Aufträge**
- `GET /orders.php` — Aufträge des Users (Filter: `?open=1`, `?q=suche`)
- `GET /orders.php?id=X` — Auftrag-Detail inkl. verknüpfte Berichte
- `GET /orders.php?id=X&action=photos` — Anhang-Liste (je Datei `taken_at` = EXIF-Aufnahmezeit, danach sortiert)
- `POST /orders.php?id=X&action=upload_photo` — Foto-Upload (multipart)
- `POST /orders.php?action=create` — Neuen Auftrag anlegen (Kunde, Ihr Zeichen, Beschreibung, Liefertermin, optional direkt freigeben)
**Kunden**
- `GET /customers.php` — Kundenliste (Filter: `?q=suche`)
- `GET /customers.php?id=X` — Kundendetail
**Berichte**
- `GET /reports.php` — Alle Berichte des Users
- `GET /reports.php?id=X` — Bericht-Detail mit Seiten (je Seite `composite_path`, `title`, `layout``composite_path` anzeigen, wenn gesetzt)
- `POST /reports.php?action=create` — Neuen Bericht anlegen
- `POST /reports.php?id=X&action=finalize` — Bericht finalisieren
- `DELETE /reports.php?id=X` — Bericht löschen
**Seiten**
- `DELETE /pages.php?id=X` — Seite löschen
- `POST /pages.php?id=X` — Seite aktualisieren (note, rotation)
- `POST /pages.php?action=signature&bericht_id=X` — Unterschrift hinzufügen
- `POST /pages.php?action=reorder` — Seiten umordnen
**Material**
- `GET /materials.php?element_type=X&element_id=Y` — Materialliste
- `POST /materials.php?element_type=X&element_id=Y` — Material hinzufügen
- `POST /materials.php?id=X&delete=1` — Material löschen
**Lieferungen**
- `GET /shipments.php?order_id=X` — Lieferungen zu einem Auftrag
- `GET /shipments.php?id=X` — Lieferung-Detail (inkl. bericht_id falls Unterschrift vorhanden)
- `GET /shipments.php?id=X&action=pdf[&variant=auto|signed|unsigned]` — Lieferschein-PDF
- `POST /shipments.php?id=X&action=confirm` — Kunden-Unterschrift einstempeln (FormData mit signature_png, signer_name, gps_lat, gps_lon, signed_at). Backend setzt signed_status=1 und schließt die Expedition
**Notizen**
- `GET /note.php?order_id=X` — Liste (Betreff, Vorschau, Zeitstempel)
- `GET /note.php?order_id=X&file=<name>` — eine Notiz mit vollem Text
- `POST /note.php?order_id=X` — anlegen `{ subject, text }`
- `POST /note.php?order_id=X&file=<name>` — ändern
- `POST /note.php?order_id=X&file=<name>&delete=1` — löschen
**Medien**
- `GET /photo.php?relpath=X` — Datei abrufen; Auth über `awl_sso`-Cookie (same-origin, auch für `<img>`-Tags — kein `?jwt=` mehr nötig)
- `GET /photo.php?relpath=X&size=thumb&w=<px>` — serverseitiges Thumbnail (GD, gecacht, ETag → 304). **Der Weg für Kachel-Ansichten.** `size=small` ist etwas anderes: das liefert nur ein von Dolibarr vorgefertigtes Thumb, das es für PWA-Uploads gar nicht gibt — dort kam bis Modul-Version 1.5.1 immer das Original zurück
- `POST /delete_photo.php` — Foto löschen
- `POST /voice.php?order_id=X` — Sprachnotiz hochladen
- `POST /transcribe.php` — Whisper-Transkription anfordern
- `GET /pdf.php?id=X` — PDF abrufen (Final oder Preview); Auth über `awl_sso`-Cookie (same-origin)
**Templates**
- `GET /templates.php` — Bericht-Vorlagen
- `GET /odt_templates.php` — ODT-Deckblatt-Vorlagen
**Konfiguration**
- `GET /config.php` — Admin-Vorgaben zur Fotoqualität (`photo_maxside`, `photo_quality`); wird beim Start und nach jedem Login neu geladen, zwischengespeichert in `localStorage` für den Offline-Fall
## Lizenz
GPL v3+