Skip to content

Dashboard v1 phases 1–4 (#280): first-class module — contracts, persistence, authoring, viewer + flow@1#293

Merged
BorisTyshkevich merged 15 commits into
mainfrom
feat/phase8-dashboard-module-280
Jul 18, 2026
Merged

Dashboard v1 phases 1–4 (#280): first-class module — contracts, persistence, authoring, viewer + flow@1#293
BorisTyshkevich merged 15 commits into
mainfrom
feat/phase8-dashboard-module-280

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

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

Issue Phase What
#283 1 — contracts & codecs 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_LIMITS enforcement (schema + codec byte/depth + runtime re-checks); whole-workspace cross-resource semantic validation. query-spec-v1 gains QueryDashboardPresentationV1 as an additive revision.
#284 2 — persistence & migration WorkspaceRepository over an IndexedDB aggregate (single record, atomic replacement, injected WorkspaceStore seam); one-shot legacy migration (favorites → tiles, asb:dashLayout/asb:dashColsflow@1) keyed on aggregate record existence, legacy keys untouched until the aggregate write succeeds; multi-tab last-successful-commit-wins; documented spec.favorite dual-write removal path.
#285 3 — authoring domain DashboardAuthoringSession + typed fallible atomic commands (clone → apply → normalize → validate → replace-only-if-valid; draft byte-for-byte unchanged on failure); draftVersion stale-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.
#286 4 — viewer & flow@1 Standalone read-only DashboardViewerSession (bounded concurrency, per-tile cancellation, stale-wave guards, destroy()), constructible/testable without Workbench/App/AppState/editors (enforced by a broadened check:arch rule + a static-scan boundary test); the #235 execution planner (filter-unaffected panels run in parallel with the filter wave); normative flow@1 (presets, span-clamp, row-major packing, mobile one-column normalization, order equivalence); derived KPI bands; layout registry + flow@1 fallback; and the live read-flip — the dashboard now renders from dashboard.tiles[], not spec.favorite.

Owner UX decisions applied at ship time (#286)

  • Tile reorder is pointer-drag-only, and span/height are tuned in the Spec editor — the keyboard Move-earlier/later (</>) 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.
  • The per-filter clear × affordance was removed (owner: design mistake). Kept: clear-all, the "N active" count, and the never-hide-blocking badge.
  • The migrated filter bar restores the rich relative-time / recents / enum comboboxes over the viewer's filter model — fixing a regression where the initial rewrite had dropped them for plain text/select inputs.

Quality

  • Per-file coverage gate held throughout (pure/model/application layers at 100% statements/lines; branches at the repo's ≥90 floor); tsc --noEmit, npm run build, and check:arch green. Full suite: 3360 tests passing.
  • CI (unit + e2e, all engines) green on every wave push, including the final integration commit.
  • #276 phase-3b DashboardSession and the standalone dashboard-kpi-band.ts runtime 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

BorisTyshkevich and others added 15 commits July 17, 2026 18:44
…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>
… 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>
…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
BorisTyshkevich merged commit 81daa94 into main Jul 18, 2026
9 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment