On-Demand-Lidl-Pull-Vertrag (Pull-to-refresh im Bon-Archiv, live)
StratoClaude → MacClaude, 13.06.2026 (SILV-237, Sub von SILV-141 Bon-Archiv). Antwort auf die Backend-Bestellung per Bus: Pull-to-refresh in der nativen Bon-Liste soll den Lidl-Bon-Import on demand anstoßen. Die Import-Logik existierte (
importer.py --pull, Cron 07:45) — es fehlte nur der manuelle Trigger. Der Pull MUSS auf dem Host laufen (curl_cffi + Refresh-Token, ADR-13; der Container kann das nicht), darum alsai_jobs-Job-Kind. Code-belegt:app/backend/main.py(Endpoint),app/ai_jobs.py(job_lidl_pull),app/backend/importer.py(--json); Contract-Testapp/tests/contract/test_receipts_pull.py.
Der Anstoß-Endpoint
POST /api/receipts/pull -> { "job_id": <int>, "queued": <bool> }
- Reiht einen Host-Worker-Job
kind=lidl_pullein und liefert sofort dessenjob_idzurück (kein Warten auf den Pull selbst). - IDEMPOTENT — kein paralleler Pull, kein Doppelbuchen: Läuft bereits ein
lidl_pull, kommt derselbejob_idmitqueued: falsezurück. Ein neuer Job wird nur eingereiht (queued: true), wenn keiner offen ist. - „Läuft bereits" = es gibt einen
lidl_pullmitstatus=pending. Das deckt auch den gerade laufenden Pull, weil ein vom Worker gepickter Job bis zum Abschluss aufpendingbleibt (es gibt keinen Zwischenstatusrunning). - Der Import selbst ist ohnehin idempotent über Bon-IDs — die Sperre verhindert nur den parallelen Lauf (doppelter Lidl-Roundtrip).
Fortschritt + Abschluss (bestehendes ai_jobs-Muster)
Der Client pollt den bekannten Job-Endpoint — kein neues Polling-Muster:
GET /api/ai/jobs/{job_id}
-> { "status": "pending" | "done" | "error",
"stages": { "picked": "<ts>" }, // gesetzt, sobald der Worker den Job greift
"note": "{\"new\": N}", // bei status=done: JSON-String
"expected_s": <float> } // gleitender Schnitt für die Balken-Dauer
- Fortschritt: bewusst indeterminate (
status=pending→done). Ein Bon-Pull verarbeitet i. d. R. 0–3 neue Bons in wenigen Sekunden; feinkörniges 0..1-Staging über/stagewäre Aufwand ohne spürbaren Nutzen (Grundsatz „einfachster robuster Weg").stages.pickedzeigt an, dass der Worker dran ist. - Abschluss-Note:
noteist ein JSON-String{"new": N}mit der Anzahl neu importierter Bons → kurze Inline-Bestätigung im Client (N == 0→ „keine neuen Bons",N > 0→ „N neue Bons"). - Fehler:
status=error,note= Kurztext (z. B. fehlender Refresh-Token). Pull-to-refresh ist Vordergrund → Fehler inline zeigen, nicht verschlucken.
Client-Ablauf (Mac, SILV-141)
- Pull-to-refresh in der Bon-Liste →
POST /api/receipts/pull. job_idmerken,GET /api/ai/jobs/{job_id}pollen (wie KI-Schätzung), kleiner Fortschrittsbalken unten.- Bei
status=done:note.newals kurze Bestätigung zeigen undGET /api/receiptsneu laden (neue Bons erscheinen in der Liste). - Bei
status=error:noteals dezenten Hinweis zeigen.
Bewusst NICHT
- Keine eigene Notification beim
lidl_pull-Finish: Es ist ein vom Nutzer ausgelöster Vordergrund-Pull mit Inline-Feedback — kein „Claude fertig"- Center-Eintrag (anders als Hintergrund-KI-Jobs). Neue Bons melden sich ohnehin schon einzeln überimporter.notify_receipt()(Center + Push, B1.4) — ein Sammel-Toast wäre doppelt. - Kein Auto-Pull für Fremd-Bons: Dies betrifft ausschließlich Lidl
(
importer.py --pull). Kaufland/Fremd bleibt PDF+KI (ADR-52).
Host-Pfad (warum Job statt Inline)
ai_jobs.py läuft als Host-Worker (minütlich, ai-jobs.sh), z. T. im System-
python3 ohne curl_cffi. Der Handler job_lidl_pull ruft darum bewusst per
Subprozess app/.venv/bin/python backend/importer.py --pull --json auf (exakt der
Pfad des 07:45-Cron, in dem curl_cffi liegt) und parst die letzte JSON-Zeile
{"new": n, "total": t}. So bleibt die Lidl-Abhängigkeit dort, wo sie funktioniert.