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

Closed
opened 2026-07-22 01:21:47 +00:00 by spikerj · 3 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


Child issues (per-repo, auto-close on merge)

No open child issues were produced by the migration — either this epic's
work was already complete, or its scope needs to be broken down into
per-repo issues before it can progress.

Checklist generated by the umbrella-tracker migration, 2026-08-07 — Opus 5 Agent

## 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) <!-- BEGIN MIGRATED-CHILDREN --> --- ## Child issues (per-repo, auto-close on merge) _No open child issues were produced by the migration — either this epic's work was already complete, or its scope needs to be broken down into per-repo issues before it can progress._ <sub>Checklist generated by the umbrella-tracker migration, 2026-08-07 — Opus 5 Agent</sub> <!-- END MIGRATED-CHILDREN -->
Author
Owner

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 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: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.

**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.
spikerj added the epic label 2026-08-07 13:43:02 +00:00
Author
Owner

Dissolved into per-repo issues as part of the umbrella-tracker breakup. This epic held
implementation work that could never auto-close from a merge; it now lives where the code is:

Backend — spikerj/spikersoft-backend

  • #619 — PUT /api/profile silently drops the social section. UpdateProfileRequest
    (ProfileController.cs:554-576) has no Social member, so the command initialiser (:77-100)
    never sets one and ApplySocial (UpdateProfileCommandHandler.cs:334-345) early-returns every
    time. This is the epic's known follow-up 2 — and the 2026-07-29 audit above is now half-stale:
    the command and handler do carry Social today; the controller still does not.
  • #620 — expose the availability engine beyond self. IntersectFreeWeeks
    (AvailabilityService.cs:68-101) is implemented and unit-tested but has no production caller —
    git grep outside the service finds only its tests and the DI line
    (ServiceCollectionExtensions.cs:1103). Trip scheduling, the epic's stated first consumer, cannot
    reach it. Also folds in the missing child-scoped grid read: the parental surface has a
    PUT parental/availability/{childUserId} (ProfileController.cs:422) and no matching GET.
  • #621 — per-occurrence reminders for recurring events. One delayed message per series
    (CalendarNotificationService.cs:90-115), and CancelEventReminderAsync (:127-143) is an
    explicitly-commented no-op. Epic follow-up 1, confirmed still unstarted.

Angular — spikerj/spikersoft-angular

  • #720 — commitment editor. Deduce mode derives the weekly grid from events flagged
    affectsAvailability, and no UI can set that flag: affectsAvailability,
    availabilityIntensity and timeBands appear only in calendar-api.models.ts:41-43,73-75 and in
    zero components or templates, while the backend round-trips all three
    (CalendarController.cs:478-480, 592-594). So deduce mode derives from an empty set for every
    user. Also covers the preset picker, which today writes a label and seeds nothing.
  • #721 — .ics import + subscription UI. Phase 3's server half is deployed and has no client:
    POST /api/calendar/import and the three /api/calendar/subscriptions routes have zero callers
    anywhere in projects/ or libraries/.
  • #722 — parent-manages-child availability selector. profileService.updateChildAvailability
    (profile.service.ts:150) exists and is called from nowhere; schedule-tab.component.ts has no
    child concept at all. Depends on #620 for the read side.
  • #723 — the #785 fix never reached the template. See below.

