Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Phase: 1040
Plan: Not started
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.
Status: Phase complete — ready for verification
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)
Last activity: 2026-06-02 - Completed quick task 260602-p2t: renamed dashboard toolbar "Reset" button to "Redraw" (the button forces a full widget re-render, not a reset)

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

Expand Down Expand Up @@ -97,6 +97,7 @@ Other main PRs (#138, #139, #141, #144, #145, #146) auto-merged without conflict
| 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/) |
| 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) |
| 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/) |
| 260602-p2t | Rename the dashboard top-bar (toolbar) button from "Reset" to "Redraw". The button forces a full re-render of every widget on the active page (`onReset` → `DashboardEngine.rerenderWidgets`), so "Reset" was misleading — the label now matches the behaviour (and the existing tooltip, "Force re-render of all widgets…"). Label-only change in `libs/Dashboard/DashboardToolbar.m`; internal handle/handler `hResetBtn`/`onReset` kept (not user-facing). The **time-panel** "Reset" button (`DashboardEngine.hTimeResetBtn` → `resetTimeRange`) is a genuine time-window reset and is NOT in the top bar — left unchanged. Synced `tests/test_dashboard_toolbar_buttons.m` (button-names list + label assertion `'Reset'`→`'Redraw'`). Verified R2025a (live MCP): `test_dashboard_toolbar_buttons` **7/7** (label, tooltip-mentions-widget, onReset re-render); MATLAB Code Analyzer no new findings on both files. Executed inline (single-label rename + test sync) with GSD bookkeeping — no separate planner/executor subagents. | 2026-06-02 | 84a5ec2 | Verified | [260602-p2t-rename-dashboard-toolbar-reset-button-to](./quick/260602-p2t-rename-dashboard-toolbar-reset-button-to/) |

## Progress Bar

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Quick Task 260602-p2t: Rename dashboard toolbar "Reset" button to "Redraw"

**Created:** 2026-06-02
**Mode:** quick (inline execution)

## Task Boundary

The dashboard top bar (toolbar) has a button labelled "Reset" whose callback
(`onReset` → `DashboardEngine.rerenderWidgets`) deletes and re-creates every
widget panel on the active page — i.e. it forces a full re-render. The label
"Reset" is misleading; rename the user-facing label to "Redraw" so the button's
purpose is clear.

Out of scope: the **time-panel** "Reset" button (`DashboardEngine.hTimeResetBtn`
→ `resetTimeRange`) is a genuine time-window reset and is NOT in the top bar —
leave it unchanged.

## Approach Note

This is a single-string user-facing rename with a matching test assertion. Given
the change was fully scoped during investigation, it was executed inline (no
separate planner/executor subagents) while preserving GSD guarantees: scoped
artifacts, STATE.md tracking, and an atomic commit.

## Tasks

### Task 1 — Rename the toolbar button label

- **Files:** `libs/Dashboard/DashboardToolbar.m`
- **Action:** Change the toolbar push-button `'String'` from `'Reset'` to
`'Redraw'` (~line 163). Update the adjacent explanatory comment to use the new
label and note that the internal handle/handler (`hResetBtn`/`onReset`) keep
their historical names. Leave the tooltip ("Force re-render of all widgets…")
unchanged — it already describes the redraw behaviour.
- **Verify:** Static analysis clean (no new issues); button label reads
"Redraw" after render.
- **Done:** Toolbar shows "Redraw"; internal names and behaviour untouched.

### Task 2 — Sync the toolbar-button test

- **Files:** `tests/test_dashboard_toolbar_buttons.m`
- **Action:** Update the button-names list (`'Reset'` → `'Redraw'`) and the
label assertion (`strcmp(..., 'Reset')` → `'Redraw'`) plus its comment/message.
- **Verify:** `test_dashboard_toolbar_buttons` passes (7/7).
- **Done:** Test asserts the label is "Redraw" and is green.

## must_haves

- truths:
- The toolbar button that triggers a full widget re-render is labelled "Redraw".
- The time-panel "Reset" button (`resetTimeRange`) is unchanged.
- Internal handle `hResetBtn` and handler `onReset` are unchanged.
- artifacts:
- `libs/Dashboard/DashboardToolbar.m` (label = "Redraw")
- `tests/test_dashboard_toolbar_buttons.m` (asserts "Redraw")
- key_links:
- `libs/Dashboard/DashboardToolbar.m` onReset → DashboardEngine.rerenderWidgets
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Quick Task 260602-p2t: Summary

**Completed:** 2026-06-02
**Status:** Done — verified (7/7 tests pass)

## What changed

Renamed the dashboard top-bar (toolbar) button from **"Reset" → "Redraw"** so the
label matches what the button actually does: force a full re-render of every
widget on the active page (a manual recovery action).

### Files

- **`libs/Dashboard/DashboardToolbar.m`**
- Button `'String'`: `'Reset'` → `'Redraw'` (the toolbar push-button wired to
`onReset` → `DashboardEngine.rerenderWidgets`).
- Updated the adjacent comment to use the new label and to note that the
internal handle/handler (`hResetBtn` / `onReset`) intentionally keep their
historical names — only the user-facing label changed.
- Tooltip left unchanged ("Force re-render of all widgets on the active page…")
— it already describes the redraw behaviour.

- **`tests/test_dashboard_toolbar_buttons.m`**
- Button-names list: `'Reset'` → `'Redraw'`.
- Label assertion: `strcmp(get(hResetBtn,'String'), 'Reset')` → `'Redraw'`,
plus comment/message wording.

## Deliberately NOT changed

- **Time-panel "Reset" button** (`DashboardEngine.hTimeResetBtn` →
`resetTimeRange`): this is a genuine time-window reset and is on the time
panel, not the top bar. Left as "Reset" — renaming it would be wrong.
- **Internal names** `hResetBtn` / `onReset`: not user-facing; kept to avoid
rippling changes through `DashboardEngine.m` and the test suite for no user
benefit.

## Verification

- `mh`-style static analysis (MATLAB Code Analyzer) on both files: no new issues
(only pre-existing `info`-level suggestions outside the edited lines).
- `test_dashboard_toolbar_buttons`: **7 passed, 0 failed**, including:
- label assertion now expects "Redraw" ✓
- tooltip still mentions "widget" ✓
- `onReset()` still re-renders widgets (replaces panel handle) ✓

## Notes

Executed inline rather than via separate planner/executor subagents: the change
was a fully-scoped single-label rename + matching test sync, so the subagent
orchestration would have added cost/worktree complexity with no benefit. GSD
guarantees preserved (scoped artifacts, STATE.md tracking, atomic commit).
8 changes: 5 additions & 3 deletions libs/Dashboard/DashboardToolbar.m
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,18 @@
'TooltipString', 'Reset all widgets to global time range', ...
'Callback', @(~,~) obj.Engine.resetGlobalTime());

% Reset button — manual recovery; forces full re-render of every
% Redraw button — manual recovery; forces full re-render of every
% widget on the active page. Placed next to Sync because both
% are "fix the dashboard" actions, but their roles differ:
% Sync resets the time range; Reset re-renders widget panels.
% Sync resets the time range; Redraw re-renders widget panels.
% (Internal handle/handler keep the historical hResetBtn/onReset
% names — only the user-facing label was renamed to "Redraw".)
rightEdge = rightEdge - btnW - 0.005;
obj.hResetBtn = uicontrol('Parent', obj.hPanel, ...
'Style', 'pushbutton', ...
'Units', 'normalized', ...
'Position', [rightEdge btnY btnW btnH], ...
'String', 'Reset', ...
'String', 'Redraw', ...
'TooltipString', ['Force re-render of all widgets on the active page ' ...
'(recovery action when widgets get stuck)'], ...
'Callback', @(~,~) obj.onReset());
Expand Down
12 changes: 6 additions & 6 deletions tests/test_dashboard_toolbar_buttons.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function test_dashboard_toolbar_buttons()
d.Toolbar.hExportBtn, d.Toolbar.hSyncBtn, ...
d.Toolbar.hResetBtn, ...
d.Toolbar.hInfoBtn};
names = {'Live', 'Config', 'Image', 'Export', 'Sync', 'Reset', 'Info'};
names = {'Live', 'Config', 'Image', 'Export', 'Sync', 'Redraw', 'Info'};
for i = 1:numel(handles)
tip = get(handles{i}, 'TooltipString');
assert(~isempty(tip), ...
Expand Down Expand Up @@ -100,18 +100,18 @@ function test_dashboard_toolbar_buttons()
nFailed = nFailed + 1;
end

% Reset button is created and labelled "Reset" after render
% Redraw button is created and labelled "Redraw" after render
try
d = DashboardEngine('ResetButtonExistsTest');
d.addWidget('number', 'Title', 'T', 'Position', [1 1 6 2], 'StaticValue', 1);
d.render();
set(d.hFigure, 'Visible', 'off');
assert(~isempty(d.Toolbar.hResetBtn), ...
'reset button should exist after render');
'redraw button should exist after render');
assert(ishandle(d.Toolbar.hResetBtn), ...
'reset button should be a valid handle');
assert(strcmp(get(d.Toolbar.hResetBtn, 'String'), 'Reset'), ...
'reset button label should be "Reset"');
'redraw button should be a valid handle');
assert(strcmp(get(d.Toolbar.hResetBtn, 'String'), 'Redraw'), ...
'redraw button label should be "Redraw"');
close(d.hFigure);
nPassed = nPassed + 1;
catch err
Expand Down
Loading