---
phase: 06-ux-polish
plan: "01"
subsystem: pwa/lib
tags: [tdd, date-math, event-form, d-04]
dependency_graph:
  requires: []
  provides:
    - computeNewTimedEnd (apps/pwa/src/lib/eventDateTime.ts)
    - computeNewAllDayEnd (apps/pwa/src/lib/eventDateTime.ts)
  affects:
    - Plan 06-06 (EventForm wires these helpers into onChange handlers)
tech_stack:
  added: []
  patterns:
    - Local Date accessors (WR-05): getFullYear/getMonth/getDate/getHours/getMinutes — never toISOString().slice
    - TDD RED→GREEN with Vitest in apps/pwa
key_files:
  created: []
  modified:
    - apps/pwa/src/lib/eventDateTime.ts
    - apps/pwa/src/lib/eventDateTime.test.ts
decisions:
  - WR-05 enforced: all new date formatting uses local accessors; toISOString().slice banned for date strings
  - Private helpers (localDateISO, localTimeHHMM, dateDiffDays, addDaysISO) added to eventDateTime.ts to support the two exports
metrics:
  duration_minutes: 2
  completed_date: "2026-06-10"
  tasks_completed: 2
  files_changed: 2
---

# Phase 06 Plan 01: End-Tracking Duration Math (D-04) Summary

**One-liner:** Pure duration-preservation helpers (`computeNewTimedEnd` + `computeNewAllDayEnd`) with 1h/same-day floor rules, TDD RED→GREEN in eventDateTime.ts.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 (RED) | Failing tests for computeNewTimedEnd / computeNewAllDayEnd | `16cdbf3` | eventDateTime.test.ts |
| 2 (GREEN) | Implement the two end-tracking helpers | `605f543` | eventDateTime.ts |

## What Was Built

Two exported pure functions added to `apps/pwa/src/lib/eventDateTime.ts`:

- **`computeNewTimedEnd(newStartDate, newStartTime, oldStartDate, oldStartTime, oldEndDate, oldEndTime)`** — preserves a timed event's duration when the start moves. Returns `{ endDate: 'YYYY-MM-DD', endTime: 'HH:MM' }`. If the old end was already behind the old start (stale state), floors to newStart + 1 hour.
- **`computeNewAllDayEnd(newStartDate, oldStartDate, oldEndDate)`** — preserves an all-day event's inclusive day-span when the start moves. Returns `'YYYY-MM-DD'`. If the old span was negative (stale state), floors to 0 days (same day as newStart).

Four private helpers added to the same file: `localDateISO`, `localTimeHHMM`, `dateDiffDays`, `addDaysISO`. All use local Date accessors (WR-05 compliance).

## Test Coverage

Six new unit tests in `apps/pwa/src/lib/eventDateTime.test.ts`:

| Test | Behavior |
|------|----------|
| preserves a 1-hour timed delta | old 09:00→10:00; new start 11:00 → new end 12:00 |
| preserves a multi-day timed delta (26h) | old 08:00→+26h; new start same offset → correct |
| floors to 1h when old end was behind start | stale end → snaps to newStart+1h |
| preserves a 0-day span (single day all-day) | oldStart=oldEnd → newEnd=newStart |
| preserves a 3-day span | newEnd = newStart + 3 days |
| floors to same day when old end behind start | negative span → 0 → same day |

Full suite: **166/166 tests pass**. Pre-existing `serializeEventDateTime` / `localWallClockToUtcIso` tests unaffected.

## TDD Gate Compliance

- RED commit (`test(06-01): ...`): `16cdbf3` — 6 tests failing with `TypeError: computeNewTimedEnd is not a function`
- GREEN commit (`feat(06-01): ...`): `605f543` — all 166 tests pass
- RED→GREEN order confirmed via `git log`

## Deviations from Plan

None — plan executed exactly as written.

## Threat Flags

None. Pure client-side date arithmetic on already-trusted local form state. No network boundary, no new attack surface. T-06-01 accepted per threat model.

## Self-Check: PASSED

- `apps/pwa/src/lib/eventDateTime.ts` — FOUND (modified)
- `apps/pwa/src/lib/eventDateTime.test.ts` — FOUND (modified)
- Commit `16cdbf3` — FOUND (RED: test(06-01))
- Commit `605f543` — FOUND (GREEN: feat(06-01))
- `computeNewTimedEnd` export — FOUND in eventDateTime.ts
- `computeNewAllDayEnd` export — FOUND in eventDateTime.ts
- No `toISOString().slice` in helper code — CONFIRMED
