Skip to content

feat(companion): Tag Status Table window (quick task 260519-bs4)#149

Merged
HanSur94 merged 10 commits into
mainfrom
claude/compassionate-chaplygin-d944fd
May 19, 2026
Merged

feat(companion): Tag Status Table window (quick task 260519-bs4)#149
HanSur94 merged 10 commits into
mainfrom
claude/compassionate-chaplygin-d944fd

Conversation

@HanSur94

Copy link
Copy Markdown
Owner

Summary

Adds a Tag Status Table window to FastSenseCompanion, opened via a new Tags ↗ button on the companion's top toolbar. Detached classical figure (not a uifigure child, per cross-cutting rule) with a 12-column uitable listing every registered tag and its live status.

Designed as a quick task (/gsd:quick), wave of 6 atomic commits.

What you see

Column Source
Key tag.Key
Name tag.Name
Type class name (Sensor / Monitor / Composite / State / Derived)
Criticality tag.Criticality
Units tag.Units
Latest Y(end)
Status Smart per-type — Monitor → OK/ALARM, State → state label, others → —
Last updated X(end) formatted as wall-clock time
Activity Live if now − X(end) < 5 min, else Inactive
Events count from EventStore per tag
Samples numel(Y)
Labels comma-joined tag.Labels

UI extras on the window:

  • Last refreshed: HH:MM:SS heartbeat label (UI poll tick, distinct from per-row `Last updated`)
  • Pause/Resume polling toggle button (freezes both refresh paths; header shows `(paused)` suffix)
  • Search field — case-insensitive substring across Key + Name + Units + Labels
  • Chip filters mirroring `TagCatalogPane` pattern:
    • Type chips: Sensor / Monitor / Composite / State / Derived
    • Criticality chips: Low / Medium / High / Safety
    • Activity chips: Live / Inactive
    • Multi-toggle; AND-across-groups / OR-within-group
  • Footer count `N / M tags`

Refresh model

Two parallel paths so the table is correct regardless of `companion.IsLive`:

  1. Push-on-write via existing `FastSenseCompanion.scanLiveTagUpdates_` → new `markStatusTableDirty_(keys)` hook — instant updates when companion is in Live mode.
  2. Window-owned timer `RefreshTimer_` (1 s, fixedSpacing, BusyMode='drop', UUID-named, self-stops after 2 consecutive tick errors) — keeps Last updated / Activity / Latest correct even when companion is idle.

Both paths short-circuit while polling is paused. Window-close path runs `stop(t); delete(t);` before `Listeners_` cleanup, in that order.

Commits

# Hash Scope
01 b2ed937 `TagStatusTableWindow.m` base + 11 pure-logic tests + `ThrowingTagStub.m` helper
02 e8a1be5 Companion wiring: `Tags ↗` button, `openTagStatusTable()` public, `attachStatusTable_` / `detachStatusTable_` / `markStatusTableDirty_` privates, `scanLiveTagUpdates_` hook, teardown wiring + 7 UI lifecycle tests
03 43d2d3b Activity column + window-owned 1 s `RefreshTimer_` (deviation from CONTEXT.md "push-on-write only" decision per user feedback) +5/+2 tests
04 2a24965 `Last refreshed` heartbeat header + Type/Criticality/Activity chip filters + broadened free-text search to Key+Name+Units+Labels +4/+2 tests
05 50d464c Pause/Resume polling toggle button + `setPollingActive(tf)` public method +4 tests
06 10df740 Events count column from `EventStore` per tag +4/+1 tests

`FastSenseCompanion.m` is touched only in commit 02; the other 5 commits are scoped to `TagStatusTableWindow.m` + its two test files.

Verification

  • `tests/test_companion_tag_status_table.m` (pure-logic): 24/24 pass
  • `tests/suite/TestTagStatusTableWindow.m` (UI lifecycle): 16/16 pass
  • `tests/suite/TestFastSenseCompanion.m` (regression): 64/64 pass (no companion regression)
  • Static analysis: `mh_style` / `mh_lint` / `mh_metric --ci` clean; `checkcode` shows only pre-existing `now`/`datenum` notices, zero new
  • Live verification on `demo/industrial_plant/run_demo.m` over 5 iterative rounds:
    • 18 tags listed, 4 columns of MonitorTags showed real event counts (29/32/33/35)
    • Activity flipped Live → Inactive at exactly the 5-min boundary (proven via static `buildRow_` with simulated `nowSeconds + 360`)
    • Companion `IsLive=0` throughout — window polled itself successfully
    • Pause/Resume toggle: header shows `(paused)` suffix when frozen, value cells stop updating
    • Chip filters + search + theme switch + window close + companion close — all verified manually

Cross-cutting rules respected (locked in Phase 1018)

  • Window is a classical `figure`, NOT a `uifigure` child (companion is the only `uifigure`)
  • `Listeners_` cell array cleaned in `onCloseRequest_`
  • Timer cleanup order `stop(t); delete(t);` runs BEFORE Listeners cleanup
  • Errors namespaced `FastSenseCompanion:tagStatusTable*`
  • Every callback wrapped in try/catch + non-blocking `uialert`
  • Pure MATLAB, no new toolboxes / dependencies
  • miss_hit limits respected (line_length 160, function_length 600, cyc 95)

Deferred / out of scope

  • Polling-scope clarification — orchestrator asked whether polling should be heartbeat-only / passive-observation-guarantee / only-update-changed-cells; user dismissed. Current behavior: timer recomputes all rows on every tick, repaints only changed cells. Can revisit.
  • Info button + markdown help — escalated from a one-off button to a milestone-sized "unified in-app help/wiki" effort. Parked as backlog item 999.1 in local roadmap (gitignored). Tag Status Table ships without inline docs; future help-system milestone will add Info buttons across all panes/widgets.

Test plan for reviewer

  • `install(); run('demo/industrial_plant/run_demo.m')`
  • Click Tags ↗ in companion toolbar → window opens, 12 cols × 18 rows
  • Confirm Activity = `Live` for all tags, Events column shows non-zero only for MonitorTags
  • Type substring in search → rows filter; footer count updates
  • Click chip filters → AND-across-groups / OR-within-group semantics
  • Toggle Pause polling → header shows `(paused)`, time freezes; Resume → header ticks again
  • Close window via X → no leftover `TagStatusTable-*` timers in `timerfindall`
  • Close companion → Tag Status closes too if still open
  • Verify on Octave / R2020b CI

🤖 Generated with Claude Code

HanSur94 and others added 7 commits May 19, 2026 08:40
…c tests

- New TagStatusTableWindow class in libs/FastSenseCompanion/ (handle, classical figure)
- Static helpers buildRow_(tag) and filterRows_(rows, query) for unit-testable pure logic
- buildRow_ handles every Tag subclass (sensor/state/monitor/composite/derived)
  plus the error case via try/catch (em-dash placeholders for dynamic columns)
- filterRows_ is case-insensitive substring match on columns Key + Name
- 11/11 function-style tests pass in tests/test_companion_tag_status_table.m
- ThrowingTagStub helper class in tests/helpers/ exercises the error-recovery path

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

- Toolbar grid 1x4 -> 1x5 with new "Tags ↗" button (Tag='CompanionTagStatusBtn') in col 3
- New public method openTagStatusTable() returns the singleton window handle
- Three private hooks: attachStatusTable_, detachStatusTable_, markStatusTableDirty_
- New private one-liner shouldScanForStatusTable_ gates the scan scope
- scanLiveTagUpdates_ now scans ALL Tag kinds when the status table is open
  (was Sensor/State only); LiveLogPane.addLiveLogEntry gated to Sensor/State
- updatedKeys collected per tick and batched to markStatusTableDirty_ at end
- close() and setProject() both tear the window down (companion is the
  registered DetachClosed listener, so the window's onCloseRequest_ cleanup
  fires the companion's detachStatusTable_ for us)
- Hidden test seams: tagStatusTableWindowForTest_, scanLiveTagUpdatesForTest_
- 7/7 class-based UI lifecycle tests pass in tests/suite/TestTagStatusTableWindow.m
- TestFastSenseCompanion 64/64 still pass (no regression)
- pure-logic suite 11/11 still pass

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

User feedback after first verification: the Tag Status window must keep
"Latest" / "Last updated" accurate even when companion is NOT in Live mode,
and tags with no update in the last 5 minutes should render as Inactive.

Changes (all scoped to TagStatusTableWindow and its two test files; no
changes to FastSenseCompanion.m or the existing push-on-write hook):

- New "Activity" column at index 9 (between "Last updated" and "Samples").
  Values: "Live" if (now - X(end)) < 300s in posixtime, else "Inactive".
  Empty / NaN / unanchored / future X defensively renders "Inactive".
  Time-base inference mirrors InspectorPane.formatXTick_ (posixtime >1e9,
  datenum >7e5).

- New window-owned RefreshTimer_ (1s period, fixedSpacing, BusyMode='drop',
  unique name "TagStatusTable-<UUID>"). Starts in openWith after IsOpen=true;
  stopped+deleted in onCloseRequest_ BEFORE Listeners cleanup. Callback in
  try/catch; logs via warning (not uialert); self-stops after 2 consecutive
  failures to prevent log noise storms. Independent of companion Live mode --
  guarantees Activity/Last updated stay accurate even when companion is idle.

- buildRow_(tag) -> buildRow_(tag, nowSeconds) -- the nowSeconds parameter
  makes the static helper pure/unit-testable for the Activity column.
  Backward-compatible: nargin<2 falls back to TagStatusTableWindow.nowSeconds_.

- RowBuffer_ / table data widened to 11 cols. All existing test assertions
  updated for the new column positions (Samples now at idx 10, Labels at 11).

- The original FastSenseCompanion.scanLiveTagUpdates_ -> markStatusTableDirty_
  push path is unchanged; both mechanisms now run in parallel.

Tests:
- test_companion_tag_status_table.m: 11 existing + 5 new (Activity Live /
  Inactive across posix/datenum/empty/future/filter regression) = 16/16 pass.
- TestTagStatusTableWindow.m: 7 existing + 2 new (Activity flip without Live
  mode, RefreshTimer_ stopped+deleted on close via timerfindall sweep) = 9/9
  pass.
- TestFastSenseCompanion regression: 64/64 pass.
- checkcode / mh_lint / mh_metric: clean (informational notices on
  now/datenum match codebase convention in InspectorPane and LiveLogPane).
…ty/Activity chips + broader search

- TagStatusTableWindow: new "Last refreshed: HH:MM:SS" label at top of figure;
  seeded on openWith, updated on every clean onRefreshTick_ (even when no
  rows changed, so it acts as a heartbeat).
- Three chip groups above the table (Type / Criticality / Activity),
  multi-toggle within each group, default-all-active so first-open
  shows the same 18 rows as today. Active chips use theme.Accent +
  bold, matching TagCatalogPane.applyPillStyle_.
- Broadened free-text search from Key+Name to Key+Name+Units+Labels
  (Labels are stored comma-joined in column 11). filterRows_ signature
  extended to (rows, query, activeTypes, activeCrits, activeActivities)
  with full backward compatibility on the 2-arg form via nargin defaults
  and an iscell sentinel ([] = skip, {} = exclude-all). Combined
  semantics: AND across dimensions, OR within each chip group; a chip
  group with zero active entries excludes ALL rows.
- Layout: window 520->580 px tall; five vertical strips (label, search,
  chips, table, footer); window stays resizable.
- All widgets are uicontrol 'pushbutton'/'text' so we stay inside the
  classical-figure widget family (uibutton is uifigure-only).
- onCloseRequest_ unchanged in ordering: stop+delete RefreshTimer_
  BEFORE Listeners cleanup. New handles cleared on close.
- Tests: +4 pure-logic cases (Units search, Labels search, chip AND/OR
  semantics, zero-chip-group excludes all) + 2 UI cases (label exists
  on open, label updates after tick) via new test seams
  lastRefreshedLabelForTest / tickForTest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New uicontrol pushbutton on the last-refreshed header row labelled
  "Pause polling"/"Resume polling" (classical figure -> uicontrol family)
- Public method setPollingActive(tf) drives the flow; the button's
  callback delegates to it so click + programmatic paths are identical
- Pause stops RefreshTimer_ without deleting it; resume re-starts the
  same handle (with startRefreshTimer_ fallback if it died)
- Resume fires a synchronous one-shot onRefreshTick_ so the table is
  immediately fresh instead of waiting up to 1 s for the next tick
- markTagsDirty is now a no-op while paused: the user's mental model
  is "polling off -> table is frozen". No coupling to FastSenseCompanion
- Header label appends " (paused)" suffix in-place when paused; the
  preceding HH:MM:SS is preserved so the user sees WHEN polling stopped
- 4 new TestTagStatusTableWindow cases cover Running state transitions,
  paused markTagsDirty no-op, and button-label toggling

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface a per-tag event count in the Tag Status Table so users can see
at a glance which tags have produced violations / annotations and which
haven't. The count refreshes with the same 1 s polling cadence as the
existing Samples column.

- Insert "Events" as column 10 (between Activity and Samples); Samples
  shifts to col 11, Labels to col 12. RowBuffer_ widened to 12 columns.
- Add static helper countEventsForTag_(tag): pure function returning a
  non-negative integer; defers to Tag.eventsAttached() (which itself
  wraps EventStore.getEventsForTag, the same path CompanionEventViewer
  uses). Never throws; returns 0 for missing/empty EventStore.
- Add private precomputeEventCounts_(keys): single call site that
  buckets event counts for the listed keys; threaded through
  rebuildAll_, markTagsDirty, and onRefreshTick_ so each tick does ONE
  consolidated walk per dirty key set.
- Extend buildRow_ with optional 3rd arg eventCountsByKey: when present
  and the key is a hit, the count reads from the precomputed map;
  otherwise falls back to countEventsForTag_(tag). Backward compatible
  with 2-arg callers.
- Shave column widths so all 12 columns fit in the default 1100 px
  window without horizontal scroll; window stays resizable.
- Shift rowMatchesSearch_'s Labels column index from 11 to 12.
- Update existing tests for the new 12-col row shape; add 4 pure-logic
  tests (countEventsForTag_ with/without EventStore, buildRow_ Events
  column placement, bucketed-map precedence) and 1 UI test
  (testEventsCountColumnPopulatedFromRegistry: tag with 3 stubbed events
  -> Events='3'; tag with none -> Events='0').

No touch to FastSenseCompanion.m. All errors stay under the existing
FastSenseCompanion:tagStatusTable* namespace. mh_style/mh_lint/mh_metric
clean; 24/24 pure-logic + 16/16 UI + 64/64 TestFastSenseCompanion pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflict resolution in libs/FastSenseCompanion/FastSenseCompanion.m:
- Properties: kept both new private fields (TagStatusTableWindow_ +
  hTagStatusBtn_ from this branch; OpenedFigures_ + hTileBtn_ +
  hCloseAllBtn_ from main PR #143)
- Inner toolbar grid: combined 1x5 (ours: Events|Live|Tags|spacer|gear)
  and 1x6 (main: Events|Live|Tile|Close all|spacer|gear) into a 1x7:
  Events | Live | Tags | Tile | Close all | spacer | gear.
  Column widths {110, 110, 110, 70, 90, '1x', 36}.
- Button blocks: re-numbered main's Tile/Close all/gear columns
  3->4, 4->5, 6->7. Tags ↗ stays at col 3 next to Live.
- Test seams: kept all four hidden test getters
  (tagStatusTableWindowForTest_, scanLiveTagUpdatesForTest_,
  getOpenedFiguresForTest_, trackOpenedFigureForTest_).

Verified post-merge:
- test_companion_tag_status_table        24/24 PASS
- TestTagStatusTableWindow               16/16 PASS
- test_companion_tile_close_buttons       9/9 PASS
- TestFastSenseCompanion                 64/64 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HanSur94 and others added 2 commits May 19, 2026 10:23
Adds the 260519-bs4 row to the Quick Tasks Completed table (Verified,
6 feature commits + 1 merge commit) and refreshes the Last activity
lines after merging main into the branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI MATLAB Lint flagged 5 continuations where && started the new line
instead of ending the previous line. Pure formatting — no semantic
change. Affected:
- libs/FastSenseCompanion/FastSenseCompanion.m lines 905, 1425, 1442, 1443
- tests/test_companion_tag_status_table.m line 543

24/24 pure-logic tests still PASS.

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

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 21.81818% with 43 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
libs/FastSense/build_mex.m 0.00% 41 Missing ⚠️
libs/Dashboard/DashboardToolbar.m 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

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: e80349d Previous: 439f18b Ratio
Instantiation mean std(5M) 0.896 ms 0.224 ms 4
Render mean std(5M) 2.176 ms 1.372 ms 1.59
Zoom cycle mean std(5M) 0.542 ms 0.455 ms 1.19
Instantiation mean std10M) 1.297 ms 0.989 ms 1.31
Render mean std10M) 2.562 ms 0.494 ms 5.19
Zoom cycle mean std10M) 0.647 ms 0.483 ms 1.34
Render mean std50M) 4.647 ms 1.84 ms 2.53
Zoom cycle mean std50M) 0.643 ms 0.425 ms 1.51
Instantiation mean ( std00M) 112.927 ms 45.4 ms 2.49
Downsample mean ( std00M) 3.983 ms 0.689 ms 5.78
Instantiation mean ( std00M) 764.685 ms 45.4 ms 16.84
Render mean (500M) 392.898 ms 352.608 ms 1.11
Render mean ( std00M) 88.784 ms 2.497 ms 35.56
Zoom cycle mean ( std00M) 1.544 ms 0.725 ms 2.13
Dashboard live tick stdmean 1.341 ms 0.573 ms 2.34

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

CC: @HanSur94

CI Octave run failed because test_companion_tag_status_table tried
to load TagStatusTableWindow.m to reach the static helpers, but
the class file uses MATLAB-only syntax (uifigure / uitable /
classdef properties blocks) that Octave cannot parse — fails with
"syntax error near line 40, column 22" on every test case.

Adds the standard Octave skip guard at the top of the test (same
pattern as test_companion_apply_theme_walker, test_companion_open_ad_hoc_plot,
test_companion_inspector_resolve_state).

Companion is MATLAB-only by design (Phase 1018 cross-cutting rule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@HanSur94 HanSur94 force-pushed the claude/compassionate-chaplygin-d944fd branch from 7613a1d to e80349d Compare May 19, 2026 09:38
@HanSur94 HanSur94 merged commit 48e5f46 into main May 19, 2026
33 of 35 checks passed
@HanSur94 HanSur94 deleted the claude/compassionate-chaplygin-d944fd branch May 19, 2026 09:56
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