45 KiB
Vision / Inventory Feature
Übersicht
Hausinventar verwalten: Gegenstände per Kamera oder Barcode erfassen, KI erkennt Titel + Kategorie, Raum-Zuordnung, vollständige Asset-Pflege.
Datenmodell
rooms
id, userId, name, groupType ('living'|'functional'|'outdoor'),
icon (string, lucide icon name, z.B. 'sofa'|'monitor'|'bath'|...),
sortOrder, updatedAt, deletedAt, purgedAt, version
assets
id, userId, roomId, title, category,
status ('draft'|'complete'),
condition ('new'|'good'|'fair'|'poor'|null),
brand, model, serialNumber,
purchasePrice (real|null), purchaseYear (integer|null),
notes (markdown|null), coverImageId (→ imageBlobs.id|null),
updatedAt, deletedAt, purgedAt, version
coverImageId = Primärbild (erstes aufgenommenes Foto). Weitere Bilder in assetImages.
Cover kann auf Detailseite durch ein assetImages-Bild ersetzt werden (→ updateAsset({ coverImageId })).
assetImages
id, userId, assetId, imageId (→ imageBlobs.id),
sortOrder, updatedAt, deletedAt, purgedAt, version
Bilder werden nicht lokal gecacht — imageId referenziert Server-seitigen Blob.
assetPersons — Zuordnung Asset ↔ Person
id, userId, assetId, personId (→ contexts.id, wo type='person'),
updatedAt, deletedAt, purgedAt, version
Personen des Typs family (PersonSubType aus contexts.meta.personSubType).
Mehrere Personen pro Asset möglich (z.B. "gehört Mama und Papa").
imageBlobs — bereits vorhanden, wird wiederverwendet.
user_settings (für Vision-API-Keys)
userId, key, value (JSON), updatedAt
PRIMARY KEY (userId, key)
vision_usage (Rate Limiting)
userId, date (YYYY-MM-DD), count
PRIMARY KEY (userId, date)
Erfassungs-Modi
Foto-Modus (Loop)
[📷 Foto-Modus] → Raum wählen (einmalig)
→ Loop: Foto → KI-Erkennung → LabelConfirm → createAsset(draft) → "Nächstes"
→ [Fertig] → Dashboard
Barcode-Modus (Loop)
[🔍 Barcode-Modus] → Raum wählen (einmalig)
→ Loop: Barcode scannen → Open Food Facts → LabelConfirm → createAsset(draft) → "Nächster"
→ [Fertig] → Dashboard
Manuell (Einzel)
[+ Manuell] → Raum wählen → Titel eingeben → createAsset(draft) → Detailseite
UI-Struktur
/inventory Dashboard (Stats + Quick Actions)
/inventory/rooms Raum-Übersicht (3-Spalten-Grid, gruppiert)
/inventory/items Alle Gegenstände (gefiltert, Grid/Liste)
/inventory/capture/photo?roomId= Foto-Loop
/inventory/capture/barcode?roomId= Barcode-Loop
/inventory/[id] Asset-Detailseite
Asset-Liste /inventory/items (Mockup)
┌──────────────────────────────────────────────────────┐
│ Gegenstände [📷 Fotografieren][+ Manuell]│
│ 1 Gegenstand erfasst │
├──────────────────────────────────────────────────────┤
│ [🔍 Suchen nach Bezeichnung, Marke...] │
│ [📍 Alle Räume ▾] [🏷 Alle Kategorien ▾] [⊙ Alle Status ▾] [⊞][☰]│
├──────────────────────────────────────────────────────┤
│ ▼ 🟢 Ankleidezimmer · Eltern (1) │ ← collapsible Section
│ ┌─────────────┐ │
│ │ [Foto] │ │
│ │ [🛡] 17,00€│ ← Status-Icon links, Preis rechts │
│ └─────────────┘ │
│ Flauschige Decke │
│ 📍 Ankleidezimmer · Eltern │
└──────────────────────────────────────────────────────┘
- Suchfeld: filtert nach Titel, Marke, Modell (client-seitig, Dexie)
- Filter-Dropdowns: Raum, Kategorie, Status (draft/complete) — kombinierbar
- Grid/Liste-Toggle: Grid = Foto-Cards; Liste = kompakte Zeilen
- Sections: ein collapsible Block pro Raum (leer = ausgeblendet wenn gefiltert)
- Card: Cover-Bild, Preis-Badge unten rechts, Status-Icon (Schild) unten links
- Status-Icon: 🟢 complete, 🟡 draft (Badge-Farbe an Section-Header)
- Kein eigener Raum → Section "Ohne Raum"
Räume-Übersicht /inventory/rooms (Mockup)
┌──────────────────────────────────────────┐
│ Räume [+ Raum anlegen]│
│ Verwalte deine Räume │
├──────────────────────────────────────────┤
│ Wohnbereiche │ ← Gruppen-Header
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │[👕] Ankl.│ │[🖥] Arb. │ │[🍴] Ess. │ │ ← 3-Spalten-Grid
│ │1 Gegenst.│ │0 Gegenst.│ │0 Gegenst.│ │
│ └────── ···┘ └────── ···┘ └────── ···┘ │ ← ··· Context-Menu
├──────────────────────────────────────────┤
│ Funktionsräume │
│ ... │
└──────────────────────────────────────────┘
- Click auf Karte →
/inventory/rooms/[roomId](Asset-Liste) ···Menu: Umbenennen, Löschen (Soft-Delete)- Icons: raumspezifisch (lucide), hardcodierte Mapping-Tabelle Name→Icon
- Asset-Zähler: liveQuery auf
assets.roomId, nicht-deleted
Dashboard-Layout (Mockup)
┌─────────────────────────────────────┐
│ Willkommen! │
│ Übersicht deines Hausinventars │
├─────────────────────────────────────┤
│ 📷 Einfach fotografieren │ ← Primär-CTA (Vollbreite, dunkel)
├──────────────────┬──────────────────┤
│ + Gegenstand │ + Raum │ ← Sekundär (je halbe Breite)
├──────────────────┴──────────────────┤
│ Gegenstände [📦] │ Räume [🏠] │ ← Stats-Row (2-spaltig)
│ 1 │ 24 │
├─────────────────────────────────────┤
│ Gesamtwert [€] │ ← Breite Stats-Card
│ 17,00 € │
│ Der erfasste Gesamtwert gibt dir │
│ eine gute Orientierung für deine │
│ Versicherungssumme. │
├─────────────────────────────────────┤
│ → Räume & Gegenstände anzeigen │ ← Link zu /inventory/rooms
└─────────────────────────────────────┘
- "Einfach fotografieren" → RoomSelectSheet → Foto-Loop
- "+ Gegenstand" → RoomSelectSheet → manuell anlegen → Detailseite
- "+ Raum" → inline Modal (Name + Gruppe)
- Stats:
liveQueryaufassets+rooms(nicht-deleted, SummepurchasePrice) - Gesamtwert: Summe aller
assets.purchasePrice(null = nicht eingerechnet)
Architektur-Entscheidung: Sync vs. Online-only
Metadaten (rooms, assets, assetPersons, assetImages-Referenzen):
→ normaler Push/Pull, lokal in Dexie. Datenmenge trivial (~500 KB bei 500 Gegenständen).
→ Offline-Lesezugriff sinnvoll (Liste/Suche ohne WLAN).
Bilder — gewählter Ansatz: Server-Upload, kein lokaler Blob
storeImage() schreibt immer in Dexie (kein Bypass). 500 × 5 MB = 2,5 GB lokal — nicht akzeptabel.
→ Neues uploadInventoryImage(blob): Promise<string>:
- Bild client-seitig komprimieren: Canvas
toBlob('image/jpeg', 0.75)+ max-width 1920px → ~300 KB. HEIC wird durchaccept="image/jpeg,image/png,image/webp"im Input verhindert. - Direkt POST zu
/api/inventory/images(neuer Endpoint, kein Sync-Push) - Server speichert in
imageBlobs, gibtimageIdzurück - Client speichert nur
imageIdim Asset — kein Dexie-Blob getImageUrl(id)fetcht via/api/sync/blob/{id}— immer online
→ Folge: Kamera-Erfassung erfordert Online-Verbindung (akzeptabel für PWA mit WLAN).
→ storeImage() bleibt unverändert für alle anderen Bild-Contexts (Editor-Paste etc.).
→ Neuer Server-Endpoint: POST /api/inventory/images (multipart, authMiddleware)
Kein Online-only-Controller für Metadaten nötig — bisherige Architektur skaliert gut.
Schlüssel-Entscheidungen
- API-Key pro User — in Settings-Seite eingeben, AES-256-GCM verschlüsselt in
user_settings SETTINGS_ENCRYPTION_KEYin.env(plain, self-hosted)- Rate Limit — 100 Vision-Calls/Tag pro User, DB-basiert via
vision_usage - Provider — OpenAI
gpt-4o-mini(detail:low, ~$0.0003/Bild) oder Geminigemini-1.5-flash - Kategorien — hardcoded Liste, kein eigenes Table:
null (Keine Kategorie), Elektronik, Möbel, Kleidung, Schmuck, Kunstwerke, Sportgeräte, Haushaltsgeräte, Haustechnik, Werkzeuge, Musikinstrumente, Bücher & Medien, Spielzeug, Sammlerstücke, Gartengeräte, Fahrzeugzubehör, Sonstiges - Räume — eigene Tabelle (kein Kontext-Typ), Soft-Delete, Default-Seed beim ersten Start
- Status —
draftnach Schnellerfassung,completenach Nachbearbeitung - Löschung — Soft-Delete überall, wie Wiki-Pages
- Personen — nur
family-Subtype auswählbar (filter:contexts.type='person'+meta.personSubType='family'); Zuordnung viaassetPersons-Tabelle (n:m)
Phasen
Dependency-Graph
Phase 1 (Datenfundament)
├─→ Phase 2 (Vision-Backend) [nur Server, unabhängig von Client-Schema]
├─→ Phase 5 (Settings UI) [benötigt Phase 2 + Phase 1 Client]
└─→ Phase 4 (Inventar-UI) [benötigt Phase 1 vollständig]
└─→ Phase 6 (CommandBar) [benötigt Phase 4 Repositories]
Phase 2 + Phase 5 → Phase 3 (Erfassungs-Flow) [braucht API + UI-Key-Config]
Phase 4 → Phase 7 (Docs) [nach stabilem UI]
Parallelisierung:
- Phase 2 Schritte 2.1–2.4: vollständig parallel (je eigene neue Datei)
- Phase 5 + Phase 2.5: parallel nach Phase 2.1–2.4
- Phase 4 Schritte 4.1–4.4: parallel (je eigene neue Datei)
- Phase 3 + Phase 4 können nach Phase 1 parallel beginnen (3 benötigt noch Phase 2+5)
Phase 1 — Datenfundament
Requires: nichts (Startpunkt) Produces:
shared/src/types.ts—Room,Asset,AssetImage,AssetPersonTypes exportiertshared/src/sync.ts—SyncChangesenthält 4 neue Tabellenserver/drizzle/0009_inventory.sql— Migration ausführbarclient/src/lib/db/schema.tsv15 — Dexie öffnet ohne Fehlerclient/src/lib/db/repositories.ts— alle CRUD-Funktionen +seedDefaultRoomsIfNeededexportiert- Server
sync-service.ts+routes/sync.ts— Push/Pull für 4 neue Entitäten
Ziel: Shared Types, Server- und Client-Schema, Repositories, Sync.
Schritt 1.1 — Shared Types
- Datei:
shared/src/types.ts+ Room extends SyncEntity+ Asset extends SyncEntity(alle Felder inkl.status,condition, etc.)+ AssetImage extends SyncEntity+ AssetPerson extends SyncEntity(assetId,personId)
- Datei:
shared/src/sync.tsSyncChanges:+ rooms, assets, assetImages, assetPersonsSYNC_PULL_TABLE_ORDER:[..., 'rooms', 'assets', 'assetImages', 'assetPersons']SYNC_PUSH_TABLE_ORDER: gleich
Schritt 1.2 — Server DB-Schema + Migration
- Datei:
server/src/db/schema.ts-
rooms,assets,assetImages,assetPersons,user_settings,vision_usage
assetPersons: FK(assetId, userId)→ assets + FK(personId, userId)→ contexts- Muster: Composite PK
(id, userId), DrizzlesqliteTable
-
- Datei:
server/drizzle/0009_inventory.sql(auto-generiert viadrizzle-kit generate)
Schritt 1.3 — Client Dexie Schema v15
- Datei:
client/src/lib/db/schema.ts- Version 15: +
rooms: '&id, groupType, deletedAt' -
assets: '&id, roomId, status, deletedAt'
-
assetImages: '&id, assetId, deletedAt'
-
assetPersons: '&id, assetId, personId, deletedAt'
- Version 15: +
Schritt 1.4 — Client Repositories
- Datei:
client/src/lib/db/repositories.ts- Room:
createRoom,getRooms,updateRoom,softDeleteRoom - Asset:
createAsset,getAsset,getAssetsByRoom,getAssetsAll,getAssetsByStatus,updateAsset,softDeleteAssetgetAssetsAll()— alle nicht-deleted Assets (für CommandBar +/inventory/items)getAssetsByStatus('draft'|'complete')— für Status-Filter
- AssetImage:
addAssetImage,getAssetImages,softDeleteAssetImage - AssetPerson:
addAssetPerson,getAssetPersons,removeAssetPerson(Soft-Delete) - Helper:
getFamilyPersons()— filtertdb.contextsnachtype='person'+meta.personSubType='family' - Seed-Funktion:
seedDefaultRoomsIfNeeded(userId)— prüftsettings-Flag'inventory.seeded'
- Room:
Schritt 1.5 — Sync-Erweiterung
- Datei:
server/src/lib/sync-service.ts-
mapRoom,mapAsset,mapAssetImage,mapAssetPerson
pushChanges+pullChangesum 4 Entitäten erweitern
-
- Datei:
server/src/routes/sync.tsVALID_TABLES+'rooms','assets','assetImages','assetPersons'
- Datei:
client/src/lib/sync/syncService.ts- Push/Pull-Mapping für 4 neue Entitäten
Verifikation 1:
npm run devstartet ohne Fehler- Browser-Console: kein Dexie-Schema-Fehler beim Öffnen der App
GET /api/sync/pullmit gültigem Token → Response enthält Keysrooms,assets,assetImages,assetPersons(leere Arrays OK)POST /api/sync/pushmit{ rooms: [{ id, name, groupType, ... }] }→ HTTP 200db.rooms.count()in Browser-Console → 0 (kein Fehler)
Phase 2 — Vision-Backend
Requires: Phase 1 abgeschlossen (Server-Schema mit user_settings, vision_usage vorhanden)
Produces:
server/src/lib/user-settings-service.ts— exportiertgetSetting,setSetting,getVisionConfigserver/src/lib/rate-limiter.ts— exportiertcheckAndIncrementserver/src/lib/vision-service.ts— exportiertgetProvider(name, apiKey): VisionProviderserver/src/lib/barcode-service.ts— exportiertlookupBarcode(ean)server/src/routes/vision.ts— Router mit 4 Endpointsserver/src/routes/inventory.ts— Router mitPOST /api/inventory/imagesserver/.env.example—SETTINGS_ENCRYPTION_KEY+VISION_RATE_LIMITdokumentiert
Ziel: API-Key-Verwaltung, Rate Limiting, Vision + Barcode-Proxy auf dem Server.
Schritt 2.1 — User Settings Service [parallel]
- Datei:
server/src/lib/user-settings-service.ts(NEU)getSetting(userId, key): string | nullsetSetting(userId, key, value): voidgetVisionConfig(userId): { provider, apiKey } | null- AES-256-GCM Verschlüsselung mit
SETTINGS_ENCRYPTION_KEYaus.env
- Datei:
server/.env.example: +SETTINGS_ENCRYPTION_KEY=,VISION_RATE_LIMIT=100
Schritt 2.2 — Rate Limiter [parallel]
- Datei:
server/src/lib/rate-limiter.ts(NEU)checkAndIncrement(userId, limit): { allowed, remaining }INSERT OR IGNORE+UPDATEaufvision_usage(date = heute)
Schritt 2.3 — Vision Service [parallel]
- Datei:
server/src/lib/vision-service.ts(NEU)
Prompt (beide Provider):interface VisionProvider { recognize(base64, mime): Promise<{label, category, candidates}> } class OpenAIVisionProvider // gpt-4o-mini, detail:low class GeminiVisionProvider // gemini-1.5-flash export function getProvider(name, apiKey): VisionProviderIdentify the main object. Return JSON: { "label": "<German, max 60 chars>", "category": "<Elektronik|Möbel|Kleidung|Schmuck|Kunstwerke|Sportgeräte| Haushaltsgeräte|Haustechnik|Werkzeuge|Musikinstrumente| Bücher & Medien|Spielzeug|Sammlerstücke|Gartengeräte| Fahrzeugzubehör|Sonstiges>", "candidates": ["<alt1>", "<alt2>"] }
Schritt 2.4 — Barcode Service [parallel]
- Datei:
server/src/lib/barcode-service.ts(NEU)lookupBarcode(ean): Promise<{label, category?} | null>- Primär: UPCitemdb
https://api.upcitemdb.com/prod/trial/lookup?upc={ean}(breite Produktdatenbank, 100/Tag frei) - Fallback: Open Food Facts
https://world.openfoodfacts.org/api/v0/product/{ean}.json(nur Lebensmittel) - Letzter Fallback:
{ label: ean }(roher EAN-String) - Kategorie-Mapping: UPCitemdb
category→ Inventar-Kategorieliste
Schritt 2.5 — Vision Route + Image-Upload Endpoint [nach 2.1–2.4]
- Datei:
server/src/routes/vision.ts(NEU)POST /api/vision/recognize — multipart { image: File } POST /api/vision/barcode — JSON { ean: string } ← nur EAN-Lookup, kein Bild-Input GET /api/vision/settings — { provider, apiKeySet, dailyUsage, dailyLimit } PUT /api/vision/settings — { provider?, apiKey? } - Datei:
server/src/routes/inventory.ts(NEU, separater Router)
Routen: allePOST /api/inventory/images — multipart { image: File } → speichert in imageBlobs, gibt { id } zurückauthMiddleware. Rate-Limit-Check nur aufPOST /api/vision/recognize— nicht auf/api/vision/barcode(nutzt UPCitemdb, nicht die Vision-API des Users). UPCitemdb-Limit (100/Tag server-weit, free tier) ist ein separates Server-seitiges Limit — nicht viavision_usagegetrackt. - Datei:
server/src/index.ts: + 4 Zeilen (beide Routes registrieren)
Verifikation 2:
PUT /api/vision/settingsmit gültigem{ provider: 'openai', apiKey: '...' }→ HTTP 200GET /api/vision/settings→{ provider: 'openai', apiKeySet: true, dailyUsage: 0, dailyLimit: 100 }POST /api/vision/recognizemit JPEG-Testbild → HTTP 200, Body enthältlabel(string),category(string aus Kategorienliste),candidates(Array)POST /api/vision/barcodemit{ ean: '4006381333931' }(Stabilo-Stift) → HTTP 200,labelnicht leerPOST /api/inventory/imagesmit JPEG multipart → HTTP 200,{ id: '...' }- Zweiter Recognize-Call →
dailyUsage: 1in GET settings -
- Call (simuliert via DB-Insert) → HTTP 429
Phase 3 — Erfassungs-Flow (Client)
Requires: Phase 1 + Phase 2 + Phase 5 abgeschlossen (API-Key konfigurierbar, Endpoints live) Produces:
client/src/lib/services/visionService.ts—compressImage,uploadInventoryImage,recognizePhoto,recognizeBarcodeexportiertclient/src/lib/services/visionSettingsService.ts—getVisionSettings,saveVisionSettingsexportiertclient/src/lib/components/LabelConfirm.svelte— rendert ohne Fehlerclient/src/lib/components/PhotoCapture.svelte— rendert ohne Fehlerclient/src/lib/components/BarcodeScan.svelte— rendert ohne Fehlerclient/src/lib/components/RoomSelectSheet.svelte— rendert ohne Fehler- Zwei neue Routen:
/inventory/capture/photo+/inventory/capture/barcode
Ziel: Kamera-Flow, Barcode-Scan, Raum-Auswahl, Loop-Seiten.
Schritt 3.1 — Vision Service Client
- Datei:
client/src/lib/services/visionService.ts(NEU)compressImage(blob): Promise<Blob>— max-width 1920px + quality 0.75, JPEG-Output via Canvas. HEIC wird durchaccept-Attribut im Input vermieden (kein HEIC-Decoder nötig).uploadInventoryImage(blob): Promise<string>— POST/api/inventory/images, gibt imageId zurückrecognizePhoto(blob): Promise<RecognizeResult>— POST/api/vision/recognizerecognizeBarcode(ean: string): Promise<RecognizeResult>— POST/api/vision/barcode- Kein ZXing client-seitig — EAN wird manuell eingegeben (Nummernpad oder physischer Barcode-Scanner)
- Barcode-via-Foto entfällt aus Phase 3:
POST /api/vision/barcodeakzeptiert nur{ ean: string }, kein Bild. Foto→EAN wäre ein separater Endpoint oder via BarcodeDetector API (spätere Phase).
- Datei:
client/src/lib/services/visionSettingsService.ts(NEU)getVisionSettings(),saveVisionSettings()
@zxing/browserentfällt — kein client-seitiger Barcode-Scan
Schritt 3.2 — LabelConfirm Komponente (geteilt)
- Datei:
client/src/lib/components/LabelConfirm.svelte(NEU)- Props:
label, category, candidates, onconfirm, oncancel - Label-Input + Kategorie-Dropdown + Kandidaten-Chips
- Props:
Schritt 3.3 — RoomSelectSheet
- Datei:
client/src/lib/components/RoomSelectSheet.svelte(NEU)- Räume gruppiert nach
groupTypeanzeigen - "Neuen Raum anlegen" inline
- Props:
onselect(roomId), oncancel
- Räume gruppiert nach
Schritt 3.4 — PhotoCapture Komponente
- Datei:
client/src/lib/components/PhotoCapture.svelte(NEU)- States:
idle → analyzing → confirm | error <input type="file" accept="image/*" capture="environment">- HEIC→JPEG Normalisierung vor API-Call
- Props:
onconfirm(label, category, imageId), oncancel
- States:
Schritt 3.5 — BarcodeScan Komponente
- Datei:
client/src/lib/components/BarcodeScan.svelte(NEU)- Eingabe: EAN manuell eingeben (Nummernpad oder physischer Barcode-Scanner via Keyboard-Input)
- States:
idle → looking_up → confirm | error - Props:
onconfirm(label, category, ean?), oncancel - Kein Foto-Pfad in Phase 3 —
POST /api/vision/barcodeakzeptiert nur{ ean: string }. Foto→EAN kommt erst mit BarcodeDetector API (spätere Phase).
Schritt 3.6 — Loop-Seiten
- Datei:
client/src/routes/inventory/capture/photo/+page.svelte(NEU)- Query:
?roomId= - Loop: PhotoCapture → createAsset(draft) → Counter "X erfasst" → weiter / fertig
- Query:
- Datei:
client/src/routes/inventory/capture/barcode/+page.svelte(NEU)- Query:
?roomId= - Loop: BarcodeScan → createAsset(draft) → Counter → weiter / fertig
- Query:
Verifikation 3:
/inventory/capture/photo?roomId=Xlädt ohne JS-Fehler- Foto aufnehmen →
recognizePhotoliefert Label → LabelConfirm zeigt es an - Bestätigen →
db.assets.count()steigt um 1, neues Asset hatstatus='draft',roomId=X,coverImageIdnicht null - 3 Fotos in Serie → Counter zeigt "3 erfasst"
- "Fertig" → Redirect auf
/inventory - Barcode-Loop: EAN
4006381333931eingeben → Label erscheint → Asset angelegt ohnecoverImageId
Phase 4 — Inventar-UI
Requires: Phase 1 abgeschlossen (Repositories vorhanden) Produces:
client/src/lib/components/AssetCard.svelteclient/src/routes/inventory/+page.svelteclient/src/routes/inventory/rooms/+page.svelteclient/src/routes/inventory/rooms/[id]/+page.svelteclient/src/routes/inventory/items/+page.svelteclient/src/routes/inventory/[id]/+page.svelteclient/src/lib/components/Sidebar.svelte— Inventar-Eintrag ergänzt
Ziel: Dashboard, Asset-Detailseite, Sidebar.
Schritt 4.1 — AssetCard Komponente [parallel]
- Datei:
client/src/lib/components/AssetCard.svelte(NEU)- Cover-Bild (via
getImageUrl) oder Platzhalter-Icon - Titel + Kategorie-Badge + Draft-Badge (wenn
status='draft') - Click →
goto('/inventory/' + id)
- Cover-Bild (via
Schritt 4.2 — Inventar-Dashboard [parallel]
- Datei:
client/src/routes/inventory/+page.svelte(NEU)- Seed-Aufruf:
seedDefaultRoomsIfNeeded() - Stats-Cards: Gegenstände-Zähler, Räume-Zähler, Gesamtwert (Summe
purchasePrice) - Versicherungshinweis unter Gesamtwert
- Primär-CTA: "📷 Einfach fotografieren" → RoomSelectSheet → Foto-Loop
- Sekundär: "+ Gegenstand" (manuell) | "+ Raum" (inline-Modal)
- Link zu
/inventory/rooms+/inventory/items
- Seed-Aufruf:
Schritt 4.3 — Räume-Übersicht [parallel]
- Datei:
client/src/routes/inventory/rooms/+page.svelte(NEU)- 3-Spalten-Grid, gruppiert nach
groupType - Raumkarte: Icon (hardcodiertes Name→Icon Mapping) + Name + Asset-Zähler +
···Menu ···: Umbenennen (inline), Löschen (Soft-Delete + ConfirmDialog)- Click →
/inventory/rooms/[id](gefilterte Asset-Liste)
- 3-Spalten-Grid, gruppiert nach
- Datei:
client/src/routes/inventory/rooms/[id]/+page.svelte(NEU)- Asset-Liste gefiltert auf diesen Raum (wie
/inventory/itemsaber ohne Raum-Filter)
- Asset-Liste gefiltert auf diesen Raum (wie
Schritt 4.4 — Globale Asset-Liste [parallel]
- Datei:
client/src/routes/inventory/items/+page.svelte(NEU)- Header: "Gegenstände" + Zähler +
[📷 Fotografieren]+[+ Manuell] - Suchfeld (client-seitig, filtert Titel/Marke/Modell)
- Filter-Dropdowns: Raum | Kategorie | Status
- Grid/Liste-Toggle (Svelte store, persistiert in
settings) - Sections pro Raum (collapsible), Status-Icon (grün/gelb)
- Asset-Card: Cover-Bild, Preis-Badge, Status-Icon, Titel, Raum-Badge
- Header: "Gegenstände" + Zähler +
Schritt 4.6 — Asset-Detailseite
- Datei:
client/src/routes/inventory/[id]/+page.svelte(NEU)- Muster: analog
wiki/[id]/+page.svelte($derived(page.params.id),$effect) - Cover-Bild groß + Galerie (weitere Bilder horizontal scrollbar)
- Inline-Edit alle Felder (sofort gespeichert via
updateAsset) - Status-Toggle: Draft → Complete
- Galerie:
+ Foto hinzufügen(ohne KI) + Löschen (Soft-DeleteassetImages) - Cover-Konsistenz bei Image-Löschung:
softDeleteAssetImageprüft obasset.coverImageId === assetImage.imageId→ wenn ja:updateAsset({ coverImageId: nächstesAssetImage?.imageId ?? null }) - Notizen:
MarkdownEditor.svelte(bestehende Komponente) - Personen-Sektion: "Gehört / zugeordnet" — Chips für zugeordnete Familienmitglieder
getFamilyPersons()→ Dropdown/Picker mit allenfamily-Personen- Hinzufügen →
addAssetPerson, Entfernen →removeAssetPerson - Anzeige: Avatar-Chip mit
PersonMeta.abbreviationoderfullName
- Löschen-Button →
ConfirmDialog.svelte→softDeleteAsset→goto('/inventory')
- Muster: analog
Schritt 4.7 — Sidebar
- Datei:
client/src/lib/components/Sidebar.svelte- Neuer Abschnitt "Inventar" mit Link
/inventory, Icon:Package(lucide)
- Neuer Abschnitt "Inventar" mit Link
BottomTabBarbleibt unverändert — Navigation über Sidebar + CommandBar ausreichend
Verifikation 4:
/inventorylädt: Seed-Räume sichtbar (15 Default-Räume), Gesamtwert 0 €seedDefaultRoomsIfNeededein zweites Mal aufrufen →db.rooms.count()bleibt gleich (kein Doppel-Seed)/inventory/roomszeigt 3 Gruppen-Header, alle 15 Default-Räume als Cards- Raum umbenennen → Name ändert sich ohne Reload
- Raum löschen →
deletedAtgesetzt, Card verschwindet /inventory/itemsmit Filter Raum=X → nur Assets dieses Raums sichtbar- Asset-Detailseite: Titel ändern →
updateAssetaufgerufen, Seite zeigt neuen Titel - Status-Toggle Draft→Complete →
status='complete'in Dexie, Badge ändert Farbe - Foto hinzufügen (ohne KI) →
assetImagesEintrag + neues Bild in Galerie - Löschen-Button → ConfirmDialog →
softDeleteAsset→ Redirect auf/inventory - Sidebar zeigt "Inventar"-Link → navigiert zu
/inventory
Phase 5 — Settings (Vision-API-Key)
Requires: Phase 2 (Vision-Backend) abgeschlossen — insbesondere GET/PUT /api/vision/settings
Produces:
client/src/routes/settings/+page.svelte— Abschnitt "Bild-Erkennung" ergänztclient/src/lib/services/visionSettingsService.ts(kann parallel zu Phase 3.1 entstehen)
Ziel: Vision-API-Key pro User konfigurierbar. Muss vor Phase 3 implementiert sein.
Schritt 5.1 — Settings-Sektion
- Datei:
client/src/routes/settings/+page.svelte- Neuer Abschnitt "Bild-Erkennung"
- Provider-Wahl: Radio OpenAI / Gemini
- API-Key Input (type=password)
- Anzeige wenn gesetzt:
●●●●●●...XXXX(letzten 4 Zeichen aus Server-Response) - Tageslimit + Verbrauch (aus
GET /api/vision/settings) - Speichern →
saveVisionSettings()
Verifikation 5:
- Settings-Seite öffnen → Abschnitt "Bild-Erkennung" vorhanden
- Provider wählen + Key eingeben + Speichern →
PUT /api/vision/settingsHTTP 200 - Seite neu laden → Key-Feld zeigt
●●●●●●...XXXX(letzten 4 Zeichen), Provider korrekt vorgewählt dailyUsage+dailyLimitangezeigt
⚠️ Implementierungsreihenfolge: Phase 5 (Settings) sollte vor oder parallel zu Phase 3 (Erfassungs-Flow) umgesetzt werden — ohne konfigurierten API-Key ist Phase 3 nicht testbar.
Phase 6 — CommandBar-Integration
Requires: Phase 4 abgeschlossen (Repositories + /inventory/[id] Route vorhanden)
Produces:
client/src/lib/components/CommandBar.svelte— Assets in Suchergebnissen + 2 neue Slash-Commands
Ziel: Assets über CommandBar findbar, /asset-Command für schnelle Erfassung.
Schritt 6.1 — Asset-Suche in CommandBar
- Datei:
client/src/lib/components/CommandBar.svelte - Datenquelle:
liveQuery(() => db.assets.filter(a => !a.deletedAt).toArray())— kein neuer Endpoint - Filterung:
asset.title + asset.brand + asset.model(analog bestehende Context-Suche) - Anzeige-Format:
│ 📦 Flauschige Decke INVENTAR │ │ Ankleidezimmer · Eltern │ - Ergebnis-Limit: max. 3 Assets (in bestehende 3+3-Logik einpassen)
Enter→goto('/inventory/' + asset.id)
Schritt 6.2 — Slash-Commands
- Datei:
client/src/lib/components/CommandBar.svelte - 2 neue Einträge im
COMMANDS-Array:
| Command | Verhalten |
|---|---|
/inventar |
goto('/inventory') |
/asset [Titel] |
Mit Text: createAsset(title, roomId=null) → goto('/inventory/' + id); ohne Text: goto('/inventory') mit Manuell-Flag |
Verifikation 6:
- Asset "Flauschige Decke" in Dexie vorhanden
Ctrl+K→ "decke" tippen → Eintrag mit BadgeINVENTARerscheint in Ergebnissen- Enter → Redirect auf
/inventory/{id} Ctrl+K→/inventarEnter → Redirect auf/inventoryCtrl+K→/asset LaptopEnter → neues Asset mittitle='Laptop',roomId=null,status='draft'in Dexie, Detailseite öffnetCtrl+K→/assetohne Text Enter → Redirect auf/inventory(kein Fehler)
Phase 7 — Feature-Dokumentation
Requires: Phase 4 abgeschlossen (UI + Datenmodell stabil), Phase 6 abgeschlossen (CommandBar-Befehle final)
Produces: docs/feature-inventory.md — versioniert im Repo, Inhalt stimmt mit implementiertem Stand überein
Ziel: docs/feature-inventory.md anlegen (analog feature-tasks.md, feature-commandbar.md).
Wird nach Abschluss von Phase 4+6 geschrieben — wenn UI + Datenmodell stabil sind.
Inhalt docs/feature-inventory.md (Entwurf)
# Feature: Inventar
## Zweck
Hausinventar erfassen und verwalten: Gegenstände per Kamera oder Barcode aufnehmen,
KI erkennt Titel + Kategorie automatisch, Zuordnung zu Räumen und Familienmitgliedern.
Gesamtwert als Grundlage für Versicherungssumme.
---
## Räume
Räume werden in drei Gruppen eingeteilt (hardcodiert, nicht erweiterbar):
| `groupType` | Label | Beispiel-Default-Räume |
|---|---|---|
| `living` | Wohnbereiche | Wohnzimmer, Schlafzimmer, Kinderzimmer, Esszimmer, Flur |
| `functional` | Funktionsräume | Küche, Bad, WC, Arbeitszimmer, Hauswirtschaft |
| `outdoor` | Außen & Nebenbereiche | Keller, Dachboden, Garage, Garten, Abstellraum |
Default-Räume werden beim ersten Aufruf von `/inventory` automatisch angelegt
(`seedDefaultRoomsIfNeeded()`, Flag `'inventory.seeded'` in `settings`-Tabelle).
Räume sind löschbar (Soft-Delete). Jeder Raum hat ein `icon`-Feld (lucide icon name).
---
## Asset-Felder
| Feld | Typ | Beschreibung |
|---|---|---|
| `title` | string | Name des Gegenstands (KI-Vorschlag oder manuell) |
| `roomId` | string\|null | Zugeordneter Raum (null = ohne Raum) |
| `category` | string\|null | Aus hardcodierter Liste (s.u.) oder null |
| `status` | `draft`\|`complete` | Draft = Schnellerfassung, noch unvollständig |
| `condition` | `new`\|`good`\|`fair`\|`poor`\|null | Zustand |
| `brand` | string\|null | Marke |
| `model` | string\|null | Modell |
| `serialNumber` | string\|null | Seriennummer |
| `purchasePrice` | real\|null | Kaufpreis (€) |
| `purchaseYear` | integer\|null | Anschaffungsjahr |
| `notes` | string\|null | Freies Markdown-Notizfeld |
| `coverImageId` | string\|null | Primärbild (→ imageBlobs.id, Server-seitig) |
---
## Kategorien (hardcoded)
`null` (Keine Kategorie), Elektronik, Möbel, Kleidung, Schmuck, Kunstwerke,
Sportgeräte, Haushaltsgeräte, Haustechnik, Werkzeuge, Musikinstrumente,
Bücher & Medien, Spielzeug, Sammlerstücke, Gartengeräte, Fahrzeugzubehör, Sonstiges
Kein eigenes DB-Table — als TypeScript-Konstante in `client/src/lib/utils/inventory.ts`.
---
## Erfassungs-Modi
### Foto-Modus (Schnellerfassung)
1. Raum wählen (einmalig für die Session)
2. Loop: Foto aufnehmen → KI-Erkennung → Label + Kategorie bestätigen → Asset(draft) anlegen
3. [Fertig] → Dashboard
Route: `/inventory/capture/photo?roomId=...`
KI: POST `/api/vision/recognize` (OpenAI gpt-4o-mini oder Gemini gemini-1.5-flash)
### Barcode-Modus (Schnellerfassung)
1. Raum wählen (einmalig)
2. Loop: EAN eingeben ODER Barcode fotografieren → UPCitemdb-Lookup → bestätigen → Asset(draft)
3. [Fertig] → Dashboard
Route: `/inventory/capture/barcode?roomId=...`
Lookup: POST `/api/vision/barcode` → UPCitemdb → Open Food Facts → EAN-Fallback
### Manuell (Einzel)
Raum wählen → Titel eingeben → Asset(draft) → Detailseite
Auch via CommandBar: `/asset Titel`
---
## Bilder-Architektur
**Inventory-Fotos werden NICHT lokal gecacht** (Grund: 500 Assets × ~300 KB = 150 MB).
Statt `storeImage()` (schreibt in Dexie): `uploadInventoryImage(blob)`:
1. Client komprimiert: JPEG, max-width 1920px, quality 0.75 → ~300 KB
2. POST `/api/inventory/images` → Server speichert in `imageBlobs`-Tabelle
3. Client erhält `imageId` zurück, speichert nur die ID im Asset
4. Anzeige: `getImageUrl(id)` fetcht live via `/api/sync/blob/{id}`
`storeImage()` aus `imageStore.ts` wird weiterhin für Editor-Paste-Bilder verwendet —
**nicht** für Inventory.
---
## Personen-Zuordnung
Assets können Familienmitgliedern zugeordnet werden (`assetPersons`-Tabelle, n:m).
Auswählbar sind nur Personen mit `contexts.type='person'` + `meta.personSubType='family'`.
Anzeige auf Detailseite als Avatar-Chips (`PersonMeta.abbreviation` oder `fullName`).
---
## Vision-API-Key
Kein globaler API-Key — jeder User hinterlegt seinen eigenen Key in den Einstellungen
(Abschnitt "Bild-Erkennung"). Keys werden AES-256-GCM verschlüsselt in `user_settings`
gespeichert. Server-seitiges Verschlüsselungs-Secret: `SETTINGS_ENCRYPTION_KEY` in `.env`.
Rate-Limit: 100 Vision-Calls/Tag pro User (DB-basiert via `vision_usage`-Tabelle).
---
## Routen-Übersicht
| Route | Beschreibung |
|---|---|
| `/inventory` | Dashboard: Stats, Gesamtwert, Quick-Actions |
| `/inventory/rooms` | Räume-Grid (3-spaltig, gruppiert) |
| `/inventory/rooms/[id]` | Asset-Liste eines Raums |
| `/inventory/items` | Alle Assets (Suche, Filter, Grid/Liste) |
| `/inventory/capture/photo` | Foto-Schnellerfassungs-Loop |
| `/inventory/capture/barcode` | Barcode-Schnellerfassungs-Loop |
| `/inventory/[id]` | Asset-Detailseite |
---
## CommandBar-Integration
- Suche: Assets erscheinen in Ergebnissen (Badge `INVENTAR`, Icon 📦)
- `/inventar` → Dashboard
- `/asset [Titel]` → Asset anlegen (roomId=null) → Detailseite
---
## Server-Endpoints (Inventory-spezifisch)
POST /api/inventory/images Bild hochladen → { id } POST /api/vision/recognize Foto → { label, category, candidates } POST /api/vision/barcode EAN oder Barcode-Foto → { label, category } GET /api/vision/settings { provider, apiKeySet, dailyUsage, dailyLimit } PUT /api/vision/settings { provider?, apiKey? }
Sync läuft über den bestehenden `/api/sync/push` + `/api/sync/pull` Endpunkt.
Neue Tabellen im Sync: `rooms`, `assets`, `assetImages`, `assetPersons`.
---
## Wichtige Implementierungshinweise
### Bild-Upload vs. storeImage()
`storeImage()` und `uploadInventoryImage()` sind NICHT austauschbar:
- `storeImage()` → Dexie lokal + Sync (für Editor-Bilder)
- `uploadInventoryImage()` → direkt Server, kein Dexie (für Inventory-Fotos)
Verwechslung führt zu 75+ MB lokalem Storage.
### coverImageId und assetImages
`assets.coverImageId` = das Hauptbild (aus `imageBlobs.id`).
`assetImages` = weitere Bilder (ebenfalls `imageBlobs`-Referenzen).
Das Cover kann auf der Detailseite durch ein `assetImages`-Bild ersetzt werden
(`updateAsset({ coverImageId: assetImage.imageId })`).
Es gibt keine automatische Verknüpfung: Cover-Änderung aktualisiert NICHT `assetImages`.
### Draft vs. Complete
Assets beginnen immer als `draft`. Status `complete` muss manuell gesetzt werden.
Dashboard zeigt Gesamtzahl unabhängig vom Status.
`/inventory/items` filtert nach Status (Dropdown "Alle Status").
### Raum-Seed
`seedDefaultRoomsIfNeeded()` prüft `db.settings.get('inventory.seeded')`.
Wird nur einmal ausgeführt. Danach kann der User Räume löschen/umbenennen ohne Re-Seed.
### Barcode-Erkennung
Kein client-seitiger Barcode-Scanner (ZXing entfällt — kein live scan auf iOS).
Stattdessen: EAN manuell eingeben ODER Foto → POST `/api/vision/barcode`.
Server versucht UPCitemdb → Open Food Facts → EAN als Label.
---
## Bekannte Einschränkungen
- Kamera-Erfassung erfordert Online-Verbindung (kein offline capture)
- Barcode-Lookup max. 100/Tag (UPCitemdb free tier)
- Räume können nicht per Drag-Drop sortiert werden (`sortOrder` vorhanden, aber kein UI)
- Asset-Export (PDF/CSV) nicht implementiert (geplant als spätere Phase)
- Preisrecherche nicht implementiert (geplant: eBay Browse API)
Verifikation 7:
docs/feature-inventory.mdexistiert (nicht leer)- Alle Routen aus "Routen-Übersicht" im Dokument entsprechen tatsächlich implementierten SvelteKit-Routen
- Alle Felder in "Asset-Felder"-Tabelle stimmen mit
shared/src/types.tsüberein - Alle Server-Endpoints im Dokument sind in
routes/vision.ts+routes/inventory.tsimplementiert - CommandBar-Commands
/inventarund/assetim Dokument = exakte Command-Namen inCommandBar.svelte
Spätere Phase: Automatische Preisschätzung
Stufenmodell — je mehr Daten, desto präziser
| Verfügbare Daten | Strategie | Quelle |
|---|---|---|
| Nur Label/Kategorie | Kategorie-Durchschnittsrange (hardcoded) | Offline |
| + Brand + Model | Produktsuche → Median aktueller Listings | eBay Browse API |
| + Condition | Zustandsfaktor anwenden | Client-seitig |
| + purchaseYear | Abschreibungsfaktor zusätzlich | Client-seitig |
| + purchasePrice (ohne Markt) | Kaufpreis × Zeitfaktor × Zustandsfaktor |
Offline-Formel |
Zustandsfaktoren: new=100%, good=70%, fair=45%, poor=20%
Abschreibung: -8% pro Jahr (Elektronik: -15%), Minimum 10% Restwert
API-Quellen
| Dienst | Art | Bewertung |
|---|---|---|
| eBay Browse API | Offizielle REST-API | Gebrauchtpreise, kostenlos mit App-Key → Primärziel |
| Open EAN / UPCitemdb | Barcode→Produktinfo | Ergänzend für Basisdaten |
| Google Shopping (Custom Search) | Preisvergleich | Kostenpflichtig ab Volumen |
| idealo | Scraping | Fragil + rechtlich grenzwertig → nicht empfohlen |
Endpoint
GET /api/inventory/[id]/price-lookup → Response:
{
"estimatedValue": 320,
"rangeMin": 240,
"rangeMax": 410,
"confidence": 3,
"source": "ebay",
"breakdown": "Marktpreis 450 € × Zustand 'gut' (70%) = 315 €",
"ebayListingUrl": "https://..."
}
Konfidenz-Score (0–3):
- 0 = nur Label/Kategorie → Range aus hardcodierter Tabelle
- 1 = +Brand+Model → eBay-Suche möglich
- 2 = +Condition → Zustandsfaktor auf eBay-Preis angewendet
- 3 = +Condition+purchaseYear → Abschreibungsfaktor zusätzlich
purchaseYear erhöht Score nur wenn condition bereits bekannt (sonst keine Aussagekraft).
UI auf Detailseite
[Preis schätzen ●●●○] ← Konfidenz-Indikator
→ "~320 € (eBay-Marktpreise)" ← mit Quelle
→ "150–400 € (Kategorie-Schätzung, Marke/Modell ergänzen für besseres Ergebnis)"
[Wert übernehmen] → setzt purchasePrice
Schätzung wird in eigenen Spalten von assets gespeichert (nicht in notes — strukturierte Daten gehören nicht in Freitext-Felder):
lastPriceEstimate REAL— geschätzter Wert (Median)lastPriceEstimateAt TEXT— Zeitstempel der letzten SchätzunglastPriceEstimateSource TEXT—'ebay'|'formula'|'category'
Diese Felder sind Teil des Asset-Datenmodells (Phase 1 Schritt 1.2 + 1.3 entsprechend ergänzen).
Wiederverwendete bestehende Dateien
| Datei | Verwendung |
|---|---|
lib/db/imageStore.ts: storeImage() |
Weiterhin für Editor-Bilder — nicht für Inventory-Fotos |
lib/db/imageStore.ts: getImageUrl() |
Inventory: direkt /api/sync/blob/{id} fetchen (kein Dexie-Write) |
lib/db/helpers.ts: newId(), now() |
IDs + Timestamps |
lib/auth/authStore.ts: getAccessToken() |
Bearer Token für Vision-API |
lib/components/MarkdownEditor.svelte |
Notizfeld Detailseite |
lib/components/ConfirmDialog.svelte |
Lösch-Bestätigung |
server/src/lib/route-utils.ts: handle() |
Route-Wrapper |
Risiken
| Risiko | Mitigation |
|---|---|
| HEIC von iOS-Kamera | Browser kann HEIC nicht nativ via Canvas dekodieren. Fix: <input accept="image/jpeg,image/png,image/webp"> → iOS bietet dann JPEG an (kein HEIC). Alternativ: heic2any-Library als Fallback für direkten File-Drop. Canvas-only reicht nicht. |
| Kamera-Erfassung erfordert Online | PWA mit WLAN — akzeptabel; Fehlermeldung wenn offline |
| OpenAI/Gemini Timeout | 10s Timeout, Fallback: manuelles Label |
| Rate-Limit-Zähler bei DB-Fehler | INSERT OR IGNORE + UPDATE — Worst case: limit nicht enforced (akzeptabel) |
| UPCitemdb Tages-Limit (100/Tag frei) | Fallback auf Open Food Facts; für höheres Volumen kostenpflichtiger Plan |
| Barcode nicht in Datenbank | Fallback: roher EAN-String als Label |
| Asset ohne Bild (Barcode-Modus) | coverImageId = null, Platzhalter-Icon in Card |
| Vision-API nicht konfiguriert | Fehlermeldung "Bitte API-Key in Einstellungen hinterlegen" statt 500 |
| Rate-Limit Timezone-Drift | Server-UTC für date-Feld — dokumentiert, kein Fix nötig |
Spätere Phase: KI-Zustandserkennung aus Foto
Beim Foto-Erfassungs-Flow erkennt die KI nicht nur Label + Kategorie, sondern auch condition.
Erweiterung des Vision-Prompts:
Return JSON:
{ "label": "...", "category": "...", "condition": "new|good|fair|poor|unknown", "candidates": [...] }
conditionwird in LabelConfirm als vorausgefülltes Dropdown angezeigt (editierbar)"unknown"ist kein gültigercondition-Typ — Server-seitig aufnullmappen bevor Antwort an Client geht- Kein Breaking Change — bestehende Response ohne
condition→ null
Nutzen: Spart einen manuellen Schritt auf der Detailseite; verbessert Preisschätzungs-Qualität direkt nach Erfassung.
Spätere Phase: Offline-Capture-Queue
Aktuell: Kamera-Erfassung erfordert Online-Verbindung (hard constraint).
Verbesserung: Fotos offline aufnehmen, in IndexedDB queuen, bei nächster Verbindung hochladen.
OfflineCaptureQueue (Dexie):
id, blob, roomId, capturedAt, uploadedAt (null = pending)
- UI zeigt Badge "X Fotos ausstehend" wenn Queue nicht leer
- Background-Sync (Service Worker) oder manueller "Jetzt hochladen"-Button
- Nach Upload: normaler createAsset-Flow mit zurückgegebenem imageId
Komplexität: Mittel (Service Worker optional — auch ohne funktioniert manueller Trigger).
Spätere Phase: BarcodeDetector API (Native Browser)
Chrome 83+ und Android WebView unterstützen BarcodeDetector nativ — Safari wird nicht unterstützt (weder iOS noch macOS, Stand 2026). Für iOS-User bleibt das manuelle EAN-Eingabefeld der primäre Pfad.
const detector = new BarcodeDetector({ formats: ['ean_13', 'ean_8', 'upc_a'] });
const barcodes = await detector.detect(videoFrame);
- Live-Kamera-Stream → EAN direkt client-seitig extrahiert
- Danach:
POST /api/vision/barcodemit EAN (wie bisher) für Produktdaten-Lookup - Fallback für nicht unterstützte Browser: manuelles EAN-Eingabefeld (wie aktuell geplant)
- Kein ZXing-Dependency nötig
Nutzen: Echter Barcode-Scanner-Feeling ohne Bibliothek.
Spätere Phase: Draft-Reminder
Assets die länger als X Tage als draft verbleiben → Hinweis auf Dashboard.
Dashboard: "5 Gegenstände noch unvollständig (> 7 Tage)" [Jetzt vervollständigen →]
- Client-seitig via
liveQuery— kein Server nötig - Schwellwert: 7 Tage (hardcoded oder in Settings konfigurierbar)
Spätere Phase: Asset-Export
Für Versicherungszwecke:
- PDF-Export: Inventarliste mit Fotos, Werten, Seriennummern (via
jsPDFoder Server-seitiger PDF-Render) - CSV-Export: Alle Assets als Tabelle (client-seitig, kein Server nötig)
- Button auf Dashboard: "Inventar exportieren"