docs(08): create phase plan (4 widget vertical slices)
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
---
|
||||
phase: 08-dashboard-widgets-vollimplementierung
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [08-01]
|
||||
files_modified:
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
- apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
|
||||
- apps/web/src/app/(portal)/page.tsx
|
||||
autonomous: true
|
||||
requirements: [DASH-10]
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can start, stop/pause and reset a Stopwatch widget"
|
||||
- "User can record lap times while the stopwatch runs"
|
||||
- "A running stopwatch continues correctly after a page reload (reconstructed from persisted startedAt)"
|
||||
artifacts:
|
||||
- "apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx"
|
||||
- "apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx"
|
||||
key_links:
|
||||
- "stopwatch-widget.tsx persists state via updateWidgetConfig(instanceId, ...)"
|
||||
- "page.tsx calls wireStopwatchWidget(StopwatchWidget)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the Stopwatch widget vertical slice (DASH-10): start, stop/pause, reset, and lap recording, with state persisted into WidgetInstance.config so a running stopwatch survives a page reload.
|
||||
|
||||
Purpose: Add a persistent timer widget without any new backend endpoint — reuses the existing PATCH /dashboard/widgets/:id/config path (D-07).
|
||||
Output: A user can add a Stopwatch widget, run it, record laps, reset it, and reload the page without losing elapsed time.
|
||||
</objective>
|
||||
|
||||
<phase_goal>
|
||||
**As a** portal user, **I want to** run a stopwatch with lap times on my dashboard that keeps counting after a reload, **so that** I can time activities without a separate tool and without losing progress.
|
||||
</phase_goal>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-dashboard-widgets-vollimplementierung/08-CONTEXT.md
|
||||
@.planning/phases/08-dashboard-widgets-vollimplementierung/08-RESEARCH.md
|
||||
@.planning/phases/08-dashboard-widgets-vollimplementierung/08-01-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<artifacts_produced>
|
||||
## Artifacts this phase produces (Plan 02)
|
||||
|
||||
- Component: `StopwatchWidget` (apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx)
|
||||
- Test file: `stopwatch-widget.test.tsx`
|
||||
- StopwatchConfig shape stored in WidgetInstance.config: `{ state: 'running' | 'paused' | 'stopped', startedAt: string | null, elapsed: number, laps: number[] }`
|
||||
- Wiring line in page.tsx: `wireStopwatchWidget(StopwatchWidget)` (consumes the hook created in Plan 01)
|
||||
</artifacts_produced>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Failing tests for Stopwatch behavior and reload reconstruction (RED)</name>
|
||||
<files>
|
||||
apps/web/src/components/dashboard/widgets/stopwatch-widget.test.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
- apps/web/src/components/dashboard/widgets/note-widget.test.tsx (fake timers, fetch spy, next-intl mock patterns)
|
||||
- apps/web/src/components/dashboard/widgets/clock-widget.tsx (WidgetProps, setInterval pattern)
|
||||
- .planning/phases/08-dashboard-widgets-vollimplementierung/08-RESEARCH.md (Pattern 5 stopwatch persistence, Pitfall 2 reload drift)
|
||||
</read_first>
|
||||
<behavior>
|
||||
Mock next-intl (passthrough). Mock updateWidgetConfig from '@/lib/dashboard-api' with vi.fn resolving. Use vi.useFakeTimers.
|
||||
- Initial render with config {} shows 00:00 (elapsed 0) and a Start control.
|
||||
- Clicking Start then advancing timers by 3000ms shows elapsed ~00:03; updateWidgetConfig was called with state 'running' and a startedAt ISO string.
|
||||
- Clicking Stop/Pause while running freezes the display and persists state 'paused' with accumulated elapsed (number of ms).
|
||||
- Clicking Reset returns display to 00:00 and persists state 'stopped', elapsed 0, laps [].
|
||||
- Clicking Lap while running appends the current elapsed to laps and renders at least one lap entry.
|
||||
- Reload reconstruction: rendering with config { state: 'running', startedAt: <ISO 5s ago>, elapsed: 2000, laps: [] } shows elapsed of roughly 7000ms (2000 + 5000) — proving Date.now() - startedAt + elapsed math (Pitfall 2).
|
||||
</behavior>
|
||||
<action>
|
||||
Create stopwatch-widget.test.tsx. Mock '@/lib/dashboard-api' so updateWidgetConfig is a spy. Set a fixed system time via vi.setSystemTime for deterministic reconstruction; compute the "5s ago" startedAt from that fixed now. Query controls by their accessible name via the mocked t() keys (stopwatch.start/stop/reset/lap). Assert display text via a stable test id data-testid="stopwatch-display". Import StopwatchWidget after mocks. Tests MUST fail now (component does not exist).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/web test --run stopwatch-widget 2>&1 | grep -Eq "fail|FAIL|Cannot find|error" && echo RED-OK</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- stopwatch-widget.test.tsx exists and imports from './stopwatch-widget'
|
||||
- Covers start, stop/pause, reset, lap, and reload reconstruction
|
||||
- Test run fails (RED) due to missing implementation
|
||||
</acceptance_criteria>
|
||||
<done>The stopwatch test file exists and fails for the right reason, defining the timer contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Stopwatch implementation with config persistence + wiring (GREEN)</name>
|
||||
<files>
|
||||
apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
apps/web/src/app/(portal)/page.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
- apps/web/src/components/dashboard/widgets/clock-widget.tsx (setInterval + WidgetProps pattern)
|
||||
- apps/web/src/components/dashboard/widgets/note-widget.tsx (updateWidgetConfig usage, cleanup on unmount)
|
||||
- apps/web/src/lib/dashboard-api.ts (updateWidgetConfig signature: (id, config, signal?))
|
||||
- apps/web/src/app/(portal)/page.tsx (existing wireXWidget calls; wireStopwatchWidget already defined in Plan 01)
|
||||
- .planning/phases/08-dashboard-widgets-vollimplementierung/08-RESEARCH.md (Pattern 5, Pitfall 2, Lap-Timer decision)
|
||||
</read_first>
|
||||
<action>
|
||||
Create stopwatch-widget.tsx as `export function StopwatchWidget({ instanceId, config }: WidgetProps)` ('use client'; import type WidgetProps from '../widget-registry'; useTranslations('widgets')). Read initial state from config with the StopwatchConfig shape { state, startedAt, elapsed, laps }, defaulting to state 'stopped', startedAt null, elapsed 0, laps []. When state is 'running', compute live elapsed as Date.now() - new Date(startedAt).getTime() + (config.elapsed ?? 0) and tick via setInterval(…, 100) updating a display; clear the interval on unmount and when not running (Pitfall 2). Render the elapsed with a data-testid="stopwatch-display" formatted mm:ss (or hh:mm:ss when >= 1h) plus centiseconds optional.
|
||||
|
||||
Controls (labels via t('stopwatch.start'|'stop'|'reset'|'lap')):
|
||||
- Start: if not running, set startedAt = new Date().toISOString(), keep accumulated elapsed, state 'running', persist via updateWidgetConfig(instanceId, {...}).
|
||||
- Stop/Pause: freeze — compute accumulated elapsed, set state 'paused', startedAt null, persist.
|
||||
- Reset: state 'stopped', startedAt null, elapsed 0, laps [], persist.
|
||||
- Lap: while running, append current live elapsed (ms) to laps (newest first per research), persist; render laps as a scrollable list.
|
||||
Use Tailwind only (flex h-full flex-col, muted/foreground/primary tokens). Wrap control buttons so their click handlers call stopPropagation to avoid grid-drag interference. Persist errors may be swallowed like note-widget (best-effort); do not block the UI.
|
||||
|
||||
Wire the widget: in page.tsx import StopwatchWidget and call wireStopwatchWidget(StopwatchWidget) next to the existing wire calls.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/web test --run stopwatch-widget</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- stopwatch-widget.test.tsx passes (GREEN)
|
||||
- stopwatch-widget.tsx calls updateWidgetConfig with a startedAt ISO string on Start
|
||||
- Reload reconstruction test passes (elapsed derived from startedAt + stored elapsed)
|
||||
- page.tsx contains wireStopwatchWidget(StopwatchWidget)
|
||||
- grep -c "module.css" apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx returns 0
|
||||
</acceptance_criteria>
|
||||
<done>A user can start/stop/reset/lap the stopwatch; state persists to WidgetInstance.config and a running timer reconstructs correctly after reload.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Suite + type-check gate</name>
|
||||
<files>
|
||||
apps/web/src/components/dashboard/widgets/stopwatch-widget.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
- apps/web/package.json (scripts)
|
||||
</read_first>
|
||||
<action>
|
||||
Run the full web test suite and web type-check to confirm the stopwatch slice did not regress other widgets. Fix any type errors. No new features.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/web test --run && pnpm --filter @tessera/web exec tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Full @tessera/web vitest suite exits 0
|
||||
- tsc --noEmit passes for @tessera/web
|
||||
</acceptance_criteria>
|
||||
<done>Stopwatch slice is complete with a green suite and clean type-check.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| client → API (PATCH /dashboard/widgets/:id/config) | Stopwatch config crosses into the API; ownership already enforced by DashboardService |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-08-03 | Tampering | stopwatch config persistence | low | mitigate | Reuses DashboardService.updateWidgetConfig ownership check (WHERE userId); no new endpoint added |
|
||||
| T-08-04 | Denial of Service | setInterval tick | low | mitigate | Single interval cleared on unmount and when not running; no unbounded timers |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- pnpm --filter @tessera/web test --run passes (stopwatch suite + all others)
|
||||
- tsc --noEmit clean for web
|
||||
- Manual smoke (optional): start stopwatch, reload page, timer keeps counting; lap + reset work
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Stopwatch widget supports start, stop/pause, reset and lap recording (DASH-10)
|
||||
- Running stopwatch reconstructs elapsed time after reload from persisted startedAt/elapsed
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-dashboard-widgets-vollimplementierung/08-02-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user