feat(#366): Habit-Source - wiederkehrende Candidates (Phase 4 von #360) #367

Merged
admin-mrrm merged 1 commit from feat/366-habit-source into main 2026-05-20 22:33:14 +02:00
Owner

Summary

Phase 4 von Epic #360 (Day-Planner Multi-Source-Todo).

Habits sind die erste zeitgetriebene Source — Mail/Tracking reagieren auf externe Ereignisse, Habits feuern wenn ein Termin in der Zukunft erreicht ist.

  • Neue Tabelle habits mit title, recurrenceDays, anchorDate, nextDueAt, lastSpawnedAt, completionPolicy (Migration 0019)
  • HabitsService mit Owner-Check für list/create/update/delete
  • HabitsControllerPOST/GET/PATCH/DELETE /habits
  • HabitCandidateWriterService.spawnDueCandidates(now):
    • Findet alle Habits mit nextDueAt <= now
    • Schreibt candidate mit source='habit', sourceRef=${habitId}:${YYYY-MM-DD} (Idempotenz pro Habit pro Fälligkeitstag)
    • ON CONFLICT DO NOTHING → mehrfacher Cron-Run am selben Tag erzeugt keinen Duplicate
    • Wenn der Insert ein No-op war: kein Advance, damit ein späterer Run die Lücke schließen kann
  • HabitSpawnerCron (stündlich, abschaltbar via HABIT_SPAWN_DISABLED=true, Cron-Expression über HABIT_SPAWN_CRON)

Tests

  • Unit: HabitsService (7 Tests), HabitCandidateWriterService (5 Tests)
  • Integration (5 Tests): CRUD-Endpoints, End-to-End-Spawn mit nextDueAt-Advance, Idempotenz

Out of scope (folgt später)

  • Habit-UI im Web/Mobile (eigenes Issue)
  • RRULE-fähige Recurrence (kommt mit Planner v1)
  • Habit-Suggestions aus Verhaltens-Memory

Bezug

  • Epic: #360
  • Phase 1 (Schema-Spike): #361
  • Phase 2 (Tracking-Refactor): #363
  • Phase 3 (Mail): #178
  • Phase 4 (Habits): dieser PR#366
  • Nächste Phase: Planner v0

Test plan

  • pnpm typecheck / pnpm lint / pnpm test (387 Tests) / pnpm test:integration (69 Tests) grün
  • Migration generiert + Snapshot committed
  • Habit-Cron via HABIT_SPAWN_DISABLED=true in Tests deaktivierbar

🤖 Generated with Claude Code

## Summary Phase 4 von Epic #360 (Day-Planner Multi-Source-Todo). Habits sind die erste *zeitgetriebene* Source — Mail/Tracking reagieren auf externe Ereignisse, Habits feuern wenn ein Termin in der Zukunft erreicht ist. - Neue Tabelle `habits` mit `title`, `recurrenceDays`, `anchorDate`, `nextDueAt`, `lastSpawnedAt`, `completionPolicy` (Migration `0019`) - `HabitsService` mit Owner-Check für list/create/update/delete - `HabitsController` — `POST/GET/PATCH/DELETE /habits` - `HabitCandidateWriterService.spawnDueCandidates(now)`: - Findet alle Habits mit `nextDueAt <= now` - Schreibt `candidate` mit `source='habit'`, `sourceRef=${habitId}:${YYYY-MM-DD}` (Idempotenz pro Habit pro Fälligkeitstag) - `ON CONFLICT DO NOTHING` → mehrfacher Cron-Run am selben Tag erzeugt keinen Duplicate - Wenn der Insert ein No-op war: kein Advance, damit ein späterer Run die Lücke schließen kann - `HabitSpawnerCron` (stündlich, abschaltbar via `HABIT_SPAWN_DISABLED=true`, Cron-Expression über `HABIT_SPAWN_CRON`) ## Tests - Unit: `HabitsService` (7 Tests), `HabitCandidateWriterService` (5 Tests) - Integration (5 Tests): CRUD-Endpoints, End-to-End-Spawn mit `nextDueAt`-Advance, Idempotenz ## Out of scope (folgt später) - Habit-UI im Web/Mobile (eigenes Issue) - RRULE-fähige Recurrence (kommt mit Planner v1) - Habit-Suggestions aus Verhaltens-Memory ## Bezug - Epic: #360 - Phase 1 (Schema-Spike): #361 ✅ - Phase 2 (Tracking-Refactor): #363 ✅ - Phase 3 (Mail): #178 ✅ - Phase 4 (Habits): **dieser PR** → #366 - Nächste Phase: Planner v0 ## Test plan - [x] `pnpm typecheck` / `pnpm lint` / `pnpm test` (387 Tests) / `pnpm test:integration` (69 Tests) grün - [x] Migration generiert + Snapshot committed - [x] Habit-Cron via `HABIT_SPAWN_DISABLED=true` in Tests deaktivierbar 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(#366): Habit-Source - wiederkehrende Candidates (Phase 4 von #360)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2d37639f80
Habits sind die erste zeitgetriebene Source: ein HabitSpawner-Cron findet
faellige Habits (nextDueAt <= now) und schreibt einen Candidate pro Habit
pro Faelligkeitstag. sourceRef = `${habitId}:${YYYY-MM-DD}` macht das
idempotent gegen mehrfache Cron-Runs am selben Tag; nach erfolgreichem
Insert wird nextDueAt um recurrenceDays vorgerueckt.

- Neue Tabelle habits (Migration 0019): title, recurrenceDays,
  anchorDate, nextDueAt, lastSpawnedAt, completionPolicy
- HabitsService mit list/create/update/delete (Owner-Check)
- HabitsController (POST/GET/PATCH/DELETE /habits)
- HabitCandidateWriterService.spawnDueCandidates(now)
- HabitSpawnerCron mit HABIT_SPAWN_CRON / HABIT_SPAWN_DISABLED env-flags
- 12 Unit-Tests, 5 Integration-Tests (End-to-End-Spawn + Idempotenz)

Out of scope: Habit-UI, RRULE-faehige Recurrence, Suggestions aus
Verhaltens-Memory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!367
No description provided.