[Bug] Day-Planner: Re-Plan verliert bereits geplante Items aus Response #461
Labels
No labels
app/archiv
app/einkaufslisten
app/imap-client
app/wissensbasis
arch-answered
arch-question
area/api
area/auth
area/infra
area/mobile
area/shared
area/ui
area/web
portfolio-status
prio/high
prio/low
prio/medium
roadmap/public
size/l
size/m
size/s
size/xl
size/xs
status/blocked
status/needs-info
type/bug
type/chore
type/docs
type/feature
type/idea
type/refactor
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
admin-mrrm/mrrmlabapp#461
Loading…
Add table
Add a link
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?
Symptom
Beim ersten Öffnen von
/heutewird ein Tracking-Item ("Sendung prüfen") als bereits geplant angezeigt. Nach Klick auf "Tag jetzt planen" verschwindet der Eintrag aus der UI — obwohl er fachlich noch valide ist.Gefunden bei rc20-Device-Validation am 2026-06-12.
Root Cause
Zwei Layer beteiligt:
1. Backend —
apps/api/src/modules/planner/planner.service.ts:101-106planToday()füttert nurpending-Candidates in den Slotter. Bereitsplanned-Candidates werden ignoriert und tauchen damit nicht implacedDtos-Return auf. Folge: die Mutation-Response ist unvollständig — sie enthält nur die in diesem Aufruf neu eingeplanten Items.2. Frontend —
packages/feature-day-planner/src/hooks.ts:19-21setQueryDataschreibt die Mutation-Response direkt in den Cache. Da der Backend-Return unvollständig ist, werden die vorher sichtbaren Items überschrieben. Ein anschließendesgetToday()würde sie zwar wiederbringen (DB-State ist weiterhinplanned), aber das passiert nicht automatisch.Reproduktion
lifecycleState='planned'undplannedSlotim heutigen BereichGET /planner/today→ liefert den Eintrag (korrekt)POST /planner/run→ Response enthält den Eintrag nicht (Bug 1)Vorschlag Fix
Eine der drei Optionen — Architekt-Konsultation sinnvoll, weil es eine Planner-Semantik-Entscheidung ist:
a) Wipe-and-replan (semantisch klar, idempotent):
planTodaysetzt alleplanned-Items für den Tag zurück aufpending, bevor der Slotter läuft. Pool = pending ∪ ehemals-planned. Risiko: Items können den Slot wechseln zwischen zwei Runs.b) Merge (konservativ):
planTodayplant nurpending-Items und merged die bereitsplannedins Return-DTO. Risiko: Konflikte (zwei Items im selben Slot) müssen behandelt werden.c) No-op-für-planned (UI-only-Fix): Backend bleibt, Frontend macht
invalidateQueriesstattsetQueryData. Folge-Fetch zeigt die Wahrheit. Aber der Re-Plan-Effekt bleibt unsichtbar für planned-Items.Empfehlung Architektur: (a) — Replan soll deterministisch und vollständig sein. ADR 0001 §7 (Source-Done-Event-Bus) deutet ohnehin auf einen Lifecycle hin, in dem Items irgendwann von
plannedzudonewandern; bis dahin sind sie im Re-Plan-Pool.Acceptance
POST /planner/run: Response enthält alle für heute geplanten Items (neu eingeplante + ehemals geplante)planner.service.spec.tsdecken den Fall "planned-Candidate bleibt nach Re-Plan im Return" abArchitektur-Entscheidung: Option (a) Wipe-and-replan — mit Stabilitäts-Refinement
Begründung
POST /planner/runist im aktuellen Modell explizit ein User-Trigger ("Tag jetzt planen"), kein Hintergrund-Heuristik-Step. Die mentale Semantik aus User-Sicht ist "erzeuge mir den kanonischen Plan für heute aus dem aktuellen Pool" — und Pool heißt: alle nicht-erledigten Candidates, die heute landen sollen, unabhängig vom bisherigenlifecycleState. Option (b) Merge baut eine implizite "Pin"-Semantik ein, ohne dass es einen expliziten Pin-Mechanismus gibt — das versteckt User-Intent hinter einer Implementation-Coincidence. Option (c) lässt die DTO-Antwort fachlich falsch und kompensiert nur visuell — der Re-Plan-Aufruf wäre für planned-Items folgenlos, was den Button-Titel zur Lüge macht.Bezug ADR 0001 §7 (Source-Done-Event-Bus)
Der Lifecycle ist
pending → planned → done.doneist die einzige terminale, schutzwürdige Transition (über Event-Bus, idempotent).plannedist kein persistenter Commitment-Zustand, sondern eine Auswirkung des letzten Slotter-Laufs. Replan darfplannedaufheben,doneniemals.Implementation
planToday(), in derselben Transaktion:UPDATE candidates SET lifecycle_state = pending, planned_slot = NULL WHERE owner_sub = ? AND lifecycle_state = planned AND planned_slot BETWEEN dayStart AND dayEnd— nur heutige planned-Items zurückspulen, gestrige bleiben unangetastet (sind ohnehin Stale-Material für separate Stale-Logik).SELECT pendingzieht jetzt automatisch die ehemals planned-Items mit ein.slotPlanmuss für identischen Input deterministisch identischen Output liefern. Sortier-Tiebreaker im Slotter:(priority DESC, createdAt ASC, id ASC). Falls bereits so → gut. Falls nicht → mit-fixen.GET /planner/todayeinen Zwischenzustand (alles pending, nichts planned) sehen.doneund externe Mutationen bleiben unberührtlifecycle_state = donewerden vom Demote-Filter nicht erfasst → sie bleiben done.planned_slot < dayStartwerden vom Demote-Filter nicht erfasst → die existierende Stale-Cleanup-Logik (zeile 90-99) bleibt zuständig.Tests (planner.service.spec.ts)
planToday()im Return enthalten"planToday()-Calls mit identischem Pool liefern identische Slot-Zuordnung" (Stabilität)Frontend-Folge
Nachdem das Backend korrekt antwortet, kann
useRunPlanner.onSuccesssetQueryDatabehalten — die Response ist dann ja vollständig. AlternativinvalidateQueriesals Belt-and-Suspenders. Keine Architekturpräferenz, PM-Entscheidung.Out of scope — explizit nicht jetzt
— arch-bot