Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-CONTEXT.md
T

12 KiB

Phase 3: Event Write-Back + PWA Install - Context

Gathered: 2026-06-05 Status: Ready for planning

## Phase Boundary

Members can create, edit, and delete events that are written back to the correct Fastmail calendar through the existing CalDAV broker boundary (PUT / DELETE via tsdav — never a direct Fastmail call from the UI). The app becomes an installable PWA (web manifest + service worker, HTTPS) with a guided iOS Add-to-Home-Screen walkthrough and Android install handling. This phase also carries the Phase 1 Gate 2 live-auth verification (D-14): real Authelia OIDC login over the public Pangolin URL including the iOS standalone-PWA flow, session persistence, and distinct stable per-member colors.

Requirements: CAL-04 (create timed/all-day), CAL-05 (edit), CAL-06 (delete), CAL-07 (create recurring — whole-series only), PWA-01 (installable), PWA-02 (guided install).

Out of scope (other phases / later):

  • Single-occurrence and "this-and-following" recurring edits (CAL-09 / CAL-10) — v1.x.
  • Shared lists + live SSE sync (Phase 4); Web Push notifications (Phase 5).
  • Wall-display / kiosk theme (v2).
  • SSE as a transport — must not be relied on in Phase 3 (unverified until Phase 4 gate, D-14).
## Implementation Decisions

Target-calendar selection (write target)

  • D-01: Default target = remember last-used per member. Seed/first-time default is the creator's own personal calendar (always exists; no shared calendar may exist yet, D-16).
  • D-02: Calendar picker is shown only when the member has >1 writable calendar. With a single writable calendar (e.g. personal only, before a shared Family calendar exists) the picker is hidden entirely — zero friction for the non-technical member. It appears once a shared Family calendar is present.
  • D-03: Writable set = the member's own personal calendar + the shared Family calendar (when shared read-write to them in Fastmail). The other member's personal calendar is a read-only overlay — never a write target. Matches the two-equal-partners model.
  • D-04: Edit may move an event to a different calendar. Implemented as CalDAV delete-from-old + create-on-new (not an in-place move). Researcher/planner must handle the UID/etag implications and partial-failure (delete succeeded but create failed, or vice-versa) safely.

