feat(planner): Planner v1 — echte Slots + Location-Cluster + Calendar-Aware (Phase 7 von #360) #372
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
admin-mrrm/mrrmlabapp#372
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?
Ziel
Phase 7 von Epic #360. Planner v1 ist die erste Version mit echten Slots: jedes geplante Candidate bekommt eine konkrete Uhrzeit, Kalender-Events werden als blocked-Slots respektiert, und Candidates mit dem gleichen Location-Hint werden zusammengeplant ("alle Termine bei Lidl in einem Block").
Vorgaenger: Planner v0 (#368) hat alles auf "jetzt" geplant — keine echten Slots. Diese Phase ersetzt diese Heuristik.
Scope
Algorithmus
PlannerService.planToday(ownerSub, now):latestAt < startOfDay(now)→obsolete.locationHints[].name(case-insensitive, erstes Element) landen im gleichen Bucket. Candidates ohne Location landen im__nolocation__-Bucket.min(createdAt)aufsteigend. Habit-Candidates anchoren ihren Bucket — fallen ueber Priority-Sort heraus, kein Sonderfall noetig.dayStart + 09:00. Pro Bucket in sortierter Reihenfolge:earliestAt asc nulls last, danncreatedAt asc.duration = estDurationMin ?? 30slotStart = max(cursor, earliestAt ?? workStart)[slotStart, slotStart+duration]→ cursor hinter das Event setzen, retry.slotEnd > workEnd (21:00)oderslotEnd > latestAt→ Candidate bleibtpending(Overflow), nicht initems.plannedSlot = slotStart,lifecycleState = planned. Cursor =slotEnd.Konstanten (v1 hartkodiert)
WORK_START_HOUR = 9(UTC)WORK_END_HOUR = 21(UTC)DEFAULT_SLOT_MIN = 30(Spaeter via env konfigurierbar — Out-of-scope.)
DTO-Anpassung
DayPlanItem.plannedSlotist jetzt der echte Slot (vorher: immernow). Format unveraendert: ISO-String.Tests
PlannerService.planTodayv1:earliestAtwird respektiert (Slot nicht davor)latestAt-Verletzung → Candidate bleibt pending (nicht in items)locationHints[0].namewerden konsekutiv platziertOut of scope (Phase 8+)
dependsOn)Bezug