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

16 KiB

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-Dialogeconfirm/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, layoutcomposite_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+