feat: open favorited Library queries as a read-only Dashboard (#149 D1)#150
Merged
Conversation
Phase D1 of the Dashboard epic: a standalone `/sql/dashboard` route (the same served artifact, reached by a client-side branch) that renders the favorited subset of the Library as read-only chart tiles. No new runtime dependency, no build change. - Route: widen the SPA `http_handlers` regex to `^/sql(/dashboard)?/?$` and branch on `location.pathname` in `bootstrap` (renderDashboard vs renderApp). Config/OAuth resolve from the `/sql` base on the dashboard route (configBase). - Auth: a one-time, same-origin `postMessage` credential handoff from the opener (both target origin and peer window pinned), re-seeding the child's in-memory auth (token/authMode/idp/chCtx.origin) and its own per-tab sessionStorage. A cold/bookmarked visit falls back to the normal login flow. - Tiles: each favorite runs read-only (`readonly=2`, writes rejected server-side) and renders via the existing `renderChart`/`autoChart` seam; single-row (KPI) and non-chartable favorites are skipped with an "N not shown" header note. - File menu gains "Open as dashboard" (enabled once ≥1 query is favorited). Pure logic in `core/dashboard.js` + `core/auth-handoff.js` (100%); net helper `queryDashboardTile`; render in `ui/dashboard.js`. Tests added to the per-file coverage gate; README (routes + redirect-URI note) and CHANGELOG updated. KPI tiles, global filters, drag-to-arrange layout, per-tile controls, and export follow in #149 D2–D7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyLqZGyUkm7mP6WhZCkodj
This was referenced Jul 4, 2026
Open
Mirror the production http_handlers route in the `npm run local` dev server so "Open as dashboard" resolves instead of 404ing when testing locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyLqZGyUkm7mP6WhZCkodj
#149 D1) Addresses manual-test feedback against the design without pulling later phases forward: - Vertical scroll: the dashboard now has its own scroll container (#root is a fixed overflow:hidden flex column), so a tall grid scrolls. - Theme toggle in the header, reusing app.toggleTheme (persists + flips data-theme) like the workbench. - Arrange grid defaults to 3 columns on wide screens, degrading to 2 then 1. - The inline per-tile chart config bar is hidden (D1 is read-only); a reveal-on-click settings popover comes in D6. Drag-reorder + 2-col span (D5) and the expand modal + settings popover (D6) remain in their planned phases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyLqZGyUkm7mP6WhZCkodj
Resolve all 10 findings from the high-effort review of PR #150: - Refresh race: resolve/refresh the auth token once up front (app.ensureFreshToken) before fanning tiles out, so N tiles never race an expired rotating refresh token. - Chart leak: destroy each tile's Chart.js instance before Refresh rebuilds the grid (liveTiles), instead of orphaning them. - Expired handoff: bootstrap now refreshes a handed-off-but-expired id_token before falling back to a full re-login. - onSignedOut cascade: single pre-flight auth check redirects to login once; runTile no longer drives sign-out per tile. - Concurrency cap: tiles run through an order-preserving bounded pool (TILE_CONCURRENCY=6) instead of an unbounded Promise.all. - Config bar: renderChart gains controls:false so read-only tiles omit the Type/X/Y bar entirely (dead display:none CSS removed). - Dead abort path removed from runTile/renderTile. - Trailing SQL comments peeled before FORMAT detection (shared withTrailingFormat) so `... FORMAT JSON -- note` isn't doubled. - Base path derived once (app.basePath via configBase); isDashboardRoute made mount-agnostic and consistent with configBase. - Reuse: queryDashboardTile delegates to queryJson(extra); dashboardTileSql and prepareExportSql share withTrailingFormat. Tests updated in the same change; full unit gate + build + e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyLqZGyUkm7mP6WhZCkodj
…tion-149 # Conflicts: # CHANGELOG.md
This was referenced Jul 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Part of #149 — Phase D1 of the Dashboard epic (roadmap #68, Phase 6).
Adds a standalone
/sql/dashboardpage that opens the favorited subset ofthe Library as a read-only dashboard in a new tab — each favorited,
chartable saved query rendered as a live chart tile. It's the same served
artifact reached by a client-side route: no new runtime dependency, no build
change. KPI tiles, filters, layout, and export come in later phases (D2–D7).
What's in D1
http_handlersregex widens to^/sql(/dashboard)?/?$(the anchored
config.jsonrule is unaffected);bootstrapbranches onlocation.pathnametorenderDashboardvsrenderApp. Config/OAuth resolvefrom the
/sqlbase on the dashboard route (configBase), so config.json,token exchange, and refresh all work there.
sessionStorage), so"Open as dashboard" hands the opener's credentials over once via a
same-origin
postMessagehandshake: the child requests, the opener grants asnapshot. Both sides pin the exact target origin and the peer window;
the child re-seeds its in-memory auth (token / authMode / idp /
chCtx.origin)and its own sessionStorage. A cold/bookmarked visit has no opener and falls
back to the normal login, which returns to the dashboard after sign-in.
readonly=2, so a favorite thatcontains a write is rejected server-side rather than executed) and renders via
the existing
renderChart/autoChartseam. Single-row (KPI) andnon-chartable favorites are skipped with an "N not shown" header note.
Header: back link, editable-later title, favorites chip, source chip, Refresh.
starred.
Layers
src/core/dashboard.js(route/config helpers, JSON→rows transform, tileclassification) +
src/core/auth-handoff.js(snapshot/restore + messagepredicates) — both 100/100/100/100.
queryDashboardTileinsrc/net/ch-client.js(read-only tile fetch).src/ui/dashboard.jsover theappcontroller; the tile fetch goesthrough
app.runTile(nosrc/netimport in the UI layer).Reviews
Ran self code-review, security-review (the auth-handoff surface), and a
conventions pass. Fixes folded in: reuse
detectSqlFormatfor trailing-FORMATdetection (avoids double-
FORMATonFORMAT x SETTINGS y); wrapensureConfig/getTokeninsiderunTile's try so a thrown token refreshdegrades to a tile error instead of freezing the grid; handle the
{aborted}tile outcome; give the opener a longer listen window than the child's request
timeout; guard the grant on
hasAuth. Security review found no defects(explicit-origin postMessage, origin+source pinning, tokens stay same-origin /
per-tab, CSP unchanged,
readonly=2unbypassable, anchored route regex).Verification
npm test(1387 tests, per-file gate green) ·npm run build·npm run test:e2e(chromium + webkit, 36) · plus a real-Chromium smoke of the builtdist/sql.htmlat/sql/dashboard(signed-in session + mocked ClickHouse):the chart tile renders via real Chart.js, the KPI favorite is skipped ("1 not
shown"), and the footer shows real stats.
Notes / follow-ups
/sql/dashboardOAuth sign-in needs/sql/dashboardregistered as an IdP redirect URI (documented in README);opening from the app uses the in-session handoff and needs nothing extra.
rotating OAuth refresh token can race — BroadcastChannel sync deferred.
readonly=2confines this to reads, equivalent torunning the same query in the workbench.
Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed🤖 Generated with Claude Code