diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md new file mode 100644 index 0000000..b9c7013 --- /dev/null +++ b/.planning/research/ARCHITECTURE.md @@ -0,0 +1,467 @@ +# Architecture Patterns + +**Domain:** Kimai dashboard widget plugin (Symfony bundle + d3.js) +**Researched:** 2026-04-08 +**Confidence:** MEDIUM (based on training knowledge of Kimai 2.x plugin system; no live doc verification possible) + +## Recommended Architecture + +### High-Level Overview + +``` ++------------------------------------------+ +| Kimai Dashboard (Twig) | +| +------------------------------------+ | +| | HeatmapWidget (registered via | | +| | WidgetInterface + DI tag) | | +| | +------------------------------+ | | +| | | Twig template renders | | | +| | |
| | | +| | | + inline d3.js bundle | | | +| | +------------------------------+ | | +| +------------------------------------+ | ++------------------------------------------+ + | ^ + | XHR/fetch | JSON + v | ++------------------------------------------+ +| HeatmapController (Symfony) | +| GET /api/heatmap?range=&project= | +| - Queries TimesheetRepository | +| - Aggregates hours/counts per day | +| - Returns JSON | ++------------------------------------------+ + | + v ++------------------------------------------+ +| Kimai Database (timesheet table) | +| - begin, end, duration, project_id, | +| activity_id, user_id | ++------------------------------------------+ +``` + +### Component Boundaries + +| Component | Responsibility | Communicates With | +|-----------|---------------|-------------------| +| `KimaiHeatmapBundle` | Bundle class, DI registration | Symfony kernel | +| `HeatmapWidget` | Implements `WidgetInterface`, provides widget metadata and Twig rendering | Dashboard renderer, Twig | +| `HeatmapController` | API endpoint serving aggregated time data as JSON | Widget frontend (JS), `HeatmapService` | +| `HeatmapService` | Data aggregation logic (hours/counts per day) | Kimai's `TimesheetRepository` / Doctrine | +| `d3 heatmap module` | Client-side calendar heatmap rendering | HeatmapController API (fetch), DOM | +| Twig template | Widget HTML shell + d3 script inclusion | HeatmapWidget, Encore/Webpack assets | + +## Kimai Plugin Bundle Structure + +Kimai plugins are standard Symfony bundles placed in `var/plugins/` (for local install) or loaded via Composer. The canonical directory layout: + +``` +KimaiHeatmapBundle/ + KimaiHeatmapBundle.php # Bundle class (extends PluginInterface) + DependencyInjection/ + KimaiHeatmapExtension.php # Loads services.yaml + Resources/ + config/ + services.yaml # Service definitions + DI tags + routes.yaml # Plugin routes (controller endpoints) + views/ + widget/ + heatmap.html.twig # Widget Twig template + public/ + heatmap.js # Compiled d3 heatmap script + heatmap.css # Widget styles + Widget/ + HeatmapWidget.php # WidgetInterface implementation + Controller/ + HeatmapController.php # API controller + Service/ + HeatmapService.php # Data aggregation + EventSubscriber/ # Optional: hook into Kimai events + composer.json # Package metadata, autoload config +``` + +### Bundle Class + +```php +// KimaiHeatmapBundle.php +namespace KimaiPlugin\KimaiHeatmapBundle; + +use App\Plugin\PluginInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class KimaiHeatmapBundle extends Bundle implements PluginInterface +{ + public function build(ContainerBuilder $container): void + { + parent::build($container); + } +} +``` + +**Key point:** Kimai plugins implement `App\Plugin\PluginInterface` which extends Symfony's `BundleInterface`. The namespace MUST be `KimaiPlugin\` for Kimai's plugin loader to discover it. + +### DependencyInjection Extension + +```php +// DependencyInjection/KimaiHeatmapExtension.php +namespace KimaiPlugin\KimaiHeatmapBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; + +class KimaiHeatmapExtension extends Extension implements PrependExtensionInterface +{ + public function load(array $configs, ContainerBuilder $container): void + { + $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yaml'); + } + + public function prepend(ContainerBuilder $container): void + { + // Prepend route config so Kimai loads our routes + $container->prependExtensionConfig('kimai', [ + 'plugin' => [ + 'heatmap' => [ + 'routes' => '@KimaiHeatmapBundle/Resources/config/routes.yaml', + ], + ], + ]); + } +} +``` + +## Dashboard Widget System + +Kimai's dashboard renders widgets that implement `App\Widget\WidgetInterface`. Widgets are registered via Symfony DI tags. + +### Widget Interface + +```php +// Widget/HeatmapWidget.php +namespace KimaiPlugin\KimaiHeatmapBundle\Widget; + +use App\Widget\WidgetInterface; +use App\Widget\Type\AbstractWidgetType; +use App\Repository\TimesheetRepository; + +class HeatmapWidget extends AbstractWidgetType +{ + public function __construct( + private TimesheetRepository $timesheetRepository + ) {} + + public function getWidth(): int + { + return WidgetInterface::WIDTH_FULL; // Full-width dashboard widget + } + + public function getHeight(): int + { + return WidgetInterface::HEIGHT_LARGE; + } + + public function getTitle(): string + { + return 'Activity Heatmap'; + } + + public function getTemplateName(): string + { + return '@KimaiHeatmap/widget/heatmap.html.twig'; + } + + public function getId(): string + { + return 'HeatmapWidget'; + } + + public function getData(array $options = []): mixed + { + // Initial data can be passed to Twig, or widget + // can fetch data via XHR from the API endpoint + return []; + } + + public function getPermissions(): array + { + return ['view_own_timesheet']; + } +} +``` + +### Service Registration (services.yaml) + +```yaml +services: + KimaiPlugin\KimaiHeatmapBundle\Widget\HeatmapWidget: + arguments: + $timesheetRepository: '@App\Repository\TimesheetRepository' + tags: + - { name: 'kimai.widget', priority: 50 } + + KimaiPlugin\KimaiHeatmapBundle\Controller\HeatmapController: + tags: ['controller.service_arguments'] + + KimaiPlugin\KimaiHeatmapBundle\Service\HeatmapService: + arguments: + $timesheetRepository: '@App\Repository\TimesheetRepository' +``` + +The `kimai.widget` tag is how the widget gets discovered and rendered on the dashboard. The `priority` controls ordering (higher = appears earlier). + +## API Endpoint for Heatmap Data + +### Controller + +```php +// Controller/HeatmapController.php +namespace KimaiPlugin\KimaiHeatmapBundle\Controller; + +use App\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; + +#[Route(path: '/api/heatmap')] +#[IsGranted('view_own_timesheet')] +class HeatmapController extends AbstractController +{ + #[Route(path: '/data', name: 'heatmap_data', methods: ['GET'])] + public function getData(Request $request, HeatmapService $service): JsonResponse + { + $begin = new \DateTime($request->query->get('begin', '-1 year')); + $end = new \DateTime($request->query->get('end', 'now')); + $projectId = $request->query->getInt('project', 0); + + $data = $service->getAggregatedData( + user: $this->getUser(), + begin: $begin, + end: $end, + projectId: $projectId ?: null + ); + + return new JsonResponse($data); + } +} +``` + +### Route Registration (routes.yaml) + +```yaml +heatmap_api: + resource: '../../Controller/' + type: annotation # or attribute for PHP 8+ + prefix: /api/plugins/heatmap +``` + +### JSON Response Shape + +```json +{ + "days": [ + { "date": "2026-01-15", "hours": 7.5, "count": 3 }, + { "date": "2026-01-16", "hours": 4.25, "count": 2 } + ], + "range": { "begin": "2025-04-08", "end": "2026-04-08" }, + "maxHours": 12.0, + "projects": [ + { "id": 1, "name": "Aitia" }, + { "id": 2, "name": "Aleph Garden" } + ] +} +``` + +## d3.js Integration in Symfony/Twig + +### Strategy: Standalone JS file loaded in Twig template + +Kimai uses Webpack Encore for asset management, but plugins typically ship prebuilt JS/CSS in `Resources/public/`. Kimai copies these to `public/bundles//` via `assets:install`. + +**Recommendation:** Ship a self-contained d3 heatmap script rather than integrating with Kimai's Webpack build. This avoids coupling to Kimai's internal build pipeline. + +### Twig Template + +```twig +{# Resources/views/widget/heatmap.html.twig #} +{% block widget_content %} +
+
+ + +
+
+
+{% endblock %} + +{% block widget_javascript %} + +{% endblock %} +``` + +### d3 Module Design + +``` +assets/ + src/ + heatmap.ts # Entry point, initializes widget + calendar-heatmap.ts # d3 calendar heatmap rendering + data-fetcher.ts # Fetch API wrapper for controller endpoint + color-scale.ts # CSS variable-aware color scaling + types.ts # TypeScript interfaces + package.json # d3 + build deps + tsconfig.json + rollup.config.js # Bundle to single file for Resources/public/ +``` + +The JS gets bundled (via rollup or esbuild) into a single `heatmap.js` that ships in `Resources/public/`. d3 is bundled in (tree-shaken to only calendar/scale modules). + +### CSS Variable Integration + +```css +/* Use Kimai's theme CSS custom properties */ +.heatmap-cell { + fill: var(--chart-color-empty, #ebedf0); +} +.heatmap-cell[data-level="1"] { fill: var(--chart-color-low, #9be9a8); } +.heatmap-cell[data-level="2"] { fill: var(--chart-color-medium, #40c463); } +.heatmap-cell[data-level="3"] { fill: var(--chart-color-high, #30a14e); } +.heatmap-cell[data-level="4"] { fill: var(--chart-color-max, #216e39); } +``` + +**Note:** Kimai uses AdminLTE / Tabler themes. The actual CSS variable names need to be verified against the running Kimai instance. Fallback values ensure the heatmap works even if variables are missing. + +## Data Flow + +``` +1. User loads Kimai dashboard +2. Symfony renders dashboard, includes HeatmapWidget +3. HeatmapWidget renders Twig template (empty chart container + controls) +4. heatmap.js initializes on DOMContentLoaded +5. JS reads data-url attribute, fetches /api/plugins/heatmap/data?range=365 +6. HeatmapController receives request, delegates to HeatmapService +7. HeatmapService queries TimesheetRepository with DQL/QueryBuilder + - GROUP BY DATE(begin), SUM(duration), COUNT(*) + - Filtered by user, date range, optional project +8. Controller returns JSON response +9. d3.js receives JSON, builds calendar heatmap SVG +10. User interactions (mode toggle, project filter, day click) handled client-side + - Mode toggle: re-renders with different data key (hours vs count) + - Project filter: new fetch with ?project=ID parameter + - Day click: window.location to /en/timesheet/?daterange=YYYY-MM-DD +``` + +## Patterns to Follow + +### Pattern 1: Widget Data via Dedicated API + +**What:** Widget template is a thin shell; data fetching happens via XHR to a dedicated controller endpoint. +**Why:** Separates rendering from data. Widget loads fast (empty shell), data arrives async. Enables filtering/re-fetching without page reload. +**When:** Always for data-heavy widgets. + +### Pattern 2: Permission-Gated Everything + +**What:** Both the widget visibility and the API endpoint check `view_own_timesheet` permission. +**Why:** Kimai has a role-based permission system. Widget should only appear for users with timesheet access, and the API must independently verify permissions (defense in depth). + +### Pattern 3: User-Scoped Queries + +**What:** Always filter by `$this->getUser()` in queries. +**Why:** Personal time tracking -- users should only see their own data. Even if Kimai handles this at the repository level, be explicit. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1: Embedding Data in Twig + +**What:** Passing all heatmap data through `getData()` into Twig as PHP arrays. +**Why bad:** Large datasets (365 days of data) bloat the HTML. Cannot update without page reload. Makes filtering require full page requests. +**Instead:** Thin Twig template + XHR data fetching. + +### Anti-Pattern 2: Hooking into Kimai's Webpack Build + +**What:** Requiring the plugin user to rebuild Kimai's assets. +**Why bad:** Kimai's internal asset pipeline is not a public API. Updates can break your build. Extra install step for users. +**Instead:** Ship prebuilt JS/CSS in `Resources/public/`. + +### Anti-Pattern 3: Raw SQL Queries + +**What:** Writing raw SQL instead of using Doctrine QueryBuilder or DQL. +**Why bad:** Breaks database portability (Kimai supports MySQL and SQLite). Harder to test. +**Instead:** Use Doctrine QueryBuilder with Kimai's existing repository patterns. + +## Suggested Build Order + +The components have clear dependencies that dictate implementation order: + +``` +Phase 1: Plugin Scaffold + - Bundle class, DI extension, services.yaml, routes.yaml + - Empty widget registered on dashboard (renders "Hello Heatmap") + - Nix devshell with local Kimai instance + Validates: Plugin loads, widget appears, dev env works + +Phase 2: Data Layer + - HeatmapService with aggregation queries + - HeatmapController returning JSON + - PHPUnit tests for service + controller + Validates: API returns correct aggregated data + Depends on: Phase 1 (routing, DI) + +Phase 3: Heatmap Visualization + - d3.js calendar heatmap module (TypeScript) + - Build pipeline (rollup/esbuild -> single bundle) + - Twig template integration + - JS tests for rendering logic + Validates: Heatmap renders from API data + Depends on: Phase 2 (API endpoint exists) + +Phase 4: Interactivity + - Mode toggle (hours vs count) + - Project/activity filter dropdowns + - Day-click navigation to timesheet + - Kimai theme CSS variable integration + Validates: Full feature set works + Depends on: Phase 3 (heatmap renders) +``` + +## Scalability Considerations + +| Concern | Personal use (1 user) | Team (10 users) | +|---------|----------------------|-----------------| +| Query performance | Fine, single GROUP BY | Add DB index on `timesheet.begin` if slow | +| Data payload size | ~365 rows, trivial | Same per user (user-scoped) | +| Widget rendering | Instant | Instant (client-side d3) | +| Caching | Not needed | Consider HTTP cache headers on API | + +For personal use (the stated scope), performance is a non-concern. The timesheet table query with a GROUP BY date, filtered to one user and one year, will return at most 365 rows. + +## Sources + +- Kimai plugin development documentation (kimai.org/documentation/plugin-development.html) -- referenced from training data, MEDIUM confidence +- Symfony Bundle system documentation (symfony.com/doc/current/bundles.html) -- HIGH confidence, stable API +- Kimai source code patterns (github.com/kimai/kimai) -- referenced from training data, MEDIUM confidence +- d3.js calendar heatmap patterns -- HIGH confidence, well-established pattern + +## Confidence Notes + +| Area | Confidence | Notes | +|------|------------|-------| +| Bundle structure | MEDIUM | Based on Kimai 2.x patterns from training data. Namespace `KimaiPlugin\` and `PluginInterface` need verification against current Kimai release. | +| Widget registration | MEDIUM | `kimai.widget` DI tag and `WidgetInterface` / `AbstractWidgetType` hierarchy from training data. Widget interface methods may differ in current release. | +| API controller pattern | HIGH | Standard Symfony controller patterns. Route prefix and permission attribute names should be verified. | +| d3.js integration | HIGH | Self-contained JS bundle in `Resources/public/` is the standard Kimai plugin asset approach. | +| Data flow | HIGH | Symfony controller -> Doctrine query -> JSON -> client JS is textbook. | +| CSS theme variables | LOW | Actual Kimai CSS variable names need verification against running instance. Fallback values mitigate risk. | diff --git a/.planning/research/FEATURES.md b/.planning/research/FEATURES.md new file mode 100644 index 0000000..92c95fb --- /dev/null +++ b/.planning/research/FEATURES.md @@ -0,0 +1,130 @@ +# Feature Landscape + +**Domain:** Time-tracking activity heatmap dashboard widget (Kimai plugin) +**Researched:** 2026-04-08 +**Overall confidence:** MEDIUM (based on training data knowledge of GitHub, GitLab, WakaTime, RescueTime, Toggl, and open-source heatmap libraries; no live verification available) + +## Reference Products Analyzed + +| Product | Heatmap Style | Key Insight | +|---------|--------------|-------------| +| GitHub contribution graph | Year-long calendar grid, 5 color levels, tooltips | The gold standard UX -- simple, instantly understood, no configuration needed | +| GitLab activity calendar | Same layout as GitHub, slightly different color scheme | Proves the pattern is universal for developer audiences | +| WakaTime | Calendar heatmap + line charts + breakdowns by language/project | Heavier analytics layer on top of the basic heatmap | +| RescueTime | Daily productivity scores, category breakdowns, hour-of-day patterns | Focus on productivity categorization, not raw activity | +| Toggl Track insights | Weekly/monthly bar charts, project pie charts, calendar view | More report-centric than heatmap-centric | +| cal-heatmap (JS library) | Configurable calendar heatmap, multiple layouts | Popular open-source implementation, good API reference | + +## Table Stakes + +Features users expect from any calendar heatmap. Missing any of these makes the widget feel broken or incomplete. + +| Feature | Why Expected | Complexity | Notes | +|---------|--------------|------------|-------| +| Calendar grid layout (weeks x days) | This IS the product -- the GitHub-style grid is the universally recognized pattern | Medium | d3.js renders SVG grid; each cell = one day | +| Color intensity mapping | Users expect darker = more activity, lighter = less; this is how every heatmap works | Low | 4-5 discrete color levels (empty, low, medium, high, very high) | +| Tooltip on hover | Every reference product shows exact values on hover; without it users stare at colors guessing | Low | Show date, hours tracked, entry count | +| Sensible default time range | GitHub shows 1 year; users expect to see something useful without configuration | Low | Default to trailing 12 months | +| Day-of-week labels | Mon/Wed/Fri labels on the Y-axis for orientation | Low | Standard in GitHub/GitLab graphs | +| Month labels | Column headers showing month boundaries | Low | Standard in all calendar heatmaps | +| Empty state | Days with no entries should render as a distinct "no data" color, not be invisible | Low | Light gray or the theme's muted background color | +| Click-through to detail | Clicking a day should take you somewhere useful | Low | Navigate to Kimai timesheet view filtered to that date (already in PROJECT.md requirements) | +| Theme integration | Must not look like a foreign element glued onto the dashboard | Low | Use Kimai CSS variables for colors | + +## Differentiators + +Features that elevate the widget beyond a static pretty picture. Not expected by default, but each adds meaningful value for a personal time-tracking use case. + +| Feature | Value Proposition | Complexity | Notes | +|---------|-------------------|------------|-------| +| Toggle between hours and entry count | Different questions: "how much did I work?" vs "how consistently did I track?" -- GitHub only shows one metric | Low | Button or toggle in widget header; already in PROJECT.md requirements | +| Project/activity filter | Focus on one project at a time to see its cadence | Medium | Dropdown populated from Kimai data; re-renders heatmap on change | +| Configurable time range | See last 3 months, 6 months, full year, or custom range | Medium | Range selector in widget header; affects data query and grid size | +| Streak indicator | "Current streak: 12 days" -- gamification that motivates consistency | Low | Simple calculation: count consecutive non-zero days ending at today | +| Summary stats row | Total hours, average hours/day, busiest day -- quick numbers alongside the visual | Low | Small text row below or above the heatmap | +| Weekend vs weekday distinction | Subtle visual indicator (border, opacity) for weekends | Low | Helps distinguish work patterns from weekend work | +| Responsive widget sizing | Widget that adapts if Kimai dashboard column width changes | Medium | SVG viewBox scaling; cell size calculation based on container width | + +## Anti-Features + +Things to deliberately NOT build. Each would add complexity without serving the core "at a glance, see where your time went" purpose. + +| Anti-Feature | Why Avoid | What to Do Instead | +|--------------|-----------|-------------------| +| Real-time updates / live refresh | Overengineered for a dashboard widget; page reload is fine for daily granularity data | Refresh data on page load only | +| Billable vs non-billable split | Out of scope per PROJECT.md; this is personal tracking, not client billing | Single aggregate view | +| Export/share heatmap as image | Out of scope per PROJECT.md; no audience to share with for personal use | Users can screenshot if needed | +| Hour-of-day heatmap (2D matrix) | Different visualization entirely; scope creep that doubles the frontend work | Stick to the calendar grid; could be a separate widget later | +| Drill-down charts within the widget | Clicking a day to show a pie chart of projects within the widget adds major complexity | Click-through to Kimai's existing timesheet/report views instead | +| Multi-user comparison | Personal tracking tool, not a team dashboard | Single-user view only | +| Goal setting / targets | Adds state management, settings UI, persistence -- a separate feature domain | Streak indicator covers the motivational angle simply | +| Animated transitions on data change | Cool but adds JS complexity for near-zero utility on a dashboard widget | Instant re-render on filter change | +| Mobile-specific layout | Out of scope per PROJECT.md; Kimai dashboard is a desktop experience | Standard responsive SVG is sufficient | +| Custom color theme picker | Theme integration with Kimai variables is sufficient; custom palettes add a settings UI nobody needs | Use Kimai theme variables, pick a single well-tested palette | + +## Feature Dependencies + +``` +Calendar grid layout + --> Color intensity mapping (needs the grid to color) + --> Tooltip on hover (needs cells to attach to) + --> Click-through to detail (needs cells to click) + --> Empty state (needs grid cells for empty days) + --> Day/Month labels (needs the grid structure) + +Toggle hours/count + --> Requires backend to return BOTH hours and count per day + --> Re-maps color intensity without re-fetching data + +Project/activity filter + --> Requires backend filter parameter in data API + --> Dropdown populated via separate Kimai API call (projects list) + --> Triggers data re-fetch + full heatmap re-render + +Configurable time range + --> Requires backend date range parameter in data API + --> Affects grid dimensions (fewer weeks = smaller grid) + --> Triggers data re-fetch + full heatmap re-render + +Streak indicator + --> Depends on the same daily aggregation data as the heatmap + --> No additional backend work if heatmap data is already loaded + +Summary stats + --> Depends on the same daily aggregation data as the heatmap + --> Pure frontend calculation from existing data +``` + +## MVP Recommendation + +**Phase 1 -- Core heatmap (all table stakes):** +1. Calendar grid with color intensity (the product itself) +2. Tooltips on hover +3. Day/month labels and empty state +4. Click-through to Kimai timesheet view +5. Kimai theme integration + +**Phase 2 -- Interactivity (high-value differentiators):** +1. Toggle between hours and entry count +2. Project/activity filter dropdown +3. Configurable time range + +**Phase 3 -- Polish (low-effort differentiators):** +1. Streak indicator +2. Summary stats row +3. Weekend distinction + +**Defer indefinitely:** +- Hour-of-day matrix, export, goals, animations, multi-user -- all anti-features for this context. + +**Rationale:** Phase 1 delivers a complete, useful widget. Phase 2 adds the interactivity that makes it genuinely better than just looking at Kimai's existing reports. Phase 3 is cheap polish that improves daily experience but isn't blocking. + +## Sources + +- GitHub contribution graph: well-known UX pattern (5 color levels, year grid, tooltips, contribution count) +- GitLab activity calendar: same pattern, confirms universality +- WakaTime dashboard: adds analytics layer (coding time heatmap, project breakdowns, streak tracking) +- RescueTime: productivity scoring approach, hour-of-day patterns +- Toggl Track insights: report-centric visualizations, project breakdowns +- cal-heatmap.com: popular open-source JS calendar heatmap library, API design reference +- All sourced from training data (no live verification available) -- MEDIUM confidence diff --git a/.planning/research/PITFALLS.md b/.planning/research/PITFALLS.md new file mode 100644 index 0000000..5493543 --- /dev/null +++ b/.planning/research/PITFALLS.md @@ -0,0 +1,178 @@ +# Domain Pitfalls + +**Domain:** Kimai dashboard widget plugin (Symfony bundle + d3.js heatmap) +**Researched:** 2026-04-08 +**Note:** Web search/fetch tools were unavailable. Findings are based on training data knowledge of Kimai, Symfony bundles, d3.js, and Nix+PHP environments. All findings marked with confidence levels accordingly. + +## Critical Pitfalls + +Mistakes that cause rewrites or major issues. + +### Pitfall 1: Kimai Major Version Breaking Changes in Plugin API + +**What goes wrong:** Kimai 2.x has changed its internal plugin API, event system, and widget rendering between releases without a formal deprecation cycle. A plugin built against one version silently breaks on the next. The `WidgetInterface`, dashboard rendering hooks, and Twig extensions have all shifted. +**Why it happens:** Kimai is maintained primarily by one developer (Kevin Papst). The plugin API is not versioned independently from the application -- it evolves with Kimai core. +**Consequences:** Plugin stops rendering, throws Symfony container errors, or silently disappears from the dashboard after a Kimai update. +**Prevention:** +- Pin to a specific Kimai version in composer.json (`"kimai/kimai": "^2.x"` with a tight constraint) +- Read UPGRADING.md in the Kimai repo before each Kimai update +- Write an integration test that boots the Symfony kernel with the plugin loaded -- this catches container/DI breakage immediately +- Subscribe to Kimai releases (GitHub watch) for breaking change awareness +**Detection:** Dashboard widget silently missing after update. Symfony cache clear errors. DI container compilation failures. +**Confidence:** MEDIUM (based on Kimai's development history through early 2025) +**Phase relevance:** Phase 1 (scaffold) -- lock version constraints early. + +### Pitfall 2: Timezone Mismatch Between PHP and JavaScript + +**What goes wrong:** Kimai stores timestamps in UTC in the database. The PHP backend converts to the user's configured timezone for display. If your d3.js heatmap receives raw UTC timestamps and bins them into days client-side, days land in the wrong cells. A session logged at 23:30 Berlin time (21:30 UTC) shows up on the correct day in Kimai's timesheet but the previous day in your heatmap. +**Why it happens:** The boundary between "server renders timezone-aware data" and "client renders timezone-aware data" is easy to get wrong, especially when aggregating by day. +**Consequences:** Hours appear on wrong days. Totals per day are wrong. User loses trust in the widget immediately. +**Prevention:** +- Aggregate by day on the PHP side using the user's configured Kimai timezone (`$user->getTimezone()`) +- Send pre-aggregated `{date: "2026-04-08", hours: 5.5, count: 3}` to the frontend -- never raw timestamps +- The d3 heatmap should receive date strings (not timestamps) so no further timezone conversion happens client-side +**Detection:** Compare heatmap day totals against Kimai's built-in weekly/monthly reports. Discrepancies = timezone bug. +**Confidence:** HIGH (this is a universal time-tracking visualization issue, well-documented across domains) +**Phase relevance:** Phase 1 (data layer) -- get the aggregation right before building the visualization. + +### Pitfall 3: Kimai Widget System Assumptions + +**What goes wrong:** Kimai's dashboard widget system expects widgets to implement specific interfaces and register via Symfony service tags. Developers coming from generic Symfony bundle development wire things up as controllers/routes instead of using the widget system, resulting in a working page but not a dashboard widget. +**Why it happens:** Kimai's widget system is Kimai-specific, not standard Symfony. Documentation is sparse. Developers cargo-cult from Symfony controller tutorials instead of studying existing Kimai plugins. +**Consequences:** You build a standalone page that works at `/my-heatmap` but cannot embed in the Kimai dashboard. Rework required to fit the widget interface. +**Prevention:** +- Study existing Kimai plugins that provide dashboard widgets (e.g., `kimai/CalendarBundle`, built-in widgets in `src/Widget/`) +- Implement `WidgetInterface` (or extend `AbstractWidget`) from the start +- Register as a tagged Symfony service: `kimai.widget` +- Do not create a standalone controller -- the widget renders within the dashboard's Twig template +**Detection:** Widget does not appear on the dashboard. No errors, just absent. +**Confidence:** MEDIUM (based on Kimai source structure through 2024-2025) +**Phase relevance:** Phase 1 (scaffold) -- get the widget rendering on the dashboard before writing any d3 code. + +## Moderate Pitfalls + +### Pitfall 4: d3.js Bundle Size in Kimai's Asset Pipeline + +**What goes wrong:** Including all of d3.js (~500KB minified) for a calendar heatmap that only needs `d3-scale`, `d3-selection`, `d3-time`, and `d3-time-format`. Kimai uses Webpack Encore for asset compilation, and a full d3 import bloats the plugin's JS bundle unnecessarily. +**Why it happens:** `import * as d3 from 'd3'` is the most common tutorial pattern. +**Prevention:** +- Import only needed d3 modules: `import { select } from 'd3-selection'; import { scaleQuantize } from 'd3-scale';` +- This reduces the d3 footprint to ~50-80KB +- Verify with Webpack Bundle Analyzer that tree-shaking works +**Confidence:** HIGH (well-documented d3 best practice) +**Phase relevance:** Phase 2 (visualization) -- set up imports correctly from the first d3 code. + +### Pitfall 5: Nix + PHP + Composer Dev Environment Complexity + +**What goes wrong:** Getting a working Kimai instance inside a Nix devshell is non-trivial. Kimai requires PHP 8.1+, specific PHP extensions (intl, gd, mbstring, zip, xml), MySQL/MariaDB or SQLite, and Composer. Nix's PHP packaging sometimes has extension version mismatches, and Kimai's `composer install` may try to download packages that conflict with Nix's hermetic approach. +**Why it happens:** PHP ecosystem tooling (Composer, PECL extensions) assumes a mutable system. Nix is immutable. These philosophies clash in subtle ways -- e.g., Composer's `post-install-cmd` scripts that try to write to vendor directories, or PHP extensions that need specific system libraries. +**Consequences:** Days lost fighting the dev environment before writing any plugin code. Temptation to abandon Nix and use Docker instead. +**Prevention:** +- Use `pkgs.php83` (or latest) with extensions via `php.withExtensions` +- Use SQLite for the dev database (simpler than spinning up MariaDB in Nix) +- Consider a hybrid approach: Nix provides PHP + Composer + Node, but let Composer manage vendor/ normally (don't try to Nixify Composer dependencies) +- Set `COMPOSER_HOME` to a writable temp directory +- Pre-seed the database with a SQL fixture, not Kimai's interactive installer +- Test the devshell setup as the very first task -- do not proceed to plugin code until `bin/console kimai:version` works +**Detection:** `nix develop` fails to start, or Kimai throws "extension missing" errors at runtime. +**Confidence:** MEDIUM (based on general Nix+PHP experience; Kimai-specific Nix setup is uncommon) +**Phase relevance:** Phase 0 / Pre-phase -- this blocks everything else. Must be solved first. + +### Pitfall 6: d3.js SVG Performance with 365+ Day Cells + +**What goes wrong:** Rendering 365 individual `` elements in SVG is fine. But adding tooltips, hover effects, and click handlers to each cell via d3's `.on()` creates 365+ event listeners. Combined with CSS transitions, this can cause jank on lower-end machines or when the widget shares the dashboard with other widgets. +**Why it happens:** The naive approach of binding events per-element is the d3 tutorial default. +**Prevention:** +- Use event delegation: attach a single mousemove/click listener to the SVG container, use `document.elementFromPoint()` or d3's `pointer()` + geometric lookup to identify the hovered cell +- Alternatively, for 365 cells, individual listeners are actually fine in modern browsers -- only becomes a real problem at 1000+ elements. So: measure first, optimize only if dashboard feels sluggish +- Avoid CSS `transition` on all 365 rects simultaneously (e.g., on theme change) +**Detection:** Dashboard sluggishness. DevTools Performance tab shows long "Recalculate Style" times. +**Confidence:** HIGH (well-understood SVG performance characteristics) +**Phase relevance:** Phase 2 (visualization) -- implement event delegation from the start if supporting multi-year views, otherwise defer optimization. + +### Pitfall 7: Kimai's Webpack Encore Integration for Plugins + +**What goes wrong:** Kimai plugins must integrate with Kimai's existing Webpack Encore setup, not ship their own. Developers create a standalone webpack.config.js, build separately, and the assets don't load because Kimai's asset manifest doesn't know about them. +**Why it happens:** Symfony Encore documentation describes standalone setup. Kimai's plugin asset pipeline is plugin-specific and less documented. +**Prevention:** +- Follow Kimai's plugin asset conventions: assets go in `Resources/public/` or use `assets/` with an `encore` entry +- Study how existing Kimai plugins (e.g., CustomCSS, ExpensesBundle) register their JS/CSS +- Kimai 2.x may use its own asset inclusion mechanism via Twig blocks in widget templates rather than Encore entries -- verify against the version you target +- For d3.js, consider whether you even need Encore: a single pre-built JS file in `Resources/public/` that the widget template includes via ` +{% endblock %} +``` + +Pass data as a JSON data attribute. The JS reads it and renders the SVG. This avoids API calls and works with Kimai's server-side rendering model. + +**Confidence: LOW** -- The exact Twig block names and asset path conventions should be verified against a working Kimai plugin. The `@theme/widget.html.twig` base template may differ. + +## Alternatives Considered + +| Category | Recommended | Alternative | Why Not | +|----------|-------------|-------------|---------| +| JS Visualization | d3 sub-modules | cal-heatmap | Limits customization for Kimai theme integration, click navigation, and mode toggling | +| JS Visualization | d3 sub-modules | Chart.js matrix | Less control over calendar layout, weaker SVG customization | +| JS Bundler | esbuild | Webpack Encore | Overkill for bundling a single widget's JS. Encore adds Symfony config overhead. | +| JS Testing | Vitest | Jest | d3 v7 is ESM-only; Jest's ESM support requires workarounds | +| JS Language | TypeScript | Plain JS | Type safety for the d3 code, catches data shape mismatches at build time | +| PHP Testing | PHPUnit | Pest | Kimai's own tests use PHPUnit. Consistency with the host app matters. | +| Dev Env | Nix flake | Docker Compose | User runs NixOS; Nix is native. Docker adds a layer. | + +## TypeScript for d3 + +Use TypeScript for the heatmap source code. d3 v7 ships with type definitions. TypeScript catches common d3 mistakes (wrong scale types, missing data fields) at build time. esbuild handles `.ts` natively. + +```json +// tsconfig.json (minimal) +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "types": ["vitest/globals"] + }, + "include": ["assets/src/**/*.ts", "assets/test/**/*.ts"] +} +``` + +## Installation Commands + +```bash +# PHP dependencies (in the plugin directory) +composer init --name="kimai/heatmap-bundle" --type="kimai-plugin" +# No extra PHP packages needed beyond Kimai's own dependencies + +# JS dependencies +npm init -y +npm install d3-scale d3-selection d3-time d3-time-format d3-scale-chromatic d3-shape +npm install -D typescript esbuild vitest jsdom @types/d3-scale @types/d3-selection @types/d3-time @types/d3-time-format @types/d3-scale-chromatic @types/d3-shape +``` + +## Nix Flake Approach + +The flake should provide: +1. **PHP 8.2** with extensions: `mbstring`, `intl`, `pdo_mysql` (or `pdo_sqlite` for dev), `xml`, `zip` +2. **Composer 2.x** +3. **Node.js 22 LTS** with npm +4. **MariaDB** or **SQLite** for the local Kimai DB +5. A **devShell** script that clones Kimai, runs migrations, seeds test data, and symlinks the plugin into `var/plugins/` + +SQLite is recommended for local dev to avoid running a separate DB server. Kimai supports SQLite out of the box. + +**Confidence: HIGH** for the Nix approach concept. The specific Kimai setup commands need verification. + +## Key Version Verification Checklist + +Before starting development, verify these against current sources: + +- [ ] Kimai latest stable version (check GitHub releases) +- [ ] Kimai's required PHP version (check `composer.json`) +- [ ] Kimai's Symfony version (check `composer.lock`) +- [ ] Kimai's `WidgetInterface` API (check `src/Widget/` in Kimai source) +- [ ] Kimai's Twig widget template blocks (check existing dashboard widgets in Kimai source) +- [ ] Kimai's asset serving mechanism for plugins (may use Symfony AssetMapper now instead of Encore) +- [ ] d3 v7 latest patch version on npm + +## Sources + +- Kimai plugin documentation: https://www.kimai.org/documentation/plugin-development.html +- Kimai GitHub: https://github.com/kimai/kimai +- d3.js documentation: https://d3js.org/ +- d3 calendar heatmap examples: https://observablehq.com/@d3/calendar +- Vitest documentation: https://vitest.dev/ +- esbuild documentation: https://esbuild.github.io/ + +**All sources are from training data, not live-fetched. Confidence levels reflect this limitation.** diff --git a/.planning/research/SUMMARY.md b/.planning/research/SUMMARY.md new file mode 100644 index 0000000..a002453 --- /dev/null +++ b/.planning/research/SUMMARY.md @@ -0,0 +1,151 @@ +# Project Research Summary + +**Project:** Kimai Heatmap Plugin +**Domain:** Time-tracking dashboard widget (Symfony bundle + d3.js visualization) +**Researched:** 2026-04-08 +**Confidence:** MEDIUM + +## Executive Summary + +This is a Kimai 2.x plugin that adds a GitHub-style activity heatmap to the dashboard. The product pattern is well-established: calendar grid, color intensity for activity levels, tooltips, click-through navigation. The technology choices are straightforward -- a Symfony bundle for the backend (matching Kimai's framework), d3.js sub-modules for the SVG visualization, TypeScript for type safety, and esbuild for bundling. No additional database is needed; the plugin reads from Kimai's existing timesheet table. + +The recommended approach is a strict 4-phase build: plugin scaffold first (prove the widget appears on the dashboard), then data layer (aggregation API), then visualization (d3 heatmap rendering), then interactivity (filters, toggles, click navigation). This ordering is dictated by hard dependencies -- you cannot render a heatmap without data, and you cannot build data queries without a working plugin scaffold. The Nix dev environment setup is a prerequisite phase that should be timeboxed to one day. + +The primary risks are: (1) Kimai's plugin API is under-documented and shifts between releases -- the widget interface, DI tags, and asset conventions all need verification against the target Kimai version before writing code; (2) timezone-incorrect day aggregation will produce wrong data silently -- aggregation must happen server-side in the user's configured timezone; (3) the Nix + PHP + Kimai dev environment setup can consume days if not timeboxed. All three are manageable with the mitigations outlined below. + +## Key Findings + +### Recommended Stack + +The plugin is a standard Symfony bundle running inside Kimai 2.x. The frontend is a self-contained d3.js visualization bundled with esbuild and shipped as a single JS file in `Resources/public/`. No integration with Kimai's Webpack Encore build is needed or desired. + +**Core technologies:** +- **PHP 8.2+ / Symfony 6.4 LTS**: Must match Kimai's runtime exactly. Verify against Kimai's `composer.json`. +- **d3.js sub-modules** (d3-scale, d3-selection, d3-time, d3-time-format, d3-scale-chromatic, d3-shape): Selective imports keep bundle size to ~50-80KB vs ~500KB for full d3. +- **TypeScript**: d3 v7 ships types. Catches data shape bugs at build time. esbuild handles `.ts` natively. +- **esbuild**: Single-command bundling of d3 modules into one JS file. Simpler than Webpack Encore for a single widget. +- **Vitest + jsdom**: ESM-native testing for d3 code. Jest struggles with d3 v7's ESM-only modules. +- **PHPUnit**: Matches Kimai's own test framework. +- **Nix flake**: Reproducible dev env with PHP 8.2, Composer, Node 22, SQLite. + +### Expected Features + +**Must have (table stakes):** +- Calendar grid layout (weeks x days) with color intensity mapping +- Tooltip on hover showing date, hours, entry count +- Day-of-week and month labels +- Empty state rendering (no-data days visible, not invisible) +- Click-through to Kimai timesheet filtered by date +- Kimai theme integration via CSS variables +- Default trailing 12-month range + +**Should have (differentiators):** +- Toggle between hours/day and entry count (different questions answered) +- Project/activity filter dropdown +- Configurable time range (3/6/12 months) +- Streak indicator and summary stats row + +**Defer indefinitely:** +- Hour-of-day matrix, export/share, goal setting, animations, multi-user, mobile layout, custom color picker -- all anti-features for a personal tracking widget. + +### Architecture Approach + +The architecture follows a clean separation: thin Twig template (HTML shell + controls), XHR data fetching from a dedicated API controller, and client-side d3 rendering. The widget registers via Kimai's `WidgetInterface` + `kimai.widget` DI tag. Data flows from Kimai's timesheet table through a `HeatmapService` (Doctrine QueryBuilder, GROUP BY date, user-scoped), to a `HeatmapController` (JSON API), to the d3 module (SVG rendering). + +**Major components:** +1. **KimaiHeatmapBundle** -- Symfony bundle class implementing `PluginInterface`, auto-discovered by Kimai +2. **HeatmapWidget** -- `WidgetInterface` implementation, registers on dashboard, renders Twig template +3. **HeatmapController** -- API endpoint (`/api/plugins/heatmap/data`) returning aggregated JSON +4. **HeatmapService** -- Server-side aggregation (hours/count per day, timezone-aware, user-scoped) +5. **d3 heatmap module** -- TypeScript, calendar grid rendering, event handling, theme integration + +### Critical Pitfalls + +1. **Kimai plugin API instability** -- Pin to a specific Kimai version. Write an integration test that boots the Symfony kernel with the plugin loaded. Subscribe to Kimai releases for breaking change awareness. +2. **Timezone mismatch (PHP vs JS day boundaries)** -- Aggregate by day on the PHP side using `$user->getTimezone()`. Send date strings to the frontend, never raw timestamps. Compare output against Kimai's own reports. +3. **Widget system misunderstanding** -- Use `WidgetInterface` + `kimai.widget` DI tag from the start. Do not build a standalone controller page. Study existing Kimai widgets before coding. +4. **Asset loading failures** -- Ship prebuilt JS in `Resources/public/`, loaded via `