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

Closed
opened 2026-05-20 22:11:50 +02:00 by admin-mrrm · 1 comment
Owner

Ziel

Phase 4 von Epic #360. Die Habit-Source erzeugt wiederkehrende Candidates aus Routinen (z.B. wöchentlicher Einkauf, monatliche Kontoabfrage, tägliches Lüften).

Habit ist die erste Source, die zeitgesteuert Candidates produziert — Mail/Tracking reagieren auf ein externes Ereignis, Habit auf "ein Termin in der Zukunft ist erreicht".

Datenmodell

Neue Tabelle habits:

  • id (uuid)
  • ownerSub
  • title (z.B. "Wocheneinkauf")
  • recurrenceDays (integer — alle N Tage; v0-MVP, später cron-like)
  • anchorDate (date — Ausgangspunkt der Folge)
  • nextDueAt (timestamp — wann der nächste Candidate fällig ist)
  • lastSpawnedAt (timestamp | null)
  • createdAt
  • completionPolicy (textmanual / time-elapsed, default manual)

Spawner-Logik

Service HabitCandidateWriter.spawnDueCandidates(now):

  1. Finde alle Habits mit nextDueAt <= now.
  2. Für jeden Habit: schreibe candidate mit
    • source = 'habit'
    • sourceRef = ${habitId}:${nextDueAt-YYYY-MM-DD} (Idempotenz: ein Candidate pro Habit pro Fälligkeitstag)
    • title = habit.title
    • completionPolicy = habit.completionPolicy
  3. Advance nextDueAt += recurrenceDays; setze lastSpawnedAt = now.

ON CONFLICT DO NOTHING auf (ownerSub, source, sourceRef) → mehrfacher Cron-Run am selben Tag erzeugt keinen Duplicate.

Cron-Integration

@Cron läuft (z.B. stündlich) und ruft spawnDueCandidates(new Date()).

API (v0, minimal)

  • POST /habits — neuen Habit anlegen
  • GET /habits — eigene Habits listen
  • PATCH /habits/:id — title / recurrenceDays / anchorDate ändern
  • DELETE /habits/:id

(Habit-UI separates Issue, hier nur Backend-Surface.)

Tests

  • Unit-Tests HabitCandidateWriter.spawnDueCandidates:
    • Spawnt nichts wenn nextDueAt > now
    • Spawnt einen Candidate wenn fällig + advanced nextDueAt
    • Idempotent — zweiter Run am selben Tag schreibt keinen Duplicate
    • Mehrere fällige Habits
  • Unit-Tests CRUD-Service
  • Integration-Test End-to-End: Habit anlegen, nextDueAt in der Vergangenheit setzen, Spawn auslösen, Candidate prüfen

Out of scope (für später)

  • Cron-Ausdrücke (komplexere Recurrence) — kommt mit Planner v1
  • Habit-UI im Web/Mobile (eigenes Issue)
  • Habit-Suggestions aus Verhaltens-Memory

Bezug

  • Epic: #360
  • Phase 1 (Schema): #361
  • Phase 2 (Tracking-Refactor): #363
  • Phase 3 (Mail): #178
## Ziel Phase 4 von Epic #360. Die **Habit-Source** erzeugt wiederkehrende Candidates aus Routinen (z.B. wöchentlicher Einkauf, monatliche Kontoabfrage, tägliches Lüften). Habit ist die erste Source, die *zeitgesteuert* Candidates produziert — Mail/Tracking reagieren auf ein externes Ereignis, Habit auf "ein Termin in der Zukunft ist erreicht". ## Datenmodell Neue Tabelle `habits`: - `id` (uuid) - `ownerSub` - `title` (z.B. "Wocheneinkauf") - `recurrenceDays` (`integer` — alle N Tage; v0-MVP, später cron-like) - `anchorDate` (`date` — Ausgangspunkt der Folge) - `nextDueAt` (`timestamp` — wann der nächste Candidate fällig ist) - `lastSpawnedAt` (`timestamp` | null) - `createdAt` - `completionPolicy` (`text` — `manual` / `time-elapsed`, default `manual`) ## Spawner-Logik Service `HabitCandidateWriter.spawnDueCandidates(now)`: 1. Finde alle Habits mit `nextDueAt <= now`. 2. Für jeden Habit: schreibe `candidate` mit - `source = 'habit'` - `sourceRef = ${habitId}:${nextDueAt-YYYY-MM-DD}` (Idempotenz: ein Candidate pro Habit pro Fälligkeitstag) - `title = habit.title` - `completionPolicy = habit.completionPolicy` 3. Advance `nextDueAt += recurrenceDays`; setze `lastSpawnedAt = now`. `ON CONFLICT DO NOTHING` auf `(ownerSub, source, sourceRef)` → mehrfacher Cron-Run am selben Tag erzeugt keinen Duplicate. ## Cron-Integration `@Cron` läuft (z.B. stündlich) und ruft `spawnDueCandidates(new Date())`. ## API (v0, minimal) - `POST /habits` — neuen Habit anlegen - `GET /habits` — eigene Habits listen - `PATCH /habits/:id` — title / recurrenceDays / anchorDate ändern - `DELETE /habits/:id` (Habit-UI separates Issue, hier nur Backend-Surface.) ## Tests - Unit-Tests `HabitCandidateWriter.spawnDueCandidates`: - Spawnt nichts wenn `nextDueAt > now` - Spawnt einen Candidate wenn fällig + advanced `nextDueAt` - Idempotent — zweiter Run am selben Tag schreibt keinen Duplicate - Mehrere fällige Habits - Unit-Tests CRUD-Service - Integration-Test End-to-End: Habit anlegen, `nextDueAt` in der Vergangenheit setzen, Spawn auslösen, Candidate prüfen ## Out of scope (für später) - Cron-Ausdrücke (komplexere Recurrence) — kommt mit Planner v1 - Habit-UI im Web/Mobile (eigenes Issue) - Habit-Suggestions aus Verhaltens-Memory ## Bezug - Epic: #360 - Phase 1 (Schema): #361 ✅ - Phase 2 (Tracking-Refactor): #363 ✅ - Phase 3 (Mail): #178 ✅
Collaborator

PM-Housekeeping (Convention-Falle): Fix wurde am 2026-05-20 via PR #367 (Merge 95312d2) auf main gemerged. Das Issue blieb offen weil der Merge-Title nur feat(#366): / fix(366): enthielt, nicht Closes #366.

Habit-Source (Phase 4 von #360) implementiert.

Schließe manuell als verifiziert-implementiert. Konvention ist dokumentiert in convention_commit_close_keywords.md; CI-Guard ist als arch-question #406 in Bearbeitung.

**PM-Housekeeping (Convention-Falle):** Fix wurde am 2026-05-20 via PR #367 (Merge `95312d2`) auf `main` gemerged. Das Issue blieb offen weil der Merge-Title nur `feat(#366):` / `fix(366):` enthielt, nicht `Closes #366`. Habit-Source (Phase 4 von #360) implementiert. Schließe manuell als verifiziert-implementiert. Konvention ist dokumentiert in `convention_commit_close_keywords.md`; CI-Guard ist als arch-question #406 in Bearbeitung.
Sign in to join this conversation.
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#366
No description provided.