docs: add project research

This commit is contained in:
Christopher Mühl 2026-04-08 10:24:43 +02:00
parent 5ca38e1584
commit 19d03c2baa
No known key found for this signature in database
GPG key ID: 925AC7D69955293F
5 changed files with 1181 additions and 0 deletions

View file

@ -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 | | |
| | | <div id="heatmap-widget"> | | |
| | | + 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\<BundleName>` 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/<bundlename>/` 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 %}
<div class="heatmap-widget" id="heatmap-container"
data-url="{{ path('heatmap_data') }}"
data-mode="hours"
data-range="365">
<div class="heatmap-controls">
<select id="heatmap-mode">
<option value="hours">Hours per day</option>
<option value="count">Entry count</option>
</select>
<select id="heatmap-project">
<option value="">All projects</option>
</select>
</div>
<div id="heatmap-chart"></div>
</div>
{% endblock %}
{% block widget_javascript %}
<script src="{{ asset('bundles/kimaiheatmap/heatmap.js') }}"></script>
{% 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. |

View file

@ -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

View file

@ -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 `<rect>` 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 `<script>` may be simpler and more maintainable than wiring into Encore
**Detection:** JavaScript console shows 404 for your JS file. Widget renders but heatmap area is blank.
**Confidence:** MEDIUM (Kimai's asset pipeline has evolved; verify against current version)
**Phase relevance:** Phase 1 (scaffold) -- resolve asset loading before writing d3 code.
## Minor Pitfalls
### Pitfall 8: Color Scale Not Adapting to Kimai Themes
**What goes wrong:** Hard-coding heatmap colors (e.g., GitHub's green palette) that clash with Kimai's dark mode or custom themes. The heatmap looks fine in default theme but illegible in dark mode.
**Why it happens:** GitHub's green heatmap palette is the default in every tutorial. Kimai supports theme switching.
**Prevention:**
- Read Kimai's CSS custom properties / SCSS variables for primary/accent colors
- Build the color scale from CSS variables: `getComputedStyle(document.documentElement).getPropertyValue('--primary-color')`
- Use luminance-based interpolation (light background to saturated primary) rather than a fixed green palette
- Test in both light and dark Kimai themes
**Detection:** Visual review in dark mode -- colors wash out or become invisible.
**Confidence:** HIGH (standard theming concern)
**Phase relevance:** Phase 2 (visualization) -- implement theme-aware colors from the start.
### Pitfall 9: Missing Empty State Handling
**What goes wrong:** New users or users with sparse data see a completely blank widget. No indication that the widget is working -- looks broken.
**Why it happens:** Developers test with seeded data and never encounter the empty state.
**Prevention:**
- Show an empty state message: "No time entries in this period"
- Handle partial data gracefully: a year view where only 2 months have data should still render the full year grid with empty (but visible) cells
- Include the empty state in test fixtures
**Detection:** Install plugin on a fresh Kimai instance with no time entries.
**Confidence:** HIGH (universal UX concern)
**Phase relevance:** Phase 2 (visualization) -- implement alongside the heatmap.
### Pitfall 10: Clicking Day Cell -- Kimai URL Structure Assumptions
**What goes wrong:** Building the "click to navigate to timesheet" URL by guessing Kimai's route structure (`/en/timesheet?date=2026-04-08`). The URL structure depends on Kimai's locale prefix, routing configuration, and filter parameter names, which change between versions.
**Why it happens:** Hardcoding URLs instead of using Symfony's router to generate them.
**Prevention:**
- Generate the timesheet URL on the PHP side using Symfony's `UrlGeneratorInterface` / `router->generate()`
- Pass the URL template to JavaScript as a data attribute: `data-timesheet-url-template="/en/timesheet/?daterange={date}~{date}"`
- Let JS only do string interpolation on the template, never URL construction
**Detection:** Clicking a cell leads to 404 or wrong page. Breaks when Kimai locale changes.
**Confidence:** MEDIUM (URL parameter names need verification against current Kimai version)
**Phase relevance:** Phase 2/3 (interactivity) -- when implementing click-to-navigate.
### Pitfall 11: Testing d3.js Output in a Headless Environment
**What goes wrong:** PHPUnit cannot test JavaScript. Jest/Vitest cannot access a real Kimai DOM. Developers skip frontend tests entirely because "it's just a visualization."
**Why it happens:** d3.js renders SVG in the DOM. Testing SVG output requires either a DOM environment (jsdom) or snapshot testing. Neither feels natural.
**Prevention:**
- Use jsdom with Vitest to test d3 rendering: create a container, run your heatmap function, assert SVG structure (number of rects, correct date attributes, color classes)
- Separate data transformation (pure functions: input dates/hours, output cell data) from rendering (d3 DOM manipulation). Test transformations with plain unit tests -- these catch the important bugs
- Snapshot test the SVG output for regression detection, but keep snapshots small (test one month, not a full year)
**Detection:** Heatmap breaks after refactoring with no test catching it.
**Confidence:** HIGH (standard d3 testing approach)
**Phase relevance:** Phase 1 (TDD setup) -- establish the test infrastructure before writing d3 code.
## Phase-Specific Warnings
| Phase Topic | Likely Pitfall | Mitigation |
|-------------|---------------|------------|
| Dev environment (Phase 0) | Nix+PHP+Kimai setup eats days (Pitfall 5) | Timebox to 1 day. Fall back to Docker if stuck. |
| Scaffold (Phase 1) | Widget not appearing on dashboard (Pitfall 3) | Study existing plugins first. Get empty widget visible before anything else. |
| Scaffold (Phase 1) | Asset loading fails (Pitfall 7) | Start with inline `<script>` in Twig, migrate to proper assets after it works. |
| Data layer (Phase 1) | Timezone-wrong day aggregation (Pitfall 2) | Aggregate in PHP, send date strings. Write test comparing against Kimai's own report. |
| Visualization (Phase 2) | Hard-coded colors break in dark mode (Pitfall 8) | Use CSS variables from day one. |
| Visualization (Phase 2) | Full d3 import bloats bundle (Pitfall 4) | Import specific modules only. |
| Interactivity (Phase 2-3) | Hardcoded timesheet URLs break (Pitfall 10) | Generate URL template server-side. |
| Maintenance (ongoing) | Kimai update breaks plugin (Pitfall 1) | Pin version, integration test that boots kernel. |
## Sources
- Kimai documentation at kimai.org/documentation (not fetched -- web tools unavailable)
- Kimai GitHub repository structure and plugin examples (based on training data, not live verification)
- d3.js modular import documentation (d3js.org)
- General Symfony bundle development practices
- Nix PHP packaging ecosystem knowledge
**Confidence note:** All findings are based on training data (cutoff ~early 2025). Kimai's plugin API, asset pipeline, and widget system should be verified against the current Kimai release before implementation begins. The Kimai ecosystem moves fast and is primarily one-developer-driven, so APIs can shift without extended deprecation periods.

255
.planning/research/STACK.md Normal file
View file

@ -0,0 +1,255 @@
# Technology Stack
**Project:** Kimai Heatmap Plugin
**Researched:** 2026-04-08
**Note:** WebSearch/WebFetch unavailable. Recommendations based on training data (cutoff May 2025). Versions should be verified against current Kimai releases before starting development.
## Recommended Stack
### Core Framework (Kimai Plugin)
| Technology | Version | Purpose | Why | Confidence |
|------------|---------|---------|-----|------------|
| PHP | 8.2+ | Plugin runtime | Kimai 2.x requires PHP 8.1 minimum; 8.2 for current features and performance. Verify against Kimai's composer.json. | MEDIUM |
| Symfony | 6.4 LTS | Bundle framework | Kimai 2.x is built on Symfony 6.4 LTS. Plugins must match the host Symfony version exactly. | MEDIUM |
| Kimai | 2.x (latest) | Host application | Target the current stable release. Check github.com/kimai/kimai/releases before starting. | MEDIUM |
### Visualization
| Technology | Version | Purpose | Why | Confidence |
|------------|---------|---------|-----|------------|
| d3 | ^7.9 | Core visualization library | d3 v7 is the current stable. ESM-native, tree-shakeable. Use specific sub-modules, not the full bundle. | HIGH |
| d3-scale | ^4.0 | Color and position scales | Needed for mapping hours/counts to color intensities and day positions. | HIGH |
| d3-selection | ^3.0 | DOM manipulation | Core d3 pattern for binidng data to SVG elements. | HIGH |
| d3-time | ^3.1 | Date calculations | Week/day grid layout calculations for the calendar. | HIGH |
| d3-time-format | ^4.1 | Date formatting | Tooltip and axis labels. | HIGH |
| d3-scale-chromatic | ^3.1 | Color schemes | Provides sequential color scales (Greens, Blues) as starting points before mapping to Kimai theme vars. | HIGH |
| d3-shape | ^3.2 | Rect generation | For the heatmap cell rectangles. | HIGH |
**Do NOT use:**
- `cal-heatmap` or other d3-wrapper heatmap libraries: They add abstraction over d3 that limits customization (Kimai theme integration, click-to-navigate, toggle modes). Rolling your own with raw d3 modules is straightforward for a calendar heatmap and gives full control.
- `d3` full bundle import: Import only the sub-modules you need. Keeps the asset small and avoids polluting the Kimai frontend.
### Testing
| Technology | Version | Purpose | Why | Confidence |
|------------|---------|---------|-----|------------|
| PHPUnit | ^10.5 or ^11.0 | Backend tests | Kimai uses PHPUnit for its own tests. Match the version Kimai ships with in its dev dependencies. | MEDIUM |
| Vitest | ^3.0 | JS heatmap tests | Fast, ESM-native, works with d3's ESM modules out of the box. Jest struggles with ESM d3 imports without transformation config. | HIGH |
| jsdom | (via vitest) | DOM environment | Vitest's jsdom environment provides enough DOM for d3 selection/rendering tests without a browser. | HIGH |
**Do NOT use:**
- `Jest` for JS tests: d3 v7 is ESM-only. Jest's ESM support requires `--experimental-vm-modules` and transform config. Vitest handles ESM natively.
- `Cypress`/`Playwright` for the heatmap: Overkill for a single widget. SVG output assertions via jsdom + snapshot testing covers the rendering. Save E2E for integration testing against a running Kimai instance if needed later.
### Development Environment
| Technology | Version | Purpose | Why | Confidence |
|------------|---------|---------|-----|------------|
| Nix flake | - | Reproducible dev env | Matches Toph's NixOS infra. Provides PHP, Node, Composer, and a local Kimai instance. | HIGH |
| Composer | ^2.7 | PHP dependency management | Standard for Symfony/Kimai. | HIGH |
| npm | ^10.x | JS dependency management | For d3 modules and Vitest. Simpler than yarn/pnpm for a single-widget plugin. | HIGH |
| esbuild | ^0.24 | JS bundling | Bundle d3 modules into a single file for Kimai's asset pipeline. Faster than webpack, simpler config, handles ESM natively. | HIGH |
**Do NOT use:**
- `Webpack Encore`: Kimai's own frontend uses it, but for a plugin shipping a single JS file, esbuild is simpler. One build command, no Symfony Encore config to maintain.
- `Docker` for dev: The project spec calls for Nix flake. Docker would work but adds friction on NixOS and doesn't match the user's infrastructure.
### Database
No additional database needed. The plugin reads from Kimai's existing `kimai2_timesheet` table via Kimai's `TimesheetRepository` or a custom DQL query. No migrations required.
## Kimai Plugin Structure
This is the critical structural knowledge. A Kimai plugin is a Symfony bundle installed in `var/plugins/` (development) or via Composer (distribution).
### Directory Layout
```
KimaiHeatmapBundle/
KimaiHeatmapBundle.php # Bundle class, extends PluginInterface
DependencyInjection/
KimaiHeatmapExtension.php # Loads services config
Resources/
config/
services.yaml # Service definitions
views/
widget/
heatmap.html.twig # Widget template
public/
heatmap.js # Bundled d3 heatmap (esbuild output)
heatmap.css # Widget styles
EventSubscriber/
DashboardSubscriber.php # Registers widget on dashboard
Widget/
HeatmapWidget.php # Widget class implementing WidgetInterface
Repository/
HeatmapRepository.php # Data aggregation queries
composer.json # Package metadata
package.json # JS dependencies (d3, vitest)
esbuild.config.mjs # JS build config
assets/
src/
heatmap.ts # Source d3 heatmap code
test/
heatmap.test.ts # Vitest tests
tests/
Widget/
HeatmapWidgetTest.php # PHPUnit tests
Repository/
HeatmapRepositoryTest.php # PHPUnit tests
```
**Confidence: MEDIUM** -- This structure follows Kimai's documented plugin patterns as of my training data. Verify against the current plugin developer guide and existing plugins like `ExpensesBundle` or `CustomContentBundle` on GitHub.
### Bundle Registration
```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
{
}
```
Kimai auto-discovers bundles in `var/plugins/` by scanning for classes implementing `PluginInterface`. No manual kernel registration needed.
### Dashboard Widget Registration
Kimai uses an event-based widget system. You implement `WidgetInterface` and subscribe to the dashboard event.
```php
// Widget/HeatmapWidget.php
namespace KimaiPlugin\KimaiHeatmapBundle\Widget;
use App\Widget\Type\AbstractWidget;
use App\Widget\WidgetInterface;
class HeatmapWidget extends AbstractWidget
{
public function getTitle(): string
{
return 'Activity Heatmap';
}
public function getTemplateName(): string
{
return '@KimaiHeatmap/widget/heatmap.html.twig';
}
public function getData(array $options = []): mixed
{
// Query timesheet data, aggregate by day
// Return array of [date => hours/count]
}
}
```
**Confidence: MEDIUM** -- Widget API may have changed. Check `App\Widget\Type\AbstractWidget` and `App\Widget\WidgetInterface` in current Kimai source.
### Twig Template Pattern
```twig
{# Resources/views/widget/heatmap.html.twig #}
{% extends '@theme/widget.html.twig' %}
{% block widget_content %}
<div id="kimai-heatmap"
data-entries="{{ widget.data|json_encode }}"
data-base-url="{{ path('timesheet') }}">
</div>
{% endblock %}
{% block widget_javascript %}
<script src="{{ asset('bundles/kimaiheatmap/heatmap.js') }}"></script>
{% 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.**

View file

@ -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 `<script>` in Twig. Do not hook into Kimai's Webpack Encore. Start with inline script as fallback.
5. **Nix + PHP dev environment** -- Timebox to 1 day. Use SQLite. Set `COMPOSER_HOME` to a writable temp dir. Fall back to Docker if stuck.
## Implications for Roadmap
### Phase 0: Development Environment
**Rationale:** Everything blocks on a working Kimai instance with the plugin loaded. Nix+PHP setup is the highest-risk non-code task.
**Delivers:** `nix develop` shell with PHP 8.2, Composer, Node 22, local Kimai on SQLite, plugin symlinked into `var/plugins/`.
**Avoids:** Pitfall 5 (Nix+PHP complexity) -- timeboxed to 1 day with Docker fallback.
### Phase 1: Plugin Scaffold + Data Layer
**Rationale:** Proves the plugin loads, widget appears on dashboard, and data flows correctly. These are the hardest unknowns (Kimai plugin API, widget system, timezone aggregation).
**Delivers:** Empty widget visible on dashboard. API endpoint returning correct per-day aggregated JSON. PHPUnit tests for service and controller.
**Addresses:** Table stakes groundwork (no visible features yet, but the pipeline works end-to-end).
**Avoids:** Pitfalls 1, 2, 3, 7 (plugin API, timezone, widget system, asset loading).
### Phase 2: Core Heatmap Visualization
**Rationale:** With data flowing, build the actual product. The calendar grid with all table-stakes features is the MVP.
**Delivers:** d3 calendar heatmap rendering from API data. Tooltips, labels, empty state, click-through, theme integration.
**Addresses:** All table-stakes features from FEATURES.md.
**Avoids:** Pitfalls 4, 8, 9 (d3 bundle size, color theming, empty state).
### Phase 3: Interactivity and Polish
**Rationale:** Differentiators that make the widget genuinely useful beyond a static picture. Low complexity, high value.
**Delivers:** Hours/count toggle, project filter dropdown, configurable time range, streak indicator, summary stats.
**Addresses:** All differentiator features from FEATURES.md.
**Avoids:** Pitfall 10 (hardcoded URLs -- generate timesheet URL template server-side).
### Phase Ordering Rationale
- Phase 0 before everything: no plugin code without a working dev environment.
- Phase 1 combines scaffold + data because the scaffold alone is not testable in a meaningful way -- you need data flowing to confirm the widget system integration works.
- Phase 2 is pure frontend work that depends on Phase 1's API but is otherwise independent.
- Phase 3 layers interactivity onto an already-working heatmap. Each feature is independently shippable.
- This ordering front-loads risk: the hardest unknowns (Kimai plugin API, widget system, timezone handling) are resolved in Phase 1. Phases 2 and 3 use well-established d3 patterns with high confidence.
### Research Flags
Phases likely needing deeper research during planning:
- **Phase 0:** Kimai Nix setup is uncommon. May need to inspect current Kimai `composer.json` and PHP extension requirements live.
- **Phase 1:** Kimai's `WidgetInterface`, DI tags, and route registration need verification against the target Kimai version. The plugin API documentation is sparse -- reading existing plugin source code is essential.
Phases with standard patterns (skip research):
- **Phase 2:** d3 calendar heatmap is a well-documented pattern with official Observable examples. TypeScript + esbuild bundling is straightforward.
- **Phase 3:** Filter dropdowns, toggle buttons, and URL template interpolation are standard frontend work.
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | MEDIUM | PHP/Symfony/d3 choices are solid. Exact Kimai version requirements need live verification. |
| Features | MEDIUM-HIGH | GitHub-style heatmap is a proven pattern. Feature priorities are well-reasoned from reference products. |
| Architecture | MEDIUM | Symfony bundle + API + d3 rendering is sound. Kimai-specific widget API details (interface methods, DI tags, Twig blocks) need verification. |
| Pitfalls | MEDIUM-HIGH | Timezone and d3 pitfalls are universal and well-understood. Kimai-specific pitfalls (plugin API instability, asset pipeline) based on training data, not live docs. |
**Overall confidence:** MEDIUM
### Gaps to Address
- **Kimai widget API verification**: The exact `WidgetInterface` methods, `AbstractWidgetType` hierarchy, and `kimai.widget` DI tag must be verified against the target Kimai release. This is the single biggest unknown.
- **Kimai asset serving for plugins**: Whether Kimai uses `assets:install` to copy `Resources/public/` to `public/bundles/` or has a different mechanism needs checking.
- **Kimai CSS variable names**: The actual theme variable names for colors are unknown. Fallback values mitigate this, but proper theme integration requires inspecting a running Kimai instance.
- **Kimai timesheet URL structure**: The route name and filter parameter format for click-through navigation must be verified.
- **Twig widget template blocks**: The exact block names (`widget_content`, `widget_javascript`) and base template path need verification.
## Sources
### Primary (HIGH confidence)
- d3.js documentation and calendar heatmap patterns (d3js.org, Observable)
- Symfony bundle system documentation (symfony.com)
- Vitest and esbuild documentation
- General timezone handling and SVG performance patterns
### Secondary (MEDIUM confidence)
- Kimai plugin development documentation (kimai.org/documentation/plugin-development.html) -- from training data
- Kimai GitHub repository structure and existing plugins -- from training data
- Kimai widget system internals -- from training data
### Tertiary (LOW confidence)
- Kimai Twig template block names and asset path conventions -- inferred, needs validation
- Kimai CSS custom property names -- inferred, needs validation
---
*Research completed: 2026-04-08*
*Ready for roadmap: yes*