baustelle-pwa/ROADMAP.md

13 KiB
Raw Blame History

ROADMAP — Baustelle PWA

Stand: 2026-08-31 · Gegenstück: data/bericht (Modul-Version 1.5.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.


1. Nachgezogen aus Bericht 1.5.0 (2026-08-22, Modul 1.5.1)

Das Modul hatte mit 1.5.0 Infrastruktur bekommen, von der die PWA nichts hatte. Die Beschreibungen unten schildern jeweils den Zustand davor.

1.1 Echte Thumbnails statt Vollbilder [API]

Bis dahin lud die PWA für jede Kachel das komplette Foto:

  • loadThumbs() ruft api.getPhotoBlobUrl(rel, 'small') auf → photo.php?size=small
  • photo.php liefert nur dann ein Thumbnail, wenn Dolibarr eines unter thumbs/<name>_small.<ext> vorgefertigt hat
  • Die PWA lädt ihre Fotos aber per orders.php?action=upload_photo hoch, und dort wird nie vignette() aufgerufen → es existiert nie ein Thumb → es kommt immer das Original
  • Bei 30 Baustellenfotos sind das im Mobilfunknetz 30 × 0,51 MB (bei über die Dolibarr-Oberfläche hochgeladenen Originalen entsprechend mehr)

Das Modul kann das seit 1.5.0 längst: bericht_attachment_thumb($full, $maxside) in lib/bericht.lib.php (GD, Cache unter bericht/thumbs/, EXIF-Rotation, Speicher-Notbremse). api/photo.php lädt diese lib bereits.

  • [API] photo.php um size=thumb&w=<px> erweitern → bericht_attachment_thumb(), mit ETag aus mtime und Cache-Control: private, max-age=…, immutable
  • loadThumbs() umbauen: <img src="…api/photo.php?…&size=thumb"> direkt setzen statt fetch → Blob → ObjectURL. Damit greifen HTTP-Cache und loading="lazy" wirklich; heute sind beide wirkungslos, weil der Blob schon vorher komplett geladen wird.
  • Sequenzielle Schleife auflösen — for (const t of thumbs) { await … } lädt Foto für Foto nacheinander. Beim <img>-Weg erledigt der Browser das parallel von selbst.
  • Doppel-Ladung entfernen: schlägt size=small fehl, wird das Original noch einmal geladen.

1.2 Aufnahmezeit statt Dateidatum [API]

1.5.0 hat einen eigenen EXIF-Parser bekommen (bericht_jpeg_taken_at() / bericht_file_taken_at()), weil im Prod-Container das exif-Modul fehlt. Der Editor sortiert Anhänge damit nach Aufnahmezeit. orders.php?action=photos lieferte nur filemtime.

  • [API] taken_at je Datei ausgeben
  • Fotoliste nach Aufnahmezeit sortieren (Auswahl gemerkt, wie im Editor)

1.3 Seiten-Titel kommt nie an [API]

api/pages.php kann title einer Seite setzen, die PWA rendert ihn (page-title-badge, openPageActionsModal(…, title)) — aber api/reports.php gibt title im pages-Array nicht aus. Ein im Editor gesetzter Titel ist in der PWA also unsichtbar, und ein in der PWA gesetzter Titel scheint nach dem Neuladen verschwunden.

  • [API] 'title' => $p->title in reports.php ergänzen (eine Zeile)

1.4 Bearbeitete Seiten und Raster-Layouts [API]

Seit 1.5.0 hat eine Seite composite_path (das im Editor gebaute Seitenbild) und layout (single, grid_2, grid_4, before_after, title_only, …). Die PWA zeigt stur source_path:

  • Nach Bearbeitung im Desktop-Editor zeigt die PWA weiter das Rohbild ohne Anmerkungen

  • Bei einer Rasterseite ist source_path nur eines von bis zu sechs Bildern

  • Bei title_only gibt es gar keine Quelle → -Kachel

  • [API] composite_path in reports.php mit ausgeben

  • PWA zeigt composite_path, wenn vorhanden, sonst source_path

  • Layout-Kennzeichen an der Kachel (z. B. „⊞ 4er-Raster"), damit klar ist, warum dort mehr drauf ist als das eine Foto


2. Verbesserungen unabhängig vom Modul

2.1 Browser-Dialoge raus (10 Stück)

confirm() / alert() / prompt() widersprechen der Hausregel und sehen in der Standalone-PWA auf Android wie ein Systemfehler aus. Ein sauberes Muster liegt schon vor: das „Bericht löschen"-Modal in router.on('/reports/:id').

Stelle Zeile (vor dem Umbau)
PIN stimmt nicht überein alert 295
Bericht finalisieren confirm 1111
PIN-Schutz deaktivieren confirm 1264
Material-Eintrag löschen confirm 1432
Seite aus Bericht entfernen confirm 1809
Foto löschen confirm 2701
Nicht hochgeladenes Foto löschen confirm 3019
Mess-Skala: Länge eingeben prompt 3409
Ungültiges Längenformat alert 3419
„Zuerst kalibrieren" alert 3440
  • Eine confirmDialog({title, text, danger})-Hilfsfunktion (Promise) + inputDialog() für die Längeneingabe, alle zehn Stellen darauf umstellen

2.2 Fotos offline sichtbar halten

Der Service Worker reichte alles unter /custom/bericht/api/ ungecacht durch. Auf der Baustelle ohne Netz ist damit kein einziges schon angesehenes Foto verfügbar.

  • Eigener Runtime-Cache baustelle-media für photo.php-GETs, cache-first mit Größenbegrenzung (LRU, z. B. 200 Einträge). Die Thumb-URLs aus 1.1 sind dafür der richtige Kandidat — klein und unveränderlich.
  • Vom App-Shell-Cache getrennt halten, damit ein Deploy die Medien nicht wegräumt

3. Textnotizen (2026-08-22)

Getippte Notiz zum Auftrag — für alles, was man lieber schreibt als diktiert: Zählernummern, Absprachen, Maße.

  • 📝 Notizen (n)" am Auftrag, direkt unter der Sprachnotiz
  • Liste mit Betreff, zweizeiliger Vorschau und Zeitstempel; Antippen öffnet zum Lesen und Ändern, 🗑 löscht (mit Rückfrage)
  • Editor: Betreff-Feld + Textfeld über die volle Höhe, Speichern über ✓ in der Kopfzeile, Rückfrage beim Verlassen mit ungespeicherten Änderungen
  • Ablage als notiz_<betreff>_<datum>.txt im Auftragsordner (api/note.php) — genau der Weg der Sprachnotiz. Damit auch im Dolibarr-Auftrag und in der Anhänge-Spalte des Bericht-Editors sichtbar.
  • Notizen sind aus „Weitere Dokumente" ausgefiltert — sie haben ihre eigene Sektion und stünden dort sonst doppelt

Nicht umgesetzt (und bewusst verworfen): eine Zeichenfläche für echte Handschrift. Am Handy ohne Stift ist das unbrauchbar.

4. Teilen aus WhatsApp (2026-08-30)

Bilder und Beschreibungstexte, die die Kundin per WhatsApp schickt, landen direkt im Auftrag. Vorher endete jeder Teilen-Vorgang in einer Sackgasse:

  • share.html fragte api.getToken() — ein JWT in IndexedDB, das es seit der SSO-Umstellung gar nicht mehr gibt (api.login() legt nur noch user ab, die Sitzung steckt im HttpOnly-Cookie awl_sso). Ergebnis: immer „Bitte zuerst anmelden", und der Weg „Zur App" verwarf das Geteilte.

  • Der Service Worker sicherte nur Dateien. Ein Text-Share (oder Bild + Bildunterschrift) schrieb nichts in IndexedDB → „Keine geteilten Fotos gefunden".

  • Anmeldung über api/verify.php (Cookie) statt über das nicht mehr existierende Token

  • Anmelden in der Teilen-Seite (Passwort + Passkey) — das Geteilte bleibt dabei gesichert

  • Service Worker sichert title, text und url mit; Weiterleitung auch ohne Datei

  • Auswahl-Seite: Bildvorschau, Betreff + Text zum Nachbessern, Auftragssuche (ohne Suchbegriff die offenen Aufträge, mit Suchbegriff alle)

  • Ablegen: Bilder über die normale Upload-Queue (Persist-First), Text als Notiz über api/note.php. Schlägt die Notiz fehl (kein Netz), bleibt der Text gesichert und der Vorgang ist wiederholbar

  • Banner in der App, wenn noch etwas Geteiltes unabgelegt bereitliegt

  • Mehrere Nachrichten hintereinander: die letzten drei Aufträge stehen unter „Zuletzt benutzt" oben; innerhalb von 30 Minuten ist der letzte vorausgewählt (danach nur noch Schnellzugriff — eine Dauer-Vorauswahl würde irgendwann im falschen Auftrag landen)

5. Offline lesbar statt „Failed to fetch" (2026-08-31)

Gemeldet von Eddy am 2026-08-31: Die App zeigte beim normalen Öffnen nur ⚠️ Failed to fetch", während das Teilen aus WhatsApp weiter funktionierte.

Befund

  • Prod-Deploy vollständig, alle Dateien liefern 200, die API antwortet korrekt (403 ohne X-Requested-With, 401 ohne Anmeldung) — kein Server- oder Deploy-Fehler.
  • Im frischen Desktop-Chromium lief der ganze Weg sauber durch (auth.phpverify.phporders.php?open=1, alle 200).
  • „Failed to fetch" ist die rohe Browsermeldung eines fetch(), das gar nicht bis zum Server kam. Sie stand 1:1 im UI, weil die Routen im catch stumpf e.message anzeigten.
  • Warum es beim Teilen nicht auffiel: seit eaad796 merkt sich share.html die letzten Aufträge lokal und kommt ohne Netz aus. Die Hauptapp hatte für die Listen keinerlei Offline-Fallbacklib/offline.js puffert nur Foto-Uploads.

Umgesetzt (Muster aus KB #1037 / #1041, dort im Stundenzettel verifiziert)

  • Netzfehler kenntlich machenlib/api.js wandelt den TypeError aus fetch() in einen eigenen Fehler mit offline = true und deutschem Text („Keine Verbindung zum Server" / „Zeitüberschreitung"). Ein 4xx/5xx trägt die Markierung nicht und wird weiterhin als Serverfehler gemeldet.
  • isTransient() in lib/offline.js nachgezogen — die Funktion erkannte Netzfehler an err instanceof TypeError. Ohne Anpassung hätte ein Foto aus dem Funkloch als dauerhaft fehlgeschlagen gegolten und wäre nach sechs Versuchen in der Quarantäne gelandet, statt hochgeladen zu werden.
  • Datenspiegel (offline.mirrorSave/mirrorLoad/mirrorClear) — jede erfolgreiche Antwort wird in IndexedDB gespiegelt: Auftragsliste, Kundenliste, Berichte sowie Auftrags- und Kundendetails (letztere auf 30 Einträge begrenzt, der gerade geschriebene Schlüssel ist beim Aufräumen geschützt — KB #1037).
  • Sichtbares Band📴 Kein Netz — angezeigt wird der Stand von HH:MM Uhr". Ohne den Hinweis hält man alte Zahlen für aktuell. Es gehört zur Ansicht und wird vom Router bei jedem Routenwechsel zurückgesetzt.
  • Suche ohne Netz filtert im gespiegelten Stand statt ins Leere zu laufen — mit einmaligem Hinweis, dass nur der gespeicherte Stand durchsucht wird.
  • Beim Abmelden wird der Spiegel gelöscht — sonst sähe der nächste Benutzer am selben Gerät offline die Aufträge, Kunden und Preise des vorherigen (KB #1041).
  • sw.js: echter Fallback. Der Same-Origin-Zweig endete auf caches.match(e.request); fand der Cache nichts, wurde respondWith(undefined) aufgerufen — und die Anfrage scheiterte erneut mit genau der Meldung, die der Fallback verhindern sollte. Jetzt kommt immer eine echte Response: Seite → App-Hülle aus dem Cache, sonst eine erklärende Seite; API → 503 mit JSON.
  • Nur vollständige Antworten cachen (ok && status === 200 && type !== 'opaque') — eine 206 (Teilinhalt beim Audio-Seek) liefert der Worker später als kaputte Datei aus.
  • Precache nach Deploy — der Cache trägt nach jedem Deploy einen neuen Namen und startet leer, app.js/app.css inklusive. Eine feste Liste hilft nicht, weil die URLs ?v=<mtime> tragen. Die Seite meldet dem Worker nach dem Laden, was sie tatsächlich geholt hat (PRECACHE-Nachricht), er cacht es einzeln nach (KB #1041).

Verifiziert (2026-08-31, lokale Testinstanz, Chromium)

  • Spiegel wird geschrieben (mirror:orders:open, :customers, :reports, :detail:order:<id>, :detail:customer:<id>)
  • fetch auf Fehler gesetzt: Auftragsliste zeigt 6 Aufträge aus dem Spiegel + Band „Stand von 19:03 Uhr"; Kundenliste ohne Spiegel zeigt die verständliche Meldung statt „Failed to fetch"
  • Suche „Rolfs" ohne Netz → 2 Treffer aus dem Spiegel + Hinweis-Toast
  • Band verschwindet auf Routen ohne Spiegel und nach Rückkehr des Netzes
  • Echtes Funkloch (context.setOffline(true) + reload()): App startet aus dem Cache, zeigt Band und 6 Aufträge; im Cache liegen app.js, app.css und alle lib/*.js mit ihrem ?v= — vorher wären sie nach einem Deploy nicht dagewesen
  • Konsole beim Offline-Start: nur Netzfehler, keine JS-Fehler

Bewusst nicht gebaut

Keine Schreib-Warteschlange für Auftragsdaten. Die bräuchte Idempotenz und Konfliktbehandlung (zwei Geräte am selben Auftrag). Für Fotos existiert sie längst (Persist-First-Queue), für alles andere gilt: lesen ja, ändern nur mit Netz.

Erledigt

2026-08-22

Abschnitte 1, 2 und 3 vollständig. Dazu nebenbei behoben:

  • .modal-header, .close-btn und .modal-body hatten überhaupt kein CSS, obwohl das Markup sie seit Längerem nutzt (u. a. beim Löschen eines Berichts) — der Schließen-Knopf saß dadurch unter der Überschrift statt daneben.
  • Das Laden eines Thumbnails überschrieb den kompletten Kachel-Inhalt (t.innerHTML = …) und nahm die Overlays mit: Auswahl-Häkchen in der Mehrfachauswahl und Seitennummer der Bericht-Seiten waren weg, sobald das Bild da war.
  • Beim Abmelden bleiben keine Kundenfotos mehr auf dem Gerät (CLEAR_MEDIA an den Service Worker + clearPhotoCache()).

Davor

Siehe README (Feature-Liste) und die Commit-Historie: SSO-Migration auf awlauth, WebAuthn-Login, Neuer-Auftrag-Formular, Live-Kamera mit Persist-First-Queue, Lieferschein-Unterschrift.