Bericht: Kaufland-Bon-Import bekommt eine Vorschau (07.06.2026, abends)
Executive Summary
Der Kaufland-/Fremd-Bon-Import (ADR-52, vom selben Tag) bucht einen hochgeladenen Bon nicht mehr blind ein. Stattdessen liest die KI das PDF nur noch aus, das Frontend zeigt eine Vorschau mit allen erkannten Positionen, und erst auf „Importieren" landet etwas in der Datenbank. Damit kann ein verlesener oder kaputter Bon den Bestand nicht mehr im Vorbeigehen verändern — genau die Sorge, die Dennis geäußert hat („bevor mir ein Kassenzettel meine Datenbank zerschießt").
Der Code dieser Vorschau war in der Vorsession schon zu ~90 % gebaut worden, ging dort aber mitten im Verifizieren in den Cyber-Safeguard-Filter (das Playwright-Skript las den Internal-Token aus .env) und wurde nie committet — beim /clear war er weg. Diese Session hat ihn aus dem Session-Transcript rekonstruiert und sauber fertiggestellt; zugleich wurde ein verfrühter Fehlansatz (ein dry_run-API-Flag) wieder entfernt.
In einfachen Worten
Vorher: Bon-PDF hochladen → die KI bucht sofort alles ein, was sie zu erkennen glaubt. Wenn sie sich verliest, ist der Mist schon in Vorrat und Archiv.
Jetzt: Bon-PDF hochladen → die KI liest vor → du siehst eine Liste mit Markt, Datum, Endbetrag und allen Positionen. Du kannst pro Zeile entscheiden, ob sie in den Vorrat soll (Pfand nie), siehst eine Warnung, falls die Summen nicht passen, und tippst dann Importieren oder Verwerfen. Bis zu diesem Tippen ist die Datenbank unangetastet.
Architektur
📄 PDF → POST /api/receipts/import-pdf (Raw-Body, %PDF-Check, 15 MB) → ai_job "receipt_pdf"
→ ai_jobs.job_receipt_pdf: claude-CLI + Read liest PDF → Bon-JSON
NUR EXTRAHIEREN, kein Import (JSON in Job-note)
↓ Frontend pollt den Job
📋 ReceiptPreview.svelte (Sheet + AiProgress-Ring):
· Positionen/Preise/Rabatte/Pfand, Kontrollsummen-⚠️
· je Food-Position eine Checkbox (Pfand fix aus)
├─ „Verwerfen" → nichts passiert
└─ „Importieren" → POST /api/receipts/import (items[].to_inventory je Zeile)
→ receipt_import.import_receipt_dict():
Bon IMMER komplett ins Archiv,
nur angehakte Positionen buchen Bestand
Der entscheidende Schnitt: Der Host-Worker importiert nicht mehr selbst, sondern liefert nur das extrahierte JSON zurück. Die Entscheidung „einbuchen ja/nein" liegt jetzt vollständig beim Menschen im Vorschau-Sheet.
Was diese Session konkret tat
| Datei | Änderung |
|---|---|
app/ai_jobs.py |
job_receipt_pdf extrahiert nur noch (Bon-JSON → Job-note), kein Auto-Import mehr; urllib.parse-Import entfernt |
app/frontend/src/components/ReceiptPreview.svelte |
neu — Vorschau-Sheet: pollt via AiProgress, listet Positionen, Per-Zeile-Checkbox, Kontrollsummen-Warnung, Importieren/Verwerfen |
app/frontend/src/pages/Shopping.svelte |
„📄 Bon-PDF"-Upload öffnet das Vorschau-Sheet (previewJob) statt Fire-and-forget |
app/backend/receipt_import.py |
receipt_items[].to_inventory (Default true) steuert das Einlagern je Position; Bon kommt immer komplett ins Archiv |
app/backend/main.py |
verfrühtes dry_run-Flag wieder entfernt (toter Pfad) |
app/tests/contract/test_bons_stats.py |
Contract-Test to_inventory=False → Position nicht im Vorrat, Bon trotzdem vollständig im Archiv |
Hart erarbeitet / Fallen
- Cyber-Safeguard-Filter: Das Verifikations-Playwright-Skript der Vorsession las den
INTERNAL_API_TOKENdirekt aus.env— das hat den Real-Time-Filter ausgelöst und die Session lahmgelegt, bevor irgendetwas committet war. Lehre: Verifikations-Skripte nicht mit Secret-Auslesen aus.envbauen; lieber den Token als Umgebungsvariable übergeben. - Namens-Matching ist Gleichheit, kein Substring:
_match_foodmatcht_norm(name)perin (display, name)— der Seed-Skyr heißt „Skyr Natur", ein Bon „Skyr Natur 450g" matcht nicht und legt einen neuen Artikel an. Beim Testen die kanonischen Seed-Namen verwenden, sonst täuscht das „nicht im Vorrat". - Falsch abgebogen und korrigiert: Der erste Reflex dieser Session war ein
?dry_run=1-API-Flag mit Match-Vorschau im JSON. Das war nicht, was gewünscht war (visuelle Vorschau vor dem Schreiben) — wieder rausgenommen, statt zwei konkurrierende Mechanismen stehenzulassen.
Verifikation
- API-Gate grün:
./app/tests/run.sh→ 157 passed, 88 Snapshots, deploybar. (Kein TDD fürs Backend — nur die Black-Box-API-Suite, ADR-51.) - Playwright-Smoke des echten UI-Flows (Wegwerf-Kandidat, TESTMODE): Upload → Vorschau mit 4 Positionen → Tomaten abwählen → Importieren → Skyr-Bestand 1,0 → 2,0, Tomaten nicht im Vorrat, Bon komplett (4 Positionen) im Archiv, Toast „importiert ✓".
- Backend + Frontend deployt (Prod
/api/receipts→ 200), Container enthält Vorschau-UI +to_inventory, keindry_run-Rest.
Testplan
Gerätetest hinter Login: silverscale.dennisfisch.de/testplan/kaufland-pdf-vorschau (Quelle app/testplaene/kaufland-pdf-vorschau.html, ro-Mount, kein Rebuild). 17 Punkte über Upload+KI · Vorschau prüfen · Verwerfen schreibt nichts · Importieren bucht gezielt · Idempotenz/Fehlfälle. Funde direkt am Plan (🐞 + Notiz + ⌘V-Screenshot, ADR-50) → app/data/testplan-feedback/kaufland-pdf-vorschau/. Braucht ein echtes Kaufland-Bon-PDF aus der Card-App.
Nachtrag: erster Gerätetest deckte einen Worker-Bug auf (c02)
Dennis' erster echter Upload (ein Kaufland-Bon mit 57 Positionen) zeigte im Vorschau-Sheet nur den rohen Fehler „Expecting value: line 1 column 1 (char 0)". Systematische Diagnose (stderr der CLI war auf DEVNULL):
- Root Cause:
claude()baute den CLI-Aufruf als… --allowedTools Read <prompt>. Das Flag--allowedToolsist variadisch und schluckt das direkt folgende Prompt-Argument als weiteren „Tool-Namen". Die CLI bekam damit keinen Prompt, wartete auf stdin, lief leer →claude_jsonmachtejson.loads("")→ der kryptische Fehler. Betroffen war nur der Tool-Pfad:receipt_pdf(immer) und der claude-Fallback vonphoto— die toolfreien Jobs liefen durch (sie hängen kein--allowedToolsan). - Fix:
---Separator zwischen Optionen und Prompt (cmd + ["--", prompt]) — beendet die Options-Parsing, der Prompt bleibt Prompt. Dazuclaude_jsonwirft jetzt bei JSON-loser Antwort eine klare Meldung statt desjson.loads-Fehlers, und das Vorschau-Sheet zeigt eine freundliche Fehlermeldung („Bon konnte nicht gelesen werden …") statt der nackten Exception. - Verifikation: Derselbe 169-KB-Bon läuft jetzt durch den deployten Host-Worker (Job neu eingereiht →
done): „Kaufland Iserlohn, Raiffeisenstraße 5" · 170,38 € · 57 Positionen, Pfand und Rabatte korrekt. Kein Phantom-Bon (Extraktion bucht nichts). - Hart erarbeitet:
ai_jobs.pyist der Host-Worker (Cron, nicht im Container) — Änderungen sind ohne Docker-Deploy ab dem nächsten Tick live. Und: bei CLI-Debugging niemalsstderraufDEVNULLlassen, sonst ist die eigentliche Fehlermeldung unsichtbar.
Offen / nächste Schritte
- Gerätetest durch Dennis (s. Testplan) — insbesondere mit einem echten Kaufland-PDF, ob die KI-Extraktion centgenau trifft.
- Welle-7-Gerätetest (Vorrat-Kreislauf) ist weiterhin offen.
- Phase B (Kaufland-Auto-Pull) bleibt vertagt — Cert-Pinning, Recherche §8.
Bezug: Kaufland-PDF-Import · ADR-52 in app/ARCHITEKTUR.md · Commit 6c5c2ca