feat(planner): Planner v0 — pending Candidates auf heute (Phase 5 von #360) #368

Closed
opened 2026-05-20 22:34:07 +02:00 by admin-mrrm · 0 comments
Owner

Ziel

Phase 5 von Epic #360. Der Planner v0 ist die einfachste mögliche Heuristik: alle pending Candidates werden auf heute geplant, sortiert nach Priorität + earliestAt.

Ziel ist nicht schon ein realistischer Tagesplan — die schlauen Schichten (Location-Hints, Habit-Context, LLM) kommen später (Phasen 7/8). v0 macht eine Liste, mehr nicht. Damit ist die Pipeline durchgehend testbar: Source → Candidate → Planner → Day-View.

Scope

Service

PlannerService.planToday(ownerSub, now):

  1. Lade alle Candidates mit lifecycleState='pending' und ownerSub=….
  2. Überspringe alle mit latestAt < startOfDay(now) → markiere sie als obsolete.
  3. Sortiere die Übrigen nach priority (high>normal>low), dann earliestAt asc nulls last, dann createdAt asc.
  4. Setze plannedSlot = now (alle auf jetzt — v0 weiß noch nichts über Slots), lifecycleState='planned'.
  5. Liefere die Liste sortiert zurück.

PlannerService.getToday(ownerSub, now):

  • Liefere alle Candidates mit lifecycleState='planned' und plannedSlot im Range [startOfDay(now), endOfDay(now)], in Plan-Reihenfolge.

API

  • POST /planner/run — triggert planToday für den eingeloggten User. Body leer; Response = Day-View-DTO.
  • GET /planner/today — Day-View-DTO für heute.

DTO:

{
  date: 'YYYY-MM-DD',
  items: Array<{
    candidateId: string,
    source: 'mail'|'tracking'|'habit'|'manual'|'project'|'calendar',
    title: string,
    priority: 'low'|'normal'|'high',
    earliestAt: string | null,
    latestAt: string | null,
    estDurationMin: number | null,
    plannedSlot: string,
  }>
}

Tests

  • Unit PlannerService.planToday: leere Liste, sortierung, obsolete-Markierung, idempotenz (zweimal aufrufen markiert nichts neu, weil schon planned)
  • Unit PlannerService.getToday: nur planned-Candidates für heute
  • Integration End-to-End: 3 Candidates aus 3 Sourcen anlegen, POST /planner/run, GET /planner/today, prüfen Reihenfolge

Out of scope (folgt später)

  • Echte Slots (8:00, 10:00 …) → Phase 7 (Planner v1)
  • Calendar-Slots / Free-Busy-Berücksichtigung → Phase 6
  • Location-Hint-Reihenfolge → Phase 7
  • LLM-Assistenz → Phase 8 (#122)
  • Day-View-UI → eigenes Issue

Bezug

  • Epic: #360
  • Phase 1 (Schema): #361
  • Phase 2 (Tracking): #363
  • Phase 3 (Mail): #178
  • Phase 4 (Habits): #366
## Ziel Phase 5 von Epic #360. Der **Planner v0** ist die einfachste mögliche Heuristik: alle pending Candidates werden auf *heute* geplant, sortiert nach Priorität + `earliestAt`. Ziel ist **nicht** schon ein realistischer Tagesplan — die schlauen Schichten (Location-Hints, Habit-Context, LLM) kommen später (Phasen 7/8). v0 macht eine *Liste*, mehr nicht. Damit ist die Pipeline durchgehend testbar: Source → Candidate → Planner → Day-View. ## Scope ### Service `PlannerService.planToday(ownerSub, now)`: 1. Lade alle Candidates mit `lifecycleState='pending'` und `ownerSub=…`. 2. Überspringe alle mit `latestAt < startOfDay(now)` → markiere sie als `obsolete`. 3. Sortiere die Übrigen nach `priority (high>normal>low)`, dann `earliestAt asc nulls last`, dann `createdAt asc`. 4. Setze `plannedSlot = now` (alle auf jetzt — v0 weiß noch nichts über Slots), `lifecycleState='planned'`. 5. Liefere die Liste sortiert zurück. `PlannerService.getToday(ownerSub, now)`: - Liefere alle Candidates mit `lifecycleState='planned'` und `plannedSlot` im Range `[startOfDay(now), endOfDay(now)]`, in Plan-Reihenfolge. ### API - `POST /planner/run` — triggert `planToday` für den eingeloggten User. Body leer; Response = Day-View-DTO. - `GET /planner/today` — Day-View-DTO für heute. DTO: ```ts { date: 'YYYY-MM-DD', items: Array<{ candidateId: string, source: 'mail'|'tracking'|'habit'|'manual'|'project'|'calendar', title: string, priority: 'low'|'normal'|'high', earliestAt: string | null, latestAt: string | null, estDurationMin: number | null, plannedSlot: string, }> } ``` ### Tests - Unit `PlannerService.planToday`: leere Liste, sortierung, obsolete-Markierung, idempotenz (zweimal aufrufen markiert nichts neu, weil schon `planned`) - Unit `PlannerService.getToday`: nur planned-Candidates für heute - Integration End-to-End: 3 Candidates aus 3 Sourcen anlegen, `POST /planner/run`, `GET /planner/today`, prüfen Reihenfolge ## Out of scope (folgt später) - Echte Slots (8:00, 10:00 …) → Phase 7 (Planner v1) - Calendar-Slots / Free-Busy-Berücksichtigung → Phase 6 - Location-Hint-Reihenfolge → Phase 7 - LLM-Assistenz → Phase 8 (#122) - Day-View-UI → eigenes Issue ## Bezug - Epic: #360 - Phase 1 (Schema): #361 ✅ - Phase 2 (Tracking): #363 ✅ - Phase 3 (Mail): #178 ✅ - Phase 4 (Habits): #366 ✅
Sign in to join this conversation.
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#368
No description provided.