Appearance
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.vuefür Dateiauswahl und File-PickerImageUploadCard.vuefü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
truewechselt, wiederholt es wartende Uploads und verarbeitet ausstehende Löschungen - beim Unmounten widerruft es erzeugte Blob-URLs
app/plugins/04.pendingImageUploadSync.client.tsverarbeitet 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 umgehenacceptanceStore.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:
acceptanceIdparentTypeparentId- sync state metadata
- upload path
- error state
pendingDeletepdfSinglePagefü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-compressionkomprimiert - Komprimierung kann pro Nutzung mit
compressImages: falsedeaktiviert 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
pdfSinglePagezum 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-LeisteuseImageEditor(): aktives Werkzeug, Zoom, Pan, Stil und Dirty-StateuseImageAnnotations(): Annotationen, Auswahl, Undo/Redo und HistorieuseEditorPointerEvents(): Pointer-Interaktion für Zeichnen, Auswählen und VerschiebenrenderAnnotations.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.tsgerenderte 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-GaleriegetSinglePageTitle(...)undgetSinglePageContextLines(...)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.