feat(planner): mark candidate done with todo-source bridge sync (#464) #479

Merged
pm-bot merged 2 commits from feat/candidate-mark-done into main 2026-06-15 09:08:39 +02:00
Collaborator

Summary

Erste Interaktion auf /heute (#464): Tap auf den Done-Button entfernt das Item optimistisch. Backend bekommt POST /candidates/:id/done mit symmetrischer Brücke zur Todo-Liste — schließt den Rückweg, den rc22 (#472) hingelegt hat.

What changed

  • API: CandidatesService.markDone läuft in db.transaction mit SELECT FOR UPDATE; bei source='todo' wird list_items.data.done im selben Tx via jsonb_set mitgeführt. Cross-tenant-Guard über JOIN auf lists.owner_sub (list_items hat keine eigene ownerSub-Spalte). Idempotent bei done, 409 bei obsolete, 404 sonst.
  • api-client: CandidatesResource.markDone mit zod-Response-Schema (Schema-Drift-Guard wie rc22).
  • feature-day-planner: useMarkCandidateDone — TanStack Query Optimistic Update mit Snapshot/Rollback/Invalidate. Tamagui ItemRow bekommt Done-Button mit accessibilityLabel='Erledigt: <title>'.
  • e2e: apps/web/e2e/heute-mark-done.spec.ts spiegelt heute-todo-flow.spec.ts — Item erscheint → Tap → verschwindet ≤5s.
  • ADR: docs/adr/0001-candidate-model.md §7 Addendum dokumentiert die Phase-1-Entscheidung (per-source inline statt Event-Bus, weil N=1).

Architektur-Entscheidung

Vor der Implementierung als arch-q #478 konsultiert. Verdict: Option (b) symmetrischer inline Writer, kein Event-Bus für N=1 backward-sync. ADR 0001 §7 wird re-interpretiert als Datenkonsistenz-Vertrag, nicht als Bus-Mandat. Sobald die zweite Source backward-sync braucht (vermutlich Tracking), wird SourceDoneHandler-Interface + Dispatcher extrahiert — bis dahin wäre der Bus reine Indirektion.

Test plan

  • API: candidate-mark-done.int-spec.ts — 5 Cases (todo+list_items lockstep, idempotent, obsolete→409, missing→404, non-todo source flippt nur Candidate)
  • API-Suite: 96 integration + 459 unit grün
  • api-client: 11 tests grün (inkl. Schema-Drift-Guard)
  • Typecheck: day-planner, web, mobile grün
  • Lint grün (api, api-client, feature-day-planner, web)
  • CI: Drone-Build grün
  • Device-Test post-merge an rc24-APK

Fixes #464

## Summary Erste Interaktion auf /heute (#464): Tap auf den Done-Button entfernt das Item optimistisch. Backend bekommt `POST /candidates/:id/done` mit symmetrischer Brücke zur Todo-Liste — schließt den Rückweg, den rc22 (#472) hingelegt hat. ## What changed - **API**: `CandidatesService.markDone` läuft in `db.transaction` mit `SELECT FOR UPDATE`; bei `source='todo'` wird `list_items.data.done` im selben Tx via `jsonb_set` mitgeführt. Cross-tenant-Guard über JOIN auf `lists.owner_sub` (list_items hat keine eigene ownerSub-Spalte). Idempotent bei `done`, 409 bei `obsolete`, 404 sonst. - **api-client**: `CandidatesResource.markDone` mit zod-Response-Schema (Schema-Drift-Guard wie rc22). - **feature-day-planner**: `useMarkCandidateDone` — TanStack Query Optimistic Update mit Snapshot/Rollback/Invalidate. Tamagui ItemRow bekommt Done-Button mit `accessibilityLabel='Erledigt: <title>'`. - **e2e**: `apps/web/e2e/heute-mark-done.spec.ts` spiegelt `heute-todo-flow.spec.ts` — Item erscheint → Tap → verschwindet ≤5s. - **ADR**: `docs/adr/0001-candidate-model.md` §7 Addendum dokumentiert die Phase-1-Entscheidung (per-source inline statt Event-Bus, weil N=1). ## Architektur-Entscheidung Vor der Implementierung als arch-q #478 konsultiert. Verdict: **Option (b) symmetrischer inline Writer**, kein Event-Bus für N=1 backward-sync. ADR 0001 §7 wird re-interpretiert als Datenkonsistenz-Vertrag, nicht als Bus-Mandat. Sobald die zweite Source backward-sync braucht (vermutlich Tracking), wird `SourceDoneHandler`-Interface + Dispatcher extrahiert — bis dahin wäre der Bus reine Indirektion. ## Test plan - [x] API: `candidate-mark-done.int-spec.ts` — 5 Cases (todo+list_items lockstep, idempotent, obsolete→409, missing→404, non-todo source flippt nur Candidate) - [x] API-Suite: 96 integration + 459 unit grün - [x] api-client: 11 tests grün (inkl. Schema-Drift-Guard) - [x] Typecheck: day-planner, web, mobile grün - [x] Lint grün (api, api-client, feature-day-planner, web) - [ ] CI: Drone-Build grün - [ ] Device-Test post-merge an rc24-APK Fixes #464
feat(planner): mark candidate done with todo-source bridge sync
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
4711904afd
POST /candidates/:id/done flips lifecycleState=done in a tx and — when
source='todo' — mirrors list_items.data.done in the same tx. Cross-tenant
guard via JOIN on lists.owner_sub (list_items has no own ownerSub column).
Idempotent on already-done; 409 on obsolete; 404 otherwise.

useMarkCandidateDone hook applies the TanStack Query optimistic-update
pattern (onMutate snapshot → onError rollback → onSettled invalidate).
ItemRow gets a tap-target with accessibilityLabel='Erledigt: <title>' for
the new Playwright regression apps/web/e2e/heute-mark-done.spec.ts.

ADR 0001 §7 addendum: until a second source needs backward-sync, we keep
the bridge writer inline per source rather than introducing the event bus
sketched in the original §7. Documented so phase-2 (Tracking) knows when
to extract a SourceDoneHandler dispatcher.

v0.6.6-rc24.

Fixes #464
fix(planner): narrow drizzle returning() to non-undefined
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
b37477c29c
CI typecheck flagged the destructured [updated] as possibly undefined.
Locally noUncheckedIndexedAccess behaves differently — added explicit
guard. The row is locked above, so this branch is unreachable in practice
but keeps tsc strict-mode happy.
pm-bot merged commit e8d5e5c565 into main 2026-06-15 09:08:39 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
admin-mrrm/mrrmlabapp!479
No description provided.