docs(07): create phase plan

This commit is contained in:
Christopher Mühl 2026-04-09 10:57:48 +02:00
parent 32b00f7776
commit 9c0a3398ab
No known key found for this signature in database
GPG key ID: 925AC7D69955293F
3 changed files with 600 additions and 4 deletions

View file

@ -39,8 +39,8 @@
4. Adding a new visualization mode requires only implementing a ModeRenderer interface and registering it 4. Adding a new visualization mode requires only implementing a ModeRenderer interface and registering it
**Plans:** 2 plans **Plans:** 2 plans
Plans: Plans:
- [ ] 06-01-PLAN.md — Type contracts, state, registry, and shared utility extraction - [x] 06-01-PLAN.md — Type contracts, state, registry, and shared utility extraction
- [ ] 06-02-PLAN.md — YearModeRenderer, orchestrator rewrite, test migration, visual check - [x] 06-02-PLAN.md — YearModeRenderer, orchestrator rewrite, test migration, visual check
### Phase 7: Mode Switcher + Week Mode ### Phase 7: Mode Switcher + Week Mode
**Goal**: Users can switch between year and week visualization modes and toggle between hours and entry-count display **Goal**: Users can switch between year and week visualization modes and toggle between hours and entry-count display
@ -52,7 +52,10 @@ Plans:
3. Hours/count toggle switches the color scale metric across both year and week modes without re-fetching data 3. Hours/count toggle switches the color scale metric across both year and week modes without re-fetching data
4. Switching modes preserves the current filter selection 4. Switching modes preserves the current filter selection
5. Vitest tests cover mode switcher interaction, week renderer output, and display toggle behavior 5. Vitest tests cover mode switcher interaction, week renderer output, and display toggle behavior
**Plans**: TBD **Plans:** 2 plans
Plans:
- [ ] 07-01-PLAN.md — UI controls (mode switcher + metric toggle), Twig template, orchestrator wiring
- [ ] 07-02-PLAN.md — WeekModeRenderer implementation, registration, visual verification
**UI hint**: yes **UI hint**: yes
### Phase 8: Backend Aggregation + Filtering ### Phase 8: Backend Aggregation + Filtering
@ -106,7 +109,7 @@ Note: Phases 7 and 8 can execute in parallel (both depend only on Phase 6).
| 4. Heatmap Interaction | v1.0 | 2/2 | Complete | 2026-04-08 | | 4. Heatmap Interaction | v1.0 | 2/2 | Complete | 2026-04-08 |
| 5. Polish | v1.0 | 2/2 | Complete | 2026-04-08 | | 5. Polish | v1.0 | 2/2 | Complete | 2026-04-08 |
| 6. Renderer Architecture | v1.1 | 0/2 | Planned | - | | 6. Renderer Architecture | v1.1 | 0/2 | Planned | - |
| 7. Mode Switcher + Week Mode | v1.1 | 0/? | Not started | - | | 7. Mode Switcher + Week Mode | v1.1 | 0/2 | Planned | - |
| 8. Backend Aggregation + Filtering | v1.1 | 0/? | Not started | - | | 8. Backend Aggregation + Filtering | v1.1 | 0/? | Not started | - |
| 9. Day + Combined Modes | v1.1 | 0/? | Not started | - | | 9. Day + Combined Modes | v1.1 | 0/? | Not started | - |
| 10. Entity Pickers | v1.1 | 0/? | Not started | - | | 10. Entity Pickers | v1.1 | 0/? | Not started | - |

View file

@ -0,0 +1,264 @@
---
phase: 07-mode-switcher-week-mode
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- Resources/views/widget/heatmap.html.twig
- assets/src/ui/controls.ts
- assets/src/heatmap.ts
- Resources/public/heatmap.css
- assets/test/controls.test.ts
autonomous: true
requirements: [VIZ-01, VIZ-05, TEST-01]
must_haves:
truths:
- "A segmented control in the widget header shows Year and Week buttons"
- "Clicking Week sets state.mode to week and triggers doRender"
- "A separate compact segmented control shows Hours and Count buttons"
- "Clicking Count sets state.metric to count and triggers doRender"
- "Controls persist across re-renders (live in card header, not SVG area)"
- "Stats row is hidden when mode is week, shown when mode is year"
artifacts:
- path: "assets/src/ui/controls.ts"
provides: "createModeControl and createMetricControl functions"
exports: ["createModeControl", "createMetricControl"]
- path: "Resources/views/widget/heatmap.html.twig"
provides: "Controls placeholder div in card header"
contains: "heatmap-controls"
- path: "assets/test/controls.test.ts"
provides: "Tests for mode switcher and metric toggle"
min_lines: 40
key_links:
- from: "assets/src/heatmap.ts"
to: "assets/src/ui/controls.ts"
via: "import createModeControl, createMetricControl"
pattern: "import.*controls"
- from: "assets/src/ui/controls.ts"
to: "state.mode / state.metric"
via: "onChange callbacks"
pattern: "onChange"
---
<objective>
Build the mode switcher and hours/count toggle UI controls, wire them into the heatmap orchestrator, and add the Twig template placeholder.
Purpose: Enables switching between year/week modes and hours/count metrics -- the UI backbone for all v1.1 visualization features.
Output: Two Tabler nav-segmented controls in the widget header, wired to HeatmapState, with tests.
</objective>
<execution_context>
@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/workflows/execute-plan.md
@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-mode-switcher-week-mode/07-CONTEXT.md
@.planning/phases/07-mode-switcher-week-mode/07-RESEARCH.md
@.planning/phases/07-mode-switcher-week-mode/07-UI-SPEC.md
@.planning/phases/06-renderer-architecture/06-01-SUMMARY.md
@.planning/phases/06-renderer-architecture/06-02-SUMMARY.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
From assets/src/types.ts:
```typescript
export type DisplayMetric = 'hours' | 'count';
export type HeatmapMode = 'year' | 'week' | 'day' | 'combined';
```
From assets/src/state.ts:
```typescript
export interface HeatmapState {
mode: HeatmapMode;
metric: DisplayMetric;
filters: FilterState;
weekStart: string;
data: HeatmapData | null;
}
export function createInitialState(weekStart: string): HeatmapState;
```
From assets/src/renderers/registry.ts:
```typescript
export function getRenderer(mode: string): ModeRenderer;
```
From assets/src/shared/stats.ts:
```typescript
export function renderStats(container: HTMLElement, days: DayEntry[]): void;
```
From assets/src/heatmap.ts (current doRender pattern):
```typescript
const doRender = () => {
if (!state.data) return;
const renderer = getRenderer(state.mode);
renderer.destroy?.();
renderer.render({ container: svgArea, data: state.data, state, config: {...}, onCellClick, emptyMessage: ... });
renderStats(container, state.data.days);
svgArea.scrollLeft = svgArea.scrollWidth;
};
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create UI controls module and wire into heatmap orchestrator</name>
<files>
assets/src/ui/controls.ts,
assets/src/heatmap.ts,
Resources/views/widget/heatmap.html.twig,
Resources/public/heatmap.css,
assets/test/controls.test.ts
</files>
<read_first>
assets/src/heatmap.ts,
assets/src/types.ts,
assets/src/state.ts,
assets/src/shared/stats.ts,
Resources/views/widget/heatmap.html.twig,
Resources/public/heatmap.css,
.planning/phases/07-mode-switcher-week-mode/07-UI-SPEC.md
</read_first>
<behavior>
- createModeControl('year', [{key:'year',label:'Year'},{key:'week',label:'Week'}], cb) returns nav element with class 'nav nav-segmented' and role='tablist'
- createModeControl: first button has class 'nav-link active', second has 'nav-link' (no active)
- createModeControl: clicking inactive button adds 'active' to it, removes 'active' from previous, calls onChange with key
- createMetricControl('hours', cb) returns nav element with class 'nav nav-segmented nav-sm'
- createMetricControl: clicking 'Count' calls onChange with 'count'
- Both controls have role='tab' on buttons and aria-selected attribute
</behavior>
<action>
**1. Create `assets/src/ui/controls.ts`** with two exported functions:
`createModeControl(activeMode: string, modes: Array<{key: string; label: string}>, onChange: (mode: string) => void): HTMLElement`
- Create `<nav>` with `className = 'nav nav-segmented'` and `role="tablist"`
- For each mode, create `<button>` with `className = 'nav-link'` (add ` active` if `m.key === activeMode`)
- Set `role="tab"`, `aria-selected` = `"true"/"false"` based on active
- On click: remove `active` from all siblings, add `active` to clicked, update `aria-selected`, call `onChange(m.key)`
- Return the nav element
`createMetricControl(activeMetric: string, onChange: (metric: string) => void): HTMLElement`
- Same pattern but `className = 'nav nav-segmented nav-sm'`
- Two hardcoded options: `{key: 'hours', label: 'Hours'}` and `{key: 'count', label: 'Count'}`
**2. Modify `Resources/views/widget/heatmap.html.twig`** -- change the `{% block box_title %}` block from:
```
{% block box_title %}
{{ title }}
{% endblock %}
```
To:
```
{% block box_title %}
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
{{ title }}
<div id="heatmap-controls" style="display: flex; gap: 8px; margin-left: auto;"></div>
</div>
{% endblock %}
```
**3. Modify `assets/src/heatmap.ts`** -- in the `init()` function:
- Add import: `import { createModeControl, createMetricControl } from './ui/controls';`
- After building wrapper and svgArea, before the filter dropdown section, add control wiring:
```typescript
const controlsContainer = document.getElementById('heatmap-controls');
if (controlsContainer) {
const modeControl = createModeControl(state.mode, [
{ key: 'year', label: 'Year' },
{ key: 'week', label: 'Week' },
], (mode) => {
state.mode = mode as HeatmapMode;
doRender();
});
const metricControl = createMetricControl(state.metric, (metric) => {
state.metric = metric as DisplayMetric;
doRender();
});
controlsContainer.appendChild(modeControl);
controlsContainer.appendChild(metricControl);
}
```
- Modify `doRender()` to conditionally render stats: replace `renderStats(container, state.data.days);` with:
```typescript
if (state.mode === 'year') {
renderStats(container, state.data.days);
} else {
const existingStats = container.querySelector('.heatmap-stats');
if (existingStats) existingStats.remove();
}
```
- Modify `doRender()` to only auto-scroll for year mode: wrap `svgArea.scrollLeft = svgArea.scrollWidth;` in `if (state.mode === 'year') { ... }`
**4. Add to `Resources/public/heatmap.css`:**
```css
.heatmap-week-cell {
cursor: default;
}
```
**5. Create `assets/test/controls.test.ts`** with tests per the behavior block above. Import from `../src/ui/controls`. Use jsdom to verify DOM output, class names, click handlers, and aria attributes.
</action>
<verify>
<automated>cd /home/toph/code/toph/kimai-heatmap && npx vitest run assets/test/controls.test.ts</automated>
</verify>
<acceptance_criteria>
- assets/src/ui/controls.ts contains `export function createModeControl(`
- assets/src/ui/controls.ts contains `export function createMetricControl(`
- assets/src/ui/controls.ts contains `nav nav-segmented`
- assets/src/ui/controls.ts contains `nav-sm`
- assets/src/ui/controls.ts contains `role` and `tablist` and `aria-selected`
- assets/src/heatmap.ts contains `import { createModeControl, createMetricControl } from './ui/controls'`
- assets/src/heatmap.ts contains `document.getElementById('heatmap-controls')`
- assets/src/heatmap.ts contains `state.mode === 'year'` (conditional stats)
- Resources/views/widget/heatmap.html.twig contains `id="heatmap-controls"`
- Resources/views/widget/heatmap.html.twig contains `display: flex; align-items: center; gap: 12px`
- Resources/public/heatmap.css contains `.heatmap-week-cell`
- assets/test/controls.test.ts exists and `npx vitest run assets/test/controls.test.ts` exits 0
- `npm test` exits 0 (all 62+ tests pass)
</acceptance_criteria>
<done>Mode switcher and metric toggle controls render with correct Tabler classes, respond to clicks by updating active state and calling callbacks, and are wired into heatmap.ts to update state and trigger doRender. Stats row hidden in non-year modes. All tests pass.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
No new trust boundaries. Controls are purely client-side UI manipulating existing client state. No new data flows, authentication, or input from users beyond button clicks.
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-07-01 | T (Tampering) | mode/metric state | accept | Client-side only; tampering has no security impact (changes own view) |
</threat_model>
<verification>
- `npx vitest run assets/test/controls.test.ts` passes
- `npm test` passes (all existing + new tests)
- `npm run build:dev` succeeds
- Twig template contains `#heatmap-controls` div
</verification>
<success_criteria>
- Mode switcher with Year/Week buttons renders in widget header using Tabler nav-segmented
- Metric toggle with Hours/Count buttons renders adjacent to mode switcher using nav-segmented nav-sm
- Clicking controls updates HeatmapState and calls doRender
- Stats row conditionally hidden in week mode
- All tests green
</success_criteria>
<output>
After completion, create `.planning/phases/07-mode-switcher-week-mode/07-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,329 @@
---
phase: 07-mode-switcher-week-mode
plan: 02
type: execute
wave: 2
depends_on: [07-01]
files_modified:
- assets/src/renderers/week.ts
- assets/src/heatmap.ts
- assets/test/week.test.ts
autonomous: false
requirements: [VIZ-02, TEST-01]
must_haves:
truths:
- "Switching to week mode renders 7 horizontal cells colored by aggregated metric"
- "Day labels (Mon-Sun or Sun-Sat) respect user's start-of-week preference"
- "Tooltip on week cell shows full weekday name + aggregated value"
- "Weekdays with zero tracked time render as empty cells (not lowest green)"
- "Switching back to year mode restores the full calendar heatmap"
artifacts:
- path: "assets/src/renderers/week.ts"
provides: "WeekModeRenderer implementing ModeRenderer"
exports: ["WeekModeRenderer"]
min_lines: 60
- path: "assets/test/week.test.ts"
provides: "Tests for week renderer aggregation and rendering"
min_lines: 50
key_links:
- from: "assets/src/heatmap.ts"
to: "assets/src/renderers/week.ts"
via: "import + registerRenderer"
pattern: "registerRenderer.*WeekModeRenderer"
- from: "assets/src/renderers/week.ts"
to: "assets/src/shared/color-scale.ts"
via: "buildColorScale for week cell fill"
pattern: "buildColorScale"
- from: "assets/src/renderers/week.ts"
to: "assets/src/shared/tooltip.ts"
via: "createTooltip/showTooltip/hideTooltip"
pattern: "createTooltip"
---
<objective>
Implement the WeekModeRenderer that aggregates DayEntry data by weekday and renders a 7-cell horizontal heatmap, then wire it into the orchestrator.
Purpose: Delivers the first new visualization mode -- users can see which weekdays are busiest at a glance.
Output: Working week-mode renderer with tooltip, registered and dispatch-ready, with tests.
</objective>
<execution_context>
@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/workflows/execute-plan.md
@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-mode-switcher-week-mode/07-CONTEXT.md
@.planning/phases/07-mode-switcher-week-mode/07-RESEARCH.md
@.planning/phases/07-mode-switcher-week-mode/07-UI-SPEC.md
@.planning/phases/07-mode-switcher-week-mode/07-01-SUMMARY.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
From assets/src/renderers/types.ts:
```typescript
export interface RenderContext {
container: HTMLElement;
data: HeatmapData;
state: HeatmapState;
config: HeatmapConfig;
onCellClick?: (dateStr: string) => void;
emptyMessage?: string;
}
export interface ModeRenderer {
readonly mode: string;
render(ctx: RenderContext): void;
destroy?(): void;
}
```
From assets/src/types.ts:
```typescript
export interface DayEntry {
date: string; // "YYYY-MM-DD"
hours: number;
count: number;
}
export type DisplayMetric = 'hours' | 'count';
```
From assets/src/state.ts:
```typescript
export interface HeatmapState {
mode: HeatmapMode;
metric: DisplayMetric;
filters: FilterState;
weekStart: string;
data: HeatmapData | null;
}
```
From assets/src/shared/color-scale.ts:
```typescript
export function buildColorScale(days: DayEntry[], metric?: DisplayMetric): ScaleQuantize<string>;
```
From assets/src/shared/tooltip.ts:
```typescript
export function createTooltip(): HTMLDivElement;
export function showTooltip(tip: HTMLDivElement, html: string, anchorRect: DOMRect, cellSize: number): void;
export function hideTooltip(tip: HTMLDivElement): void;
```
From assets/src/shared/date-utils.ts:
```typescript
export function getDayLabels(weekStart: string): string[];
// DAY_LABELS_MONDAY = ['Mon', '', 'Wed', '', 'Fri', '', '']
// DAY_LABELS_SUNDAY = ['Sun', '', 'Tue', '', 'Thu', '', 'Sat']
```
From assets/src/renderers/registry.ts:
```typescript
export function registerRenderer(renderer: ModeRenderer): void;
```
Existing registration pattern in heatmap.ts:
```typescript
import { YearModeRenderer } from './renderers/year';
registerRenderer(new YearModeRenderer());
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement WeekModeRenderer and register in orchestrator</name>
<files>
assets/src/renderers/week.ts,
assets/src/heatmap.ts,
assets/test/week.test.ts
</files>
<read_first>
assets/src/renderers/year.ts,
assets/src/renderers/types.ts,
assets/src/renderers/registry.ts,
assets/src/shared/color-scale.ts,
assets/src/shared/tooltip.ts,
assets/src/shared/date-utils.ts,
assets/src/heatmap.ts,
assets/src/types.ts,
assets/src/state.ts,
.planning/phases/07-mode-switcher-week-mode/07-UI-SPEC.md
</read_first>
<behavior>
- WeekModeRenderer.mode equals 'week'
- render() with 3 DayEntries (Mon, Mon, Wed) aggregates: Monday totalHours = sum of both Mon entries, Wednesday totalHours = Wed entry hours
- render() produces an SVG with exactly 7 rect elements
- Rect for a weekday with data has a fill color from buildColorScale (not empty fill)
- Rect for a weekday with no data has class 'heatmap-empty'
- Labels respect weekStart: Monday-start first label is 'Mon', Sunday-start first label is 'Sun'
- Tooltip on hover shows full weekday name + aggregated value (e.g. "Monday: 8.5h (4 entries)")
- Tooltip for count metric shows "Monday: 4 entries (8.5h)" (count first)
- Empty weekday tooltip shows "Tuesday: No tracked time"
- destroy() removes tooltip element
</behavior>
<action>
**1. Create `assets/src/renderers/week.ts`:**
Define a local `WeekdayAggregate` interface:
```typescript
interface WeekdayAggregate {
dayIndex: number; // 0-6, relative to weekStart
label: string; // Full name: "Monday", "Tuesday", etc.
shortLabel: string; // Short: "Mon", "Tue", etc.
totalHours: number;
totalCount: number;
dayCount: number; // number of distinct days with entries
}
```
Weekday name arrays (for tooltip -- full names per D-07):
```typescript
const WEEKDAY_NAMES_MONDAY = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const WEEKDAY_NAMES_SUNDAY = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const WEEKDAY_SHORT_MONDAY = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const WEEKDAY_SHORT_SUNDAY = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
```
`aggregateByWeekday(days: DayEntry[], weekStart: string): WeekdayAggregate[]`:
- Create 7 buckets, indexed 0-6
- For each day entry: parse date, get JS day (`new Date(d.date + 'T00:00:00').getDay()`), compute index using `weekStart === 'sunday' ? jsDay : (jsDay + 6) % 7`
- Accumulate totalHours, totalCount, dayCount into bucket
- Set label from WEEKDAY_NAMES array by index, shortLabel from WEEKDAY_SHORT array
- Return all 7 buckets
`export class WeekModeRenderer implements ModeRenderer`:
- `readonly mode = 'week'`
- Private `tooltip: HTMLDivElement | null = null`
`render(ctx: RenderContext)`:
- Clear container: `ctx.container.innerHTML = ''`
- Handle empty data (same pattern as YearModeRenderer -- show emptyMessage or "No tracking data available")
- Destroy previous tooltip, create new one via `createTooltip()`
- Call `aggregateByWeekday(ctx.data.days, ctx.state.weekStart)`
- Build color scale: only pass DayEntry items that have data to `buildColorScale()` so scale domain reflects actual values. Create synthetic DayEntry array from aggregates for scale: `aggregates.filter(a => a.dayCount > 0).map(a => ({ date: '', hours: a.totalHours, count: a.totalCount }))`
- Then call `buildColorScale(syntheticDays, ctx.state.metric)`
- SVG layout constants (per UI-SPEC): cellWidth=60, cellHeight=40, cellGap=4, labelWidth=50
- SVG dimensions: width = `labelWidth + 7 * (cellWidth + cellGap)`, height = cellHeight
- Create SVG via d3 `select(ctx.container).append('svg')` with computed dimensions, class `heatmap-svg`
- Render 7 labels as `<text>` elements: class `heatmap-label`, x=0, y = vertically centered in cell (`cellHeight / 2 + 3`), text = shortLabel for each aggregate. Show ALL 7 labels (not the sparse pattern from year-mode)
- Render 7 rects: x = `labelWidth + i * (cellWidth + cellGap)`, y = 0, width = cellWidth, height = cellHeight
- Class: `heatmap-week-cell` always, add `heatmap-empty` if `dayCount === 0`
- Fill: if `dayCount === 0`, return `''` (CSS handles empty fill). Otherwise, use `colorScale(ctx.state.metric === 'hours' ? agg.totalHours : agg.totalCount)`
- rx="2" ry="2" for rounded corners
- Tooltip on mouseenter: if `dayCount > 0`:
- hours metric: `"<strong>{label}</strong><br>{totalHours.toFixed(1)}h ({totalCount} entries)"`
- count metric: `"<strong>{label}</strong><br>{totalCount} entries ({totalHours.toFixed(1)}h)"`
- If `dayCount === 0`: `"<strong>{label}</strong><br>No tracked time"`
- Use `showTooltip(tooltip, html, rect.getBoundingClientRect(), cellWidth)`
- On mouseleave: `hideTooltip(tooltip)`
- No click handler on week cells (per discretion -- aggregation has no single-date target)
`destroy()`:
- Remove tooltip if exists, set to null
Export the class.
**2. Modify `assets/src/heatmap.ts`:**
- Add import: `import { WeekModeRenderer } from './renderers/week';`
- Add registration below existing: `registerRenderer(new WeekModeRenderer());`
- That's the only change needed in heatmap.ts for this task (controls wiring done in Plan 01)
**3. Create `assets/test/week.test.ts`:**
Test the WeekModeRenderer using the pattern from existing test files. Import the renderer, create a container div, build test DayEntry arrays with known dates and values. Verify:
- SVG contains exactly 7 rect elements
- Aggregation sums correctly (two entries on same weekday -> combined hours)
- Empty weekdays get `heatmap-empty` class
- Data weekdays get a fill color (not empty string)
- Labels appear in correct weekStart order
- destroy() removes tooltip
- Test with weekStart='sunday' to verify label order changes
</action>
<verify>
<automated>cd /home/toph/code/toph/kimai-heatmap && npx vitest run assets/test/week.test.ts && npm test</automated>
</verify>
<acceptance_criteria>
- assets/src/renderers/week.ts contains `export class WeekModeRenderer implements ModeRenderer`
- assets/src/renderers/week.ts contains `readonly mode = 'week'`
- assets/src/renderers/week.ts contains `aggregateByWeekday`
- assets/src/renderers/week.ts contains `buildColorScale`
- assets/src/renderers/week.ts contains `createTooltip`
- assets/src/renderers/week.ts contains `showTooltip` and `hideTooltip`
- assets/src/renderers/week.ts contains `No tracked time`
- assets/src/renderers/week.ts contains `heatmap-week-cell`
- assets/src/renderers/week.ts contains `heatmap-empty`
- assets/src/renderers/week.ts contains `(jsDay + 6) % 7` (Monday-start weekday index)
- assets/src/heatmap.ts contains `import { WeekModeRenderer } from './renderers/week'`
- assets/src/heatmap.ts contains `registerRenderer(new WeekModeRenderer())`
- assets/test/week.test.ts exists and `npx vitest run assets/test/week.test.ts` exits 0
- `npm test` exits 0 (all tests pass including new week tests)
- `npm run build:dev` exits 0 (bundle builds)
</acceptance_criteria>
<done>WeekModeRenderer aggregates DayEntry data by weekday, renders 7 horizontal SVG cells with color scale and tooltips, respects weekStart preference, and is registered for dispatch. All tests pass.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Visual verification of mode switcher and week mode</name>
<files>none</files>
<action>Human visual verification of the complete Phase 7 feature set in a running Kimai instance. No code changes -- verify that Plan 01 (controls) and Plan 02 Task 1 (week renderer) work correctly together end-to-end.</action>
<verify>
<automated>cd /home/toph/code/toph/kimai-heatmap && npm test && npm run build:dev</automated>
</verify>
<done>Human confirms: mode switcher toggles year/week views, metric toggle switches hours/count coloring, week-mode shows 7 cells with correct tooltips, stats row hidden in week mode, no console errors.</done>
<what-built>
Mode switcher (Year/Week) and metric toggle (Hours/Count) controls in the widget header. Week-mode renderer showing 7 weekday cells with aggregated data. Stats row hidden in week mode, visible in year mode.
</what-built>
<how-to-verify>
1. Start local Kimai dev environment, navigate to dashboard
2. Verify the heatmap widget shows two controls in the header: "Year | Week" and "Hours | Count"
3. Click "Week" -- should show 7 horizontal cells with weekday labels (Mon-Sun), stats row should disappear
4. Hover over a colored cell -- tooltip shows full weekday name + aggregated hours and entry count
5. Hover over an empty cell -- tooltip shows "No tracked time"
6. Click "Count" -- cell colors should change (re-colored by entry count instead of hours)
7. Click "Year" -- should return to full calendar heatmap, stats row reappears
8. Click "Count" in year mode -- year cells re-color by count
9. Change project filter -- both modes should reflect filtered data
10. Check browser console -- no JavaScript errors
</how-to-verify>
<resume-signal>Type "approved" or describe issues</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
No new trust boundaries. Week renderer consumes the same authenticated data as year renderer. No new endpoints, inputs, or data flows.
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-07-02 | T (Tampering) | weekday aggregation | accept | Client-side aggregation of already-authenticated data; no security impact |
</threat_model>
<verification>
- `npx vitest run assets/test/week.test.ts` passes
- `npm test` passes (full suite)
- `npm run build:dev` succeeds
- Human verifies visual output in running Kimai
</verification>
<success_criteria>
- Week-mode renders 7 horizontal cells colored by aggregated hours or count
- Day labels respect start-of-week preference
- Tooltips show weekday name + value in correct format for hours/count metrics
- Empty weekdays render as empty cells (not lowest green)
- Mode switching between year and week works without errors
- Metric toggle affects both modes
</success_criteria>
<output>
After completion, create `.planning/phases/07-mode-switcher-week-mode/07-02-SUMMARY.md`
</output>