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.
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.
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).
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 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:
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.
#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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
RecurringTypeenum;Ical.Netreferenced 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
CalendarEvent; range query expands series; recurrence editor in the calendar event modal; RRULE ICS export. ✅ implemented — backendspikersoft-backendPR + angularspikersoft-angularPR (linked below).AvailabilityProfilesub-document;ExpandWeeks/IntersectFreeWeeks; dedicatedScheduleprofile tab (deduce vs direct modes, presets, week grid); parent-manages-child. Threaded through every UpdateProfile layer..icsupload (school/Google) → events → availability;.icsexport.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/familiesbackend.Known follow-ups
Socialprofile-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
Progress — Phase 1 and Phase 2 are merged.
Phase 1 (recurrence engine) ✅
RecurrenceExpansionService(Ical.Net/RRULE), recurrence + availability fields onCalendarEvent, range query expands series. (Closed the calendar's no-recurrence gap.).icsexport.Phase 2 (availability projection) ✅
AvailabilityProfile+AvailabilityService(ExpandWeeks/IntersectFreeWeeks), threaded through every UpdateProfile layer, parent-manages-child endpoint.GET /api/profile/availability/weeks(derived grid).Scheduleprofile tab (profile?tab=schedule): week-start, deduce/direct mode, preset, 52-week paint-to-override grid.Remaining (keeping this epic open):
CalendarEvents from the preset + custom entries).PUT /api/profile/parental/availability/{childUserId}already exist)..icsimport (school/Google calendars → availability) + export.Separately surfaced during this work (worth their own tickets): the
Socialprofile-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).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 3 is done and the epic body doesn't reflect it.
CalendarController.cs:830—ImportIcs(IFormFile file, [FromForm] string? source = null)with aCalendarImportResultDto(:826) and rejection logging (:860). Backed by a real parser:SpikerSoft.Business.Scheduling/Calendar/IcsImportParser.cs, DI-registered atServiceCollectionExtensions.cs:1109-1111alongsideICalendarImportService. The parser handles the two things that actually matter for school/Google feeds — RRULE preserved as its serialised string (:21-23), and VEVENTs carrying aRECURRENCE-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
Socialnear-miss:AvailabilityProfilereachesProfileController,UpdateProfileCommand/Handler, and a dedicatedUpdateChildAvailabilityCommand/Handlerfor the parent-manages-child path.Phase 1's recurrence expansion is real:
RecurrenceExpansionService.cs:106mintsOccurrenceId = $"{seriesId}_{occ.Start.Ticks}"per occurrence, andCalendarController.cs:127-129surfaces it so FullCalendar sees distinct events.Two things keep this open:
Socialsilent-drop bug is still live.git grep -niE "\bSocial\b"acrossProfileController.csandUpdateProfileCommand*.csreturns 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.padBoundaryMonthsandunschedulableReasonare inschedule-tab.component.ts:137-170, the.unschedulableSCSS 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;
OccurrenceIdexists 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
createChildEventexists.Suggested: update the body to mark Phase 3 shipped, then close on #785's template binding + the
Socialmapping fix. Those are both small, and the epic's substance is delivered.