docs(07): add code review report
This commit is contained in:
parent
12e734a911
commit
ad2ed0b45e
1 changed files with 106 additions and 0 deletions
106
.planning/phases/07-mode-switcher-week-mode/07-REVIEW.md
Normal file
106
.planning/phases/07-mode-switcher-week-mode/07-REVIEW.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
phase: 07-mode-switcher-week-mode
|
||||
reviewed: 2026-04-09T12:00:00Z
|
||||
depth: standard
|
||||
files_reviewed: 9
|
||||
files_reviewed_list:
|
||||
- Resources/public/heatmap.css
|
||||
- Resources/public/heatmap.js
|
||||
- Resources/views/widget/heatmap.html.twig
|
||||
- assets/src/heatmap.ts
|
||||
- assets/src/renderers/week.ts
|
||||
- assets/src/renderers/year.ts
|
||||
- assets/src/ui/controls.ts
|
||||
- assets/test/controls.test.ts
|
||||
- assets/test/week.test.ts
|
||||
findings:
|
||||
critical: 0
|
||||
warning: 3
|
||||
info: 2
|
||||
total: 5
|
||||
status: issues_found
|
||||
---
|
||||
|
||||
# Phase 7: Code Review Report
|
||||
|
||||
**Reviewed:** 2026-04-09T12:00:00Z
|
||||
**Depth:** standard
|
||||
**Files Reviewed:** 9
|
||||
**Status:** issues_found
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 7 adds a mode switcher (year/week toggle) and metric toggle (hours/count) to the heatmap widget, plus a new week-mode renderer that aggregates time data by weekday. The code is well-structured: clean renderer registry pattern, good separation of concerns between UI controls and renderers, solid test coverage for the new components.
|
||||
|
||||
Three warnings found -- two are potential bugs related to tooltip XSS and missing null safety, one is a subtle interaction issue with the renderer lifecycle. Two minor info items for code quality.
|
||||
|
||||
## Warnings
|
||||
|
||||
### WR-01: Tooltip innerHTML allows XSS via project/weekday names
|
||||
|
||||
**File:** `assets/src/shared/tooltip.ts:18`, `assets/src/renderers/week.ts:134`, `assets/src/renderers/year.ts:114`
|
||||
**Issue:** `showTooltip` sets `tip.innerHTML = html`, and the callers build HTML strings by interpolating data values. In week.ts, `agg.label` comes from hardcoded weekday names (safe), but in year.ts line 114, `DISPLAY_FORMAT(d.date)` is also safe since it formats a Date object. However, `showTooltip` itself is a shared utility -- any future caller interpolating user-controlled data (e.g., project names from the server) would be vulnerable to stored XSS. The pattern is fragile.
|
||||
**Fix:** Consider using `textContent` for data values and building DOM nodes instead of HTML strings, or at minimum add an escaping utility:
|
||||
```typescript
|
||||
function escapeHtml(s: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
### WR-02: Renderer destroy() is optional but called unconditionally
|
||||
|
||||
**File:** `assets/src/heatmap.ts:69`
|
||||
**Issue:** Line 69 calls `renderer.destroy?.()` on the *new* renderer (just fetched via `getRenderer(state.mode)`), not the *previous* renderer. When switching from year to week mode, the year renderer's tooltip is never cleaned up -- `destroy()` is called on the week renderer instance (which hasn't rendered yet, so it's a no-op). The previous mode's tooltip element leaks in the DOM.
|
||||
**Fix:** Track the active renderer and destroy it before switching:
|
||||
```typescript
|
||||
let activeRenderer: ModeRenderer | null = null;
|
||||
|
||||
const doRender = () => {
|
||||
if (!state.data) return;
|
||||
activeRenderer?.destroy?.();
|
||||
activeRenderer = getRenderer(state.mode);
|
||||
activeRenderer.render({ /* ... */ });
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### WR-03: Color scale domain starts at 0, producing color for zero-value entries
|
||||
|
||||
**File:** `assets/src/shared/color-scale.ts:15`
|
||||
**Issue:** The quantize scale domain is `[0, maxVal]`, which means a value of exactly 0 maps to the lightest green (`#9be9a8`). In the week renderer this is mitigated by the `hasData` guard (line 115-118 in week.ts), and in year.ts by the `if (!d.entry)` check (line 107). But if a DayEntry exists with `hours: 0` and `count: 0`, it would render with a green fill rather than appearing empty. This is an edge case but a data-truthfulness issue.
|
||||
**Fix:** Either filter zero-value entries in the color-scale builder, or add a guard in the renderers:
|
||||
```typescript
|
||||
// In year.ts, line 107:
|
||||
if (!d.entry || (ctx.state.metric === 'hours' ? d.entry.hours : d.entry.count) === 0) return '';
|
||||
```
|
||||
|
||||
## Info
|
||||
|
||||
### IN-01: Duplicate stats-removal logic in doRender
|
||||
|
||||
**File:** `assets/src/heatmap.ts:78-83`
|
||||
**Issue:** Lines 78-83 have two consecutive `if (state.mode === 'year')` blocks that could be combined into a single if/else:
|
||||
```typescript
|
||||
if (state.mode === 'year') {
|
||||
renderStats(container, state.data.days);
|
||||
svgArea.scrollLeft = svgArea.scrollWidth;
|
||||
} else {
|
||||
const existingStats = container.querySelector('.heatmap-stats');
|
||||
if (existingStats) existingStats.remove();
|
||||
}
|
||||
```
|
||||
**Fix:** Merge the two conditionals as shown above.
|
||||
|
||||
### IN-02: `createMetricControl` is a thin wrapper that adds no value
|
||||
|
||||
**File:** `assets/src/ui/controls.ts:33-42`
|
||||
**Issue:** `createMetricControl` simply calls `createModeControl` with hardcoded hours/count options. This is fine as a convenience function, but the naming is slightly misleading -- it suggests a different control type when it's really just pre-configured mode buttons. No action needed, just noting for awareness.
|
||||
**Fix:** None required. Could add a brief doc comment clarifying it's a convenience wrapper.
|
||||
|
||||
---
|
||||
|
||||
_Reviewed: 2026-04-09T12:00:00Z_
|
||||
_Reviewer: Claude (gsd-code-reviewer)_
|
||||
_Depth: standard_
|
||||
Loading…
Add table
Reference in a new issue