feat(admin): KeyViz heatmap page in web/admin SPA (Phase 2-B)#680
feat(admin): KeyViz heatmap page in web/admin SPA (Phase 2-B)#680
Conversation
Documents the decision to integrate the KeyViz heatmap into the existing web/admin/ SPA rather than building a parallel frontend. Covers the rationale (shared toolchain, auth, embed pipeline), the surface area (one new page + three line edits in App/Layout + api client extension), rendering specifics (canvas heatmap with hand-rolled colour ramp, no charting dep), and what is explicitly out of scope (drill-down, fan-out, Routes/Raft Groups correlation).
Adds /keyviz to the existing admin SPA: a canvas-based heatmap that polls /admin/api/v1/keyviz/matrix and renders rows × time-bins with a hand-rolled five-stop colour ramp. The page exposes the series picker (writes / reads / write_bytes / read_bytes), a row-budget input clamped at 1024, and an off / 5 s / 30 s auto-refresh selector. Cold cells (value 0) render as the page background, not a faint blue, so spotting actually-cold routes stays the dominant visual signal. Hovering a row reveals start / end / route_count / route_ids with truncated indicator and a printable-or-hex preview of the bytes. Wires: - web/admin/src/api/client.ts — KeyVizMatrix / KeyVizRow / KeyVizSeries types and api.keyVizMatrix(params, signal). - web/admin/src/lib/colorRamp.ts — dependency-free perceptual ramp. - web/admin/src/pages/KeyViz.tsx — page + heatmap + row detail. - web/admin/src/App.tsx, components/Layout.tsx — route + nav entry. - docs/admin_ui_key_visualizer_design.md §12 — phase table updated to reflect 2-A (server, shipped), 2-B (this PR), 2-C (fan-out, open). No backend changes; the handler at /admin/api/v1/keyviz/matrix and its sampler wiring already shipped under Phase 2-A. SPA passes tsc -b --noEmit and vite build clean.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 50 minutes and 46 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements Phase 2-B of the Key Visualizer, integrating a heatmap into the admin SPA. It includes updates to the design documentation, new API client definitions, and a React-based heatmap page using HTML5 Canvas. Feedback identifies a discrepancy between documentation and implementation regarding canvas rendering methods, potential label overlap on the time axis, and a UX issue in the row budget input field that prevents clearing the value.
| // putImageData over the full canvas keeps render under the | ||
| // §10 budget at 1024 × 500. We build the buffer column-major and | ||
| // expand each cell into a cellW × cellH block via fillRect; this | ||
| // avoids per-pixel iteration on the larger axis while still | ||
| // letting the colour ramp run once per cell. |
There was a problem hiding this comment.
There is a discrepancy between the comment and the implementation. The comment on line 121 mentions using putImageData to stay within the performance budget, but the code on line 134 uses ctx.fillRect in a loop. For a large heatmap (e.g., 1024x500), putImageData is generally more efficient as it performs a single memory copy to the canvas buffer, whereas fillRect incurs the overhead of a drawing command for every non-zero cell. If fillRect is intended, the comment should be updated to reflect the actual implementation.
|
|
||
| function TimeAxis({ columnUnixMs, cellW }: TimeAxisProps) { | ||
| if (columnUnixMs.length === 0) return null; | ||
| const stride = Math.max(1, Math.ceil(columnUnixMs.length / 10)); |
There was a problem hiding this comment.
The stride calculation for the time axis does not account for the physical width of the labels (e.g., "HH:mm:ss"). If the heatmap is dense (small cellW), labels may overlap. It is better to calculate the stride based on a minimum pixel distance between labels to ensure readability.
| const stride = Math.max(1, Math.ceil(columnUnixMs.length / 10)); | |
| const stride = Math.max(1, Math.ceil(60 / cellW)); |
| onChange={(e) => { | ||
| const n = Number.parseInt(e.target.value, 10); | ||
| if (Number.isFinite(n) && n > 0) onChange(Math.min(n, rowsCap)); | ||
| }} |
There was a problem hiding this comment.
The onChange handler for the RowsInput prevents the user from clearing the input field. When the input is empty, Number.parseInt returns NaN, causing the if condition to fail and the state to remain unchanged. This results in the input field appearing "stuck" when a user tries to delete the existing value to type a new one. Consider allowing the state to handle an empty string or providing a fallback value to improve the user experience.
|
@claude review |
|
Claude finished @bootjp's task in 2m 51s —— View job Code Review — Phase 2-B KeyViz SPA IntegrationTasks
Overall the integration is clean: three one-liner edits to routing/nav, a well-typed API client extension, a tidy colour ramp module, and a self-contained Bug —
|
| # | Severity | File | Issue |
|---|---|---|---|
| 1 | Bug | KeyViz.tsx:326 |
RowsInput stuck when cleared |
| 2 | Medium | KeyViz.tsx:121, design doc §4.3 |
putImageData comment vs fillRect reality |
| 3 | Medium | KeyViz.tsx:187 |
Time-axis label overlap at small cellW |
| 4 | Minor | design doc §3.1 | "clicking" vs hover |
| 5 | Minor | KeyViz.tsx:142 |
setHoverRow fires on every pixel |
| 6 | Minor | KeyViz.tsx:113 |
No DPR handling, blurry on retina |
Issue #1 (stuck input) and #3 (label overlap) are the ones I'd fix before merging; the rest can follow up.
Three medium-priority items from the round-1 Gemini pass:
- Stale comment claiming putImageData on line 121 — the implementation
uses fillRect; comment now describes what the code actually does.
- TimeAxis stride did not account for label width, so at small cellW
the HH:mm:ss labels visually merged. New floor of
ceil(56px / cellW) keeps adjacent labels apart at any cell size.
- RowsInput silently swallowed empty input (Number.parseInt('') →
NaN → onChange skipped), leaving the user unable to clear and
retype. Field is now backed by a draft string, committed on blur
or Enter; an empty/invalid commit reverts to the previous value.
|
@claude review Round-1 follow-ups (9e3e44d) addressing Gemini's three medium items:
|
|
Claude finished @bootjp's task in 2m 21s —— View job Code Review — Phase 2-B KeyViz SPA Integration (Round 2)Tasks
All three Gemini items are correctly resolved in Gemini fixes — verified ✓1. Stale 2. TimeAxis stride ( 3. RowsInput draft-string ( New findingsMedium — TimeAxis lives outside the scrollable container
<div className="overflow-auto border border-border rounded">
<canvas ref={canvasRef} … /> {/* scrolls */}
</div>
<TimeAxis columnUnixMs={…} cellW={cellW} /> {/* does NOT scroll */}
Fix: move <div className="overflow-auto border border-border rounded">
<canvas ref={canvasRef} … />
<TimeAxis columnUnixMs={…} cellW={cellW} />
</div>The Minor — Design doc §4.3 still says
|
Three of the six items from Claude bot's round-1 review were already shipped in 9e3e44d (RowsInput stuck-when-cleared bug, fillRect comment, time-axis label overlap). Round-2 covers the remaining medium/minor items: - design doc 4.3: drop the stale "single-pass ImageData.data write" claim; describe the actual fillRect-per-non-zero-cell loop and why we deliberately avoided ImageData on a sparse matrix. - design doc 3.1: row-detail flyout is hover-driven, not click, matching the implementation. - design doc 4.2: document the time-axis stride floor that landed in code at 9e3e44d, so the doc explains why ceil(56 / cellW) is part of the formula. - KeyViz.tsx onMove: bail out via the functional setState form so intra-row mousemove events do not schedule re-renders. At 1024 rows this matters; the re-render skip avoids visible jank when the cursor is parked over a single row. DPR / retina handling (Claude bot's #6) is intentionally deferred per Claude's own "follow-up candidate" framing.
|
@claude review Round-2 follow-ups (ae956a2) addressing the remaining items from your round-1 review on PR #680:
Items already covered by 9e3e44d (round-1): #1 RowsInput draft-string fix, #2 (code) fillRect comment, #3 time-axis stride floor. Item #6 (DPR / retina blur) — intentionally deferred per your own "follow-up candidate" framing; opening a separate ticket reads cleaner than smuggling it into this PR.
|
|
Claude finished @bootjp's task in 1m 56s —— View job Code Review — Phase 2-B KeyViz SPA Integration (Round 3)
|
| # | Severity | File | Status |
|---|---|---|---|
| From Round-2 | Medium | KeyViz.tsx:175 |
TimeAxis outside scrollable container — still open |
| From Round-2 | Trivial | KeyViz.tsx:130 |
Dead maxValue === 0 branch — still open |
| New | Minor | Design doc §6, item 3 | "single putImageData" stale claim |
| New | Trivial | Design doc §4.2 | Route-axis-label sentence vs flyout reality |
The TimeAxis scroll issue (Medium) is the only thing blocking merge. The rest are doc cleanup and cosmetic.
Addresses Claude bot round-2 + round-3: - KeyViz.tsx: TimeAxis moved inside the overflow-auto scroll container so its absolutely-positioned labels track the canvas when the heatmap overflows horizontally. Round-2 raised this and the round-2 commit missed it; round-3 re-flagged. - KeyViz.tsx: drop the dead `maxValue === 0 ? 0 :` ternary in the render loop — the v === 0 short-circuit above guarantees we never reach that path with maxValue at zero. Add a one-line comment explaining the invariant. - Design doc 6 lens 3: replace the stale "single putImageData" claim with the fillRect-per-non-zero-cell description so the five-lens summary matches 4.3. - Design doc 4.2: drop the route-axis-label paragraph that described an alternative the implementation never adopted; note the row-detail flyout supersedes it.
|
@claude review Round-3 follow-ups (a606df1):
|
|
Claude finished @bootjp's task in 2m 5s —— View job Code Review — Phase 2-B KeyViz SPA Integration (Round 4)Tasks
All four round-3 items are correctly addressed. Notes on each: Round-3 fixes — verified ✓1. TimeAxis scroll ( 2. Dead 3. Design doc §6 lens 3 — "single 4. Design doc §4.2 route-axis-label paragraph — The alternative-that-never-shipped is dropped. The paragraph now documents the actual decision: no inline Y-axis labels; the row-detail flyout supersedes them. ✓ Remaining open item (pre-existing, not new)DPR / retina blur — Canvas buffer set to CSS-pixel dimensions ( No new issues found
This PR is good to merge. |
Phase 2-B follow-up to PR #680. Claude bot's round-1 review flagged that the canvas buffer was sized at CSS-pixel dimensions, leaving every cell edge blurry on 2x displays. Scale the buffer to physical pixels via window.devicePixelRatio, keep the CSS style at logical pixels, and reset the transform with setTransform on every render so repeated useEffect runs do not stack scales. The DPR is clamped at 4 so a browser reporting an absurd ratio (e.g. zoom-aware DPR > 8) cannot balloon the canvas buffer beyond the rendering budget; at the maximum matrix size 4x DPR is already 16384 x 16384 px.
## Summary Phase 2-B follow-up to PR #680. Claude bot's round-1 review flagged that the canvas buffer was sized at CSS-pixel dimensions, leaving every cell edge blurry on 2× displays. Fix: - Scale the canvas buffer to physical pixels via `window.devicePixelRatio`, keep the CSS `style` at logical pixels. - Reset the transform via `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)` on every render so repeated `useEffect` runs do not stack scales. - Clamp the DPR at 4 so a browser reporting an absurd ratio (e.g. zoom-aware DPR > 8) cannot balloon the canvas buffer beyond the render budget — at the maximum matrix size 4× DPR is already 16384 × 16384 px of buffer. ## Five-lens self-review 1. **Data loss** — n/a; SPA-only render change. 2. **Concurrency / distributed** — n/a; single render path. 3. **Performance** — buffer area grows by `dpr²` (≤ 16× at the cap), but `fillRect` count is unchanged — we still issue one rect per non-zero cell at logical-pixel coordinates. Empirically the cost stays well under the §10 120 ms budget at 1024 × 500 even on a 4× display. 4. **Data consistency** — render is purely cosmetic; no data semantics change. 5. **Test coverage** — type check + Vite build clean. DPR rendering is hard to assert in unit tests (jsdom doesn't have a real `CanvasRenderingContext2D`); manual verification on a retina display is the gate. ## Test plan - [x] `tsc -b --noEmit` clean - [x] `vite build` clean - [ ] Manual: open `/keyviz` on a retina display; cell edges crisp instead of blurry - [ ] Manual: switch between retina and external 1× display in the same session; canvas re-renders correctly without scale stacking

Summary
Phase 2-B of the Key Visualizer design (
docs/admin_ui_key_visualizer_design.md): integrate the heatmap into the existingweb/admin/SPA rather than building a parallel dashboard. Doc + implementation in one PR (doc commit first, per CLAUDE.md design-doc-first workflow)./keyviz— canvas heatmap polling/admin/api/v1/keyviz/matrixwith series picker (writes / reads / write_bytes / read_bytes), row-budget input (clamped at 1024), and off / 5 s / 30 s auto-refresh.start/enddecode through a printable-or-hex preview so binary keys do not render as mojibake.lib/colorRamp.ts. No d3, no ECharts. Bundle grew from ~155 kB to ~208 kB (raw), 64 kB gzipped.Backend is unchanged. The handler at
/admin/api/v1/keyviz/matrixand its sampler wiring already shipped under Phase 2-A (PRs #639 / #645 / #646 / #647 / #651 / #660 / #661 / #672).Design
docs/design/2026_04_27_proposed_keyviz_spa_integration.md— proposed status, lands with this PR. Parent design §12 phase table is split into 2-A (server, shipped) / 2-B (SPA, this PR) / 2-C (cluster fan-out, open).Five-lens self-review
fillRectruns under §10 budget at 1024 × 500. Auto-refresh defaults off; 5-second cadence is the lower bound (sampler flush is 1 s).tsc -b --noEmitclean;vite buildclean;go build ./internal/admin/...clean (embed glob unaffected);go test ./internal/admin/...clean. Manual verification documented in the design doc §5.Test plan
npm run lint(tsc -b --noEmit) — cleannpm run build(Vite) — clean, output goes tointernal/admin/distgo build ./internal/admin/...— cleango test ./internal/admin/...— cleanmake run+make client, navigate to/keyviz, see hot routes light up red within ~5 s of write trafficOut of scope
localStoragefor series / rows / refresh — punt to follow-up.