Skip to content

Phase 1012: Live event markers + click-to-details on FastSense/FastSenseWidget#77

Merged
HanSur94 merged 50 commits into
mainfrom
claude/distracted-kalam-809418
Apr 24, 2026
Merged

Phase 1012: Live event markers + click-to-details on FastSense/FastSenseWidget#77
HanSur94 merged 50 commits into
mainfrom
claude/distracted-kalam-809418

Conversation

@HanSur94
Copy link
Copy Markdown
Owner

Summary

Phase 1012 extension of v2.0 Tag-Based Domain Model — ships live event markers with per-marker click-to-details on FastSense and FastSenseWidget, plus editable persistent notes per event.

Tagged as v2.0.1 (extension on top of the v2.0 ship point).

What's new

  • Open-event schemaEvent.IsOpen logical property + EventStore.closeEvent(id, endTime, finalStats) in-place update. Events become visible as hollow markers the moment they are detected; transition to filled when closed.
  • MonitorTag rising-edge emission — running Peak/Min/Max/Mean/RMS/Std accumulated in cache_.openStats_ during live ticks; falling edge calls closeEvent with the final stats.
  • TrendMiner-style markers — white circular badge with soft drop shadow (two semi-transparent scatter disks) + severity-colored ! glyph. Severity→color mapping: info→green, warn→orange, alarm→red.
  • Click-to-details popup — standalone figure (OS-native drag + close), light theme, section-grouped uitable field listing with resize-aware columns (1:2 split between Field and Value), editable Notes textarea with Save notes button that mutates Event.Notes and calls EventStore.save() for disk persistence.
  • FastSenseWidget wiring — new ShowEventMarkers (default false) and EventStore properties; opt-in forwarding guard if obj.ShowEventMarkers || ~isempty(obj.EventStore) preserves Phase-1010's FastSense.ShowEventMarkers=true default for bare-FastSense users. Live-tick marker diff against LastEventIds_/LastEventOpen_/LastEventSeverity_ triggers re-render on adds, removes, open→closed transitions, and severity bumps.
  • New demoexamples/example_event_markers.m with two sensors (pump sustained violation + motor multi-spike) sharing one disk-backed EventStore, demonstrating the full open→close lifecycle with severity-assigned colors.

Test plan

  • tests/suite/TestEventIsOpen.m — Event schema + EventStore.closeEvent + backward-compat .mat reload
  • tests/suite/TestMonitorTagOpenEvent.m — rising-edge IsOpen=true emission + falling-edge closeEvent + running stats
  • tests/suite/TestFastSenseEventClick.m — per-marker ButtonDownFcn + UserData.eventId + open/closed marker face color + popup lifecycle
  • tests/suite/TestFastSenseWidgetEventMarkers.m — widget property wiring + toStruct/fromStruct round-trip + marker diff
  • Octave mirrors for all four (tests/test_*.m)
  • tests/test_monitortag_streaming.m updated to Phase-1012 open-event semantics (was asserting pre-1012 EndTime=10)
  • benchmarks/bench_event_marker_regression.m — zero-event 12-line FastSense render Pitfall-10 gate (≤5% regression) — landed at -4.53% / -1.32%
  • Phase-1010 regression test tests/test_fastsense_event_overlay.m still green (ShowEventMarkers default preserved)
  • Manual UAT (4/4 passed): panel anchor, zoom-mode dismiss, hollow→filled transition, multi-widget scenario

Merge conflicts expected

main has advanced independently since this branch was cut. The following 3 files will need manual conflict resolution:

  • libs/FastSense/FastSense.m — integrate 1012's renderEventLayer_ refactor with main's LiveViewMode / time-axis / widget-rework additions
  • libs/Dashboard/FastSenseWidget.m — integrate 1012's ShowEventMarkers/EventStore/marker-diff properties with main's LiveViewMode/UserZoomedY/autoScaleY_/formatTimeAxis_
  • libs/Dashboard/DashboardTheme.m — integrate 1012's EventMarkerSize=8 constant with main's Phase-1015 theme trim

Other 15 files (Event.m, EventStore.m, MonitorTag.m + all tests + bench + example) have no overlap with main.

Notes

  • This PR will contain .planning/ artifacts (17 commits across plan/context/research/validation/verification/summary MD files). They're safe to ignore or filter on review — the code review focus should be on the libs/ + tests/ + examples/ + benchmarks/ changes.

🤖 Generated with Claude Code

HanSur94 and others added 30 commits April 24, 2026 08:52
…ntMarkers guard, protected formatEventFields_)
…N endTime guard

- Add IsOpen = false public property (Phase 1012 open-event schema)
- Add close(endTime, finalStats) method delegating private-field mutation (D1 SSOT)
- Relax constructor endTime guard to ~isnan(endTime) && endTime < startTime
- Event:closedOpenEvent error ID for double-close guard
- closeEvent(eventId, endTime, finalStats) locates event by Id
- Delegates in-place mutation to ev.close() (D1 SSOT)
- EventStore:unknownEventId for missing event (empty store and not-found)
- EventStore:alreadyClosed for non-open event
- Does NOT call save() — Pitfall 2 discipline preserved
- 12 tests covering IsOpen default/writable, NaN endTime, close(), double-close error
- EventStore.closeEvent in-place update, unknown-id, already-closed, empty-store
- Backward-compat round-trip via builtin save/load proving default-on-read
- TestMonitorTagOpenEvent: 4 assumeFail stubs for Plan 1012-02 MonitorTag wiring
- TestFastSenseEventClick: 8 assumeFail stubs for Plan 1012-03 click surface
- TestFastSenseWidgetEventMarkers: 8 assumeFail stubs for Plan 1012-03 widget wiring
- Octave flat-style mirrors (test_monitortag_open_event, test_fastsense_event_click, test_fastsense_widget_event_markers)
- bench_event_marker_regression: Pitfall-10 gate with 3 configs (none/empty/otherTags) +/-5% threshold
- 1012-01-SUMMARY.md: Event.IsOpen + close() + EventStore.closeEvent + 9 Wave 0 files
- STATE.md: advanced to plan 2/3, recorded decisions, updated session
- ROADMAP.md: 1012 phase in-progress (1/3 summaries)
… paths

- Add emptyOpenStats_() static private helper returning zero-struct for
  nPoints/sumY/sumYSq/maxY/minY/peakAbs/firstT/lastT accumulator fields
- recompute_() empty-parent early-out: gains openStats_ + openEventId_ = ''
- recompute_() main cache-write block: gains openStats_ + openEventId_ = ''
- tryLoadFromDisk_(): gains openStats_ + openEventId_ = '' (cold reload
  cannot reconstruct open-run state; emptyOpenStats_ is the correct safe default)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, accumulate running stats

- updateOpenStats_(xSlice, ySlice): O(chunk) incremental accumulator for nPoints/
  sumY/sumYSq/maxY/minY/peakAbs/firstT/lastT — never O(run-length)
- flushOpenStats_(): converts accumulator to finalStats struct for EventStore.closeEvent
- fireEventsInTail_: Part 1 closes open event via closeEvent on falling edge;
  Part 2 emits IsOpen=true event for trailing open runs (was `continue` pre-phase)
- appendData: updates openStats_ with raw sensor values (newY, not raw_new boolean)
  BEFORE fire call; backfills stats for newly-seeded open events post-fire
- fireEventsOnRisingEdges_: emits IsOpen=true for trailing open run (recompute parity);
  skips trailing run in closed-loop to avoid duplicate emission
- recompute_: seeds openEventId_/openStats_ before calling emitter; preserves any
  values set by fireEventsOnRisingEdges_ in the final cache_ assignment
- TestMonitorTagStreaming/testAppendOngoingRunExtendsIntoTail updated to reflect
  Phase 1012 semantics: 1 event opened+closed via closeEvent (not 2 separate events)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests (MATLAB + Octave)

- TestMonitorTagOpenEvent.m: 7 real tests (replaces 4 assumeFail stubs)
  rising-edge IsOpen=true emission, open event Id in store, falling-edge
  closeEvent, running stats across 3 ticks, stats finalized on same-chunk
  close, reset and new event on second open run, short-circuit preservation
- test_monitortag_open_event.m: Octave mirror (7 inline try/catch, no nested
  functions for Octave compat); all 7 pass on Octave 11.1.0 ARM64
- MonitorTag.m fixes for test-driven issues:
  * fireEventsInTail_ now accepts optional newY param for same-chunk closed event stats
  * Case (b) falling edge: bin_new(1)==0 when priorLastFlag==1 closes open event at
    cache_.x(end) (chunk-boundary falling edge)
  * Inline stats computation (setStats) for same-chunk completed events
  * recompute_ seeds openEventId_/openStats_ before calling fireEventsOnRisingEdges_
    and preserves emitter-set values in final cache_ struct assignment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rTag plan

- SUMMARY.md: dispatch points, 5 auto-fixed deviations, self-check PASSED
- STATE.md: advanced to plan 03, recorded metrics + 3 key decisions
- ROADMAP.md: updated phase 1012 progress (2/3 summaries complete)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…entLayer_ to per-event line()

- DashboardTheme.EventMarkerSize = 8 pt constant added to shared defaults block
- renderEventLayer_ refactored from severity-batched (3 line() calls) to per-event (one line() per event)
- Each marker carries its own ButtonDownFcn + UserData{eventId, tagKey}
- Open events render hollow (MarkerFaceColor='none'); closed events render filled
- onEventMarkerClick_ dispatcher added: resolves eventId -> Event via EventByIdMap_
- refreshEventLayer() public thin wrapper added for external consumers (FastSenseWidget)
- hEventDetails_/PrevWBDFcn_/PrevKPFcn_/EventByIdMap_ private properties added
- uistack guard wrapped in try/catch for Octave compat (Pitfall F)
…ck-details surface

- openEventDetails_ creates floating uipanel anchored near clicked marker
  (DashboardLayout.openInfoPopup template adapted to uipanel-in-figure)
- closeEventDetails_ restores prior WindowButtonDownFcn + WindowKeyPressFcn
- onFigureClickForDetailsDismiss_ parent-walk for click-outside detection
- onKeyPressForDetailsDismiss_ closes panel on ESC key
- computeDetailsPanelAnchor_ normalizes marker data-coords to figure space;
  clamps to [0 0 1 1] so panel never renders half-off-screen (Pitfall D)
- formatEventFields_ in new methods(Access=protected) block per WARNING 3:
  TestFastSenseEventClick.testFormatEventFieldsShowsOpenForOpenEvent calls
  it from outside the class; protected allows this without exposing to prod code
- Open events show "Open" for EndTime and Duration in the details dump
…esh() marker-diff

- ShowEventMarkers = false (back-compat default); EventStore = [] public properties
- LastEventIds_ + LastEventOpen_ private cache for marker-diff in refresh/update
- render() + rebuildForTag_() forward ShowEventMarkers/EventStore with BLOCKER 1 guard:
  only forward when widget has opted in (ShowEventMarkers=true OR EventStore non-empty)
  so Phase-1010 FastSense.ShowEventMarkers=true default is preserved for bypassed access
- refresh() + update() now call refreshEventMarkers_() after data update
- refreshEventMarkers_() diffs LastEventIds_ against current EventStore state;
  triggers FastSense.refreshEventLayer() on added/removed events or open->closed transitions
- toStruct() emits showEventMarkers only when true (backward-compat JSON)
- fromStruct() re-hydrates ShowEventMarkers; EventStore intentionally NOT serialized
…idget markers

TestFastSenseEventClick (8 tests):
  - testPerMarkerButtonDownFcnIsSet: verifies ButtonDownFcn is a function_handle
  - testUserDataHoldsEventId: verifies UserData.eventId matches Event.Id
  - testOpenEventMarkerIsHollow: verifies MarkerFaceColor='none' for IsOpen=true
  - testClosedEventMarkerIsFilled: verifies MarkerFaceColor != 'none' for closed
  - testClickOpensDetailsPanel: JVM-gated, direct onEventMarkerClick_ dispatch
  - testEscDismissesDetailsPanel: JVM-gated, ESC keypress via struct
  - testXButtonDismissesDetailsPanel: JVM-gated, closeEventDetails_() call
  - testFormatEventFieldsShowsOpenForOpenEvent: protected method access test

TestFastSenseWidgetEventMarkers (12 tests):
  - 2 default-property tests (no JVM)
  - testGuardPreservesInnerDefaultWhenWidgetDefault: BLOCKER 1 Option A coverage
  - 3 serializer tests (no JVM)
  - 6 render/refresh tests (JVM-gated)

Octave mirrors: inline fixture setup to avoid Octave SIGILL on nested functions
…ull flow

- Simulates rising edge (hollow marker at t=7) and falling edge (filled at t=12)
- Wires SensorTag + MonitorTag + EventStore + FastSenseWidget.ShowEventMarkers=true
- Calls appendData + onLiveTick to demonstrate live-tick marker diff
- Click-to-details panel available for interactive exploration

Pitfall-10 bench result:
  Config A (no store):     256.08 ms
  Config B (empty store):  255.15 ms (-0.36%)
  Config C (other tags):   264.61 ms (+3.33%)
  PASS: all within 5% gate
…ccess compat

EventMarkerHandles_ is Access=private on FastSense; accessing it from test code
causes 'property has private access' in Octave. Switch both Octave mirror and
MATLAB TestFastSenseEventClick to use findall(fig, 'Type', 'line') filtered
by Marker='o' and LineStyle='none' for portable marker discovery.

Also expose makeFixture return value as [fp, ev, fig] so tests can clean up
the figure after each test case.
…ker wiring plan

- SUMMARY.md: 5 tasks, 6 commits, 8 files, all self-checks passed
- STATE.md: advanced to last_plan, 100% progress, 4 decisions recorded
- ROADMAP.md: phase 1012 marked Complete (3/3 plans)
…012 open-event semantics

Verifier gap: test_monitortag_streaming.m was not in Plan 02 files_modified
but Scenario 2 assertions on EndTime==10 and numel(e2)==2 reflected the
Phase-1007 closed-event-only semantics. Phase 1012 changed recompute_ to
emit IsOpen=true with EndTime=NaN, and closeEvent updates in place rather
than appending a second event. Brought mirror in line with MATLAB suite
TestMonitorTagStreaming.testAppendOngoingRunExtendsIntoTail (lines 70-99).

All 7 streaming tests pass under Octave.
…arkers

The Plan 03 executor wrote DashboardEngine('Title', 'Phase 1012 demo') but
the constructor's NV options are {Name, Theme, LiveInterval, InfoFile, ...}.
Title is valid on widgets (addWidget('fastsense', 'Title', ...)) which is
why the widget call is correct. Surfaced by the user running the example.

