Skip to content

Dashboard polish: nested-group preview, full-width widget header bar, cross-page time sync, Reset button + HoverCrosshair guard#121

Merged
HanSur94 merged 36 commits intomainfrom
claude/happy-ramanujan-7d436a
May 8, 2026
Merged

Dashboard polish: nested-group preview, full-width widget header bar, cross-page time sync, Reset button + HoverCrosshair guard#121
HanSur94 merged 36 commits intomainfrom
claude/happy-ramanujan-7d436a

Conversation

@HanSur94
Copy link
Copy Markdown
Owner

@HanSur94 HanSur94 commented May 8, 2026

Summary

A series of dashboard polish quick-fixes from this session, bundled into one PR. Each commit is atomic and references its 260508-<id> task in .planning/quick/.

User-visible improvements:

  • Stale-data banner now sits in a dedicated reserved strip at the top of the dashboard — no longer overlaps the multi-page tab strip or any widget content.
  • Slider preview now shows nested GroupWidget children (e.g. Feed Line page's nested FastSenseWidgets) and uses raw fidelity below 100 samples for cleaner live previews.
  • Time-range sync (slider drag, Sync button, Reset) now propagates across all dashboard pages, not just the active one. Lazy-realized widgets on tab-switch inherit the synced range.
  • Widget header bar restored to full-width, with widget content rendering into a dedicated sub-panel beneath it — no more truncated titles or axes labels.
  • New Reset button in the dashboard toolbar (recovery action when widgets get stuck).
  • HoverCrosshair.onFigureMove_ no longer throws "Invalid or deleted object" when its closure outlives the underlying axes.

Internal refactors:

  • New DashboardWidget.hCellPanel property + getNestedWidgets() virtual method to support the new chrome layout and nested-iteration patterns.
  • New DashboardEngine.BannerHeight and LastSyncedTimeRange_ properties; new flattenWidgetsForPreview_ helper.
  • repositionStaleBanner_ keeps the banner aligned across resize/visibility changes.

Reverts inside this PR (forward-fix history):

  • 260508-kau (aggregate preview across all pages) → reverted by 260508-kov (per-tab preview is what the user actually wanted).
  • 260508-m52 (shrink button bar to 64px) → superseded by 260508-mhv (keep full-width bar, render content below).
  • 260508-ny6 (mark-dirty+refresh sweep on tab-switch) → reverted by 260508-od4 (didn't fix the stuck-widget symptom and added per-tab cost).

Quick tasks included

ID Description
260508-jf1 Banner sits below page-tab strip (initial fix, superseded by jyh)
260508-jyh Reserved top strip for stale-data banner (final geometry)
260508-kau Slider preview aggregates across all pages (reverted)
260508-kov Revert kau → per-tab preview
260508-l2k Slider preview recurses into GroupWidget children
260508-llw Time-range broadcast across all pages
260508-m52 Shrink button bar to 64px (superseded by mhv)
260508-mhv Full-width bar + content sub-panel below
260508-n3u Skip preview downsampling under 100 samples
260508-ng1 Add Reset button to dashboard toolbar
260508-ny6 switchPage refresh sweep (reverted)
260508-od4 Roll back ny6 + HoverCrosshair invalid-obj guard

Test plan

  • tests/test_dashboard_stale_banner.m — 7/7
  • tests/test_dashboard_preview_overlay.m — 10/10 (incl. per-tab + nested-group cases)
  • tests/test_dashboard_engine_event_markers.m — 9/9
  • tests/test_dashboard_time_sync_all_pages.m — 5/5
  • tests/test_dashboard_widget_button_bar.m — 5/5 (full-width + content-panel-below)
  • tests/test_dashboard_toolbar_buttons.m — 7/7 (incl. Reset button case)
  • tests/test_hover_crosshair.m — invalid-obj guard regression added
  • mh_lint clean on every modified file
  • Manually exercised in demo/industrial_plant/run_demo across all 6 pages

🤖 Generated with Claude Code

HanSur94 and others added 30 commits May 8, 2026 14:06
Position the orange stale-data banner BELOW both toolbar and the
(effective) page-bar height instead of sharing the page-bar slot, so
multi-page tabs stay clickable and visible while the banner is shown.

- DashboardEngine.createStaleBanner: y = 1 - toolbarH - effPageBarH - bannerH
  (effPageBarH == 0 in single-page mode, preserving today's geometry).
- New private helper repositionStaleBanner_ recomputes Y from current
  chrome state; safe no-op when the banner handle is empty.
- applyVisibilityAndRelayout and onResize call repositionStaleBanner_
  so the banner tracks chrome-visibility and figure-resize changes.
- Layout.ContentArea is untouched — banner stays a passive overlay.
- New regression scenario testBannerBelowPageBarMultiPage in
  test_dashboard_stale_banner.m (7/7 passing in Octave).
Adds 260508-jf1 PLAN + SUMMARY artifacts and records the quick task
in STATE.md. Bug: orange stale-data banner shared the page-bar slot
and was uistack'd on top, hiding multi-page tabs. Fix: position banner
below the (effective) page-bar height; new repositionStaleBanner_
helper invoked from applyVisibilityAndRelayout and onResize. 7/7
test_dashboard_stale_banner scenarios passing in Octave.
- Replace testBannerBelowPageBarMultiPage with
  testBannerInReservedStripAboveAllChrome — banner sits at top, never
  overlaps toolbar / page tabs / widgets.
- Add testReservedStripStableWhenHidden — toolbar/pagebar/content-area
  positions are byte-identical across show/hide.
- Add testBannerHeightProperty — public BannerHeight = 0.035 default.

Octave run: 6 passed, 3 failed (RED).
…nner

Eliminate the banner-as-overlay model. A new public BannerHeight = 0.035
strip is reserved at the very top of the dashboard figure, and toolbar /
page-bar / content-area all shift down by BannerHeight. Reserved space
persists when the banner is hidden — toggling visibility no longer shifts
chrome or widgets.

DashboardEngine:
- Add BannerHeight public property (default 0.035, single source of truth).
- render(): hidden-pagebar placeholder Y, content-area Y now subtract
  BannerHeight.
- applyVisibilityAndRelayout(): same content-area formula.
- createStaleBanner(): banner Y = 1 - BannerHeight (top strip);
  toolbarH parameter retained for signature compat but unused.
- repositionStaleBanner_(): simplified — constant Y, no chrome-height
  reads.
- renderPageBar(): page-bar Y subtracts BannerHeight.

DashboardToolbar:
- hPanel Y subtracts engine.BannerHeight so toolbar sits directly below
  the reserved strip.
- getContentArea(): content area Y subtracts BannerHeight for
  consistency with engine inline calc.

Octave run: 9 passed, 0 failed.
Adds 260508-jyh PLAN + SUMMARY artifacts and records the quick task in
the Quick Tasks Completed table. Plan executed in TDD: RED commit
2e333cd added 3 failing scenarios; GREEN commit bdf1dc5 introduced
DashboardEngine.BannerHeight (default 0.035) and shifted toolbar /
page-bar / content-area down by BannerHeight so the banner is no
longer an overlay. Final Octave run: 9 passed, 0 failed.
- DashboardEngine.computePreviewEnvelopeReturning_ now iterates allPageWidgets()
  so the time-slider preview folds in widgets from every page, not just the
  active tab (KAU-01).
- DashboardEngine.computeEventMarkers now iterates allPageWidgets() so event
  markers reflect the entire dashboard regardless of which tab is in front.
- DOC headers for both methods updated to describe the all-pages contract;
  single-page behavior is preserved bit-for-bit because allPageWidgets()
  returns obj.Widgets when Pages is empty.
- Updated case_switch_page (test_dashboard_engine_event_markers.m) and
  testEventMarkersUpdateOnSwitchPage (TestDashboardEngineEventMarkers.m) to
  assert the sorted union of all pages' events both before and after each
  switchPage call.
case_multipage_preview_aggregates_all_pages locks in the KAU-01 contract:
builds a 2-page dashboard with disjoint X ranges per page (P1 in [0,10],
P2 in [200,210]) and asserts the slider preview folds in BOTH pages'
widgets — proving the inactive page contributes — both at initial render
and after switchPage(2).

Verified RED before commit by reverting the engine fix locally: case
failed exactly on the inactive-page assertion. With Task 1's
allPageWidgets() switch in place the case passes (9/9 in
test_dashboard_preview_overlay).
…quick task

Adds 260508-kau PLAN + SUMMARY artifacts and records the quick task in
the Quick Tasks Completed table. Plan executed as fix + TDD test:
Task 1 (a098be2) switched DashboardEngine.computePreviewEnvelopeReturning_
and computeEventMarkers from activePageWidgets() to allPageWidgets() so
the slider preview + event markers aggregate across ALL pages (KAU-01),
plus DOC-header rewrites and updated switchPage tests. Task 2 (70c3c4c)
added regression case_multipage_preview_aggregates_all_pages — verified
RED before commit by reverting the engine fix locally, then GREEN with
fix in place. Final results: 2/2 + 9/9 + 8/8 + 8/8 across the four test
scenarios.
… iteration

- computePreviewEnvelopeReturning_ now iterates obj.activePageWidgets()
  (was obj.allPageWidgets() under 260508-kau / KAU-01)
- computeEventMarkers now iterates obj.activePageWidgets() likewise
- DOC headers for both methods describe the active-page contract; the
  switchPage() hook (already wired) recomputes envelope + markers on
  every tab switch, so navigation stays in sync per-tab.
- allPageWidgets() helper itself is unchanged; setEventMarkersVisible
  (L143) and the config-load injection in fromStruct (L2299) keep using
  it legitimately to broadcast across every page.

Forward-fix supersedes a098be2 (kau Task 1) without git revert.
…ions

- case_switch_page (function-based) asserts [5 15] / [100 200 300] /
  [5 15] across the three switchPage transitions instead of the kau
  sorted-union expectation.
- testEventMarkersUpdateOnSwitchPage (class suite) mirrors the same
  per-tab assertions via verifyEqual.
- Both methods are byte-identical to e7f312a:tests/... — the original
  pre-kau contract that switchPage already wired via the L218-223
  computePreviewEnvelope + computeEventMarkers calls.
…per-tab regression

- Drops case_multipage_preview_aggregates_all_pages (kau Task 2 / KAU-01).
- Adds case_multipage_preview_follows_active_tab asserting the inverse
  invariant: only the active page's preview lines are visible, and
  switching tabs swaps the visible line. Three transitions covered
  (initial -> P1 only, switchPage(2) -> P2 only, switchPage(1) -> P1
  only) so the reverse-navigation path is also pinned.
- Factors the X-range classifier into classifyPreviewLines_ helper.
- runCase_ dispatch list keeps 9 entries (only the 9th symbol/label
  changes); earlier 8 cases unchanged.
…review + event markers

- DashboardWidget: add virtual getNestedWidgets() returning {} on the base.
- GroupWidget: override getNestedWidgets() returning Children + flattened
  Tabs widgets (mirrors getTimeRange's recursion shape).
- DashboardEngine: add private flattenWidgetsForPreview_ helper (depth-first,
  defensive try/catch + depth cap of 10) and call it at the two iteration
  sites — computePreviewEnvelopeReturning_ and computeEventMarkers — both
  still fed by activePageWidgets() so per-tab semantics from 260508-kov
  are preserved.
- Update doc headers of computePreviewEnvelope and computeEventMarkers to
  mention "including nested GroupWidget children".

Restores preview lines and event markers on dashboard tabs whose only
data/event-bearing widgets live inside a GroupWidget. Existing test quartet
(envelope, overlay, event_markers script, suite) still green.
…es + event markers

- test_dashboard_preview_overlay: add case_nested_group_preview_lines —
  GroupWidget with two FastSenseWidget children, asserts the slider has
  exactly 2 preview lines after the engine flattens the active page's
  widget tree.
- test_dashboard_engine_event_markers: add case_nested_group_event_markers —
  GroupWidget with two EventTimelineWidget children carrying disjoint
  events, asserts the slider's marker XData equals the union of both
  children's start times ([5 15 25 35]).

Both tests verified RED on HEAD~1 (pre-fix) and GREEN on HEAD (post-fix).
…t active (LLW-01, LLW-02)

DashboardEngine.broadcastTimeRange and resetGlobalTime previously iterated
activePageWidgets(), so dragging the time slider or clicking "Sync all" /
"Reset" only affected widgets on the currently active tab. Multi-page
dashboards drifted out of sync as soon as the user navigated away.

- broadcastTimeRange now iterates allPageWidgets() and caches the [tStart
  tEnd] window in a new private property LastSyncedTimeRange_.
- resetGlobalTime now iterates allPageWidgets() so UseGlobalTime is
  re-attached for every widget on every page.
- Per-widget UseGlobalTime=false (manually zoomed) opt-out is preserved
  by FastSenseWidget.setTimeRange's existing guard.
- Per-tab slider PREVIEW iteration (computePreviewEnvelope,
  computeEventMarkers — fixed in 260508-kov / 260508-l2k) is intentionally
  unchanged: those remain active-page-only.

Refs LLW-01 (broadcast all pages) and LLW-02 (reset all pages); LLW-03
(re-broadcast on tab switch) follows in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (LLW-03)

When a multi-page dashboard has unrealized widgets on hidden pages,
switching tabs realizes them lazily — but they came up at their
construction default xlim, ignoring whatever time window the user had
already synced from another tab. After this patch switchPage re-applies
the cached LastSyncedTimeRange_ after realizeBatch, so newly-realized
widgets immediately inherit the dashboard-wide window.

- Empty-cache guard prevents re-broadcasting an unset range (would
  otherwise clobber widgets' construction xlim with []).
- Re-broadcast runs unconditionally when the cache is set; the call is
  cheap and idempotent thanks to FastSenseWidget.setTimeRange's
  ~isempty(FastSenseObj) / ishandle(ax) guards.
- Placed BEFORE the preview/markers refresh so FastSense pyramid resolves
  trigger first; preview computation then sees the consistent state.

Refs LLW-03.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests/test_dashboard_time_sync_all_pages.m guarding all three
LLW-* requirements plus the per-widget UseGlobalTime opt-out and the
single-page (no Pages) fallthrough path:

- case_active_and_inactive_pages_receive_broadcast (LLW-01)
- case_reset_global_time_reattaches_all_pages       (LLW-02)
- case_unrealized_widget_on_tab_switch_inherits_synced_range (LLW-03)
- case_manual_zoom_widget_opts_out_of_broadcast      (per-widget contract)
- case_single_page_dashboard_unaffected              (allPageWidgets fallthrough)

Builds a headless 2-page DashboardEngine with one FastSenseWidget per
page, each backed by a synthetic sensor. Uses broadcastTimeRangeNow
(the Hidden test hook) to bypass the slider debounce timer. 5/5 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the three-commit fix that promotes broadcastTimeRange and
resetGlobalTime to dashboard-wide scope (allPageWidgets) and adds a
LastSyncedTimeRange_ cache that switchPage replays after lazy widget
realize. Documents the deliberately-untouched code paths (live tick
updateLiveTimeRange, per-tab slider preview/markers) so future readers
don't accidentally re-introduce the bug.

All 5 new regression cases pass; preview overlay (10), event markers
function test (9), and event markers suite (8) confirmed unregressed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uttonBar

- Asserts bar Position(3) <= 64 and right-anchored
- Asserts info + detach buttons fit inside bar
- Asserts SizeChangedFcn keeps right-anchor invariant after resize
- Currently RED: bar is rendered at full panel width (~770px)
…strip

Bar was rendered full-width (panel_width - 2*inset), covering each widget's
own title rendering (GroupWidget header, StatusWidget label, TextWidget
title, BarChartWidget axes title, DividerWidget body).

- getOrCreateButtonBar_: barW = min(64, ...), x = pp(3) - barW - inset
- reflowButtonBar_: same right-anchor formula on resize; promoted to
  public Static so tests can drive deterministic resize without relying
  on SizeChangedFcn (skipped under -batch on R2020b headless)
- Doc-comment refreshed to match actual top-right strip behavior
- Test 3 in tests/test_dashboard_widget_button_bar.m updated to call
  the now-public reflowButtonBar_ directly

Verified: new regression test (3/3) + sanity sweep all green
(test_dashboard_toolbar_buttons 4/4, test_dashboard_preview_overlay 10/10,
test_dashboard_engine_event_markers 9/9, test_dashboard_time_sync_all_pages
5/5, suite/TestDashboardEngineEventMarkers 8/8).
…ght-anchor fix

- 260508-m52-PLAN: single-task TDD plan
- 260508-m52-SUMMARY: documents the fix, test seam decision, full
  regression + sanity sweep results, deviation notes
- STATE.md: append m52 row, advance last_activity to 1410524
… content-panel contract

- Replaces m52 'small right-anchored strip' assertions with mhv full-width
  invariant and a new WidgetContentPanel sub-panel below the bar
- Adds DividerWidget no-chrome regression
- Renames the reflow test to call DashboardLayout.reflowChrome_
- Failing RED before implementation lands (4/5 fail as expected)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt sub-panel below

Reverts m52's 64px right-anchored shrink and instead introduces a
WidgetContentPanel sub-panel BELOW the bar so widget titles, axes,
status text and group headers render entirely below the chrome strip
without overlap.

Changes:
- DashboardLayout.realizeWidget: chrome-first ordering — bar + content
  sub-panel created before widget.render so widgets render into the
  visible content area
- DashboardLayout.getOrCreateButtonBar_: full-width formula
  (barW = pp(3) - 2*inset, x = inset); bar parented to widget.hCellPanel
- DashboardLayout.createContentPanel_ (new): bottom-anchored full-width
  WidgetContentPanel sub-panel sized to (cellW, cellH - 28 - inset)
- DashboardLayout.reflowChrome_ (renamed from reflowButtonBar_): resizes
  both bar AND content sub-panel on cell resize; reflowButtonBar_ kept
  as deprecated forwarding shim
- DashboardWidget.hCellPanel (new property): outer cell panel handle so
  layout helpers + visibility cascades + drag/resize math can find the
  cell after widget.render reassigns hPanel to the content sub-panel
- DashboardWidget.delete: prefers hCellPanel over hPanel so the entire
  chrome cascade is reaped together
- DashboardEngine page-switch visibility toggle: cascades on hCellPanel
  (the cell) so bar + content + widget content all hide together
- DashboardBuilder drag/resize/relayout/overlay paths: read OUTER cell
  position from hCellPanel (with hPanel fallback for unrealized or
  no-chrome widgets) so canvas-normalized math stays correct
- DividerWidget no-chrome path preserved — render directly into cell,
  no bar, no content sub-panel
- tests/test_dashboard_builder_interaction.m: getCellPanel_ helper so
  drag/resize coordinates are read from the canvas-normalized cell
  panel (not the pixel-units content sub-panel)

Verification (all green):
- test_dashboard_widget_button_bar: 5/5 (full-width, content panel,
  buttons fit, reflow, divider no-chrome)
- test_dashboard_preview_overlay: 10/10
- test_dashboard_engine_event_markers: 9/9
- test_dashboard_time_sync_all_pages: 5/5
- suite/TestDashboardEngineEventMarkers: 8/8
- test_dashboard_toolbar_buttons: 4/4
- test_dashboard_multipage_render: 6/6

Pre-existing failures in test_dashboard_builder_interaction (5 of 23)
are out of scope and tracked in deferred-items.md — same failure set
exists on the pre-mhv baseline; mhv neither introduces nor fixes them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…toration

- PLAN.md captured upstream; checked in here to keep the quick-task folder self-contained
- SUMMARY: 5/5 mhv sub-tests pass; 47/47 across required regression sweep
- Three Rule-1 deviations (visibility cascade, drag/resize math, delete cascade) in service of the hCellPanel/hPanel split
- m52 row marked Superseded by mhv
- deferred-items.md tracks 5 pre-existing test_dashboard_builder_interaction failures verified out of scope

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mples

- Add private Constant PreviewRawThreshold_=100 on FastSenseWidget
- Gate both nBucketsEff sites in getPreviewSeries on the threshold so
  small / freshly-live datasets render at per-sample fidelity instead
  of the previous floor(numel(x)/2) downsampling that produced visibly
  stair-stepped slider preview lines during the first ~100 samples
- Pin the boundary with five new test cases in
  test_dashboard_preview_envelope.m (50, 100, 101, 500, 60-with-NaNs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oolbar

- Adds 3 RED test cases to tests/test_dashboard_toolbar_buttons.m:
  testResetButtonExists, testResetButtonTooltip, testResetReRendersWidgets
- Extends tooltip-loop test to include hResetBtn/'Reset'
- All 4 cases fail with 'unknown method or property: hResetBtn / onReset',
  confirming the RED step before implementation
- New hResetBtn property + button creation block placed between Sync and
  Events (rationale: visually adjacent to other 'fix the dashboard' actions
  but distinct in role — Sync resets time range, Reset re-renders widgets).
- New onReset() callback delegates to DashboardEngine.rerenderWidgets() and
  catches failures with namespaced 'DashboardToolbar:resetFailed' warning per
  CLAUDE.md error-ID conventions.
- Tooltip: 'Force re-render of all widgets on the active page (recovery
  action when widgets get stuck)'.
- Octave compatibility: uses isempty() guard instead of isvalid() (Octave
  has no isvalid for handle classes).
- All 7 toolbar tests pass on Octave; all 8 event-marker and 8 preview-overlay
  tests pass on MATLAB. Smoke check confirms panel handle is replaced after
  Reset and widget is re-realized.
…uick task

- Updates STATE.md Quick Tasks Completed table with 260508-ng1 entry
  (commit 5cbcc89, status Verified).
- Plan/Summary live under .planning/quick/260508-ng1-add-reset-button-to-dashboard-toolbar/
  (gitignored alongside other quick tasks per repo convention).
… sweep

- New tests/test_dashboard_switch_page_refresh.m with 4 cases:
  paints_histogram, paints_group_children, isolates_failure,
  repeated_marks_dirty.
- Adds tests/fixtures/ThrowingTextWidget.m to exercise per-widget
  refresh failure isolation in switchPage.
- The repeated_marks_dirty case fails today: when a widget's axes are
  emptied while its Dirty flag has self-cleared, switchPage does not
  repaint, leaving the axes blank. This locks in the bug 260508-ny6
  fixes.
HanSur94 added 6 commits May 8, 2026 17:24
…efreshes

Insert a two-pass markDirty + refresh sweep into DashboardEngine.switchPage
between realizeBatch and broadcastTimeRange:

- markDirty on the FLAT widget list (via flattenWidgetsForPreview_) so
  nested GroupWidget children are also re-armed.
- refresh on the TOP-LEVEL list so GroupWidget.refresh's existing
  recursion into Children/Tabs paints the now-dirty inner widgets.

Without this, widgets like HistogramWidget / HeatmapWidget / BarChartWidget
self-clear Dirty after their first paint and short-circuit on subsequent
tab switches, leaving axes empty until the next live tick.

Per-widget refresh failures are isolated with a try/catch + DebugPreview_-
gated warning (DashboardEngine:switchPageRefreshFailed), matching the
existing computePreviewEnvelope / computeEventMarkers pattern.

Verified via tests/test_dashboard_switch_page_refresh.m (4 cases) and
the regression sweep:
  - test_dashboard_preview_overlay (10/10)
  - test_dashboard_engine_event_markers (9/9)
  - test_dashboard_time_sync_all_pages (5/5)
  - test_dashboard_widget_button_bar (5 passed, 0 failed)
Document switchPage refresh-sweep fix in SUMMARY and append the ny6
row to the STATE.md Quick Tasks Completed table.
The ny6 sweep added a flat-markDirty + top-level-refresh pass over every
active-page widget on every switchPage. User re-tested and reports the
'widgets stuck after tab change' symptom unchanged, so the fix did NOT
solve the original bug — it only added per-tab redraw cost. Removing it
restores the snappier pre-ny6 redraw flow (visibility toggle + lazy
realizeBatch only).

- Delete the ny6 markDirty/refresh block from DashboardEngine.switchPage
  (between realizeBatch(5) and the LastSyncedTimeRange_ rebroadcast).
- Delete tests/test_dashboard_switch_page_refresh.m and the
  tests/fixtures/ThrowingTextWidget.m fixture (added solely for ny6).
- Update STATE.md: mark ny6 as 'Superseded by od4', append od4 row,
  refresh last_activity / last_updated.

Forward-fix (no git revert) so the commit graph captures the rollback
rationale. Dashboard regression sweep (preview_overlay, event_markers,
time_sync_all_pages, widget_button_bar, toolbar_buttons) all pass.
…d obj

Move the ~isvalid(obj) + figure/axes handle guards to the very top of
onFigureMove_, BEFORE the chained PrevWBMFcn_ invocation. Previously the
PrevWBMFcn_ access happened first (via obj.PrevWBMFcn_), so when the
HoverCrosshair was deleted but a stale closure was still installed on
the figure (Reset / rerenderWidgets path tears down widget panels but
the figure-level WindowButtonMotionFcn closure can fire in the brief
window before delete() runs, or be re-installed by an unrelated code
path), the closure threw "Invalid or deleted object." at the property
read, surfacing the user-reported stack trace.

With the guard hoisted, an invalidated obj causes a silent early return
— matching the existing intent of the delete() WBMFcn restore.

Add tests/test_hover_crosshair.m::test_invalid_obj_no_error exercising
exactly this path: construct, capture closure, delete(hc), re-install
captured closure, invoke. Pre-fix: throws. Post-fix: returns silently.

mh_lint: clean. Manually verified pre-fix throws, post-fix does not.

Note: tests/test_hover_crosshair.m::test_default_on has a pre-existing
failure on this branch unrelated to this change (FastSense.HoverCrosshair_
lifecycle, not onFigureMove_ guard). Out of scope per the rollback
charter; flagged in od4 SUMMARY.
Capture the rollback of ny6 (switchPage refresh sweep didn't fix the
user-reported stuck-widget symptom and added per-tab cost) and the
HoverCrosshair.onFigureMove_ invalid-object guard reorder. Append the
od4 row to the STATE.md Quick Tasks Completed table pointing at the
two task commits (6ef1a86 revert + 936feac fix), and write the SUMMARY
documenting evidence (sweep clean, lint clean, A/B repro of the
HoverCrosshair guard) plus deferred items (pre-existing test_default_on
failure, undiagnosed root cause of stuck-widget symptom).
@HanSur94 HanSur94 merged commit 939d902 into main May 8, 2026
4 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'FastSense Performance'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.10.

Benchmark suite Current: e45aa1a Previous: 939d902 Ratio
Render mean std(1M) 2.385 ms 1.788 ms 1.33
Downsample mean std(5M) 0.098 ms 0.085 ms 1.15
Instantiation mean std(5M) 2.34 ms 1.208 ms 1.94
Render mean std(5M) 2.159 ms 1.123 ms 1.92
Zoom cycle mean std(5M) 0.999 ms 0.539 ms 1.85
Downsample mean std10M) 0.088 ms 0.069 ms 1.28
Render mean std10M) 4.112 ms 1.92 ms 2.14
Zoom cycle mean std10M) 0.555 ms 0.47 ms 1.18
Downsample mean std50M) 0.118 ms 0.105 ms 1.12
Instantiation mean std50M) 28.51 ms 4.141 ms 6.88
Downsample mean ( std00M) 1.049 ms 0.232 ms 4.52
Render mean ( std00M) 6.844 ms 3.449 ms 1.98
Zoom cycle mean ( std00M) 0.627 ms 0.441 ms 1.42
Downsample mean ( std00M) 52.002 ms 0.232 ms 224.15
Instantiation mean (500M) 59554.29 ms 23392.075 ms 2.55
Instantiation mean ( std00M) 873.808 ms 72.082 ms 12.12
Render mean ( std00M) 26.831 ms 3.449 ms 7.78
Zoom cycle mean ( std00M) 0.833 ms 0.441 ms 1.89
Dashboard create+render stdmean 196.906 ms 36.17 ms 5.44
Dashboard live tick stdmean 0.481 ms 0.272 ms 1.77

This comment was automatically generated by workflow using github-action-benchmark.

CC: @HanSur94

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