# 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 (€), manuell oder via KI-Schätzung übernommen | | `purchaseYear` | integer\|null | Anschaffungsjahr | | `notes` | string\|null | Freies Markdown-Notizfeld | | `coverImageId` | string\|null | Primärbild (→ imageBlobs.id, Server-seitig) | | `lastPriceEstimate` | real\|null | Letzter geschätzter Marktwert (€) | | `lastPriceEstimateAt` | string\|null | ISO-Zeitstempel der letzten Schätzung | | `lastPriceEstimateSource` | `'ebay'`\|`'formula'`\|`'category'`\|null | Quelle der Schätzung | --- ## 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) Input: `accept="image/jpeg,image/png,image/webp"` — kein HEIC (Canvas kann HEIC nicht dekodieren) ### Barcode-Modus (Schnellerfassung) 1. Raum wählen (optional via `?roomId=` Query-Parameter) 2. Loop: EAN manuell eingeben oder via physischem Scanner → UPCitemdb-Lookup → bestätigen → Asset(draft) 3. [Fertig] → Dashboard Route: `/inventory/capture/barcode?roomId=...` Lookup: `POST /api/vision/barcode` → UPCitemdb → Open Food Facts → EAN als Fallback-Label Rate-Limit gilt **nicht** für Barcode-Lookups (nutzt UPCitemdb, nicht die Vision-API des Users). ### Manuell (Einzel) Titel eingeben → Asset(draft) → Detailseite Auch via CommandBar: `/asset Titel` --- ## KI-Preisschätzung (enrichAsset) Auf der Detailseite: Button "Preis schätzen" → `POST /api/vision/enrich` Die KI schätzt auf Basis der vorhandenen Felder: - `title` (Pflicht) - `brand`, `model` (optional, verbessern Genauigkeit) **Response:** ```json { "brand": "Sony", "model": "WH-1000XM5", "estimatedNewPrice": 350, "estimatedUsedPrice": 180 } ``` Der User kann Marke/Modell und Preise einzeln übernehmen. `purchasePrice` wird auf den übernommenen Wert gesetzt. `lastPriceEstimate` / `lastPriceEstimateAt` / `lastPriceEstimateSource` werden beim Speichern der Schätzung aktualisiert. Rate-Limit: zählt gegen die 100 Vision-Calls/Tag des Users (gleicher Zähler wie Foto-Erkennung). --- ## 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. ### Cover-Konsistenz bei Image-Löschung `softDeleteAssetImage` prüft ob `asset.coverImageId === assetImage.imageId`. Wenn ja: `updateAsset({ coverImageId: nächstesAssetImage?.imageId ?? null })`. --- ## 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 (`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 (`vision_usage`-Tabelle). Gilt für: - `POST /api/vision/recognize` (Foto-Erkennung) - `POST /api/vision/enrich` (Preisschätzung) Gilt **nicht** für `POST /api/vision/barcode` (UPCitemdb, kein User-API-Key). --- ## 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, remaining } POST /api/vision/enrich { label, brand?, model? } → { brand, model, estimatedNewPrice, estimatedUsedPrice } POST /api/vision/barcode { ean } → { label, category? } (kein Rate-Limit) 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) ### 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 (BarcodeDetector API unterstützt kein iOS/Safari). EAN manuell eingeben oder physischen Scanner verwenden → `POST /api/vision/barcode`. --- ## Bekannte Einschränkungen - Kamera-Erfassung erfordert Online-Verbindung (kein offline capture) - Barcode-Lookup max. 100/Tag server-weit (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) - BarcodeDetector API (live Kamera-Scan) nicht implementiert — iOS unterstützt es nicht