From 71213085250ddca3a61d5079758b62cc26c166bb Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 7 Mar 2026 09:05:36 +0100 Subject: [PATCH] upd inventory --- docs/feature-inventory.md | 80 +- ka-note/VERSION | 2 +- .../drizzle/0018_graceful_butterfly.sql | 3 + .../server/drizzle/meta/0018_snapshot.json | 1913 +++++++++++++++++ ka-note/server/drizzle/meta/_journal.json | 7 + ka-note/server/ka-note.db-shm | Bin 32768 -> 32768 bytes ka-note/server/ka-note.db-wal | Bin 3798672 -> 4144752 bytes ka-note/server/src/db/schema.ts | 5 +- ka-note/server/src/lib/sync-service.ts | 5 +- ka-note/shared/src/types.ts | 3 + plans/vision-inventory.md | 147 +- 11 files changed, 2123 insertions(+), 42 deletions(-) create mode 100644 ka-note/server/drizzle/0018_graceful_butterfly.sql create mode 100644 ka-note/server/drizzle/meta/0018_snapshot.json diff --git a/docs/feature-inventory.md b/docs/feature-inventory.md index a6d37b5..4925ea0 100644 --- a/docs/feature-inventory.md +++ b/docs/feature-inventory.md @@ -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 diff --git a/ka-note/VERSION b/ka-note/VERSION index b7ae2cf..c6fad92 100644 --- a/ka-note/VERSION +++ b/ka-note/VERSION @@ -1 +1 @@ -1.2.33 \ No newline at end of file +1.2.34 \ No newline at end of file diff --git a/ka-note/server/drizzle/0018_graceful_butterfly.sql b/ka-note/server/drizzle/0018_graceful_butterfly.sql new file mode 100644 index 0000000..d8acf93 --- /dev/null +++ b/ka-note/server/drizzle/0018_graceful_butterfly.sql @@ -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; \ No newline at end of file diff --git a/ka-note/server/drizzle/meta/0018_snapshot.json b/ka-note/server/drizzle/meta/0018_snapshot.json new file mode 100644 index 0000000..113995a --- /dev/null +++ b/ka-note/server/drizzle/meta/0018_snapshot.json @@ -0,0 +1,1913 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "dba0d183-ce68-4951-a5ab-bda1b48bbd8b", + "prevId": "545c0342-22e0-46aa-b833-ad968f590798", + "tables": { + "ai_locks": { + "name": "ai_locks", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "locked_at": { + "name": "locked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + "key_hash" + ], + "isUnique": true + }, + "api_keys_user_id_idx": { + "name": "api_keys_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "api_keys_key_hash_idx": { + "name": "api_keys_key_hash_idx", + "columns": [ + "key_hash" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "asset_images": { + "name": "asset_images", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "asset_images_updated_at_idx": { + "name": "asset_images_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "asset_images_asset_id_idx": { + "name": "asset_images_asset_id_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + }, + "asset_images_user_id_idx": { + "name": "asset_images_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "asset_images_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "asset_images_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "asset_persons": { + "name": "asset_persons", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "person_id": { + "name": "person_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "asset_persons_updated_at_idx": { + "name": "asset_persons_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "asset_persons_asset_id_idx": { + "name": "asset_persons_asset_id_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + }, + "asset_persons_user_id_idx": { + "name": "asset_persons_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "asset_persons_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "asset_persons_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serial_number": { + "name": "serial_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purchase_price": { + "name": "purchase_price", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purchase_year": { + "name": "purchase_year", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_price_estimate": { + "name": "last_price_estimate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_price_estimate_at": { + "name": "last_price_estimate_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_price_estimate_source": { + "name": "last_price_estimate_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "assets_updated_at_idx": { + "name": "assets_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "assets_user_id_idx": { + "name": "assets_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "assets_room_id_idx": { + "name": "assets_room_id_idx", + "columns": [ + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "assets_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "assets_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contexts": { + "name": "contexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_favorite": { + "name": "is_favorite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "contexts_updated_at_idx": { + "name": "contexts_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "contexts_user_id_idx": { + "name": "contexts_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "contexts_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "contexts_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "history_entries": { + "name": "history_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "linked_context_id": { + "name": "linked_context_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "done_at": { + "name": "done_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wiedervorlage_date": { + "name": "wiedervorlage_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wiedervorlage_resolved_at": { + "name": "wiedervorlage_resolved_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "history_entries_updated_at_idx": { + "name": "history_entries_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "history_entries_topic_id_idx": { + "name": "history_entries_topic_id_idx", + "columns": [ + "topic_id" + ], + "isUnique": false + }, + "history_entries_user_id_idx": { + "name": "history_entries_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "history_entries_topic_id_user_id_topics_id_user_id_fk": { + "name": "history_entries_topic_id_user_id_topics_id_user_id_fk", + "tableFrom": "history_entries", + "tableTo": "topics", + "columnsFrom": [ + "topic_id", + "user_id" + ], + "columnsTo": [ + "id", + "user_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "history_entries_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "history_entries_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_blobs": { + "name": "image_blobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "image_blobs_user_id_idx": { + "name": "image_blobs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "image_blobs_content_hash_idx": { + "name": "image_blobs_content_hash_idx", + "columns": [ + "content_hash" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "image_blobs_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "image_blobs_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notebooks": { + "name": "notebooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_favorite": { + "name": "is_favorite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "notebooks_updated_at_idx": { + "name": "notebooks_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "notebooks_user_id_idx": { + "name": "notebooks_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "notebooks_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "notebooks_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "page_notebooks": { + "name": "page_notebooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notebook_id": { + "name": "notebook_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "page_notebooks_updated_at_idx": { + "name": "page_notebooks_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "page_notebooks_page_id_idx": { + "name": "page_notebooks_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + }, + "page_notebooks_notebook_id_idx": { + "name": "page_notebooks_notebook_id_idx", + "columns": [ + "notebook_id" + ], + "isUnique": false + }, + "page_notebooks_user_id_idx": { + "name": "page_notebooks_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "page_notebooks_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "page_notebooks_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pages": { + "name": "pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "is_private": { + "name": "is_private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_favorite": { + "name": "is_favorite", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "pages_updated_at_idx": { + "name": "pages_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "pages_user_id_idx": { + "name": "pages_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "pages_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "pages_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ratings": { + "name": "ratings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "history_entry_id": { + "name": "history_entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "person_name": { + "name": "person_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "ratings_updated_at_idx": { + "name": "ratings_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "ratings_topic_id_idx": { + "name": "ratings_topic_id_idx", + "columns": [ + "topic_id" + ], + "isUnique": false + }, + "ratings_user_id_idx": { + "name": "ratings_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ratings_topic_id_user_id_topics_id_user_id_fk": { + "name": "ratings_topic_id_user_id_topics_id_user_id_fk", + "tableFrom": "ratings", + "tableTo": "topics", + "columnsFrom": [ + "topic_id", + "user_id" + ], + "columnsTo": [ + "id", + "user_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ratings_history_entry_id_user_id_history_entries_id_user_id_fk": { + "name": "ratings_history_entry_id_user_id_history_entries_id_user_id_fk", + "tableFrom": "ratings", + "tableTo": "history_entries", + "columnsFrom": [ + "history_entry_id", + "user_id" + ], + "columnsTo": [ + "id", + "user_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ratings_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "ratings_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_type": { + "name": "group_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "rooms_updated_at_idx": { + "name": "rooms_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "rooms_user_id_idx": { + "name": "rooms_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rooms_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "rooms_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "history_entry_id": { + "name": "history_entry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'open'" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "tasks_updated_at_idx": { + "name": "tasks_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "tasks_context_id_idx": { + "name": "tasks_context_id_idx", + "columns": [ + "context_id" + ], + "isUnique": false + }, + "tasks_assignee_idx": { + "name": "tasks_assignee_idx", + "columns": [ + "assignee" + ], + "isUnique": false + }, + "tasks_user_id_idx": { + "name": "tasks_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "tasks_source_external_id_idx": { + "name": "tasks_source_external_id_idx", + "columns": [ + "user_id", + "source", + "external_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tasks_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "tasks_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "topics": { + "name": "topics", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "snooze_until": { + "name": "snooze_until", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_new": { + "name": "is_new", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "topics_updated_at_idx": { + "name": "topics_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "topics_context_id_idx": { + "name": "topics_context_id_idx", + "columns": [ + "context_id" + ], + "isUnique": false + }, + "topics_user_id_idx": { + "name": "topics_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "topics_context_id_user_id_contexts_id_user_id_fk": { + "name": "topics_context_id_user_id_contexts_id_user_id_fk", + "tableFrom": "topics", + "tableTo": "contexts", + "columnsFrom": [ + "context_id", + "user_id" + ], + "columnsTo": [ + "id", + "user_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "topics_id_user_id_pk": { + "columns": [ + "id", + "user_id" + ], + "name": "topics_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_settings": { + "name": "user_settings", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_settings_user_id_key_pk": { + "columns": [ + "user_id", + "key" + ], + "name": "user_settings_user_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vision_usage": { + "name": "vision_usage", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "vision_usage_user_id_date_pk": { + "columns": [ + "user_id", + "date" + ], + "name": "vision_usage_user_id_date_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/ka-note/server/drizzle/meta/_journal.json b/ka-note/server/drizzle/meta/_journal.json index efc6fe9..48b69d6 100644 --- a/ka-note/server/drizzle/meta/_journal.json +++ b/ka-note/server/drizzle/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/ka-note/server/ka-note.db-shm b/ka-note/server/ka-note.db-shm index c780ce68c25b575df316a3285639f8a09f249c24..51f55d4dae8e9279916139697195a12da46f2f19 100644 GIT binary patch delta 458 zcmb7<%`O8`7>2+1OsCbNr{lN&X)y++ek);1Vj&@2*tiaf1)Y1CxC4=Nr{)ISgVYkS zwUCI7i15xi8(Uv8=lR~}d^vMY7^*N-jp9p1l2ZI2QmBz-2JE}9-oe|`@%l$L|Nij2 zRZ8z2ZeQ;HcRo2gspQ>v%E{EEX6Hd1_p_Hz>iwG z?I>9$k5II7&PYe9Mh;mS7@3k4BZsY=Cjwx^rUh=bp}E#EYSkjafib!O6^169oYWZ4 zqnp!IbgQqOtZZ`Q)5SRPtuR5<6?!?;#3Vxir$aW__&}erQw*FeS(FL>q@QTOb)_Jy kY|nrJW1O_CvH4Q)2o81;e0z^g-Gr|&9VM5~o1kXo7d!QXQ~&?~ literal 32768 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 delta 1259 zcmZA0drVVz6bJCz7rnH-+_u2RODJy`paomNfDAV_v#=TB9CSntI#ZA>j>j}W7!;^r zCbIhCP8hLBoQiBK5;=eYYKd$UGkM51UZxBM$789FY+*pzxf_2hYx4Qy+~4`YB{JYM4b_j>L4$?cZZ%_~gK z1Fk3M5;NH$c%5?z6rBPSO=iE*<`P(BFN2ISv7KgS-;&8m?S+`4)Cpw`*nSa6rQ~Y- z_><>5FH?}%O=7ux_dum8>i~danO*It!=5dMT+3w4x5fEzt4P*BlW%t)xqQX6w*^w% z9WY{P{$Au28CD6#HMT+TZ&_=iHxq)v&@7Wmg}Ke8$sOGpaDS7dOiEUljQyCb>%I@B z_t~x9BkKCl_mb0@6hDbC=OWA>cKDqp*Bd``_wB8^3D!b3^oRC*Rclb^7SO#W!P2@^ zIX|1#TEduba6;ksZpj=?8b7}3{;lfsHZUDzBU;C+eJ`dSZe^_b+)#L$$41WWKeR?1 zSKb3_IU9*DU(Dq>pALqcarPc#r#~>pcvnjpX_LfXqSNWPe4;m+Am|;iqUGDUu>C8^ zjD+kvTgS~yci}MBV|+henqCL*v`H3dQrHzhC_Ul~kl4pIPwI_ZQct}qf5?zd&Qx>$RW|IAIx`erjM$9Qp&8_Of$mJz+jB zKO5HtDS_;2{jK?e3x3NouokncmBSYCu0W^T&C%r~Mr@U}zNMs&YEF|IVo8qm9EdS>l$hQ6wnZQ`oHo4a1{J z4VJ=% zh(wM^gv3aKDCC4_#O{+JIdVoW$Q8YWUPf-n9eE&6`: - 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` — HEIC→JPEG + max-width 1920px + quality 0.75 + - `compressImage(blob): Promise` — 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` — POST `/api/inventory/images`, gibt imageId zurück - `recognizePhoto(blob): Promise` — POST `/api/vision/recognize` - `recognizeBarcode(ean: string): Promise` — 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: `` → 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: