Ka-Note/docs/feature-inventory.md

8.2 KiB
Raw Permalink Blame History

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:

{
  "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