Skip to content

Commit 87015be

Browse files
HanSur94claude
andauthored
Add crosshair-link toggle to FastSense dashboard widgets (260602-mri) (#184)
* feat(260602-mri-01): CrosshairLinked property + HoverCrosshair broadcast hook - FastSenseWidget: add CrosshairLinked=false public property, setCrosshairLink(tf) setter (validates logical/0/1, throws FastSenseWidget:invalidCrosshairLink), toStruct omits field when false (legacy JSON byte-identical), fromStruct restores flag pre-render (no graphics touch) - HoverCrosshair: add BroadcastFcn_/BroadcastLeaveFcn_ callbacks + setBroadcastFcn; onMoveExternal(x) sets SuppressLeaveUntil_=tic then drives onMove with InBroadcast_=true; onLeaveExternal() clears suppress + hides; onMove fires BroadcastFcn_ at tail; onLeave honors SuppressWindow_ guard and fires BroadcastLeaveFcn_ when source leaves; delete() nulls callbacks first - tests/test_fastsense_crosshair_link.m: 11 cases (6 PURE + 1 pure-engine + 2 render-guarded suppress-leave proof + broadcast-reentry proof + 2-widget mirror integration) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(260602-mri-02): DashboardEngine active-page crosshair-link coordination - collectLinkedCrosshairs_(widgets): pure flattening helper (public); returns cell of {widget,hc} structs for linked+rendered FastSenseWidgets - rewireCrosshairLinks_(): clears all active-page broadcast hooks then re-primes linked crosshairs with engine broadcast closures - broadcastCrosshairX_(sourceHc, x): mirrors data-x onto all OTHER linked crosshairs via onMoveExternal - broadcastCrosshairLeave_(sourceHc): tells all OTHER linked crosshairs to hide via onLeaveExternal - onCrosshairLinkToggle(widget): re-derives full active-page link set - rewireCrosshairLinks_ called after rerenderWidgets, switchPage, detachWidget (260512-eu2 re-establish-after-rerender lesson) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(260602-mri-03): Crosshair-link 'X' toggle button on WidgetButtonBar - DashboardLayout.realizeWidget: inject CrosshairLinkButton via ismethod(widget,'setCrosshairLink') duck-type after addPlantLogToggle, before final reflowChrome_ - addCrosshairLinkToggle(widget): idempotent 24x24 'X' pushbutton; highlighted when CrosshairLinked=true via chooseYLimitActiveBg_; calls onCrosshairLinkTogglePressed_ on click; tails with reflowChrome_ for callback-driven rebuilds - onCrosshairLinkTogglePressed_: toggles CrosshairLinked, calls EngineRef.onCrosshairLinkToggle, rebuilds button look; try/catch + warning + non-blocking uialert mirrors addPlantLogToggle pattern - reflowChrome_: re-anchors CrosshairLinkButton as LEFTMOST chrome button (xVisible - gap - bw, left of V/A cluster) after resize - DashboardWidget.clearPanelControls: add 'CrosshairLinkButton' to protectedTags so button survives re-render sweeps Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(260602-mri-01): complete crosshair-link plan SUMMARY Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(260602-mri-04): deterministic crosshair-link suppress + leave-recursion fix Orchestrator live-MATLAB verification found the suppress-leave tic-window (SuppressLeaveUntil_/SuppressWindow_) was flaky: it depends on wall-clock elapsed time inside a synchronous motion dispatch, so the crux test passed in isolation (6.7ms) but failed in the full suite. Replaced with a deterministic IsMirrored_ boolean - set by onMoveExternal, cleared when the widget becomes the hover source (real onMove) or via onLeaveExternal. Also fixes a latent unbounded leave ping-pong: onLeaveExternal now hides directly via a new private hideGraphics_ helper instead of re-entering the broadcasting onLeave (which would recurse across >=2 linked widgets and hang). Test fixes: collectLinkedCrosshairs_ enumeration passes the widget list as a parameter, and the 2-widget integration test uses addWidget(), since DashboardEngine.Widgets is SetAccess=private. Verified R2025a: test_fastsense_crosshair_link 11/11; regressions green (test_hover_crosshair 11/11, test_fastsense_widget_ylimit_modes 11/11, test_time_range_selector_reinstall_after_rerender pass, test_dashboard_time_sync_all_pages 5/5); MISS_HIT clean; live UI smoke pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(quick-260602-mri): crosshair-link toggle — plan, summary, state Quick task 260602-mri complete and verified (R2025a live MATLAB): a crosshair-link 'X' toggle on the FastSense widget grey bar mirrors the hover crosshair across all FastSense widgets on the active dashboard page. - PLAN.md: the executable plan (3 tasks) — now tracked - SUMMARY.md: updated for the deterministic IsMirrored_ suppress redesign, the latent leave-recursion fix, and the green test/lint/live-smoke results - STATE.md: Quick Tasks Completed row + last-activity line Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent be70d4b commit 87015be

9 files changed

Lines changed: 1197 additions & 4 deletions

File tree

.planning/STATE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Phase: 1040
2727
Plan: Not started
2828
Milestone: v3.0 FastSense Companion — SHIPPED 2026-04-30; v4.0 Multi-User LAN Concurrency — shipping via PR #152 (parallel branch); v1.0 perf line tracks phase 1028 — now COMPLETE via PR #114.
2929
Status: Phase complete — ready for verification
30-
Last activity: 2026-06-02
30+
Last activity: 2026-06-02 - Completed quick task 260602-mri: crosshair-link toggle on the FastSense widget bar (mirrors the hover crosshair across active-page FastSense widgets)
3131

3232
### Note on parallel v4.0 work (main branch state)
3333

@@ -96,6 +96,7 @@ Other main PRs (#138, #139, #141, #144, #145, #146) auto-merged without conflict
9696
| 260526-pqz | Raise per-signal slider-preview cap from 400 → 1000 buckets in `DashboardEngine.computePreviewEnvelopeReturning_` — three textual edits (1 code clamp + 2 documenting comments) in `libs/Dashboard/DashboardEngine.m` plus one consistency comment in `tests/test_dashboard_preview_overlay.m` (no assertion change; `numel(xd) >= 4` is cap-independent). Edit sites: line 3524 doc-comment (`computePreviewEnvelope` range), line 3542 inline comment (clamp range), line 3555 actual clamp `max(50, min(1000, floor(axWpx / 2)))`. Out of scope per plan: cache invalidation of `PreviewNBuckets_` — running demos must restart (or trigger the existing resize-invalidation path at `DashboardEngine.m:2241`) for the new cap to take effect. Static analysis clean: `mh_lint` + `mh_style` on both edited files report "everything seems fine"; regression sweep `grep -rn "\b400\b" tests/ \| grep -iE "(preview\|bucket\|envelope)"` returns no matches. MATLAB R2025a: `test_dashboard_preview_envelope` 7/7, `test_dashboard_preview_overlay` 10/10. Octave 11.1.0: `test_dashboard_preview_envelope` 2/2 (5 skipped — pre-existing TimeRangeSelector guard for patch+FaceAlpha+NaN on xvfb), `test_dashboard_preview_overlay` skipped entirely (pre-existing). | 2026-05-26 | 834b43c | — | [260526-pqz-raise-preview-line-cap-per-signal-from-4](./quick/260526-pqz-raise-preview-line-cap-per-signal-from-4/) |
9797
| 260529-rxf | Real per-event email alerts for background monitoring — new `EmailTransport` (SMTP auth/STARTTLS:587 default, also `none`/`ssl`; Octave `exist('sendmail','file')` log-and-skip guard; pure static `buildMailProps` CI seam) that `NotificationService` now delegates to via an injectable `Transport` property; per-(sensor,threshold) email cooldown (default 5 min, 0 disables; dry-run honors it too) with public `SuppressedCount`; `LiveEventPipeline.processMonitorTag_`/`runCycle` now forward real per-event `sensorData` (X/Y/thresholdValue/thresholdDirection from the live tick) so `IncludeSnapshot` rules attach PNGs in live mode. MATLAB-only per user decision. **Backward-compat preserved**: pipeline still defaults to `NotificationService('DryRun', true)` and all prior tests stay green. Verified locally (R2025a, live MATLAB MCP): `test_email_transport` 5/5, `test_notification_service` 10/10 (7 original + 3 new: delegation / cooldown-suppress / cooldown-expiry-via-Hidden-DI-seam), `test_live_event_pipeline_tag` 3/3, plus class suites `TestEmailTransport` 5/5, `TestNotificationService` 7/7, `TestLiveEventPipelineTag` 3/3. MISS_HIT (`mh_style`+`mh_lint`) clean on all 8 files; MATLAB Code Analyzer clean on the 3 new/edited libs. Real SMTP delivery is the single manual step via `examples/05-events/smoke_email_send.m` (FASTSENSE_SMTP_* env vars, STARTTLS:587), out of CI. | 2026-05-29 | 203da7a, 2ac6887, 341bab2, cef1fc5 | Verified | [260529-rxf-real-per-event-email-alerts-for-backgrou](./quick/260529-rxf-real-per-event-email-alerts-for-backgrou/) |
9898
| 260529-fnt | Add `FunctionTransport` adapter (`libs/EventDetection/FunctionTransport.m`) — wraps a user-supplied function handle as a `NotificationService` `Transport` so an existing site/company MATLAB mailer can be reused for alerts with **no SMTP config** (no server/port/creds, no Gmail App Password). Drop-in duck-typed `send(recipients,subject,body,attachments)` (same as EmailTransport), normalizes recipients to a flat cellstr, defaults attachments to `{}`, Octave-safe (only calls user code). Purely additive — EmailTransport/NotificationService behavior unchanged. Built via **/gsd:fast** (inline, no subagents). Verified (R2025a): `test_function_transport` 5/5 (forwarding / recipients-normalization / attachments-default / invalid-handle / NotificationService integration), `test_notification_service` 10/10 (no regression); MISS_HIT + Code Analyzer clean on all touched files. `example_live_pipeline.m` gains a commented FunctionTransport option. Follow-up to 260529-rxf after the user opted to reuse their company mailer instead of configuring Gmail SMTP. | 2026-05-29 | 706e9d5 | Verified | (inline) |
99+
| 260602-mri | Add a crosshair-link toggle ('X') to the FastSenseWidget grey WidgetButtonBar that mirrors the hover crosshair across all FastSense widgets on the **active dashboard page**. New `CrosshairLinked` public property + `setCrosshairLink(tf)` on `FastSenseWidget` (default OFF; `toStruct` omits when false so legacy serialized dashboards are byte-identical; `fromStruct` restores). `HoverCrosshair` gains `setBroadcastFcn`/`onMoveExternal`/`onLeaveExternal` + a **deterministic `IsMirrored_` suppress flag** so a mirrored peer's same-dispatch self-leave is a no-op — **ZERO new figure-WBM closures**, respecting the 260512-egv/eu2 chained-WBM constraint; the broadcast rides on existing per-crosshair `onMove`/`onLeave` and each peer computes its OWN per-series datatip at the shared data-x via `computeYAtX_` (raw-x, no Y transmitted). `DashboardEngine` derives the active-page link set on demand (`collectLinkedCrosshairs_`, flattens GroupWidget children via `flattenWidgetsForPreview_`) through `broadcastCrosshairX_`/`broadcastCrosshairLeave_`/`onCrosshairLinkToggle`, and re-primes broadcast hooks via `rewireCrosshairLinks_` after `rerenderWidgets`/`switchPage`/`detachWidget`. Duck-typed 'X' button injected in `DashboardLayout.realizeWidget` via `ismethod(widget,'setCrosshairLink')` (leftmost chrome button, left of V/A), re-anchored by `reflowChrome_`, protected in `DashboardWidget.clearPanelControls`. **Orchestrator verification (R2025a, live MCP)** replaced the executor's flaky `tic`-window suppress (`SuppressLeaveUntil_`/`SuppressWindow_` — passed in isolation, failed in-suite) with the deterministic `IsMirrored_` flag, and fixed a **latent unbounded leave ping-pong** (`onLeaveExternal` now hides directly via a private `hideGraphics_` instead of re-entering broadcasting `onLeave`). Tests: new `test_fastsense_crosshair_link` **11/11**; regressions green (`test_hover_crosshair` 11/11, `test_fastsense_widget_ylimit_modes` 11/11, `test_time_range_selector_reinstall_after_rerender` pass, `test_dashboard_time_sync_all_pages` 5/5); MISS_HIT clean on all 6 files; Code Analyzer no new findings; live UI smoke on a rendered 2-widget dashboard (button renders + toggles, hovering one linked widget mirrors crosshair+datatip on the other, leave hides both, unlink stops mirroring). DetachedMirror crosshair-link parity OUT OF SCOPE (detached widgets use a figure-level `FastSenseToolbar`, not a WidgetButtonBar — matches 260513-sfp). | 2026-06-02 | a495cbc9, 635632e2, 8950abd5, 485154b7 | Verified | [260602-mri-add-crosshair-link-toggle-to-fastsense-w](./quick/260602-mri-add-crosshair-link-toggle-to-fastsense-w/) |
99100

100101
## Progress Bar
101102

0 commit comments

Comments
 (0)