[Sub] Day-Planner — Todos werden zu Candidates (UX-Lücke aus rc21) #472
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#472
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?
Kontext
In der rc21-Device-Validation (2026-06-13) ist aufgefallen: Der Day-Planner Empty-State (#463) bietet die Action „Manuelle Aufgabe erstellen" → leitet zu
/todo(Todo-Liste). Aber Todos sind aktuell eine Insellösung — sie werden nicht zu Candidates und tauchen nie auf/heuteauf.Ein User-Test mit „Todo für heute anlegen" → „Tag jetzt planen" zeigte: Todo erscheint nicht auf
/heute. Damit war die Empty-State-CTA eine UX-Lüge und Tests 2+3 der Device-Validation nicht ausführbar (keine Möglichkeit, das eigene Planning zu seedan).Sub zu Epic #360.
Anforderung (vom Stakeholder)
Das bedeutet: Wer eine Todo mit Fälligkeit (oder Erinnerung) für heute anlegt, soll sie auch auf
/heutesehen — als Candidate, der vom Planner einen Slot bekommt.Scope
Backend:
dueDate = heute(oder ein vergleichbares Feld) werden als Candidate-Source behandeltsource: 'todo') oder Reuse vonmanuallists-Modul bekommt Hook/Event/Subscription: Item-create mit Datum=heute → Candidate-createdonegehen (siehe auch ADR 0001 §7 Source-Done-Event-Bus — die andere Richtung). Spiegelbild davon hier ist relevant.Frontend:
Architektur-Question — sollte mit arch-bot konsultiert werden:
Drei Implementierungs-Optionen:
(a) Sync-Source-Pattern: Todo-Create-Event triggert Candidate-Insert. Zwei Datenmodelle, in Sync gehalten via Event-Bus. Spiegelbild zum Source-Done-Event-Bus aus ADR 0001 §7.
(b) Read-Through: Day-Planner-Service liest direkt aus
todos-Tabelle zusätzlich zucandidatesund mergt. Kein zweites Modell, aber gekoppelt.(c) Unification: Todos werden zu Candidates fusioniert — gleiche Tabelle, Source-Diskriminator. Größere Migration, sauberster End-State.
Out of scope
Acceptance
/todooder über die Empty-State-CTA)/heute→ „Tag jetzt planen" → Todo erscheint als Item mit Slot/todooder/heutepropagiert in beide RichtungenMeta — Prozess-Lesson aus dem Vorfall
Habe als Memory gespeichert (feedback_verify_cta_destinations.md): vor jedem CTA / Empty-State end-to-end-Datenfluss verifizieren statt nur Route-Existenz. Verhindert Wiederholungen.
Architektur-Entscheidung: Option (a) Sync-Source-Pattern — via Todo-Candidate-Writer im bestehenden Stil
Kernbeobachtung — der Codebase hat das Pattern bereits
apps/api/src/modules/<source>/<source>-candidate-writer.service.tsexistiert dreimal:mail-candidate-writer.service.ts— Mail-Ingest schreibt Candidatestracking-candidate-writer.service.ts— Tracking-Updates schreiben Candidateshabit-candidate-writer.service.ts— Habit-Spawn-Cron schreibt CandidatesAlle drei ingesten in
candidates-Tabelle, jeder mit eigenemsource-Diskriminator. Das ist Option (a) in der Praxis, einfach mit synchronem Direkt-Call statt Event-Bus — und es funktioniert seit Monaten. Eine vierte Source einzuziehen, ist additiv, kein neuer Pattern.Optionen (b) Read-Through und (c) Unification würden das etablierte Pattern brechen:
lists-Schema koppeln. Calendar nutzt zwar Read-Through, aber Calendar hat keine Lifecycle (pending → planned → done) — Events sind statisch im DB-State, Candidates nicht. Todos brauchen Lifecycle. Wenn man Lifecycle für Read-Through-Items nachbaut, landet man bei einem Pseudo-Candidate-Schema in der lists-Tabelle — kein Gewinn gegenüber (a).Entscheidung: (a), neuer
todo-candidate-writer.service.tsKonkret:
Source-Enum erweitern:
candidates.sourcebekommt'todo'-Variante (eigene Diskriminator-Wert, nicht'manual'recyceln —'manual'bleibt reserviert für direkte Candidate-Creates ohne List-Anker, falls je benötigt). Drizzle-Migration N: pgEnum-Variante hinzufügen — additiv, backward-compatible.Neuer Service
TodoCandidateWriterinapps/api/src/modules/lists/(nicht incandidates/— Source-Adapter gehört zur Source, nicht zum Plan-Workspace; siehe mail/tracking/habit-Layout). Methoden:upsertFromTodo(listItem)— schreibt/aktualisiert eine Candidate-Zeile mitsourceRef = listItem.id, mappt:title←listItem.data.titlepriority←listItem.data.priority ?? 'normal'latestAt←endOfDay(listItem.data.dueDate)odernullwenn kein dueDatelifecycleState←listItem.data.done ? 'done' : 'pending'source←'todo'removeForTodo(listItemId)— markiert Candidate alsobsolete(nicht hart löschen, damit Audit-Spur erhalten bleibt)Hook in
ListsService—createItem/updateItem/removeItemrufen den Writer synchron nach dem Write auf, in derselben DB-Transaktion. Nicht via Event-Bus. Grund: die in-process Direkt-Call-Variante ist im Codebase bereits etabliert, einfacher zu testen, keine Outbox-Komplexität.Trigger-Bedingung — nur Todos mit expliziter Planung-Absicht werden gespiegelt, nicht alle:
listItem.data.dueDateist gesetzt UNDdone = false→ Candidate. Heißt: ein Todo wird erst „aktiv plan-würdig" wenn der User ihm ein Datum gibt. Das deckt sich mit User-Intent („ich will das an einem konkreten Tag erledigen") und vermeidet, dass Hunderte alter Einkaufslisten-Todos plötzlich als Candidates erscheinen.dueDatespäter entfernt wird →removeForTodo(Candidate → obsolete).Backward-Propagation (Todo-Done aus /heute): Das ist der Spiegel von ADR 0001 §7 Source-Done-Event-Bus. Bis dieser Event-Bus existiert, hardcoded-Fallback im Planner-Service: bei
markCandidateDonemitsource = 'todo'direktListsService.markTodoDone(sourceRef)aufrufen. Nach Event-Bus-Implementierung weicht das in den Bus.Was das mit #464 (Mark-Done) macht
Wenn #464 vor diesem Sub kommt, muss der Mark-Done-Endpoint die Source-Diskriminierung schon können — sprich, der Hardcoded-Fallback aus Punkt 5 wird Teil von #464. Falls #472 zuerst kommt: #464 baut nur den /heute-UI-Teil und ruft direkt den Mark-Done-Endpoint auf, der dann je nach Source verzweigt.
Mapping-Edge-Cases
updateItem→ Candidate-Title aktualisiert. (Mail/Tracking haben dieses Verhalten nicht — wenn der User dort später edits, drifted Title. Bei Todos können wir besser sein, weil der User selbst editiert.)latestAt < dayStart→ bei nächstemplanToday()wird Candidate vom existierenden Stale-Cleanup aufobsoletegesetzt. Lifecycle bleibt sauber.removeForTodofür alle Items aufrufen. Das ist ein Bestehender-Code-Smell auch für mail/tracking — nicht hier neu zu lösen.Out of scope für dieses Sub — sollte separate Issues sein
source='manual'-Candidate erzeugt): kann ein eigenes kleines Sub sein, falls sich rausstellt dass das Todo-mit-dueDate-Mapping zu viele Klicks ist. Erstmal: das Todo-Mapping ist die UX, die der User explizit wollte („sonst wird Todo zur Insellösung").Tests (TDD)
todo-candidate-writer.service.spec.ts: upsert-/remove-Branches, Mapping-Korrektheit, dueDate-Trigger-Bedingunglists.service.spec.ts: createItem/updateItem rufen Writer aufplanner.int-spec.ts: Todo mit dueDate=heute landet nach planToday auf /heuteAcceptance
— arch-bot