Skip to content

Bilder-Upload

Die Bildverarbeitung ist local-first. Bilder werden sofort in Dexie gespeichert und anschließend bei Möglichkeit mit dem Server synchronisiert.

Einstieg-Composable

app/composables/media/images/useImageUpload.ts setzt das komplette Bilder-Upload-Feature zusammen.

Es kombiniert:

  • useImageUploadState(...)
  • useImageUploadCrud(...)
  • useImageUploadQueue(...)
  • useImageUploadServerSync(...)
  • usePendingImageUploadSync(...) für appweite Nachläufe ausstehender Uploads

UI-Schicht

app/components/Image/ImageUploadPanel.vue ist nur noch die Kompositionsschicht für den wiederverwendeten Upload-Bereich.

Die Panel-Zustände liegen in useImageUploadPanelController.ts. Dort werden Upload-Operationen, AI-Beschreibungsvorschläge, Beschreibungs-Popover und der Bildeditor-Dialog zusammengeführt.

Die sichtbaren Bausteine sind getrennt:

  • ImageUploadToolbar.vue für Dateiauswahl und File-Picker
  • ImageUploadCard.vue für einzelne Bildkarten inklusive Status-Overlays, Beschreibung, AI-Aktion, Retry und Bildeditor-Button

Der Bildeditor-Button ist an useSettingsStore().enableImageEditor gebunden. Ist die lokale Option aus, bleibt der Upload-Bereich ohne Editor-Aktion bedienbar.

Lebenszyklus

  • beim Mounten lädt es Bilder aus Server und lokalem Speicher
  • wenn der Online-Status auf true wechselt, wiederholt es wartende Uploads und verarbeitet ausstehende Löschungen
  • beim Unmounten widerruft es erzeugte Blob-URLs
  • app/plugins/04.pendingImageUploadSync.client.ts verarbeitet ausstehende Uploads und Löschungen zusätzlich beim App-Start und beim Browser-Online-Event
  • der Protokoll-Speichern-Flow verarbeitet ausstehende Bild-Uploads und Bildlöschungen acceptance-scoped, auch wenn keine Sektion dirty ist
  • acceptanceStore.saveLocalToServer(...) synchronisiert pending Bilder ebenfalls zentral, damit direkte Speichern-Pfade wie Finalisierung oder manueller Upload den Medienstand nicht umgehen
  • acceptanceStore.sync(...) lädt Serverbilder acceptance-weit nach; bei bewusstem Force-Reload wird der lokale Bildspeicher auf den Serverstand zurückgesetzt

Lokaler Speicher

Die Dexie-Tabelle images ist in app/db/parts/images.ts definiert.

Gespeicherte Bilddatensätze enthalten:

  • acceptanceId
  • parentType
  • parentId
  • sync state metadata
  • upload path
  • error state
  • pendingDelete
  • pdfSinglePage für Bilder, die im PDF auf einer eigenen Seite ausgegeben werden sollen
  • optionale Bildeditor-Annotationen
  • local image blob

CRUD-Ebene

useImageUploadCrud.ts handles:

  • Hinzufügen eines Bildes
  • Aktualisieren eines Bildes
  • Import eines Bildes aus Remote-Daten
  • Entfernen eines Bildes
  • Aktualisieren der Beschreibung
  • Aktualisieren der Bildeditor-Annotationen
  • Aktualisieren der PDF-Einzelseiten-Markierung
  • Upload eines einzelnen Bildes

Im Code verifizierte Implementierungsdetails:

  • Bilder werden clientseitig mit browser-image-compression komprimiert
  • Komprimierung kann pro Nutzung mit compressImages: false deaktiviert werden, wenn Originaldatei und MIME-Type erhalten bleiben müssen
  • lokale Persistenz passiert vor dem Upload
  • Uploads verwenden den gemeinsamen XHR-Helfer aus app/utils/upload.ts
  • geänderte Annotationen markieren das Bild als unsynchronisiert und werden beim komponentengebundenen Bild-Upload als JSON-Metadaten mitgeschickt
  • die PDF-Einzelseiten-Markierung markiert das Bild ebenfalls als unsynchronisiert und wird als pdfSinglePage zum Server gesendet

Queue-Ebene

useImageUploadQueue.ts wiederholt ausstehende Uploads, sobald wieder Online-Verbindung besteht.

usePendingImageUploadSync.ts nutzt dieselben Dexie-Metadaten ohne Component-State. Dadurch können lokal gepufferte Bilder auch dann synchronisiert werden, wenn die ursprüngliche Upload-Komponente nicht mehr gemountet ist. Dieser Pfad synchronisiert Datei, ID, Beschreibung, Annotationen und pdfSinglePage.

Der Nachlauf kann global laufen oder auf eine einzelne acceptanceId begrenzt werden. useFormSave() und useAcceptanceEditorPage().saveToServer(...) verwenden den acceptance-scoped Pfad, damit Speichern nur Medien des aktuellen Protokolls überträgt.

Der globale Nachlauf und der komponentengebundene Einzel-Upload teilen sich imageUploadLocks.ts. Der Lock läuft pro Bild-ID und verhindert, dass App-Start-/Online-Sync und ein geöffnetes Upload-Panel dieselbe Datei parallel hochladen. Wenn ein zweiter Pfad auf einen aktiven Upload trifft, wartet er auf dessen Ergebnis und liest danach den aktuellen Dexie-Stand.

usePendingImageChanges(...) beobachtet den lokalen Bild-Pending-Zustand einer Acceptance direkt über Dexie.liveQuery(). Änderungen an Bilddatei, Beschreibung, Annotationen, pdfSinglePage oder vorgemerkten Löschungen aktualisieren dadurch die Save-/Dirty-UI, ohne dass die Image-Helper zusätzliche Browser-Events auslösen müssen.

Server-Abgleich

useImageUploadServerSync.ts hält lokalen und entfernten Zustand für ein gemountetes Upload-Panel synchron. useAcceptanceImageServerSync.ts ergänzt denselben Vertrag acceptance-weit für den zentralen Acceptance-Sync, damit Serverbilder auch ohne gemountetes Parent-Panel wieder in Dexie landen.

Aufgaben:

  • lokale Bilder aus Dexie laden
  • Remote-Dateilisten pro Parent-Entity abrufen
  • fehlende Remote-Dateien in die lokale DB importieren
  • serverseitige Annotationen aus den Datei-Metadaten übernehmen
  • serverseitige pdfSinglePage-Markierungen aus den Datei-Metadaten übernehmen
  • per HEAD synchronisierte Remote-Assets prüfen
  • aufgeschobene Löschungen verarbeiten

Bildeditor-Annotationen

Wenn der Bildeditor aktiviert ist, speichert die App Annotationen zuerst lokal am Dexie-Bilddatensatz. Der komponentengebundene Upload sendet die Annotationen als JSON-Metadaten mit. Der Mock-Server speichert diese Metadaten im Upload-Datensatz und liefert sie über GET /files wieder als Array zurück.

Leere Annotationen werden als [] synchronisiert, damit entfernte Markierungen auch serverseitig gelöscht werden. Das Originalbild bleibt dabei erhalten; die Annotationen sind Metadaten, kein Ersatzbild.

Der Editor liegt unter app/features/image-editor/** und ist vom Upload-Code getrennt. Die wichtigsten Bausteine sind:

  • ImageEditorModal.vue: Modal-Shell mit Topbar, Toolbar, Canvas, Annotationsliste und Stil-Leiste
  • useImageEditor(): aktives Werkzeug, Zoom, Pan, Stil und Dirty-State
  • useImageAnnotations(): Annotationen, Auswahl, Undo/Redo und Historie
  • useEditorPointerEvents(): Pointer-Interaktion für Zeichnen, Auswählen und Verschieben
  • renderAnnotations.ts: rendert Annotationen bei Bedarf auf ein Bild, zum Beispiel für PDF-sichere Ausgabe

Unterstützte Werkzeuge sind select, pan, marker, rectangle, circle, arrow, text, freehand und delete. Persistiert werden strukturierte Annotationen mit Typ, Punkten oder Geometrie, Farbe, Strichstärke, Deckkraft, Text und Erstellzeit.

Wichtig für Offline-Sync: Der appweite Nachlauf aus usePendingImageUploadSync() lädt pending Bilder ohne gemountete Upload-Komponente hoch. Dieser Pfad synchronisiert dieselben Upload-Metadaten wie der komponentengebundene Upload.

Parallelität wird über denselben Image-ID-Lock wie im Upload-Panel begrenzt. Dadurch bleibt der globale Nachlauf als Backstop erhalten, ohne Doppeluploads zu erzeugen, wenn gleichzeitig eine Upload-Komponente aktiv ist.

Zusätzlich ist der normale Protokoll-Speichern-Button ein verlässlicher Sync-Pfad: Wenn nur Bildmetadaten geändert wurden, erkennt useUnsavedChanges() diesen Zustand über usePendingImageChanges(...), und useFormSave() ruft syncPendingUploads({ acceptanceId }) auf.

PDF-Einzelseiten

Normale Upload-Karten zeigen keinen technischen Dateinamen mehr als primäre Karteninfo. Stattdessen zeigen sie den fachlichen Kontext des Bildes und einen Schalter Eigene PDF-Seite.

Wenn pdfSinglePage aktiv ist, erscheint das Bild nicht zusätzlich in der normalen PDF-Bildertabelle. useCustomerPdfPreview.ts rendert es über app/utils/pdf/singlePageImages.ts wie den Bevollmächtigten-Nachweis auf einer eigenen Seite. Die Seite enthält:

  • eine Überschrift mit dem fachlichen Bildkontext
  • Kontextdaten aus dem zugehörigen Editor-Eintrag, z. B. Zähler, Raumdetail oder bauliche Veränderung
  • die Bildbeschreibung als Beschriftung
  • die in renderAnnotations.ts gerenderte Bildeditor-Version, falls Annotationen vorhanden sind

Die Markierung ist ein Bild-Metadatum am Dexie- und Server-Upload-Datensatz. Sie ändert nicht den Upload-Pfad oder den Server-Dateinamen.

Neue bildbasierte PDF-Abschnitte sollen die bestehende Funktion buildPdfImagesForEntries(...) aus app/utils/pdf/singlePageImages.ts nutzen. Die Sektion liefert nur noch:

  • die Einträge mit stabiler ID
  • einen Loader für die Uploads des Eintrags
  • toInlineImage(...) für die normale Tabellen-Galerie
  • getSinglePageTitle(...) und getSinglePageContextLines(...) für eigene PDF-Seiten

Einzelne Standalone-Bilder wie der Bevollmächtigten-Nachweis nutzen createUploadPdfPageImage(...), damit Seitenaufbau, Beschriftung und Bildmetadaten dieselbe Form behalten.

PDF-Sicherheit

Die PDF-Generierung verwendet Rohbilder nicht direkt.

app/utils/pdfImage.ts konvertiert Eingaben in sichere Raster-Data-URLs und lehnt nicht unterstütztes SVG für pdfmake ab.

Für Proxy-Nachweise in der Signatur wird WEBP beim PDF-Erzeugen clientseitig in ein PNG-Data-URL umgewandelt, weil pdfmake WEBP nicht zuverlässig direkt ausgibt.