# 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 den jeweiligen Komponenten/Seiten. --- ## Erfassungs-Modi ### Foto-Modus (Schnellerfassung) 1. Raum wählen (optional via `?roomId=` Query-Parameter) 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 (optional via `?roomId=` Query-Parameter) 2. Loop: EAN eingeben → 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 Chips. --- ## 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 })`). ### 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. Stattdessen: EAN manuell eingeben → 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)