feat(#426): Databases/Dashboards switcher and a read-only Dashboard hierarchy tree - #444
Merged
Conversation
…er + pending delivery `MainSurfaceState`'s dashboard branch carried ONE `focus` field doing two jobs. #426's tree needs them apart: - `currentMember` — which member the user navigated to inside this Dashboard. Retained until another member/Dashboard/query is opened, because the tree paints its current-resource styling from it. - `pendingFocus` — a DOM focus delivery still owed to the surface. Consumed exactly once, so a later repaint cannot re-focus a node the user has since navigated away from. They now move independently: `withoutFocus` becomes `withoutPendingFocus` and deliberately leaves `currentMember` alone, `reconcileMainSurface` drops either field when committed truth no longer contains that member (resolved against its OWN collection, so a colliding tile/filter id cannot cross-resolve), and `adoptRouteMainSurface`'s View/Edit switch preserves the current member while owing no new delivery. `withCurrentMember` selects a member without owing one — the in-place path lands next. `DashboardRenderTarget.focus` is deliberately unrenamed: it genuinely is a one-shot render request. No behaviour change beyond the retention itself, which app.test.ts's "delivers a focus target once" now asserts. Sabotage-checked: clearing `currentMember` in `withoutPendingFocus`, and resolving a member against both collections, each fail the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…ace command port The tree makes repeated same-Dashboard member navigation a NORMAL operation. #425 delivered focus by re-rendering, which rebuilt the viewer session, re-ran the Dashboard, and wrote another history entry every time — fine for a direct API call, unacceptable for a click. Rather than inventing a second port, this extends the one that already exists (`app.surfaceCommands`, generation-stamped, installed by the Dashboard render and cleared by Dashboard cleanup AND every surface transition) with: focusMember(member): 'ok' | 'pending' | 'missing' `pending` is deliberately not a failure — it means "not deliverable in place right now", and the caller falls back to the normal render transition, which owns the deferred delivery. Two things produce it: a superseded port, and a curated filter requested before the opening wave settles (the first publish replaces the whole filter bar, so a node focused earlier would be detached moments later). A TILE has no such problem, so the port is installed SYNCHRONOUSLY, before the wave's await — a click that lands mid-load still focuses its panel. `applyNavigationFocus` is factored into one `deliverFocus(target, opts)` body, so the tabindex/scroll/focus/highlight sequence cannot drift into two copies. `respectUserInteraction` is the only difference: the render-time one-shot must yield to a user who got there first, but an in-place request IS the user's click — sharing that guard would make every tree click after the first silently do nothing. There is a regression test named for exactly that. `openDashboard` uses it as the same-id/same-mode fast path, and `state` gains the tree's explicit repaint invalidation (`dashboardTreeRevision`, bumped through the single `app.invalidateDashboardTree` writer) plus the session-only `upperRole`. Opening a Dashboard ROW clears the current member and still invalidates, so the tree cannot keep marking a stale row. Also fixes two tests the change would otherwise have made vacuous rather than failing: "re-opening the same id and mode" is split into the in-place, pending and missing paths, and "delivers a focus target once" now opens a DIFFERENT Dashboard so it genuinely exercises the render path it exists to pin. Sabotage-checked: removing the mid-wave filter gate, and sharing the user-interaction guard with the in-place path, each fail the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…biter Three pure modules, no wiring yet — all at the per-file gate. `core/tree-click-arbiter.ts` is the one behaviour #426 explicitly forbids copying from the schema tree. That tree DETECTS a double click post hoc: the single action runs immediately and the double runs in addition, which is harmless when the first action is expansion. A panel row's single action opens a query, so detection would flash the Query surface and write a history entry on the way to a double-click. This arbitrates instead — the single is scheduled for the double-click window and cancelled outright by a second press. Timers are injected, so it is pure. It also becomes the shared home of `DBLCLICK_MS`, which `ui/schema.ts` now imports rather than keeping a second copy of the same 300ms. Subtlety worth naming: the window opens even for a row with NO single action. A source-less transitional filter has query-open disabled but still answers a double-click, so the second press has to be recognisable as a repeat. `application/dashboard-tree-ui-state.ts` holds expansion/search/scroll/keyboard state per workspace id, copy-on-write. Deliberately NOT a signal, following `state.libraryFilter`'s precedent: a repaint effect over this would lose the caret on every keystroke and repaint on every scroll frame. `preSearchScrollTop` is separate from `scrollTop` because one field cannot both track the filtered list and remember where to return to when the search clears. `clampKeyboardRow` keeps the roving tabindex on a row that is actually rendered — a row can leave the visible set through a collapsed ancestor or a search, which pruning cannot see. `application/dashboard-tree-model.ts` derives the flat visible-row list. Each row carries level/parent/expansion/match/invalid AND its own action set, so the view gets one generic dispatch path instead of a per-kind gesture switch — that is what keeps the view inside the coverage gate. It lives in src/application/ because it reads both the workspace aggregate and MainSurfaceState. It holds the transitional data rules exactly: an ABSENT filter `sourceQueryId` is transitional (row stays, query-open unavailable, NO diagnostic), while a PRESENT but unresolved one is a real broken reference. Conflating them is sabotage-checked, as is Filters-always-before-Panels. Input shapes are deliberately looser than the generated types so every never-throws guard has a malformed fixture reaching it rather than being unreachable padding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…archy tree The upper sidebar pane gains an equal-role tab row over TWO PERSISTENT hosts, exactly one exposed. Switching roles only flips `hidden`, so schema search text and input focus, schema expansion, lazily-loaded columns, and schema scroll are preserved BY CONSTRUCTION rather than by save/restore logic — and the upper-pane height, splitter and sidebar width are untouched, because nothing replaces the `.side-pane` they belong to. The tab row reuses the lower switcher's .side-tabs/.side-tab/.side-count vocabulary verbatim (#426 asks for it; DESIGN.md requires one tab language app-wide). `ui/dashboard-tree.ts` renders the model's flat row list 1:1: role=tree with role=treeitem rows, aria-level, aria-expanded on expandables only, a roving tabindex, Up/Down/Left/Right/Home/End/Enter/Shift+Enter, and a keyboard-reachable trailing action menu carrying every operation otherwise reachable only by double-click. Gestures run through the pure arbiter, so a panel click opens its query only after the double-click window and a double-click cancels it outright — no Query-surface flash, no duplicate history entry. Group rows deliberately bypass arbitration (no competing gesture, so waiting would only feel slow), and the chevron is the instant expansion path. Two details worth naming. The tree repaints only on the explicit `dashboardTreeRevision` signal; expansion, search and scroll repaint the row list directly, which is why typing keeps the caret and scrolling costs nothing. And focus is restored after a repaint ONLY when the tree already had it, so a background workspace commit cannot steal focus from the editor. Steps 4 and 5 of the plan landed together on purpose: splitting them would have shipped a placeholder search box wired to a tree that did not exist yet, then deleted it. Both new UI modules are added to typography-contract.test.js's curated source list — that gate is hand-curated, and its own comment warns that carving markup out of a listed file into an unlisted one silently drops it from the gate. Also removed two unreachable statements the coverage gate correctly rejected: `focusRow`'s null bailout, and an empty-rows guard in the key handler that can never run (the handler is not installed for an empty tree). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…seam, and relabel the mobile segment The header's `SQL Browser | Dashboard` pair is gone; the brand zone is now non-interactive `Altinity® SQL Browser`. That control could only ever reach ONE Dashboard, which the upper-left tree now supersedes. `routeButton` survives — the Dashboard toolbar's View/Edit control is its remaining consumer, and `surface-label` is on the typography ALLOWED list because of it. `app.dom.dashboardNav` / `renderDashboardNav` are deleted outright: the element was BUILT but never appended to any parent (`libraryControls` returned only `[fileBtn, workspaceTitle]`), and the function only ever set `hidden = false`. Its three call sites were keeping a no-op in sync. Deleting it orphaned `actions.showDashboard`, whose only production caller was that detached button's onclick, so that goes too. Dead CSS (`.hd-dash-nav`, `.app-surface-switch`) with it. Each of those three call sites marked a moment the Dashboard COLLECTION may have changed, so rather than only deleting them, the tree's invalidation now fires from `applyCommittedWorkspace` — the one place every projection funnels through (boot, a committed mutation, an external refresh, a workspace switch). Declared above its first caller so no path can reach it mid-wiring. Owner decision at ship time: the mobile segmented control's upper label becomes **Explore** rather than staying "Schema". That pane now hosts both the Databases and Dashboards roles, so "Schema" named only half of what it shows. The internal `data-seg`/`data-mobile-tab` values and the `.schema-pane` selectors they key are deliberately untouched — a label change, not a mobile-CSS restructuring (touch behaviour stays out of scope per #426). Dead tests are DELETED, not re-fixtured — three `dashboardNav` cases covered a seam that no longer exists. Where a removed control carried a real regression guard, the guard moved rather than vanishing: app.test.ts's "resolves the workspace key at click time, not header-mount time" check now drives `showDashboardSurface`, which is where that resolution actually lives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…ard tree
A new raw fixture mounts the REAL `mountAppShell`, so the two upper-role hosts, the
role tab row and the tree are laid out by the shipped CSS — happy-dom cannot observe
CSS layout at all, which is where every geometric claim in this change actually gets
checked:
- a hidden role host contributes NO layout (the exposed one fills the pane
exactly, minus the tab row) — so a Dashboard genuinely owns the pane rather
than sharing it with an invisible sibling;
- a very long Dashboard title ellipsizes instead of widening the sidebar or
overflowing the page;
- row indentation resolves to 10/24/38px per level;
- current-resource styling and keyboard focus are two DISTINCT signals visible on
the same row simultaneously (tonal surface + ring), which is the requirement a
class-name assertion cannot prove;
- the tree list scrolls inside its own pane;
- the sidebar width and the upper/lower splitter survive a role switch.
The gesture specs are here for a different reason: real timers and real event
ordering. A panel click opens its query only after the double-click window; a
double-click cancels that outright with no Query-surface flash and exactly one
navigation.
Two fixture traps worth recording. `createApp` pulls in the CodeMirror adapters, so
the harness needs the full CM6 import map or its module silently never runs (the
raw harnesses load /src as unbundled ESM). And the fixture must mount into `#root`,
not an arbitrary element — styles.css gives `#root` the full-height flex column, and
without it `.main-row` is content-sized, which made the pane height appear to change
across a role switch. That was the harness, not the product.
The mobile header spec drops its Application-surface assertion (that group is gone)
and now asserts the brand zone has no buttons at all.
Verified: 232 e2e tests pass on chromium + webkit, zero failures. Firefox cannot
launch a context in this sandbox (unshare CLONE_NEWPID EPERM), so CI remains its
signal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
Removing the header's `SQL Browser | Dashboard` pair opened a hole that neither issue could see on its own. #437 removed the Dashboard toolbar's Back-to-query control specifically BECAUSE the header pair covered it; #426 then removed the header pair. Together they left `g w` and "click a saved query in the lower list" as the only ways out of a Dashboard — and neither is reachable on a phone, where the mobile rules drop the sidebar AND the bottom nav for a full-bleed Dashboard. #426's own Header-cleanup section requires this control to be retained, so it comes back — but per #437's design, as the FIRST control inside the one compact toolbar rather than as a second row, icon-first so the label drops at phone widths through the existing `.dash-back-label` rule. Both the rendered Dashboard and the no-Dashboard placeholder carry it, so neither state is a dead end. The tooltip names the `G then W` shortcut, so the keyboard path stays discoverable instead of being the only way to find the action. Carries a regression guard named for the interaction, since this is the kind of gap that only appears when two separately-correct changes meet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…ns, unwired pruning Two independent read-only review passes over the branch found seven real defects. Each was verified against the code before fixing. CONFIRMED BUGS 1. In-place focus on a filtered-out panel was a silently dead click. `tileEls` is a write-only cache (no delete/clear anywhere), and both layout reconcilers rebuild the grid from the SEARCH-FILTERED tile set — so a panel excluded by the Dashboard's own tile search leaves a DETACHED card behind. `deliverFocus` never checked, so tabindex/scrollIntoView/focus/highlight all no-opped on an out-of-document node and `focusMember` still reported `ok`: the tree marked the row current while nothing moved and no diagnostic appeared. Now reports `pending`, so the caller's render transition rebuilds the surface (which resets that per-session search) and delivers the focus for real. 2. A group row's chevron was inert during a search, and clicking it wrote hidden expansion. `groupExpanded` ignored the user's expansion entirely while searching, so a group with no matches could not be opened and one with matches could not be closed — and the click surfaced later, after the search cleared. Now the forcing term is CONDITIONAL, mirroring `ui/schema.ts`'s second level (`tableCascadeForced`), which avoids this exact case deliberately. 3. A View/Edit switch discarded the current member, contradicting the spec's "switching View/Edit through Dashboard chrome preserves the current member". The mode differs, so `isSameDashboardSelection` is false and the request took the render path, where `resolveOpenDashboard` builds the surface from the request alone. New pure `carryCurrentMember` carries it across a same-Dashboard mode change only, so opening a Dashboard row still clears it. 4. `pruneTreeUi` had NO production caller — dead code standing in for the spec's "prune removed resource IDs". Wired into `applyCommittedWorkspace`, so a deleted Dashboard's expansion cannot linger for the session or make a RECREATED id render pre-expanded. Survivors are preserved. 5. Pending click timers were cancelled on role change and search only, not on the workspace change its own docstring named, nor on disposal. A deferred "open this query" from workspace A could fire 300ms after switching to B — and `openSavedQuery` with an unknown id still switches surfaces, so the user got yanked off the Dashboard with nothing opened. Now cancelled on a genuine workspace switch and in the shell disposer. SPEC COMPLIANCE 6. The action-menu button used `Icon.chevDown()` — the same glyph as the disclosure chevron, which #426 explicitly forbids ("not become confused with the row's disclosure chevron"); an expanded row carried two identical chevrons at opposite ends. New `Icon.more()` (vertical ellipsis). 7. Menu-button and group-row clicks called `arbiter.cancel()` unconditionally, killing a pending single on an UNRELATED row, against "cancel no unrelated row operation". New `cancelFor(key)` is scoped to the row. Also: the roving tabindex now moves in the DOM on click instead of waiting ~300ms for the deferred single's repaint (the visible ring and the arrow-key origin disagreed in that window), and the empty-collection Dashboard entry point — the one surface transition that bypasses `applyMainSurface` — now invalidates the tree too. Every fix carries a test named for the defect, including the three the reviews noted were missing outright: mode-switch member preservation, end-to-end pruning, and workspace-change cancellation. Plus the spec assertions nothing covered: that rendering or searching the tree executes no query and creates no viewer session, and that it never mutates the aggregate. Note on tooling: the Codex second opinion could not be obtained. Its sandbox helper cannot start here (`bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`), so every git/file read inside it failed; an earlier attempt without `--base` reviewed an unrelated changeset it found through GitHub search. Reported rather than counted as a clean pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…ion move Adds the #426 CHANGELOG entry, and CORRECTS the #437 one, which had become false: it stated that "the application header's existing SQL Browser / Dashboard switch is the navigation path back to Query" — this branch deleted that switch. The shipped changelog was documenting removed behaviour. ADR-0003 gains a #426 addendum, because the change amends the surface LIFECYCLE the #425 addendum recorded, not just the UI: surface navigation is no longer header-owned, repeated member navigation no longer restarts the viewer session (the generation-stamped SurfaceCommandPort gains focusMember, with `pending` meaning "not deliverable in place" rather than failure), and MainSurfaceState now separates retained current-member styling from consumed-once focus delivery. Roadmap #68's Phase 10 line is checked off with the shipped scope, including the two owner decisions (mobile segment relabelled Explore; Back to query restored). Filed #443 (inbox): `openSavedQuery` switches surfaces before resolving the id and stays silent on a miss. Pre-existing, out of scope here — it was the tail of the now-fixed deferred-click-after-workspace-switch path, and every current caller resolves the id first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
…-disabled menu items Three of Codex's four findings verified as real and are fixed. 1. Row keys could COLLIDE. Dashboard/tile/filter ids are schema-constrained only to `\S`, so a colon is legal — and an imported bundle preserves whatever ids it carried. Unescaped, Dashboard `a:tile:b` produced the same key as tile `b` of Dashboard `a`: two rows claiming `tabindex="0"`, `focusRow` targeting the wrong one, and the click arbiter reading clicks on two distinct resources as a double-click. Key components are now escaped (`%` first, so the escape cannot be forged), which also makes `pruneTreeUi`'s group-key split unambiguous instead of relying on a lastIndexOf heuristic. `encodeKeyPart` lives in the ui-state module so the dependency between the two pure modules stays one-way. 2. The search box did not follow a workspace switch. It is built once and outside the repainted list (that is what keeps the caret), so on a switch the tree filtered by the NEW workspace's search text while the box still displayed the old one — and the reverse coming back: a filtered tree above a blank box. Now synced during the render, guarded on inequality so an ordinary repaint never touches the caret (asserted by a test that counts writes to `value`). 3. "Disabled" menu items were only visually disabled. `openMenu` makes every item a focusable `role="menuitem"` button; `.is-disabled` blocked the pointer through CSS, but assistive technology still announced an enabled action and keyboard activation silently did nothing. `MenuRow` gains an optional `disabled`, which sets `disabled` + `aria-disabled` and keeps the row out of the roving-focus order. REJECTED: "pure logic must live in src/core/ per hard rule 2". `src/core/` may not import `src/workspace/` (build/check-boundaries.mjs), and the tree model reads both the workspace aggregate and `MainSurfaceState`. `src/application/main-surface.ts` established and documented exactly this placement in #425 for the same reason; the model would have to move with it, and it cannot. Codex ran via the stdin-diff invocation the ship skill documents — my earlier attempts used `codex exec review --base`, which errors on a custom prompt and, given `--base` alone, reviewed an unrelated changeset it found through GitHub search. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTaxTCGhPECD6PJaDeZhP5
7 tasks
BorisTyshkevich
added a commit
that referenced
this pull request
Jul 25, 2026
…le placement (#445) * fix(#426): search-forced rows, phantom current member, pure-module placement Follow-up review findings on the Dashboard tree that landed after PR #444 was pushed: - A row a search is HOLDING open now carries `toggleable: false` and no `single` command, so its chevron gets no handler and click / ArrowLeft / ArrowRight cannot write expansion state. #426 requires a search to expose paths without mutating saved expansion and to restore the pre-search state when cleared; a toggle there wrote state that was invisible until the search cleared. - `resolveOpenDashboard` no longer marks `currentMember` for a tile or filter the resolved Dashboard does not contain, while still passing `pendingFocus` through unchecked so the delivery path keeps reporting the miss. The existence check is extracted as `presentMember`, resolved against the member's own collection, and shared with post-commit reconciliation. - `dashboard-tree-ui-state.ts` moves to `src/core/` per CLAUDE.md hard rule 2: it is a pure leaf with no imports at all and resolves nothing against the workspace aggregate. - Deferred tree clicks are cancelled on every projection, not only a workspace switch: deleting a Dashboard inside the 300 ms window could otherwise let the delayed toggle re-add a pruned id, or open a dead query id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zB6jdGRgMmPNxXvxAoSLx * docs(#426): note the Dashboard-tree follow-up fixes in CHANGELOG Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zB6jdGRgMmPNxXvxAoSLx --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #426.
The upper-left pane gains an equal-role
Databases | Dashboardstab row over twopersistent hosts, and every stored Dashboard becomes reachable as a read-only
Dashboard → Filters → Panelstree. Until now #424's collection was fully persistedand validated but only its first entry was reachable from the UI.
Switching roles only hides a host, so the schema search text and input focus,
expansion, lazily-loaded columns and scroll are preserved by construction rather
than by save/restore logic — as are the sidebar width and the upper/lower splitter,
which belong to the
.side-panenothing here replaces.The tree
Collection order;
Filtersalways beforePanels; both group rows stay visiblewhile a Dashboard is expanded, including when empty (#428 will use them as stable
drop targets). Panel counts exclude filters. Search covers all six documented fields
and exposes matching paths at presentation time, so clearing it restores exactly
the expansion and scroll position the user had. Full ARIA tree:
role="tree"/role="treeitem",aria-level,aria-expandedon expandables only, a rovingtabindex, Up/Down/Left/Right/Home/End/Enter/Shift+Enter, and a keyboard-reachable
trailing action menu carrying every operation otherwise reachable only by
double-click.
Gestures are mutually exclusive, which is the one thing #426 explicitly forbids
copying from the schema tree. That tree detects a double-click after the fact — its
single action runs immediately and the double runs in addition — which is harmless
when the first action is expansion, but a panel row's single action opens a query, so
detection would flash the Query surface and write a history entry on the way to a
double-click. New pure
core/tree-click-arbiter.ts(injected timers) schedules thesingle for the double-click window and cancels it outright on a second press. The
disclosure chevron stays the instant path for expansion.
Strictly read-only: it never clones, repairs, rejects or rewrites a reference. A
broken reference renders a diagnostic row with Dashboard navigation still available,
while a curated filter that simply has no option source yet is treated as the
transitional state it is before #427 — not an error.
#425 integration
Repeated member navigation inside an already-open Dashboard is now delivered in
place, through the
SurfaceCommandPortthat already existed (generation-stamped,installed by the Dashboard render, cleared on every surface transition) rather than a
second port:
pendingis deliberately not a failure — it means "not deliverable in place rightnow", and the caller falls back to the normal render transition.
MainSurfaceStatesplits #425's single
focusinto retainedcurrentMember(what the tree marks) andconsumed-once
pendingFocus(a DOM delivery owed), so consuming a delivery no longererases the highlight.
Header cleanup
The obsolete
SQL Browser | Dashboardpair is gone (brand zone is nownon-interactive
Altinity® SQL Browser), along withdashboardNav— which I foundwas built but never appended to any parent, with a
renderDashboardNav()thatonly ever set
hidden = false, kept in sync from three call sites. Removing itorphaned
actions.showDashboard, so that went too, with its dead CSS.Back to query returns to the Dashboard toolbar. #437 removed it because the
header pair covered it; this PR removes the pair. Together they would have left
g wand "click a saved query" as the only routes back — and on a phone the mobile rules
drop the sidebar and the bottom nav for a full-bleed Dashboard, so there would have
been none. It comes back icon-first inside #437's single compact row.
Owner decisions at ship time
now holds both roles. Internal
data-seg/.schema-paneselectors unchanged.workspace
id;upperRolesession-only so "default to Databases for a freshsession" survives a reload; a matched Dashboard shows its full hierarchy; group rows
toggle immediately; the Databases count is omitted while the schema loads; mid-wave
tile focus lands immediately while a filter defers;
DashboardRenderTarget.focusstays unrenamed).
Review
Two independent read-only review passes found seven real defects, each verified
against the code and fixed in
7e4c67fwith a test named for it:silently dead click —
tileElsis a write-only cache and the layout reconcilersrebuild from the filtered set, so the card was detached; every DOM op no-opped
and
focusMemberstill returnedok.surfaced only after clearing it.
pruneTreeUihad no production caller — dead code standing in for arequirement.
The Codex pass then found four more; three verified and are fixed in
816d3b7:to
\S, so a colon is legal and an imported bundle preserves whatever ids itcarried — Dashboard
a:tile:bproduced the same key as tilebof Dashboarda.Two rows claiming
tabindex="0", focus landing on the wrong one, and the arbiterreading two distinct resources as a double-click. Components are now escaped.
workspace's text while the box still showed the old one.
announced an enabled action and keyboard activation silently did nothing.
MenuRowgains a semanticdisabled.Its fourth finding — "pure logic must live in
src/core/per hard rule 2" — isrejected:
src/core/may not importsrc/workspace/(check-boundaries.mjs),and the tree model reads both the workspace aggregate and
MainSurfaceState.src/application/main-surface.tsestablished and documented exactly this placementin #425 for the same reason.
Verification
npm test— 172 files / 5422 tests, per-file coverage gate met; all six newmodules at 100% statements/lines.
npm run build— clean; no new runtime dependency.npx playwright test— 232 passed on chromium + webkit, zero failures. Firefoxcannot launch a context in this sandbox (
unshare CLONE_NEWPID EPERM), so CIremains its signal.
mountAppShell, because happy-dom cannotobserve CSS layout: it verifies that a hidden role host contributes no layout, that
a long Dashboard title ellipsizes rather than widening the sidebar, level
indentation, that current-resource styling and keyboard focus are two distinct
simultaneous signals, that the tree list scrolls in its own pane, and that the
sidebar width and splitter survive a role switch.
Filters-before-Panels, the mid-wave filter gate, the shared user-interaction guard,
and
withoutPendingFocusretainingcurrentMember) — each fails the suite whenbroken.
Non-goals held
No drag, pencil, trash, create, delete, duplicate, reorder or ownership mutation; no
lower-pane rename to Library (#427 owns that atomically); no favourite decoupling; no
persisted tree state; no URL change.
Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core//src/application/, DOM insrc/ui/; the tree model lives insrc/application/becausebuild/check-boundaries.mjsforbidssrc/dashboard/**from importing itCHANGELOG.md([Unreleased]) updated — and the Compact the Dashboard toolbar and remove the redundant surface row #437 entry corrected, since it documented the header switch this PR removesinboxfor an out-of-scope footgun