# Phase 6: UX Polish - Context

**Gathered:** 2026-06-10
**Status:** Ready for planning

<domain>
## Phase Boundary

Smooth the rough edges surfaced during Phase 3 Gate 2 live use so the app feels slick for the non-technical Apple member (hard UX constraint). **Polish only — no new capabilities.**

Scope is the six backlog polish items promoted into this phase:

| Item  | What it fixes                                                            | Success criterion |
| ----- | ------------------------------------------------------------------------ | ----------------- |
| 999.6 | All-day events visually distinct from timed events                       | #1                |
| 999.7 | Event end auto-tracks start (any event); all-day-edit off-by-one         | #2                |
| 999.8 | Recurrence can be bounded (repeat-until / count); FREQ-persistence check | #2                |
| 999.9 | A recurring series can be edited whole                                   | #3                |
| 999.3 | Session expiry redirects cleanly to sign-in instead of hanging           | #4                |
| 999.2 | No calendar/"sign-in required" flash before Authelia on cold load        | #5                |

**Explicitly OUT of this phase** (promoted to milestone 1.1, see Deferred): 999.4 (reminder/VALARM selector) and 999.5 (first-login provider setup). The roadmap flagged both as "more feature than polish"; the user confirmed they belong in the next milestone, not here.

</domain>

<decisions>
## Implementation Decisions

### Scope (the backlog pull)

- **D-01:** Phase 6 = the six polish items only (999.2/3/6/7/8/9). Do NOT pull in 999.4 or 999.5.
- **D-02:** 999.4 (reminders/VALARM) and 999.5 (provider setup) move to a new **milestone 1.1**, started via the proper GSD milestone flow (`/gsd-new-milestone`). 999.1 (provider-abstraction) also remains backlog/1.1 candidate.

### Event-form end-tracking (999.7) — applies to ALL events, not just recurring

- **D-03:** This is a general bug: when the user moves the **start** date/time forward, the **end** selector does not follow — for one-time, single-day, and timed events alike. Today an end left behind the start produces a stale/absurd span.
- **D-04:** On any start change, **preserve the current duration** — timed: keep the start→end delta; all-day: keep the day-span — so the end moves with the start automatically. **Floor requirement** (the user's explicit minimum): the end must never strand behind the start day; at worst it snaps to the same day as the new start, and the user extends forward from there for genuine multi-day events.
- **D-05:** The all-day-edit off-by-one part of 999.7 is **already fixed** (commit `f645644`, CR-03: exclusive `DTEND` → inclusive on edit pre-fill, `EventForm.tsx:199`). Verify it still holds; don't re-implement. Remaining 999.7 work = the start→end auto-advance.

### Recurrence bounding (999.8)

- **D-06:** Add a bound to the recurrence control so users stop misusing the event end-date as "repeat until." Primary control: **"repeat until <date>"** (RRULE `UNTIL`) — matches the misuse pattern most directly. **"for N occurrences"** (RRULE `COUNT`) is acceptable to ship alongside or as the alternative; exact control set is a planning/UI-phase call. Each occurrence's duration must stay tied to start→end, NOT the recurrence span.
- **D-07:** Verify/fix the FREQ-persistence bug noted in 999.8 (a daily selection reportedly persisted as weekly). Confirm the dropdown writes the selected `FREQ`.

### Recurring-series edit (999.9)

- **D-08:** Behavior is **whole-series edit**: editing a recurring occurrence edits the master VEVENT (title / time / RRULE) for all occurrences. Per-occurrence (`RECURRENCE-ID`) and "this and following" edits stay **deferred to v1.x** (carried from Phase 3 D-03 / Deferred Items — not reopened here).
- **D-09:** The confirmation/prompt UX for "this changes the whole series" is **delegated to `/gsd-ui-phase`** — lock the behavior, defer the wording/placement.

### Visual feel & sync feedback (user-added, 2026-06-10)

- **D-12:** Beyond fixing individual rough edges, the user wants the app to **feel more distinct and modern** as a whole. The visual refresh is delegated to `/gsd-ui-phase`, which **should invoke the `frontend-design` skill** to drive a polished, non-generic aesthetic across the touched surfaces. Scope note: this widens "polish the rough edges" toward a light visual refresh — keep it to elevating the existing surfaces (calendar, event form, lists, sync feedback, auth splash), not a ground-up redesign; a full redesign would be its own phase. The user owns this call.
- **D-13:** **Sync indicators must actually spin.** Today the spinner styles reference `animation: spin …` but the `@keyframes spin` is defined locally inside `PushPermissionPrompt.tsx` (`:359`) rather than globally — so `SyncStateToast` (`:158`) and `LiveSyncIndicator` likely don't animate when that component isn't mounted. Hoist `@keyframes spin` to a global stylesheet (or per-component) so every sync indicator animates. Planner/researcher to confirm the exact failure.

### Auth-flow polish (999.2 + 999.3)

- **D-10:** 999.2 — gate the app render on auth state so no calendar shell / skeleton / "Sign-in required" alert paints before Authelia. While unauthenticated and redirecting, show a single neutral full-screen "Signing you in…" splash. Reserve the "Sign-in required" dead-end only for the one-shot-guard fall-through. Root cause + proposed fix are in ROADMAP 999.2 (CalendarShell optimistic render).
- **D-11:** 999.3 — detect session expiry (401 / opaqueredirect) from ANY query or mutation (not just the initial `/api/me`) and drive a clear re-auth via top-level navigation to `/api/login`, ideally behind a brief "Your session expired — signing you back in…" interstitial. Centralize detection in `apps/pwa/src/api/client.ts` (typed `SessionExpiredError`, consistent `redirect:'manual'`) with a single TanStack Query/Mutation error handler re-arming `maybeRedirectToLogin()`. In-flight-write preservation/replay is a _nice-to-have_, not a hard requirement — acceptable to surface a clear re-auth rather than silently losing a write; planner to decide effort.

### Claude's Discretion / delegated to UI-phase

- **All-day visual treatment (999.6):** behavior locked (must be distinguishable at a glance); the concrete treatment — full-width pill/bar vs background band vs distinct shape — is **delegated to `/gsd-ui-phase`** against the design system.
- **Series-edit prompt UX (999.9):** delegated to `/gsd-ui-phase` (see D-09).
- **Splash/interstitial copy (999.2/999.3):** exact wording open; "Signing you in…" / "Your session expired — signing you back in…" are starting points.

</decisions>

<canonical_refs>

## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Phase scope & backlog source

- `.planning/ROADMAP.md` §"Phase 6: UX Polish" — goal + 5 success criteria (the locked WHAT)
- `.planning/ROADMAP.md` §"Backlog" 999.2/999.3/999.6/999.7/999.8/999.9 — each item carries a diagnosed root cause (from Phase 3 Gate 2, 2026-06-07) and a proposed fix. These are the authoritative problem statements.
- `.planning/ROADMAP.md` §"Backlog" 999.1/999.4/999.5 — deferred to milestone 1.1 (do NOT implement here)
- `.planning/PROJECT.md` — hard UX constraint (wife adoption), recurring "create + display only" v1 stance

### Event form / write-back (999.6/7/8/9)

- `apps/pwa/src/components/EventForm.tsx` — start/end/all-day/recurrence state; `:199` exclusive→inclusive all-day fix (D-05); `:303` current all-day toggle clamp; `RecurrencePreset` usage
- `apps/pwa/src/api/client.ts` §`RecurrencePreset` (`:130`) — frequency-only today; UNTIL/COUNT to be added (D-06)
- `apps/api/src/broker/vevent.ts` — WR-04 inclusive→exclusive `DTEND` on write-back (symmetry partner for D-05)
- `apps/api/src/broker/expand.ts` — recurrence expansion (per-occurrence duration must follow start→end, D-06)
- `.planning/phases/03-event-write-back-pwa-install/03-CONTEXT.md` — D-03 recurrence create+display-only, WR-01..04 write-back contract, per-occurrence/"this-and-following" deferral (carried into D-08)

### Visual feel & sync indicators (D-12/D-13)

- `apps/pwa/src/components/SyncStateToast.tsx` `:158` — uses `animation: 'spin …'` (D-13)
- `apps/pwa/src/components/LiveSyncIndicator.tsx` — sync-state indicator (D-13)
- `apps/pwa/src/components/PushPermissionPrompt.tsx` `:359` — where `@keyframes spin` is currently (locally) defined; needs hoisting to global (D-13)
- `frontend-design` skill — to be invoked by `/gsd-ui-phase` for the modern/distinct visual refresh (D-12)

### Auth flow (999.2/999.3)

- `apps/pwa/src/components/CalendarShell.tsx` — optimistic render that causes the flash (999.2 root cause, D-10)
- `apps/pwa/src/api/client.ts` — `fetchMe` `redirect:'manual'`, opaqueredirect/401 detection (`:38–44`); centralization point for D-11
- `apps/pwa/src/lib/loginRedirect.ts` — `maybeRedirectToLogin()` / one-shot `familysync.loginRedirectAttempted` guard to be re-armed (D-11)

</canonical_refs>

<code_context>

## Existing Code Insights

### Reusable Assets

- `EventForm.tsx`: already has start/end/all-day/recurrence state and the inclusive/exclusive all-day conversion helper (`exclusiveEndToInclusiveDate`). Extend it for end-tracking (D-04) and the recurrence bound control (D-06) rather than rebuilding.
- `client.ts`: already detects `opaqueredirect`/401 for `fetchMe`; the typed error + single error-handler pattern (D-11) generalizes the existing one-shot detection.
- `loginRedirect.ts` `maybeRedirectToLogin()`: reuse for the splash/redirect on both 999.2 and 999.3.

### Established Patterns

- Write-back keeps an inclusive(form)↔exclusive(`DTEND`) convention split across PWA (`EventForm.tsx`) and API (`vevent.ts` WR-04). Any end-tracking change (D-04) must preserve this; all-day day-span is inclusive in the form.
- Recurrence is "create + display only" in v1 (Phase 3 D-03). 999.9 adds whole-series edit on top; do NOT add per-occurrence edit.
- Auth redirect is intentionally `redirect:'manual'` + document navigation (XHR can't follow cross-origin IdP redirects); keep that mechanism for D-10/D-11.

### Integration Points

- 999.8 spans PWA (`EventForm` UNTIL/COUNT control) → API write (`vevent.ts` RRULE serialization) → expansion (`expand.ts`). Per-occurrence duration must derive from start→end, independent of the recurrence span.
- 999.2/999.3 both center on `client.ts` + `CalendarShell.tsx` + `loginRedirect.ts` — a single auth-gating refactor likely serves both; plan them together.

</code_context>

<specifics>
## Specific Ideas

- The motivating bug for 999.7/999.8: a "recurring event" created start 2026-06-11, end 2026-08-13 (~2-month span) with a weekly RRULE rendered as overlapping bars across the calendar (looked duplicated). The end didn't track the start AND the series was unbounded — two distinct fixes (D-04 + D-06).
- User's framing of 999.7 (verbatim intent): end-not-tracking-start is "not just for recurrence events — it's for any event… at the very least the end selector should move to the same day as the start time and the user can move it further forward for multi-day events from there."
- Splash copy starting points: "Signing you in…" (999.2 cold load), "Your session expired — signing you back in…" (999.3 mid-use).

</specifics>

<deferred>
## Deferred Ideas

- **999.4 — Event reminder/VALARM options** → milestone 1.1. The reminder-input half of the now-shipped Phase 5 push system; a real write-back feature, not polish.
- **999.5 — First-login provider setup (Fastmail app password)** → milestone 1.1. Multi-member onboarding; security + UX surface, not polish.
- **999.1 — Calendar provider abstraction** → backlog / milestone 1.1 candidate.
- **Per-occurrence (`RECURRENCE-ID`) and "this and following" recurring edits** → v1.x (carried from Phase 3; 999.9 delivers whole-series only).

</deferred>

---

_Phase: 6-ux-polish_
_Context gathered: 2026-06-10_
