You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a Dashboard view that opens all favorited (starred) Library queries in a new
browser tab as a single, full-scale interactive dashboard. Each saved query renders in its
saved shape — per its saved panel config (#166): a chart, a table, a logs view, a
single-row result as a KPI tile — laid out
in a grid the user can rearrange. The dashboard layers on global filters (shared {name:Type} parameter bindings that re-run every tile), per-tile controls, drill-down, expand-to-fullscreen, refresh, and export (Markdown / PDF / per-chart PNG).
No separate dashboard document is introduced. The dashboard remains the favorited subset of the
Library. Each saved query may carry two independent pieces of metadata:
panel// how the query result is visualizeddashboard// what job the saved query performs in the dashboard
The dashboard role is optional and defaults to Panel, so existing saved queries retain their
behavior.
2026-07-04 revision — spec reconciled against the codebase: parameter syntax standardized
on the already-shipped ClickHouse-native {name:Type} mechanism (#134), four open questions
pinned (see Decisions pinned), architecture (routing / auth / no-framework) decided, and the
work split into shippable phases. Original design-generated spec preserved otherwise.
2026-07-12 clarification — visualization vs dashboard role.
Panels remain visual result renderers: chart, table, logs, text, and KPI. Query-backed filter
choices and setup scripts are not Panel types. A saved query has a separate optional dashboard
participation field:
dashboard.role='panel'|'filter'|'setup'
Missing metadata defaults to panel. A favorited Panel becomes a grid tile; a favorited Filter
source docks into the filter bar; a favorited Setup source runs before filters and Panels and
remains hidden. Existing panel configuration is preserved while another role is active.
Motivation
The Library already treats saved queries as a portable, document-style collection. Users curate
favorites as "the things worth watching." Rendering that starred subset together — KPIs on top,
charts below, one shared filter bar — turns a query collection into a monitoring surface without
a separate dashboard-authoring tool or schema. Everything the dashboard needs is already stored
per query, or expressible in SQL.
Reference implementation
An interactive HTML/React mockup demonstrating the tile/layout/drill/expand behaviour (against
sample airline.ontime data) lives in the Claude Design project ("sql browser"):
dashboard-root.jsx — header, filter bar, layout switcher, export, tweaks
Treat the mockup as the visual + interaction source of truth; this issue is the buildable
spec. Two mockup caveats:
The mockup's charts are hand-rolled SVG (the design tool can't bundle Chart.js). The real
build reuses the app's chart stack — pure src/core/chart-data.js + the injected app.Chart
seam + renderChart — not a port of the SVG code.
The mockup's filter bar is hardcoded (Year/Range selects over sample data). The real
filter engine (§6) has no mockup yet — supplemental mockups requested from Claude Design
(tasks/dashboard-filter-mockups.md in the design project).
Requirements
1. Entry point
Library ▾ menu → "Open as dashboard", enabled when ≥1 favorite exists
(src/ui/file-menu.js).
Opens the standalone route /sql/dashboard in a new tab.
The dashboard is a snapshot of favorites at open time. Later Library edits do not
live-mutate an open tab; Refresh re-reads.
2. Scope — dashboard participation
Only favorite:true saved queries participate.
Before execution, favorites are partitioned by dashboard.role:
Panel (default) — renders as a dashboard grid panel using its saved/derived panel
configuration;
Filter source — supplies options for one existing dashboard parameter and docks into the
filter bar; never a grid panel;
Setup — runs before Filter sources and Panels in session mode; never rendered.
Role and favorite state are independent. Unfavoriting removes the saved query from the dashboard
but preserves its configured role.
Unknown roles are preserved on import and excluded with a visible diagnostic.
Among displayed Panels: a favorite renders as a KPI tile when its result is a single row
(§4), or a Chart tile using its saved chart-family panel config or a chartable multi-row
result (§5). Non-chartable/non-single-row favorites skip in v1 with a small "N not shown" note
in the header (superseded for table-shaped results by #164, Phase D9). KPI tiles float to a top
row; chart tiles fill the grid beneath in Library order, then user-reorderable.
3. Layout — two views + a switcher
Arrange (default) — uniform grid, 2 or 3 columns. Tiles are drag-reorderable (grip
handle in each tile header) and each tile can snap between 1- and 2-column width.
Report — single scrolling column, full-width tiles, taller charts; still drag-reorderable
(width toggle N/A in one column).
A segmented switcher (Arrange | Report) lives in the filter bar — the primary, visible
control. Column count (2/3), KPI-row visibility, and appearance live in secondary settings.
e.g. '%', ' min'; % also switches value to fixed-decimal.
delta
change vs comparison period
Signed → ▲/▼. Computed in the query (e.g. vs prior year).
higher_is_better
0/1
Colours the delta green/red. Delays/cancellations set 0.
Example (real prior-year delta in the same query):
SELECT
sumIf(Cancelled, Year = {year:UInt16}) AS value,
''AS unit,
round((sumIf(Cancelled, Year = {year:UInt16})
/ sumIf(Cancelled, Year = {year:UInt16} -1) -1) *100, 1) AS delta,
0AS higher_is_better
FROMairline.ontimeWHERE Year IN ({year:UInt16}, {year:UInt16} -1)
Graceful fallback: a bare SELECT count() … (one column, no aliases) still renders — the
single numeric column is value, no unit/delta. Tile footer shows 1 row × N cols + query time.
No chart → apply the existing autoChart heuristic (src/core/chart-data.js) for a default.
Per-tile controls (settings popover): change Type / X / Y inline. Changes persist with the
dashboard as overrides, not back to the source query.
Hover tooltip (x-label + every series value). Footer meta: rows · ms · bytes scanned.
6. Global filters
Filters are named parameters in ClickHouse-native syntax — {name:Type}, e.g. {year:UInt16} — injected into every tile query referencing them. This mechanism already
exists (#134): detection in src/core/query-params.js, values in the shared persisted state.varValues map, transport as injection-safe param_<name> HTTP args. The type is
declared in the SQL itself, which resolves typing/quoting by construction.
Option lists — text by default, query-backed when configured (we deliberately do not
introspect the schema, because panels may use arbitrary joins and there's no reliable single
table to DISTINCT): no list supplied → free-text box; typed value injected; empty = predicate
omitted.
Query-backed filter options
A saved query configured as:
dashboard: {role: 'filter',param: 'origin'}
may supply the choices for an existing dashboard parameter.
Re-run: changing any filter re-runs affected tiles with a loading shimmer and updates the
"Updated …" timestamp; option-list queries re-run on Refresh.
7. Drill-down
Clicking a chart element (bar / pie slice / hbar row) sets a global drill filter { column, value }, shown as a dismissible chip; non-matching data dims across charts.
Re-clicking the element or the chip ✕ clears it.
Pinned: v1 is client-side highlight only (what the mockup implements). True
predicate-push drill-down (apply as an added predicate and re-run tiles) needs a
column→parameter mapping that is ill-defined across arbitrary queries — deferred to a future
phase with its own design.
8. Expand / fullscreen
Each chart tile → Expand modal: large chart + the same per-tile controls + a read-only SQL peek. Esc / backdrop closes.
9. Header
← SQL Browser back link.
Editable title (defaults to Library name; inline rename, Enter/Esc; drives export filename;
persisted).
"N favorites" chip — the sole scope marker (+ "N not shown" note when tiles are skipped,
§2). (Per-tile star badges intentionally omitted: in an all-favorites view a badge true for
100% of tiles carries no information.)
Refresh (re-runs all tiles + option queries; spinner; updates timestamp).
Export ▾ — Markdown / PDF / per-chart PNG.
10. Export
Markdown — heading + KPI bullet list (with delta) + one ## <name> section per chart with a
fenced ```sql block and a small results-table preview. Extend the Library's Markdown serializer
(buildMarkdownDoc, `src/core/saved-io.js`).
PDF — browser print (print CSS hides interactive chrome).
PNG — per-chart PNG via the Chart.js canvas (toDataURL), free of new dependencies. Whole-dashboard PNG is out of v1: DOM rasterization means an html2canvas-class runtime
dependency — an explicit hard-rule-4 decision to make separately if wanted.
Architecture (decided)
Stack: no React / no framework — same architecture as the app (hyperscript render
functions + @preact/signals-core), per CLAUDE.md rule 5 and the ADR-0001 Preact-spike
addendum. The mockup's React is its prototyping medium, not a directive: the valuable mockup
logic (readKpi, formatKpiValue, conventions) is framework-free and ports to src/core/
directly; charts reuse the existing Chart.js seam. The dashboard's dataflow (filters → re-run
affected tiles → re-render) is exactly what signals model; drag-reorder stays an imperative
pointer surface like the app's other ones. If dashboard state coordination proves genuinely
painful, that is new evidence for an ADR-0001 addendum — not a pre-emptive fork.
Serving/routing: one artifact, client-side route. Widen the ^/sql/?$ rule in deploy/http_handlers.xml to also serve /sql/dashboard from the same sql.html, and branch
on location.pathname in bootstrap (src/main.js). Zero build changes; auth/config wiring
shared.
Auth: one-time handoff + full login fallback. Tokens live in per-tab sessionStorage, so
a new tab is signed out by default. "Open as dashboard" opens the tab via window.open and
passes the current credentials (OAuth tokens or basic-auth) once via postMessage; the
dashboard tab stores them in its ownsessionStorage, which survives reloads of that tab.
A cold/bookmarked visit to /sql/dashboard runs the same login/OAuth flow as the main page
(same artifact, same bootstrap). No migration to localStorage. Known residual: two tabs
independently refreshing a rotating refresh token can race — resolve in Phase 1
(BroadcastChannel token sync, or document as unsupported IdP config).
Decisions pinned (2026-07-04)
Parameter syntax = {name:Type} (ClickHouse-native, already shipped in Support variables in SELECT queries #134). The
original :param wording was mockup shorthand. Typing/quoting is resolved by the SQL itself.
Filter values live in the existing state.varValues — already global across queries and
persisted (asb:varValues), so dashboard filters and workbench variable values stay
consistent by design. A dashboard-scoped namespace can be added later if needed.
"Open as dashboard" appears in the Library menu and opens /sql/dashboard in a new tab
showing only favorites; the new tab is authenticated via handoff; a cold visit falls
back to the normal login flow.
Single-row favorites render as KPI tiles reading value/unit/delta/higher_is_better
by convention, with type-driven formatting and a graceful bare-scalar fallback.
delta is computed in-query (prior-period) and coloured by higher_is_better.
No saved-query schema change — superseded by the Panels delivery (Panels: visualization registry + Panel drawer tab + Library panel field #166) and the
2026-07-12 dashboard-role clarification: the saved query gains the panel field (with
transparent import upgrade from chart) and an independent optional dashboard.role. No separate dashboard schema is introduced — that pin stands.
Refresh-token rotation across two tabs (Phase D1): BroadcastChannel sync vs documented
limitation.
Table tiles / true predicate drill-down / whole-dashboard PNG / auto-refresh / filters:
bundle — all deferred post-v1, each needing its own small design.
Summary
Add a Dashboard view that opens all favorited (starred) Library queries in a new
browser tab as a single, full-scale interactive dashboard. Each saved query renders in its
saved shape — per its saved panel config (#166): a chart, a table, a logs view, a
single-row result as a KPI tile — laid out
in a grid the user can rearrange. The dashboard layers on global filters (shared
{name:Type}parameter bindings that re-run every tile), per-tile controls, drill-down,expand-to-fullscreen, refresh, and export (Markdown / PDF / per-chart PNG).
No separate dashboard document is introduced. The dashboard remains the favorited subset of the
Library. Each saved query may carry two independent pieces of metadata:
The dashboard role is optional and defaults to Panel, so existing saved queries retain their
behavior.
Motivation
The Library already treats saved queries as a portable, document-style collection. Users curate
favorites as "the things worth watching." Rendering that starred subset together — KPIs on top,
charts below, one shared filter bar — turns a query collection into a monitoring surface without
a separate dashboard-authoring tool or schema. Everything the dashboard needs is already stored
per query, or expressible in SQL.
Reference implementation
An interactive HTML/React mockup demonstrating the tile/layout/drill/expand behaviour (against
sample
airline.ontimedata) lives in the Claude Design project ("sql browser"):Dashboard.html— shell + theme tokens (matches the app's dark/light system)dashboard-data.jsx— sample favorites, savedchartconfigs, KPI queriesdashboard-charts.jsx— chart canvas (bar/hbar/line/area/pie) + hover tooltip + drill callbackdashboard-app.jsx— tiles, KPI reader (readKpi/formatKpiValue), per-tile controls, expand modaldashboard-root.jsx— header, filter bar, layout switcher, export, tweaksTreat the mockup as the visual + interaction source of truth; this issue is the buildable
spec. Two mockup caveats:
build reuses the app's chart stack — pure
src/core/chart-data.js+ the injectedapp.Chartseam +
renderChart— not a port of the SVG code.filter engine (§6) has no mockup yet — supplemental mockups requested from Claude Design
(
tasks/dashboard-filter-mockups.mdin the design project).Requirements
1. Entry point
(
src/ui/file-menu.js)./sql/dashboardin a new tab.live-mutate an open tab; Refresh re-reads.
2. Scope — dashboard participation
Only
favorite:truesaved queries participate.Before execution, favorites are partitioned by
dashboard.role:configuration;
filter bar; never a grid panel;
Role and favorite state are independent. Unfavoriting removes the saved query from the dashboard
but preserves its configured role.
Unknown roles are preserved on import and excluded with a visible diagnostic.
Among displayed Panels: a favorite renders as a KPI tile when its result is a single row
(§4), or a Chart tile using its saved chart-family panel config or a chartable multi-row
result (§5). Non-chartable/non-single-row favorites skip in v1 with a small "N not shown" note
in the header (superseded for table-shaped results by #164, Phase D9). KPI tiles float to a top
row; chart tiles fill the grid beneath in Library order, then user-reorderable.
3. Layout — two views + a switcher
handle in each tile header) and each tile can snap between 1- and 2-column width.
(width toggle N/A in one column).
control. Column count (2/3), KPI-row visibility, and appearance live in secondary settings.
layout,gridCols, KPI-row visibility, tileorder, per-tilespan.4. KPI tiles — SQL-driven, one-row / multi-column
A KPI is a favorited query returning one row. There is no stored display config; the tile
reads the returned row by column-name convention:
value6.71M/402.0K; float →13.2.unit'%',' min';%also switches value to fixed-decimal.deltahigher_is_better0.Example (real prior-year delta in the same query):
Graceful fallback: a bare
SELECT count() …(one column, no aliases) still renders — thesingle numeric column is
value, nounit/delta. Tile footer shows1 row × N cols+ query time.5. Chart tiles
{ type, x, y[], series },type ∈ bar | hbar | line | area | pie(same config the Results → Chart view produces).chart→ apply the existingautoChartheuristic (src/core/chart-data.js) for a default.dashboard as overrides, not back to the source query.
6. Global filters
Filters are named parameters in ClickHouse-native syntax —
{name:Type}, e.g.{year:UInt16}— injected into every tile query referencing them. This mechanism alreadyexists (#134): detection in
src/core/query-params.js, values in the shared persistedstate.varValuesmap, transport as injection-safeparam_<name>HTTP args. The type isdeclared in the SQL itself, which resolves typing/quoting by construction.
referenced by at least one executable dashboard source becomes a candidate filter.
Filter-source option SQL is parameter-free in Dashboard: multi-filter option bundles, shared preview, and role-aware result selector #160 v1.
introspect the schema, because panels may use arbitrary joins and there's no reliable single
table to
DISTINCT): no list supplied → free-text box; typed value injected; empty = predicateomitted.
Query-backed filter options
A saved query configured as:
may supply the choices for an existing dashboard parameter.
The single-select MVP is tracked in #160:
Multiselect is tracked separately in #189.
Dependent/cascading option queries and caches are deferred until a concrete dashboard requires
them.
{name:Type}params, re-run affected tiles #152) shipped the required-param half: an unset paramblocks that tile via the existing unfilled-params gate. The "empty/'All' omits the predicate"
half is Optional SQL blocks
/*[ … ]*/+ explicit filter activation — an empty parameter cannot omit surrounding SQL #165: an author wraps the predicate in a/*[ … ]*/optional block, which is removedbefore execution when any param inside is inactive (explicit
filterActivestate, distinctfrom the stored value); params outside blocks keep D3's blocking behavior. Values still bind
only through native
{name:Type}— no value interpolation."Updated …" timestamp; option-list queries re-run on Refresh.
7. Drill-down
{ column, value }, shown as a dismissible chip; non-matching data dims across charts.predicate-push drill-down (apply as an added predicate and re-run tiles) needs a
column→parameter mapping that is ill-defined across arbitrary queries — deferred to a future
phase with its own design.
8. Expand / fullscreen
SQL peek.
Esc/ backdrop closes.9. Header
persisted).
§2). (Per-tile star badges intentionally omitted: in an all-favorites view a badge true for
100% of tiles carries no information.)
10. Export
## <name>section per chart with afenced ```sql block and a small results-table preview. Extend the Library's Markdown serializer
(
buildMarkdownDoc, `src/core/saved-io.js`).toDataURL), free of new dependencies.Whole-dashboard PNG is out of v1: DOM rasterization means an html2canvas-class runtime
dependency — an explicit hard-rule-4 decision to make separately if wanted.
Architecture (decided)
functions +
@preact/signals-core), per CLAUDE.md rule 5 and the ADR-0001 Preact-spikeaddendum. The mockup's React is its prototyping medium, not a directive: the valuable mockup
logic (
readKpi,formatKpiValue, conventions) is framework-free and ports tosrc/core/directly; charts reuse the existing Chart.js seam. The dashboard's dataflow (filters → re-run
affected tiles → re-render) is exactly what signals model; drag-reorder stays an imperative
pointer surface like the app's other ones. If dashboard state coordination proves genuinely
painful, that is new evidence for an ADR-0001 addendum — not a pre-emptive fork.
^/sql/?$rule indeploy/http_handlers.xmlto also serve/sql/dashboardfrom the samesql.html, and branchon
location.pathnameinbootstrap(src/main.js). Zero build changes; auth/config wiringshared.
sessionStorage, soa new tab is signed out by default. "Open as dashboard" opens the tab via
window.openandpasses the current credentials (OAuth tokens or basic-auth) once via
postMessage; thedashboard tab stores them in its own
sessionStorage, which survives reloads of that tab.A cold/bookmarked visit to
/sql/dashboardruns the same login/OAuth flow as the main page(same artifact, same bootstrap). No migration to
localStorage. Known residual: two tabsindependently refreshing a rotating refresh token can race — resolve in Phase 1
(BroadcastChannel token sync, or document as unsupported IdP config).
Decisions pinned (2026-07-04)
{name:Type}(ClickHouse-native, already shipped in Support variables in SELECT queries #134). Theoriginal
:paramwording was mockup shorthand. Typing/quoting is resolved by the SQL itself.state.varValues— already global across queries andpersisted (
asb:varValues), so dashboard filters and workbench variable values stayconsistent by design. A dashboard-scoped namespace can be added later if needed.
Table tiles are out of v1— superseded by **Phase D9 — table tiles + logs view**: saved view:'table' + non-chartable fallback render as table/logs tiles, server row cap for all tiles #164 (Phase D9): non-chartablemulti-row favorites (and any favorite explicitly saved with
view:'table') render as a tableor logs tile instead of being skipped; only empty/single-row (KPI, D5) results still count
toward the "N not shown" header note.
(new-dependency decision).
Build phases (each a shippable PR passing the coverage gate)
http_handlers.xmlregex +bootstrappathname branch), auth handoff + login fallback, "Open as dashboard" menu item,page shell, favorites → static chart tiles (reusing
renderChart/autoChart), header(back link, favorites chip, Refresh-all).
{name:Type}params, re-run affected tiles #152, PR feat: dashboard global filter bar (#149 D3) #156): filter bar over params detected across tileSQL (
detectParams/readStatementParams), text inputs bound to the sharedvarValues+paramArgswiring, re-run affected tiles (per-tile gating + generation guard), empty-valuesemantics. Uses the same
{name:Type}mechanism as the SQL Browser workbench (Support variables in SELECT queries #134). No filterbar at all when no favorite has a param.
panel.cfgregistry (chart×5 | table |logs | text), workbench Panel drawer tab, Library
panelfield with import upgrade,autoPanelheuristic. KPI (Phase D5 — KPI panel: one-row scalar/named-tuple cards, canonical Presentation Spec contract, and shared renderer #154), Filter source (Dashboard: multi-filter option bundles, shared preview, and role-aware result selector #160), and Setup source (Dashboard: shared-session setup wave and role-aware drawer integration #175) are separatedashboard-role follow-ups, not
panel.cfgarms.readKpi+formatKpiValueinsrc/core/(100%), column-role cfg with convention defaults, KPI row, bare-scalarfallback.
/*[ … ]*/+ explicit filter activation — an empty parameter cannot omit surrounding SQL #165):/*[ … ]*/comment-wrapped blocks + explicit persistedfilterActivestate, so an inactive filter omits its predicate instead of blocking thequery. Standalone — implemented and tested on the existing workbench variables (Support variables in SELECT queries #134) and the
D3 filter bar before D6; not part of the dashboard route itself.
dashboard state, not the source query), drill-down highlight + chip.
buildMarkdownDoc), print CSS for PDF, per-chartPNG.
view:'table'+ non-chartablefallback render as table tiles (
renderGrid, sort/resize/scroll); log-shaped results get adedicated compact logs view (level colors, monospace, wrapped message); server-side row cap
(
DASH_TILE_ROW_CAP) applied to all tiles, chart included.Acceptance criteria
/sql/dashboardin a new tabshowing only favorites; the new tab is authenticated via handoff; a cold visit falls
back to the normal login flow.
value/unit/delta/higher_is_betterby convention, with type-driven formatting and a graceful bare-scalar fallback.
deltais computed in-query (prior-period) and coloured byhigher_is_better.autoPanel;non-chartable multi-row favorites and
view:'table'favorites render as table/logs tiles(**Phase D9 — table tiles + logs view**: saved view:'table' + non-chartable fallback render as table/logs tiles, server row cap for all tiles #164); only empty/single-row (KPI) favorites are skipped, with an "N not shown" note.
{name:Type}) are auto-discovered from tile SQL; each renders as text by default,or a strict single-select when a favorited Filter source (Dashboard: multi-filter option bundles, shared preview, and role-aware result selector #160) targets it.
is dismissible (client-side highlight).
switcher; layout/order/spans/title persist per dashboard.
No saved-query schema change— superseded by the Panels delivery (Panels: visualization registry + Panel drawer tab + Library panel field #166) and the2026-07-12 dashboard-role clarification: the saved query gains the
panelfield (withtransparent import upgrade from
chart) and an independent optionaldashboard.role. Noseparate dashboard schema is introduced — that pin stands.
Open questions (remaining)
Empty-filter semantics (Phase D3)— resolved in **Phase D3 — global filters (text)**: filter bar over detected{name:Type}params, re-run affected tiles #152 for required params: anunset/empty param blocks that tile's execution via the existing
unfilledParamsgate, appliedper-tile (no SQL-side sentinel convention needed). Extended by Optional SQL blocks
/*[ … ]*/+ explicit filter activation — an empty parameter cannot omit surrounding SQL #165 for optionalpredicates: a
/*[ … ]*/optional block is removed when its params are inactive, so an emptyfilter can mean "no filter" without a sentinel; explicit
filterActivestate (persistedalongside
varValues) distinguishes inactive from active-empty-string.requires them. Dashboard: multi-filter option bundles, shared preview, and role-aware result selector #160 ships only an independent, single-select Filter source; a dependency DAG,
evaluation order, and cache-key policy will be designed against a real use case if one appears.
limitation.
filters:bundle — all deferred post-v1, each needing its own small design.