Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-14
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Design: fix-today-completed-logical-date

## Context

Driven by FR1–FR3 from proposal. Current state (`ActiveTasksPage.tsx:145-150`):

```ts
const todayCompleted = useMemo(() => {
const logicalToday = getLogicalDate(systemClock, getCachedDayBoundary());
return completedTasks.filter(
(task) => task.completed_at?.slice(0, 10) === logicalToday,
);
}, [completedTasks]);
```

`logicalToday` is a local logical date; `completed_at.slice(0, 10)` is a UTC calendar date. The comparison mixes frames of reference. Meanwhile `groupCompletedTasks()` (`shared/lib/utils.ts:19`) already solves the same classification correctly for CompletedPage: it builds `startOfToday` as a `Temporal.Instant` (logical today at `dayBoundary` in the user's timezone) and compares instants.

## Goals / Non-Goals

**Goals:**
- Instant-based "completed today" classification in ActiveTasksPage (FR1)
- One source of truth shared with CompletedPage (FR2)

**Non-Goals:**
- Reactivity to day-boundary setting changes without remount (proposal NG1)
- Any change to `groupCompletedTasks` internals (proposal NG3)

## Decisions

### D1: Reuse `groupCompletedTasks().todayTasks` instead of writing a local instant comparison

```ts
const todayCompleted = useMemo(
() =>
groupCompletedTasks(completedTasks, undefined, getCachedDayBoundary())
.todayTasks,
[completedTasks],
);
```

**Why**: identical call shape already exists in `CompletedPage.tsx:77-81`, so both pages provably classify the same task the same way (FR2), and the empty-`completed_at` guard (FR3) comes for free (`groupCompletedTasks` routes such tasks to `earlierTasks`).

**Alternative considered**: local `Temporal.Instant.compare(completedInstant, startOfToday) >= 0` filter inside ActiveTasksPage. Rejected — duplicates the boundary math in a second place, and the two pages could drift apart again.

**Trade-off accepted**: `groupCompletedTasks` computes all five groups while we need one. Input is the already-filtered list of completed tasks inside a memo; the extra work is a single O(n) pass with four unused array pushes — negligible (NFR-P1).

### D2: Keep the non-reactive `getCachedDayBoundary()` + `[completedTasks]` deps pattern

Same pattern as CompletedPage. Making dayBoundary reactive is NG1 and would have to change both pages at once — out of scope. The predecessor change's task 1.3 ("add clock and dayBoundary to useMemo dependencies") was never implementable as written: `systemClock` is a constant import and `getCachedDayBoundary()` is a plain function call, neither is a reactive value.

### D3: Tests live in `ActiveTasksPage.completed.test.tsx`, component-level

The bug is in the page's filter wiring, not in `groupCompletedTasks` (which has its own tests). Component tests with mocked `useCompletedTasks` and controlled `completed_at` timestamps pin the wiring. Timezone-sensitive scenarios (delta spec: UTC+5 early morning, UTC-4 evening, custom boundary) require controlling both "now" and the timezone — use the project's `fakeClock` from `@/lib/temporal` (never `vi.setSystemTime`). If `systemClock` inside the page cannot see the fake time, mock `@/hooks/useSettings#getCachedDayBoundary` and `getLogicalDate`'s inputs via module mock of `@/lib/temporal`'s `systemClock` — the existing testSetup already centralizes page mocks; extend it rather than inventing a parallel harness.

## Risks / Trade-offs

- [Component tests can't easily fake the system timezone] → mock `systemClock` at module level via `vi.mock("@/lib/temporal", ...)` re-exporting a `fakeClock("...", "Asia/Almaty")`; verify the RED state (test fails against `slice(0,10)` implementation) before applying the fix, per TDD workflow
- [Behavior change visible to users: early-morning completions previously *left* the section at UTC midnight; now the section follows the logical day] → this is exactly the intended fix (proposal U1/U2); no migration needed, data untouched

## Migration Plan

Pure client-side filter fix — no data, schema, or sync changes. Rollback = revert the commit.

## Open Questions

None.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# fix-today-completed-logical-date

Supersedes: fix-date-and-weekly-bugs (FR1 — the fix was incomplete)

## Why

The "Completed today" section in ActiveTasksPage compares a timezone-aware logical date (`getLogicalDate`) against a UTC date slice of `completed_at` (`task.completed_at?.slice(0, 10)`). The two sides live in different frames of reference, so whenever the local calendar date differs from the UTC date at completion time, the task never appears in "Completed today". The predecessor change `fix-date-and-weekly-bugs` fixed only the "today" side of the comparison (FR1) and its promised regression test was never added; its own scenario U1 (user in UTC+5 completes a task between 00:00–05:00 local) still fails — and now fails harder than before the fix (the task used to show until 05:00, now it never shows).

## What Changes

- **FIX**: `ActiveTasksPage.tsx` — replace the manual `completed_at.slice(0, 10) === logicalToday` filter with `groupCompletedTasks(completedTasks, systemClock, getCachedDayBoundary()).todayTasks`, the instant-based grouping already used by CompletedPage (`shared/lib/utils.ts`)
- **ADDED**: regression tests for todayCompleted in non-UTC timezones and with a custom day boundary — the RED test promised by `fix-date-and-weekly-bugs` task 1.1 that never landed

## Capabilities

### New Capabilities

_(none)_

### Modified Capabilities

- `day-boundary`: the todayCompleted filter requirement changes — the completion day of a task MUST be derived from the `completed_at` instant in the user's timezone with the day boundary applied (instant comparison against start-of-logical-day), never from a UTC string slice

## Impact

- `packages/client/src/pages/ActiveTasksPage.tsx` — todayCompleted useMemo
- `packages/client/src/pages/ActiveTasksPage.completed.test.tsx` — new timezone/day-boundary tests
- No API, schema, or dependency changes; `groupCompletedTasks` is reused as-is

## Goals

- G1: A task completed "today" in the user's logical day (timezone + day boundary) appears in "Completed today" regardless of the UTC date of its `completed_at`
- G2: ActiveTasksPage and CompletedPage classify the same task into "today" identically (single source of truth: `groupCompletedTasks`)

## Non-Goals

- NG1: Making the section reactive to day-boundary setting changes without remount — CompletedPage has the same non-reactive `getCachedDayBoundary()` pattern; changing it is a separate initiative
- NG2: Auto-recalculation of the section on day rollover while the app is open (explicitly excluded by fix-date-and-weekly-bugs NG1 as well)
- NG3: Any changes to `groupCompletedTasks` itself — it is already correct

## Users & Scenarios

- U1: User in UTC+5 (Almaty) completes a task at 02:00 local — `completed_at` is 21:00 UTC of the previous day; the task MUST appear in "Completed today"
- U2: User in UTC-4 (New York, summer) completes a task at 21:00 local — `completed_at` is 01:00 UTC of the next day; the task MUST appear in "Completed today" immediately
- U3: User with day boundary "04:00" completes a task at 01:30 local — the completion belongs to the previous logical day; the task MUST appear in "Completed today" (both sides shift together)

## Requirements

### Functional

- FR1: todayCompleted in ActiveTasksPage MUST classify tasks by comparing the `completed_at` instant against the start of the logical day (logical today at `dayBoundary` in the user's timezone), not by comparing date strings
- FR2: todayCompleted MUST reuse `groupCompletedTasks().todayTasks` so ActiveTasksPage and CompletedPage cannot diverge
- FR3: Tasks with empty `completed_at` MUST NOT appear in "Completed today" (preserved behavior)

### Non-Functional

#### Performance

- NFR-P1: The filter stays inside `useMemo` keyed on `completedTasks` — no additional renders versus the current implementation

#### Accessibility

_(no changes — list rendering is untouched)_

#### Responsive

_(no changes)_

## UX Acceptance Criteria

- UX1: Completing a task at any local time of the user's logical day shows it in "Completed today" without reload, in every timezone

## UI States Matrix

No changes — the section already renders only when non-empty; empty/loading/error handling is untouched.

## Behavior

Scenarios are covered by unit tests in `ActiveTasksPage.completed.test.tsx` (component-level, fakeClock-driven); no new Gherkin feature — this is a correctness fix inside an existing behavior, consistent with the predecessor change.

## Visual Reference

No visual changes.

## Affected IA

No changes.

## Success Metrics

- M1: New regression tests (U1, U2, U3 scenarios) pass; the same tests fail against the current implementation (RED verified before GREEN)
- M2: All existing ActiveTasksPage and shared/lib tests keep passing
- M3: Mutation score for the changed filter code >= 95%

## Open Questions

- Q1: none
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Delta: day-boundary — fix-today-completed-logical-date

## MODIFIED Requirements

### Requirement: Logical date computation from day boundary
# implements FR1, FR2 of fix-today-completed-logical-date

The system SHALL provide a `getLogicalDate(clock, dayBoundary)` function that returns the current logical date. If the current local time is before `dayBoundary`, the function SHALL return the previous calendar day. If the current local time is at or after `dayBoundary`, the function SHALL return the current calendar day. When `dayBoundary` is `"00:00"`, the function SHALL always return the current calendar day (fast path).

**Changed behavior**: ActiveTasksPage todayCompleted MUST classify a task as "completed today" by comparing the `completed_at` instant against the start of the logical day (logical today at `dayBoundary` in the user's timezone), never by comparing date strings. A UTC string slice of `completed_at` (`slice(0, 10)`) MUST NOT be used on either side of the comparison. ActiveTasksPage MUST obtain the "today" group via `groupCompletedTasks().todayTasks` so that ActiveTasksPage and CompletedPage classify tasks identically.

#### Scenario: Midnight boundary returns calendar date
- **WHEN** day boundary is "00:00" and current time is any time of day
- **THEN** logical date equals the calendar date from `clock.plainDateISO()`

#### Scenario: Before boundary returns previous day
- **WHEN** day boundary is "02:00" and current local time is 01:30 on June 5
- **THEN** logical date is June 4

#### Scenario: At boundary returns current day
- **WHEN** day boundary is "02:00" and current local time is 02:00 on June 5
- **THEN** logical date is June 5

#### Scenario: After boundary returns current day
- **WHEN** day boundary is "02:00" and current local time is 14:00 on June 5
- **THEN** logical date is June 5

#### Scenario: Large boundary value
- **WHEN** day boundary is "06:00" and current local time is 05:59 on June 5
- **THEN** logical date is June 4

#### Scenario: Respects current timezone
- **WHEN** day boundary is "02:00" and timezone is "Asia/Tokyo" and current time is 01:00 JST on June 5
- **THEN** logical date is June 4 (computed in Tokyo timezone)

#### Scenario: todayCompleted uses logical date in non-UTC timezone
- **WHEN** user timezone is UTC+5, current UTC time is 2026-06-05T01:00:00Z (local 06:00), day boundary is "00:00"
- **AND** task was completed at 2026-06-05T00:30:00Z (local 05:30 on June 5)
- **THEN** todayCompleted includes the task (both are June 5 in local timezone)

#### Scenario: todayCompleted includes early-morning completion whose UTC date is yesterday
- **WHEN** user timezone is UTC+5 (Asia/Almaty), current UTC time is 2026-06-09T21:30:00Z (local 02:30 June 10), day boundary is "00:00"
- **AND** task was completed at 2026-06-09T21:00:00Z (local 02:00 June 10)
- **THEN** todayCompleted includes the task even though the UTC date of `completed_at` is June 9

#### Scenario: todayCompleted includes evening completion whose UTC date is tomorrow
- **WHEN** user timezone is UTC-4 (America/New_York, summer), current UTC time is 2026-06-10T01:30:00Z (local 21:30 June 9), day boundary is "00:00"
- **AND** task was completed at 2026-06-10T01:00:00Z (local 21:00 June 9)
- **THEN** todayCompleted includes the task even though the UTC date of `completed_at` is June 10

#### Scenario: todayCompleted includes post-boundary completion whose UTC date is yesterday (custom boundary)
- **WHEN** user timezone is UTC+5, day boundary is "04:00", current UTC time is 2026-06-10T07:00:00Z (local 12:00 June 10 — after boundary, logical day is June 10)
- **AND** task was completed at 2026-06-09T23:30:00Z (local 04:30 June 10 — after the 04:00 boundary, UTC date June 9)
- **THEN** todayCompleted includes the task (completed after the June 10 logical day started at 04:00 local, even though the UTC date of `completed_at` is June 9)

#### Scenario: todayCompleted respects custom day boundary
- **WHEN** user timezone is UTC+5, day boundary is "04:00", current UTC time is 2026-06-09T21:30:00Z (local 02:30 June 10 — before boundary, logical day is June 9)
- **AND** task was completed at 2026-06-09T05:00:00Z (local 10:00 June 9)
- **THEN** todayCompleted includes the task (completed after the June 9 logical day started at 04:00 local)

#### Scenario: todayCompleted excludes yesterday's tasks
- **WHEN** user timezone is UTC+5, current UTC time is 2026-06-05T01:00:00Z (local 06:00 June 5)
- **AND** task was completed at 2026-06-04T10:00:00Z (local 15:00 June 4)
- **THEN** todayCompleted does NOT include the task

#### Scenario: todayCompleted excludes tasks without completed_at
- **WHEN** a task has `is_completed = true` and `completed_at = ""`
- **THEN** todayCompleted does NOT include the task
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## 1. Regression tests (RED) — FR1, FR2, FR3

- [x] 1.1 Extend `activeTasksPage.testSetup.tsx` to allow faking clock/timezone for the page (module mock of `systemClock` per design D3) and mocking `getCachedDayBoundary` — no behavior change for existing tests
- [x] 1.2 Write failing test: task completed at 2026-06-09T21:00:00Z, timezone UTC+5, now local 02:30 June 10 → appears in "Completed today" (FR1, delta scenario "early-morning completion whose UTC date is yesterday")
- [x] 1.3 Write failing test: task completed at 2026-06-10T01:00:00Z, timezone UTC-4, now local 21:30 June 9 → appears in "Completed today" (FR1, delta scenario "evening completion whose UTC date is tomorrow")
- [x] 1.4 Fix test-harness bug (`settingsMocks.ts` duplicate `useSettings` mock overrides testSetup's `pageConfig.dayBoundary`); write failing test: day boundary "04:00", timezone UTC+5, now local 12:00 June 10, task completed local 04:30 June 10 (UTC 2026-06-09T23:30:00Z) → appears in "Completed today" (FR1, delta scenario "post-boundary completion whose UTC date is yesterday"); keep the original "respects custom day boundary" scenario as a green-only regression guard
- [x] 1.5 Run `npx vitest run src/pages/ActiveTasksPage.completed.test.tsx` — confirm the three RED tests (1.2, 1.3, 1.4) FAIL against current implementation, the green guard and existing tests pass; show output

## 2. Fix (GREEN) — FR1, FR2, FR3

- [x] 2.1 Replace the manual filter in `ActiveTasksPage.tsx` todayCompleted with `groupCompletedTasks(completedTasks, undefined, getCachedDayBoundary()).todayTasks` (design D1); remove now-unused `getLogicalDate`/`systemClock` imports if nothing else uses them
- [x] 2.2 Add JSDoc traceability on the memo: `Implements FR1, FR2 of fix-today-completed-logical-date`
- [x] 2.3 Run `npx vitest run src/pages/ActiveTasksPage.completed.test.tsx` — all tests green; show output

## 3. Verification

- [x] 3.1 Run `npx vitest run src/pages/ActiveTasksPage` — full page suite green (M2)
- [x] 3.2 Run `npx vitest run src/shared/lib` — groupCompletedTasks suite untouched and green (M2)
- [x] 3.3 Call `get_file_problems` for changed files via JetBrains MCP; run `pnpm run build`
- [x] 3.4 Scoped mutation run: `cd packages/client && npx stryker run --mutate 'src/pages/ActiveTasksPage.tsx'`; analyze report at `packages/client/reports/mutation/mutation-report.json` — filter-related mutants killed (M3); pre-existing JSX survivors out of scope
Loading
Loading