feat(#335): compound Dashboard time-range control — token-based From/To picker in the filter bar - #376
Merged
Merged
Conversation
…absolute-instant parser, recents - src/core/time-range.ts: inferTimeRangePairs (#334 interim name-pair table, curated filters excluded, ambiguity drops), resolveTimeRangeGroups gated on scalar date-like consumer contracts via resolveFilterSelection, validateTimeRangeDraft (one shared preview now, from<=to at resolved instants, equal permitted), pushRecentRange (dedupe, cap 6) - src/core/relative-time.ts: parseAbsoluteInstant (strict, generous syntax: preview formats, ISO-T, epoch digits for DateTime; UTC convention); formatPreviewInstant exported for reuse - src/ui/relative-time-field.ts: TIME_RANGE_CONSTANTS (design's 14 tokens), filterTokenList generalization; RELATIVE_TIME_PRESETS bit-identical Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-clock snapshot per wave - applyFilters(entries): atomic multi-filter commit — unknown/duplicate id or zero-change call is a whole-call no-op; one publish, one commitAndRerun over the changed parameters (single wave over the union of resolved targets) - readonly timeRangeGroups: resolved once at construction via core/time-range.ts, after #189 source-fallback resolution (curated excluded) - DashboardViewState.waveWallNowMs: each wave entry point captures one deps.wallNow() and threads it through prepareBatch/filter-source waves — fixes the latent bug where one refresh resolved relative tokens against multiple instants; getFilterField keystroke validation keeps live wall-now Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the multiselect - src/ui/popover.ts: openAnchoredDialog owns the generic dialog chrome (overlay/backdrop, role/aria-modal, Escape, Tab trap with per-press recompute, fixedAnchor placement, aria-expanded, teardown + focus return with skipFocus); documents the close-before-commit rule (#364) - multi-select-field.ts migrated behavior-preserving; its spec untouched byte-for-byte as the regression proof; busy state/live region stay content - dom.ts fixedAnchor: optional pure right-edge clamp (panelW + viewportW) for the upcoming time-range popover; existing callers unchanged Second consumer lands in the next commit (CLAUDE.md rule 5 threshold). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pover - src/ui/time-range-field.ts: dialog-pattern trigger (resolved range label, Not set, error states; aria-label carries tokens + resolved range; refreshLabel(nowMs) for per-wave re-resolution), popover via the shared openAnchoredDialog primitive (second consumer): staged From/To editors with one shared preview now, per-field constants column with typing filter, group recents with immediate apply, Cancel/Apply footer gated by validateTimeRangeDraft, identical-draft Apply closes without commit - .trf-* styles on existing theme tokens (light+dark) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oard - filter-bar.ts: Time section (one control per group, grouped params suppressed) ahead of the remaining filters; the two parallel handle maps unified into one FieldHandle map with opaque keys (param | group:key); multiselect-specific hooks generalized (openPopoverKey/focusedFieldKey/ focusFieldTrigger); refreshTimeRangeLabels(nowMs) - dashboard.ts: timeRange assembly from session.timeRangeGroups + published filter state; shell-owned session-scoped recents (outgoing pair, no-op and first-commit excluded); onApplyTimeRange -> session.applyFilters (both bounds, one wave) + polite live-region announcement; per-wave trigger label re-resolution off waveWallNowMs without a bar rebuild; read-only mode uses the same path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…refreshTile wave snapshot, year floor, NUL hygiene - time-range-field: a constant pick re-focuses the field input (the click detaches the picked button); polite sr-only region announces validation failures (per-field + range), deduped, cleared on valid - dashboard-viewer-session: refreshTile is a wave of one — publishes its own waveWallNowMs and binds the tile against it (was an untethered wallNow()) - relative-time: calendar years below 1900 rejected (no ClickHouse date type reaches lower; keeps Date.UTC's 0-99 remap unreachable) - time-range-field.test.ts: literal NUL byte replaced with the \u0000 escape (file was binary-classified by git); stale hook name in a multiselect comment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tests/e2e/time-range.html/.spec.js: real viewer session + real buildFilterBar through the dashboard glue — group resolution, pair replacement, staged editing, disabled-Apply gating, exactly-one-wave batch commit, recents immediate apply, Escape/backdrop/focus-return, light/dark, 360px clamping (chromium+webkit: 38 passed) - dashboard-mobile fixture: compound control in the narrow-bar layout checks - CHANGELOG [Unreleased]: #335 feature entry + the wave-snapshot fix Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 22, 2026
Open
…activation is the change (merge-gate review) The identical-draft no-op compared only the two text values; with a committed-but-inactive pair (clearFilter keeps the typed value and only flips active off, and defaultValue+defaultActive:false is authorable) the popover seeded valid text and Apply silently closed without committing, leaving no UI path to activate the range. The no-op now additionally requires the pair to already be active; the session's own changed-set comparison treats the active flip as a real change and runs one wave. Recents stay clean: the outgoing pair is only pushed when it was active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BorisTyshkevich
added a commit
that referenced
this pull request
Jul 22, 2026
…n never silently clear Review blocker: a Library star/rename on a dirty linked tab stamped the newest persisted token onto a tab whose draft was based on older content; a later refresh then classified it noop and (with the previous noop auto-clear) quietly made a stale draft ordinarily saveable — silently overwriting the other tab's change. The same masking also applied when the star/rename happened BEFORE any refresh flagged the conflict at all. - patchSavedSpec advances a linked tab's lastCommittedQueryToken ONLY when the tab's baseline matched the pre-patch latest entry; a lagging tab keeps its old token so the classifier keeps seeing the divergence - a lagging DIRTY tab discovered at patch time is flagged 'conflict' immediately (the tab's own commit makes the workspace token current, so the next refresh would no-op and the classifier would never run) - the reconcile noop branch NEVER auto-clears an existing conflict — the flag survives until Reload-saved-version or Keep-my-draft succeeds (kept counted in the refresh summary) - secondary: patchSavedSpec aborts on an externally-deleted target return deletedExternally; Library star/rename toast and trigger refreshWorkspaceFromStore so the dead row reconciles immediately; Save-button/tab-badge repaint after Library patches - regression tests: the reported scenario (conflict → star/rename → unrelated external change → refresh keeps conflict), the pre-refresh masking variant, baseline-not-advanced unit test, noop-keeps-conflict (replacing the previous opposite-direction test), deletedExternally surfacing Also merges origin/main (time-range control #376, dep bumps) — clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
Merged
7 tasks
BorisTyshkevich
added a commit
that referenced
this pull request
Jul 22, 2026
…nvalidate stale tabs (#377) * feat(#343): centralize workspace projection + cross-tab invalidation in mutateWorkspace Step 1-2 of the cross-tab consistency plan: - new pure src/workspace/workspace-sync.ts: workspaceToken/queryToken/ queriesChanged + reconcileLinkedTabs linked-tab classifier (adopt/conflict/detach/orphan/noop), 100% covered - app.mutateWorkspace transform now returns { candidate, data? }; on a successful commit the primitive itself projects via applyCommittedWorkspace exactly once, records the snapshot token, and posts one { type: 'workspace-changed', sourceTabId, workspaceId } invalidation (never the workspace body) - new env seams: broadcastChannel(name) factory (capability-or-null) and documentVisible(); per-tab sourceTabId; inbound messages ignore self and route to the onExternalWorkspaceChange hook (wired in a later step) - file-menu commitWorkspace and dashboard settleCommand stop projecting on success (route caches + rollback preserved) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f * feat(#343): Workbench saved-query mutations become transforms over the latest workspace Step 3: create/save-linked/star/rename/delete no longer commit candidates built from stale app.state. - state.ts planners (createSavedQuery, commitSavedQuery, patchSavedSpec, renameSaved, toggleFavorite, deleteSaved) fold their change into the latest committed aggregate inside app.mutateWorkspace's transform; the CommitWorkspace seam and buildWorkspaceCandidate are gone - save-linked and spec patches resolve their target by id against latest.queries and abort (never recreate) when it was deleted externally - star keeps the dual-write semantics but derives tile membership from latest.dashboard, expressed as an explicit desired membership - saved-query-service takes deps.mutateWorkspace instead of a raw workspace.commit; saved-history and the app.ts create/commit call sites drop their serializeWrite wrappers (the primitive serializes) - new tests/unit/cross-tab-consistency.test.ts: two createApp() instances over one shared fake IndexedDB cover the read-before-write matrix Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f * feat(#343): cross-tab refresh, coalesced invalidation, and linked-tab conflict resolution Steps 4-5: stale tabs now learn about external commits and reconcile. - app.refreshWorkspaceFromStore() runs inside the same serializeWrite queue as mutations; token compare (workspaceToken vs lastCommittedToken) makes unchanged reloads a no-op and prevents projecting an older read over a newer local commit; failed loads warn non-destructively, retry on next activation, and never wedge the queue - channel pokes, window focus, and visibilitychange (when visible) coalesce through one refreshPending flag into a single queued refresh; the fallback works when BroadcastChannel is absent - per-tab lastCommittedQueryToken recorded at every sync point (open-from-saved/create/save/adopt, gap-filled at projection); reconcileLinkedTabsToLatest applies the classifier: clean tabs adopt the external version, dirty tabs keep their draft and enter an explicit conflict state, deleted+clean detaches, deleted+dirty becomes an unsaved draft flagged deleted-elsewhere (Save-as-new only) - new src/ui/conflict-resolution.ts: two-action chooser (Reload saved version / Keep my draft with explicit confirm); Save on a conflicted tab routes here and never silently overwrites Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f * feat(#343): editable Dashboard rebuilds on external workspace changes Step 6: the Dashboard route reacts to cross-tab invalidation. - renderDashboard registers app.onWorkspaceExternallyChanged per render; an editable route rebuilds its viewer session from committed truth through the shared rebuildRouteFromCommitted() funnel (also used by settleCommand's membership-restore path, so the two can never double-render); rebuilds defer while command descriptors are pending and coalesce via the rebuilding flag - a full-route rebuild re-reads both the Dashboard document and the query collection, so an external query-only change rebuilds even a byte-identical document; the rebuild never commits - detached/read-only Dashboard views ignore primary-workspace invalidation at both layers: app.dashboardReadOnly short-circuits the cross-tab refresh (never projecting the primary aggregate over a detached snapshot) and the route hook no-ops - per-Dashboard filter values survive the rebuild via the existing KEYS.dashFilters store (covered by test) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f * fix(#343): surface externally-deleted saves, clear stale conflict flags (review) Two-reviewer pass findings applied (no blockers found): - a linked Save whose target was deleted in another tab no longer fails silently: commitSavedQuery flags the abort (deletedExternally), the service maps it to reason 'deleted', and commitLinkedQuery toasts + triggers refreshWorkspaceFromStore so the tab gets its orphan/detach treatment immediately instead of waiting for the next activation; the vanished-query guard in reloadSavedVersion refreshes the same way - a reconcile that classifies a still-linked tab as noop clears a stale 'conflict' flag (divergence disappeared); the 'deleted' badge on orphaned tabs is deliberately kept until the user acts - create/delete failure narrowing uses the outcome union instead of an as-cast; documented the conservative-skip window during a Dashboard route rebuild - CHANGELOG [Unreleased] entry for #343 Live-verified in a real two-tab browser session (broadcast refresh, conflict flagging with draft preservation, Reload saved version, Keep my draft behind explicit Overwrite, clean-tab adopt). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f * fix(#343): conflict-token lifecycle — a flagged or unseen conflict can never silently clear Review blocker: a Library star/rename on a dirty linked tab stamped the newest persisted token onto a tab whose draft was based on older content; a later refresh then classified it noop and (with the previous noop auto-clear) quietly made a stale draft ordinarily saveable — silently overwriting the other tab's change. The same masking also applied when the star/rename happened BEFORE any refresh flagged the conflict at all. - patchSavedSpec advances a linked tab's lastCommittedQueryToken ONLY when the tab's baseline matched the pre-patch latest entry; a lagging tab keeps its old token so the classifier keeps seeing the divergence - a lagging DIRTY tab discovered at patch time is flagged 'conflict' immediately (the tab's own commit makes the workspace token current, so the next refresh would no-op and the classifier would never run) - the reconcile noop branch NEVER auto-clears an existing conflict — the flag survives until Reload-saved-version or Keep-my-draft succeeds (kept counted in the refresh summary) - secondary: patchSavedSpec aborts on an externally-deleted target return deletedExternally; Library star/rename toast and trigger refreshWorkspaceFromStore so the dead row reconciles immediately; Save-button/tab-badge repaint after Library patches - regression tests: the reported scenario (conflict → star/rename → unrelated external change → refresh keeps conflict), the pre-refresh masking variant, baseline-not-advanced unit test, noop-keeps-conflict (replacing the previous opposite-direction test), deletedExternally surfacing Also merges origin/main (time-range control #376, dep bumps) — clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f * fix(#343): clean lagging tab adopts committed truth at patch time (review blocker 2) A clean linked tab whose baseline lagged latest (external change, missed poke) could be left silently stale by a Library star/rename: the patch folded into latest correctly, but the tab kept its old SQL/Spec, the tab's own commit made the workspace token current, and the promised adopt-on-next-refresh never ran (refresh no-ops at the workspace level). An ordinary edit+Save from that tab would then overwrite the external change without ever seeing a conflict. patchSavedSpec post-apply now implements the full three-case table: - baseline matched pre-patch latest -> apply local metadata patch, advance the baseline token - baseline lagged + dirty tab -> preserve the draft, flag/keep conflict - baseline lagged + clean tab -> adoptSavedIntoTab(saved) immediately (latest content + this patch; stays clean; token = committed token) An ABSENT baseline is unknown, not provably lagged (every production link path stamps one) - legacy draft-patch behavior is kept there so clean draft-only Spec extensions survive. Also: Library star/rename handlers re-fire the tabs signal so an adopted active tab's editor resyncs; new saved-history tests cover the deletedExternally toast+refresh branches (the CI coverage failure). Regression tests: unit clean-lagging adopt; cross-tab "B opens q1 clean, A changes SQL, B renames without refreshing -> B immediately holds both changes, stays clean, refresh no-ops, store holds both". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f --------- Co-authored-by: Claude Fable 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.
What & why
Closes #335 — the Dashboard filter bar gains a compound time-range control: any pair of scalar date-like filters matching #334's recognized name-pair table (
from/to,from_time/to_time,start/end,start_time/end_time, case-insensitive) renders as one control in a Time section, replacing the pair's two independent fields. This is the migration from separate From/To time filters to a single coordinated range.Not set/ error states per the issue.Filters Panel.html) stages token-or-absolute From/To edits with live resolved previews against one sharednow, per-field relative-time constants (typing filters; selecting stages), group-scoped session-only Recently used ranges (immediate apply), and Apply gated on both bounds resolving withfrom ≤ to(equal permitted).DashboardViewerSession.applyFilters(entries)— validate-all-then-mutate, one publish, onecommitAndRerunwave over the union of both parameters' resolved targets; unknown/duplicate ids or an identical draft commit nothing. Add synchronized Dashboard time-range groups, crosshairs, and drag-to-zoom #334's drag-to-select will commit through this same API.core/time-range.ts), gated on consumer contracts via the Dashboard query-backed filters: searchable multiselect with Apply #189resolveFilterSelectionmachinery (isDateLikeType, scalar, zero diagnostics); curated/source-backed filters never group. Pair discovery sits behind a seam so Add synchronized Dashboard time-range groups, crosshairs, and drag-to-zoom #334'stimeRangesmetadata replaces the interim name inference without UI changes.parseAbsoluteInstantincore/relative-time.ts): preview formats, ISO-Tvariants, epoch digits for DateTime types, real calendar checks, years ≥ 1900. No second grammar/formatter.ui/popover.tsopenAnchoredDialog) extracted from the feat(#189): searchable multiselect for query-backed Dashboard filters with Apply #364 multiselect at the rule-5 second-consumer threshold — the multiselect migrated behavior-preserving with its 717-line spec untouched byte-for-byte;fixedAnchorgained an opt-in pure viewport clamp. The filter bar's two parallel handle maps were unified into oneFieldHandlemap with generalized popover hooks (openPopoverKey/focusedFieldKey/focusFieldTrigger).wallNow()snapshot (published asDashboardViewState.waveWallNowMs) and threads it through every relative-token resolution — previously one refresh took several independent readings, sonow/-1din different sub-phases could resolve seconds apart.Owner decisions recorded on the issue: strict #334 name table only; no un-set affordance in v1; curated date filters excluded from grouping; strict-but-generous absolute syntax.
Verification: 163 unit files / 4825 tests green (per-file gate: new core+UI files at 100%); e2e 38/38 on chromium+webkit (real viewer session: pair replacement, staged editing, disabled-Apply, exactly-one-wave commit, recents, Escape/backdrop/focus, light/dark, 360 px clamping); three independent review passes (spec compliance, correctness/concurrency, quality/a11y) — all findings fixed in
a205295.Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed🤖 Generated with Claude Code