---
phase: 06-ux-polish
plan: 06
type: execute
wave: 2
depends_on: ['06-01', '06-02', '06-03', '06-05']
files_modified:
  - apps/pwa/src/components/EventForm.tsx
  - apps/pwa/src/components/SeriesEditPrompt.tsx
  - apps/pwa/src/styles/index.css
autonomous: false
requirements: []
must_haves:
  truths:
    - 'Scope fence (D-01/D-02): this phase delivers only the six promoted polish items (999.2/3/6/7/8/9); 999.4 reminders/VALARM, 999.5 provider setup, and 999.1 provider abstraction are NOT built here (deferred to milestone 1.1)'
    - "Moving an event's start moves its end with it, preserving duration; the end never strands behind the start (D-03/D-04, success criterion 2)"
    - "A recurring event can be bounded in the form via 'Ends: Never / On date / After N times' (D-06, success criterion 2)"
    - "Editing a recurring occurrence prompts 'Edit recurring series' before saving the whole-series change (D-08/D-09, success criterion 3)"
    - 'The all-day-edit off-by-one stays fixed — re-editing an all-day event does not grow it by a day (D-05 verify, success criterion 2)'
    - 'All-day events are visually distinct from timed events at a glance (999.6/D-12, success criterion 1)'
  artifacts:
    - path: 'apps/pwa/src/components/EventForm.tsx'
      provides: 'start onChange handlers that call computeNewTimedEnd/computeNewAllDayEnd; recurrence-bound control; hasRrule-gated series-edit confirmation'
      contains: 'computeNewTimedEnd'
    - path: 'apps/pwa/src/components/SeriesEditPrompt.tsx'
      provides: 'whole-series edit confirmation sheet/dialog (focus trap, Escape=cancel)'
      contains: 'Update series'
    - path: 'apps/pwa/src/styles/index.css'
      provides: 'Schedule-X all-day chip override (full-width filled pill)'
      contains: 'sx__all-day-event'
  key_links:
    - from: 'apps/pwa/src/components/EventForm.tsx'
      to: 'apps/pwa/src/lib/eventDateTime.ts'
      via: 'start onChange → computeNewTimedEnd / computeNewAllDayEnd'
      pattern: 'computeNewTimedEnd|computeNewAllDayEnd'
    - from: 'apps/pwa/src/components/EventForm.tsx'
      to: 'apps/pwa/src/api/client.ts'
      via: 'payload carries recurrenceUntil/recurrenceCount; occurrence.hasRrule gates the prompt'
      pattern: 'recurrenceUntil|recurrenceCount|hasRrule'
---

<objective>
Wire the prepared logic into the event form so the user-facing 999.7/999.8/999.9 fixes are live, and give all-day events their distinct look (999.6). After this plan a real user can: move a start and watch the end follow (D-04), bound a recurring series with "Ends: On date / After N times" (D-06), edit a recurring series behind a confirming prompt (D-08/D-09), and tell all-day from timed events at a glance (999.6) — with the all-day-edit off-by-one staying fixed (D-05).

This is the Wave-2 integration slice. It consumes (does NOT redefine) artifacts from earlier plans: `computeNewTimedEnd`/`computeNewAllDayEnd` (Plan 01), the `recurrenceUntil`/`recurrenceCount` payload fields + `hasRrule` on `CalendarOccurrence` (Plan 05 client types, backed by Plan 02 API contract + Plan 03 server expansion).

Purpose: This is glue + UI (form wiring, a confirmation component, a CSS override) — standard tasks, verified with playwright-cli per CLAUDE.md. The deterministic math/serialization it depends on is already unit-tested in Plans 01–03.
Output: EventForm end-tracking handlers + recurrence-bound control + series-edit prompt trigger; `SeriesEditPrompt.tsx`; all-day Schedule-X override in `index.css`.
</objective>

<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-ux-polish/06-RESEARCH.md
@.planning/phases/06-ux-polish/06-PATTERNS.md
@.planning/phases/06-ux-polish/06-UI-SPEC.md
@apps/pwa/src/components/DeleteConfirmationDialog.tsx
@.planning/phases/06-ux-polish/06-01-SUMMARY.md
@.planning/phases/06-ux-polish/06-05-SUMMARY.md
</context>

<artifacts_this_plan_produces>
NEW symbols introduced here (exclude from drift/convergence checks):

- Start `onChange` handlers in `EventForm.tsx` that call `computeNewTimedEnd`/`computeNewAllDayEnd`
- `recurrenceBound: 'never'|'until'|'count'`, `recurrenceUntil: string`, `recurrenceCount: number` state + the "Ends" control in `EventForm.tsx`
- `SeriesEditPrompt` component (`apps/pwa/src/components/SeriesEditPrompt.tsx`)
- `.sx__all-day-event` CSS override block in `apps/pwa/src/styles/index.css`
  NOTE: `computeNewTimedEnd`, `computeNewAllDayEnd`, `recurrenceUntil`/`recurrenceCount` payload fields, and `hasRrule` are NOT new here — they are consumed from Plans 01/05.
  </artifacts_this_plan_produces>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: End-tracking wiring + recurrence-bound control in EventForm (D-04, D-06, D-07, D-05 verify)</name>
  <files>apps/pwa/src/components/EventForm.tsx, apps/pwa/src/components/EventForm.test.tsx</files>
  <read_first>
    - apps/pwa/src/components/EventForm.tsx — start date input `onChange` (~line 671) and start time input `onChange` (~line 684); state block (199–210); the reset `useEffect` (~232–262); the submit handler payload construction (~355–370, the `...(isEdit ? {} : { recurrence })` pattern); the recurrence `<select>` (~768–772)
    - apps/pwa/src/lib/eventDateTime.ts — `computeNewTimedEnd` / `computeNewAllDayEnd` signatures (from Plan 01) and the `exclusiveEndToInclusiveDate` D-05 helper at line ~199 (verify it still pre-fills inclusive on all-day edit)
    - apps/pwa/src/api/client.ts — `CreateEventPayload.recurrenceUntil`/`recurrenceCount` (from Plan 05) — the payload fields to send
    - .planning/phases/06-ux-polish/06-PATTERNS.md §"apps/pwa/src/components/EventForm.tsx" — exact onChange replacement, new state additions, reset-effect extension, payload extension
    - .planning/phases/06-ux-polish/06-UI-SPEC.md §"Surface 4" (end-tracking behavior) + §"Surface 5" (bound control: label "Ends", options Never/On date/After N times, 44px targets, inline validation copy) + §"Copywriting Contract" (exact labels/errors)
  </read_first>
  <behavior>
    - Changing startDate (timed) updates endDate/endTime so the duration is preserved (delegates to computeNewTimedEnd); never lands end before start.
    - Changing startDate (all-day) updates endDate preserving the day-span (computeNewAllDayEnd).
    - Changing startTime (timed) recomputes end preserving the delta.
    - Selecting recurrence ≠ "None" reveals the "Ends" control; "On date" reveals a date input, "After N times" reveals a number input (min 1); "Never" sends neither bound field.
    - Submitting with bound="until" sends `recurrenceUntil`; bound="count" sends `recurrenceCount`; neither when recurrence==='none' or bound==='never'.
    - Inline validation: count < 1 → "Must be at least 1 occurrence"; until before start → "End date must be after the event starts".
    - D-05 regression: opening an existing all-day event pre-fills the inclusive end (no +1 drift); saving twice does not grow the event.
  </behavior>
  <action>
    Replace the bare start date/time `onChange` handlers with handlers that call `computeNewAllDayEnd` (all-day) or `computeNewTimedEnd` (timed) to recompute end, then set start — per PATTERNS §EventForm. Add `recurrenceBound`/`recurrenceUntil`/`recurrenceCount` state alongside the existing state block; extend the reset `useEffect` to reset them to defaults (mirror the `setRecurrence(... ?? 'none')` line). Render the "Ends" control below the recurrence `<select>`, shown only when `recurrence !== 'none'`, using the exact UI-SPEC Surface 5 labels/options and 44px touch targets, with inline `--color-destructive` validation messages. Extend the submit payload to conditionally include `recurrenceUntil` (bound==='until') or `recurrenceCount` (bound==='count') only when `recurrence !== 'none'` — mirror the existing spread-conditional pattern. Do NOT re-implement the D-05 `exclusiveEndToInclusiveDate` pre-fill — leave line ~199 intact and add/keep a test asserting the all-day edit round-trip does not drift. For D-07, confirm the recurrence `<select>` value flows 1:1 to `recurrence` in the payload (the API mapping is locked in Plan 02). Add/extend EventForm.test.tsx cases for end-tracking wiring and bound-field emission where feasible in jsdom. Commit `feat(06-06): wire end-tracking and recurrence-bound control into EventForm`.
  </action>
  <verify>
    <automated>cd apps/pwa && pnpm test -- run components/EventForm</automated>
  </verify>
  <acceptance_criteria>
    - Start `onChange` handlers call `computeNewTimedEnd`/`computeNewAllDayEnd` (grep present in EventForm.tsx).
    - The "Ends" control renders only when recurrence ≠ none with the exact UI-SPEC labels and emits `recurrenceUntil`/`recurrenceCount` correctly (and neither when "Never").
    - Inline validation messages use the exact UI-SPEC copy.
    - `exclusiveEndToInclusiveDate` pre-fill at ~line 199 is unchanged; an all-day edit round-trip test shows no day drift (D-05 holds).
    - EventForm test suite green.
  </acceptance_criteria>
  <done>End auto-tracks start with a floor; recurrence is boundable in the form; all-day edit stays drift-free; D-07 mapping confirmed 1:1.</done>
