Ka-Note/plans/vision-inventory.md

45 KiB
Raw Blame History

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: liveQuery auf assets + rooms (nicht-deleted, Summe purchasePrice)
  • 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>:

  1. Bild client-seitig komprimieren: Canvas toBlob('image/jpeg', 0.75) + max-width 1920px → ~300 KB. HEIC wird durch accept="image/jpeg,image/png,image/webp" im Input verhindert.
  2. Direkt POST zu /api/inventory/images (neuer Endpoint, kein Sync-Push)
  3. Server speichert in imageBlobs, gibt imageId zurück
  4. Client speichert nur imageId im Asset — kein Dexie-Blob
  5. 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_KEY in .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 Gemini gemini-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
  • Statusdraft nach Schnellerfassung, complete nach Nachbearbeitung
  • Löschung — Soft-Delete überall, wie Wiki-Pages
  • Personen — nur family-Subtype auswählbar (filter: contexts.type='person' + meta.personSubType='family'); Zuordnung via assetPersons-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.12.4: vollständig parallel (je eigene neue Datei)
  • Phase 5 + Phase 2.5: parallel nach Phase 2.12.4
  • Phase 4 Schritte 4.14.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.tsRoom, Asset, AssetImage, AssetPerson Types exportiert
  • shared/src/sync.tsSyncChanges enthält 4 neue Tabellen
  • server/drizzle/0009_inventory.sql — Migration ausführbar
  • client/src/lib/db/schema.ts v15 — Dexie öffnet ohne Fehler
  • client/src/lib/db/repositories.ts — alle CRUD-Funktionen + seedDefaultRoomsIfNeeded exportiert
  • 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.ts
    • SyncChanges: + rooms, assets, assetImages, assetPersons
    • SYNC_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), Drizzle sqliteTable
  • Datei: server/drizzle/0009_inventory.sql (auto-generiert via drizzle-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'

Schritt 1.4 — Client Repositories

  • Datei: client/src/lib/db/repositories.ts
    • Room: createRoom, getRooms, updateRoom, softDeleteRoom
    • Asset: createAsset, getAsset, getAssetsByRoom, getAssetsAll, getAssetsByStatus, updateAsset, softDeleteAsset
      • getAssetsAll() — 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() — filtert db.contexts nach type='person' + meta.personSubType='family'
    • Seed-Funktion: seedDefaultRoomsIfNeeded(userId) — prüft settings-Flag 'inventory.seeded'

Schritt 1.5 — Sync-Erweiterung

  • Datei: server/src/lib/sync-service.ts
      • mapRoom, mapAsset, mapAssetImage, mapAssetPerson
    • pushChanges + pullChanges um 4 Entitäten erweitern
  • Datei: server/src/routes/sync.ts
    • VALID_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 dev startet ohne Fehler
  • Browser-Console: kein Dexie-Schema-Fehler beim Öffnen der App
  • GET /api/sync/pull mit gültigem Token → Response enthält Keys rooms, assets, assetImages, assetPersons (leere Arrays OK)
  • POST /api/sync/push mit { rooms: [{ id, name, groupType, ... }] } → HTTP 200
  • db.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 — exportiert getSetting, setSetting, getVisionConfig
  • server/src/lib/rate-limiter.ts — exportiert checkAndIncrement
  • server/src/lib/vision-service.ts — exportiert getProvider(name, apiKey): VisionProvider
  • server/src/lib/barcode-service.ts — exportiert lookupBarcode(ean)
  • server/src/routes/vision.ts — Router mit 4 Endpoints
  • server/src/routes/inventory.ts — Router mit POST /api/inventory/images
  • server/.env.exampleSETTINGS_ENCRYPTION_KEY + VISION_RATE_LIMIT dokumentiert

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 | null
    • setSetting(userId, key, value): void
    • getVisionConfig(userId): { provider, apiKey } | null
    • AES-256-GCM Verschlüsselung mit SETTINGS_ENCRYPTION_KEY aus .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 + UPDATE auf vision_usage (date = heute)

Schritt 2.3 — Vision Service [parallel]

  • Datei: server/src/lib/vision-service.ts (NEU)
    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): VisionProvider
    
    Prompt (beide Provider):
    Identify 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.12.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)
    POST /api/inventory/images   — multipart { image: File } → speichert in imageBlobs, gibt { id } zurück
    
    Routen: alle authMiddleware. Rate-Limit-Check nur auf POST /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 via vision_usage getrackt.
  • Datei: server/src/index.ts: + 4 Zeilen (beide Routes registrieren)

Verifikation 2:

  • PUT /api/vision/settings mit gültigem { provider: 'openai', apiKey: '...' } → HTTP 200
  • GET /api/vision/settings{ provider: 'openai', apiKeySet: true, dailyUsage: 0, dailyLimit: 100 }
  • POST /api/vision/recognize mit JPEG-Testbild → HTTP 200, Body enthält label (string), category (string aus Kategorienliste), candidates (Array)
  • POST /api/vision/barcode mit { ean: '4006381333931' } (Stabilo-Stift) → HTTP 200, label nicht leer
  • POST /api/inventory/images mit JPEG multipart → HTTP 200, { id: '...' }
  • Zweiter Recognize-Call → dailyUsage: 1 in GET settings
    1. 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.tscompressImage, uploadInventoryImage, recognizePhoto, recognizeBarcode exportiert
  • client/src/lib/services/visionSettingsService.tsgetVisionSettings, saveVisionSettings exportiert
  • client/src/lib/components/LabelConfirm.svelte — rendert ohne Fehler
  • client/src/lib/components/PhotoCapture.svelte — rendert ohne Fehler
  • client/src/lib/components/BarcodeScan.svelte — rendert ohne Fehler
  • client/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 durch accept-Attribut im Input vermieden (kein HEIC-Decoder nötig).
    • uploadInventoryImage(blob): Promise<string> — POST /api/inventory/images, gibt imageId zurück
    • recognizePhoto(blob): Promise<RecognizeResult> — POST /api/vision/recognize
    • recognizeBarcode(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/barcode akzeptiert 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/browser entfä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

Schritt 3.3 — RoomSelectSheet

  • Datei: client/src/lib/components/RoomSelectSheet.svelte (NEU)
    • Räume gruppiert nach groupType anzeigen
    • "Neuen Raum anlegen" inline
    • Props: onselect(roomId), oncancel

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

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/barcode akzeptiert 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
  • Datei: client/src/routes/inventory/capture/barcode/+page.svelte (NEU)
    • Query: ?roomId=
    • Loop: BarcodeScan → createAsset(draft) → Counter → weiter / fertig

Verifikation 3:

  • /inventory/capture/photo?roomId=X lädt ohne JS-Fehler
  • Foto aufnehmen → recognizePhoto liefert Label → LabelConfirm zeigt es an
  • Bestätigen → db.assets.count() steigt um 1, neues Asset hat status='draft', roomId=X, coverImageId nicht null
  • 3 Fotos in Serie → Counter zeigt "3 erfasst"
  • "Fertig" → Redirect auf /inventory
  • Barcode-Loop: EAN 4006381333931 eingeben → Label erscheint → Asset angelegt ohne coverImageId

Phase 4 — Inventar-UI

Requires: Phase 1 abgeschlossen (Repositories vorhanden) Produces:

  • client/src/lib/components/AssetCard.svelte
  • client/src/routes/inventory/+page.svelte
  • client/src/routes/inventory/rooms/+page.svelte
  • client/src/routes/inventory/rooms/[id]/+page.svelte
  • client/src/routes/inventory/items/+page.svelte
  • client/src/routes/inventory/[id]/+page.svelte
  • client/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)

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

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)
  • Datei: client/src/routes/inventory/rooms/[id]/+page.svelte (NEU)
    • Asset-Liste gefiltert auf diesen Raum (wie /inventory/items aber ohne Raum-Filter)

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

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-Delete assetImages)
    • Cover-Konsistenz bei Image-Löschung: softDeleteAssetImage prüft ob asset.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 allen family-Personen
      • Hinzufügen → addAssetPerson, Entfernen → removeAssetPerson
      • Anzeige: Avatar-Chip mit PersonMeta.abbreviation oder fullName
    • Löschen-Button → ConfirmDialog.sveltesoftDeleteAssetgoto('/inventory')

Schritt 4.7 — Sidebar

  • Datei: client/src/lib/components/Sidebar.svelte
    • Neuer Abschnitt "Inventar" mit Link /inventory, Icon: Package (lucide)
  • BottomTabBar bleibt unverändert — Navigation über Sidebar + CommandBar ausreichend

Verifikation 4:

  • /inventory lädt: Seed-Räume sichtbar (15 Default-Räume), Gesamtwert 0 €
  • seedDefaultRoomsIfNeeded ein zweites Mal aufrufen → db.rooms.count() bleibt gleich (kein Doppel-Seed)
  • /inventory/rooms zeigt 3 Gruppen-Header, alle 15 Default-Räume als Cards
  • Raum umbenennen → Name ändert sich ohne Reload
  • Raum löschen → deletedAt gesetzt, Card verschwindet
  • /inventory/items mit Filter Raum=X → nur Assets dieses Raums sichtbar
  • Asset-Detailseite: Titel ändern → updateAsset aufgerufen, Seite zeigt neuen Titel
  • Status-Toggle Draft→Complete → status='complete' in Dexie, Badge ändert Farbe
  • Foto hinzufügen (ohne KI) → assetImages Eintrag + 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änzt
  • client/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/settings HTTP 200
  • Seite neu laden → Key-Feld zeigt ●●●●●●...XXXX (letzten 4 Zeichen), Provider korrekt vorgewählt
  • dailyUsage + dailyLimit angezeigt

⚠️ 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)
  • Entergoto('/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 Badge INVENTAR erscheint in Ergebnissen
  • Enter → Redirect auf /inventory/{id}
  • Ctrl+K/inventar Enter → Redirect auf /inventory
  • Ctrl+K/asset Laptop Enter → neues Asset mit title='Laptop', roomId=null, status='draft' in Dexie, Detailseite öffnet
  • Ctrl+K/asset ohne 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.md existiert (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.ts implementiert
  • CommandBar-Commands /inventar und /asset im Dokument = exakte Command-Namen in CommandBar.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 (03):

  • 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
→ "150400 € (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ätzung
  • lastPriceEstimateSource 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": [...] }
  • condition wird in LabelConfirm als vorausgefülltes Dropdown angezeigt (editierbar)
  • "unknown" ist kein gültiger condition-Typ — Server-seitig auf null mappen 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/barcode mit 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 jsPDF oder Server-seitiger PDF-Render)
  • CSV-Export: Alle Assets als Tabelle (client-seitig, kein Server nötig)
  • Button auf Dashboard: "Inventar exportieren"