Dashboard v1 phases 1–4 (#280): first-class module — contracts, persistence, authoring, viewer + flow@1#293
Merged
Conversation
…rce limits Phase 1 of #280 (Dashboard as a first-class module): pure contracts and codecs only — no persistence, UI, or import planner. Schemas (through the existing manifest pipeline, generated types + Ajv validators committed): - stored-workspace-v1, dashboard-v1 (tile/filter/layout defs + DashboardLayoutFallbackV1 = flow@1), dashboard-layout-flow-v1 (normative flow@1, closed FlowTilePlacementV1), portable-bundle-v1 (queries + dashboards, both required even when empty). - query-spec-v1 gains QueryDashboardPresentationV1 + QueryPresentationPatchV1 as an in-place additive revision (pinned decision 1); Spec-editor autocomplete picks the new fields up. Pure src/ modules (100% statements/functions/lines per file): - dashboard/model: canonical-json (one deterministic encoder for export, persistence, hashing, equality), workspace-diagnostics (deterministic numeric-aware sorter), portable-limits (PORTABLE_LIMITS verbatim; 10 MiB cap kept for the IndexedDB-backed phase-2 repo — pinned decision 2), json-limits (UTF-8 byte + depth guards before parsing), bundle-order (dependency-closure + multi-dashboard ordering), workspace-semantics (whole-workspace cross-resource validation), portable-bundle-codec. - workspace/stored-workspace: aggregate codec + validation. Boundary rules added for src/dashboard + src/workspace (model <- application <- UI direction). check:arch, check:schemas, tsc, build all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing, resource limits
… phase 2 Add the WorkspaceRepository (loadCurrent/commit/clearCurrent) that validates the complete candidate through phase-1 whole-workspace validation before an atomic, single-transaction replacement — failed writes leave the previous workspace intact, never bump revision, never mix aggregates (multi-tab last-commit-wins). Persistence is IndexedDB behind an injected WorkspaceStore seam (env.indexedDB, mirroring crypto/storage) so the pure repository unit-tests against an in-memory fake. Add the one-shot legacy migration: build one StoredWorkspaceV1 from the flat asb:* keys (initial Dashboard from panel-role favorites, asb:dashLayout/asb:dashCols -> flow@1), validate the whole candidate, persist atomically, keyed on aggregate record existence, legacy keys untouched. Plus pure workspace-operation semantics (rename/create-new/import-queries/ replace/ID generation). Wire app.workspace in createApp; the favorites-driven render still reads legacy keys this phase (reads move to the aggregate in phases 3-6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
openDb() cached dbPromise on the first call including on rejection, so one transient open failure (or a no-factory "IndexedDB is unavailable") poisoned the store for the whole page lifetime — every later read/write/clear rejected with the stale error and a same-session retry could never succeed, contradicting the repository's "failed write leaves a draft to retry" contract. Cache only a successful open; drop the cached promise on rejection so the next call reopens. Add an onblocked handler that rejects the open (dormant at version 1, but an unhandled blocked event would hang the open and wedge the cache the same way). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce + legacy migration
…n resolution, workspace-wide validation Phase 3 of #280. Adds the fallible, atomic Dashboard authoring domain on top of the phase-1 contracts/codecs and phase-2 persistence: - RFC 7396 JSON Merge Patch (in-house, no dependency) and the ONE canonical presentation resolver (base -> variant -> override -> final validation), shared by the authoring session, saved-query mutation validation, and tests. - A minimal flow@1 layout plugin (placement validation, orphan pruning, size-hint-derived placement, active-plugin resolution with fallback). - DashboardAuthoringSession + typed DashboardCommands with the atomic algorithm (clone -> apply -> normalize -> validate structure/refs/roles/presentations -> replace only when valid); draftVersion stale protection separate from the persisted revision (increments once per successful commit). - Star rewires to toggleMembership (add-query/remove-tile command) with the spec.favorite dual-write; no direct membership mutation anywhere. - planSavedQueryMutation enforces whole-workspace validity for saved-query mutations, rejecting invalidating ones unless an atomic repair is supplied. - New check:arch rule for src/dashboard/application; CHANGELOG entry. Seven new modules, each 100%-covered; full gate (tsc, arch, schemas, vitest) and build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… presentation resolution, workspace-wide validation)
…, close validation seams Addresses the #285 review follow-ups (new commit on top of 662dd5a): 1. update-tile presentation footgun (fix): `presentation` now merges ONE level — `variant` and `override` are independent sub-fields, each replaced wholesale (null deletes that sub-field), so a caller can set/clear one without wiping the other. `override` values are still stored verbatim (no deep merge into their meaningful nested nulls). Top-level tile-field behavior unchanged. 2. Session-level tests for the delegated-validation failure class (the validateStoredWorkspaceDocument stage between applyCommand and presentation resolution): add-query incompatible role, tile-limit exceeded, and add-query-instance with an undeclared variant — each asserting structured diagnostics and a byte-for-byte-unchanged draft. 3. saved-query-mutation: tests that APPLY the filter-source repairs and prove the candidate validates — remove-affected-filters on a filter-source role change, and remap of a query that is a filter's own sourceQueryId. 4. Cover the reachable defensive branches (empty/absent presentation on a variant switch; a dashboard without a tiles array). The favorite dual-write's non-object-spec guard is unreachable by contract (a query with a non-object spec never survives command validation to reach the dual-write) and is left as-is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the read-only Dashboard runtime and the normative flow@1 layout math as pure/application modules, all unit-tested at the per-file gate and free of any Workbench/App/AppState/editor/service/net dependency: - dashboard/application/dashboard-viewer-session.ts: DashboardViewerSession reading membership from dashboard.tiles[] (not spec.favorite), resolving presentations through the shared presentation-resolver, owning filter/tile runtime state behind one state signal, with bounded concurrency, per-tile cancellation, stale-wave guards, and destroy() teardown. The #235 planner runs filter-source-unaffected panels in parallel with the filter wave. Filter usability (#188): per-filter clear (keeps last value), coalesced clear-all (one wave), activeFilterCount, and never-hidden blocking reasons. - dashboard/layouts/flow-layout.ts: presetColumns, effectiveSpan, resolvePlacement, and computeFlowLayout (deterministic row-major packing, KPI-band grouping, mobile one-column normalization without mutating persistence, semantic = DOM = keyboard = visual = print order). - dashboard/layouts/layout-registry.ts: compile-time-registered, lazy-loadable layouts with the flow@1 fallback contract (fail-closed without a valid one). - build/check-boundaries.mjs: explicit phase-4 boundary for dashboard/application, plus a dashboard-boundaries unit test. Accessible authoring uses the phase-3 move-tile/update-placement commands; pointer drag is an equivalent alternative. Live DOM integration (viewer-driven render replacing the favorites path) is the remaining wiring step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…a11y/KPI DOM Wire the DashboardViewerSession, flow@1 model, layout registry, and shared presentation resolver into the running app, flipping Dashboard membership READS from spec.favorite to dashboard.tiles[]. - src/ui/dashboard.ts: rewritten to build a DashboardViewerSession from the persisted StoredWorkspaceV1 (app.loadDashboardWorkspace → repository → one-shot legacy migration) and render/reconcile from its state signal — flow@1 rows/columns, KPI bands (renderKpiCards, #240), per-tile loading/unfilled/error/ready with chart-paint-once, mobile one-column normalization, empty state. Migrated dashboards' implicit {name:Type} params are surfaced as runtime-only filter definitions. - Accessible reorder + sizing (keyboard Move-earlier/later, span/height) drive the phase-3 move-tile/update-placement commands, mirrored into the viewer via a new syncDocument (no re-execution) and best-effort persisted; focus stays on the moved tile with an ARIA live announcement. Pointer drag is an equivalent alternative. - src/ui/filter-bar.ts: shared per-filter clear / clear-all / active-count / blocking-badge affordances, driven by the viewer's filter semantics. - src/ui/app.ts + app.types.ts: app.loadDashboardWorkspace() (migrate-if-needed then loadCurrent), wired at the composition root where the IndexedDB store lives; fake-app gains the seam + a workspace override. - Delete the superseded #276 phase-3b runtime (ui/dashboard/dashboard-session.ts) and the #240 ui/dashboard-kpi-band.ts — absorbed by the viewer + renderKpiCards. Closes #235 and the reorder half of #153; dissolves #188. npm test (3358), build, and check:arch all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nly reorder Product-owner final scope for #286 (we stop here; #287/#288 deferred). - RESTORE rich filter fields (regression fix): the Dashboard filter bar is now the shared buildFilterBar — relative-time presets, recents, enum + curated comboboxes — driven over the viewer. Bridge added to DashboardViewerSession: `controls`, `getFilterField` (draft-aware #170 validation), `applyFilter` (commit with explicit activation), and a parameterized `prepareBatch`. A shim FilterBarApp feeds a draft value/active bag + recents from the real app; the viewer still imports no AppState/app.state (check:arch forbidden list intact). - REMOVE the per-filter "×" clear button (and filterClearButton). - REMOVE the in-tile span (1×/2×/3×) and height controls; span/height are tuned in the Spec editor. The update-placement/setTileSpan/setTileHeight authoring commands stay in the application layer. - Tile reordering is pointer DRAG ONLY: keyboard Move-earlier/later buttons removed; a drop persists dashboard.tiles[] via move-tile. Deliberate owner override of #280's a11y "not the only mechanism" note (recorded in CHANGELOG). - KEEP: "N active" count, coalesced clear-all, never-hide-blocking badge, KPI bands. npm test (3360) + build + check:arch green; per-file coverage at the gate (dashboard.ts / filter-bar.ts / dashboard-viewer-session.ts). No e2e fixture changes (no App-shape change; removed chrome unreferenced there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…order; remove per-filter-x + in-tile size/move chrome
The .dash-grid container kept the pre-#286 grid-template-columns from the model where .dash-tile were direct children. The flow@1 rewrite wraps tiles in .dash-row grids, so the container was laying the ROWS out in columns — full-width rendered rows side by side, 3-column multiplied them across the page. Make the container a vertical flex stack; each .dash-row owns its column count. happy-dom does not compute CSS layout, so the unit suite could not catch this; verified in real Chromium (3 rows stacked, 3 tiles across each). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… decision) The "X: Required filter value is not set." badges under the filter bar were noise. Remove the badge rendering, the filterBlockingBadge helper, and the viewer's now-unused per-filter `blocking` computation (a prepareBatch per wave that fed nothing). An unfilled required filter simply leaves its target tiles in their normal unfilled state. Genuine dashboard-config diagnostics still render. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BorisTyshkevich
added a commit
that referenced
this pull request
Jul 18, 2026
…ll (#297) * fix(#294): dashboard filter strips never wrap, remove visible Clear-all Reverses the visible Clear-all UX decision from #286/#293: the toolbar and filter field region now stay flex-wrap:nowrap at every viewport width (not just the ≤768px mobile breakpoint), with the field region scrolling horizontally in a dedicated `.filter-strip-scroll` viewport and the fixed "N active" count as a non-scrolling sibling. The visible Clear-all button is deleted; DashboardViewerSession.clearAllFilters() stays a tested application-level operation with no UI trigger. Also, per multi-angle review: factors the shared scroll-viewport CSS contract between the Workbench var-strip and the new Dashboard filter strip (previously copy-pasted), fixes a focus-ring clipping regression on the new scroll viewport, and keeps the "N active" count vertically centered against a filter row taller than one line. New Playwright coverage for both surfaces (dashboard-mobile.spec.js, workbench-var-strip.spec.js) since CSS wrap/overflow is invisible to the happy-dom unit suite. Closes #294 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz * fix(#294): remove N-active count, move layout switcher to header, bold required filters Same-PR owner follow-up on the just-shipped filter nowrap/Clear-all-removal change: - The "N active" count (`filterActiveCount`, `.dash-filter-count`/ `.dash-filter-count-host`) is deleted outright, same as Clear-all — DashboardViewerSession.activeFilterCount stays tested app-level state with no display. `.dash-filter-host` collapses back to a single-level scroll viewport (no more `.filter-strip-scroll` wrapper) now that there's no count sibling to keep outside it. The toolbar is hidden whenever there are no filters at every width (was mobile-only), since it now holds nothing else. - The flow preset switcher moves off the filter toolbar and into the header's top row (right after the tile-count chip), as a compact `<select class="result-panel-select">` matching the Workbench's panel-type-picker convention, replacing the four-button `.dash-seg` segmented control. - Required vs optional `{name:Type}` filter names (shared by the Workbench var-strip and the Dashboard filter bar) are now bold instead of a leading "*" glyph. Also, from a CSS-orphan audit prompted by this change: removes `.dash-skip` (dead since #286 flipped Dashboard membership onto `dashboard.tiles[]` — no "N not shown" concept remains) from styles.css and the e2e fixture. A second dead class, `.dash-grid.is-wide`, turned out to guard a real, still-intended "Full width" preset behavior #286's flow@1 rewrite never re-wired — filed as #298 rather than fixed here (distinct rendering bug, needs its own >1560px-viewport regression test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.
Summary
Phase 8 of the roadmap (#68) — the first four sub-issues of #280, making Dashboard a first-class, independently-constructible module instead of deriving it from favorited saved queries and global
AppState.Delivered as four sequential, individually-reviewed waves on one integration branch (per-issue small review + fixes, whole-branch review at the end). Phases 5–6 (#287 portable import/export, #288 direct/full-screen viewing) are deferred to a follow-up by owner decision and are not in this PR.
What shipped
stored-workspace-v1/dashboard-v1/portable-bundle-v1/flow@1/ fallback JSON Schemas + generated types/validators; one canonical deterministic encoder; numeric-aware diagnostic sorter;PORTABLE_LIMITSenforcement (schema + codec byte/depth + runtime re-checks); whole-workspace cross-resource semantic validation.query-spec-v1gainsQueryDashboardPresentationV1as an additive revision.WorkspaceRepositoryover an IndexedDB aggregate (single record, atomic replacement, injectedWorkspaceStoreseam); one-shot legacy migration (favorites → tiles,asb:dashLayout/asb:dashCols→flow@1) keyed on aggregate record existence, legacy keys untouched until the aggregate write succeeds; multi-tab last-successful-commit-wins; documentedspec.favoritedual-write removal path.DashboardAuthoringSession+ typed fallible atomic commands (clone → apply → normalize → validate → replace-only-if-valid; draft byte-for-byte unchanged on failure);draftVersionstale-command protection (no index clamping); in-house RFC 7396 JSON Merge Patch + the one shared presentation resolver (base → variant → override → validate); saved-query mutations validate a complete candidate workspace with atomic repair; revision-on-commit semantics.DashboardViewerSession(bounded concurrency, per-tile cancellation, stale-wave guards,destroy()), constructible/testable without Workbench/App/AppState/editors (enforced by a broadenedcheck:archrule + a static-scan boundary test); the #235 execution planner (filter-unaffected panels run in parallel with the filter wave); normativeflow@1(presets, span-clamp, row-major packing, mobile one-column normalization, order equivalence); derived KPI bands; layout registry +flow@1fallback; and the live read-flip — the dashboard now renders fromdashboard.tiles[], notspec.favorite.Owner UX decisions applied at ship time (#286)
</>) buttons and the in-tile 1–3×/height controls were removed by owner decision. This is a deliberate override of Make Dashboard a first-class module with one-dashboard-per-current-workspace v1 UX #280's accessibility section (which required a keyboard-accessible reorder/sizing path); it is a11y-revisitable in a follow-up.×affordance was removed (owner: design mistake). Kept: clear-all, the "N active" count, and the never-hide-blocking badge.Quality
tsc --noEmit,npm run build, andcheck:archgreen. Full suite: 3360 tests passing.#276phase-3bDashboardSessionand the standalonedashboard-kpi-band.tsruntime were absorbed and deleted (no remaining importers).Closes #283
Closes #284
Closes #285
Closes #286
Closes #235
Part of #280 (phases 5–6 remain: #287, #288). Part of #153 (reorder half done here; open-in-window follows with #288). #188 was already closed as dissolved into #286's scope.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz