fix: workspace/dashboard persistence follow-ups — star→tile (#299), corrupt-aggregate surface (#300), dashboard filter persistence (#303)#304
Merged
Conversation
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
This was referenced Jul 18, 2026
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
Three workspace/dashboard persistence follow-ups from the #287/#280 refactor, in one PR (three commits).
Part of #299— Workbench star → Dashboard tile membership. Sincedashboard.tiles[]became canonical Dashboard membership (#280/#287), the Workbench star only flippedspec.favoriteand never added/removed a tile, so favoriting a query had no effect on/sql/dashboard.toggleFavoritenow reflects the flip onto tile membership in the same atomic commit via a new puretoggleTileMembership(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.favoritestays the star's visual state — the documented dual-write retirement is deferred (so this isPart of, notCloses).Closes #300— surface a corrupt-but-present aggregate on boot.WorkspaceRepositorygainsloadCurrentResult(), distinguishingempty/ok/corrupt(with codec diagnostics) instead of collapsing "no record" and "unreadable record" both tonull— 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.flashToastgained an optional, non-auto-dismissing action button. (Both the OAuth boot and the basic-auth connect path go throughloadWorkspaceOnBoot, 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 fromdef.defaultValue/defaultActiveand 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'sasb:varValues/asb:filterActive(Option B) — and are seeded back on load. Read/write/merge is a puredashboard/modelstore behind the existingloadJSON/saveJSONseam, so the viewer session stays AppState/storage-free (check:archclean). Opening a dashboard never persists its defaults — only genuine committed changes write.New
inboxissue #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 testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/dashboard/model+src/dashboard/application, DOM insrc/ui/, storage behind the injected seamCHANGELOG.md([Unreleased]) updated🤖 Generated with Claude Code
https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz