Epic: Unified scheduling — recurrence engine + availability (calendar + iCal) #781

Open
opened 2026-07-22 01:21:47 +00:00 by spikerj · 2 comments
Owner

Goal

Make availability, the existing /calendar, and iCal one integrated scheduling system. "When is this person/kid free" is a general platform primitive — first consumer is trip scheduling (find weeks everyone's free → realistic date-anchored budgets), next is planning classes/lessons around a child's public-school calendar and existing sports.

This also fixes a real gap: the calendar had no recurrence (dead RecurringType enum; Ical.Net referenced but unused; ICS export wrote single VEVENTs).

Architecture

One Ical.Net/RRULE recurrence engine → expands calendar events → availability projection (weekly Free/Limited/Busy) → IntersectFreeWeeks (trip scheduling) + day+time fit (class scheduling). Commitments (school/sports/work) are recurring calendar events flagged availability-affecting.

Phases

  • Phase 1 — Recurrence engine (foundation). Ical.Net expansion service; RRULE fields on CalendarEvent; range query expands series; recurrence editor in the calendar event modal; RRULE ICS export. implemented — backend spikersoft-backend PR + angular spikersoft-angular PR (linked below).
  • Phase 2 — Availability projection + Schedule tab. AvailabilityProfile sub-document; ExpandWeeks/IntersectFreeWeeks; dedicated Schedule profile tab (deduce vs direct modes, presets, week grid); parent-manages-child. Threaded through every UpdateProfile layer.
  • Phase 3 — iCal import / external calendars. .ics upload (school/Google) → events → availability; .ics export.

Confirmed decisions

Week-level ratings Free/Limited/Busy; user-chosen week-start normalized to dates; two input modes (deduce from commitments / enter free weeks directly); commitments carry day-of-week + morning/afternoon/evening bands; dedicated Schedule tab; parents edit children's; unified Ical.Net engine. Not touching sponsor cards; not building the missing /api/sponsor/families backend.

Known follow-ups

  • Per-occurrence reminders for recurring events (current delayed-reminder pipeline can't cancel / schedules one delay).
  • Latent Social profile-section silent-drop bug (Angular saves {social} the controller never maps) — Phase 2 threads availability through every layer to avoid the same class.

🤖 Generated with Claude Code

## Goal Make availability, the existing `/calendar`, and iCal one integrated scheduling system. "When is this person/kid free" is a general platform primitive — first consumer is **trip scheduling** (find weeks everyone's free → realistic date-anchored budgets), next is **planning classes/lessons around a child's public-school calendar and existing sports**. This also fixes a real gap: the calendar had **no recurrence** (dead `RecurringType` enum; `Ical.Net` referenced but unused; ICS export wrote single VEVENTs). ## Architecture One `Ical.Net`/RRULE recurrence engine → expands calendar events → availability projection (weekly Free/Limited/Busy) → `IntersectFreeWeeks` (trip scheduling) + day+time fit (class scheduling). Commitments (school/sports/work) are recurring calendar events flagged availability-affecting. ## Phases - **Phase 1 — Recurrence engine (foundation).** Ical.Net expansion service; RRULE fields on `CalendarEvent`; range query expands series; recurrence editor in the calendar event modal; RRULE ICS export. ✅ implemented — backend `spikersoft-backend` PR + angular `spikersoft-angular` PR (linked below). - **Phase 2 — Availability projection + Schedule tab.** `AvailabilityProfile` sub-document; `ExpandWeeks`/`IntersectFreeWeeks`; dedicated `Schedule` profile tab (deduce vs direct modes, presets, week grid); parent-manages-child. Threaded through every UpdateProfile layer. - **Phase 3 — iCal import / external calendars.** `.ics` upload (school/Google) → events → availability; `.ics` export. ## Confirmed decisions Week-level ratings Free/Limited/Busy; user-chosen week-start normalized to dates; two input modes (deduce from commitments / enter free weeks directly); commitments carry day-of-week + morning/afternoon/evening bands; dedicated Schedule tab; parents edit children's; unified Ical.Net engine. Not touching sponsor cards; not building the missing `/api/sponsor/families` backend. ## Known follow-ups - Per-occurrence **reminders** for recurring events (current delayed-reminder pipeline can't cancel / schedules one delay). - Latent `Social` profile-section silent-drop bug (Angular saves `{social}` the controller never maps) — Phase 2 threads availability through every layer to avoid the same class. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Progress — Phase 1 and Phase 2 are merged.

Phase 1 (recurrence engine)

  • spikersoft-backend #448RecurrenceExpansionService (Ical.Net/RRULE), recurrence + availability fields on CalendarEvent, range query expands series. (Closed the calendar's no-recurrence gap.)
  • spikersoft-angular #513 — recurrence editor in the event modal, series-targeted edit/delete, RRULE .ics export.

Phase 2 (availability projection)

  • spikersoft-backend #449AvailabilityProfile + AvailabilityService (ExpandWeeks/IntersectFreeWeeks), threaded through every UpdateProfile layer, parent-manages-child endpoint.
  • spikersoft-backend #450GET /api/profile/availability/weeks (derived grid).
  • spikersoft-angular #515 — the Schedule profile tab (profile?tab=schedule): week-start, deduce/direct mode, preset, 52-week paint-to-override grid.

Remaining (keeping this epic open):

  • Phase 2 follow-up: commitment editor for deduce mode (create availability-affecting recurring CalendarEvents from the preset + custom entries).
  • Phase 2 follow-up: parent-manages-child UI selector (service method + PUT /api/profile/parental/availability/{childUserId} already exist).
  • Phase 3 — iCal .ics import (school/Google calendars → availability) + export.

Separately surfaced during this work (worth their own tickets): the Social profile-section silent-drop bug (Angular PUTs {social} the controller never maps), and per-occurrence reminders for recurring calendar events (the delayed-reminder pipeline can't cancel / schedules one delay).

Progress — **Phase 1 and Phase 2 are merged.** **Phase 1 (recurrence engine)** ✅ - spikersoft-backend **#448** — `RecurrenceExpansionService` (Ical.Net/RRULE), recurrence + availability fields on `CalendarEvent`, range query expands series. (Closed the calendar's no-recurrence gap.) - spikersoft-angular **#513** — recurrence editor in the event modal, series-targeted edit/delete, RRULE `.ics` export. **Phase 2 (availability projection)** ✅ - spikersoft-backend **#449** — `AvailabilityProfile` + `AvailabilityService` (`ExpandWeeks`/`IntersectFreeWeeks`), threaded through every UpdateProfile layer, parent-manages-child endpoint. - spikersoft-backend **#450** — `GET /api/profile/availability/weeks` (derived grid). - spikersoft-angular **#515** — the `Schedule` profile tab (`profile?tab=schedule`): week-start, deduce/direct mode, preset, 52-week paint-to-override grid. **Remaining (keeping this epic open):** - Phase 2 follow-up: **commitment editor** for deduce mode (create availability-affecting recurring `CalendarEvent`s from the preset + custom entries). - Phase 2 follow-up: **parent-manages-child** UI selector (service method + `PUT /api/profile/parental/availability/{childUserId}` already exist). - **Phase 3** — iCal `.ics` import (school/Google calendars → availability) + export. Separately surfaced during this work (worth their own tickets): the `Social` profile-section **silent-drop** bug (Angular PUTs `{social}` the controller never maps), and **per-occurrence reminders** for recurring calendar events (the delayed-reminder pipeline can't cancel / schedules one delay).
Author
Owner

Epic status roll-up — audited all three phases plus both known follow-ups against origin/master. All three phases have shipped, including Phase 3, which the body still lists as unstarted.

Phase State
1 — Recurrence engine shipped
2 — Availability projection + Schedule tab shipped (one UI gap, #785)
3 — iCal import / export shipped — body is stale

Phase 3 is done and the epic body doesn't reflect it. CalendarController.cs:830ImportIcs(IFormFile file, [FromForm] string? source = null) with a CalendarImportResultDto (:826) and rejection logging (:860). Backed by a real parser: SpikerSoft.Business.Scheduling/Calendar/IcsImportParser.cs, DI-registered at ServiceCollectionExtensions.cs:1109-1111 alongside ICalendarImportService. The parser handles the two things that actually matter for school/Google feeds — RRULE preserved as its serialised string (:21-23), and VEVENTs carrying a RECURRENCE-ID (a moved single occurrence of a series) detected and skipped rather than silently duplicating the series (:24, :90), with counters for both (:39-42).

Phase 2 is threaded through properly, which was the stated design goal after the Social near-miss: AvailabilityProfile reaches ProfileController, UpdateProfileCommand/Handler, and a dedicated UpdateChildAvailabilityCommand/Handler for the parent-manages-child path.

Phase 1's recurrence expansion is real: RecurrenceExpansionService.cs:106 mints OccurrenceId = $"{seriesId}_{occ.Start.Ticks}" per occurrence, and CalendarController.cs:127-129 surfaces it so FullCalendar sees distinct events.

Two things keep this open:

  1. Follow-up 2 — the Social silent-drop bug is still live. git grep -niE "\bSocial\b" across ProfileController.cs and UpdateProfileCommand*.cs returns nothing. Angular still saves a {social} section the controller never maps, so it's silently discarded. The epic explicitly cites this as the failure class Phase 2 was designed to avoid — and Phase 2 succeeded at that, but the original instance was never fixed.
  2. #785 — the Schedule tab's boundary-month placeholders exist in the model but the template never binds them. padBoundaryMonths and unschedulableReason are in schedule-tab.component.ts:137-170, the .unschedulable SCSS rule and both i18n keys exist — and zero templates reference any of them. Placeholders render as ordinary interactive tiles. (Details on that ticket.)

Follow-up 1 — per-occurrence reminders — is not done, as expected; OccurrenceId exists on the read path but nothing in the reminder pipeline consumes it. Still correctly scoped as a follow-up rather than a phase.

Also worth noting for scope: #790 (create-for-child from the parent calendar) is filed as a follow-up to #788 and is entirely unstarted — neither the backend endpoint nor createChildEvent exists.

Suggested: update the body to mark Phase 3 shipped, then close on #785's template binding + the Social mapping fix. Those are both small, and the epic's substance is delivered.

**Epic status roll-up** — audited all three phases plus both known follow-ups against `origin/master`. **All three phases have shipped**, including Phase 3, which the body still lists as unstarted. | Phase | State | |---|---| | 1 — Recurrence engine | ✅ shipped | | 2 — Availability projection + Schedule tab | ✅ shipped (one UI gap, #785) | | 3 — iCal import / export | ✅ **shipped — body is stale** | **Phase 3 is done and the epic body doesn't reflect it.** `CalendarController.cs:830` — `ImportIcs(IFormFile file, [FromForm] string? source = null)` with a `CalendarImportResultDto` (`:826`) and rejection logging (`:860`). Backed by a real parser: `SpikerSoft.Business.Scheduling/Calendar/IcsImportParser.cs`, DI-registered at `ServiceCollectionExtensions.cs:1109-1111` alongside `ICalendarImportService`. The parser handles the two things that actually matter for school/Google feeds — RRULE preserved as its serialised string (`:21-23`), and VEVENTs carrying a `RECURRENCE-ID` (a moved single occurrence of a series) detected and skipped rather than silently duplicating the series (`:24`, `:90`), with counters for both (`:39-42`). **Phase 2 is threaded through properly**, which was the stated design goal after the `Social` near-miss: `AvailabilityProfile` reaches `ProfileController`, `UpdateProfileCommand`/`Handler`, and a dedicated `UpdateChildAvailabilityCommand`/`Handler` for the parent-manages-child path. **Phase 1's recurrence expansion is real**: `RecurrenceExpansionService.cs:106` mints `OccurrenceId = $"{seriesId}_{occ.Start.Ticks}"` per occurrence, and `CalendarController.cs:127-129` surfaces it so FullCalendar sees distinct events. **Two things keep this open:** 1. **Follow-up 2 — the `Social` silent-drop bug is still live.** `git grep -niE "\bSocial\b"` across `ProfileController.cs` and `UpdateProfileCommand*.cs` returns **nothing**. Angular still saves a `{social}` section the controller never maps, so it's silently discarded. The epic explicitly cites this as the failure class Phase 2 was designed to avoid — and Phase 2 succeeded at that, but the original instance was never fixed. 2. **#785 — the Schedule tab's boundary-month placeholders exist in the model but the template never binds them.** `padBoundaryMonths` and `unschedulableReason` are in `schedule-tab.component.ts:137-170`, the `.unschedulable` SCSS rule and both i18n keys exist — and zero templates reference any of them. Placeholders render as ordinary interactive tiles. (Details on that ticket.) **Follow-up 1 — per-occurrence reminders — is not done**, as expected; `OccurrenceId` exists on the read path but nothing in the reminder pipeline consumes it. Still correctly scoped as a follow-up rather than a phase. **Also worth noting for scope:** **#790** (create-for-child from the parent calendar) is filed as a follow-up to #788 and is entirely unstarted — neither the backend endpoint nor `createChildEvent` exists. **Suggested:** update the body to mark Phase 3 shipped, then close on #785's template binding + the `Social` mapping fix. Those are both small, and the epic's substance is delivered.
Sign in to join this conversation.