Already shipped, no issue filed:

  • Phase 1 — recurrence engine. Live. RecurrenceExpansionService.cs:106 mints
    OccurrenceId = $"{seriesId}_{occ.Start.Ticks}"; CalendarController.cs:127-129 surfaces it;
    GetCalendarEventsByDateRangeQueryHandler expands series on the range query. Angular ships the
    recurrence editor (event-modal.component.html:191-252, .ts:309-323) and RRULE .ics export
    (calendar-export.service.ts:27,75-88). Backend PR #448, angular PR #513.
  • Phase 2 — availability projection + Schedule tab. Live. AvailabilityProfile threaded through
    ProfileController → UpdateProfileCommand/Handler and the dedicated UpdateChildAvailability
    path; GET /api/profile/availability/weeks → 401 on api.spikersoft.com (deployed,
    auth-gated); the 52-week paint-to-override grid renders. Backend PRs #449/#450, angular PR #515.
  • Phase 3 — iCal import. Live, and the body above never reflected it. POST /api/calendar/import
    → 405 on GET (route present, POST-only), GET /api/calendar/subscriptions → 401. Real
    parser (SpikerSoft.Business.Scheduling/Calendar/IcsImportParser.cs), idempotent upsert on
    (CreatedBy, ExternalUid) (CalendarImportService.cs), plus subscription-feed refresh. Backend
    PRs #456/#458 (spikersoft-issues#802/#803). Only the frontend half was never built → #721.

A correction to the 2026-07-29 audit above, and a note for whoever tracks #785:

#785 was closed on angular PR #519 (c4d1e096), but the fix is still incomplete in exactly the way
that audit described. On spikersoft-angular@8e5a4048, padBoundaryMonths +
schedulable/unschedulableReason are in schedule-tab.component.ts:22-29,137-171, the
.week-cell.unschedulable rule is at schedule-tab.component.scss:182, both i18n keys are in
en.json:227-228 and es.json:573-574, and the spec covers the model at
schedule-tab.component.spec.ts:45-70 — but
grep "schedulable\|unschedulableReason" schedule-tab.component.html returns zero matches. The
cell at .html:68-79 binds only free/limited/busy/overridden. Placeholders are emitted with
level: "Busy", so they render as ordinary busy tiles at full opacity inside an undisabled
<button> with no tooltip; clicks are inert only because of a TS-side guard. The tests pass because
they assert on the model, never on the render. Filed as spikerj/spikersoft-angular#723.

Also verified, and worth flagging elsewhere: Ical.Net is not an unused dependency. It is
the live engine behind RecurrenceExpansionService and IcsImportParser. spikersoft-backend#548
currently lists it among unused NuGet references — removing it would break recurrence expansion and
.ics import.

The unit is tracked by the shared [Scheduling] title prefix and by sibling cross-links in each
issue. Closing here — the umbrella tracker is being emptied.

— Opus 5 Agent

Dissolved into per-repo issues as part of the umbrella-tracker breakup. This epic held implementation work that could never auto-close from a merge; it now lives where the code is: **Backend — `spikerj/spikersoft-backend`** - **#619** — `PUT /api/profile` silently drops the `social` section. `UpdateProfileRequest` (`ProfileController.cs:554-576`) has no `Social` member, so the command initialiser (`:77-100`) never sets one and `ApplySocial` (`UpdateProfileCommandHandler.cs:334-345`) early-returns every time. This is the epic's known follow-up 2 — and the 2026-07-29 audit above is now half-stale: the *command and handler* do carry `Social` today; the *controller* still does not. - **#620** — expose the availability engine beyond self. `IntersectFreeWeeks` (`AvailabilityService.cs:68-101`) is implemented and unit-tested but has **no production caller** — `git grep` outside the service finds only its tests and the DI line (`ServiceCollectionExtensions.cs:1103`). Trip scheduling, the epic's stated first consumer, cannot reach it. Also folds in the missing **child-scoped** grid read: the parental surface has a `PUT parental/availability/{childUserId}` (`ProfileController.cs:422`) and no matching GET. - **#621** — per-occurrence reminders for recurring events. One delayed message per *series* (`CalendarNotificationService.cs:90-115`), and `CancelEventReminderAsync` (`:127-143`) is an explicitly-commented no-op. Epic follow-up 1, confirmed still unstarted. **Angular — `spikerj/spikersoft-angular`** - **#720** — commitment editor. Deduce mode derives the weekly grid from events flagged `affectsAvailability`, and **no UI can set that flag**: `affectsAvailability`, `availabilityIntensity` and `timeBands` appear only in `calendar-api.models.ts:41-43,73-75` and in zero components or templates, while the backend round-trips all three (`CalendarController.cs:478-480, 592-594`). So deduce mode derives from an empty set for every user. Also covers the preset picker, which today writes a label and seeds nothing. - **#721** — `.ics` import + subscription UI. Phase 3's server half is deployed and has **no client**: `POST /api/calendar/import` and the three `/api/calendar/subscriptions` routes have zero callers anywhere in `projects/` or `libraries/`. - **#722** — parent-manages-child availability selector. `profileService.updateChildAvailability` (`profile.service.ts:150`) exists and is called from nowhere; `schedule-tab.component.ts` has no child concept at all. Depends on #620 for the read side. - **#723** — the #785 fix never reached the template. See below. **Already shipped, no issue filed:** - **Phase 1 — recurrence engine.** Live. `RecurrenceExpansionService.cs:106` mints `OccurrenceId = $"{seriesId}_{occ.Start.Ticks}"`; `CalendarController.cs:127-129` surfaces it; `GetCalendarEventsByDateRangeQueryHandler` expands series on the range query. Angular ships the recurrence editor (`event-modal.component.html:191-252`, `.ts:309-323`) and RRULE `.ics` export (`calendar-export.service.ts:27,75-88`). Backend PR #448, angular PR #513. - **Phase 2 — availability projection + Schedule tab.** Live. `AvailabilityProfile` threaded through `ProfileController` → `UpdateProfileCommand`/Handler and the dedicated `UpdateChildAvailability` path; `GET /api/profile/availability/weeks` → **401** on `api.spikersoft.com` (deployed, auth-gated); the 52-week paint-to-override grid renders. Backend PRs #449/#450, angular PR #515. - **Phase 3 — iCal import.** Live, and the body above never reflected it. `POST /api/calendar/import` → **405** on GET (route present, POST-only), `GET /api/calendar/subscriptions` → **401**. Real parser (`SpikerSoft.Business.Scheduling/Calendar/IcsImportParser.cs`), idempotent upsert on `(CreatedBy, ExternalUid)` (`CalendarImportService.cs`), plus subscription-feed refresh. Backend PRs #456/#458 (spikersoft-issues#802/#803). Only the **frontend** half was never built → #721. **A correction to the 2026-07-29 audit above, and a note for whoever tracks #785:** #785 was closed on angular PR #519 (`c4d1e096`), but the fix is still incomplete in exactly the way that audit described. On `spikersoft-angular@8e5a4048`, `padBoundaryMonths` + `schedulable`/`unschedulableReason` are in `schedule-tab.component.ts:22-29,137-171`, the `.week-cell.unschedulable` rule is at `schedule-tab.component.scss:182`, both i18n keys are in `en.json:227-228` **and** `es.json:573-574`, and the spec covers the model at `schedule-tab.component.spec.ts:45-70` — but `grep "schedulable\|unschedulableReason" schedule-tab.component.html` returns **zero matches**. The cell at `.html:68-79` binds only free/limited/busy/overridden. Placeholders are emitted with `level: "Busy"`, so they render as ordinary busy tiles at full opacity inside an undisabled `<button>` with no tooltip; clicks are inert only because of a TS-side guard. The tests pass because they assert on the model, never on the render. Filed as **spikerj/spikersoft-angular#723**. **Also verified, and worth flagging elsewhere:** `Ical.Net` is **not** an unused dependency. It is the live engine behind `RecurrenceExpansionService` and `IcsImportParser`. `spikersoft-backend#548` currently lists it among unused NuGet references — removing it would break recurrence expansion and `.ics` import. The unit is tracked by the shared `[Scheduling]` title prefix and by sibling cross-links in each issue. Closing here — the umbrella tracker is being emptied. — Opus 5 Agent
Sign in to join this conversation.