# Domain Pitfalls **Domain:** Kimai heatmap plugin v1.1 -- visualization modes, TomSelect entity pickers, cascading filters, display toggle **Researched:** 2026-04-08 **Confidence:** HIGH (based on direct source analysis of existing plugin code and Kimai's KimaiFormSelect internals) ## Critical Pitfalls Mistakes that cause rewrites or major issues. ### Pitfall 1: KimaiFormSelect Depends on Kimai's Plugin Container -- Cannot Use It Standalone **What goes wrong:** SEED-001 suggests using Kimai's `KimaiFormSelect.js` with `data-api-url`, `data-related-select`, and `data-empty-url` attributes to get cascading for free. But `KimaiFormSelect._activateApiSelects()` calls `this.getContainer().getPlugin('api')` to make API requests (line 433-435). This requires the select to be inside a form that Kimai's JS plugin system initialized via `activateForm()`. The heatmap widget is a dashboard card, not a Symfony form. **Why it happens:** Kimai's form system (`KimaiFormSelect`, `KimaiFormPlugin`) is tightly coupled to Kimai's JS plugin container and form lifecycle. The cascading logic in `_activateApiSelects` registers a delegated `change` event listener on `document`, but the handler calls `this.getContainer().getPlugin('api')` which requires the KimaiFormSelect instance to have been created by Kimai's app initialization. Our widget's IIFE bundle runs independently. **Consequences:** Adding `data-api-url` and `data-related-select` attributes to `` elements styled with Tabler CSS. Add TomSelect only if the UX demands it. **Detection:** Change the customer picker and observe whether the project picker updates. If it does not, you hit this. ### Pitfall 2: Destroying and Recreating SVG Without Cleaning Up Tooltips **What goes wrong:** The current `renderHeatmap()` starts with `container.innerHTML = ''` (line 184) but tooltips are appended to `document.body` (line 267). When switching visualization modes (year -> week -> day), each render creates a new tooltip. The existing cleanup at line 263 (`document.querySelectorAll('.heatmap-tooltip').forEach(el => el.remove())`) only runs inside `renderHeatmap` -- if new modes have their own render functions without this cleanup, tooltips accumulate. **Why it happens:** The tooltip is necessarily outside the SVG container (fixed positioning to escape overflow clipping). Each render function must know to clean up the shared tooltip. **Consequences:** Multiple `.heatmap-tooltip` divs on the page. Stale tooltip positioning. Memory leaks on repeated mode switches. **Prevention:** Extract tooltip into a shared module that all visualization modes import. One tooltip div, created on first use, reused across mode switches. A single `cleanup()` function that any render path calls before rebuilding. **Detection:** Switch modes 5+ times, inspect `document.body` children for orphaned `.heatmap-tooltip` elements. ### Pitfall 3: Hour-Level Aggregation Query Performance **What goes wrong:** Day-mode and combined day/hour views need time-of-day data. The current `getDailyAggregation()` groups by `DATE(t.date)` which benefits from Kimai's date index. An hour-level query (`HOUR(t.begin)` or `EXTRACT(HOUR FROM t.begin)`) applies a function to the column, defeating index usage. GROUP BY HOUR over a full year of data scans every row for that user. **Why it happens:** MySQL/MariaDB cannot use an index when a function wraps the indexed column. **Consequences:** Slow queries for users with thousands of entries. For personal use (likely <10K entries) it is noticeable latency (~200-500ms), not catastrophic. But it makes mode switching feel sluggish. **Prevention:** 1. Limit hour-level queries to a narrow date range (one week or one month, not a full year). The UI for day-mode should show a specific date range anyway. 2. For the combined matrix, fetch raw timesheet entries for the selected range and aggregate in PHP rather than doing a complex GROUP BY. 3. Profile with `EXPLAIN` on the actual query against the dev database. **Detection:** Mode switch to day-view takes noticeably longer than year-view load. ## Moderate Pitfalls ### Pitfall 4: Mode State Lost on Filter Change **What goes wrong:** User switches to week-mode, then changes the project filter. The filter's fetch callback re-renders the visualization, but defaults back to year-mode because the mode selection is not part of the render state. The current `doRender()` function (line 339-344) always calls `renderHeatmap()` -- the year view. **Why it happens:** The current architecture has no shared state object. Mode and filter are independent UI controls that both trigger re-renders, but neither knows about the other's state. **Consequences:** User frustration. Every filter change resets the view mode. Feels broken. **Prevention:** Introduce a state object BEFORE adding the first new mode: ```typescript interface WidgetState { mode: 'year' | 'week' | 'day' | 'combined'; metric: 'hours' | 'count'; filters: { customerId: number | null; projectId: number | null; activityId: number | null }; data: HeatmapData | null; } ``` All UI actions (mode switch, filter change, metric toggle) update state, then call a single `render(state)` dispatcher that picks the right visualization function. **Phase warning:** This refactor MUST happen before adding any new visualization mode. Retrofitting state management after multiple modes exist is painful and error-prone. ### Pitfall 5: Cascading API Response Format Assumptions **What goes wrong:** Kimai's `/api/projects?customer=X` returns objects with `parentTitle` (customer name), `id`, `name`, and more. Activities can be "global" (no project parent). If you build your own cascade, you must handle this format correctly -- or your own simplified format if you use custom controller endpoints. **Why it happens:** Kimai's API response shape is designed for `KimaiFormSelect._updateSelect()` which groups by `parentTitle` into optgroups. Rolling your own cascade means either parsing the same format or defining your own. **Prevention:** Use custom controller endpoints (like the existing `getUserProjects()`) that return a simple `[{id, name}]` format. This avoids parsing Kimai's complex API response structure with optgroups and `parentTitle`. Handle edge cases: - Customer with no projects -> empty project list - Project with no activities -> empty activity list - Global activities (no project association) -> include them when no project filter is set ### Pitfall 6: Color Scale Domain Mismatch Across Modes **What goes wrong:** Year-mode uses `max(data.days, d => d.hours)` as the scale ceiling (typically 8-12). Week-mode aggregates by day-of-week over a year (totals could be 200+). Day-mode shows hour slots (0-3 range). Reusing one color scale makes week-mode cells all dark and day-mode cells all light. **Why it happens:** Each mode has fundamentally different value ranges. Easy to forget when the scale is set up once in shared code. **Prevention:** Each mode computes its own color scale domain from its own data. Extract scale creation into a factory function that accepts a data array. Never share a global scale instance. ### Pitfall 7: TomSelect Bundle Size Duplication **What goes wrong:** Importing `tom-select` in `heatmap.ts` and bundling with esbuild ships a second copy (~50KB min) alongside Kimai's own TomSelect that Webpack Encore already loaded. **Why it happens:** The plugin ships a standalone IIFE bundle independent of Kimai's module system. No shared module resolution between them. **Prevention:** Do NOT bundle TomSelect. Options: 1. **Best for personal use:** Plain `