</task>

<task type="auto">
  <name>Task 2: Series-edit confirmation prompt, gated on hasRrule (D-08, D-09)</name>
  <files>apps/pwa/src/components/SeriesEditPrompt.tsx, apps/pwa/src/components/EventForm.tsx</files>
  <read_first>
    - apps/pwa/src/components/DeleteConfirmationDialog.tsx — the existing bottom-sheet(phone)/dialog(desktop) pattern, focus trap, Escape-to-cancel, role="dialog"/aria-modal — the exact analog to mirror
    - apps/pwa/src/components/EventForm.tsx — the submit handler (where Save fires) + how `occurrence` is available; the edit-mode branch (`isEdit`)
    - apps/pwa/src/api/client.ts — `CalendarOccurrence.hasRrule` (from Plan 05) — the gate signal
    - .planning/phases/06-ux-polish/06-UI-SPEC.md §"Surface 6" (layout, ≤767px sheet / ≥768px dialog max-width 480px, focus trap, Escape=Cancel) + §"Copywriting Contract" (heading "Edit recurring series", body "This will update all occurrences of this event.", confirm "Update series" accent-filled, "Cancel" ghost) + §"EventForm primary CTAs" (Edit recurring occurrence CTA = "Update series")
    - .planning/phases/06-ux-polish/06-RESEARCH.md §"Focus 3 — Confirmation prompt (D-09)" — render when editMode && occurrence.hasRrule && Save tapped; PUT replaces master VEVENT (no RECURRENCE-ID)
  </read_first>
  <action>
    Create `SeriesEditPrompt.tsx` mirroring `DeleteConfirmationDialog`'s responsive sheet/dialog, focus trap, and Escape-to-cancel, with `role="dialog"`, `aria-modal="true"`, `aria-labelledby` → the heading. Use the exact UI-SPEC Surface 6 copy: heading "Edit recurring series", body "This will update all occurrences of this event.", primary accent-filled "Update series", ghost "Cancel" (NO destructive color — this is an edit). In `EventForm.tsx`, when in edit mode AND `occurrence?.hasRrule === true`, tapping Save opens `SeriesEditPrompt` instead of submitting directly; confirming "Update series" runs the existing edit submit (the same PATCH `/api/events/:uid/edit` that PUTs the master VEVENT wholesale — no RECURRENCE-ID, per D-08); Cancel returns to the form without submitting. For non-recurring or create mode, Save submits directly as today. Set the edit-recurring CTA label to "Update series" per UI-SPEC. Commit `feat(06-06): add whole-series edit confirmation prompt`.
  </action>
  <verify>
    <automated>cd apps/pwa && pnpm test -- run 2>&1 | tail -3</automated>
  </verify>
  <acceptance_criteria>
    - `SeriesEditPrompt` renders the exact UI-SPEC copy with focus trap + Escape=cancel + role="dialog"/aria-modal.
    - In edit mode with `occurrence.hasRrule === true`, Save opens the prompt; confirming runs the existing whole-series PATCH; canceling does not submit.
    - Non-recurring / create-mode Save behavior is unchanged (no prompt).
    - PWA suite green.
  </acceptance_criteria>
  <done>Recurring-occurrence edits are confirmed via a whole-series prompt; master-VEVENT PUT path unchanged; no per-occurrence edit introduced.</done>
</task>

<task type="auto">
  <name>Task 3: All-day visual distinction — Schedule-X override (999.6, D-12)</name>
  <files>apps/pwa/src/styles/index.css</files>
  <read_first>
    - apps/pwa/src/styles/index.css — the existing `.sx__*` override section (the documented Schedule-X selector overrides) to extend with an all-day rule
    - apps/pwa/src/lib/hydrateEvents.ts + apps/pwa/src/lib/calendarConfig.ts — how `_familySync.color` / `calendarId` color propagates to Schedule-X chips (so the member color already drives the fill)
    - .planning/phases/06-ux-polish/06-UI-SPEC.md §"Surface 3" — treatment contract: all-day = full-width filled rounded pill (border-radius 4px), white label, font-weight 600, `--text-label-size`; override selector `.sx__all-day-event`; timed events keep their existing partial-fill chip; color comes from the existing calendarId color config (no per-event inline override)
  </read_first>
  <action>
    Add a `.sx__all-day-event` override to the Schedule-X section of `index.css` per UI-SPEC Surface 3: render all-day chips as a full-width rounded pill (`border-radius: 4px`), white (`#FFFFFF`) label text, `font-weight: 600`, `font-size: var(--text-label-size)`, with the member color as solid background fill sourced from the existing `calendarId` color config (do NOT add per-event inline styles — the color already propagates via `buildCalendarConfig`). Leave timed-event chip styling untouched so the contract holds: all-day = solid filled pill, timed = partial-fill chip with colored border accent. Keep all existing `.sx__*` rules intact. Commit `feat(06-06): distinct all-day event pill styling`.
  </action>
  <verify>
    <automated>grep -v '^#' apps/pwa/src/styles/index.css | grep -c 'sx__all-day-event' | grep -qx 1 && cd apps/pwa && pnpm test -- run 2>&1 | tail -3</automated>
  </verify>
  <acceptance_criteria>
    - `.sx__all-day-event` override present exactly once with full-width pill + white bold label per UI-SPEC.
    - Existing `.sx__*` layout rules unchanged.
    - PWA suite green (no test regression from the CSS addition).
  </acceptance_criteria>
  <done>All-day chips render as distinct filled pills; timed chips unchanged.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 4: playwright-cli — end-tracking, recurrence bound, series-edit prompt, all-day distinction</name>
  <files>(verification only — no files modified)</files>
  <action>
    Verification task (no code changes). Using the playwright-cli skill against desktop Chromium (dev stack host-side per docs/deployment.md, DEV_AUTH_BYPASS=true), exercise the six behaviors: (1) end-tracking — move a timed event's start and confirm the end follows preserving 1h and never lands before start; repeat all-day (day-span preserved); (2) recurrence bound — set Weekly, confirm the "Ends" control appears, pick "On date" and save a bounded series (occurrences stop), then "After N times" N=3 → 3 occurrences, and "Never" stays unbounded; (3) FREQ persistence — create a Daily recurrence and confirm occurrences render daily not weekly; (4) series edit — edit an existing recurring occurrence, tap Save, confirm the focus-trapped "Edit recurring series" prompt (Escape cancels) and that "Update series" applies across occurrences; (5) all-day distinction — confirm all-day events render as full-width filled pills visually distinct from timed chips; (6) all-day no-drift — edit an existing all-day event and save twice, confirming it does not grow by a day. This is a blocking human-verify checkpoint — pause for operator confirmation.
  </action>
  <read_first>
    - .claude/skills/playwright-cli/SKILL.md — drive desktop Chromium, interact with the event form and calendar
    - docs/deployment.md §"Running locally (host-side, no Docker)" — dev run command (DEV_AUTH_BYPASS=true)
    - .planning/phases/06-ux-polish/06-UI-SPEC.md — Surfaces 3/4/5/6 acceptance behavior
  </read_first>
  <what-built>
    EventForm end-tracking, the "Ends" recurrence-bound control, the whole-series edit confirmation prompt, and the all-day filled-pill visual treatment.
  </what-built>
  <how-to-verify>
    1. End-tracking (D-04): open New Event, set a 1h timed event, then move the start date/time forward — confirm the end follows, preserving 1h, and never lands before the start. Repeat for an all-day event (day-span preserved).
    2. Recurrence bound (D-06): set recurrence to Weekly, confirm the "Ends" control appears; pick "On date" and a date, save, and confirm the created series is bounded (occurrences stop at/after the date, not an endless/2-month bar). Try "After N times" with N=3 and confirm 3 occurrences. Confirm "Never" is unbounded as before.
    3. FREQ persistence (D-07): create a Daily recurrence and confirm occurrences render daily (not weekly).
    4. Series edit (D-08/D-09): open an existing recurring occurrence, edit the title/time, tap Save — confirm the "Edit recurring series" prompt appears (focus-trapped, Escape cancels), confirm "Update series" applies the change across occurrences.
    5. All-day distinction (999.6): confirm all-day events render as full-width filled pills visually distinct from timed chips at a glance.
    6. All-day edit no-drift (D-05): edit an existing all-day event and save twice — confirm it does not grow by a day.
  </how-to-verify>
  <verify>
    <human-check>End follows start with a floor; recurrence is boundable and FREQ persists; series-edit prompt gates whole-series edits; all-day pills are visually distinct; all-day edits do not drift.</human-check>
  </verify>
  <resume-signal>Type "approved" or describe which behavior failed.</resume-signal>
  <acceptance_criteria>
    - All six behaviors above observed correctly in desktop Chromium.
  </acceptance_criteria>
  <done>The full event-form polish set verified live via playwright-cli.</done>
</task>

</tasks>

<threat_model>

## Trust Boundaries

| Boundary                                   | Description                                                                                                                                           |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| client → API (PATCH /api/events/:uid/edit) | Whole-series edit PUTs the master VEVENT back to Fastmail; the new `recurrenceUntil`/`recurrenceCount` cross here (validated server-side in Plan 02). |
| user input → form state                    | Recurrence bound date/count are user inputs shaped in the form before submit.                                                                         |

## STRIDE Threat Register

| Threat ID      | Category        | Component                                                | Disposition | Mitigation Plan                                                                                                                                                                                                                                                                             |
| -------------- | --------------- | -------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| T-06-06-input  | Tampering       | recurrence bound inputs (EventForm)                      | mitigate    | Form-side validation (count ≥ 1, until ≥ start) plus the authoritative server-side Zod validation from Plan 02 (`recurrenceUntil` max-10, `recurrenceCount` int≥1) — the client check is UX, the server check is the enforcement boundary. Defense in depth; no raw passthrough. (ASVS V5.) |
| T-06-06-series | Tampering       | whole-series edit PUT (EventForm → existing /edit route) | mitigate    | Reuses the existing edit route's ownership + objectUrl/etag lookup (unchanged from Phase 3) — the prompt only gates the UX; it adds no new privilege. The PUT replaces the master VEVENT for the caller's own event only; access scope is the existing per-user filter.                     |
| T-06-06-xss    | Tampering / XSS | all-day pill label, prompt copy                          | accept      | All-day labels and prompt text render as plain-text JSX children (existing EventForm XSS posture, T-03-15) — no `dangerouslySetInnerHTML`; the CSS override sets presentation only. No new injection surface.                                                                               |
| T-06-06-SC     | Tampering       | npm installs                                             | accept      | No package installs (zero new deps).                                                                                                                                                                                                                                                        |

</threat_model>

<verification>
- `cd apps/pwa && pnpm test -- run components/EventForm` green; full `pnpm --filter @familysync/pwa test` green.
- `grep -n "computeNewTimedEnd\|computeNewAllDayEnd" apps/pwa/src/components/EventForm.tsx` present; `grep -n "recurrenceUntil\|recurrenceCount" apps/pwa/src/components/EventForm.tsx` present.
- `grep -c 'sx__all-day-event' apps/pwa/src/styles/index.css` returns 1.
- playwright-cli verifies all six event-form behaviors.
</verification>

<success_criteria>

- D-03/D-04 (criterion 2): end follows start with a floor.
- D-06 (criterion 2): recurrence is boundable; D-07: FREQ persists.
- D-08/D-09 (criterion 3): whole-series edit behind a confirmation prompt.
- D-05 (criterion 2): all-day edit off-by-one stays fixed.
- 999.6/D-12 (criterion 1): all-day events visually distinct.
  </success_criteria>

<output>
Create `.planning/phases/06-ux-polish/06-06-SUMMARY.md` when done (note playwright-cli observations for each behavior).
</output>
