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

6.6 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions duration completed
03-event-write-back-pwa-install 02 broker, caldav
ical.js
tsdav
vevent-builder
caldav-write
d-13
tdd
phase plan provides
03-event-write-back-pwa-install 01 Wave 0 RED test scaffold (vevent.test.ts, write.test.ts), calendarOutbox schema
buildVeventString(NewEventParams) → { uid, icsString } in broker/vevent.ts
createCalendarEvent / updateCalendarEvent / deleteCalendarEvent in broker/write.ts
RRULE_PRESETS map and NewEventParams interface exported from vevent.ts
03-03
03-04
added patterns
ICAL.Recur.fromString + new ICAL.Property('rrule') for RRULE serialization (addPropertyWithValue on string produces char-split output)
ICAL.Time({ isDate: true }, ICAL.Timezone.localTimezone) for all-day DATE values (TS types require 2-arg constructor)
ICAL.Time.fromJSDate(date, true) for timed UTC events (useUTC=true → Z suffix, no TZID)
null etag passed as '' in tsdav calendarObject (safe default; tsdav skips If-Match header)
created modified
apps/api/src/broker/vevent.ts
apps/api/src/broker/write.ts
D-02-RRULE: ICAL.Recur.fromString + ICAL.Property('rrule') is required for correct RRULE serialization. ICAL.Component.addPropertyWithValue('rrule', string) treats the string as a TEXT value and serializes each character individually — unusable. Use ICAL.Recur.fromString → prop.setValue(recur) → vevent.addProperty(prop).
D-02-DATE-ZONE: ICAL.Time constructor TypeScript signature requires 2 args (data, zone). For all-day DATE values, isDate:true suppresses any TZID output regardless of which zone is passed. ICAL.Timezone.localTimezone is the safe choice; it satisfies the type without adding TZID to DATE properties.
~4min 2026-06-05

Phase 03 Plan 02: Broker Primitives — vevent.ts + write.ts Summary

VEVENT builder and tsdav write wrappers implemented GREEN against Wave 0 RED scaffolds — all 13 broker tests pass, tsc clean

Performance

  • Duration: ~4 min
  • Started: 2026-06-05T21:44Z
  • Completed: 2026-06-05T21:48Z
  • Tasks: 2
  • Files created: 2

Accomplishments

  • Implemented buildVeventString(params: NewEventParams): { uid: string; icsString: string } in broker/vevent.ts using ical.js ICAL.Component/ICAL.Time APIs
  • D-13 DATE/DATETIME split: all-day events use ICAL.Time({ isDate: true }) → VALUE=DATE (no TZID, no time); timed events use ICAL.Time.fromJSDate(date, true) → DTSTART:...Z (no TZID)
  • RRULE correctly serialized via ICAL.Recur.fromString + ICAL.Property (not addPropertyWithValue which produces char-split output)
  • Exported NewEventParams interface and RRULE_PRESETS map (daily/weekly/monthly/yearly preset strings)
  • Implemented createCalendarEvent, updateCalendarEvent, deleteCalendarEvent in broker/write.ts as the sole CalDAV write boundary (D-12)
  • All etag null-coalescion to '' so tsdav safely omits the If-Match header rather than crashing

Task Commits

  1. Task 1: GREEN — buildVeventStringb23b959 (feat)
  2. Task 2: GREEN — tsdav write wrappers + vevent.ts TS fixa1243c1 (feat)

Files Created

  • apps/api/src/broker/vevent.ts — buildVeventString, NewEventParams, RRULE_PRESETS (117 lines)
  • apps/api/src/broker/write.ts — createCalendarEvent, updateCalendarEvent, deleteCalendarEvent (99 lines)

Decisions Made

  • D-02-RRULE: ICAL.Component.addPropertyWithValue('rrule', string) treats the raw string as a TEXT value and serializes character-by-character (e.g., RRULE:0=F;1=R;2=E;3=Q...). The correct approach is ICAL.Recur.fromString(rruleString)prop = new ICAL.Property('rrule')prop.setValue(recur)vevent.addProperty(prop). This produces the correct RRULE:FREQ=WEEKLY;BYDAY=MO output.

  • D-02-DATE-ZONE: ical.js ICAL.Time TypeScript types require 2 arguments (data: timeInit, zone: Timezone). For all-day DATE values, isDate: true in the data object suppresses any TZID/time output regardless of the zone passed. ICAL.Timezone.localTimezone is the appropriate second arg — it satisfies the type and has no effect on DATE serialization.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] RRULE serialization via addPropertyWithValue produces character-split output

  • Found during: Task 1 — first test run showed RRULE:0=F;1=R;2=E;3=Q... instead of RRULE:FREQ=WEEKLY;BYDAY=MO
  • Issue: vevent.addPropertyWithValue('rrule', string) passes a JavaScript string where ical.js expects a RECUR value type. ical.js iterates the string object properties (0, 1, 2...) and serializes each character as a key-value pair.
  • Fix: Use ICAL.Recur.fromString(params.rruleString) to parse the string into a RECUR value object, then new ICAL.Property('rrule') + prop.setValue(recur) + vevent.addProperty(prop).
  • Files modified: apps/api/src/broker/vevent.ts
  • Commit: a1243c1 (combined with Task 2)

2. [Rule 1 - Bug] ICAL.Time constructor TypeScript type error (2 args required)

  • Found during: Task 2 — tsc --noEmit reported Expected 2 arguments, but got 1 for new ICAL.Time({ isDate: true }) calls
  • Issue: ical.js TypeScript declarations define constructor(data: timeInit, zone: Timezone) as requiring both arguments, though the JavaScript implementation accepts 1.
  • Fix: Pass ICAL.Timezone.localTimezone as the second arg. For isDate: true DATE values, the zone has no effect on serialization — it does not add TZID to the property.
  • Files modified: apps/api/src/broker/vevent.ts
  • Commit: a1243c1

Known Stubs

None — both files are fully implemented primitives. No hardcoded placeholder values.

Threat Surface Scan

No new network endpoints or auth paths introduced. broker/write.ts is a low-level CalDAV I/O primitive called only by the outbox worker (planned in 03-03). The T-03-03 (ical.js escaping) and T-03-04 (etag sourced server-side) mitigations from the threat model are implemented as designed.

Self-Check: PASSED

  • apps/api/src/broker/vevent.ts — exists (confirmed)
  • apps/api/src/broker/write.ts — exists (confirmed)
  • Commit b23b959 — exists (git log confirmed)
  • Commit a1243c1 — exists (git log confirmed)
  • pnpm --filter @familysync/api exec vitest run tests/broker/vevent.test.ts — 7/7 PASS
  • pnpm --filter @familysync/api exec vitest run tests/broker/write.test.ts — 6/6 PASS
  • pnpm --filter @familysync/api exec tsc --noEmit — clean (no errors)

Phase: 03-event-write-back-pwa-install Completed: 2026-06-05