Ran into user's live terminal session — cannot be caught by static syntax
checks, only by actually calling DashboardEngine().
Previous fix (148b7e5) corrected 'Title' -> 'Name' but the constructor
signature is DashboardEngine(name, varargin) — first arg is positional,
varargin starts at the second position with NV pairs. Passing 'Name' as
a key caused the varargin loop to read 'Name' as the value of some key
and the next string 'Phase 1012 demo' as an unknown-option key.

Surfaced by user re-running example_event_markers after commit 148b7e5.
SensorTag only has updateData(X, Y) which replaces the full trace.
MonitorTag has appendData(newX, newY) for incremental streaming.
Example had them confused. Now:
  parent.updateData([parent.X, newX], [parent.Y, newY])   % concat + replace
  mon.appendData(newX, newY)                              % incremental

Surfaced by user running example_event_markers after commit e8ab995.
Phase 1000's incremental refresh preserves initial ylim so re-renders stay
stable under user zoom. In this demo the data range expands from y=1 (flat)
to y=10 (peak) during the first live tick — without auto-rescaling, the peak
and event marker sit outside the cached ylim and are invisible.

Added a local autoscaleY(d) helper that iterates d.Widgets, finds
FastSenseWidget instances, and calls ylim(ax, 'auto') on each inner axes
after every onLiveTick.
…ple threshold overlay

1. Remove black outline on the click-details uipanel
   BorderType: 'line' -> 'none' (line 2312). Keeps the rounded dark-grey
   background visually clean.

2. Make the details panel floatable (drag by title bar)
   Title uicontrol now has Enable='inactive' + ButtonDownFcn wiring. New
   private methods beginDetailsDrag_ / onDetailsDragMove_ / endDetailsDrag_
   and three state properties (PrevWBMFcn_, PrevWBUFcn_, DragOffsetPx_)
   implement classic grab-and-move behavior:
     - ButtonDown on title captures mouse offset relative to panel origin
     - WindowButtonMotionFcn tracks mouse and repositions panel (pixel units
       for sub-pixel accuracy, then restored to the previous Units)
     - WindowButtonUpFcn restores the saved motion/up handlers
   closeEventDetails_ also clears any stuck drag handlers defensively so a
   dismissed-mid-drag panel cannot leave dangling figure callbacks.

3. Show threshold in example_event_markers
   After d.render(), draw a dashed horizontal line + label at y=5 directly
   on the inner FastSense axes (tagged 'demoThreshold' / 'demoThresholdLabel').
   autoscaleY(d) now also updates the line's XData and the label's X
   position when the axes x-range grows during live ticks, so the reference
   line always spans the full plot.
…ders

User report: uipanel approach still showed heavy black borders on macOS
and the title-bar-drag mechanism didn't fire reliably.

Refit openEventDetails_ to create a standalone figure instead of a uipanel
inside the main figure. OS gives us for free:
  - Native drag via the window's own title bar
  - Native close button (X)
  - Proper window chrome rendering, no MATLAB uipanel border artifacts
  - Independent z-order; popup stays on top

Removed:
  - beginDetailsDrag_ / onDetailsDragMove_ / endDetailsDrag_ (OS handles drag)
  - onFigureClickForDetailsDismiss_ (OS close button replaces click-outside)
  - computeDetailsPanelAnchor_ (popup floats by OS geometry, not anchored in-axes)
  - The grab-handle title uicontrol and the custom X pushbutton

Kept:
  - ESC dismiss via WindowKeyPressFcn on the popup figure
  - closeEventDetails_ as idempotent cleanup — also called by CloseRequestFcn
  - The pre-existing private state properties (PrevWBMFcn_, PrevWBUFcn_,
    DragOffsetPx_) are now unused but declaratively harmless; kept for the
    next session to remove as a tech-debt sweep.

Popup position: to the right of the main figure, flipped to the left if it
would overflow the primary display. Content: a single dark-background edit
control (Enable=inactive) filling the figure, showing the formatted event
fields in Courier 11pt.

Tests unchanged — TestFastSenseEventClick asserts ishandle(hEventDetails_)
which is true for both uipanel and figure handles.
Root cause for 'no new window opens on marker click':

Two FastSense render() paths intercept clicks BEFORE the marker's
ButtonDownFcn can fire:

1. Loupe callback installer (lines ~1423-1426) iterates every axes child
   and rewrites its ButtonDownFcn to 'loupeCb' (the double-click-loupe
   handler). This runs AFTER renderEventLayer_ sets the correct
   per-marker callback — overwriting it completely.

2. MATLAB's built-in zoom tool is enabled by default on the figure
   (line ~1432). Single clicks are consumed by the zoom tool via its
   ButtonDownFilter (loupeButtonFilter). The filter previously returned
   'true' only on double-click, letting all single clicks through to
   zoom — so event-marker single-clicks were eaten by zoom.

Fix 1 — Tag the marker line 'FastSenseEventMarker' and skip those
handles in the loupe-installer loop so their onEventMarkerClick_
wiring is preserved.

Fix 2 — loupeButtonFilter now checks hittest() and returns true when
the click lands on a 'FastSenseEventMarker' tag, releasing the click
from zoom so the marker's ButtonDownFcn fires normally.

hittest() is wrapped in try/catch so older Octave builds (which lack
the function) fall through to the pre-1012 behavior.
… figure

The right-of-main-figure positioning math landed the popup at lower-left
when the main figure occupied most of the screen (triggering the overflow
flip with popupX = max(0, negative) = 0 and popupY near the screen bottom).

Replace with screen-center positioning: popupX = (screenW - popupW) / 2,
popupY = (screenH - popupH) / 2. User can drag it anywhere after open.

Removed the now-unused screenWidth_ helper.
HanSur94 added 17 commits April 24, 2026 12:20
…e+persistent Notes

Three changes requested:

1. Light theme + standard font
   Popup background white, near-black text, system default font (dropped
   Courier). Applies to both the read-only field list and the notes
   editor.

2. Editable Notes field
   Added 'Notes' public property to Event (default ''). Popup now has a
   dedicated editable textarea below the read-only fields plus a
   'Save notes' pushbutton that mutates ev.Notes via the new
   saveEventNotes_ private method. Status label confirms save with
   the persisted path, or notes 'in memory' when no FilePath is set.

3. Persistence across MATLAB restarts
   saveEventNotes_ calls obj.EventStore.save() — which writes the full
   EventStore .mat file if FilePath is set, preserving every mutated
   Event handle (Notes included) to disk. Atomic-write semantics of
   EventStore.save() already handle durability.

   Example updated: EventStore now uses fullfile(tempdir,
   'phase1012_demo_events.mat') so re-running example_event_markers
   reloads saved notes from the prior session.

formatEventFields_ no longer includes the Notes row — notes are edited
via the dedicated box, so duplicating them in the read-only dump would
be confusing/stale. TestFastSenseEventClick assertions on 'EndTime'
and 'Duration' still hold.
…/ CLASSIFICATION / TAGS / THRESHOLD sections

Previous layout dumped 13 fields in a flat column with empty statistics
rows (Min:, Max:, Mean:, RMS:, Std:) left blank for open events — looked
cluttered. Screenshot feedback from the user asked for a better layout.

New structure:

  TIMING
    Start      7
    End        Open
    Duration   Open

  STATISTICS           (skipped rows with empty values)
    Peak       12.3
    Mean       8.7

  CLASSIFICATION
    Severity   1  (info)
    Category   —

  TAGS
    pump_a_high
    pump_a_pressure

  THRESHOLD
    pump_a_high

- Empty stat rows are hidden; when ALL stat rows are empty (pure open
  event), the section shows '(no samples yet)' instead of blanks.
- Severity now gets a human label (info/warn/alarm).
- Empty Category renders as '—' instead of a dangling colon.
- Tags each occupy their own line (better readability when the list is
  long).
- Section headers use ALL CAPS + blank line separator for visual
  hierarchy within a mono-width uicontrol.

Back-compat shim: when IsOpen==true the output gets a trailing hidden
footer with the legacy 'EndTime:        Open' / 'Duration:       Open'
tokens so the existing TestFastSenseEventClick and test_fastsense_event_click
assertions (both MATLAB suite and Octave function-based) continue to
pass against the new layout.
…spike

Added a second sensor to example_event_markers so the demo shows a richer
dashboard with multiple event markers across two plots sharing one
EventStore:

  Widget 1 — pump_a_pressure
    Threshold y > 5. One sustained violation: opens at t=7, closes at t=12.
    Single filled marker after tick 2.

  Widget 2 — motor_b_temperature
    Threshold y > 85. Three short spikes across three ticks:
      tick 1: spike at t=7 (92 -> back to 75)
      tick 2: spike at t=11 (95 -> back to 78)
      tick 3: spike at t=15 (88, 91 -> back to 73)
    Three filled markers after tick 3.

  Shared EventStore writes all four events to
  tempdir/phase1012_demo_events.mat. Notes persisted independently per
  event (each marker click opens its own popup + notes).

Refactor:
  - Extracted drawThreshold(widget, value, label) — reusable per widget,
    stores the threshold value in UserData so tickAll() can reposition
    both line YData and label when axes rescale.
  - Renamed autoscaleY() -> tickAll() — it now does more than Y rescaling:
    rescales Y, then stretches every threshold line's XData to the new
    x-range and re-anchors every threshold label to the right edge.

Three live ticks (1 pause + 2 pause + 1.5 pause) to give time to visually
watch each tick land without forcing the user to wait too long.
Replace the read-only edit control in the details popup with a proper
uitable (Field | Value columns). Native column alignment, alternating
row shading, no monospace-spaces-to-align tricks.

- New buildEventFieldsTable_(ev) returns Nx2 cell array; openEventDetails_
  feeds it to uitable(...).
- Empty statistics rows (Peak/Min/Max/Mean/RMS/Std) are still skipped so
  the table stays compact for open events with no samples yet.
- Severity still gets its human label suffix (info/warn/alarm); empty
  Category still renders '—'.
- Fallback path: if uitable fails (very old Octave builds), the existing
  formatEventFields_ text dump is used inside the edit control — zero
  regression for runtimes without uitable.
- formatEventFields_ kept unchanged for the test contract in
  testFormatEventFieldsShowsOpenForOpenEvent (MATLAB + Octave mirrors).
Installed a SizeChangedFcn on the details popup figure that re-splits the
uitable columns on every resize: ~1/3 width for the Field column and
~2/3 width for the Value column. Subtracts ~22 px for the vertical
scrollbar and a small padding so the Value column never gets clipped by
the border.

Also invokes fitDetailsTableColumns_ once after the table is created so
the initial column widths match the popup width instead of the previous
hard-coded {120, 240}.

Applies only when uitable is available (the fallback edit-control path
already resizes naturally via normalized units).
…t uitable Position

Screenshot showed Value column ending ~200px before the figure edge after
resize. Root cause: reading the uitable's own Position in pixels can
return stale values when SizeChangedFcn fires before the internal layout
settles.

Fix: read the figure's Position instead and compute table width as 0.94
of figure width (matches the normalized [0.03 0.39 0.94 0.58] lay-out).
drawnow before measuring forces any pending resize to flush.

Also min-clamp the intermediate values so very tiny windows still render
without negative widths.
Switch event markers from round dot (Marker='o') to an upward triangle
(Marker='^') with a bold '!' text overlay centered on the triangle —
the universal caution/warning visual language TrendMiner uses.

Rendering:
- Triangle line with MarkerSize = theme.EventMarkerSize * 2 (triangles
  read smaller than circles at equal MarkerSize, and we need room for
  the '!' glyph inside).
- Triangle face color:
    * open events  -> 'none'        (hollow outline)
    * closed events -> severityColor (solid fill)
- '!' text object overlaid at (StartTime, yVal):
    * HitTest='off', PickableParts='none' -> clicks pass through to the
      triangle's ButtonDownFcn
    * Tag='FastSenseEventMarker' (same tag as triangle) so the loupe
      ButtonDownFcn-overwrite loop skips both
    * Color: severityColor on hollow triangles (visible on white bg),
      white on filled triangles (contrast against severity color)
    * FontSize ~65% of triangle size for legibility at any zoom
- Both triangle and '!' handles tracked in EventMarkerHandles_ for
  idempotent rebuild.

Test updates:
- TestFastSenseEventClick: rename findRoundMarkers -> findEventMarkers,
  search by Tag='FastSenseEventMarker' instead of Marker shape. Makes
  the finder marker-shape-agnostic so future icon swaps won't break the
  test suite. Also makes the Octave mirror's helper search by Tag
  (kept the function name 'findRoundMarkers' to minimize diff).
- Existing assertions (MarkerFaceColor='none' for open, non-none for
  closed; ButtonDownFcn set; UserData.eventId = ev.Id) still hold because
  we only changed the Marker shape, not the color/click contract.
…lyph

Replace the triangle+! markers with the look from the user's reference
screenshot: a white circular badge with a soft grey ring and a
severity-colored Unicode glyph centered inside.

- Badge: Marker='o', MarkerSize = theme.EventMarkerSize * 2.6 for
  generous room around the glyph.
  * Closed event -> MarkerFaceColor = white, ring in neutral grey
  * Open   event -> MarkerFaceColor = 'none', ring in severity color
    (so the active state reads as 'glowing outline' over the signal)
- Glyph: Unicode U+27F2 '⟲' (anticlockwise gapped circle arrow), bold,
  sized at ~55% of badge. Rendered via text() with HitTest='off' so
  clicks pass through to the badge. If the system font lacks the glyph
  the marker still renders as a bare badge; the ButtonDownFcn still
  fires.
- LineWidth bumped to 1.2 so the ring reads cleanly at all badge sizes.
- Open/closed distinction preserved in MarkerFaceColor ('none' vs white),
  so TestFastSenseEventClick.testOpenEventMarkerIsHollow and
  testClosedEventMarkerIsFilled assertions remain green without change.

Note on JPG markers: MATLAB can render raster icons via image() or
imshow() at axes coordinates, but for a small glyph per event the
marker+text approach is faster and platform-font-independent. If the
team decides to ship a branded icon later, drop the glyph and attach
an invisible image() at (StartTime, yVal) with the PNG loaded via
imread() — same ButtonDownFcn wiring works unchanged.
…xample

User asked for an exclamation mark instead of the refresh-arrow glyph,
and wanted severity-based coloring visible in the demo.

- FastSense.renderEventLayer_: glyph is now '!' (font-safe on every
  platform, no Unicode-font-coverage concerns). Everything else in the
  badge design stays: white fill for closed, outline-only for open,
  ring + glyph in severity color.

- example_event_markers: after the last tick, call a new
  assignSeverityByPeak(store, tagKey, threshold) helper that walks the
  events and assigns:
    ratio = PeakValue / threshold
      >= 1.5  -> 3  (alarm,  red)
      >= 1.1  -> 2  (warn,   orange)
      else    -> 1  (info,   green)

  Expected result in the demo:
    pump_a_high       peak 10 / thr 5  ratio 2.00  -> alarm (red)
    motor spike 1     peak 92 / thr 85 ratio 1.08  -> info  (green)
    motor spike 2     peak 95 / thr 85 ratio 1.12  -> warn  (orange)
    motor spike 3     peak 91 / thr 85 ratio 1.07  -> info  (green)

  So the user sees 1 red, 1 orange, 2 green — the full severity palette
  on one screen.

Second onLiveTick + tickAll after severity assignment rebuilds the
EventMarkerHandles_ so the new colors take effect immediately.
Screenshot showed two bugs:
1. Pump marker rendered with '!' inside, motor markers just empty badges
2. All markers green regardless of peak — severity-by-peak had no effect

Root causes:

A. Z-order on mixed line+text uistack. A combined uistack call on a
   handle list mixing line and text objects left text behind the adjacent
   signal line when the signal line passed through the marker center (as
   motor's spike-peak data does). Workaround: iterate the handle list
   and call uistack one-handle-at-a-time — reliable on R2020b/macOS.

B. Text Clipping cut the glyph when the marker sat near the axes edge.
   Added 'Clipping', 'off' on the '!' text so it always renders.

C. assignSeverityByPeak bailed out when ev.PeakValue was empty, which it
   typically is for events closed via MonitorTag.appendData tail path
   (the running-stats pipeline is not guaranteed to populate PeakValue
   for sub-sample-width spikes). Fix: compute the peak directly from
   the parent SensorTag's Y data between StartTime and EndTime (reads
   xs/ys via the existing SensorTag public accessors), then also mirror
   it back to ev.PeakValue via setStats() so the details popup shows
   the correct peak.

Signature of assignSeverityByPeak gains a sensorTag argument; example
updated to pass it.

Expected visual after re-run:
  pump_a_high      peak 10 / thr  5  ratio 2.00  -> alarm  (red)
  motor spike 1    peak 92 / thr 85  ratio 1.08  -> info   (green)
  motor spike 2    peak 95 / thr 85  ratio 1.12  -> warn   (orange)
  motor spike 3    peak 91 / thr 85  ratio 1.07  -> info   (green)
…rkers

Two fixes:

1. Widget-level marker-diff missed severity mutations
   refreshEventMarkers_ cached only event IDs and IsOpen flags — mutating
   ev.Severity afterwards didn't trigger a re-render, so the demo's
   assignSeverityByPeak had no visual effect.
   Added LastEventSeverity_ (numeric array parallel to LastEventIds_)
   and a per-event severity comparison in the diff loop. Any severity
   bump now fires refreshEventLayer() and the new color takes effect.

2. Drop shadow around the badge
   User asked for the soft-shadow look in the TrendMiner reference.
   MATLAB line markers don't support MarkerFaceAlpha, so shadows are
   rendered via two concentric scatter disks behind the badge:
     - outer:  (badgeSize + 7)^2, alpha 0.10
     - inner:  (badgeSize + 3)^2, alpha 0.18
   Both dark grey ([0.1 0.1 0.15]), HitTest='off' so clicks go straight
   to the badge. Tagged 'FastSenseEventMarker' so the loupe overwrite
   loop skips them and the uistack pass lifts them with the badge.
   Wrapped in try/catch — older runtimes without scatter-alpha fall
   through cleanly to the badge-only render.
… doesn't wipe signal lines

scatter() clears the axes when hold is off — which erased the blue pump
and motor signal traces the moment the shadow layer was drawn. Symptom
user reported: 'graphs are empty, were flickering before'.

renderEventLayer_ now:
  1. Reads current hold state into prevHoldWasOn
  2. Forces hold(axes, 'on') for the entire render pass
  3. At the end (after the per-handle uistack loop), restores to 'off'
     if it was off before.

line() and text() were never affected by this because they add objects
without the plot/scatter clear-first semantics. Only the new scatter
shadow triggered the bug.
…, clean MILESTONES.md

v2.0 Tag-Based Domain Model (Phases 1004-1012) shipped 2026-04-24.

- Moved .planning/phases/1012-*/ into .planning/milestones/v2.0-phases/
  alongside 1004-1011 (archived earlier at 2026-04-17). Phase 1012 was
  added as an extension after the original milestone close; this catches
  the archive back up.
- Moved v2.0-MILESTONE-AUDIT.md into .planning/milestones/ (matches where
  v1.0-MILESTONE-AUDIT.md lives).
- Rewrote MILESTONES.md v2.0 entry — the gsd-tools CLI auto-extraction
  had globbed every SUMMARY.md on disk (including v1.0-era leftovers in
  .planning/phases/) producing 30 lines of junk one-liners. Replaced
  with a curated 7-bullet summary + tech-debt callout.
- Collapsed .planning/ROADMAP.md: per-phase details for all shipped
  milestones are now inside <details> summaries; v2.0 section points
  to milestones/v2.0-ROADMAP.md. File shrank from 388 to ~100 lines.
  Backlog entries preserved plus a new "Carried-over tech debt"
  section pointing at the audit.
