diff --git a/.planning/STATE.md b/.planning/STATE.md index 162d25a8..61e3d4dc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -27,7 +27,7 @@ Phase: — (none active; latest shipped = Phase 1041, MERGED via PR #189 on 2026 Plan: — Milestone: v3.0 FastSense Companion — SHIPPED 2026-04-30; v3.1 Plant Log Integration — SHIPPED 2026-05-19; v4.0 Multi-User LAN Concurrency — SHIPPED 2026-05 via PR #152 (parallel branch); v1.0 perf line — COMPLETE via PR #114. No milestone in flight. Status: Phase 1041 complete — inline time-range control (toolbar dropdown + Custom date strip) shipped; PR #189 MERGED 2026-06-03. No planned milestone in flight — repo in polish/housekeeping. Outstanding: 12 wiki-bot dup PRs were closed to 1 (#190) + workflow root-caused (260609-mcz); backlog Phase 999.1 (in-app help system) unplanned; ROADMAP v4.0 boxes stale (shipped on main via #152 — router misreports, see memory gsd-router-stale-v4-misroute). -Last activity: 2026-06-10 - Completed quick task 260610-ov3: Optimize data loading speed in populated FastSense dashboards (DashboardEngine) — per-render Tag-data cache in FastSenseWidget (<=1 resolve per render, consume-once through the engine preview pass), O(1) ctor time-range pull, bench_dashboard_load.m. On branch claude/unruffled-gagarin-e2ca3d; MATLAB-only suite runs deferred (MCP down). +Last activity: 2026-06-24 - Completed quick task 260624-nvf: Fix StatusWidget crash when a status widget is bound to a MonitorTag via 'Tag' — the constructor now routes a monitor-kind Tag to the Threshold/monitor path so refresh() uses deriveStatusFromMonitorTag_ instead of the SensorTag-only obj.Sensor.Y access. Unblocks the README "Build a dashboard" quickstart. Verified in a fresh process: TestStatusWidget+MultiStatus 23/23, dashboard suites 68/68, README dashboard repro renders 4 widgets. Code 14b3d529; on branch claude/recursing-cray-0dbbac. ### Note on parallel v4.0 work (main branch state) @@ -107,6 +107,7 @@ Other main PRs (#138, #139, #141, #144, #145, #146) auto-merged without conflict | 260609-v5p | Speed up DashboardEngine live refresh: data-unchanged fast path in FastSenseWidget.update()/refresh() (fingerprint [n,x1,xend,yend], same append-only contract as PreviewCacheKey_) skips updateData/preview-invalidate/formatTimeAxis on idle ticks; single Tag.getXY per tick (updateTimeRangeCache(x) optional arg); refreshEventMarkers_ O(nE²)→O(nE) isequal diff; computeEventMarkers vectorized accumulators + sortrows-based dedup (max-severity-wins preserved, non-finite sev→1); getEventMarkers preallocation + per-unique-severity color lookup; vectorized formatTimeAxis_. New bench_dashboard_live.m (8 Tag-bound widgets × 50k pts + 200 events): idle tick 281→34 ms (~8×), active tick ~50→30 ms. Verified R2025b: test_dashboard_perf_fixes 9/9 (2 new), preview-envelope 7/7, events-toggle 22/22, time-window 8/8, TestDashboardEngine 18/18, TestDashboardEngineEventMarkers 8/8, TestFastSenseWidgetUpdate 2/2, TestFastSenseWidgetEventMarkers 12/12, TestDashboardDirtyFlag 6/6. Known stale test: flat test_dashboard_engine_event_markers case_render assumes one handle per marker — broken since 260508 color-group batching, fails pre- and post-change identically (Octave mirror that self-skips on Octave). | 2026-06-09 | 8cd6443f, c29be759, cbd66937, 98184f36 | — | [260609-v5p-speed-up-dashboardengine-live-refresh-pa](./quick/260609-v5p-speed-up-dashboardengine-live-refresh-pa/) | | 260610-hwj | Review-sweep fixes batch 2 (branch claude/review-fixes-batch2, separate PR from the perf pass): GaugeWidget.fromStruct restores Threshold (was Tag — threshold coloring dead after load) + constructor probes for allValues() (pre-v2.0-only method; MonitorTag-bound gauges crashed at construction since the migration); GroupWidget round-trips ExpandedHeight (collapsed groups were stuck collapsed after load); central themeOverride backfill in DashboardWidgetRegistry.fromStruct (dropped on load for every widget except GroupWidget); FastSense exportData routes through lineFullData (disk-backed lines exported empty columns); markerXData test helper parses batched NaN-separated marker polylines (stale since 260508). New test_review_fixes_batch2.m 4/4 R2025b / 3/3+gate Octave 11; event_markers 9/9 (first MATLAB pass ever); SerializerRoundTrip 15/15; Serializer 12/12; toolbar 19/19. | 2026-06-10 | 18387785 | — | [260610-hwj-review-sweep-fixes-batch-2-widget-serial](./quick/260610-hwj-review-sweep-fixes-batch-2-widget-serial/) | | 260610-ov3 | Optimize data loading speed in populated Tag-bound dashboards (LOAD path; complements 260609-v5p/260610-g0w live-tick passes): render-scoped `RenderDataCache_` in FastSenseWidget collapses 3-4 redundant Tag resolves per render() to <=1 — probe pull seeds the cache, binding (addLine for non-state kinds; State keeps addTag staircase), yInit autoscale, and updateTimeRangeCache all reuse it; consume-once lifetime (warm through DashboardEngine's post-render computePreviewEnvelope pass, cleared on preview read + refresh()/update() entry); ctor `updateTimeRangeCache()` no-arg now uses O(1) Tag.getTimeRange() instead of full getXY (also fixes disk-backed ctor range = inf/-inf). Disk widgets: 2 SQLite range queries/render → 1, and they now contribute slider-preview envelopes at load (previously silently empty). New bench_dashboard_load.m (12×50k pts, 4 disk-backed): Render 6834→6711 ms Octave (graphics-bound; data-load slice halved). Octave 11.1: render_cache 4/4, load_perf 5/5, 13 regression tests green (batch segfault = known teardown flake, isolated runs clean). DEFERRED to live MATLAB: TestDashboardEngine, TestFastSenseWidgetUpdate, TestDashboardSerializerRoundTrip suites. | 2026-06-10 | 3b7535ea | — | [260610-ov3-optimize-data-loading-speed-in-populated](./quick/260610-ov3-optimize-data-loading-speed-in-populated/) | +| 260624-nvf | Fix StatusWidget crash when bound to a MonitorTag via 'Tag' (route monitor-kind Tag to the Threshold/monitor path; unblocks README dashboard quickstart) | 2026-06-24 | 14b3d529 | Verified | [260624-nvf-fix-statuswidget-crash-when-bound-to-a-m](./quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/) | ## Progress Bar diff --git a/.planning/quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/260624-nvf-PLAN.md b/.planning/quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/260624-nvf-PLAN.md new file mode 100644 index 00000000..32119db6 --- /dev/null +++ b/.planning/quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/260624-nvf-PLAN.md @@ -0,0 +1,44 @@ +--- +quick_id: 260624-nvf +title: Fix StatusWidget crash when bound to a MonitorTag via Tag +status: complete +date: 2026-06-24 +--- + +# Quick Task 260624-nvf — Fix StatusWidget crash when bound to a MonitorTag via Tag + +## Problem + +`addWidget('status', 'Tag', monitorTag)` crashed at render with +`Unrecognized method, property, or field 'Y' for class 'MonitorTag'` +(`libs/Dashboard/StatusWidget.m` `refresh`). `DashboardWidget.Sensor` is a +backward-compat **alias for `Tag`**, so a `'Tag'`-bound MonitorTag is read back +through `obj.Sensor`, and the Sensor branch assumes a SensorTag-like `.Y` value +series. MonitorTags are 0/1 alarm signals with no `.Y`. The README +"Build a dashboard" quickstart binds `status` to an alarm MonitorTag and hit this. + +## Approach + +StatusWidget already supports MonitorTags through the **Threshold/monitor path** +(`deriveStatusFromMonitorTag_`, which reads `obj.Threshold.getXY()`), previously +reachable only when the monitor was passed via the `Threshold` property. Reroute a +monitor-kind Tag to `Threshold` in the constructor so `refresh()` (and the label, +asciiRender, and toStruct paths) use that existing handling. The sibling +`MultiStatusWidget` already supports Tag-bound MonitorTags — this brings StatusWidget +to parity. + +## Tasks + +1. `libs/Dashboard/StatusWidget.m` — constructor: after Threshold-key resolution, + if no `Threshold` is set and the bound Tag is monitor-kind + (`thresholdIsMonitorKind_(obj.Sensor)`), move it to `Threshold` and clear `Sensor`. + - verify: `TestStatusWidget` passes; README dashboard repro renders without error. + - done: a monitor-bound status widget renders and `CurrentStatus` reflects the + monitor's latest 0/1 sample ('violation' / 'ok'). +2. `tests/suite/TestStatusWidget.m` — add `testRefreshWithMonitorTag` covering the + violation (last sample 1) and ok (last sample 0) cases. + +## Constraints + +Pure MATLAB, toolbox-free; backward compatible (SensorTag-bound and Threshold-bound +status widgets unchanged); pure-MATLAB MEX fallback intact; break no existing test. diff --git a/.planning/quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/260624-nvf-SUMMARY.md b/.planning/quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/260624-nvf-SUMMARY.md new file mode 100644 index 00000000..5f7c056d --- /dev/null +++ b/.planning/quick/260624-nvf-fix-statuswidget-crash-when-bound-to-a-m/260624-nvf-SUMMARY.md @@ -0,0 +1,48 @@ +--- +quick_id: 260624-nvf +title: Fix StatusWidget crash when bound to a MonitorTag via Tag +status: complete +date: 2026-06-24 +commit: 14b3d529 +--- + +# Quick Task 260624-nvf — Summary + +**Status:** complete + +## What changed + +- `libs/Dashboard/StatusWidget.m` (constructor): if no `Threshold` is set and the + bound Tag (`obj.Sensor`, the alias for `obj.Tag`) is monitor-kind, reroute it to + `obj.Threshold` and clear `obj.Sensor`. `refresh()` then takes the existing + `deriveStatusFromMonitorTag_` path instead of the SensorTag-only `obj.Sensor.Y` + access that crashed on a MonitorTag. +- `tests/suite/TestStatusWidget.m`: added `testRefreshWithMonitorTag` — binds a + MonitorTag via `'Tag'`, renders into an offscreen figure, asserts no render error + and `CurrentStatus` == `'violation'` (last sample > 0.5) / `'ok'` (<= 0.5). + +## Verification (fresh `matlab -batch`, `restoredefaultpath`, this worktree) + +- **RED (before fix):** `testRefreshWithMonitorTag` errored with + `Unrecognized ... 'Y' for class 'MonitorTag'` at `StatusWidget/refresh:119`; + the other 10 TestStatusWidget tests passed. +- **GREEN (after fix):** `TestStatusWidget` + `TestMultiStatusWidget` + + `TestMultiStatusWidgetTag` → **23/23**. +- **Regression:** `TestDashboardSerializerRoundTrip` + `TestDashboardBugFixes` + + `TestDashboardPreview` + `TestInfoTooltip` → **68/68**. +- **End-to-end:** the README "Build a dashboard" snippet + (`addWidget('status','Tag',alarm,...)` + `d.render()`) renders all 4 widgets, + no error. + +## Backward compatibility + +SensorTag-bound (kind `sensor` → reroute guard false → unchanged) and Threshold-bound +(`obj.Threshold` already set → guard skips) status widgets are unchanged. Only the +previously-crashing Tag-bound MonitorTag case changes behaviour. + +## Notes + +- Code commit: `14b3d529`. +- Unblocks the README dashboard quickstart; the companion `'Label'` → `'Title'` doc + fix landed earlier in `c0f0d949`. +- Originated from the `/first-run-check` loop (task_693ced52). diff --git a/README.md b/README.md index 7df43900..027e82b3 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,17 @@ Everything in FastSense — sensors, machine states, alarms, derived signals — Tags carry their own metadata (units, criticality, labels) and live in a shared **`TagRegistry`** so every part of the system — plots, dashboards, event detection, the web bridge — speaks the same language. ```matlab +t = linspace(0, 100, 1e5); % your sensor's timestamps +pressure_data = 50 + 8*sin(t/5) + randn(size(t)); % ...and its readings + press = SensorTag('press_a', 'Name', 'Chamber Pressure', 'Units', 'bar'); press.updateData(t, pressure_data); % Alarm whenever pressure > 55 bar alarm = MonitorTag('press_high', press, @(x, y) y > 55); -TagRegistry.register(press); -TagRegistry.register(alarm); +TagRegistry.register('press_a', press); +TagRegistry.register('press_high', alarm); fp = FastSense(); fp.addTag(press); @@ -89,9 +92,9 @@ Compose monitoring dashboards from widgets on a 24-column grid. The same Tags dr d = DashboardEngine('Process Monitor'); d.Theme = 'dark'; d.addWidget('fastsense', 'Position', [1 1 16 8], 'Tag', press); -d.addWidget('number', 'Position', [17 1 8 4], 'Tag', press, 'Label', 'Pressure'); -d.addWidget('gauge', 'Position', [17 5 8 4], 'Tag', press, 'Label', 'Live'); -d.addWidget('status', 'Position', [1 9 24 2], 'Tag', alarm, 'Label', 'Alarm'); +d.addWidget('number', 'Position', [17 1 8 4], 'Tag', press, 'Title', 'Pressure'); +d.addWidget('gauge', 'Position', [17 5 8 4], 'Tag', press, 'Title', 'Live'); +d.addWidget('status', 'Position', [1 9 24 2], 'Tag', alarm, 'Title', 'Alarm'); d.render(); d.save('process.json'); % JSON-persist diff --git a/examples/01-basics/example_basic.m b/examples/01-basics/example_basic.m index 4bf9966f..0d2c1c93 100644 --- a/examples/01-basics/example_basic.m +++ b/examples/01-basics/example_basic.m @@ -44,11 +44,11 @@ fp2.addLine(x2, y2, 'DisplayName', 'Exponential Growth'); fp2.addThreshold(100, 'Direction', 'upper', 'ShowViolations', true, ... 'Label', 'Warning'); -fp2.render(); - % Switch Y axis to log scale (can be called before or after render). +% Setting it before render() lets the axis autoscale directly in log space. % Try fp2.setScale('YScale', 'linear') to toggle back. fp2.setScale('YScale', 'log'); +fp2.render(); title(fp2.hAxes, 'setScale — Logarithmic Y Axis'); fprintf('setScale() demo: Y axis switched to log scale.\n'); diff --git a/libs/Dashboard/StatusWidget.m b/libs/Dashboard/StatusWidget.m index d5948128..af4c55af 100644 --- a/libs/Dashboard/StatusWidget.m +++ b/libs/Dashboard/StatusWidget.m @@ -43,6 +43,15 @@ obj.Threshold = []; end end + % A MonitorTag bound via 'Tag' (or its 'Sensor' alias) is a 0/1 + % alarm signal, not a value series, so route it through the + % Threshold/monitor path — refresh() then uses + % deriveStatusFromMonitorTag_ instead of the SensorTag-only + % obj.Sensor.Y access (which errors on a MonitorTag). + if isempty(obj.Threshold) && thresholdIsMonitorKind_(obj.Sensor) + obj.Threshold = obj.Sensor; + obj.Sensor = []; + end % Mutual exclusivity: Threshold wins (per D-08) if ~isempty(obj.Threshold) && ~isempty(obj.Sensor) obj.Sensor = []; diff --git a/tests/suite/TestStatusWidget.m b/tests/suite/TestStatusWidget.m index 598b8dc4..9da01e19 100644 --- a/tests/suite/TestStatusWidget.m +++ b/tests/suite/TestStatusWidget.m @@ -106,6 +106,33 @@ function testRefreshWithTag(testCase) 'No threshold rules means status should be ok'); end + function testRefreshWithMonitorTag(testCase) + %% A MonitorTag bound via 'Tag' renders without error and maps + % its latest 0/1 sample to violation/ok. Regression: MonitorTag + % has no .Y, which the Sensor-value path assumed (#task_693ced52). + sHi = SensorTag('mw_press_hi', 'Name', 'Pressure Hi', 'Units', 'bar'); + sHi.updateData([1 2 3], [50 52 60]); % last 60 + monHi = MonitorTag('mw_mon_hi', sHi, @(x, y) y > 55); % last -> 1 + + wHi = StatusWidget('Tag', monHi, 'Title', 'Alarm'); + hFig = figure('Visible', 'off'); + testCase.addTeardown(@() close(hFig)); + hp = uipanel('Parent', hFig, 'Position', [0 0 1 1]); + testCase.verifyWarningFree(@() wHi.render(hp)); + testCase.verifyEqual(wHi.CurrentStatus, 'violation', ... + 'MonitorTag with last sample > 0.5 should read as violation'); + + sLo = SensorTag('mw_press_lo', 'Name', 'Pressure Lo', 'Units', 'bar'); + sLo.updateData([1 2 3], [50 51 52]); % last 52 + monLo = MonitorTag('mw_mon_lo', sLo, @(x, y) y > 55); % last -> 0 + + wLo = StatusWidget('Tag', monLo, 'Title', 'Alarm'); + hp2 = uipanel('Parent', hFig, 'Position', [0 0 1 1]); + testCase.verifyWarningFree(@() wLo.render(hp2)); + testCase.verifyEqual(wLo.CurrentStatus, 'ok', ... + 'MonitorTag with last sample <= 0.5 should read as ok'); + end + function testToStruct(testCase) %% Serialization includes type, title, position, and source s = SensorTag('V-100', 'Name', 'Valve'); diff --git a/wiki/Installation.md b/wiki/Installation.md index 2422812b..8e9ebd62 100644 --- a/wiki/Installation.md +++ b/wiki/Installation.md @@ -11,25 +11,31 @@ ## Setup 1. Clone or download the repository -2. In MATLAB/Octave, navigate to the FastPlot directory +2. In MATLAB/Octave, navigate to the FastSense directory 3. Run install: ```matlab install; ``` This adds the library paths: -- `libs/FastPlot` — core plotting engine -- `libs/SensorThreshold` — sensor and threshold system +- `libs/FastSense` — core plotting engine +- `libs/SensorThreshold` — sensor, tag, and threshold system - `libs/EventDetection` — event detection and viewer - `libs/Dashboard` — dashboard engine and widgets - `libs/WebBridge` — TCP server for web-based visualization +- `libs/FastSenseCompanion` — companion navigator app +- `libs/PlantLog` — plant-log entry storage +- `libs/Concurrency` — file-locking helpers for live/multi-process use +- `libs/Help` — in-app wiki browser + +It also adds `examples/`, `benchmarks/`, and `tests/` to the path, so example and benchmark scripts can be run by name. ## MEX Compilation (Optional) For maximum performance, compile the C MEX accelerators: ```matlab -cd libs/FastPlot +cd libs/FastSense build_mex(); ``` diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md index ca1e1132..773a00f2 100644 --- a/wiki/_Sidebar.md +++ b/wiki/_Sidebar.md @@ -1,4 +1,4 @@ -**FastPlot Wiki** +**FastSense Wiki** - [[Home]] - [[Installation]]