Write feedback & sync (the load-bearing architecture decision)

  • D-05: Optimistic-accept + server-side outbox. On save the UI optimistically reflects the change with a "syncing…" indicator; the API writes a pending row to a MariaDB outbox and returns immediately. A backend worker drains the outbox against Fastmail.
  • D-06: Re-sync on confirm. When the queued write confirms, the worker triggers a targeted re-sync of just that one calendar (not a full poll) so the MariaDB cache becomes authoritative, then clears the pending state. (This is the async evolution of the operator's initial "forced re-sync, then show" — same authoritative-cache guarantee, without a blocking spinner.)
  • D-07: Retry policy — backoff transient, fail-fast hard errors.
    • Transient (network error, 5xx, timeout) → retry with exponential backoff over a bounded window (a few minutes), keeping the "not synced yet" toast visible.
    • Hard (401/403 auth, 400 validation) → stop immediately and surface a real "didn't save" error to the user. (Transient failures must be short; anything non-transient is a hard fail that won't self-resolve.)
  • D-08: Edit-conflict handling = detect + warn + reload latest. Writes send If-Match with the cached etag. On 412 Precondition Failed, the write is routed out of the retry loop into a conflict flow: re-sync that event from Fastmail and warn the user ("this event changed elsewhere — review the latest version") before they retry. No silent last-write-wins overwrite.
  • D-09: Sync-state is surfaced via polling, not SSE. The "pending / not-synced" state the toast reads must be exposed over a polled endpoint (or query refetch), because SSE-over-Pangolin is unverified until the Phase 4 entry gate (D-14). Do not build Phase 3 sync feedback on SSE.

Carried forward — locked, NOT re-discussed

  • D-10: The edit/delete surface reuses EventDetailPopover — it was built in Phase 2 with a reserved footer action area specifically for this (Phase 2 D-08). Create can reuse the same surface shell.
  • D-11: Recurring events: create + whole-series edit only in v1. Single-occurrence (RECURRENCE-ID/EXDATE) and "this-and-following" edits are v1.x (CAL-09/CAL-10).
  • D-12: Broker is the only Fastmail I/O boundary. Routes touch only the MariaDB cache; all PUT/DELETE goes through src/broker/. No tsdav import in route handlers.
  • D-13: Dev-auth bypass stays available for local build/test (project D-14); live Authelia verification is the Gate 2 item folded into this phase.

Claude's Discretion (researcher / planner decide)

  • Event form & fields — exact field set (title, start/end, all-day toggle, location, description) and layout. Build it into / alongside the reused EventDetailPopover surface (D-10). Must be slick and low-friction for the non-technical Apple member.
  • Recurrence creation UX — how whole-series RRULE is exposed (simple presets daily/weekly/ monthly/yearly vs a small custom builder). Keep it minimal for v1; whole-series only (D-11).
  • iOS install onboarding — trigger (auto-detect iOS-Safari-non-standalone vs help button vs first-visit banner) and the annotated Add-to-Home-Screen walkthrough content. Load-bearing: success criterion 4 requires a non-technical user to follow it independently; no install ⇒ no push in Phase 5.
  • Android installbeforeinstallprompt handling (custom button vs native prompt).
  • PWA toolingvite-plugin-pwa is in the recommended stack (CLAUDE.md) but not yet installed; manifest + service worker config is the planner's call. Keep the service worker conservative (don't break the OIDC redirect / standalone-PWA login flow — Gate 2 risk).
  • Outbox worker mechanics (interval vs trigger, idempotency key, max-attempt count, dead-letter surfacing) — implement to satisfy D-05/D-06/D-07.

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Project decisions & scope

  • .planning/PROJECT.md — core value, constraints, Key Decisions incl. D-14 (dev-auth bypass; live AUTH + iOS smoke folded into Phase 3), D-15 (local Newt test rig for Gate 2), D-16 (shared Family calendar is a collection on the operator's primary account, may not exist yet; is_shared flag marks it).
  • .planning/ROADMAP.md §"Phase 3: Event Write-Back + PWA Install" — goal + the 6 success criteria (incl. criterion 6, the carried Gate 2 live-auth/iOS verification).
  • .planning/REQUIREMENTS.md — CAL-04/05/06 (create/edit/delete), CAL-07 (recurring create), PWA-01/02 (installable + guided install); v1.x CAL-09/10 (single-occurrence edits — OUT).

Phase 1/2 foundation this builds on

  • .planning/phases/02-calendar-display/02-CONTEXT.md — design-token layer (D-01/02/03), EventDetailPopover reuse-as-edit-surface (Phase 2 D-08), server-side expansion, color/owner model.
  • .planning/phases/01-foundation-broker-spike/CAL-08-DECISION.md — per-member app-password model (how each member's calendars are reached/written; informs the writable set, D-03).
  • docs/deployment.mdGate 2 checklist (live Authelia OIDC over Pangolin, iOS standalone PWA) and the dev-auth bypass context. Required reading for success criterion 6.

Code this phase extends

  • apps/api/src/broker/client.ts — tsdav createDAVClient; add PUT/DELETE write methods here (broker boundary, D-12).
  • apps/api/src/broker/sync.ts — REPORT→ical.js→upsert; the targeted single-calendar re-sync (D-06) builds on this.
  • apps/api/src/broker/poller.ts — ctag poller; the outbox worker is a sibling background process.
  • apps/api/src/routes/events.ts — current read-only GET /api/events; add create/edit/delete endpoints + the pending/sync-state surface (D-09) alongside.
  • apps/api/src/db/schema.tscalendars (userId, isShared, url), calendarEvents (uid, etag, rawVevent, dtstart split). New outbox table lives here (D-05).
  • apps/pwa/src/components/EventDetailPopover.tsx — reserved footer action area is the edit/delete entry point (D-10).
  • apps/pwa/src/api/client.ts — typed fetch client to extend with write calls + sync-state poll.
  • apps/pwa/vite.config.ts — no PWA plugin yet; manifest + service worker added here (PWA-01).
  • CLAUDE.md — locked stack incl. vite-plugin-pwa 1.3.0, tsdav write-back guidance (PUT new .ics / DELETE by UID), iOS PWA constraints (16.4+, home-screen install required).

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • EventDetailPopover (Phase 2) — read-only detail overlay with a reserved footer for Phase 3 edit/delete (D-10); responsive bottom-sheet/popover, focus trap, XSS-safe plain-text rendering.
  • apps/pwa/src/api/client.ts — typed fetch client (fetchMe, windowed fetchEvents, credentials: 'include'); extend with create/edit/delete + sync-state poll.
  • Broker module (client.ts/sync.ts/poller.ts) — owns all Fastmail I/O and ical.js parsing; write methods and the outbox worker attach here.
  • Design-token layer + Zustand UI store + TanStack Query — server state in Query, UI state in Zustand.

Established Patterns

  • Broker boundary (T-03-02): routes read the MariaDB cache only; never import tsdav in a route. Write-back must keep this — endpoint enqueues to the outbox; the broker worker does the CalDAV PUT.
  • D-13 schema split for all-day (dtstartDate) vs timed (dtstartUtc) — write-back must produce correct VEVENTs for both and never coerce DATE→DATETIME.
  • Idempotency: calendar_id + uid composite unique key; etag tracked per event (drives D-08).
  • Hono app exported without auto-start (testable); add write routes + outbox worker in that shape.

Integration Points

  • New MariaDB outbox table (D-05): pending create/edit/delete operations with status, attempt count, target calendar URL, payload/UID, etag for If-Match.
  • Outbox worker (sibling to the ctag poller) drains the queue, applies D-07 retry/backoff, triggers the targeted re-sync (D-06), and updates pending status read by the polled sync-state endpoint (D-09).
  • Write endpoints on the events router (create/edit/delete) that validate input (zod) and enqueue rather than calling Fastmail inline.
  • PWA layer: vite-plugin-pwa manifest + service worker (PWA-01) — keep the SW conservative so it does not break the OIDC redirect / iOS standalone login (Gate 2, success criterion 6).

</code_context>

## Specific Ideas
  • The non-technical Apple member is the design center: the calendar picker disappears when there's only one choice (D-02), saves never block on a spinner (D-05/D-06), and a failed sync shows a clear, persistent "not synced yet" state rather than silently losing the edit.
  • "Transient failures should be short; anything beyond that is a hard fail that won't auto-resolve" — drives the bounded-backoff-then-fail policy (D-07).
  • iOS install walkthrough must be followable independently with annotated screenshots — it's the prerequisite for her getting Web Push in Phase 5.
## Deferred Ideas
  • Single-occurrence / "this-and-following" recurring edits (CAL-09/CAL-10) — v1.x; Phase 3 is create + whole-series edit only.
  • Writing to the other member's personal calendar — out; other members' personals are read-only overlays in v1 (D-03).
  • SSE-based live sync-state push — deferred to after the Phase 4 SSE-over-Pangolin gate; Phase 3 surfaces sync state via polling (D-09).
  • Event form areas not deep-dived (exact fields, recurrence-builder richness, iOS/Android install UX) — left to researcher/planner discretion within the constraints above; not deferred out of phase, just not operator-locked.

Phase: 3-Event Write-Back + PWA Install Context gathered: 2026-06-05