Skip to content

feat: open favorited Library queries as a read-only Dashboard (#149 D1)#150

Merged
BorisTyshkevich merged 5 commits into
mainfrom
feat/dashboard-foundation-149
Jul 4, 2026
Merged

feat: open favorited Library queries as a read-only Dashboard (#149 D1)#150
BorisTyshkevich merged 5 commits into
mainfrom
feat/dashboard-foundation-149

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Part of #149 — Phase D1 of the Dashboard epic (roadmap #68, Phase 6).

Adds a standalone /sql/dashboard page that opens the favorited subset of
the 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

  • Route — the SPA http_handlers regex widens to ^/sql(/dashboard)?/?$
    (the anchored config.json rule is unaffected); bootstrap branches on
    location.pathname to renderDashboard vs renderApp. Config/OAuth resolve
    from the /sql base on the dashboard route (configBase), so config.json,
    token exchange, and refresh all work there.
  • Auth handoff — a new tab starts signed-out (per-tab sessionStorage), so
    "Open as dashboard" hands the opener's credentials over once via a
    same-origin postMessage handshake: the child requests, the opener grants a
    snapshot. 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.
  • Tiles — each favorite runs read-only (readonly=2, so a favorite that
    contains a write is rejected server-side rather than executed) and renders via
    the existing renderChart / autoChart seam. Single-row (KPI) and
    non-chartable favorites are skipped with an "N not shown" header note.
    Header: back link, editable-later title, favorites chip, source chip, Refresh.
  • Entry point — File ▾ → "Open as dashboard", enabled once ≥1 query is
    starred.

Layers

  • Pure: src/core/dashboard.js (route/config helpers, JSON→rows transform, tile
    classification) + src/core/auth-handoff.js (snapshot/restore + message
    predicates) — both 100/100/100/100.
  • Net: queryDashboardTile in src/net/ch-client.js (read-only tile fetch).
  • Render: src/ui/dashboard.js over the app controller; the tile fetch goes
    through app.runTile (no src/net import in the UI layer).

Reviews

Ran self code-review, security-review (the auth-handoff surface), and a
conventions pass. Fixes folded in: reuse detectSqlFormat for trailing-FORMAT
detection (avoids double-FORMAT on FORMAT x SETTINGS y); wrap
ensureConfig/getToken inside runTile's try so a thrown token refresh
degrades 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=2 unbypassable, 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 built
dist/sql.html
at /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

  • Deploy: a cold/bookmarked /sql/dashboard OAuth sign-in needs
    /sql/dashboard registered as an IdP redirect URI (documented in README);
    opening from the app uses the in-session handoff and needs nothing extra.
  • Known limitation (per Feature: open favorited Library queries as an interactive Dashboard #149): two tabs independently refreshing a
    rotating OAuth refresh token can race — BroadcastChannel sync deferred.
  • Favorites auto-run on open; readonly=2 confines this to reads, equivalent to
    running the same query in the workbench.

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency (or it's a deliberate, justified addition — see CONTRIBUTING)
  • README / CHANGELOG.md ([Unreleased]) updated if behavior or the deployed surface changed
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, the issue body, ADR/CHANGELOG) if this change reshaped it

🤖 Generated with Claude Code

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
BorisTyshkevich and others added 4 commits July 4, 2026 10:46
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
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