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 — deferred + re-scoped 2026-07-17
onto DashboardFilterDefinitionV1.selection (Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280 ); implement after Dashboard v1 phase 3 (#280): authoring domain — atomic commands, presentation resolution, workspace-wide validation #285 /Dashboard v1 phase 4 (#280): DashboardViewerSession + normative flow@1 layout #286 .
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 (#149 /#160 non-goal); 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 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.
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).onto
DashboardFilterDefinitionV1.selection(Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280); implement after Dashboard v1 phase 3 (#280): authoring domain — atomic commands, presentation resolution, workspace-wide validation #285/Dashboard v1 phase 4 (#280): DashboardViewerSession + normative flow@1 layout #286.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 (#149/#160 non-goal); 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 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.
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