#102 — Offline-Verfügbarkeit Phase 2 — Smart-Add & absolute Wertänderungen #105
Labels
No labels
priority/could
priority/must
priority/should
priority/wont
status/blocked
status/claimed
status/done-migrated
type/bug
type/feature
type/infra
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/todo#105
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Story: Offline-Verfügbarkeit Phase 2 — Smart-Add & absolute Wertänderungen
As a Haushaltsmitglied,
I want to auch die restlichen Einkaufslisten-/Vorratsschrank-Aktionen offline nutzen können, die
#97(Phase 1) bewusst ausgeklammert hat,
so that Offline-Nutzung nicht auf Abhaken/Ankreuzen und einfaches Hinzufügen beschränkt bleibt.
Depends on:
#97(liefert die Basis-Infrastruktur: IndexedDB-Queue, Read-Cache, Sync-on-Reconnect,Status-Banner — diese Story erweitert nur die Aktions-Whitelist und braucht kein neues Fundament).
Aus
#97übernommene, hier zu schließende Lücken:SmartAddInput, ML-Worker-gestützt) und dessen einfacher Fallback(
AddOrActivateShoppingProductCommand) offline nutzbar machen.(
SetPantryProductTargetQuantityCommand,RenameShoppingProductCommand,RenamePantryProductCommand, Kategorie-Zuordnung/Move-Befehle) — diese brauchen laut#97'sArchitect-Hinweis Last-Write-Wins plus Sichtbarkeit im Aktivitäts-Feed, damit ein überschriebener
Stand nicht spurlos verloren geht.
Story — deutlich seltener im "kein Empfang"-Moment gebraucht als die Kernaktionen.
Blocker/Voraussetzung:
#101ist inzwischen vollständig geliefert (inkl.#101dAktivitäts-Feed) - derursprünglich hier vermerkte Blocker ist aufgelöst.
PO-Scope-Entscheidung (2026-08-10, beim Aufgreifen dieser Story): Recherche beim Aufgreifen ergab,
dass die ursprüngliche Annahme "Aktivitäts-Feed-Sichtbarkeit fehlt nur für Pantry" falsch war - Shopping
hat überhaupt keine Produkt-Aktivitäts-Feed-Infrastruktur (keine Entity, keine Query, kein UI-Panel),
während Pantry sie durch
#101bereits vollständig besitzt (PantryActivityEventEntity+GetPantryActivityFeedQuery+PantryActivityFeedPanel, inkl. bereits laufender Emission ausRenamePantryProductCommandHandler/SetPantryProductTargetQuantityCommandHandler). Ein Shopping-Aktivitäts-Feed von Grund auf neu zu bauen (Entity + Migration + Handler + Query + UI-Panel) ist ein
eigenständiges, für sich stehendes Stück Arbeit und für diesen Zyklus nicht proportional - abgespalten
als eigene Folge-Story (
docs/features/new/117_shopping_product_activity_feed.md). Diese Story liefertfür Shopping-seitige Rename/Move-Befehle Last-Write-Wins (keine Daten gehen verloren, der nächste
erfolgreiche Sync/Refetch versöhnt den Stand), aber noch ohne Aktivitäts-Feed-Sichtbarkeit - ein
bewusst offener, dokumentierter Punkt bis zur Folge-Story, nicht stillschweigend übersprungen.
Ebenfalls beim Aufgreifen entdeckt und in derselben Story behoben:
AddOrActivateShoppingProductCommandist die einzige der hier ergänzten Befehle, die serverseitig einen neuen Datensatz anlegen kann (nicht
nur einen bestehenden überschreiben) - ein Offline-Create dieser Art gefolgt von einem sofortigen
Folgebefehl auf dieselbe (temporäre) Id (Kategorie zuweisen, Menge setzen) würde beim Replay nach
Wiederverbindung fehlschlagen, da die Warteschlange jede Aktion mit ihrem ursprünglichen Anfrageinhalt
wortwörtlich wiederholt, ohne Id-Umschreibung. Gelöst durch dasselbe Zugeständnis, das
CreatePantryProductCommands eigener Offline-Pfad in#97bereits akzeptiert hat: ein Offline-Createlandet unkategorisiert/mengenlos, der nächste Sync/Refetch versöhnt den echten Stand - siehe
SmartAddInput.tsxsisOfflineCreate-Guard.Acceptance criteria:
Warteschlangen-/Sync-Mechanik wie
#97. (AddOrActivateShoppingProductCommandneu inofflineConfig.ts; Folgebefehle auf ein Offline-Create werden übersprungen, s. o.)per Last-Write-Wins. Für Pantry landet ein überschriebener Zwischenstand sichtbar im
Aktivitäts-Feed (Infrastruktur bereits aus
#101vorhanden, Body um den alten Wert ergänzt). FürShopping fehlt die Aktivitäts-Feed-Sichtbarkeit noch - bewusst an
#117delegiert, s. o.(kein stiller Fehlschlag, klare Fehlermeldung statt der generischen Netzwerkfehler-Toast) - neue
requireOnline()-Vorprüfung inCsvExportDialog/CsvImportDialog/PantryBarcodeScanner/ShoppingBarcodeScanDialog/InvitePanel/MembersPanel.design (
102_offline_support_phase2_absolute_value_actions_design.md)Design:
#102— Offline-Verfügbarkeit Phase 2Was diese Story an
#97's Fundament ändert#97baute die komplette Infrastruktur (offlineDb.tslocalStorage-Queue+Read-Cache,offlineWrite.tssoptimistisches Apply,
offlineSync.tss FIFO-Drain-on-reconnect,OfflineBanner.tsx) bereits fertig.#102ist reine Whitelist-Erweiterung inofflineConfig.tsplus punktuelle Frontend-Änderungen - keineneue Infrastruktur, mit zwei Ausnahmen:
offlineConfig.tssisCreate?: boolean-Flag ist weg.handleOfflineWriteentscheidet jetzt anhandvon
cached.some(p => p.id === updated.id), ob ein Datensatz angehängt oder ersetzt wird, stattanhand eines statischen Flags pro Befehl. Notwendig, weil
AddOrActivateShoppingProductCommandpro Aufruf zwischen Erstellen und Aktivieren entscheidet (je nachdem, ob ein Namens-Match im Cache
existiert) - ein statisches Flag kann das nicht ausdrücken. Verhalten für alle bestehenden Einträge
unverändert (ein echter Create liefert immer eine neue negative Id, die nie im Cache steht; ein
Update liefert immer dieselbe Id wie das gefundene Element).
offlineGuard.ts:requireOnline(actionLabel)prüftofflineSync.getSnapshot().isOnlineundzeigt bei
falseeinen spezifischen Toast stattcallApis generischem"Failed to send the request..." - für die drei AC-benannten "erfordert Verbindung"-Aktionen.
Neue Whitelist-Einträge (
offlineConfig.ts)AddOrActivateShoppingProductCommandAddOrActivateShoppingProductCommandHandler.csfindExisting- sucht selbst per case-insensitivem Namens-Match (nachstripQuantityPrefix, wiederverwendet ausutils/shoppingQuantityPrefix.ts, statt einer zweiten Kopie der Logik). Kein Treffer → neuer, bereits aktivierter, unkategorisierter Datensatz (negative Temp-Id).RenameShoppingProductCommandRenameShoppingProductCommandHandler.csRenamePantryProductCommandRenamePantryProductCommandHandler.csMoveShoppingProductCommand/MovePantryProductCommandhandleOfflineWritekann pro Aufruf nur eine Cache-Zeile ersetzen - die serverseitige Geschwister-Neunummerierung (alte + neue Kategorie) wird offline nicht repliziert, nurcategoryId/sortOrderdes bewegten Produkts selbst. Andere Zeilen tragen bis zum nächsten Sync/Refetch einen veraltetensortOrder- dieselbe Art Zugeständnis wiecheckOutPantryProducts bereits akzeptierter Low-Stock-Seiteneffekt.SetPantryProductTargetQuantityCommandSetPantryProductTargetQuantityCommandHandler.csBewusst nicht ergänzt:
CreateShoppingProductCommand(der zweistufige Create-dann-Activate-Pfad inSmartAddInput.createInCategorywürde einen zweiten Warteschlangen-Eintrag erzeugen, der beim Replay aufdie temporäre Id des ersten verweist - siehe "Temp-Id-Verkettung" unten).
ImportShoppingProductsFromCsvCommand/ImportPantryProductsFromCsvCommand(CSV bleibt laut AC explizit "erfordert Verbindung").Temp-Id-Verkettung — die eine echte Falle dieser Story
offlineSync.tssdrainQueue()wiederholt jede Warteschlangen-Aktion mit ihrem ursprünglichenAnfrageinhalt, ohne Id-Umschreibung. Eine Kette aus zwei Offline-Aktionen, bei der die zweite auf die
(negative, temporäre) Id der ersten verweist, schlägt beim echten Replay fehl: der Server kennt die
temporäre Id nie, antwortet mit 404, und
drainQueue()s bestehende 404-Regel (Produkt wurdezwischenzeitlich gelöscht → verwerfen statt endlos wiederholen) verwirft die zweite Aktion still - ein
echter, sonst unsichtbarer Datenverlust, genau das, was diese Story mit "damit ein überschriebener Stand
nicht spurlos verloren geht" verhindern soll.
Das trifft
SmartAddInput.tsxan zwei Stellen, die nach einemAddOrActivateShoppingProductCommandsofort einen Folgebefehl auf dessen zurückgegebene Id schicken (Kategorie-Auswahl →
MoveShoppingProductCommand,Mengen-Popup →
ActivateShoppingProductCommand). Fix:product.id < 0(negative Temp-Id) überspringtbeide Folgeschritte - identisch zu
createPantryProducts bereits akzeptiertem#97-Kompromiss("unkategorisiert bis zum nächsten Refetch"). Eine vollständige Lösung (Warteschlange schreibt spätere
Einträge um, sobald ein Create real aufgelöst ist) wäre eine eigenständige Architektur-Erweiterung an
#97sKern und für diesen Zyklus nicht verhältnismäßig.Aktivitäts-Feed-Sichtbarkeit — Pantry ja, Shopping (noch) nein
Siehe die Story-Datei selbst für die volle Begründung. Kurzfassung: Pantry hatte die Infrastruktur
bereits aus
#101, Shopping hat gar keine - letzteres als eigene Folge-Story#117abgespalten statthier eine Mini-Version davon nebenbei zu bauen.
SetPantryProductTargetQuantityCommandHandlersAktivitäts-Body zeigt jetzt zusätzlich den alten Wert (
"changed target of 'X' from 3 to 5"statt nur"set target of 'X' to 5"), damit ein Last-Write-Wins-Überschreiben im Feed nachvollziehbar bleibt, nichtnur implizit aus einem früheren Feed-Eintrag rekonstruierbar.
requireOnline()— "erfordert Verbindung" statt genereller FehlerAngewendet an sechs Stellen, jeweils am Anfang der auslösenden Aktion, bevor irgendein Netzwerk-Call
versucht wird:
CsvExportDialog.handleExport,CsvImportDialog.handleImport,PantryBarcodeScanner.submitBarcode/submitWithName,ShoppingBarcodeScanDialog.submitBarcode/submitWithName,InvitePanel.handleGenerate/handleRevoke,MembersPanel.handleRemove.InvitePanel/MembersPanelsind listentyp-übergreifend geteilte Komponenten (auch für Todo-Listen, die nie eineeigene Offline-Unterstützung bekommen haben) - die klarere Meldung gilt dort einheitlich, statt nur für
Shopping/Pantry-Aufrufer bedingt zu gelten, was implementierungstechnisch unnötig verzweigt hätte und für
Todo-Nutzer ohnehin eine reine Verbesserung gegenüber der bisherigen generischen Meldung ist.
Security-Vorprüfung
Keine neue Backend-Angriffsfläche - alle sechs neuen
offlineConfig.ts-Einträge mirrorn bereitsexistierende, bereits autorisierte Commands 1:1 (dieselbe serverseitige
WithAuthorization-Prüfung greiftbeim tatsächlichen Replay unverändert). Der Client vertraut sich selbst nur optimistisch bis zum echten
Server-Roundtrip - identisch zu
#97sbereits geprüftem Modell.requireOnline()ist reine UX, keineSicherheitsgrenze.