Skip to content

dashboard: collapse alert stack traces, and fix the nav scroll spy - #32

Open
CamiloSierraH wants to merge 3 commits into
mainfrom
cami/dashboard-alert-collapse-and-nav-spy
Open

CamiloSierraH wants to merge 3 commits into
mainfrom
cami/dashboard-alert-collapse-and-nav-spy

Conversation

@CamiloSierraH

@CamiloSierraH CamiloSierraH commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Two independent dashboard fixes, one commit each.

1. Alert messages bury the page under stack traces

An alert message is the rule's template with one row's values substituted, and those values carry whatever the server said. A ClickHouse exception embeds its stack trace in the message text — one measured in a real support bundle ran 1725 characters over 15 lines, of which the first 216 were the error. The panel rendered every row in full with white-space:pre-wrap, so replication_queue_errors at its 50-row limit produced 750 lines of stack frames and pushed the rest of the page out of sight.

Three things now collapse behind native <details>, each only when there is something to hide:

Shown by default One click away
a message up to the stack trace, capped at 260 chars, trailing (version X (official build)) dropped the complete original text
instances the first 5 N more instances
rule description the first paragraph more about this rule

The version suffix is the same on every row and the header already states it; dropping it is what turns each instance into one line instead of two. A short single-line message gets no toggle at all.

Nothing leaves the page. DATA.alerts still carries every row and every frame — bundle-layout.md now tells the skill to read it rather than the rendered page. Worst case measured with the real exception: 750 lines become 6, with the full text behind six disclosures.

2. The nav scroll spy never highlighted anything

A scroll spy already existed. It had never worked on any bundle.

Every optional panel starts at display:none, and a non-rendered element reports getBoundingClientRect().top = 0. Zero always passes the "its top is above the line" test, so the last such section in the document won every pass — on a typical bundle sec-async-inserts, whose nav link is hidden too. The class landed on an invisible link and the band looked dead. Confirmed in a browser before changing anything: after scrolling to Storage, the active link was #sec-async-inserts (HIDDEN LINK).

The section list is now rebuilt on each pass and filtered to those actually rendered that also have a nav link. It syncs on load and resize, not only on scroll, because panels un-hide after their data renders — the same lateness that makes a one-time snapshot wrong. The first section is current at the top of the page, the last at the bottom where a short final section can never reach the threshold, and the active link carries aria-current.

Hover and active shared one CSS rule, so the current section was indistinguishable from whatever the pointer was over. Active now gets an accent underline and heavier weight.

The requestAnimationFrame coalescing this first used is gone: rAF never fires under a headless --virtual-time-budget, which made the behaviour untestable, and the pass is ~20 rect reads with no writes against scroll events the browser already caps at the frame rate. Reading live also stays correct when a disclosure expands and shifts everything below it.

Verification

Both fixes were driven in a real browser (headless Chrome), not only asserted against the template.

  • Scroll spy: all 15 rendered sections of the preview page, and 15 scroll positions of a live dashboard — every one highlights its own link, including the bottom-of-page case.
  • Alert collapse: the three JS helpers driven in node against a real trace, a version-only message, a plain short message and a <script> injection attempt. Escaping holds; the short message correctly gets no toggle.
  • End-to-end run against ClickHouse 26.7: dashboard renders, node --check clean on the inline script, light and dark themes both correct.
  • go vet ./... and go test ./... pass. Two new test files pin the caps, the escaping, the payload retention and each part of the spy fix. theme_test's threshold assertion now checks that the line derives from the measured band height rather than pinning the tolerance, which widened from 8 to 16 px so a subpixel rounding after an anchor jump cannot flip the comparison.

Seeing it

make dashboard-preview writes two pages:

  • bin/alerts_preview.html — every alert case: stack trace, over-cap instances, a message needing no disclosure, a rule whose query failed, a rule that was not applicable.
  • bin/keeper_incident_preview.html — 15 sections to scroll through plus the stack-trace alerts, so both fixes are visible on one page.

Review follow-up (5e700b0)

Two off-by-ones in alertMessageParts, both raised on review:

  • The 260-character cap was exclusive of the ellipsis. The word-boundary branch stayed inside it; the no-space fallback emitted slice(0,260) plus the ellipsis, so 261. Reachable with a long path or any 260-character run without a space after position 156. Now slices to ALERT_HEAD_CHARS-1.
  • The toggle compared the head against a version-stripped copy of the message. For a message that is nothing but a version suffix, stripping empties the head, the if(!head) head=flat fallback restores it, and the comparison against the empty copy reported a difference — rendering a disclosure whose body repeated the line above it. Replaced with truncated: head !== flat, which states the actual question and drops flatTrimmed entirely.

The second is unreachable with the shipped rules — every template in alerts/ wraps its substitutions in literal prose and ALERT_VERSION_RE is $-anchored — but the predicate is simpler than the one it replaced, so it went in.

The verification below notes a version-only message was among the cases driven in node; it was, but the assertion checked escaping and the head, not whether the toggle appeared. The function is now run over eight shapes with both properties asserted (head length within the cap, no disclosure whose body equals its inline line): before, exactly these two failed; after, none, with every other shape unchanged. alerts_panel_test.go pins both fixed forms and bans the pre-fix ones.

🤖 Generated with Claude Code

