upd inventory
This commit is contained in:
parent
b4ed2055a2
commit
7121308525
|
|
@ -36,10 +36,13 @@ Räume sind löschbar (Soft-Delete). Jeder Raum hat ein `icon`-Feld (lucide icon
|
|||
| `brand` | string\|null | Marke |
|
||||
| `model` | string\|null | Modell |
|
||||
| `serialNumber` | string\|null | Seriennummer |
|
||||
| `purchasePrice` | real\|null | Kaufpreis (€) |
|
||||
| `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 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -61,34 +64,66 @@ Kein eigenes DB-Table — als TypeScript-Konstante in den jeweiligen Komponenten
|
|||
3. [Fertig] → Dashboard
|
||||
|
||||
Route: `/inventory/capture/photo?roomId=...`
|
||||
KI: POST `/api/vision/recognize` (OpenAI gpt-4o-mini oder Gemini gemini-1.5-flash)
|
||||
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 eingeben → UPCitemdb-Lookup → bestätigen → Asset(draft)
|
||||
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-Fallback
|
||||
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)
|
||||
Raum wählen → Titel eingeben → Asset(draft) → Detailseite
|
||||
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
|
||||
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.
|
||||
`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 })`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -96,7 +131,7 @@ Statt `storeImage()` (schreibt in Dexie): `uploadInventoryImage(blob)`:
|
|||
|
||||
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.
|
||||
Anzeige auf Detailseite als Chips (`PersonMeta.abbreviation` oder `fullName`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -106,7 +141,11 @@ Kein globaler API-Key — jeder User hinterlegt seinen eigenen Key in den Einste
|
|||
(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).
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -136,8 +175,9 @@ Rate-Limit: 100 Vision-Calls/Tag pro User (DB-basiert via `vision_usage`-Tabelle
|
|||
|
||||
```
|
||||
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 }
|
||||
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? }
|
||||
```
|
||||
|
|
@ -153,13 +193,6 @@ Neue Tabellen im Sync: `rooms`, `assets`, `assetImages`, `assetPersons`.
|
|||
`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.
|
||||
|
|
@ -171,16 +204,15 @@ Dashboard zeigt Gesamtzahl unabhängig vom Status.
|
|||
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.
|
||||
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 (UPCitemdb free tier)
|
||||
- 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)
|
||||
- Preisrecherche nicht implementiert (geplant: eBay Browse API)
|
||||
- BarcodeDetector API (live Kamera-Scan) nicht implementiert — iOS unterstützt es nicht
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.2.33
|
||||
1.2.34
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE `assets` ADD `last_price_estimate` real;--> statement-breakpoint
|
||||
ALTER TABLE `assets` ADD `last_price_estimate_at` text;--> statement-breakpoint
|
||||
ALTER TABLE `assets` ADD `last_price_estimate_source` text;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -127,6 +127,13 @@
|
|||
"when": 1772831133949,
|
||||
"tag": "0017_kind_luke_cage",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "6",
|
||||
"when": 1772869791129,
|
||||
"tag": "0018_graceful_butterfly",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
|||
import { sqliteTable, text, integer, blob, index, primaryKey, foreignKey } from 'drizzle-orm/sqlite-core';
|
||||
import { sqliteTable, text, integer, real, blob, index, primaryKey, foreignKey } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const contexts = sqliteTable('contexts', {
|
||||
id: text('id').notNull(),
|
||||
|
|
@ -236,6 +236,9 @@ export const assets = sqliteTable('assets', {
|
|||
purchaseYear: integer('purchase_year'),
|
||||
notes: text('notes'),
|
||||
coverImageId: text('cover_image_id'),
|
||||
lastPriceEstimate: real('last_price_estimate'),
|
||||
lastPriceEstimateAt: text('last_price_estimate_at'),
|
||||
lastPriceEstimateSource: text('last_price_estimate_source'),
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
deletedAt: text('deleted_at'),
|
||||
purgedAt: text('purged_at'),
|
||||
|
|
|
|||
|
|
@ -206,6 +206,9 @@ function mapAsset(row: typeof assets.$inferSelect): Asset {
|
|||
purchaseYear: row.purchaseYear ?? null,
|
||||
notes: row.notes ?? null,
|
||||
coverImageId: row.coverImageId ?? null,
|
||||
lastPriceEstimate: row.lastPriceEstimate ?? null,
|
||||
lastPriceEstimateAt: row.lastPriceEstimateAt ?? null,
|
||||
lastPriceEstimateSource: (row.lastPriceEstimateSource ?? null) as Asset['lastPriceEstimateSource'],
|
||||
updatedAt: row.updatedAt,
|
||||
deletedAt: row.deletedAt ?? null,
|
||||
purgedAt: row.purgedAt ?? null,
|
||||
|
|
@ -427,7 +430,7 @@ export async function pushChanges(request: SyncPushRequest, userId: string): Pro
|
|||
}
|
||||
|
||||
for (const as of assts) {
|
||||
const row = { id: as.id, userId, roomId: as.roomId ?? null, title: as.title, category: as.category ?? null, status: as.status, condition: as.condition ?? null, brand: as.brand ?? null, model: as.model ?? null, serialNumber: as.serialNumber ?? null, purchasePrice: as.purchasePrice ?? null, purchaseYear: as.purchaseYear ?? null, notes: as.notes ?? null, coverImageId: as.coverImageId ?? null, updatedAt: as.updatedAt, deletedAt: as.deletedAt, purgedAt: as.purgedAt ?? null, version: as.version };
|
||||
const row = { id: as.id, userId, roomId: as.roomId ?? null, title: as.title, category: as.category ?? null, status: as.status, condition: as.condition ?? null, brand: as.brand ?? null, model: as.model ?? null, serialNumber: as.serialNumber ?? null, purchasePrice: as.purchasePrice ?? null, purchaseYear: as.purchaseYear ?? null, notes: as.notes ?? null, coverImageId: as.coverImageId ?? null, lastPriceEstimate: as.lastPriceEstimate ?? null, lastPriceEstimateAt: as.lastPriceEstimateAt ?? null, lastPriceEstimateSource: as.lastPriceEstimateSource ?? null, updatedAt: as.updatedAt, deletedAt: as.deletedAt, purgedAt: as.purgedAt ?? null, version: as.version };
|
||||
if (await upsertEntity(assets, row, conflicts, 'asset', userId)) accepted++;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,9 @@ export interface Asset extends SyncEntity {
|
|||
purchaseYear: number | null;
|
||||
notes: string | null;
|
||||
coverImageId: string | null;
|
||||
lastPriceEstimate: number | null;
|
||||
lastPriceEstimateAt: string | null;
|
||||
lastPriceEstimateSource: 'ebay' | 'formula' | 'category' | null;
|
||||
}
|
||||
|
||||
export interface AssetImage extends SyncEntity {
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ PRIMARY KEY (userId, date)
|
|||
`storeImage()` schreibt immer in Dexie (kein Bypass). 500 × 5 MB = 2,5 GB lokal — nicht akzeptabel.
|
||||
|
||||
→ Neues `uploadInventoryImage(blob): Promise<string>`:
|
||||
1. Bild client-seitig komprimieren: `canvas.toBlob('image/jpeg', 0.75)` + max-width 1920px → ~300 KB
|
||||
1. Bild client-seitig komprimieren: Canvas `toBlob('image/jpeg', 0.75)` + max-width 1920px → ~300 KB. HEIC wird durch `accept="image/jpeg,image/png,image/webp"` im Input verhindert.
|
||||
2. Direkt POST zu `/api/inventory/images` (neuer Endpoint, kein Sync-Push)
|
||||
3. Server speichert in `imageBlobs`, gibt `imageId` zurück
|
||||
4. Client speichert nur `imageId` im Asset — kein Dexie-Blob
|
||||
|
|
@ -366,7 +366,7 @@ Phase 4 → Phase 7 (Docs) [nach stabilem UI]
|
|||
- Datei: `server/src/routes/vision.ts` (NEU)
|
||||
```
|
||||
POST /api/vision/recognize — multipart { image: File }
|
||||
POST /api/vision/barcode — JSON { ean: string }
|
||||
POST /api/vision/barcode — JSON { ean: string } ← nur EAN-Lookup, kein Bild-Input
|
||||
GET /api/vision/settings — { provider, apiKeySet, dailyUsage, dailyLimit }
|
||||
PUT /api/vision/settings — { provider?, apiKey? }
|
||||
```
|
||||
|
|
@ -374,7 +374,8 @@ Phase 4 → Phase 7 (Docs) [nach stabilem UI]
|
|||
```
|
||||
POST /api/inventory/images — multipart { image: File } → speichert in imageBlobs, gibt { id } zurück
|
||||
```
|
||||
Alle Routen: `authMiddleware`. `/api/vision/*` zusätzlich Rate-Limit-Check.
|
||||
Routen: alle `authMiddleware`. Rate-Limit-Check **nur** auf `POST /api/vision/recognize` — nicht auf `/api/vision/barcode` (nutzt UPCitemdb, nicht die Vision-API des Users).
|
||||
UPCitemdb-Limit (100/Tag server-weit, free tier) ist ein separates Server-seitiges Limit — nicht via `vision_usage` getrackt.
|
||||
- Datei: `server/src/index.ts`: + 4 Zeilen (beide Routes registrieren)
|
||||
|
||||
**Verifikation 2:**
|
||||
|
|
@ -404,12 +405,12 @@ Phase 4 → Phase 7 (Docs) [nach stabilem UI]
|
|||
|
||||
#### Schritt 3.1 — Vision Service Client
|
||||
- Datei: `client/src/lib/services/visionService.ts` (NEU)
|
||||
- `compressImage(blob): Promise<Blob>` — HEIC→JPEG + max-width 1920px + quality 0.75
|
||||
- `compressImage(blob): Promise<Blob>` — max-width 1920px + quality 0.75, JPEG-Output via Canvas. HEIC wird durch `accept`-Attribut im Input vermieden (kein HEIC-Decoder nötig).
|
||||
- `uploadInventoryImage(blob): Promise<string>` — POST `/api/inventory/images`, gibt imageId zurück
|
||||
- `recognizePhoto(blob): Promise<RecognizeResult>` — POST `/api/vision/recognize`
|
||||
- `recognizeBarcode(ean: string): Promise<RecognizeResult>` — POST `/api/vision/barcode`
|
||||
- **Kein ZXing client-seitig** — Barcode-Erkennung läuft server-seitig (via UPCitemdb-Lookup)
|
||||
- Barcode-Modus: User gibt EAN manuell ein ODER fotografiert Barcode → Server erkennt via KI+Lookup
|
||||
- **Kein ZXing client-seitig** — EAN wird manuell eingegeben (Nummernpad oder physischer Barcode-Scanner)
|
||||
- Barcode-via-Foto entfällt aus Phase 3: `POST /api/vision/barcode` akzeptiert nur `{ ean: string }`, kein Bild. Foto→EAN wäre ein separater Endpoint oder via BarcodeDetector API (spätere Phase).
|
||||
- Datei: `client/src/lib/services/visionSettingsService.ts` (NEU)
|
||||
- `getVisionSettings()`, `saveVisionSettings()`
|
||||
- `@zxing/browser` **entfällt** — kein client-seitiger Barcode-Scan
|
||||
|
|
@ -434,12 +435,10 @@ Phase 4 → Phase 7 (Docs) [nach stabilem UI]
|
|||
|
||||
#### Schritt 3.5 — BarcodeScan Komponente
|
||||
- Datei: `client/src/lib/components/BarcodeScan.svelte` (NEU)
|
||||
- **Zwei Eingabepfade:**
|
||||
1. EAN manuell eingeben (Nummernpad, für Nutzer mit Barcode-Scanner-Gerät)
|
||||
2. Foto aufnehmen → Server-Lookup (wie Foto-Modus, aber `/api/vision/barcode` mit Bild)
|
||||
- **Eingabe:** EAN manuell eingeben (Nummernpad oder physischer Barcode-Scanner via Keyboard-Input)
|
||||
- States: `idle → looking_up → confirm | error`
|
||||
- Props: `onconfirm(label, category, ean?), oncancel`
|
||||
- **Kein ZXing** — Barcode-Erkennung im Foto via Server (KI-Modell kann Barcodes lesen)
|
||||
- **Kein Foto-Pfad** in Phase 3 — `POST /api/vision/barcode` akzeptiert nur `{ ean: string }`. Foto→EAN kommt erst mit BarcodeDetector API (spätere Phase).
|
||||
|
||||
#### Schritt 3.6 — Loop-Seiten
|
||||
- Datei: `client/src/routes/inventory/capture/photo/+page.svelte` (NEU)
|
||||
|
|
@ -513,6 +512,7 @@ Phase 4 → Phase 7 (Docs) [nach stabilem UI]
|
|||
- Inline-Edit alle Felder (sofort gespeichert via `updateAsset`)
|
||||
- Status-Toggle: Draft → Complete
|
||||
- Galerie: `+ Foto hinzufügen` (ohne KI) + Löschen (Soft-Delete `assetImages`)
|
||||
- **Cover-Konsistenz bei Image-Löschung:** `softDeleteAssetImage` prüft ob `asset.coverImageId === assetImage.imageId` → wenn ja: `updateAsset({ coverImageId: nächstesAssetImage?.imageId ?? null })`
|
||||
- Notizen: `MarkdownEditor.svelte` (bestehende Komponente)
|
||||
- **Personen-Sektion**: "Gehört / zugeordnet" — Chips für zugeordnete Familienmitglieder
|
||||
- `getFamilyPersons()` → Dropdown/Picker mit allen `family`-Personen
|
||||
|
|
@ -816,9 +816,22 @@ Server versucht UPCitemdb → Open Food Facts → EAN als Label.
|
|||
|
||||
---
|
||||
|
||||
## Spätere Phase: Automatische Preisrecherche
|
||||
## Spätere Phase: Automatische Preisschätzung
|
||||
|
||||
Auf Basis von Marke + Modell (+ optional Seriennummer):
|
||||
### Stufenmodell — je mehr Daten, desto präziser
|
||||
|
||||
| Verfügbare Daten | Strategie | Quelle |
|
||||
|---|---|---|
|
||||
| Nur Label/Kategorie | Kategorie-Durchschnittsrange (hardcoded) | Offline |
|
||||
| + Brand + Model | Produktsuche → Median aktueller Listings | eBay Browse API |
|
||||
| + Condition | Zustandsfaktor anwenden | Client-seitig |
|
||||
| + purchaseYear | Abschreibungsfaktor zusätzlich | Client-seitig |
|
||||
| + purchasePrice (ohne Markt) | `Kaufpreis × Zeitfaktor × Zustandsfaktor` | Offline-Formel |
|
||||
|
||||
**Zustandsfaktoren:** `new`=100%, `good`=70%, `fair`=45%, `poor`=20%
|
||||
**Abschreibung:** `-8% pro Jahr` (Elektronik: `-15%`), Minimum 10% Restwert
|
||||
|
||||
### API-Quellen
|
||||
|
||||
| Dienst | Art | Bewertung |
|
||||
|---|---|---|
|
||||
|
|
@ -826,9 +839,45 @@ Auf Basis von Marke + Modell (+ optional Seriennummer):
|
|||
| **Open EAN / UPCitemdb** | Barcode→Produktinfo | Ergänzend für Basisdaten |
|
||||
| **Google Shopping** (Custom Search) | Preisvergleich | Kostenpflichtig ab Volumen |
|
||||
| **idealo** | Scraping | Fragil + rechtlich grenzwertig → nicht empfohlen |
|
||||
| **Versicherungswert-Formel** | Offline | `Kaufpreis × Altersfaktor` als schnelle Schätzung |
|
||||
|
||||
Umsetzung: Button "Preis recherchieren" auf Detailseite → `GET /api/inventory/[id]/price-lookup` → eBay API Query mit `brand + model` → Median-Preis + Link.
|
||||
### Endpoint
|
||||
|
||||
`GET /api/inventory/[id]/price-lookup` → Response:
|
||||
```json
|
||||
{
|
||||
"estimatedValue": 320,
|
||||
"rangeMin": 240,
|
||||
"rangeMax": 410,
|
||||
"confidence": 3,
|
||||
"source": "ebay",
|
||||
"breakdown": "Marktpreis 450 € × Zustand 'gut' (70%) = 315 €",
|
||||
"ebayListingUrl": "https://..."
|
||||
}
|
||||
```
|
||||
|
||||
**Konfidenz-Score (0–3):**
|
||||
- 0 = nur Label/Kategorie → Range aus hardcodierter Tabelle
|
||||
- 1 = +Brand+Model → eBay-Suche möglich
|
||||
- 2 = +Condition → Zustandsfaktor auf eBay-Preis angewendet
|
||||
- 3 = +Condition+purchaseYear → Abschreibungsfaktor zusätzlich
|
||||
|
||||
`purchaseYear` erhöht Score nur wenn `condition` bereits bekannt (sonst keine Aussagekraft).
|
||||
|
||||
### UI auf Detailseite
|
||||
|
||||
```
|
||||
[Preis schätzen ●●●○] ← Konfidenz-Indikator
|
||||
→ "~320 € (eBay-Marktpreise)" ← mit Quelle
|
||||
→ "150–400 € (Kategorie-Schätzung, Marke/Modell ergänzen für besseres Ergebnis)"
|
||||
[Wert übernehmen] → setzt purchasePrice
|
||||
```
|
||||
|
||||
Schätzung wird in eigenen Spalten von `assets` gespeichert (nicht in `notes` — strukturierte Daten gehören nicht in Freitext-Felder):
|
||||
- `lastPriceEstimate REAL` — geschätzter Wert (Median)
|
||||
- `lastPriceEstimateAt TEXT` — Zeitstempel der letzten Schätzung
|
||||
- `lastPriceEstimateSource TEXT` — `'ebay'|'formula'|'category'`
|
||||
|
||||
Diese Felder sind Teil des Asset-Datenmodells (Phase 1 Schritt 1.2 + 1.3 entsprechend ergänzen).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -850,7 +899,7 @@ Umsetzung: Button "Preis recherchieren" auf Detailseite → `GET /api/inventory/
|
|||
|
||||
| Risiko | Mitigation |
|
||||
|---|---|
|
||||
| HEIC von iOS-Kamera | `compressImage()`: Canvas `toBlob('image/jpeg', 0.75)` + max-width 1920px |
|
||||
| HEIC von iOS-Kamera | Browser kann HEIC nicht nativ via Canvas dekodieren. Fix: `<input accept="image/jpeg,image/png,image/webp">` → iOS bietet dann JPEG an (kein HEIC). Alternativ: `heic2any`-Library als Fallback für direkten File-Drop. Canvas-only reicht **nicht**. |
|
||||
| Kamera-Erfassung erfordert Online | PWA mit WLAN — akzeptabel; Fehlermeldung wenn offline |
|
||||
| OpenAI/Gemini Timeout | 10s Timeout, Fallback: manuelles Label |
|
||||
| Rate-Limit-Zähler bei DB-Fehler | `INSERT OR IGNORE` + UPDATE — Worst case: limit nicht enforced (akzeptabel) |
|
||||
|
|
@ -860,6 +909,74 @@ Umsetzung: Button "Preis recherchieren" auf Detailseite → `GET /api/inventory/
|
|||
| Vision-API nicht konfiguriert | Fehlermeldung "Bitte API-Key in Einstellungen hinterlegen" statt 500 |
|
||||
| Rate-Limit Timezone-Drift | Server-UTC für `date`-Feld — dokumentiert, kein Fix nötig |
|
||||
|
||||
## Spätere Phase: KI-Zustandserkennung aus Foto
|
||||
|
||||
Beim Foto-Erfassungs-Flow erkennt die KI nicht nur Label + Kategorie, sondern auch `condition`.
|
||||
|
||||
Erweiterung des Vision-Prompts:
|
||||
```
|
||||
Return JSON:
|
||||
{ "label": "...", "category": "...", "condition": "new|good|fair|poor|unknown", "candidates": [...] }
|
||||
```
|
||||
|
||||
- `condition` wird in LabelConfirm als vorausgefülltes Dropdown angezeigt (editierbar)
|
||||
- `"unknown"` ist **kein gültiger** `condition`-Typ — Server-seitig auf `null` mappen bevor Antwort an Client geht
|
||||
- Kein Breaking Change — bestehende Response ohne `condition` → null
|
||||
|
||||
**Nutzen:** Spart einen manuellen Schritt auf der Detailseite; verbessert Preisschätzungs-Qualität direkt nach Erfassung.
|
||||
|
||||
---
|
||||
|
||||
## Spätere Phase: Offline-Capture-Queue
|
||||
|
||||
Aktuell: Kamera-Erfassung erfordert Online-Verbindung (hard constraint).
|
||||
|
||||
**Verbesserung:** Fotos offline aufnehmen, in IndexedDB queuen, bei nächster Verbindung hochladen.
|
||||
|
||||
```
|
||||
OfflineCaptureQueue (Dexie):
|
||||
id, blob, roomId, capturedAt, uploadedAt (null = pending)
|
||||
```
|
||||
|
||||
- UI zeigt Badge "X Fotos ausstehend" wenn Queue nicht leer
|
||||
- Background-Sync (Service Worker) oder manueller "Jetzt hochladen"-Button
|
||||
- Nach Upload: normaler createAsset-Flow mit zurückgegebenem imageId
|
||||
|
||||
**Komplexität:** Mittel (Service Worker optional — auch ohne funktioniert manueller Trigger).
|
||||
|
||||
---
|
||||
|
||||
## Spätere Phase: BarcodeDetector API (Native Browser)
|
||||
|
||||
Chrome 83+ und Android WebView unterstützen `BarcodeDetector` nativ — **Safari wird nicht unterstützt** (weder iOS noch macOS, Stand 2026). Für iOS-User bleibt das manuelle EAN-Eingabefeld der primäre Pfad.
|
||||
|
||||
```javascript
|
||||
const detector = new BarcodeDetector({ formats: ['ean_13', 'ean_8', 'upc_a'] });
|
||||
const barcodes = await detector.detect(videoFrame);
|
||||
```
|
||||
|
||||
- Live-Kamera-Stream → EAN direkt client-seitig extrahiert
|
||||
- Danach: `POST /api/vision/barcode` mit EAN (wie bisher) für Produktdaten-Lookup
|
||||
- Fallback für nicht unterstützte Browser: manuelles EAN-Eingabefeld (wie aktuell geplant)
|
||||
- Kein ZXing-Dependency nötig
|
||||
|
||||
**Nutzen:** Echter Barcode-Scanner-Feeling ohne Bibliothek.
|
||||
|
||||
---
|
||||
|
||||
## Spätere Phase: Draft-Reminder
|
||||
|
||||
Assets die länger als X Tage als `draft` verbleiben → Hinweis auf Dashboard.
|
||||
|
||||
```
|
||||
Dashboard: "5 Gegenstände noch unvollständig (> 7 Tage)" [Jetzt vervollständigen →]
|
||||
```
|
||||
|
||||
- Client-seitig via `liveQuery` — kein Server nötig
|
||||
- Schwellwert: 7 Tage (hardcoded oder in Settings konfigurierbar)
|
||||
|
||||
---
|
||||
|
||||
## Spätere Phase: Asset-Export
|
||||
|
||||
Für Versicherungszwecke:
|
||||
|
|
|
|||
Loading…
Reference in New Issue