219 lines
8.2 KiB
Markdown
219 lines
8.2 KiB
Markdown
# 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
|