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 `