Skip to content

feat(dashboard): persist per-table snapshot phase timing after completion - #258

Merged
cnlangzi merged 2 commits into
mainfrom
feat/snapshot-dashboard-timing-breakdown
May 13, 2026
Merged

feat(dashboard): persist per-table snapshot phase timing after completion#258
cnlangzi merged 2 commits into
mainfrom
feat/snapshot-dashboard-timing-breakdown

Conversation

@cnlangzi

Copy link
Copy Markdown
Owner

Summary

After a snapshot finishes, the dashboard timing panel now keeps a per-table breakdown (Read / Build / Transform / Write + total), so users still see each table's phase timings alongside the aggregate cards.

Changes

  • API: Add done to each entry in /api/snapshot/status tables array (TableProgress.Done in snapshot capturer).
  • UI: Add timing-breakdown table under Phase Timing; populate during run and on completion (showSummaryPanel).
  • UI: hideRunningPanel() no longer hides the timing panel (avoids losing breakdown when switching from running to completed).

Testing

  • go test ./... (via pre-commit hook before lint step).
  • Manual: run snapshot, confirm breakdown visible after Snapshot Completed.

Made with Cursor

- Add TableProgress.Done and expose done in snapshot status API
- Render A/B/C/D breakdown table in Phase Timing panel (running + completed)
- Stop hiding timing panel when closing running panel

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93669bb0-b2f0-49a1-b378-9167c56fa427

📥 Commits

Reviewing files that changed from the base of the PR and between bcc2c2b and f92c287.

📒 Files selected for processing (1)
  • cmd/app/dashboard/pages/snapshot.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/app/dashboard/pages/snapshot.html

Walkthrough

This PR exposes per-table completion (Done) from the capturer, includes done in /api/snapshot/status, adds a #timing-breakdown container and a renderTimingBreakdown(tables, state) renderer that shows per-table phase durations and totals, and integrates the renderer into initialization, showSummaryPanel, and updateRunningUI for live and summary displays.

Poem

🐰 I hopped through lines of code today,

Tables timed in A→D display,
Read, Build, Transform, Write in view,
Done or waiting — all clear and true,
Dashboard hums, the timings play ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main change: adding and persisting per-table snapshot phase timing display after completion.
Description check ✅ Passed The description is well-organized and explains the feature, API changes, UI updates, and testing approach—all related to the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In renderTimingBreakdown, the t.table value is interpolated directly into HTML; if table names can be influenced by users or external systems, consider HTML-escaping the name or using a safer DOM construction pattern to avoid potential XSS.
  • renderTimingBreakdown rebuilds the entire table and sets innerHTML on every status update; if this function is called frequently or for many tables, consider a more incremental update strategy (or at least reusing the SVG icon markup outside the function) to reduce unnecessary allocations and DOM work.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `renderTimingBreakdown`, the `t.table` value is interpolated directly into HTML; if table names can be influenced by users or external systems, consider HTML-escaping the name or using a safer DOM construction pattern to avoid potential XSS.
- `renderTimingBreakdown` rebuilds the entire table and sets `innerHTML` on every status update; if this function is called frequently or for many tables, consider a more incremental update strategy (or at least reusing the SVG icon markup outside the function) to reduce unnecessary allocations and DOM work.

## Individual Comments

### Comment 1
<location path="cmd/app/dashboard/pages/snapshot.html" line_range="318-324" />
<code_context>
 function showSummaryPanel(data) {
     document.getElementById('panel-summary').classList.remove('hidden');
     showTimingPanel();
+    if (data.tables && data.tables.length > 0) {
+        document.getElementById('run-read-ms').textContent = fmtMs(data.read_ms);
+        document.getElementById('run-build-ms').textContent = fmtMs(data.build_ms);
+        document.getElementById('run-transform-ms').textContent = fmtMs(data.transform_ms);
+        document.getElementById('run-write-ms').textContent = fmtMs(data.write_ms);
+        document.getElementById('run-rows-per-sec').textContent = data.read_rows_per_sec != null ? data.read_rows_per_sec.toFixed(1) + ' r/s' : '—';
+        renderTimingBreakdown(data.tables, data.state || 'completed');
+    }
     document.getElementById('sum-tables').textContent = (data.processed || 0) + ' / ' + (data.total || 0);
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle the case where there are no tables by clearing/ resetting timing metrics and breakdown.

Because we only update these fields when `data.tables && data.tables.length > 0`, a snapshot with zero tables will keep showing values from the previous run. Please add an `else` that resets the `run-*` fields (e.g., to ``) and clears `timing-breakdown` so the panel doesn’t show stale data.
</issue_to_address>

### Comment 2
<location path="cmd/app/dashboard/pages/snapshot.html" line_range="436" />
<code_context>
+            ? '<span class="font-semibold ' + (isDone ? 'text-text' : 'text-primary') + '">' + fmtMs(total) + '</span>'
+            : '<span class="text-textMuted/40">—</span>';
+        return '<tr class="border-b border-border/40 last:border-0 ' + rowBg + '">'
+            + '<td class="py-2 pr-4 whitespace-nowrap text-sm ' + nameColor + ' font-medium">' + icon + t.table + '</td>'
+            + '<td class="py-2 px-3 text-center font-mono text-xs">' + fmtMs(t.read_ms) + '</td>'
+            + '<td class="py-2 px-3 text-center font-mono text-xs">' + fmtMs(t.build_ms) + '</td>'
</code_context>
<issue_to_address>
**🚨 issue (security):** HTML-escape `t.table` before injecting it into `innerHTML` to avoid markup injection.

Concatenating `t.table` into an HTML string and assigning it via `innerHTML` means any `<`, `>`, or `&` in the table name can break the markup or allow injected HTML. Please either HTML-escape `t.table` before concatenation or build these cells via `createElement`/`textContent` to close off this injection vector.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +318 to +324
if (data.tables && data.tables.length > 0) {
document.getElementById('run-read-ms').textContent = fmtMs(data.read_ms);
document.getElementById('run-build-ms').textContent = fmtMs(data.build_ms);
document.getElementById('run-transform-ms').textContent = fmtMs(data.transform_ms);
document.getElementById('run-write-ms').textContent = fmtMs(data.write_ms);
document.getElementById('run-rows-per-sec').textContent = data.read_rows_per_sec != null ? data.read_rows_per_sec.toFixed(1) + ' r/s' : '—';
renderTimingBreakdown(data.tables, data.state || 'completed');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Handle the case where there are no tables by clearing/ resetting timing metrics and breakdown.

Because we only update these fields when data.tables && data.tables.length > 0, a snapshot with zero tables will keep showing values from the previous run. Please add an else that resets the run-* fields (e.g., to ) and clears timing-breakdown so the panel doesn’t show stale data.

? '<span class="font-semibold ' + (isDone ? 'text-text' : 'text-primary') + '">' + fmtMs(total) + '</span>'
: '<span class="text-textMuted/40">—</span>';
return '<tr class="border-b border-border/40 last:border-0 ' + rowBg + '">'
+ '<td class="py-2 pr-4 whitespace-nowrap text-sm ' + nameColor + ' font-medium">' + icon + t.table + '</td>'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): HTML-escape t.table before injecting it into innerHTML to avoid markup injection.

Concatenating t.table into an HTML string and assigning it via innerHTML means any <, >, or & in the table name can break the markup or allow injected HTML. Please either HTML-escape t.table before concatenation or build these cells via createElement/textContent to close off this injection vector.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/snapshot/capturer.go">

<violation number="1" location="internal/snapshot/capturer.go:308">
P2: `Done` is derived from `tableIndex`, but `tableIndex` also advances when a table is skipped due to query/scan errors. That marks failed tables as completed in progress output.</violation>
</file>

<file name="cmd/app/dashboard/pages/snapshot.html">

<violation number="1" location="cmd/app/dashboard/pages/snapshot.html:436">
P2: `t.table` is concatenated directly into an HTML string assigned via `innerHTML` without escaping. If a table name contains `<`, `>`, or `&`, it will break the markup or allow HTML injection. Escape the value before interpolation, or build the cell using `createElement`/`textContent`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

for i, t := range c.tables {
tp := TableProgress{
Table: t.FullName(),
Done: c.completed || i < c.tableIndex,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Done is derived from tableIndex, but tableIndex also advances when a table is skipped due to query/scan errors. That marks failed tables as completed in progress output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/snapshot/capturer.go, line 308:

<comment>`Done` is derived from `tableIndex`, but `tableIndex` also advances when a table is skipped due to query/scan errors. That marks failed tables as completed in progress output.</comment>

<file context>
@@ -304,6 +305,7 @@ func (c *SnapshotCapturer) Progress() Progress {
 	for i, t := range c.tables {
 		tp := TableProgress{
 			Table: t.FullName(),
+			Done:  c.completed || i < c.tableIndex,
 		}
 		if i < len(c.tableTotals) {
</file context>

? '<span class="font-semibold ' + (isDone ? 'text-text' : 'text-primary') + '">' + fmtMs(total) + '</span>'
: '<span class="text-textMuted/40">—</span>';
return '<tr class="border-b border-border/40 last:border-0 ' + rowBg + '">'
+ '<td class="py-2 pr-4 whitespace-nowrap text-sm ' + nameColor + ' font-medium">' + icon + t.table + '</td>'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: t.table is concatenated directly into an HTML string assigned via innerHTML without escaping. If a table name contains <, >, or &, it will break the markup or allow HTML injection. Escape the value before interpolation, or build the cell using createElement/textContent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/app/dashboard/pages/snapshot.html, line 436:

<comment>`t.table` is concatenated directly into an HTML string assigned via `innerHTML` without escaping. If a table name contains `<`, `>`, or `&`, it will break the markup or allow HTML injection. Escape the value before interpolation, or build the cell using `createElement`/`textContent`.</comment>

<file context>
@@ -406,6 +416,43 @@ <h3 class="text-base sm:text-lg font-semibold text-text">CDC Tables</h3>
+            ? '<span class="font-semibold ' + (isDone ? 'text-text' : 'text-primary') + '">' + fmtMs(total) + '</span>'
+            : '<span class="text-textMuted/40">—</span>';
+        return '<tr class="border-b border-border/40 last:border-0 ' + rowBg + '">'
+            + '<td class="py-2 pr-4 whitespace-nowrap text-sm ' + nameColor + ' font-medium">' + icon + t.table + '</td>'
+            + '<td class="py-2 px-3 text-center font-mono text-xs">' + fmtMs(t.read_ms) + '</td>'
+            + '<td class="py-2 px-3 text-center font-mono text-xs">' + fmtMs(t.build_ms) + '</td>'
</file context>

Render idle header button before async status fetch so the action is never blank.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cnlangzi
cnlangzi merged commit 6609508 into main May 13, 2026
5 checks passed
@cnlangzi
cnlangzi deleted the feat/snapshot-dashboard-timing-breakdown branch May 13, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant