Repository navigation
Roadmap to 1.0.0 #68
Description
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 --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)
- Add WebKit/Safari to e2e + decide the supported-browser stance #69 — add WebKit/Safari to Playwright e2e — done (test(e2e): add WebKit/Safari to the Playwright matrix + decide the support stance (#69) #90, merged)
- De-risk the html{zoom} layout dependency (Safari verification + fallback/guard) #70 — de-risk the
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)
- tabs / activeTabId · sidePanel
- resultView / running · libraryName / libraryDirty — merged (Incremental reactivity via @preact/signals-core + architecture decisions (part of #88) #89)
- Convert the schema slice to signals (schema/schemaError/schemaFilter) — Phase 1 of #88 #91 — schema / schemaFilter → signals (replaced Set/Map, not in-place mutation); the schema panel stays imperative
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 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 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 —
EditorPortseam: 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 + 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). - 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.jsat 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.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). - 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
- extract
GraphSurface(src/ui/graph-surface.js) +src/core/graph-selection.js(pure) — shared by the schema graph and, where possible, the EXPLAIN pipeline graph - Schema graph: multi-select + group-move of nodes (marquee, Shift-click, ⌘A) with single-step undo #66 — multi-select + group-move (marquee / Shift-click / ⌘A, align/distribute, single-step undo) on
GraphSurface - Schema lineage: cap the EXPLAIN AST fan-out on large schemas #42 — cap EXPLAIN AST fan-out on large schemas (pure
core/)
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
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— 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 viatargets). - 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 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. - 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) + sharedrenderResultView. 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.runReadIntostreaming
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. ExtractedrenderKpiCards()(kpi-panel.js) shared by the
workbench preview and the newsrc/ui/dashboard-kpi-band.jsmodule. 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-specificpanel.cfg.styleobject,
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
Reconciled by #447 (2026-07-26). This phase's framing predates the removal of
the curated-filter/provider model. The query-backed Filter source (#160) and
everything layered on it — #359's shared-source runtime, #360's parameterized
sources, #189's multiselect — are removed, not merely superseded. The surviving
concept is a Dashboard VARIABLE inferred from panel SQL; #165's optional blocks,
#170/#171's validation and recents, and the shared{name:Type}/
state.varValuesmachinery all remain and are what variables are built on. Read
the rows below as history for anything filter-source-shaped.Amended by #468 (2026-07-26, shipped). One piece came back: #189's searchable
multiselect CONTROL, now driven by anArray(scalar T)variable's own
Dashboard-local option SQL rather than by a Filter-role source query. Nothing
else from the curated model returned — no provider, no shared source, no target
list, no persisted selection mode. #447's "Multiselect or Array-valued variable
controls" non-goal is annotated accordingly.
One track: every surface shares the{name:Type}/state.varValuesmachinery, 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
boundParamsper 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 control shipped as #335 — PR #376 (merge gate): compound Time-section control with staged From/To popover on the shared popover primitive extracted from the #189 multiselect, atomic applyFilters batch commit (one wave), and one wallNow() snapshot per wave published as waveWallNowMs; #334 — PR #390 (merge gate) replaces #335's interim name-pair inference with authoritative saved-query timeRanges metadata, derives independent groups on Dashboard load, and adds synchronized exact-time crosshairs plus atomic mouse-brush range selection across temporal line/area/column charts; 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:
additivequery-spec-v1revision for variants/size-hints;maxDecodedJsonBytesstays
10 MiB, IndexedDB-backed phase 2) — shipped (phase-8 PR) -
Dashboard v1 phase 2 (#280): atomic StoredWorkspaceV1 persistence + legacy migration #284 — phase 2: atomic
StoredWorkspaceV1persistence + one-shot legacy migration
(decided: IndexedDB aggregate store behind an injected seam;spec.favoritedual-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+ 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;
partly reversed by Dashboard panel header: duplicate, widen and expand actions #535 (2026-07-29): a single in-tile WIDEN button is back on the tile
head, stepping the widths the active style has — the free-form two-dimensional resize stays
the Add a rowless Grafana-style tile-grid Dashboard layout #291 corner drag, and the keyboard-reorder controls stay removed. Tile-header delete should remove the panel’s owned query atomically #537 (2026-07-29)
then cut that head back to two controls: widen keeps its inline button, and duplicate,
expand and remove became rows of a⋯overflow menu — the inline widen additionally
withdrawing below ~260px of tile through a CSS container query, with its menu row remaining). 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: theStoredWorkspaceV1
aggregate became the saved-query source of truth (strict async CRUD replaces the
flatasb:savedpath), 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, thespec.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 normativeflow@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 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 committedStoredWorkspaceV1. Dashboard commands (move/resize/layout/
delete) now run through the sharedapp.serializeWritequeue (all workspace
writes share one order), build each candidate inside the queued op from the
freshest committed baseline (monotonic revisions), project the committed
result viaapp.applyCommittedWorkspace, and roll back deterministically on
failure; both exportsflushWorkspaceWrites()→loadCurrent()so the
Dashboard doc + shared queries are canonically identical across bundles.
Same-tab serialization only. — Merged (PR fix(#341): one committed workspace for Dashboard + Workbench, consistent exports #344). Cross-tab consistency
shipped as Cross-tab workspace consistency: refresh before write and invalidate stale tabs #343 (PR feat(#343): cross-tab workspace consistency — refresh before write, invalidate stale tabs #377): refresh-before-write through one mandatory
mutateWorkspacepath, BroadcastChannel + focus/visibility invalidation,
linked-tab adopt/conflict/orphan reconciliation, and editable-Dashboard
rebuild — revision CAS explicitly OUT of scope per Cross-tab workspace consistency: refresh before write and invalidate stale tabs #343's non-goals
(no successor issue; simultaneous-commit protection stays unplanned). -
Dashboard tile removal leaves saved-query star state inconsistent #370 — Dashboard tile removal now treats
dashboard.tiles[]as canonical panel-query membership: trash removes one tile, scrubs explicit filter targets, preserves the star while another instance remains, mirrorsspec.favorite, and commits one normalized revision; legacy/imported favorite flags never auto-create tiles. — PR fix(#370): keep Dashboard membership and stars consistent #380 (merge gate).
Superseded for panel-owned queries by Tile-header delete should remove the panel’s owned query atomically #537 (2026-07-29): once Separate Library queries from dashboard-owned query copies #427 made every tile the
sole owner of a dedicated saved-query copy, "removes one tile and leaves the query" left that
copy with zero owners — i.e. projected into Library. The tile header now commits the atomic
tile+query removal the Dashboards tree already used, and refuses outright for a shared,
missing, ambiguous, retargeted or non-panel query. The membership normalisation and
spec.favoritemirroring Dashboard tile removal leaves saved-query star state inconsistent #370 defined are unchanged; they run inside that transform.
That trash was also grid-only (a CSS scope plus an engine gate), so Report and the two
column presets had no delete at all until Tile-header delete should remove the panel’s owned query atomically #537 made it a menu row. -
Make KPI tiles frameless and consistently movable across Dashboard layouts #340 — KPI query tiles now share a frameless presentation across every Dashboard style: flow KPI-band members and transparent Grid Tiles/Full-view wrappers move with Command/Ctrl-drag through the shared floating-preview, destination, auto-scroll, cancellation, and exact-once commit path; Grid KPI edit chrome is transient and non-layout-shifting, with keyboard-operable resize. — PR feat(#340): unify frameless KPI movement #388 at merge gate.
Phase 9 — multi-workspace and unified workspace routing
Strict sequence: #406 persistence foundation → #407 canonical unified route → #408 selector/management UI → #409 stale-write protection. The repository phase deliberately retains last-write-wins for simultaneous cross-tab commits; #409 owns conflict detection.
- Multi-workspace persistence: keyed repository, stable workspace keys, list/create/delete #406 — multi-workspace persistence:
StoredWorkspaceV2separates immutable opaqueid, immutable human URLkey, and mutablename; IndexedDB stores one validated record per workspace with a unique key index, collection list/create/replace/delete operations, separate last-used preference plus injected-clocklastOpenedAtfallback, additive workspace import, and stable-key Dashboard links. PR feat: add multi-workspace persistence #410 at merge gate. - Unified /sql routing for workspace and dashboard surfaces with live view/edit modes #407 — unified
/sql?ws=...&surface=...&mode=...routing for Workbench and Dashboard surfaces. - Workspace selector and management UI; simplify File menu for multi-workspace flow #408 — workspace selector/management UI and File-menu simplification.
- Cross-tab workspace conflict detection and stale-write protection #409 — cross-tab conflict detection and stale-write protection.
Phase 10 — multi-Dashboard workspace + Dashboard/Library UX (#430)
Epic: #430 turns the workspace's single Dashboard into a real multi-Dashboard
product surface — selection state, a Databases/Dashboards tree, Library queries
separated from Dashboard-owned copies, copy-on-drag assignment, and tree
editing. #430 specifies ONE continuous pass and ONE final PR closing #424–#429;
#424 was shipped separately by owner request (it is pure storage and has no UI
change, so it stands alone safely). The remaining phases still belong together.
-
Introduce StoredWorkspaceV3 with a dashboard collection and no UI change #424 —
StoredWorkspaceV3: the stored workspace holds an ordered
dashboards[]collection instead of zero-or-one Dashboard. Manifest-compiled
v3 schema, a pure deterministic V2 to V3 migration at the decode boundary
(v2 stays readable; every write is v3; a legacy record upgrades on its next
ordinary commit), per-Dashboard semantic validation against the shared query
collection withdashboards[i]...paths, workspace-unique Dashboard ids with
Dashboard-local tile/filter/layout ids, and the collection bounded by the
shared portable capacity. One centralized compatibility selector
(src/workspace/workspace-dashboards.ts) keeps the UI on exactly one
Dashboard, so favourite-driven membership and every route are behaviourally
unchanged; the Dashboard route commits BY Dashboard id. Export carries every
stored Dashboard and Import workspace ingests every bundled Dashboard (its
single-Dashboard picker retired). Owner decisions at plan time: import-all
over the picker, and lazy record upgrade (no write on the read path). -
Separate Library queries from dashboard-owned query copies #427 — separate Library queries from Dashboard-owned query copies (this is
what finally retires Introduce StoredWorkspaceV3 with a dashboard collection and no UI change #424's temporary favourite/membership coupling) -
Add dashboard selection state and a full-size dashboard work surface #425 — Dashboard selection state + full-size Dashboard work surface: one
persistent shell owns#root(header slot, sidebar, mobile nav) with the
query column and the Dashboard as sibling HOSTS, exactly one exposed — so a
Dashboard owns the complete editor-plus-results area while the sidebar stays
visible, and the Query surface survives a round trip by identity (editor
contents/selection/scroll, active tab, result view, editor/results split)
rather than being rebuilt.openDashboard({dashboardId, mode, focus})is the
one ID-addressed entry point (missing/duplicate ids report through
diagnostics and change nothing);showQuerySurface/openSavedQuery
centralise the way back, and every existing query-opening path returns to
Query mode. Selection is SESSION state inapplication/main-surface.ts—
never persisted, cleared on sign-out, re-validated against every committed
workspace (a deleted or ambiguous selection falls back to Query mode, never
to another Dashboard) — and the single writer of the route's surface/mode,
with URLs unchanged. New surface toolbar: Back to query · title · View|Edit.
Focus lands on one tile (by TILE id) or one curated filter (by filter id)
with a bounded navigation highlight, at the deterministic point each node
exists. Fixes it exposed: a surface switch no longer issuesKILL QUERYvia
workbench.destroy(), a same-workspace Back no longer inerts the mounted
Query controls, sign-out now disposes the Dashboard and advances the
renderer generation, and Export/Import Dashboard address the SELECTED entry.
The favourite star is gated while a non-first Dashboard is selected
(Retire the #425 star gate when #427 separates Dashboard-owned query copies #434 retires that with Separate Library queries from dashboard-owned query copies #427). ADR-0003 addendum records the amended
surface lifecycle. PR feat(#425): Dashboard as a full-size main work surface, selected by stable id #436 at merge gate. -
Add a Databases/Dashboards switcher and dashboard hierarchy tree #426 — Databases/Dashboards switcher + read-only Dashboard hierarchy tree:
an equal-role tab row over TWO PERSISTENT hosts in the upper-left pane, so a
role switch only hides a host and the schema search text/focus, expansion,
lazily-loaded columns and scroll survive by construction (as do the sidebar
width and the upper/lower splitter). Every stored Dashboard is reachable as
Dashboard -> Filters -> Panelsin collection order, with panel counts
(filters excluded), search across all six documented fields with
presentation-time forced expansion that never writes the user's expansion
sets, a full ARIA tree (roving tabindex, Up/Down/Left/Right/Home/End/Enter/
Shift+Enter) and a keyboard-reachable trailing action menu for every
pointer-only operation. Gestures are mutually exclusive through a pure
injected-timer click ARBITER (core/tree-click-arbiter.ts) rather than the
schema tree's post-hoc detector — a panel row's query-open is deferred for
the double-click window and cancelled outright by the second click, so no
Query-surface flash and no duplicate history entry; the chevron stays the
instant expansion path. (Revised by Dashboard tree management: direct Dashboard and Panel actions #429 phase 2: the Dashboard row no
longer arbitrates at all — its name opens View, Shift-click opens Edit, and its
chevron became a real disclosure BUTTON. The arbiter and the deferred
query-open survive on PANEL rows only.) Repeated member navigation inside an
open Dashboard is delivered IN PLACE through the existing generation-stamped
SurfaceCommandPort(focusMember(): ok | pending | missing) — no rebuild,
no re-run, no history entry — andMainSurfaceStatesplits Add dashboard selection state and a full-size dashboard work surface #425's one-shot
focusinto retainedcurrentMember(tree styling, preserved across a
View/Edit switch) plus consumed-oncependingFocus. Strictly read-only: it
never clones, repairs or rewrites a reference, renders a diagnostic row for a
broken one while keeping Dashboard navigation available, and treats a curated
filter with no option source as the TRANSITIONAL state it is before Separate Library queries from dashboard-owned query copies #427 —
not an error. Header cleanup: the obsoleteSQL Browser | Dashboardpair and
a vestigial never-attached Dashboard-nav seam are gone, and Back to query
returns to the compact toolbar (Compact the Dashboard toolbar and remove the redundant surface row #437 had dropped it because the header pair
covered it — with the pair gone a phone would otherwise have no route back).
Owner decision at ship time: the mobile segment is relabelled Explore,
since that pane now holds both roles. PR open. -
Add Library drag assignment to Dashboard panels and variables #428 — Library assignment to Dashboards. Drag path shipped (feat(#428): assign a Library query to a Dashboard by dragging it #484);
keyboard chooser shipped (feat(#483): add keyboard Library assignment chooser #516).
One Library drag publishes two payloads: the existingSUBQUERY_MIMEtext
the editor inserts (PR feat(editor): drag a Library/History query into the editor as a ( … ) subquery #40, untouched) and a newLIBRARY_QUERY_MIME
identity{workspaceId, queryId}that only Dashboard targets read and
re-resolve insidemutateWorkspace. Dropping on a Dashboard row or its
Panels group mints an independent panel-owned copy plus a tile (via
applyCommand('add-query-instance'), so placement follows the source's size
hints); dropping on an INFERRED Variables row copies SQL text only. An
orphaned variable and the Variables group itself both reject. The
dirty-variable-tab gate runs inside the write transaction, not before it.
Acceptance bullet 9 — the keyboardAdd to dashboard…chooser — is
implemented by Keyboard-accessible "Add to dashboard…" command for Library queries #483 / PR feat(#483): add keyboard Library assignment chooser #516 through the same application command;
mobile is out of scope for the drag gesture entirely. -
Dashboard tree management: direct Dashboard and Panel actions #429 — Dashboard-tree management and click operations. Rescoped
2026-07-27: it now ABSORBS Separate Dashboard-tree expansion from opening the Dashboard #472, openSavedQuery switches surfaces before resolving the id, and stays silent when it fails #443, planSavedQueryMutation still has no production caller; deleteSaved rejects instead of offering its repairs #449, A Dashboard the UI cannot reach can make a saved query permanently undeletable #431, A whitespace-only tile title survives into accessible names #476 and Reconcile the two Dashboard-creation paths behind one action #481 (all six
closed) and is the single owner of the Dashboard row's gesture split, the
Dashboard-document create/rename path, and panel-row edit/delete. The
curated-filter half is gone: Replace curated Dashboard filters with inferred Variables and batched option queries #447 phase 1 shipped orphan-config delete + the
per-variable SQL editor, and deleting an ACTIVE inferred variable is a Replace curated Dashboard filters with inferred Variables and batched option queries #447
non-goal. Six phases, one PR each offmain— see the ship log on Dashboard tree management: direct Dashboard and Panel actions #429 for
live status. Phase 1 merged (fix(#429): resolve a saved query before navigating, trim blank tile titles (phase 1) #490):openSavedQueryresolves before
it navigates and reports a miss (openSavedQuery switches surfaces before resolving the id, and stays silent when it fails #443), and the Dashboard viewer trims an
authoredtile.titlebefore the fallback so a whitespace-only title never
reaches a heading or an accessible name (A whitespace-only tile title survives into accessible names #476). Phase 2 merged (feat(#429): split the Dashboard tree row into chevron, name and trailing targets (phase 2) #492):
the Dashboard row is three independent targets (Separate Dashboard-tree expansion from opening the Dashboard #472) — its name opens View
and Shift-click opens Edit, both immediately; expansion belongs to a real
chevron BUTTON with its ownaria-expanded, Expand/Collapse accessible name,
Enter/Space and focus ring; the trailing⋯does neither. Add a Databases/Dashboards switcher and dashboard hierarchy tree #426's delayed
single/double arbitration is gone from Dashboard rows (panel rows keep it
unchanged), and the row's now-redundant Open in View menu item went with it
while Open in Edit stayed, being reachable only through hidden Shift
modifiers. Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438 gates the focus-tile gestures in phases 4/5 on the flow engine
only — phase 2 adds no tile-focus gesture. Phase 3 merged (feat(#429): unify Dashboard creation and add the metadata rename pencil (phase 3) #495): exactly one command
creates a Dashboard now — File ▸ New dashboard… and the empty-workspace
placeholder both prompt and commit throughappendDashboard, never the
compatibility slot (Reconcile the two Dashboard-creation paths behind one action #481) — and each Dashboard row's new pencil edits
title/description on a dialog prefilled from the committed document.
Phases 4 and 5 were re-cut by Dashboard tree: replace overflow menus with direct Panel focus, edit and delete actions (#429 phase 4) #494, which supersedes the old
panel-edit / panel-delete split and folds the⋯-menu removal in with
them. Phase 4 in review (feat(#494): direct Dashboard/Panel row actions, replacing the ⋯ overflow menus (phase 4) #505): no production ROW renders an overflow
menu any more — a Dashboard row carries a pencil and a trash, a Panel row
a pencil that edits its dedicated query's metadata and a trash that removes
the tile and that query atomically, all proven against the Separate Library queries from dashboard-owned query copies #427
exactly-one-owner rule and confirmed before they destroy anything. Phase 5
is Dashboard tree: replace overflow menus with direct Panel focus, edit and delete actions (#429 phase 4) #494's remainder — the Open-in-Dashboard focus button alone — and stays
blocked on Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438, since that button is a known no-op on flow-layout KPI
tiles until Fix Dashboard focus and scrolling for flow-layout KPI tile groups #438 lands. Dashboard tree: replace overflow menus with direct Panel focus, edit and delete actions (#429 phase 4) #494's row-level conclusion is untouched by Tile-header delete should remove the panel’s owned query atomically #537
(2026-07-29), which gave the Dashboard TILE HEAD a⋯: that is a
width-constrained head carrying five controls beside a title, not a
two-control row in a fixed-width side pane. See the ADR-0003 addendum. -
Replace curated Dashboard filters with inferred Variables and batched option queries #447 — inferred Dashboard Variables, replacing the curated filter model.
Supersedes the curated-filter/provider direction and closes Attribute filter option columns per filter, not globally by name (duplicate-provider errors once sources are copied) #450. Phase 1
merged (feat(#447): inferred Dashboard Variables, replacing the curated filter model (phase 1) #458): the whole option-provider layer removed (filter-role
queries, provider discovery by output-column name, duplicate-provider
arbitration, shared-source dedup, arrays/multiselect, per-filter targets), and
variables now INFERRED from{name:Type}in panel-owned SQL, matched exactly
and case-sensitively, with optional Dashboard-local option SQL under
variableConfigs. stored-workspace v5 / Dashboard document v2 /
portable bundle v2, legacy records migrating on read; afilter-role
record is rejected rather than migrated. Dashboard time-range control: token-based From/To picker in the filter bar #335/Add synchronized Dashboard time-range groups, crosshairs, and drag-to-zoom #334 survive, re-based onto
variables. Phase 2 merged (feat(#447): Dashboard variable controls and batched option execution (phase 2) #462): typed direct-input controls, the
option-backed single-select, and oneUNION ALLrequest per refresh reading
option rows POSITIONALLY (tupleElement(tuple(*), n)— a name-keyed read
collapsesSELECT environment, environment). Phase 3 = the
example rewrite,variableConfigsimport/export coverage, docs and E2E —
unblocked: Add Library drag assignment to Dashboard panels and variables #428's PR feat(#428): assign a Library query to a Dashboard by dragging it #484 discharges global bullet 12 (Library SQL onto
a variable), so assignment is no longer phase 3's to build.
See the issue's ship-log comment for the per-phase handoff. -
Remove the variable drawer and edit option SQL in a dedicated main-editor tab #457 — the variable drawer replaced by a main-editor tab. Clicking a
Variables row opens that variable's option SQL as a dedicated
Variable: <name>tab, identified by(dashboardId, variableName), reusing
the real editor / tab strip / toolbar / Run / results instead of the
second SQL editing surface Replace curated Dashboard filters with inferred Variables and batched option queries #447 phase 1 built. Deletes
ui/variable-editor.ts, theVariableEditorseam, the textarea fallback,
runOptionQueryand the drawer-local Test. AddsQueryTab.doc, a
document-kind Save dispatch, and resets variable tabs on a workspace switch
(a Dashboard id is unique per workspace, not globally). Merged (refactor(#457): edit variable option SQL in a main-editor tab, not a drawer #467).
Follow-ups: Validate dashboard-variable option SQL on editor Run #465 (re-host Test's shape check on the tab's Run — done,
restorescompileOptionProbe/isOptionColumnType/validateOptionColumns
and wires them into a newWorkbenchSession.runVariableSql, ahead of the
run()/runScript()split so a multi-statement variable query can never
reach the script runner unvalidated), Reject dashboard-variable option queries returning more than 1000 rows #496 (reject the probe's 1001st
sentinel row before History/source success bookkeeping — in review
(fix(#496): reject oversized variable option probes #534)), No unsaved-change protection when closing a dirty tab #466 (no close-guard for any dirty tab — still open). -
Replace the Dashboard-level Query button with per-tile “Open in Workbench” actions #471 — a per-tile
Open in Workbenchaction replaces the Dashboard-level
< Querybutton. Every query-backed tile opens its OWN query document in
the main editor, in both View and Edit mode; re-opening selects the tab it
already has. No new tab-identity model was needed: Separate Library queries from dashboard-owned query copies #427 already gives each
tile a dedicated owned copy, so itsqueryIdIS the stable per-Dashboard
origin, andloadIntoNewTab/commitSavedQueryalready resolve by id — Disambiguate duplicate query names in editor tabs by stable origin #464
is left owning only the VISIBLE collision badges and origin tooltips.
Consequences: a Grid-Tiles KPI tile's overlay head now reveals in View mode,
and the mobile bottom nav stops hiding itself on the Dashboard surface
(showing only Editor), because Add a Databases/Dashboards switcher and dashboard hierarchy tree #426's phone route died with the button and
a tile-less Dashboard has no per-tile action. Also fixed the hole that removal
exposed: Back out of a tile used to rebuild the workspace's FIRST Dashboard at
the top of the page, so each Dashboard history entry now carries its own id,
current member and scroll offset inhistory.state(never the URL — Add dashboard selection state and a full-size dashboard work surface #425's
contract stands). Follow-up: Flow layout's KPI band members have no delete, grip, resize or title chrome #475, rescoped — flow's KPI band members DO get the
action now (anchored inside the card, the reach-through.is-nav-targetalready
uses); what remains there is delete/grip/resize/title chrome. Merged (feat(#471): a per-tile Open in Workbench action, replacing the Dashboard Query button #477). -
Disambiguate duplicate query names in editor tabs by stable origin #464 — disambiguate duplicate query names in editor tabs by stable
origin. Narrowed by Replace the Dashboard-level Query button with per-tile “Open in Workbench” actions #471, which shipped the identity half: tab reuse and
Save targeting are already by document id, never by name. What remains is
presentation — compact source badges on the COLLIDING tabs only, minimally
unique Dashboard abbreviations, and fullDashboard / querycontext in the
tooltip and accessible name. In review (feat(#464): disambiguate colliding tab origins #539). -
Add a drag-foldable desktop left navigation with rail and focused drawers #487 — a drag-foldable desktop left navigation with a rail and focused
drawers. The vertical frame border becomes the only fold control: dragging
the two-pane sidebar narrow enough converts it to a 48px icon rail, and a
rail icon opens its section as a DOCKED full-height drawer that pushes the
work surface rather than floating over it. No chevron/hamburger/pin buttons.
Shipping in four phases (structure recorded in the issue body; the
<!-- ship-log -->comment is the state of record):
1 — pure layout core (core/left-nav-layout.ts: thresholds, the hysteresis
mode reducer, keyboard resolver, ARIA range) plus the two new browser
preferences, inert (SHIPPED, feat(#487): left-navigation layout core and preferences (phase 1) #571);
2 — navigation section registry, so one set of section hosts serves both the
wide sidebar and the drawer and no second search/expansion/scroll store
exists (SHIPPED, feat(#487): navigation section registry (phase 2) #573);
3 — the rail, the docked focused drawer and therole="separator"resize
control — the feature turns on,openFocusedSection()lands as the
deterministic seam Add Library drag assignment to Dashboard panels and variables #428's drag-hover path needs, and the centre-width clamp
lands with it rather than after it (a safety constraint must not trail the
feature it protects)..sidebaris re-presented viadata-nav-moderather
than a separate drawer module, and a dedicated separator module replaces
splitters.ts's old axis outright (IN REVIEW, feat(#487): rail, focused drawer and resize separator (phase 3) #574);
4 — centre reflow through the existing resize observers, verification that
the clamp holds alongside the right inspector, the Add Library drag assignment to Dashboard panels and variables #428 hover launcher, e2e
and a real-browser threshold pass.
Cross-cutting
-
Preserve the document session across ClickHouse auth loss and reauthentication #512 — authentication lifecycle and document-session preservation: Phases 1–3 merged in feat(#512): add epoch-fenced connection lifecycle #523, feat(#512): suspend authenticated execution without losing the document session #524, and feat(#512): restore dirty documents after OAuth reauthentication (phase 3) #527.
-
Refactor app.ts into explicit application services and route sessions #276 — refactor
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):- Phases 0–1 —
check:archboundary guard +QueryExecutionService
(app.exec.executeRead/executeScript/kill;app.runReadIntodeleted) — PR open - Phase 2 —
ConnectionSession(auth/config/chCtx lifecycle;app.conn,
rendering inverted viaonAuthLost; scalar auth fields left theApp
contract) — PR open - Phase 3 — route-scoped sessions (split into two PRs by plan review):
- 3a —
WorkbenchSession(app.workbench: run/script/cancel orchestration,
private RunState + AbortController,attachShelleffects,destroy();
boundary guard generalized to a rule list) — PR open - 3b —
DashboardSession(tile/filter wave runtime, per-slot cancellation,
DashboardRuntimeInput, filter-bar dispose seam,destroy())
- 3a —
- Phase 4 — supporting services (grouping in the issue's Phase-4 comment;
five sub-PRs after plan review split 4B):- 4A —
SchemaCatalogService(app.catalog; writable refData/completions
accessors preserved for the CM6 harness;invalidate()for Phase 5) — PR open - 4B1 —
WorkbenchParameterSession(app.params) - 4B2 —
ExportService+ExportSink(app.exports) - 4C —
QueryDocumentSession+SavedQueryService(app.queryDoc/app.saved;
sessionParamsForstays app.ts-local — Phase 5 decides its home) - 4D —
SchemaGraphSession+AppPreferences(app.graph/app.prefs)
— 4B1–4D shipped as ONE combined PR (user decision) — PR open
- 4A —
- Phase 5 — composition cleanup: temporary adapters deleted (one documented
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)
- Phases 0–1 —
-
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-appAppstub), 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 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)
- De-risk the html{zoom} layout dependency (Safari verification + fallback/guard) #70 (
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
\n\n## Dashboard layout sizing correction (#549)\n\nDraft PR #549 follows up on #538 with the corrected #535 behavior: grafana-grid@2 persists independent Grid, Full, and Report sizing; 2/3 Columns are session-only 300px previews; and Widen changes width only in Grid or a temporary preview. Full and Report have fixed authored widths and no Widen action.\n
Activity
chore: add issue/PR templates, CODEOWNERS, and a <noscript> fallback
chore: release-readiness hygiene (attribution, CHANGELOG, CONTRIBUTIN…
BorisTyshkevich commentedon Jun 29, 2026
Roadmap state audit (2026-06-29)
Reconciled the checklist against
mainand the linked issues/PRs; ticked the items now actually complete.Newly checked off (all landed in code):
<noscript>fallback, removedesign/prototype — all shipped in PR chore: release-readiness hygiene (attribution, CHANGELOG, CONTRIBUTING, dependabot) #75 (merged). Verified present onmain;design/is gone; the bundled Chart.js/dagre notices are embedded indist/sql.html.dependabot.ymlshipped in PR chore: release-readiness hygiene (attribution, CHANGELOG, CONTRIBUTING, dependabot) #75..github/workflows/is justci.yml+release.yml). If CodeQL is wanted for 1.0, it's still outstanding — say the word and I'll open a follow-up.v0.1.4 (<commit>)in the user menu).Still open / unchecked (accurate):
html{zoom}), Document the supported-browser matrix (browsers, ClickHouse versions, IdP requirements) #71 (browser matrix), Add SECURITY.md: disclosure policy + client_secret-in-config.json threat model #72 (SECURITY.md) — all open. ConfirmedSECURITY.mdis not yet in the repo.prefers-reduced-motion); "desktop-only" statement (tracked in Document the supported-browser matrix (browsers, ClickHouse versions, IdP requirements) #71).Net: the should-have/nice-to-have hygiene is essentially done; what remains for 1.0 is the four release-blockers (#69–#72) plus the per-track feature decisions.
docs: add SECURITY.md (disclosure policy + config.json threat model) (#…
BorisTyshkevich commentedon Jun 30, 2026
Architecture + build order recorded (from #88 + the Preact spike; ADR-0001).
Reactivity:
@preact/signals-core, no UI framework (Preact evaluated on the schema panel and rejected — evidence:spike/preact-schema, ADR-0001 addendum). Architecture going forward: signals (state) + purecore/+ imperative adapters behind seams (editor/graph/Chart); complex UI gets a shared primitive once a second consumer appears (EditorPort, GraphSurface, a result-view registry, Drawer).Build order:
0. #69 — add WebKit to Playwright + #70 WebKit-verify the
html{zoom}dependency (cheap baseline before refactors).EditorPortseam.core/graph-selection→ Schema graph: multi-select + group-move of nodes (marquee, Shift-click, ⌘A) with single-step undo #66.html{zoom}removal, folded into the rewritten editor/graph coordinate paths.test(e2e): add WebKit/Safari to the Playwright matrix + decide the su…
Incremental reactivity via @preact/signals-core + architecture decisi…
fix(zoom): correctly size fullscreen graph panels on Safari under htm…
fix(zoom): correctly size fullscreen graph panels on Safari under htm…
refactor(schema): convert the schema slice to signals (finishes #88) (#…
feat(results): cap SELECT result rows (default 500) with a 100/500/1k…
feat(schema): EXPLAIN pretty/compact, underscore-tables-last, table-d…
fix: remove html{zoom:1.2} to kill phantom scrollbars; restore editor…
Phase 7 — filters & variables: shared param pipeline, optional SQL bl…
2 remaining items
refactor(#276): extract ConnectionSession — phase 2 (#279)
refactor(#276): extract WorkbenchSession — phase 3a (#281)
refactor(#276): extract DashboardSession — phase 3b (#282)
refactor(#276): extract SchemaCatalogService — phase 4A (#289)
feat(#287): Dashboard v1 Phase 5 — portable import/export + import pl…
BorisTyshkevich commentedon Jul 18, 2026
Dashboard track (#280) — Phase 6 shipped; #280 is now 6/6.
#288 (direct + full-screen viewing with the one-time IndexedDB handoff) plus #302 (Dashboard nav/import/export moved out of the Workbench File menu) landed together on
feat/dashboard-viewing-288. New:DashboardOpenSourceroute contract (?ws=&dash=edit /?st=&dash=one-time view handoff), a one-time handoff store + a persistent detached-views store (ADR-0003), edit/view/not-found viewer branching, a Workbench-headerDashboard →control, and the Dashboard page's own File menu. Closes #153. This completes the #280 epic.feat(#291): grafana-grid@1 — rowless Grafana-style tile-grid Dashboar…
BorisTyshkevich commentedon Jul 19, 2026
Roadmap note: #316 (Grafana-grid KPI tile polish — edit/view-mode shell, state-card a11y roles, equal-width card grid, value+unit/description/delta card polish) shipped as a follow-on to #291's
grafana-grid@1. It changes no schema/persistence/placement semantics, so no #68 track re-scoping is needed; recording it here since it isn't a tracked checklist item. Owner decisions pinned on #316 (card polish applies to workbench/flow cards too; flow stream layout unchanged).feat(#332): grip-drag + live grafana-grid reflow for Dashboard tile move
feat(#332): Dashboard Command/Ctrl-drag tile move, text selection, sh…
feat(#426): Databases/Dashboards switcher and a read-only Dashboard h…
chore(ship): drop the stale #68 roadmap references and add coordinato…
BorisTyshkevich commentedon Jul 31, 2026
V2 scoping (professional UI redesign) is now tracked separately in #582, decided 2026-07-31. This issue (#68) remains the V1 roadmap; V1 is nearly complete per that discussion. See #582 for what carries into V2, what's deferred, and what's out of scope.
docs(Altinity#457): CHANGELOG, ADR-0003 addendum, README and issue re…
feat(Altinity#468): restore the searchable multi-select for Array(sca…