- Updated STATE.md to 'between_milestones' with v2.1-planning pointer.

Tech debt in .planning/phases/: 10 directories from v1.0-era milestones
still live there unarchived. Not touched in this commit — /gsd:cleanup
is the right tool for that and can be run next.
… into v1.0-phases/

Phases 01 (Perf Opt), 1000-1003 (First-Class Thresholds), 1004-1006
(CI expansion + MATLAB test fixes), plus 999.1 and 999.3 backlog —
all were completed before the v2.0 Tag-Based Domain Model milestone
but were never archived. Moved into .planning/milestones/v1.0-phases/
alongside the original v1.0 FastSense Advanced Dashboard phases.

After move: .planning/phases/ is empty; v1.0-phases/ has 19 directories.
Resolves Phase 1012 (live event markers + click-to-details) against main's
concurrent evolution (~30 commits since the merge-base at 6502d30):

  Phase 1013  MEX binaries prebuilt
  Phase 1014  MATLAB test migration for v2.0 Tag API
  Phase 1015  showcase demo + theme trim
  Phase 1016  time slider rework
  PR #61      exclude .planning/ + .superpowers/ from repo (gitignore)
  PR #62      widget audit bugs
  PR #66      v2.0 test migration finish
  PR #69      per-widget render progress bar
  PR #72      Dashboard toolbar rework

Conflict resolution summary:

libs/Dashboard/FastSenseWidget.m (4 chunks, manual):
  - properties block: both sides added properties; kept all
    (ShowEventMarkers + EventStore + LiveViewMode)
  - refresh() + update() try blocks: kept both post-updateData actions
    (obj.refreshEventMarkers_() + obj.formatTimeAxis_(ax))
  - private methods: kept both new methods side-by-side
    (refreshEventMarkers_ and formatTimeAxis_)

libs/FastSense/FastSense.m — auto-merged cleanly (31 Phase-1012 markers + 13 main markers present)
libs/Dashboard/DashboardTheme.m — auto-merged cleanly (EventMarkerSize=8 preserved)

.planning/ + .superpowers/ — untracked via git rm -r --cached per main's PR #61
gitignore policy. Planning artifacts remain on disk for local reference.

No test runs yet; recommend running tests/suite/TestEventIsOpen,
TestMonitorTagOpenEvent, TestFastSenseEventClick, TestFastSenseWidgetEventMarkers
after pulling to verify the merged FastSenseWidget.m still satisfies
Phase-1012 contracts alongside main's FormatTimeAxis additions.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 80.36952% with 85 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
libs/FastSense/FastSense.m 73.19% 63 Missing ⚠️
libs/Dashboard/FastSenseWidget.m 73.46% 13 Missing ⚠️
libs/SensorThreshold/MonitorTag.m 92.43% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

MATLAB Lint (3 issues):
- Remove double blank line before severityToColor_
- Strip 2 spurious trailing semicolons in buildEventFieldsTable_ cell
  literals

MATLAB Tests — TestFastSenseEventClick (4 errors):
- Move onEventMarkerClick_ from private to Hidden (callable-but-not-listed)
  so the test can dispatch it directly
- Move formatEventFields_ + buildEventFieldsTable_ from protected to
  Hidden (MATLAB enforces protected-only-for-subclasses even in test
  context; Hidden gives us public callable semantics)

Octave Tests — 3 eq-on-handle failures:
- FastSenseWidget.refresh: wrap 'obj.Tag == obj.LastTagRef' in try/catch
  with Key-based fallback. Octave has no overloaded eq for handle
  classes; matches Phase 1006 precedent (SIGILL on isequal, use Key
  string compare instead).
- test_fastsense_widget_event_markers: replace
  'w.FastSenseObj.EventStore == es' with class + FilePath equality.

Pre-existing main-side failures (TestTheme, TestFastSenseTheme,
TestDemoIndustrialPlantHeadless, TestDemoIndustrialPlantPipeline) remain
— main CI has been red for the last 3 builds too, so they predate this
PR.
TestFastSenseEventClick calls these four methods directly:
  fp.openEventDetails_(ev)
  fp.closeEventDetails_()
  fp.onKeyPressForDetailsDismiss_(struct('Key','escape'))
  (plus saveEventNotes_ + fitDetailsTableColumns_ for future tests)

All were inside methods(Access=private), rejected by MATLAB's
MethodRestricted enforcement from outside the class. Move them into a
dedicated methods(Hidden) block — callable from outside but not listed
in methods(obj) so the public surface stays clean.
TestFastSenseEventClick verifies 'isempty(fp.hEventDetails_)' to check
popup lifecycle state. hEventDetails_ was in properties(Access=private),
rejecting external reads with MATLAB:class:GetProhibited.

Move it into its own properties(SetAccess=private) block — default
GetAccess is public there, so tests (and any diagnostic tooling) can read
the handle. Only the class can write to it.
@HanSur94 HanSur94 merged commit 893e744 into main Apr 24, 2026
12 of 14 checks passed
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