Meta/tracking issue + living build plan . Architecture is settled (#88 / ADR-0001); the phases below are ordered so each refactor lands on a stable base and the most-wanted features ship early. Check items off as they land.
Architecture (settled — #88 , ADR-0001)
State: @preact/signals-core, migrated slice-by-slice. No UI framework (React/Preact/Solid). A Preact spike on the schema panel (spike/preact-schema, ADR-0001 addendum) confirmed a component model removes the in-place-mutation pain but buys a second render paradigm the roadmap doesn't justify.
Shape: signals (state) · pure logic in src/core/ · imperative adapters behind injected seams for the hard / third-party / high-frequency-pointer surfaces (editor, EXPLAIN + schema graphs, Chart.js, result grid). Extract a shared primitive (EditorPort, GraphSurface, a result-view registry, Drawer) on the second consumer — not speculatively (CLAUDE.md rule 5).
Types: ADR-0002 (2026-07) — incremental strict TypeScript, dev-time only : tsc --noEmit joins the gate, files convert leaf-up one at a time (seams → state.js → core/ → net/ → editor/ → ui/), persisted-data types generated from the canonical JSON Schemas; esbuild and the artifact untouched, runtime deps stay four. Phases 0–5 landed via ADR-0002 phase 0: TypeScript gate — tsconfig, tsc --noEmit in the test gate, typed seam interfaces #262 /PR ADR-0002: strict TypeScript migration — gate + phases 1–5 (generated schema types, state, contract spine, param pipeline, dashboard runtime, tests) #265 (gate, generated schema types, state, contract spine, param pipeline, dashboard runtime + tests); the remainder converts leaf-up with feature work. The no-framework line was re-evaluated at the dashboard milestone and stands (ADR-0001 2026-07 addendum, with a sharpened concrete re-evaluation trigger).
Build order
Phase 0 — baseline before refactors (cheap, do first)
Phase 1 — finish reactivity (#88 )
Phase 2 — near-term wins
Cap SELECT result rows (default 500) with a 100/500/1000/5000/10000 selector #86
Multiquery + run-selection: execute a ;-separated script (DDL / INSERT / single-row SELECT) with per-statement output #83
Export button: stream full query result to disk as TSV (File System Access API, uncapped, bypasses the grid) #87 — streaming Export, uncapped, via File System Access API — merged (PR feat: streaming Export — stream full query result to disk, uncapped (#87) #103 )
Export script: run multi-statement SQL with log pane and multiple output files #99 — script export (multi-statement follow-up to Export button: stream full query result to disk as TSV (File System Access API, uncapped, bypasses the grid) #87 ): directory picker, sequential per-statement files, live log pane — PR feat: script export — multi-statement follow-up to streaming Export (#99) #104 open
Support variables in SELECT queries #134 — query variables: detect {name:Type} while editing, show an input strip, gate Run until filled, and substitute via ClickHouse's native param_<name> args (server-side, type-correct); reads only, so CREATE VIEW definitions stay verbatim. Values shared by name + persisted (asb:varValues). Pure detection/classification in src/core/query-params.js; no new dep. Distinct from Composable queries: reference other library queries via {{name}} (CTE-merge) #39 ({{name}} composable-query CTE-merge — different syntax and purpose). — merged (feat: query variables ({name:Type}) with native param_ substitution (#134) #138 ) ; follow-up filter/variable work now tracked in Phase 7
Phase 3 — Windows
Detached-tab pattern: extract shared primitive; add Data Pane Expand + convert Pipeline Expand #100 — detached-tab primitive (src/ui/detached-view.js) extracted from the schema graph's tab/overlay logic; now shared by the schema graph, the EXPLAIN pipeline graph (gains real-tab support), and a new Data Pane Expand (snapshot grid, sort/copy, in a tab or overlay). app.state.detachedView (a count) tracks open views. — PR open
Cell-detail right-hand drawer: add horizontal resize #101 — cell-detail drawer horizontal resize (drag handle, splitters.js 'drawer' axis) — PR open
UI consistency polish: disclosure chevrons, toast dismiss, popover autofocus, open-state tracking #102 — UI consistency polish: disclosure chevrons, toast dismiss, popover autofocus, open-state tracking — PR open
Phase 4 — editor: CodeMirror 6 + the features it unlocks
Strict sequence — each issue is the foundation of the next: #143 → #21 → #84 → #60 .
EditorPort: extract the editor behind an injected port; textarea as first adapter (no behavior change) #143 — EditorPort seam: every consumer moves onto an injected port; the current textarea editor becomes its first adapter. No behavior change — makes the CM6 swap reversible.
Adopt CodeMirror 6 behind the EditorPort seam (replace the hand-rolled textarea editor) #21 — adopt CM6 behind the port (the deliberate 4th runtime dep ; bundle delta measured: +406 KB raw / +134 KB gzip, approved at the plan gate): ClickHouse-dialect highlighting, per-tab undo, completion/hover parity v0 from core/completions.js; textarea adapter + superseded ui/editor-* / core/editor-* modules deleted . — PR feat: adopt CodeMirror 6 behind the EditorPort seam (#21) #145 open ; manual real-Safari pass still owed post-merge (De-risk the html{zoom} layout dependency (Safari verification + fallback/guard) #70 lesson).
Column-name autocompletion: FROM-driven column loading + alias/scope awareness #84 — schema-aware autocomplete on CM6: pure core/from-scope.js (FROM/alias resolution) + debounced idle-tick column loading feeding the completion source (never on the keystroke path). — PR feat: FROM-aware autocompletion — alias/scope resolution + debounced column loading (#84) #146 open (alias→table resolution, statement-scoped unqualified columns, 300ms idle-tick column prefetch; core/from-scope.js + core/completions.js at 100%).
Version-exact ClickHouse reference documentation in Workbench #60 — version-exact reference docs (umbrella: Docs Phase 1: rich function reference in CM6 hover and docs pane #313 function docs / Docs Phase 2: structured reference for formats, engines, and data types #314 formats+engines+types / Docs Phase 3: broad system.documentation reference and safe Markdown rendering #315 system.documentation+safe Markdown): capability-gated per-source probing (no blanket 26.6 check; system.documentation alone version-short-circuited), CM6 hover/completion cards + F1 + accessible disambiguation, persistent non-modal reference pane, schema-surface engine/type actions, bounded pure Markdown parser reusing markdown-lite's inline layer. Shared Drawer chrome extracted from the .cd-* scaffold (cell detail + rows viewer + docs pane = 3 consumers; schema-detail keeps its own geometry per the umbrella non-goal). Editor layer now an enforced leaf (check-boundaries: src/editor must not import src/ui). — PR feat(#60): version-exact ClickHouse reference docs — CM6 hover/F1, non-modal pane, structured sources, system.documentation Markdown #320 open ; live-verified on otel (26.6.1).
Shared read-only CodeMirror viewer for structured and large text #213 — shared injected read-only CodeMirror viewer: text/JSON/SQL/XML + HTML-source/plain-Markdown modes, line numbers/search/wrapping, detached-document realm, idempotent teardown; common presentation only, editable EditorPort behavior unchanged. Foundation for Cell detail: type-aware structured content viewer with shared Markdown rendering #214 . — PR feat: add shared read-only CodeMirror viewer #215 open
Phase 5 — schema / data-flow graph
Phase 6 — Dashboard (#149 )
Epic: open favorited Library queries as an interactive dashboard in a new tab
(/sql/dashboard). Spec + pinned decisions + sub-phases D1–D8 live in #149 . Same architecture
(signals + hyperscript, no framework — reaffirmed against the React mockup); one artifact,
client-side route; auth via one-time postMessage handoff + login fallback. Builds on #134
({name:Type} params / varValues) and the chart core.
D1 — feat: open favorited Library queries as a read-only Dashboard (#149 D1) #150 (merged)
D2 — feat: dashboard Arrange/Report layout switcher (#149 D2) #151 (merged)
D3 — **Phase D3 — global filters (text)**: filter bar over detected {name:Type} params, re-run affected tiles #152 (PR feat: dashboard global filter bar (#149 D3) #156 merged)
Extract the renderGrid local-state wiring (sort/widths + repaint) — four hand-rolled copies #167
Panels: visualization registry + Panel drawer tab + Library panel field #166 — Panels delivered: visualization registry + Panel result view + saved panel
field. Panels are visual only (chart/table/logs/text; KPI follows separately).
Saved-query format v2: separate SQL from an extensible Spec document #211
Workbench: add separate SQL and Spec JSON editor modes #212 — PR feat: add separate SQL and Spec editor modes #218 open
Spec: canonical JSON Schema service for validation and shared tooling #220 — canonical Draft 2020-12 Spec schema + shared validation/tooling service — PR feat: add canonical Spec schema service #222 open
Spec editor: schema-driven autocomplete with SQL-editor-consistent popup UX #221 — schema-driven Spec editor autocomplete with native SQL-consistent popup UX — PR feat(#221): add schema-driven Spec autocomplete #226 open
D6 — Dashboard: multi-filter option bundles, shared preview, and role-aware result selector #160 — PR feat(#160): add Dashboard Filter sources #232 open : query-backed dashboard Filter source, strict single-select MVP.
Adds saved-query Dashboard use (Panel | Filter source), option-query execution,
persisted-selection reconciliation, and fallback diagnostics. No cascading/cache/multiselect.
Dashboard: shared-session setup wave and role-aware drawer integration #175 — dashboard Setup sources — deferred 2026-07-17 (no urgency) : conflicts with
Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 's "no Setup execution in Dashboard v1" pin; needs a rewrite against the
DashboardDocumentV1 model before reviving. role: "setup" stays reserved in the Spec.
Dashboard filter bar: clear actions, active count, impact hints, and overflow #188 — filter-bar clear actions, active count, impact hints, and overflow — closed
2026-07-17, dissolved : clear one/all + active count + never-hide-blocking absorbed into
Dashboard v1 phase 4 (#280): DashboardViewerSession + normative flow@1 layout #286 as baseline filter-bar requirements; impact hints and overflow dropped
(deferred-until-demonstrated-need — trivial post-Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 via targets).
Dashboard query-backed filters: searchable multiselect with Apply #189 — searchable query-backed multiselect with Apply — PR feat(#189): searchable multiselect for query-backed Dashboard filters with Apply #364 (merge gate) :
runtime-inferred single/multiselect from the agreed consumer type
(Array(T) → multiselect) with optional selection.mode override,
dedicated draft-until-Apply checklist control, arrays end-to-end through
the typed pipeline/persistence, targets-aware affected-panel planning,
and array-aware option-refresh reconciliation.
D5 — Phase D5 — KPI panel: one-row scalar/named-tuple cards, canonical Presentation Spec contract, and shared renderer #154 — PR Phase D5 — KPI panel: scalar and named-tuple cards #231 open
Make the detached Data view interactive: Panel switcher + query filters #185 — interactive detached Data view: Expand opens a re-runnable full-screen
surface (Panel switcher + {name:Type} filter row) bound to the result's captured
source; shared filter bar (src/ui/filter-bar.js) + shared streaming exec seam
(app.runReadInto) + shared renderResultView. merged (PR feat: interactive detached Data view — Panel switcher + query filters (#185) #194 ).
Detached Data view: keep the committed result visible during streaming reruns #198 — follow-up (bug): detached rerun is now commit-on-success — streaming chunks
update a lightweight progress status only; the previous committed Table/JSON/Panel
stays visible until success (no metadata-only "0 rows" flash, no per-chunk chart churn),
matching the dashboard's progress-only policy. PR open (fix(#198): commit-on-success streaming for the detached Data view #204 ).
Dashboard tiles: migrate onto the shared streaming read seam (app.runReadInto) #193 — follow-up: migrate dashboard tiles onto the shared app.runReadInto streaming
seam (readonly:2 guard, CAP+1/CAP row-cap split, footer stats, 6-way concurrency both
waves, generation+abort reserved at wave creation, explicit-FORMAT rejection). merged (PR refactor(#193): migrate dashboard tiles onto the shared app.runReadInto streaming seam #199 ).
Dashboard KPI bands: flat content-sized cards across layout modes #240 — Dashboard KPI bands: explicit favorited KPI queries render as a
full-width flat card stream (spanning every layout mode) instead of a
nested KPI grid inside a generic tile; auto-detected KPI results remain
ordinary tiles. Extracted renderKpiCards() (kpi-panel.js) shared by the
workbench preview and the new src/ui/dashboard-kpi-band.js module. merged (PR feat(#240): Dashboard KPI bands — full-width flat card streams for explicit KPI favorites #247 ).
Dashboard mobile version: header takes too much space #248 — compact mobile Dashboard presentation at the canonical 768px
breakpoint: one-line icon-first header, responsive one-column layout
override, horizontally scrolling filter strip, and no empty mobile toolbar. PR feat(#248): compact Dashboard layout on mobile #250 open.
Chart panels: add compact Line/Area style controls for dense time series #252 — compact Line/Area style presets with deterministic dense-series point
visibility, renderer-independent persistence, and shared workbench/Dashboard rendering. merged (PR feat(#252): add Line and Area chart style controls #253 ).
Chart panels: apply field metadata to legends, tooltips, axes, and visibility #254 — chart field metadata now drives display names, tooltip/axis units
and decimals, descriptions, hidden measures, and NULL gaps through the
shared workbench/Dashboard renderer. merged (PR feat(#254): apply field metadata to charts #255 ).
Chart panels: extend the Style selector with complete presentation presets #256 — seven complete Line/Area presentation presets now cover curve,
points, scale, legend, grid, and axes through the compact Style selector;
unmatched advanced Spec combinations remain visible as Custom. merged (PR feat(#256): add complete chart style presets #259 ).
Chart-family presentation presets: extend the single Style selector for Bar, Column, Area, and Pie #258 — the same single Style selector now carries type-specific Bar,
Column, Area, and Pie/Donut presets in one type-specific panel.cfg.style object,
with shared workbench/Dashboard rendering. PR feat(#258): add chart family style presets #260 open.
Saved-query model: panel controls visualization; optional dashboard.role controls
dashboard participation (panel | filter | setup). Missing role defaults to panel.
Favorite state controls inclusion and is independent of role.
Design detail lives in #149 and the linked issues — this list tracks order + status only.
(History: this checklist once had D3/D4 swapped relative to #149 ; #149 is the source of truth
for phase order.)
Phase 7 — filters & variables
One track: every surface shares the {name:Type} / state.varValues machinery, and every
prerequisite is already merged (#134 , #152 /D3, #84 ) — the whole phase is implementable now,
independent of the Panels track (the query-backed Filter source #160 is dashboard-only and moved
to Phase 6 with its #166 prerequisite; the workbench keeps free text inputs). Build order : #173
first (7.0 foundation — everything else is a pipeline stage or consumer, absorbs #155 ; its
typed serializer is what #189 will need for multiselect), then #165 → #170 → #169 → #171 /
#172 -v1 → #172 -v2; #174 (combobox a11y primitive) is normative from the first dropdown
consumer; its former dashboard clear/overflow/multiselect scope now lives in #286 (clear/count
baseline; #188 dissolved) and #189 (multiselect). Design
detail lives in the issues — this list tracks order + status only.
Phase 7.0 — shared parameter pipeline: analyze()/prepare() two-phase multi-source API, type conflicts, typed serializer (arrays, big ints), field states #173 — PR open (phase7 branch) : two-phase pipeline + typed serializer, absorbs paramArgs doesn't split multi-statement SQL, inconsistent with dashboardParams/readStatementParams #155 ; all gate/exec call sites migrated
Optional SQL blocks /*[ … ]*/ + explicit filter activation — an empty parameter cannot omit surrounding SQL #165 — PR open (phase7 branch) : /*[ … ]*/ optional blocks + state.filterActive, both surfaces
Typed validation for variable inputs — range-checked Int/UInt, Float syntax, inline error + run gating #170 — PR open (phase7 branch) : typed validation (grammar sized by live-server smoke checks; ranges deliberately stricter than the server's silent wraparound)
Relative time values for date/time variables — -1h, now-7d, now/d + preset dropdown #169 — PR open (phase7 branch) : relative time + preset dropdown (first combobox consumer, built to Accessible shared combobox primitive #174 §1)
Recent values for variable inputs — per-name MRU history (10), dropdown on focus #171 — PR open (phase7 branch) : per-variable recents (MRU 10, global-LRU cap), recorded from boundParams per successful statement
Enum variables → dropdown: values from the declared type (v1) or the autocomplete schema cache (v2, workbench) #172 — PR open (phase7 branch) : enum dropdowns, v1 + v2 both shipped (declared-type parsing incl. implicit auto-numbered members; schema-cache inference, suggestion-only)
Accessible shared combobox primitive #174 — accessible shared combobox primitive delivered and consumed by relative-time,
recent-value, and Enum fields. Former dashboard clear/overflow/multiselect scope moved
to Dashboard filter bar: clear actions, active count, impact hints, and overflow #188 and Dashboard query-backed filters: searchable multiselect with Apply #189 (2026-07-17: Dashboard filter bar: clear actions, active count, impact hints, and overflow #188 since dissolved into Dashboard v1 phase 4 (#280): DashboardViewerSession + normative flow@1 layout #286 ; overflow dropped).
Deferred until demonstrated need: dependent/cascading query-backed option sources, option
dependency graph, and cache policy. File a focused issue only when a real dashboard requires it.
Also revisit on demand (each needs its own small design): filters: one-query-many-params
bundle (was a #149 /#160 non-goal) — now the shared Filter-source track: #359 establishes the one-source-runtime boundary (source runs once per wave, provider keyed by source-query id, diagnostics published, options cleared-on-failure + active reconciled), and #360 adds parameterized sources + single-layer dependency scheduling on top (shipped — PR #362 , merge gate); paired from/to time-range quick picker built on #169 ; explicit SQL
NULL filter options (#165 "SQL NULL" notes).
Phase 8 — Dashboard as a first-class module (#280 )
Epic: retire the favorites-derived dashboard in favor of an explicit persisted
DashboardDocumentV1 inside an atomic StoredWorkspaceV1 aggregate (one current workspace,
zero-or-one Dashboard), PortableBundleV1 interchange, presentation variants via JSON Merge
Patch, a normative flow@1 layout, and separate authoring/viewer sessions. Normative spec in
#280 ; six sequential sub-issues. Starts after #276 completes (phase 4's viewer session builds
on #276 3b DashboardSession); #283 is pure core/schema work and may start in parallel with the
#276 tail. Absorbs and closed: #235 (wave scheduling → #286 ), #153 (reorder/span → #286 ,
open-in-window → #288 ).
Dashboard v1 phase 1 (#280): contracts, codecs, canonical encoding, resource limits #283 — phase 1: contracts, codecs, canonical encoding, resource limits (decided:
additive query-spec-v1 revision for variants/size-hints; maxDecodedJsonBytes stays
10 MiB, IndexedDB-backed phase 2) — shipped (phase-8 PR)
Dashboard v1 phase 2 (#280): atomic StoredWorkspaceV1 persistence + legacy migration #284 — phase 2: atomic StoredWorkspaceV1 persistence + one-shot legacy migration
(decided: IndexedDB aggregate store behind an injected seam; spec.favorite dual-write
removal path documented) — shipped (phase-8 PR)
Dashboard v1 phase 3 (#280): authoring domain — atomic commands, presentation resolution, workspace-wide validation #285 — phase 3: authoring domain — atomic fallible commands, JSON Merge Patch presentation
resolution, whole-workspace validation on every saved-query mutation — shipped (phase-8 PR)
Dashboard v1 phase 4 (#280): DashboardViewerSession + normative flow@1 layout #286 — phase 4: DashboardViewerSession + normative flow@1 (closes Dashboard Refresh serializes the Filter wave before the Panel wave for every tile, even unaffected ones #235 /Dashboard: drag-reorder, open panel in window. #153 -reorder;
derived KPI bands). Owner UX override at ship time: tile reorder is pointer-drag-only (redesigned in Use command-drag for Dashboard tile moves and add cell-detail preview to table/log tiles #332 : grip-drag with no modifier or ⌘/Ctrl body-drag + live grafana-grid reflow, ≥2/3-overlap commit; plain body drags select text; read-only never reorders) and
span/height are tuned in the Spec editor (the keyboard-reorder / in-tile sizing controls from
Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 's accessibility section were removed by owner decision — a11y-revisitable follow-up). The
migrated filter bar restores the rich relative-time/recent/enum fields (regression fix) over
the viewer's filter model. — shipped (phase-8 PR) . Reversed by Remove Dashboard “Clear all” control and make filter strips horizontally scroll instead of wrapping #294 (2026-07-18): the
visible Clear-all filter button/toolbar wrapping was undone — filters now scroll horizontally
in a fixed-height row at every width (never wrap) with no visible Clear-all control;
clearAllFilters() stays as a tested application-level operation with no UI trigger.
Dashboard v1 phase 5 (#280): portable open/import/export + transactional import planner #287 — phase 5: portable open/import/export, transactional import planner, Append UX
replaced by Import queries / Import Dashboard / Replace workspace + Export Dashboard/workspace
— PR open 2026-07-18. Re-scoped by owner decision: the StoredWorkspaceV1
aggregate became the saved-query source of truth (strict async CRUD replaces the
flat asb:saved path), broader than the issue's written acceptance; the
external-bundle trust preflight + transport were moved to Dashboard v1 phase 6 (#280): direct + full-screen viewing with one-time IndexedDB handoff #288 (Phase 5 file
ops only commit to the workspace, never execute). Follow-ups filed: Wire the Workbench star button to Dashboard tile membership (post-#287 aggregate) #299
(Workbench star → Dashboard tile wiring), Corrupt-but-present workspace aggregate silently blocks re-migration with no error surfaced #300 (corrupt-aggregate surfacing),
Dashboard filter values are not persisted across reload (regression from #280 rewrite) #303 (Dashboard filter values lost on reload — regression from the Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280
rewrite). Being shipped together 2026-07-18 (one PR): Corrupt-but-present workspace aggregate silently blocks re-migration with no error surfaced #300 + Dashboard filter values are not persisted across reload (regression from #280 rewrite) #303 fully
closed; Wire the Workbench star button to Dashboard tile membership (post-#287 aggregate) #299 partial — the star→tile wiring lands, the spec.favorite
dual-write retirement stays deferred.
Dashboard v1 phase 6 (#280): direct + full-screen viewing with one-time IndexedDB handoff #288 — phase 6: bookmarkable workspace route + one-time IndexedDB session-bundle handoff
(closes Dashboard: drag-reorder, open panel in window. #153 -open-in-window) — shipped 2026-07-18 (PR feat(#288, #302): Dashboard v1 phase 6 — direct + full-screen viewing (one-time IndexedDB handoff) + File-menu restructure #306 , with Move Dashboard navigation and file operations out of the Workbench File menu #302 File-menu restructure; ADR-0003) .
Now also carries the external-bundle trust preflight ("Run Dashboard on " confirm,
never-auto-execute, validate-before-execute, row/byte caps, bounded concurrency) moved here
from Dashboard v1 phase 5 (#280): portable open/import/export + transactional import planner #287 , since the untrusted-external-view execution path lands with this phase's transport.
Add a rowless Grafana-style tile-grid Dashboard layout #291 — post-Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 follow-on: grafana-grid@1, a rowless Grafana-style 12-column tile-grid
layout engine next to the normative flow@1 (deterministic packing from tiles[] order,
per-tile span 1–12 + semantic heights, container-width column clamp, Workbench-only
drag-reorder/corner-resize/delete with immediate propagation, auto-regenerated valid
flow@1 fallback on every grid mutation; no rows/folding, no Save/Undo — owner decisions
2026-07-18). Registration-time-lazy in the layout registry (single-file artifact keeps
hard rule 4; no network splitting). — PR open 2026-07-18.
Keep Dashboard and Workbench on one committed workspace state and make exports consistent #341 — post-Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 bug: make editable Dashboard + Workbench two editors over
ONE committed StoredWorkspaceV1. Dashboard commands (move/resize/layout/
delete) now run through the shared app.serializeWrite queue (all workspace
writes share one order), build each candidate inside the queued op from the
freshest committed baseline (monotonic revisions), project the committed
result via app.applyCommittedWorkspace, and roll back deterministically on
failure; both exports flushWorkspaceWrites() → loadCurrent() so the
Dashboard doc + shared queries are canonically identical across bundles.
Same-tab serialization only; cross-tab revision CAS deferred to Dashboard/Workbench cross-tab optimistic concurrency (revision CAS) #343
(per Keep Dashboard and Workbench on one committed workspace state and make exports consistent #341 's cross-tab minimum contract — reload consistency holds). — PR open.
Cross-cutting
Refactor app.ts into explicit application services and route sessions #276 — refactor app.ts into explicit application services and route sessions
(modular monolith; sequential PRs, one per phase — plan + considered API
deviations recorded in the issue's first comment):
ADR-0002 phase 0: TypeScript gate — tsconfig, tsc --noEmit in the test gate, typed seam interfaces #262 — ADR-0002 phases 0–5 (scope grew from phase 0 by maintainer decision — one PR instead of per-phase issues): the TypeScript gate, generated persisted-data types (build/emit-schema-types.mjs → src/generated/json-schema.types.ts; hand-rolled emitter, see ADR-0002 addendum), and strict conversion of 16 modules — state.ts, the saved-query/panel/dashboard contract spine, the param/filter/execution pipeline, and the dashboard runtime — plus their unit tests. PR ADR-0002: strict TypeScript migration — gate + phases 1–5 (generated schema types, state, contract spine, param pipeline, dashboard runtime, tests) #265 . Build/artifact untouched; runtime deps stay four. Remaining .js (app.js glue, net/, editor adapters, rest of core/ui) still converts leaf-up folded into normal feature work — inventory, test-helper debt (fake-app App stub), and scaffolding retirement tracked in Finish the ADR-0002 TypeScript migration: remaining .js modules, fake-app App stub, mixed-tree scaffolding retirement #267 .
full html{zoom} removal — the 1.2× scale went away first (--zoom: 1, PR fix: remove html{zoom:1.2} to kill phantom scrollbars; restore editor bars #147 ) , then the dead zoom-bridging scaffolding itself was torn out (Remove dormant html{zoom} and coordinate-compensation infrastructure #148 ): html{zoom}/--zoom/--vp-zoom are gone from CSS, zoomScale()/viewportZoom()/app.applyViewportZoom are deleted, fixedAnchor/splitter/grid-column-resize/schema-detail/cell-drawer/Chart.js all operate on native coordinates with no scale argument. One coordinate system (native browser CSS px) app-wide now. Still wants a GraphSurface Schema graph: multi-select + group-move of nodes (marquee, Shift-click, ⌘A) with single-step undo #66 / real-Safari manual pass for confidence, but that's no longer a special architectural gate — it's ordinary release QA.
Release-blockers (1.0 must not ship without)
Meta/tracking issue + living build plan. Architecture is settled (#88 / ADR-0001); the phases below are ordered so each refactor lands on a stable base and the most-wanted features ship early. Check items off as they land.
Architecture (settled — #88, ADR-0001)
@preact/signals-core, migrated slice-by-slice. No UI framework (React/Preact/Solid). A Preact spike on the schema panel (spike/preact-schema, ADR-0001 addendum) confirmed a component model removes the in-place-mutation pain but buys a second render paradigm the roadmap doesn't justify.src/core/· imperative adapters behind injected seams for the hard / third-party / high-frequency-pointer surfaces (editor, EXPLAIN + schema graphs, Chart.js, result grid). Extract a shared primitive (EditorPort,GraphSurface, a result-view registry,Drawer) on the second consumer — not speculatively (CLAUDE.md rule 5).tsc --noEmitjoins the gate, files convert leaf-up one at a time (seams →state.js→core/→net/→editor/→ui/), persisted-data types generated from the canonical JSON Schemas; esbuild and the artifact untouched, runtime deps stay four. Phases 0–5 landed via ADR-0002 phase 0: TypeScript gate — tsconfig, tsc --noEmit in the test gate, typed seam interfaces #262/PR ADR-0002: strict TypeScript migration — gate + phases 1–5 (generated schema types, state, contract spine, param pipeline, dashboard runtime, tests) #265 (gate, generated schema types, state, contract spine, param pipeline, dashboard runtime + tests); the remainder converts leaf-up with feature work. The no-framework line was re-evaluated at the dashboard milestone and stands (ADR-0001 2026-07 addendum, with a sharpened concrete re-evaluation trigger).Build order
Phase 0 — baseline before refactors (cheap, do first)
html{zoom}dependency — fix landed (PR fix(zoom): correctly size fullscreen graph panels on Safari under html{zoom} (#70) #92). Manual Safari testing showed the only real divergence is viewport units underzoom(Chromium'svh/vwignore it, Safari's track it), which mis-sized the fullscreen graph panels on Safari; the divisor is now measured at runtime and published as--vp-zoom, plus the@supports not (zoom:1)1× fallback. (The pointer/caret/drag math self-calibrates, so it was already correct on Safari — the originally-planned "mis-scaling banner" was dropped as a false-positive.) Finding: Playwright WebKit ≠ real Safari forzoom×rect/viewport, so Add WebKit/Safari to e2e + decide the supported-browser stance #69's CI "Safari verified" didn't exercise this — folded into Document the supported-browser matrix (browsers, ClickHouse versions, IdP requirements) #71. Fullzoomremoval still folds into Phase 4/5.Phase 1 — finish reactivity (#88)
Phase 2 — near-term wins
{name:Type}while editing, show an input strip, gate Run until filled, and substitute via ClickHouse's nativeparam_<name>args (server-side, type-correct); reads only, soCREATE VIEWdefinitions stay verbatim. Values shared by name + persisted (asb:varValues). Pure detection/classification insrc/core/query-params.js; no new dep. Distinct from Composable queries: reference other library queries via {{name}} (CTE-merge) #39 ({{name}}composable-query CTE-merge — different syntax and purpose). — merged (feat: query variables ({name:Type}) with native param_ substitution (#134) #138); follow-up filter/variable work now tracked in Phase 7Phase 3 — Windows
src/ui/detached-view.js) extracted from the schema graph's tab/overlay logic; now shared by the schema graph, the EXPLAIN pipeline graph (gains real-tab support), and a new Data Pane Expand (snapshot grid, sort/copy, in a tab or overlay).app.state.detachedView(a count) tracks open views. — PR openPhase 4 — editor: CodeMirror 6 + the features it unlocks
Strict sequence — each issue is the foundation of the next: #143 → #21 → #84 → #60.
EditorPortseam: every consumer moves onto an injected port; the current textarea editor becomes its first adapter. No behavior change — makes the CM6 swap reversible.core/completions.js; textarea adapter + supersededui/editor-*/core/editor-*modules deleted. — PR feat: adopt CodeMirror 6 behind the EditorPort seam (#21) #145 open; manual real-Safari pass still owed post-merge (De-risk the html{zoom} layout dependency (Safari verification + fallback/guard) #70 lesson).core/from-scope.js(FROM/alias resolution) + debounced idle-tick column loading feeding the completion source (never on the keystroke path). — PR feat: FROM-aware autocompletion — alias/scope resolution + debounced column loading (#84) #146 open (alias→table resolution, statement-scoped unqualified columns, 300ms idle-tick column prefetch;core/from-scope.js+core/completions.jsat 100%).system.documentation+safe Markdown): capability-gated per-source probing (no blanket 26.6 check;system.documentationalone version-short-circuited), CM6 hover/completion cards + F1 + accessible disambiguation, persistent non-modal reference pane, schema-surface engine/type actions, bounded pure Markdown parser reusing markdown-lite's inline layer. SharedDrawerchrome extracted from the.cd-*scaffold (cell detail + rows viewer + docs pane = 3 consumers; schema-detail keeps its own geometry per the umbrella non-goal). Editor layer now an enforced leaf (check-boundaries: src/editor must not import src/ui). — PR feat(#60): version-exact ClickHouse reference docs — CM6 hover/F1, non-modal pane, structured sources, system.documentation Markdown #320 open; live-verified on otel (26.6.1).Phase 5 — schema / data-flow graph
GraphSurface(src/ui/graph-surface.js) +src/core/graph-selection.js(pure) — shared by the schema graph and, where possible, the EXPLAIN pipeline graphGraphSurfacecore/)Phase 6 — Dashboard (#149)
Epic: open favorited Library queries as an interactive dashboard in a new tab
(
/sql/dashboard). Spec + pinned decisions + sub-phases D1–D8 live in #149. Same architecture(signals + hyperscript, no framework — reaffirmed against the React mockup); one artifact,
client-side route; auth via one-time
postMessagehandoff + login fallback. Builds on #134(
{name:Type}params /varValues) and the chart core.{name:Type}params, re-run affected tiles #152 (PR feat: dashboard global filter bar (#149 D3) #156 merged)panelfield. Panels are visual only (chart/table/logs/text; KPI follows separately).
Adds saved-query Dashboard use (
Panel | Filter source), option-query execution,persisted-selection reconciliation, and fallback diagnostics. No cascading/cache/multiselect.
Dashboard: shared-session setup wave and role-aware drawer integration #175 — dashboard Setup sources— deferred 2026-07-17 (no urgency): conflicts withMake Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280's "no Setup execution in Dashboard v1" pin; needs a rewrite against the
DashboardDocumentV1model before reviving.role: "setup"stays reserved in the Spec.Dashboard filter bar: clear actions, active count, impact hints, and overflow #188 — filter-bar clear actions, active count, impact hints, and overflow— closed2026-07-17, dissolved: clear one/all + active count + never-hide-blocking absorbed into
Dashboard v1 phase 4 (#280): DashboardViewerSession + normative flow@1 layout #286 as baseline filter-bar requirements; impact hints and overflow dropped
(deferred-until-demonstrated-need — trivial post-Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 via
targets).runtime-inferred single/multiselect from the agreed consumer type
(
Array(T)→ multiselect) with optionalselection.modeoverride,dedicated draft-until-Apply checklist control, arrays end-to-end through
the typed pipeline/persistence, targets-aware affected-panel planning,
and array-aware option-refresh reconciliation.
surface (Panel switcher + {name:Type} filter row) bound to the result's captured
source; shared filter bar (src/ui/filter-bar.js) + shared streaming exec seam(
app.runReadInto) + sharedrenderResultView. merged (PR feat: interactive detached Data view — Panel switcher + query filters (#185) #194).update a lightweight progress status only; the previous committed Table/JSON/Panel
stays visible until success (no metadata-only "0 rows" flash, no per-chunk chart churn),
matching the dashboard's progress-only policy. PR open (fix(#198): commit-on-success streaming for the detached Data view #204).
app.runReadIntostreamingseam (readonly:2 guard, CAP+1/CAP row-cap split, footer stats, 6-way concurrency both
waves, generation+abort reserved at wave creation, explicit-FORMAT rejection). merged (PR refactor(#193): migrate dashboard tiles onto the shared app.runReadInto streaming seam #199).
full-width flat card stream (spanning every layout mode) instead of a
nested KPI grid inside a generic tile; auto-detected KPI results remain
ordinary tiles. Extracted
renderKpiCards()(kpi-panel.js) shared by theworkbench preview and the new
src/ui/dashboard-kpi-band.jsmodule. merged (PR feat(#240): Dashboard KPI bands — full-width flat card streams for explicit KPI favorites #247).breakpoint: one-line icon-first header, responsive one-column layout
override, horizontally scrolling filter strip, and no empty mobile toolbar. PR feat(#248): compact Dashboard layout on mobile #250 open.
visibility, renderer-independent persistence, and shared workbench/Dashboard rendering. merged (PR feat(#252): add Line and Area chart style controls #253).
and decimals, descriptions, hidden measures, and NULL gaps through the
shared workbench/Dashboard renderer. merged (PR feat(#254): apply field metadata to charts #255).
points, scale, legend, grid, and axes through the compact Style selector;
unmatched advanced Spec combinations remain visible as Custom. merged (PR feat(#256): add complete chart style presets #259).
Column, Area, and Pie/Donut presets in one type-specific
panel.cfg.styleobject,with shared workbench/Dashboard rendering. PR feat(#258): add chart family style presets #260 open.
Saved-query model:
panelcontrols visualization; optionaldashboard.rolecontrolsdashboard participation (
panel | filter | setup). Missing role defaults topanel.Favorite state controls inclusion and is independent of role.
Design detail lives in #149 and the linked issues — this list tracks order + status only.
(History: this checklist once had D3/D4 swapped relative to #149; #149 is the source of truth
for phase order.)
Phase 7 — filters & variables
One track: every surface shares the
{name:Type}/state.varValuesmachinery, and everyprerequisite is already merged (#134, #152/D3, #84) — the whole phase is implementable now,
independent of the Panels track (the query-backed Filter source #160 is dashboard-only and moved
to Phase 6 with its #166 prerequisite; the workbench keeps free text inputs). Build order: #173
first (7.0 foundation — everything else is a pipeline stage or consumer, absorbs #155; its
typed serializer is what #189 will need for multiselect), then #165 → #170 → #169 → #171 /
#172-v1 → #172-v2; #174 (combobox a11y primitive) is normative from the first dropdown
consumer; its former dashboard clear/overflow/multiselect scope now lives in #286 (clear/count
baseline; #188 dissolved) and #189 (multiselect). Design
detail lives in the issues — this list tracks order + status only.
/*[ … ]*/+ explicit filter activation — an empty parameter cannot omit surrounding SQL #165 — PR open (phase7 branch):/*[ … ]*/optional blocks +state.filterActive, both surfaces-1h,now-7d,now/d+ preset dropdown #169 — PR open (phase7 branch): relative time + preset dropdown (first combobox consumer, built to Accessible shared combobox primitive #174 §1)boundParamsper successful statementrecent-value, and Enum fields. Former dashboard clear/overflow/multiselect scope moved
to Dashboard filter bar: clear actions, active count, impact hints, and overflow #188 and Dashboard query-backed filters: searchable multiselect with Apply #189 (2026-07-17: Dashboard filter bar: clear actions, active count, impact hints, and overflow #188 since dissolved into Dashboard v1 phase 4 (#280): DashboardViewerSession + normative flow@1 layout #286; overflow dropped).
Deferred until demonstrated need: dependent/cascading query-backed option sources, option
dependency graph, and cache policy. File a focused issue only when a real dashboard requires it.
Also revisit on demand (each needs its own small design):
filters:one-query-many-paramsbundle (was a #149/#160 non-goal) — now the shared Filter-source track: #359 establishes the one-source-runtime boundary (source runs once per wave, provider keyed by source-query id, diagnostics published, options cleared-on-failure + active reconciled), and #360 adds parameterized sources + single-layer dependency scheduling on top (shipped — PR #362, merge gate); paired from/to time-range quick picker built on #169; explicit SQL
NULLfilter options (#165 "SQL NULL" notes).Phase 8 — Dashboard as a first-class module (#280)
Epic: retire the favorites-derived dashboard in favor of an explicit persisted
DashboardDocumentV1inside an atomicStoredWorkspaceV1aggregate (one current workspace,zero-or-one Dashboard),
PortableBundleV1interchange, presentation variants via JSON MergePatch, a normative
flow@1layout, and separate authoring/viewer sessions. Normative spec in#280; six sequential sub-issues. Starts after #276 completes (phase 4's viewer session builds
on #276 3b
DashboardSession); #283 is pure core/schema work and may start in parallel with the#276 tail. Absorbs and closed: #235 (wave scheduling → #286), #153 (reorder/span → #286,
open-in-window → #288).
additive
query-spec-v1revision for variants/size-hints;maxDecodedJsonBytesstays10 MiB, IndexedDB-backed phase 2) — shipped (phase-8 PR)
StoredWorkspaceV1persistence + one-shot legacy migration(decided: IndexedDB aggregate store behind an injected seam;
spec.favoritedual-writeremoval path documented) — shipped (phase-8 PR)
resolution, whole-workspace validation on every saved-query mutation — shipped (phase-8 PR)
DashboardViewerSession+ normativeflow@1(closes Dashboard Refresh serializes the Filter wave before the Panel wave for every tile, even unaffected ones #235/Dashboard: drag-reorder, open panel in window. #153-reorder;derived KPI bands). Owner UX override at ship time: tile reorder is pointer-drag-only (redesigned in Use command-drag for Dashboard tile moves and add cell-detail preview to table/log tiles #332: grip-drag with no modifier or ⌘/Ctrl body-drag + live grafana-grid reflow, ≥2/3-overlap commit; plain body drags select text; read-only never reorders) and
span/height are tuned in the Spec editor (the keyboard-reorder / in-tile sizing controls from
Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280's accessibility section were removed by owner decision — a11y-revisitable follow-up). The
migrated filter bar restores the rich relative-time/recent/enum fields (regression fix) over
the viewer's filter model. — shipped (phase-8 PR). Reversed by Remove Dashboard “Clear all” control and make filter strips horizontally scroll instead of wrapping #294 (2026-07-18): the
visible Clear-all filter button/toolbar wrapping was undone — filters now scroll horizontally
in a fixed-height row at every width (never wrap) with no visible Clear-all control;
clearAllFilters()stays as a tested application-level operation with no UI trigger.replaced by Import queries / Import Dashboard / Replace workspace + Export Dashboard/workspace
— PR open 2026-07-18. Re-scoped by owner decision: the
StoredWorkspaceV1aggregate became the saved-query source of truth (strict async CRUD replaces the
flat
asb:savedpath), broader than the issue's written acceptance; theexternal-bundle trust preflight + transport were moved to Dashboard v1 phase 6 (#280): direct + full-screen viewing with one-time IndexedDB handoff #288 (Phase 5 file
ops only commit to the workspace, never execute). Follow-ups filed: Wire the Workbench star button to Dashboard tile membership (post-#287 aggregate) #299
(Workbench star → Dashboard tile wiring), Corrupt-but-present workspace aggregate silently blocks re-migration with no error surfaced #300 (corrupt-aggregate surfacing),
Dashboard filter values are not persisted across reload (regression from #280 rewrite) #303 (Dashboard filter values lost on reload — regression from the Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280
rewrite). Being shipped together 2026-07-18 (one PR): Corrupt-but-present workspace aggregate silently blocks re-migration with no error surfaced #300 + Dashboard filter values are not persisted across reload (regression from #280 rewrite) #303 fully
closed; Wire the Workbench star button to Dashboard tile membership (post-#287 aggregate) #299 partial — the star→tile wiring lands, the
spec.favoritedual-write retirement stays deferred.
(closes Dashboard: drag-reorder, open panel in window. #153-open-in-window) — shipped 2026-07-18 (PR feat(#288, #302): Dashboard v1 phase 6 — direct + full-screen viewing (one-time IndexedDB handoff) + File-menu restructure #306, with Move Dashboard navigation and file operations out of the Workbench File menu #302 File-menu restructure; ADR-0003).
Now also carries the external-bundle trust preflight ("Run Dashboard on " confirm,
never-auto-execute, validate-before-execute, row/byte caps, bounded concurrency) moved here
from Dashboard v1 phase 5 (#280): portable open/import/export + transactional import planner #287, since the untrusted-external-view execution path lands with this phase's transport.
grafana-grid@1, a rowless Grafana-style 12-column tile-gridlayout engine next to the normative
flow@1(deterministic packing from tiles[] order,per-tile span 1–12 + semantic heights, container-width column clamp, Workbench-only
drag-reorder/corner-resize/delete with immediate propagation, auto-regenerated valid
flow@1fallback on every grid mutation; no rows/folding, no Save/Undo — owner decisions2026-07-18). Registration-time-lazy in the layout registry (single-file artifact keeps
hard rule 4; no network splitting). — PR open 2026-07-18.
ONE committed
StoredWorkspaceV1. Dashboard commands (move/resize/layout/delete) now run through the shared
app.serializeWritequeue (all workspacewrites share one order), build each candidate inside the queued op from the
freshest committed baseline (monotonic revisions), project the committed
result via
app.applyCommittedWorkspace, and roll back deterministically onfailure; both exports
flushWorkspaceWrites()→loadCurrent()so theDashboard doc + shared queries are canonically identical across bundles.
Same-tab serialization only; cross-tab revision CAS deferred to Dashboard/Workbench cross-tab optimistic concurrency (revision CAS) #343
(per Keep Dashboard and Workbench on one committed workspace state and make exports consistent #341's cross-tab minimum contract — reload consistency holds). — PR open.
Cross-cutting
app.tsinto explicit application services and route sessions(modular monolith; sequential PRs, one per phase — plan + considered API
deviations recorded in the issue's first comment):
check:archboundary guard +QueryExecutionService(
app.exec.executeRead/executeScript/kill;app.runReadIntodeleted) — PR openConnectionSession(auth/config/chCtx lifecycle;app.conn,rendering inverted via
onAuthLost; scalar auth fields left theAppcontract) — PR open
WorkbenchSession(app.workbench: run/script/cancel orchestration,private RunState + AbortController,
attachShelleffects,destroy();boundary guard generalized to a rule list) — PR open
DashboardSession(tile/filter wave runtime, per-slot cancellation,DashboardRuntimeInput, filter-bar dispose seam,destroy())five sub-PRs after plan review split 4B):
SchemaCatalogService(app.catalog; writable refData/completionsaccessors preserved for the CM6 harness;
invalidate()for Phase 5) — PR openWorkbenchParameterSession(app.params)ExportService+ExportSink(app.exports)QueryDocumentSession+SavedQueryService(app.queryDoc/app.saved;sessionParamsForstays app.ts-local — Phase 5 decides its home)SchemaGraphSession+AppPreferences(app.graph/app.prefs)— 4B1–4D shipped as ONE combined PR (user decision) — PR open
survivor:
app.saveVarRecent),ui/workbench/workbench-shell.tsextracted,dashboard shell narrowed, session teardown wired into signOut (behavior
fix),
docs/ARCHITECTURE.mdrewritten — PR open (closes Refactor app.ts into explicit application services and route sessions #276)build/emit-schema-types.mjs→src/generated/json-schema.types.ts; hand-rolled emitter, see ADR-0002 addendum), and strict conversion of 16 modules —state.ts, the saved-query/panel/dashboard contract spine, the param/filter/execution pipeline, and the dashboard runtime — plus their unit tests. PR ADR-0002: strict TypeScript migration — gate + phases 1–5 (generated schema types, state, contract spine, param pipeline, dashboard runtime, tests) #265. Build/artifact untouched; runtime deps stay four. Remaining.js(app.js glue, net/, editor adapters, rest of core/ui) still converts leaf-up folded into normal feature work — inventory, test-helper debt (fake-appAppstub), and scaffolding retirement tracked in Finish the ADR-0002 TypeScript migration: remaining .js modules, fake-app App stub, mixed-tree scaffolding retirement #267.html{zoom}removal — the 1.2× scale went away first (--zoom: 1, PR fix: remove html{zoom:1.2} to kill phantom scrollbars; restore editor bars #147), then the dead zoom-bridging scaffolding itself was torn out (Remove dormanthtml{zoom}and coordinate-compensation infrastructure #148):html{zoom}/--zoom/--vp-zoomare gone from CSS,zoomScale()/viewportZoom()/app.applyViewportZoomare deleted,fixedAnchor/splitter/grid-column-resize/schema-detail/cell-drawer/Chart.js all operate on native coordinates with no scale argument. One coordinate system (native browser CSS px) app-wide now. Still wants aGraphSurfaceSchema graph: multi-select + group-move of nodes (marquee, Shift-click, ⌘A) with single-step undo #66 / real-Safari manual pass for confidence, but that's no longer a special architectural gate — it's ordinary release QA.Release-blockers (1.0 must not ship without)
html{zoom}de-risk) ✅ fix via fix(zoom): correctly size fullscreen graph panels on Safari under html{zoom} (#70) #92 (Safari panel sizing, manually verified) · Document the supported-browser matrix (browsers, ClickHouse versions, IdP requirements) #71 (supported browsers / ClickHouse versions / IdP matrix) — Add WebKit/Safari to e2e + decide the supported-browser stance #69 (WebKit e2e) ✅ done via test(e2e): add WebKit/Safari to the Playwright matrix + decide the support stance (#69) #90