CamiloSierraH and others added 2 commits September 25, 2026 11:33
An alert message is the rule's template with one row's values substituted,
and those values carry whatever the server said. A ClickHouse exception
embeds its stack trace in the message text: one measured in a real bundle
ran 1725 characters over 15 lines, of which the first 216 were the error.
The panel rendered every row in full with white-space:pre-wrap, so
replication_queue_errors at its 50-row limit produced 750 lines of stack
frames and pushed the rest of the page out of sight.

Three things now collapse behind native <details>, each only when there is
something to hide:

- a message is shown up to the stack trace and capped at 260 characters,
  with the trailing "(version X (official build))" dropped — it is the same
  on every row and the header already states the version; the complete text
  sits under "full message and stack trace"
- instances past the fifth move under "N more instances"
- a rule description shows its first paragraph, the "what to check" half
  under "more about this rule"

Nothing leaves the page: DATA.alerts still carries every row and every
frame, and bundle-layout.md tells the skill to read it rather than the
rendered page. A short single-line message gets no toggle at all.

Worst case measured with the real exception: 750 lines become 6.

make dashboard-preview now also writes bin/alerts_preview.html, a fixture
covering every case — stack trace, over-cap instances, a message needing no
disclosure, a rule whose query failed, a rule that was not applicable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sticky nav was supposed to mark the section you are reading. It never
did, on any bundle.

Every optional panel starts at display:none, and a non-rendered element
reports getBoundingClientRect().top = 0. Zero always passes the "its top is
above the line" test, so the last such section in the document won every
pass — on a typical bundle sec-async-inserts, whose nav link is hidden too.
The class landed on an invisible link and the band looked dead. Confirmed
in a browser before the fix: after scrolling to Storage the active link was
#sec-async-inserts.

The section list is now rebuilt on each pass and filtered to those actually
rendered that also have a nav link. It syncs on load and resize, not only
on scroll, because panels un-hide after their data renders — the same
lateness that makes a snapshot wrong. The first section is current at the
top of the page and the last at the bottom, where a short final section can
never reach the threshold, and the active link carries aria-current.

Hover and active shared one CSS rule, so the current section was
indistinguishable from whatever the pointer was over; active now gets an
accent underline and heavier weight.

The rAF coalescing this first used is gone: requestAnimationFrame never
fires under a headless --virtual-time-budget, which made the behaviour
untestable, and the pass is ~20 rect reads with no writes against scroll
events the browser already caps at the frame rate. Reading live also stays
correct when a disclosure expands and shifts everything below it.

Verified in a browser across all 15 rendered sections of the preview page
and 15 scroll positions of a live dashboard; every one highlights its own
link. theme_test's threshold assertion now checks that the line derives
from the measured band height rather than pinning the tolerance value,
which widened from 8 to 16 px so a subpixel rounding after an anchor jump
cannot flip the comparison.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Five moderate issues remain in alert truncation and scroll-spy synchronization behavior.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
What changed in this PR

Improves dashboard readability by collapsing verbose alerts and fixing navigation scroll highlighting.

Changes:

  • Adds collapsible alert messages, instances, and descriptions.
  • Rebuilds scroll-spy behavior with active-link accessibility styling.
  • Adds tests, previews, and documentation.
File Reviewed change
skills/​clickhouse-diagnostic/​references/​bundle-layout.md Documents reading full alert payloads.
README.md Documents dashboard behavior and previews.
Makefile Generates dashboard preview pages.
internal/​dashboard/​theme_test.go Updates scroll threshold coverage.
internal/​dashboard/​nav_spy_test.go Tests scroll-spy behavior.
internal/​dashboard/​keeper_preview_test.go Adds trace alerts to the preview.
internal/​dashboard/​generator.go Implements alert disclosures and scroll-spy changes.
internal/​dashboard/​alerts_preview_test.go Adds alert preview fixtures.
internal/​dashboard/​alerts_panel_test.go Tests alert-collapse behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/dashboard/generator.go
Comment thread internal/dashboard/generator.go Outdated
Both found by review on #32.

The 260-character cap was exclusive of the ellipsis. The word-boundary
branch stayed inside it, but the no-space fallback — reachable with a long
path or any 260-character run without a space after position 156 — emitted
slice(0,260) plus the ellipsis, so 261. Slicing one short makes the
documented cap inclusive.

The disclosure toggle compared the inline head against a version-stripped
copy of the message. For a message that is NOTHING BUT a version suffix,
stripping empties head, the `if(!head) head=flat` fallback restores it, and
the comparison against the stripped (empty) copy reported a difference — so
the panel rendered a <details> whose body repeated the line above it.
Comparing head against the flat message instead states the actual question,
"is the inline line already the whole message", and drops flatTrimmed
entirely.

Unreachable with the shipped rules, since every template in alerts/ wraps
its substitutions in literal prose and the version regex is $-anchored, so
the prose always survives. Fixed anyway: the predicate is simpler than the
one it replaces, and the next rule that interpolates a bare server string
would hit it.

Verified by extracting the function out of the template and running it over
eight message shapes — version-only, short with a version suffix, short
plain, long with and without spaces, a stack trace, empty and null. Before:
the two failures above. After: none, with every other shape unchanged.
alerts_panel_test.go now pins both forms and bans the pre-fix ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants