diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md new file mode 100644 index 0000000..04f4b72 --- /dev/null +++ b/.planning/MILESTONES.md @@ -0,0 +1,20 @@ +# Milestones + +## v1.0 MVP (Shipped: 2026-04-08) + +**Phases completed:** 5 phases, 11 plans +**Stats:** 67 files, ~10k insertions, ~650 LOC source (PHP + TypeScript), ~730 LOC tests + +**Key accomplishments:** + +- Nix devshell with process-compose orchestrating MariaDB + Kimai dev server +- HeatmapService with timezone-correct daily aggregation and PHPUnit tests +- d3.js calendar heatmap with color scale, labels, tooltips, and Kimai theme integration +- Click-through navigation to Kimai timesheet + project filter dropdown +- Streak indicator, summary stats, weekend styling, and week-start preference +- 30+ tests across PHPUnit and Vitest + +**Known gaps:** +- INTR-01 partial: activity filtering descoped (project filter only) + +--- diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md index 997d3c7..f574b90 100644 --- a/.planning/PROJECT.md +++ b/.planning/PROJECT.md @@ -2,7 +2,7 @@ ## What This Is -A Kimai dashboard widget that displays a GitHub-style activity heatmap visualizing time tracking data. Built as a Symfony bundle plugin for personal time tracking use, rendering an interactive d3.js calendar heatmap on the Kimai dashboard. +A Kimai dashboard widget that displays a GitHub-style activity heatmap visualizing time tracking data. Built as a Symfony bundle plugin for personal time tracking, rendering an interactive d3.js calendar heatmap with click-through navigation, project filtering, streak tracking, and summary stats. ## Core Value @@ -12,36 +12,36 @@ At a glance, see where your time went — a visual map of tracking activity that ### Validated -(None yet — ship to validate) +- ✓ Dashboard widget renders a d3.js calendar heatmap of time entries — v1.0 +- ✓ Filterable by project — v1.0 +- ✓ Clicking a day cell navigates to Kimai's timesheet view filtered to that day — v1.0 +- ✓ Color scheme uses Kimai's theme CSS variables — v1.0 (partial: tooltip/labels/empty use CSS vars, color scale hardcoded green) +- ✓ Nix flake/devshell provides a local Kimai instance with seeded database — v1.0 +- ✓ PHPUnit tests for backend (API, data aggregation) — v1.0 +- ✓ JavaScript tests for the d3 heatmap component — v1.0 +- ✓ Streak indicator, summary stats, weekend styling — v1.0 ### Active -- [ ] Dashboard widget renders a d3.js calendar heatmap of time entries - [ ] Heatmap cells toggle between hours-per-day and entry-count display - [ ] Configurable time range (not locked to a single preset) -- [ ] Filterable by project and/or activity -- [ ] Clicking a day cell navigates to Kimai's timesheet view filtered to that day -- [ ] Color scheme uses Kimai's theme variables -- [ ] Nix flake/devshell provides a local Kimai instance with seeded database -- [ ] PHPUnit tests for backend (API, data aggregation) -- [ ] JavaScript tests for the d3 heatmap component +- [ ] Activity filtering (project filter shipped, activity deferred) ### Out of Scope -- Billable hours tracking features — this is for personal time tracking +- Billable hours tracking features — personal time tracking only - Mobile-specific layout — desktop Kimai dashboard only -- Export/sharing of heatmap images +- Export/sharing of heatmap images — no audience for personal use - Real-time updates — refresh on page load is fine +- Custom color theme picker — Kimai theme integration is sufficient +- Hour-of-day matrix — different visualization, scope creep +- Multi-user comparison — personal tracking tool ## Context -- Kimai is a self-hosted time tracking application built on Symfony -- Plugins are Symfony bundles, installable via composer -- The user runs Kimai for personal time tracking, not billable client work -- d3.js is the visualization library of choice for the heatmap -- Development follows TDD — tests written first to prevent AI-generated regressions -- Local development uses a Nix flake/devshell with a running Kimai instance and seeded test data -- The user's infra runs on NixOS +Shipped v1.0 with ~650 LOC source (PHP + TypeScript) and ~730 LOC tests. +Tech stack: Symfony bundle (PHP 8.2), d3.js v7 (TypeScript), esbuild, Vitest, PHPUnit. +Dev environment: Nix flake with process-compose, MariaDB 11.4, Kimai 2.52.0. ## Constraints @@ -54,28 +54,14 @@ At a glance, see where your time went — a visual map of tracking activity that | Decision | Rationale | Outcome | |----------|-----------|---------| -| d3.js for visualization | Flexible, well-suited for calendar heatmaps, user preference | — Pending | -| Symfony bundle (not local plugin) | Standard Kimai plugin distribution, composable | — Pending | -| Kimai theme colors (not custom) | Visual consistency with the rest of the dashboard | — Pending | -| TDD with PHPUnit + JS tests | Prevent regressions from AI-generated code | — Pending | -| Nix flake for dev environment | Reproducible local Kimai instance, matches user's NixOS infra | — Pending | - -## Evolution - -This document evolves at phase transitions and milestone boundaries. - -**After each phase transition** (via `/gsd-transition`): -1. Requirements invalidated? → Move to Out of Scope with reason -2. Requirements validated? → Move to Validated with phase reference -3. New requirements emerged? → Add to Active -4. Decisions to log? → Add to Key Decisions -5. "What This Is" still accurate? → Update if drifted - -**After each milestone** (via `/gsd-complete-milestone`): -1. Full review of all sections -2. Core Value check — still the right priority? -3. Audit Out of Scope — reasons still valid? -4. Update Context with current state +| d3.js for visualization | Flexible, well-suited for calendar heatmaps | ✓ Good | +| Symfony bundle (not local plugin) | Standard Kimai plugin distribution | ✓ Good | +| Kimai theme CSS vars | Visual consistency with dashboard | ⚠️ Partial — color scale hardcoded green (Tabler lacks green scale vars) | +| TDD with PHPUnit + Vitest | Prevent regressions from AI-generated code | ✓ Good — 30+ tests | +| Nix flake for dev environment | Reproducible local Kimai instance | ✓ Good | +| IIFE format with KimaiHeatmap global | Browser compat without Kimai's Webpack Encore | ✓ Good | +| Ship prebuilt JS in Resources/public/ | Avoids hooking into Kimai's build pipeline | ✓ Good | +| Descope activity filtering | Project filter sufficient for personal use | ✓ Acceptable | --- -*Last updated: 2026-04-08 after initialization* +*Last updated: 2026-04-08 after v1.0 milestone* diff --git a/.planning/RETROSPECTIVE.md b/.planning/RETROSPECTIVE.md new file mode 100644 index 0000000..a0dda17 --- /dev/null +++ b/.planning/RETROSPECTIVE.md @@ -0,0 +1,46 @@ +# Retrospective + +## Milestone: v1.0 — MVP + +**Shipped:** 2026-04-08 +**Phases:** 5 | **Plans:** 11 + +### What Was Built +- Nix devshell with process-compose orchestrating MariaDB + Kimai dev server +- HeatmapService with timezone-correct daily aggregation and PHPUnit tests +- d3.js calendar heatmap with color scale, labels, tooltips, Kimai theme integration +- Click-through navigation to Kimai timesheet + project filter dropdown +- Streak indicator, summary stats, weekend styling, week-start preference +- 30+ tests across PHPUnit and Vitest + +### What Worked +- Single-day delivery from zero to shipped plugin — tight phase scoping kept momentum +- TDD approach caught real issues (bootstrap paths, final class mocking) +- Process-compose made the multi-service dev environment ergonomic +- IIFE bundle strategy avoided Kimai's Webpack Encore complexity entirely + +### What Was Inefficient +- SUMMARY files not written for several phases (bookkeeping debt accumulated) +- REQUIREMENTS.md checkboxes fell out of sync (7/22 checked despite all shipped) +- Phase directory naming inconsistent (phase-2/, phase-3/ vs phases/01-dev-environment/, phases/04-heatmap-interaction/) + +### Patterns Established +- Ship prebuilt JS in Resources/public/ — don't hook into host app build pipeline +- kimai.initialized event + javascriptRequest fallback for widget lifecycle +- Callback injection pattern for cell interaction (onCellClick) +- Flex wrapper layout for widget + sidebar controls + +### Key Lessons +- Activity filtering was correctly descoped — project filter covers personal use +- Kimai's Tabler theme lacks green scale CSS vars, so hardcoded greens were pragmatic +- PHPUnit mocking requires non-final classes — caught early, would bite harder at scale + +## Cross-Milestone Trends + +| Metric | v1.0 | +|--------|------| +| Phases | 5 | +| Plans | 11 | +| Timeline | 1 day | +| Source LOC | ~650 | +| Test LOC | ~730 | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 09d5177..b92cf05 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1,113 +1,28 @@ # Roadmap: Kimai Heatmap Plugin -## Overview +## Milestones -Build a Kimai dashboard widget that displays a GitHub-style activity heatmap of time tracking data. The path goes: reproducible dev environment, then prove the plugin loads and serves data, then render the heatmap visualization, then add interaction (click-through, filtering), then polish with stats and visual refinements. Tests are written alongside each feature phase (TDD), not deferred. +- ✅ **v1.0 MVP** — Phases 1-5 (shipped 2026-04-08) — [archive](milestones/v1.0-ROADMAP.md) ## Phases -**Phase Numbering:** -- Integer phases (1, 2, 3): Planned milestone work -- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) +
+✅ v1.0 MVP (Phases 1-5) — SHIPPED 2026-04-08 -Decimal phases appear between their surrounding integers in numeric order. +- [x] Phase 1: Dev Environment (2/2 plans) — completed 2026-04-08 +- [x] Phase 2: Plugin Scaffold + Data Layer (2/2 plans) — completed 2026-04-08 +- [x] Phase 3: Core Heatmap Rendering (3/3 plans) — completed 2026-04-08 +- [x] Phase 4: Heatmap Interaction (2/2 plans) — completed 2026-04-08 +- [x] Phase 5: Polish (2/2 plans) — completed 2026-04-08 -- [x] **Phase 1: Dev Environment** - Nix flake with local Kimai instance and seeded test data -- [x] **Phase 2: Plugin Scaffold + Data Layer** - Symfony bundle, dashboard widget, aggregation API with PHPUnit tests -- [x] **Phase 3: Core Heatmap Rendering** - d3.js calendar grid with color mapping, labels, tooltips, theme integration, and JS tests -- [x] **Phase 4: Heatmap Interaction** - Click-through navigation, project/activity filtering, interaction tests (completed 2026-04-08) -- [x] **Phase 5: Polish** - Streak indicator, summary stats, weekend styling (completed 2026-04-08) - -## Phase Details - -### Phase 1: Dev Environment -**Goal**: Developer can run `nix develop` and have a working Kimai instance with test data, ready for plugin development -**Depends on**: Nothing (first phase) -**Requirements**: DEV-01, DEV-02, DEV-03 -**Success Criteria** (what must be TRUE): - 1. Running `nix develop` drops into a shell with PHP 8.2+, Composer, and Node available - 2. A local Kimai instance starts and is accessible in the browser - 3. The Kimai instance contains seeded time entry data spanning multiple months - 4. A plugin directory is symlinked/mounted so code changes are reflected without reinstallation -**Plans**: phase-1/PLAN.md - -Plans: -- [x] 01-01: Nix flake, process-compose, setup script, .gitignore -- [x] 01-02: Plugin scaffold + Kimai bootstrap verification - -### Phase 2: Plugin Scaffold + Data Layer -**Goal**: Plugin is recognized by Kimai, shows a widget on the dashboard, and serves aggregated daily time data via API -**Depends on**: Phase 1 -**Requirements**: PLUG-01, PLUG-02, PLUG-03, TEST-01, TEST-02 -**Success Criteria** (what must be TRUE): - 1. Kimai discovers the plugin and lists it in the plugin admin page - 2. A widget placeholder appears on the Kimai dashboard - 3. The API endpoint returns JSON with per-day aggregated hours and entry counts, correctly grouped by the user's timezone - 4. PHPUnit tests pass for the data aggregation service and API endpoint -**Plans**: phase-2/PLAN.md - -Plans: -- [x] 02-01: HeatmapService, PHPUnit tests, API controller -- [x] 02-02: Dashboard widget + template + DashboardSubscriber - -### Phase 3: Core Heatmap Rendering -**Goal**: The dashboard widget renders a fully styled d3.js calendar heatmap from live Kimai data -**Depends on**: Phase 2 -**Requirements**: HEAT-01, HEAT-02, HEAT-03, HEAT-04, HEAT-05, HEAT-06, HEAT-08, TEST-03 -**Success Criteria** (what must be TRUE): - 1. The widget displays a weeks-by-days calendar grid with cells colored by hours tracked (darker = more hours) - 2. Hovering any cell shows a tooltip with date, hours, and entry count - 3. Day-of-week labels appear on the Y-axis and month boundary labels along the top - 4. Days with no tracked time render with a distinct "no data" color - 5. Colors use Kimai's theme CSS variables (works with both light and dark themes) -**Plans**: phase-3/PLAN.md -**UI hint**: yes - -Plans: -- [x] 03-01: JS toolchain — npm, esbuild, TypeScript, Vitest -- [x] 03-02: d3 calendar heatmap with color scale, labels, tooltips + Vitest tests -- [x] 03-03: Twig template integration + asset serving - -### Phase 4: Heatmap Interaction -**Goal**: Users can click through to daily details and filter the heatmap by project or activity -**Depends on**: Phase 3 -**Requirements**: HEAT-07, INTR-01, TEST-04 -**Success Criteria** (what must be TRUE): - 1. Clicking a day cell navigates to Kimai's timesheet view filtered to that specific date - 2. A dropdown allows filtering the heatmap to show data for a single project or activity - 3. Filtering updates the heatmap in place without a full page reload - 4. JavaScript tests verify click navigation and tooltip interaction behavior -**Plans:** 2/2 plans complete -**UI hint**: yes - -Plans: -- [x] 04-01: Day cell click navigation + project filter dropdown -- [x] 04-02: Vitest tests for click navigation and filter behavior - -### Phase 5: Polish -**Goal**: The widget provides at-a-glance context beyond the heatmap itself -- streaks, stats, and visual cues for weekends -**Depends on**: Phase 4 -**Requirements**: POLI-01, POLI-02, POLI-03 -**Success Criteria** (what must be TRUE): - 1. A streak indicator shows the current number of consecutive days with tracked time - 2. A summary stats row displays total hours, average hours/day, and the busiest day - 3. Weekend days are visually distinct from weekdays (subtle border or opacity difference) -**Plans**: phase-5/PLAN.md -**UI hint**: yes - -Plans: -- [x] 05-01: Streak, stats, weekend styling, week-start preference -- [x] 05-02: Tests for polish features +
## Progress -**Execution Order:** -Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 - -| Phase | Plans Complete | Status | Completed | -|-------|----------------|--------|-----------| -| 1. Dev Environment | 2/2 | Done | 2026-04-08 | -| 2. Plugin Scaffold + Data Layer | 2/2 | Done | 2026-04-08 | -| 3. Core Heatmap Rendering | 3/3 | Done | 2026-04-08 | -| 4. Heatmap Interaction | 2/2 | Complete | 2026-04-08 | -| 5. Polish | 2/2 | Done | 2026-04-08 | +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 1. Dev Environment | v1.0 | 2/2 | Complete | 2026-04-08 | +| 2. Plugin Scaffold + Data Layer | v1.0 | 2/2 | Complete | 2026-04-08 | +| 3. Core Heatmap Rendering | v1.0 | 3/3 | 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 | diff --git a/.planning/STATE.md b/.planning/STATE.md index f3609dc..a60f3f2 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,17 +1,17 @@ --- gsd_state_version: 1.0 milestone: v1.0 -milestone_name: milestone -status: verifying -stopped_at: Completed 04-02-PLAN.md -last_updated: "2026-04-08T13:35:22.113Z" +milestone_name: MVP +status: shipped +stopped_at: Milestone v1.0 complete +last_updated: "2026-04-08T21:30:00.000Z" last_activity: 2026-04-08 progress: total_phases: 5 - completed_phases: 1 - total_plans: 4 - completed_plans: 3 - percent: 75 + completed_phases: 5 + total_plans: 11 + completed_plans: 11 + percent: 100 --- # Project State @@ -21,75 +21,19 @@ progress: See: .planning/PROJECT.md (updated 2026-04-08) **Core value:** At a glance, see where your time went -- a visual map of tracking activity that makes patterns obvious -**Current focus:** Phase 4 — Heatmap Interaction +**Current focus:** Planning next milestone ## Current Position -Phase: 4 (Heatmap Interaction) — EXECUTING -Plan: 2 of 2 -Status: Phase complete — ready for verification +Phase: All v1.0 phases complete +Plan: N/A +Status: v1.0 shipped Last activity: 2026-04-08 -Progress: [██░░░░░░░░] 20% - -## Performance Metrics - -**Velocity:** - -- Total plans completed: 2 -- Average duration: ~15min -- Total execution time: ~30min - -**By Phase:** - -| Phase | Plans | Total | Avg/Plan | -|-------|-------|-------|----------| -| Phase 1 | 2 | ~30min | ~15min | - -**Recent Trend:** - -- Last 5 plans: - -- Trend: - - -*Updated after each plan completion* -| Phase 2 P02-01 | 5min | 5 tasks | 8 files | -| Phase 03 P01 | 3min | 4 tasks | 7 files | -| Phase 03 P03 | 2min | 4 tasks | 1 files | -| Phase 04 P01 | 2min | 2 tasks | 7 files | -| Phase 04 P02 | 1min | 2 tasks | 2 files | - -## Accumulated Context - -### Decisions - -Decisions are logged in PROJECT.md Key Decisions table. -Recent decisions affecting current work: - -- [Roadmap]: TDD approach -- tests written alongside features, not in a separate phase -- [Roadmap]: Nix dev environment is Phase 1 prerequisite; everything else depends on it -- [Research]: Use Vitest + jsdom for JS tests (d3 v7 is ESM-only, Jest struggles) -- [Research]: Ship prebuilt JS in Resources/public/, do not hook into Kimai's Webpack Encore -- [Phase 1]: x86_64-linux only for Nix flake (NixOS-only project) -- [Phase 1]: MariaDB 11.4.9 on port 3307, Kimai on port 8010 -- [Phase 1]: Plugin needs DI extension + services.yaml to register as tagged PluginInterface service -- [Phase 1]: Kimai's autoloader needs exclude-from-classmap for recursive plugin symlink -- [Phase 1]: PHP memory_limit=1G required for Kimai cache warmup -- [Phase 1]: process-compose API server disabled (-p 0) to avoid port 8080 conflict -- [Phase 2]: Removed final from HeatmapService to allow PHPUnit mocking -- [Phase 03]: IIFE format with KimaiHeatmap global for browser compat -- [Phase 03]: kimai.initialized event for deferred widget init, javascriptRequest fallback for AJAX -- [Phase 04]: Render project filter only when user has tracked projects - -### Pending Todos - -None yet. - -### Blockers/Concerns - -- [Research]: Kimai widget API (WidgetInterface, DI tags) needs verification against target Kimai version +Progress: [██████████] 100% ## Session Continuity -Last session: 2026-04-08T13:35:22.111Z -Stopped at: Completed 04-02-PLAN.md +Last session: 2026-04-08 +Stopped at: Milestone v1.0 complete Resume file: None diff --git a/.planning/v1.0-MILESTONE-AUDIT.md b/.planning/milestones/v1.0-MILESTONE-AUDIT.md similarity index 100% rename from .planning/v1.0-MILESTONE-AUDIT.md rename to .planning/milestones/v1.0-MILESTONE-AUDIT.md diff --git a/.planning/REQUIREMENTS.md b/.planning/milestones/v1.0-REQUIREMENTS.md similarity index 96% rename from .planning/REQUIREMENTS.md rename to .planning/milestones/v1.0-REQUIREMENTS.md index 4cb0ec4..e3a6a67 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/milestones/v1.0-REQUIREMENTS.md @@ -1,3 +1,12 @@ +# Requirements Archive: v1.0 MVP + +**Archived:** 2026-04-08 +**Status:** SHIPPED + +For current requirements, see `.planning/REQUIREMENTS.md`. + +--- + # Requirements: Kimai Heatmap Plugin **Defined:** 2026-04-08 diff --git a/.planning/milestones/v1.0-ROADMAP.md b/.planning/milestones/v1.0-ROADMAP.md new file mode 100644 index 0000000..09d5177 --- /dev/null +++ b/.planning/milestones/v1.0-ROADMAP.md @@ -0,0 +1,113 @@ +# Roadmap: Kimai Heatmap Plugin + +## Overview + +Build a Kimai dashboard widget that displays a GitHub-style activity heatmap of time tracking data. The path goes: reproducible dev environment, then prove the plugin loads and serves data, then render the heatmap visualization, then add interaction (click-through, filtering), then polish with stats and visual refinements. Tests are written alongside each feature phase (TDD), not deferred. + +## Phases + +**Phase Numbering:** +- Integer phases (1, 2, 3): Planned milestone work +- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED) + +Decimal phases appear between their surrounding integers in numeric order. + +- [x] **Phase 1: Dev Environment** - Nix flake with local Kimai instance and seeded test data +- [x] **Phase 2: Plugin Scaffold + Data Layer** - Symfony bundle, dashboard widget, aggregation API with PHPUnit tests +- [x] **Phase 3: Core Heatmap Rendering** - d3.js calendar grid with color mapping, labels, tooltips, theme integration, and JS tests +- [x] **Phase 4: Heatmap Interaction** - Click-through navigation, project/activity filtering, interaction tests (completed 2026-04-08) +- [x] **Phase 5: Polish** - Streak indicator, summary stats, weekend styling (completed 2026-04-08) + +## Phase Details + +### Phase 1: Dev Environment +**Goal**: Developer can run `nix develop` and have a working Kimai instance with test data, ready for plugin development +**Depends on**: Nothing (first phase) +**Requirements**: DEV-01, DEV-02, DEV-03 +**Success Criteria** (what must be TRUE): + 1. Running `nix develop` drops into a shell with PHP 8.2+, Composer, and Node available + 2. A local Kimai instance starts and is accessible in the browser + 3. The Kimai instance contains seeded time entry data spanning multiple months + 4. A plugin directory is symlinked/mounted so code changes are reflected without reinstallation +**Plans**: phase-1/PLAN.md + +Plans: +- [x] 01-01: Nix flake, process-compose, setup script, .gitignore +- [x] 01-02: Plugin scaffold + Kimai bootstrap verification + +### Phase 2: Plugin Scaffold + Data Layer +**Goal**: Plugin is recognized by Kimai, shows a widget on the dashboard, and serves aggregated daily time data via API +**Depends on**: Phase 1 +**Requirements**: PLUG-01, PLUG-02, PLUG-03, TEST-01, TEST-02 +**Success Criteria** (what must be TRUE): + 1. Kimai discovers the plugin and lists it in the plugin admin page + 2. A widget placeholder appears on the Kimai dashboard + 3. The API endpoint returns JSON with per-day aggregated hours and entry counts, correctly grouped by the user's timezone + 4. PHPUnit tests pass for the data aggregation service and API endpoint +**Plans**: phase-2/PLAN.md + +Plans: +- [x] 02-01: HeatmapService, PHPUnit tests, API controller +- [x] 02-02: Dashboard widget + template + DashboardSubscriber + +### Phase 3: Core Heatmap Rendering +**Goal**: The dashboard widget renders a fully styled d3.js calendar heatmap from live Kimai data +**Depends on**: Phase 2 +**Requirements**: HEAT-01, HEAT-02, HEAT-03, HEAT-04, HEAT-05, HEAT-06, HEAT-08, TEST-03 +**Success Criteria** (what must be TRUE): + 1. The widget displays a weeks-by-days calendar grid with cells colored by hours tracked (darker = more hours) + 2. Hovering any cell shows a tooltip with date, hours, and entry count + 3. Day-of-week labels appear on the Y-axis and month boundary labels along the top + 4. Days with no tracked time render with a distinct "no data" color + 5. Colors use Kimai's theme CSS variables (works with both light and dark themes) +**Plans**: phase-3/PLAN.md +**UI hint**: yes + +Plans: +- [x] 03-01: JS toolchain — npm, esbuild, TypeScript, Vitest +- [x] 03-02: d3 calendar heatmap with color scale, labels, tooltips + Vitest tests +- [x] 03-03: Twig template integration + asset serving + +### Phase 4: Heatmap Interaction +**Goal**: Users can click through to daily details and filter the heatmap by project or activity +**Depends on**: Phase 3 +**Requirements**: HEAT-07, INTR-01, TEST-04 +**Success Criteria** (what must be TRUE): + 1. Clicking a day cell navigates to Kimai's timesheet view filtered to that specific date + 2. A dropdown allows filtering the heatmap to show data for a single project or activity + 3. Filtering updates the heatmap in place without a full page reload + 4. JavaScript tests verify click navigation and tooltip interaction behavior +**Plans:** 2/2 plans complete +**UI hint**: yes + +Plans: +- [x] 04-01: Day cell click navigation + project filter dropdown +- [x] 04-02: Vitest tests for click navigation and filter behavior + +### Phase 5: Polish +**Goal**: The widget provides at-a-glance context beyond the heatmap itself -- streaks, stats, and visual cues for weekends +**Depends on**: Phase 4 +**Requirements**: POLI-01, POLI-02, POLI-03 +**Success Criteria** (what must be TRUE): + 1. A streak indicator shows the current number of consecutive days with tracked time + 2. A summary stats row displays total hours, average hours/day, and the busiest day + 3. Weekend days are visually distinct from weekdays (subtle border or opacity difference) +**Plans**: phase-5/PLAN.md +**UI hint**: yes + +Plans: +- [x] 05-01: Streak, stats, weekend styling, week-start preference +- [x] 05-02: Tests for polish features + +## Progress + +**Execution Order:** +Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 + +| Phase | Plans Complete | Status | Completed | +|-------|----------------|--------|-----------| +| 1. Dev Environment | 2/2 | Done | 2026-04-08 | +| 2. Plugin Scaffold + Data Layer | 2/2 | Done | 2026-04-08 | +| 3. Core Heatmap Rendering | 3/3 | Done | 2026-04-08 | +| 4. Heatmap Interaction | 2/2 | Complete | 2026-04-08 | +| 5. Polish | 2/2 | Done | 2026-04-08 | diff --git a/.planning/phases/01-dev-environment/01-01-PLAN.md b/.planning/milestones/v1.0-phases/01-dev-environment/01-01-PLAN.md similarity index 100% rename from .planning/phases/01-dev-environment/01-01-PLAN.md rename to .planning/milestones/v1.0-phases/01-dev-environment/01-01-PLAN.md diff --git a/.planning/phases/01-dev-environment/01-01-SUMMARY.md b/.planning/milestones/v1.0-phases/01-dev-environment/01-01-SUMMARY.md similarity index 100% rename from .planning/phases/01-dev-environment/01-01-SUMMARY.md rename to .planning/milestones/v1.0-phases/01-dev-environment/01-01-SUMMARY.md diff --git a/.planning/phases/01-dev-environment/01-02-PLAN.md b/.planning/milestones/v1.0-phases/01-dev-environment/01-02-PLAN.md similarity index 100% rename from .planning/phases/01-dev-environment/01-02-PLAN.md rename to .planning/milestones/v1.0-phases/01-dev-environment/01-02-PLAN.md diff --git a/.planning/phases/04-heatmap-interaction/04-01-PLAN.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-01-PLAN.md similarity index 100% rename from .planning/phases/04-heatmap-interaction/04-01-PLAN.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-01-PLAN.md diff --git a/.planning/phases/04-heatmap-interaction/04-01-SUMMARY.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-01-SUMMARY.md similarity index 100% rename from .planning/phases/04-heatmap-interaction/04-01-SUMMARY.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-01-SUMMARY.md diff --git a/.planning/phases/04-heatmap-interaction/04-02-PLAN.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-02-PLAN.md similarity index 100% rename from .planning/phases/04-heatmap-interaction/04-02-PLAN.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-02-PLAN.md diff --git a/.planning/phases/04-heatmap-interaction/04-02-SUMMARY.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-02-SUMMARY.md similarity index 100% rename from .planning/phases/04-heatmap-interaction/04-02-SUMMARY.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-02-SUMMARY.md diff --git a/.planning/phases/phase-4/04-CONTEXT.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-CONTEXT.md similarity index 100% rename from .planning/phases/phase-4/04-CONTEXT.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-CONTEXT.md diff --git a/.planning/phases/04-heatmap-interaction/04-RESEARCH.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-RESEARCH.md similarity index 100% rename from .planning/phases/04-heatmap-interaction/04-RESEARCH.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-RESEARCH.md diff --git a/.planning/phases/phase-4/04-UI-SPEC.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-UI-SPEC.md similarity index 100% rename from .planning/phases/phase-4/04-UI-SPEC.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-UI-SPEC.md diff --git a/.planning/phases/04-heatmap-interaction/04-VALIDATION.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-VALIDATION.md similarity index 100% rename from .planning/phases/04-heatmap-interaction/04-VALIDATION.md rename to .planning/milestones/v1.0-phases/04-heatmap-interaction/04-VALIDATION.md diff --git a/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-VERIFICATION.md b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-VERIFICATION.md new file mode 100644 index 0000000..ec133e7 --- /dev/null +++ b/.planning/milestones/v1.0-phases/04-heatmap-interaction/04-VERIFICATION.md @@ -0,0 +1,142 @@ +--- +phase: 04-heatmap-interaction +verified: 2026-04-08T15:37:00Z +status: human_needed +score: 3/4 must-haves verified +gaps: + - truth: "A dropdown allows filtering the heatmap to show data for a single project or activity" + status: partial + reason: "Only project filtering is implemented. Activity filtering was explicitly descoped in 04-CONTEXT.md ('Projects only -- no activity filter') but the roadmap SC and INTR-01 both specify 'project or activity'. No later phase addresses this." + artifacts: + - path: "assets/src/heatmap.ts" + issue: "No activity filter option in dropdown, no activity query param in fetch" + - path: "Service/HeatmapService.php" + issue: "No getUserActivities() method" + - path: "Controller/HeatmapController.php" + issue: "No ?activity= query parameter handling" + missing: + - "getUserActivities() service method analogous to getUserProjects()" + - "Activity filter option in dropdown (or combined project/activity dropdown)" + - "Controller support for ?activity=N query parameter" + - "HeatmapService::getDailyAggregation() support for activityId parameter" +human_verification: + - test: "Click a heatmap day cell and verify navigation to Kimai timesheet filtered to that date" + expected: "Browser navigates to /en/timesheet/?daterange=YYYY-MM-DD%20-%20YYYY-MM-DD" + why_human: "Requires running Kimai instance with the plugin loaded to verify URL construction and Kimai's timesheet page accepting the daterange parameter" + - test: "Select a project from the filter dropdown and verify heatmap re-renders" + expected: "Heatmap cells update to show only data for the selected project, color scale adjusts" + why_human: "Requires running Kimai with real time entries across multiple projects to verify visual filtering" + - test: "Verify filter dropdown appearance and positioning" + expected: "Dropdown appears to the right of the heatmap SVG, styled with Tabler form-select classes, responsive layout" + why_human: "Visual layout verification requires browser rendering" +--- + +# Phase 4: Heatmap Interaction Verification Report + +**Phase Goal:** Users can click through to daily details and filter the heatmap by project or activity +**Verified:** 2026-04-08T15:37:00Z +**Status:** human_needed +**Re-verification:** No -- initial verification + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | Clicking a day cell navigates to Kimai's timesheet view filtered to that specific date | VERIFIED | `heatmap.ts` lines 217-224: `onCellClick` constructs URL with `daterange` param and assigns to `window.location.href`. Tests in `interaction.test.ts` verify URL format. | +| 2 | A dropdown allows filtering the heatmap to show data for a single project or activity | PARTIAL | Project filtering fully implemented (dropdown, fetch with `?project=N`, re-render). Activity filtering intentionally omitted per 04-CONTEXT.md line 25. Roadmap SC and INTR-01 both specify "or activity". | +| 3 | Filtering updates the heatmap in place without a full page reload | VERIFIED | `heatmap.ts` lines 256-271: `select.addEventListener('change', ...)` triggers `fetch()` then `renderHeatmap()` on the existing `svgArea` div. Tests in `filter.test.ts` verify re-render. | +| 4 | JavaScript tests verify click navigation and tooltip interaction behavior | VERIFIED | `interaction.test.ts` has 7 tests (callback, URL format, project in URL, fallback). `filter.test.ts` has 9 tests (dropdown rendering, fetch params, re-render, empty state). All 52 tests pass. | + +**Score:** 3/4 truths fully verified (1 partial) + +### Required Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `Service/HeatmapService.php` | getUserProjects() method | VERIFIED | Lines 54-68: queries distinct projects with IDENTITY join, returns `[{id, name}]` | +| `Widget/HeatmapWidget.php` | Injects service, getData returns projects | VERIFIED | Lines 11-12: constructor injection. Lines 40-47: getData returns projects array. No `final` keyword. | +| `Resources/views/widget/heatmap.html.twig` | data-projects and data-timesheet-url attributes | VERIFIED | Lines 9-10: both data attributes present with correct Twig expressions | +| `assets/src/heatmap.ts` | Click handler, filter dropdown, re-fetch | VERIFIED | Lines 198-201: click handler. Lines 236-276: filter dropdown. Lines 256-271: re-fetch on change. | +| `assets/src/types.ts` | ProjectOption interface | VERIFIED | Lines 22-25: `export interface ProjectOption { id: number; name: string; }` | +| `Resources/public/heatmap.css` | Click affordance, flex layout | VERIFIED | Lines 15-21: cursor:pointer, transition, pointer-events. Lines 38-58: flex wrapper layout. | +| `Resources/public/heatmap.js` | Rebuilt bundle | VERIFIED | 81,943 bytes, contains KimaiHeatmap IIFE | +| `assets/test/interaction.test.ts` | Click navigation tests | VERIFIED | 7 tests covering onCellClick callback, URL construction, project filter in URL | +| `assets/test/filter.test.ts` | Filter dropdown tests | VERIFIED | 9 tests covering rendering, options, fetch params, re-render, empty state | + +### Key Link Verification + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `heatmap.ts` | Kimai timesheet | `window.location.href` with daterange | WIRED | Line 223: `window.location.href = url` with `encodeURIComponent(daterange)` | +| `heatmap.ts` | HeatmapController::data() | `fetch` with `?project=N` | WIRED | Line 259: `fetch(fetchUrl)` where `fetchUrl = baseUrl?project=val` | +| `HeatmapWidget.php` | `HeatmapService::getUserProjects()` | getData() calls service | WIRED | Line 45: `$this->service->getUserProjects($user)` | +| `interaction.test.ts` | `heatmap.ts` | imports renderHeatmap and init | WIRED | Line 3: `import { renderHeatmap, init } from '../src/heatmap'` | +| `filter.test.ts` | `heatmap.ts` | imports init | WIRED | Line 2: `import { init } from '../src/heatmap'` | + +### Data-Flow Trace (Level 4) + +| Artifact | Data Variable | Source | Produces Real Data | Status | +|----------|---------------|--------|--------------------|--------| +| `heatmap.ts` (init) | projects | `data-projects` attr from Twig | Server-rendered from DB query via getUserProjects() | FLOWING | +| `heatmap.ts` (init) | HeatmapData | `fetch(baseUrl)` | API endpoint queries TimesheetRepository | FLOWING | +| `heatmap.ts` (filter) | HeatmapData | `fetch(baseUrl?project=N)` | API endpoint filters by projectId | FLOWING | + +### Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +|----------|---------|--------|--------| +| All tests pass | `npx vitest run` | 52 passed (26 unique + symlink dupes), 0 failed | PASS | +| Bundle builds | `ls Resources/public/heatmap.js` | 81,943 bytes | PASS | +| No TODOs/FIXMEs in phase code | grep across modified files | No matches | PASS | + +### Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|------------|-------------|--------|----------| +| HEAT-07 | 04-01 | Clicking a day cell navigates to Kimai timesheet view filtered to that date | SATISFIED | `heatmap.ts` onCellClick with window.location.href, daterange param | +| INTR-01 | 04-01 | Dropdown filter to view heatmap for a specific project or activity | PARTIAL | Project filtering implemented. Activity filtering omitted (intentional descope in 04-CONTEXT.md but not reflected in requirement text). | +| TEST-04 | 04-02 | JavaScript tests for tooltip and click interaction behavior | SATISFIED | interaction.test.ts (7 tests) + filter.test.ts (9 tests) all passing | + +### Anti-Patterns Found + +| File | Line | Pattern | Severity | Impact | +|------|------|---------|----------|--------| +| None found | - | - | - | - | + +### Human Verification Required + +### 1. Click Navigation End-to-End + +**Test:** Click a heatmap day cell in a running Kimai instance +**Expected:** Browser navigates to `/en/timesheet/?daterange=YYYY-MM-DD%20-%20YYYY-MM-DD` showing time entries for that date +**Why human:** Requires running Kimai to verify the timesheet page accepts and applies the daterange parameter correctly + +### 2. Project Filter Visual Behavior + +**Test:** Select a project from the filter dropdown with multiple projects having time data +**Expected:** Heatmap re-renders showing only that project's data, color scale recalculates, cells update +**Why human:** Requires running Kimai with real multi-project time data to verify visual filtering + +### 3. Layout and Styling + +**Test:** View the widget on the Kimai dashboard +**Expected:** Filter dropdown appears to the right of the heatmap, cells show pointer cursor on hover with opacity change, layout is clean +**Why human:** Visual layout verification requires browser rendering with Tabler CSS + +### Gaps Summary + +One partial gap identified: the roadmap success criteria and INTR-01 both specify filtering by "project **or activity**" but only project filtering was implemented. This was an intentional design decision documented in 04-CONTEXT.md ("Projects only -- no activity filter"), but it represents a deviation from the stated roadmap contract. No later phase addresses activity filtering. + +The gap is flagged as partial rather than failed because: +- Project filtering is fully functional and tested +- The descope was a conscious decision, not an oversight +- The core interaction value (filtering the heatmap) is delivered for the primary dimension (projects) + +All other success criteria are fully met with solid test coverage (16 new tests). + +--- + +_Verified: 2026-04-08T15:37:00Z_ +_Verifier: Claude (gsd-verifier)_ diff --git a/.planning/phases/05-polish/05-01-PLAN.md b/.planning/milestones/v1.0-phases/05-polish/05-01-PLAN.md similarity index 100% rename from .planning/phases/05-polish/05-01-PLAN.md rename to .planning/milestones/v1.0-phases/05-polish/05-01-PLAN.md diff --git a/.planning/phases/05-polish/05-02-PLAN.md b/.planning/milestones/v1.0-phases/05-polish/05-02-PLAN.md similarity index 100% rename from .planning/phases/05-polish/05-02-PLAN.md rename to .planning/milestones/v1.0-phases/05-polish/05-02-PLAN.md diff --git a/.planning/phases/05-polish/05-CONTEXT.md b/.planning/milestones/v1.0-phases/05-polish/05-CONTEXT.md similarity index 100% rename from .planning/phases/05-polish/05-CONTEXT.md rename to .planning/milestones/v1.0-phases/05-polish/05-CONTEXT.md diff --git a/.planning/milestones/v1.0-phases/phase-1/PLAN.md b/.planning/milestones/v1.0-phases/phase-1/PLAN.md new file mode 100644 index 0000000..69749be --- /dev/null +++ b/.planning/milestones/v1.0-phases/phase-1/PLAN.md @@ -0,0 +1,230 @@ +# Phase 1: Dev Environment — Plan + +**Goal:** Developer can run `nix develop` and have a working Kimai instance with test data, ready for plugin development. +**Requirements:** DEV-01, DEV-02, DEV-03 +**Research:** phase-1/RESEARCH.md + +## Success Criteria + +1. `nix develop` drops into a shell with PHP 8.2+, Composer, and Node available +2. A local Kimai instance starts and is accessible in the browser +3. The Kimai instance contains seeded time entry data spanning multiple months +4. A plugin directory is symlinked so code changes are reflected without reinstallation + +## Waves + +### Wave 1: Nix Flake + Infrastructure Scripts (autonomous) + +#### Plan 01-01: Nix Flake and Dev Infrastructure + +**Objective:** Create a reproducible dev environment with all dependencies and a one-command bootstrap. + +**Task 1: Create Nix flake and devshell** + +Create `flake.nix` at project root: +- Input: `nixpkgs` (use `github:NixOS/nixpkgs/nixpkgs-unstable`) +- DevShell packages: + - PHP 8.2 with extensions: `gd`, `intl`, `mbstring`, `pdo_mysql`, `xml`, `xsl`, `zip`, `tokenizer` — use `php82.buildEnv { extensions = { enabled, all }: enabled ++ (with all; [ xsl pdo_mysql ]); }` + - `php82Packages.composer` + - `nodejs_22` + - `mariadb` (11.4.x) + - `symfony-cli` + - `process-compose` +- Shell hook: print a message pointing to `dev/setup.sh` for first-time setup and `process-compose -f dev/process-compose.yaml up` for starting the stack + +Create `.envrc` with `use flake` for direnv integration. + +**Task 2: Create process-compose config** + +Create `dev/process-compose.yaml`: +- `mariadb` process: + - Command: `mysqld --datadir=$PWD/dev/.mariadb-data --socket=$PWD/dev/.mariadb.sock --port=3307 --skip-grant-tables --skip-networking=0 --bind-address=127.0.0.1` + - Readiness probe: `mysqladmin --socket=$PWD/dev/.mariadb.sock ping` (initial delay 2s, period 1s) +- `kimai` process: + - Command: `symfony server:start --no-tls --dir=$PWD/dev/kimai --port=8010` + - Depends on: `mariadb` (condition: process_healthy) + +**Task 3: Create setup script** + +Create `dev/setup.sh` (executable): +```bash +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +KIMAI_DIR="$SCRIPT_DIR/kimai" +DATA_DIR="$SCRIPT_DIR/.mariadb-data" + +# Step 1: Initialize MariaDB data directory +if [ ! -d "$DATA_DIR" ]; then + echo "==> Initializing MariaDB data directory..." + mysql_install_db --datadir="$DATA_DIR" --auth-root-authentication-method=normal +fi + +# Step 2: Start MariaDB temporarily for setup +echo "==> Starting MariaDB for setup..." +mysqld --datadir="$DATA_DIR" --socket="$SCRIPT_DIR/.mariadb.sock" --port=3307 --skip-grant-tables --skip-networking=0 --bind-address=127.0.0.1 & +MARIADB_PID=$! +trap "kill $MARIADB_PID 2>/dev/null || true" EXIT + +# Wait for MariaDB to be ready +for i in $(seq 1 30); do + if mysqladmin --socket="$SCRIPT_DIR/.mariadb.sock" ping 2>/dev/null; then + break + fi + sleep 1 +done + +# Create database +mysql --socket="$SCRIPT_DIR/.mariadb.sock" -e "CREATE DATABASE IF NOT EXISTS kimai;" + +# Step 3: Clone Kimai +if [ ! -d "$KIMAI_DIR" ]; then + echo "==> Cloning Kimai 2.52.0..." + git clone -b 2.52.0 --depth 1 https://github.com/kimai/kimai.git "$KIMAI_DIR" +fi + +# Step 4: Configure Kimai +cat > "$KIMAI_DIR/.env.local" < Installing Composer dependencies..." +cd "$KIMAI_DIR" +composer install --no-interaction + +# Step 6: Install Kimai (creates schema, runs migrations) +echo "==> Installing Kimai..." +bin/console kimai:install -n + +# Step 7: Load test fixtures +echo "==> Loading test fixtures..." +bin/console kimai:reset:dev -n + +# Step 8: Symlink plugin +echo "==> Symlinking plugin..." +mkdir -p var/plugins +ln -sfn "$PROJECT_DIR" var/plugins/KimaiHeatmapBundle + +# Step 9: Clear cache +bin/console cache:clear + +echo "" +echo "==> Setup complete!" +echo " Run: process-compose -f dev/process-compose.yaml up" +echo " Then open: http://127.0.0.1:8010" +echo " Login: susan_super / password" +``` + +**Task 4: Create .gitignore** + +Create/update `.gitignore`: +``` +dev/kimai/ +dev/.mariadb-data/ +dev/.mariadb.sock +dev/.mariadb.sock.lock +node_modules/ +vendor/ +.direnv/ +``` + +**Commit:** `feat: add nix flake, process-compose, and setup script for dev environment` + +**Verification:** +- `nix develop --command bash -c "php --version && composer --version && node --version"` succeeds +- `nix develop --command bash -c "which mariadb && which symfony && which process-compose"` succeeds + +--- + +### Wave 2: Kimai Instance + Plugin Scaffold (checkpoint required) + +#### Plan 01-02: Bootstrap Kimai and Create Plugin Scaffold + +**Objective:** Get Kimai running with test data and the plugin recognized. + +**Task 1: Create minimal plugin scaffold** + +Create these files at project root (the project root IS the plugin bundle): + +`KimaiHeatmapBundle.php`: +```php + 0 rows +- [ ] Plugin appears in Kimai admin > Plugins page + +## Requirement Coverage + +| Requirement | Plan | Verified By | +|-------------|------|-------------| +| DEV-01 | 01-01 | `nix develop` provides PHP 8.2+, Composer, Node | +| DEV-02 | 01-02 | `kimai:reset:dev` loads fixtures; SQL count check | +| DEV-03 | 01-02 | `kimai:plugins` lists the plugin; plugin visible in admin | + +## Risks + +| Risk | Mitigation | +|------|------------| +| MariaDB local datadir doesn't work | Fall back to `--socket` only or use `mysql_install_db --user=$(whoami)` | +| PHP extension missing in Nix | Add explicit extensions to `php.buildEnv`; iterate based on `composer install` errors | +| Kimai 2.52.0 incompatible with MariaDB 11.4 | Try `mariadb_1011` (10.11.x) from nixpkgs as fallback | +| `kimai:reset:dev` fixture count too low | Acceptable for dev — supplement with API calls later if needed | + +## Notes + +- Timebox this phase to 1 day. If Nix+MariaDB setup takes longer, consider Docker fallback. +- The plugin scaffold is intentionally minimal — just enough for Kimai to recognize it. Real functionality comes in Phase 2. +- `APP_ENV=dev` enables Symfony's auto-recompilation, so cache:clear is only needed after initial plugin symlink. + +--- +*Plan created: 2026-04-08* diff --git a/.planning/phase-1/RESEARCH.md b/.planning/milestones/v1.0-phases/phase-1/RESEARCH.md similarity index 100% rename from .planning/phase-1/RESEARCH.md rename to .planning/milestones/v1.0-phases/phase-1/RESEARCH.md diff --git a/.planning/phase-2/02-01-SUMMARY.md b/.planning/milestones/v1.0-phases/phase-2/02-01-SUMMARY.md similarity index 100% rename from .planning/phase-2/02-01-SUMMARY.md rename to .planning/milestones/v1.0-phases/phase-2/02-01-SUMMARY.md diff --git a/.planning/phase-2/02-RESEARCH.md b/.planning/milestones/v1.0-phases/phase-2/02-RESEARCH.md similarity index 100% rename from .planning/phase-2/02-RESEARCH.md rename to .planning/milestones/v1.0-phases/phase-2/02-RESEARCH.md diff --git a/.planning/milestones/v1.0-phases/phase-2/PLAN.md b/.planning/milestones/v1.0-phases/phase-2/PLAN.md new file mode 100644 index 0000000..63f722c --- /dev/null +++ b/.planning/milestones/v1.0-phases/phase-2/PLAN.md @@ -0,0 +1,486 @@ +# Phase 2: Plugin Scaffold + Data Layer — Plan + +**Goal:** Plugin shows a widget on the dashboard and serves aggregated daily time data via API. +**Requirements:** PLUG-01 (done), PLUG-02, PLUG-03, TEST-01, TEST-02 +**Research:** phase-2/02-RESEARCH.md + +## Success Criteria + +1. Kimai discovers the plugin and lists it in the plugin admin page (DONE — Phase 1) +2. A widget placeholder appears on the Kimai dashboard +3. The API endpoint returns JSON with per-day aggregated hours and entry counts, correctly grouped by the user's timezone +4. PHPUnit tests pass for the data aggregation service and API endpoint + +## Key Decisions from Research + +- Extend `AbstractWidget` (not `AbstractWidgetType`) — same pattern as `DailyWorkingTimeChart` +- Query `t.date` (the `date_tz` column) for timezone-correct day grouping — no manual conversion needed +- Simple `JsonResponse` controller, NOT FOS Rest Bundle — cleaner for plugin +- Unit tests with mocked repository for Phase 2 — integration tests deferred +- `DashboardSubscriber` needed for widget to appear on default dashboard + +## Waves + +### Wave 1: Data Layer + Tests (autonomous) + +#### Plan 02-01: HeatmapService, PHPUnit Tests, and API Endpoint + +**Objective:** Build the data aggregation service with tests, then the API controller. + +**Task 1: Update services.yaml for autowiring** + +Update `Resources/config/services.yaml` to enable autowiring and autoconfigure for the plugin namespace: + +```yaml +services: + _defaults: + autowire: true + autoconfigure: true + + KimaiPlugin\KimaiHeatmapBundle\: + resource: '../../{Controller,Service,Widget,EventSubscriber}/' + + KimaiPlugin\KimaiHeatmapBundle\KimaiHeatmapBundle: + tags: ['App\Plugin\PluginInterface'] +``` + +**Task 2: Create HeatmapService** + +Create `Service/HeatmapService.php`: + +```php + + */ + public function getDailyAggregation(User $user, \DateTimeInterface $begin, \DateTimeInterface $end, ?int $projectId = null): array + { + $qb = $this->repository->createQueryBuilder('t'); + + $qb + ->select('COALESCE(SUM(t.duration), 0) as duration') + ->addSelect('COUNT(t.id) as count') + ->addSelect('DATE(t.date) as day') + ->andWhere($qb->expr()->between('t.date', ':begin', ':end')) + ->andWhere($qb->expr()->eq('t.user', ':user')) + ->andWhere($qb->expr()->isNotNull('t.end')) + ->setParameter('begin', $begin->format('Y-m-d')) + ->setParameter('end', $end->format('Y-m-d')) + ->setParameter('user', $user) + ->groupBy('day') + ->orderBy('day', 'ASC') + ; + + if ($projectId !== null) { + $qb->andWhere($qb->expr()->eq('t.project', ':project')) + ->setParameter('project', $projectId); + } + + $results = $qb->getQuery()->getResult(); + + return array_map(function (array $row) { + return [ + 'date' => $row['day'], + 'hours' => round((int) $row['duration'] / 3600, 2), + 'count' => (int) $row['count'], + ]; + }, $results); + } +} +``` + +**Task 3: Create PHPUnit config and tests** + +Create `Tests/phpunit.xml`: +```xml + + + + + . + + + +``` + +Create `Tests/bootstrap.php`: +```php + '2026-04-01', 'duration' => 7200, 'count' => 3], + ['day' => '2026-04-02', 'duration' => 3600, 'count' => 1], + ]; + + $service = $this->createServiceWithResults($mockResults); + $result = $service->getDailyAggregation( + $this->createMock(User::class), + new \DateTimeImmutable('2026-04-01'), + new \DateTimeImmutable('2026-04-30') + ); + + $this->assertCount(2, $result); + $this->assertEquals('2026-04-01', $result[0]['date']); + $this->assertEquals(2.0, $result[0]['hours']); + $this->assertEquals(3, $result[0]['count']); + $this->assertEquals('2026-04-02', $result[1]['date']); + $this->assertEquals(1.0, $result[1]['hours']); + $this->assertEquals(1, $result[1]['count']); + } + + public function testGetDailyAggregationReturnsEmptyForNoData(): void + { + $service = $this->createServiceWithResults([]); + $result = $service->getDailyAggregation( + $this->createMock(User::class), + new \DateTimeImmutable('2026-04-01'), + new \DateTimeImmutable('2026-04-30') + ); + + $this->assertCount(0, $result); + } + + public function testHoursRoundedToTwoDecimals(): void + { + $mockResults = [ + ['day' => '2026-04-01', 'duration' => 5431, 'count' => 2], + ]; + + $service = $this->createServiceWithResults($mockResults); + $result = $service->getDailyAggregation( + $this->createMock(User::class), + new \DateTimeImmutable('2026-04-01'), + new \DateTimeImmutable('2026-04-30') + ); + + $this->assertEquals(1.51, $result[0]['hours']); + } + + private function createServiceWithResults(array $results): HeatmapService + { + $query = $this->createMock(AbstractQuery::class); + $query->method('getResult')->willReturn($results); + + $qb = $this->createMock(QueryBuilder::class); + $qb->method('select')->willReturnSelf(); + $qb->method('addSelect')->willReturnSelf(); + $qb->method('andWhere')->willReturnSelf(); + $qb->method('setParameter')->willReturnSelf(); + $qb->method('groupBy')->willReturnSelf(); + $qb->method('orderBy')->willReturnSelf(); + $qb->method('expr')->willReturn(new Expr()); + $qb->method('getQuery')->willReturn($query); + + $repo = $this->createMock(TimesheetRepository::class); + $repo->method('createQueryBuilder')->willReturn($qb); + + return new HeatmapService($repo); + } +} +``` + +**Task 4: Create API controller** + +Create `Controller/HeatmapController.php`: + +```php +getUser(); + $end = new \DateTimeImmutable('today'); + $begin = $end->modify('-1 year'); + + $projectId = $request->query->getInt('project') ?: null; + + $days = $service->getDailyAggregation($user, $begin, $end, $projectId); + + return new JsonResponse([ + 'days' => $days, + 'range' => [ + 'begin' => $begin->format('Y-m-d'), + 'end' => $end->format('Y-m-d'), + ], + ]); + } +} +``` + +Create `Resources/config/routes.yaml`: +```yaml +heatmap_controllers: + resource: '../../Controller/' + type: attribute +``` + +**Task 5: Create controller test** + +Create `Tests/Controller/HeatmapControllerTest.php`: + +```php + '2026-04-01', 'hours' => 2.0, 'count' => 3], + ]; + + $service = $this->createMock(HeatmapService::class); + $service->method('getDailyAggregation')->willReturn($mockDays); + + $controller = new HeatmapController(); + + // Mock user via reflection (AbstractController::getUser is protected) + $user = $this->createMock(User::class); + $container = $this->createMock(\Symfony\Component\DependencyInjection\ContainerInterface::class); + $tokenStorage = $this->createMock(\Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface::class); + $token = $this->createMock(\Symfony\Component\Security\Core\Authentication\Token\TokenInterface::class); + $token->method('getUser')->willReturn($user); + $tokenStorage->method('getToken')->willReturn($token); + $container->method('has')->willReturn(true); + $container->method('get')->willReturn($tokenStorage); + $controller->setContainer($container); + + $response = $controller->data(new Request(), $service); + + $this->assertInstanceOf(JsonResponse::class, $response); + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('days', $data); + $this->assertArrayHasKey('range', $data); + $this->assertCount(1, $data['days']); + $this->assertArrayHasKey('begin', $data['range']); + $this->assertArrayHasKey('end', $data['range']); + } +} +``` + +**Commit:** `feat: add HeatmapService, API controller, and PHPUnit tests` + +**Verification:** +- `php dev/kimai/vendor/bin/phpunit --configuration Tests/phpunit.xml` — all tests pass + +--- + +### Wave 2: Dashboard Widget (checkpoint for visual verification) + +#### Plan 02-02: HeatmapWidget + Dashboard Integration + +**Objective:** Widget placeholder appears on the Kimai dashboard. + +**Task 1: Create HeatmapWidget** + +Create `Widget/HeatmapWidget.php`: + +```php + +

Heatmap visualization coming in Phase 3

+ + {% endblock %} +{% endembed %} +``` + +**Task 3: Create DashboardSubscriber** + +Create `EventSubscriber/DashboardSubscriber.php`: + +```php + ['onDashboard', 100], + ]; + } + + public function onDashboard(DashboardEvent $event): void + { + $widget = $this->widgetService->getWidget('HeatmapWidget'); + if ($widget !== null) { + $event->addWidget($widget); + } + } +} +``` + +**Commit:** `feat: add dashboard widget with placeholder template` + +**Task 4: Verify (CHECKPOINT — requires manual verification)** + +1. Clear Kimai cache: `cd dev/kimai && bin/console cache:clear` +2. Start dev stack: `process-compose -f dev/process-compose.yaml -p 0 up` +3. Open browser: `http://127.0.0.1:8010` +4. Login: `susan_super` / `password` + +**Verification checklist:** +- [ ] "Activity Heatmap" widget appears on the dashboard +- [ ] Widget shows placeholder text "Heatmap visualization coming in Phase 3" +- [ ] Visiting `/heatmap/data` returns JSON with `days` array and `range` object +- [ ] JSON `days` entries have `date`, `hours`, `count` fields +- [ ] PHPUnit tests pass: `php dev/kimai/vendor/bin/phpunit --configuration Tests/phpunit.xml` + +## Requirement Coverage + +| Requirement | Plan | Verified By | +|-------------|------|-------------| +| PLUG-01 | Phase 1 | Already done | +| PLUG-02 | 02-02 | Widget visible on dashboard | +| PLUG-03 | 02-01 | API returns JSON; PHPUnit test | +| TEST-01 | 02-01 | HeatmapServiceTest passes | +| TEST-02 | 02-01 | HeatmapControllerTest passes | + +## Risks + +| Risk | Mitigation | +|------|------------| +| `@theme/embeds/card.html.twig` doesn't exist | Fall back to plain `
` wrapper | +| `DATE()` DQL function fails on MariaDB | Phase 1 uses MariaDB — should work natively | +| Widget not appearing despite registration | Check `WidgetService` debug, ensure autoconfigure is true | +| Test bootstrap can't find Kimai autoloader | Multiple path fallbacks in bootstrap.php | + +--- +*Plan created: 2026-04-08* diff --git a/.planning/phase-3/03-01-SUMMARY.md b/.planning/milestones/v1.0-phases/phase-3/03-01-SUMMARY.md similarity index 100% rename from .planning/phase-3/03-01-SUMMARY.md rename to .planning/milestones/v1.0-phases/phase-3/03-01-SUMMARY.md diff --git a/.planning/phase-3/03-03-SUMMARY.md b/.planning/milestones/v1.0-phases/phase-3/03-03-SUMMARY.md similarity index 100% rename from .planning/phase-3/03-03-SUMMARY.md rename to .planning/milestones/v1.0-phases/phase-3/03-03-SUMMARY.md diff --git a/.planning/milestones/v1.0-phases/phase-3/03-RESEARCH.md b/.planning/milestones/v1.0-phases/phase-3/03-RESEARCH.md new file mode 100644 index 0000000..053a493 --- /dev/null +++ b/.planning/milestones/v1.0-phases/phase-3/03-RESEARCH.md @@ -0,0 +1,85 @@ +# Phase 3 Research: Core Heatmap Rendering + +**Researched:** 2026-04-08 + +## Kimai Widget Asset Serving + +### How existing widgets load JS +- Kimai widgets use **inline ` + + {% endblock %} +{% endembed %} +``` + +**Task 2: Install assets** + +```bash +cd dev/kimai && bin/console assets:install public --symlink +``` + +This creates `dev/kimai/public/bundles/kimaiheatmap/` → plugin's `Resources/public/`. + +**Task 3: Add build step to dev workflow** + +Add a note in the README or dev setup that `npm run build` must be run after JS changes. Optionally add an esbuild watch command to process-compose. + +**Task 4: Update .gitignore** + +Add to `.gitignore`: +``` +node_modules/ +``` + +Ensure `Resources/public/heatmap.js` and `Resources/public/heatmap.css` are NOT gitignored — they're the built artifacts that ship with the plugin. + +**Commit:** `feat: wire heatmap JS/CSS into dashboard widget template` + +**Task 5: Verify (CHECKPOINT — requires manual verification)** + +1. Build JS: `npm run build` +2. Install assets: `cd dev/kimai && bin/console assets:install public --symlink` +3. Clear cache: `cd dev/kimai && bin/console cache:clear` +4. Start dev stack: `process-compose -f dev/process-compose.yaml -p 0 up` +5. Open browser: `http://127.0.0.1:8010` +6. Login: `susan_super` / `password` + +**Verification checklist:** +- [ ] Heatmap widget renders a calendar grid with colored cells +- [ ] Hovering a cell shows tooltip with date, hours, count +- [ ] Day-of-week labels (Mon, Wed, Fri) visible on left +- [ ] Month labels visible along top +- [ ] Empty days show distinct "no data" color +- [ ] Switch to dark theme — colors adapt correctly +- [ ] PHPUnit tests still pass: `php dev/kimai/vendor/bin/phpunit --configuration Tests/phpunit.xml` +- [ ] Vitest tests pass: `npm test` + +## Requirement Coverage + +| Requirement | Plan | Verified By | +|-------------|------|-------------| +| HEAT-01 | 03-02 | Calendar grid renders with d3; Vitest grid structure test | +| HEAT-02 | 03-02 | Color scale maps hours to intensity; Vitest color mapping test | +| HEAT-03 | 03-02 | Tooltip on hover; Vitest tooltip test | +| HEAT-04 | 03-02 | Day labels in SVG; Vitest day labels test | +| HEAT-05 | 03-02 | Month labels in SVG; Vitest month labels test | +| HEAT-06 | 03-02 | Empty cells have distinct class; Vitest empty cell test | +| HEAT-08 | 03-02, 03-03 | CSS uses `--tblr-*` variables; manual dark/light theme check | +| TEST-03 | 03-02 | Vitest test suite for d3 rendering | + +## Risks + +| Risk | Mitigation | +|------|------------| +| `assets:install --symlink` doesn't follow plugin symlink chain | Fall back to `assets:install` (copy mode) or manual symlink | +| d3 modules don't work in IIFE bundle | esbuild handles ESM→IIFE natively; verified in Task 4 of 03-01 | +| jsdom lacks SVG support for Vitest | d3-selection works with jsdom; test DOM attributes not visual rendering | +| `kimai_context` not available in widget template | Guard with `is defined` check; fall back to event listener only | +| Widget too wide/narrow in different dashboard layouts | Use SVG viewBox for responsive scaling; `overflow-x: auto` as safety net | + +--- +*Plan created: 2026-04-08* diff --git a/.planning/milestones/v1.0-phases/phase-4/04-RESEARCH.md b/.planning/milestones/v1.0-phases/phase-4/04-RESEARCH.md new file mode 100644 index 0000000..7fa7339 --- /dev/null +++ b/.planning/milestones/v1.0-phases/phase-4/04-RESEARCH.md @@ -0,0 +1,36 @@ +# Phase 4 Research: Heatmap Interaction + +**Researched:** 2026-04-08 + +## Kimai Timesheet Navigation + +### Route +- Route name: `timesheet` +- URL: `/timesheet` +- Date filtering uses toolbar form with `dateRange` parameter +- Query param format for date filtering needs experimentation, but likely: `/timesheet?dateRange=2026-04-08+-+2026-04-08` or similar DateRange format + +### Click-through approach +Navigate to `/timesheet` with the clicked date. The Twig template should pass the timesheet base URL via a `data-timesheet-url` attribute. The JS builds the full URL with date params. + +## Project Filtering + +### API endpoint +- `GET /api/projects?visible=1&orderBy=name` returns JSON array of projects +- Each project has `id`, `name`, `customer` fields +- This is a standard Kimai API endpoint with auth + +### Current HeatmapController +Already supports `?project=` query param for filtering aggregation data. No backend changes needed. + +### UI approach +- Render a `` above heatmap. On change, re-fetch `/heatmap/data?project=` and re-render. +- **No backend changes** — HeatmapController already supports `?project=` filtering. Project API is built into Kimai. +- **Activity filtering deferred** — INTR-01 says "project or activity". Start with project filter; activity can be added as a follow-up if needed since the API pattern is identical. + +## Waves + +### Wave 1: Click-through + Project Filter (autonomous) + +#### Plan 04-01: Day cell click navigation + project filter dropdown + +**Objective:** Add click-to-navigate on day cells and a project filter dropdown that re-renders the heatmap. + +**Task 1: Update types** + +Add to `assets/src/types.ts`: +```typescript +export interface Project { + id: number; + name: string; +} + +export interface HeatmapOptions { + dataUrl: string; + timesheetUrl: string; +} +``` + +**Task 2: Add click handler to heatmap cells** + +In `assets/src/heatmap.ts`: +- Add a `timesheetUrl` parameter to `renderHeatmap` (passed via options or config) +- On each `` cell, add a `click` event handler: + - Navigate to `${timesheetUrl}?dateRange=${dateStr}+-+${dateStr}` + - Only navigate if the cell has data (skip empty days) +- Add `cursor: pointer` style to cells with data +- Add `data-date` attribute to each rect for testability + +**Task 3: Add project filter dropdown** + +In `assets/src/heatmap.ts`, update the `init` function: +1. Before fetching heatmap data, fetch `/api/projects?visible=1&orderBy=name` +2. Create a `` element exists with project options +2. **Filter triggers re-fetch** — changing dropdown value triggers a new fetch with `?project=` param +3. **Heatmap re-renders** — after filter change, new data is rendered (different rect count or content) +4. **"All projects" resets filter** — selecting default option fetches without project param + +Test approach: +- Mock `fetch` to return project list and heatmap data +- Call `init(container)` and await fetch completion +- Programmatically change select value and dispatch `change` event +- Assert fetch was called with correct URL params + +**Verification:** +- `npm test` — all tests pass (existing + new) +- `npm run build` — bundle builds successfully + +**Commit:** `test: add click navigation and project filter tests` + +--- + +## Requirement Coverage + +| Requirement | Plan | Verified By | +|-------------|------|-------------| +| HEAT-07 | 04-01 | Click navigates to timesheet; Vitest click test | +| INTR-01 | 04-01 | Project dropdown filters heatmap; Vitest filter test | +| TEST-04 | 04-02 | Vitest tests for click + filter interaction | + +## Risks + +| Risk | Mitigation | +|------|------------| +| Timesheet date filter URL format wrong | Test with actual Kimai instance; adjust format if needed | +| `/api/projects` requires different auth | API uses same session auth as the dashboard; should work | +| Dropdown styling doesn't match Kimai | Use Bootstrap `form-select` class from Tabler theme | +| Re-render flickers | Clear container and re-render; fast enough for ~365 rects | + +--- +*Plan created: 2026-04-08*