187 lines
6.8 KiB
Markdown
187 lines
6.8 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 (€) |
|
||
| `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)
|