8.2 KiB
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)
- Raum wählen (optional via
?roomId=Query-Parameter) - Loop: Foto aufnehmen → KI-Erkennung → Label + Kategorie bestätigen → Asset(draft) anlegen
- [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)
- Raum wählen (optional via
?roomId=Query-Parameter) - Loop: EAN manuell eingeben oder via physischem Scanner → UPCitemdb-Lookup → bestätigen → Asset(draft)
- [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:
{
"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):
- Client komprimiert: JPEG, max-width 1920px, quality 0.75 → ~300 KB
POST /api/inventory/images→ Server speichert inimageBlobs-Tabelle- Client erhält
imageIdzurück, speichert nur die ID im Asset - 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 (
sortOrdervorhanden, 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