Skip to content

fix: workspace/dashboard persistence follow-ups — star→tile (#299), corrupt-aggregate surface (#300), dashboard filter persistence (#303)#304

Merged
BorisTyshkevich merged 3 commits into
mainfrom
fix/workspace-aggregate-followups-299-300
Jul 18, 2026
Merged

fix: workspace/dashboard persistence follow-ups — star→tile (#299), corrupt-aggregate surface (#300), dashboard filter persistence (#303)#304
BorisTyshkevich merged 3 commits into
mainfrom
fix/workspace-aggregate-followups-299-300

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Three workspace/dashboard persistence follow-ups from the #287/#280 refactor, in one PR (three commits).

Part of #299 — Workbench star → Dashboard tile membership. Since dashboard.tiles[] became canonical Dashboard membership (#280/#287), the Workbench star only flipped spec.favorite and never added/removed a tile, so favoriting a query had no effect on /sql/dashboard. toggleFavorite now reflects the flip onto tile membership in the same atomic commit via a new pure toggleTileMembership (src/dashboard/application/tile-membership.ts). The role gate mirrors the legacy-migration precedent — only panel-role queries become tiles; a favorited filter/setup-role query stays favorite-only. Unfavoriting removes every tile referencing the query and scrubs those tile ids from filter targets. spec.favorite stays the star's visual state — the documented dual-write retirement is deferred (so this is Part of, not Closes).

Closes #300 — surface a corrupt-but-present aggregate on boot. WorkspaceRepository gains loadCurrentResult(), distinguishing empty / ok / corrupt (with codec diagnostics) instead of collapsing "no record" and "unreadable record" both to null — which previously let boot silently continue on the legacy projection and orphan the corrupt record on the next CRUD commit, with no user-visible error. Boot now shows a toast with a Reset workspace action that clears the unreadable record and rebuilds a fresh aggregate from current local state. flashToast gained an optional, non-auto-dismissing action button. (Both the OAuth boot and the basic-auth connect path go through loadWorkspaceOnBoot, so both get the surface.)

Closes #303 — Dashboard filter values persist across reload. Regression from the #280 dashboard rewrite: the isolated viewer session initialized every filter purely from def.defaultValue/defaultActive and persisted nothing, so a committed filter value reset on reload (the Workbench var-strip was unaffected). Filter runtime value/active now persist to an isolated per-dashboard key (asb:dashFilters, dashboardId → filterId → {value,active}) — deliberately separate from the Workbench's asb:varValues/asb:filterActive (Option B) — and are seeded back on load. Read/write/merge is a pure dashboard/model store behind the existing loadJSON/saveJSON seam, so the viewer session stays AppState/storage-free (check:arch clean). Opening a dashboard never persists its defaults — only genuine committed changes write.

New inbox issue #303 was filed with the root-cause analysis. Roadmap #68's follow-ups note reconciled.

Verification

  • npm test: 3485 passed (140 files); new pure modules 100/100/100/100.
  • npx tsc --noEmit, npm run check:arch, npm run build: all clean.
  • npm run test:e2e (chromium + webkit): 90 passed.
  • /code-review (high): one real finding — the Dashboard filter values are not persisted across reload (regression from #280 rewrite) #303 persist effect wrote filter defaults to storage on first open (contradicting intent, freezing defaults against later Spec edits); fixed (seed the persist signature from the session's initial filter state) with a regression test.

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/dashboard/model + src/dashboard/application, DOM in src/ui/, storage behind the injected seam
  • No new runtime dependency
  • README / CHANGELOG.md ([Unreleased]) updated
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, issue bodies)

🤖 Generated with Claude Code

https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

BorisTyshkevich and others added 3 commits July 18, 2026 14:44
Since dashboard.tiles[] became canonical Dashboard membership (#280/#287),
the Workbench star only flipped spec.favorite and never added/removed a tile,
so favoriting a query in the Workbench had no effect on /sql/dashboard.

toggleFavorite now reflects the favorite flip onto tile membership in the SAME
atomic commit, via a new pure toggleTileMembership (src/dashboard/application/
tile-membership.ts). The role gate mirrors the legacy migration precedent
(buildLegacyMigrationCandidate): only PANEL-role queries become tiles — a
favorited filter/setup-role query stays favorite-only. Unfavoriting removes
every tile referencing the query and scrubs those tile ids from filter targets
(mirrors saved-query-mutation.ts's removeAffectedTiles); the result is run
through flowLayoutPlugin.normalize. patchSavedSpec gains an optional
DashboardTransform (default identity, so renameSaved is unaffected) and now
projects the committed dashboard back onto state, same convention as
savedQueries. spec.favorite stays the star's visual state — the documented
dual-write retirement is deferred (Part of #299).

Also adds the KEYS.dashFilters constant consumed by the #303 commit (same file).

Pure tile-membership logic 100% covered; state/saved-history tests extended.

Part of #299

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
The legacy-migration marker keys on raw record existence while loadCurrent()
treats an undecodable record as null — so a corrupt-but-present aggregate
blocked re-migration AND read as null, letting boot silently continue on the
legacy projection until the next CRUD commit orphaned the corrupt record with
no user-visible error.

WorkspaceRepository gains loadCurrentResult(), distinguishing empty / ok /
corrupt (with the codec diagnostics) instead of collapsing empty and corrupt
to null; loadCurrent() is untouched. loadWorkspaceOnBoot now uses it: on
corrupt it leaves state untouched and flashes a toast with a "Reset workspace"
action that clears the unreadable record and rebuilds a fresh aggregate from
current local state. Both the OAuth boot (main.ts) and the basic-auth connect
action go through loadWorkspaceOnBoot, so both get the surface. flashToast
gained an optional, non-auto-dismissing action button.

workspace-repository/toast 100% covered; app.ts integration-tested.

Closes #300

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
…ore)

Regression from the #280 dashboard rewrite: the isolated viewer session
initialized every filter purely from def.defaultValue/defaultActive and
persisted nothing, so a committed filter value lived only in memory and reset
on reload (the Workbench var-strip, which persists asb:varValues/filterActive,
was unaffected).

Filter runtime value/active now persist to an ISOLATED per-dashboard key
(KEYS.dashFilters = asb:dashFilters, dashboardId -> filterId -> {value,active})
— deliberately separate from the Workbench keys (Option B) — and are seeded
back on load via a new DashboardViewerDeps.initialFilters. The read/write/merge
logic is a pure dashboard/model store (dashboard-filter-store.ts, 100% covered)
behind the existing loadJSON/app.saveJSON seam, so the viewer session stays
AppState/storage-free (check:arch clean). The persist step gates on a dedicated
value/active signature seeded from the session's own initial filter state, so
opening a dashboard never writes its defaults back (which would freeze them
against later Spec-editor default changes) — only genuine committed changes
persist.

Closes #303

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz
@BorisTyshkevich
BorisTyshkevich merged commit 1636237 into main Jul 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant