Node "Chart" tab: overlay comparable child sensors (#1235)#1241
Node "Chart" tab: overlay comparable child sensors (#1235)#1241lotgon wants to merge 17 commits into
Conversation
Selecting a tree node now offers a read-only Chart tab that overlays every comparable child sensor (same type + unit) as one multi-line time chart over an operator-chosen window (last hour / 3h / day / custom). v1 draws the node's largest comparable group; children with no data in-window are omitted and intermittent series are drawn with gaps (never zero-filled). The tab is hidden when a node has fewer than two comparable children. Server: TreeViewModel.GetComparableChildGroups groups descendant sensors by (type, effective unit); SensorHistoryController.NodeChartHistory fans out over the largest group and returns one scalar line series per child (bars flattened to Mean). Client: an inline renderer reuses the bundled Plotly.js to draw the overlay, so no webpack rebuild is required. Docs: aicontext/features/site/node-children-view/feature.md. Closes #1235 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GetComparableChildGroups runs on every node selection (to decide Chart-tab visibility). Keying the groups on the raw unit enum codes instead of the reflection-based display label removes one GetDisplayName() reflection call per descendant sensor; the label is now formatted once per surviving group. Behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Limit the node "Chart" overlay to MaxSensorsPerChart (20) child sensors. The cap is applied to the chosen comparable group before reading history, so it bounds both the number of overlaid lines (legend readability) and the number of per-request history reads (worst-case cost). When the group is larger, the first 20 are shown and the response note reports "N of M"; ranked selection is v2. The client palette is extended to 20 distinct colors to match the cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Instead of always charting the largest comparable group and discarding the rest, the node Chart tab now exposes a "Type" selector next to the Period selector. NodeChartHistory returns all comparable groups (key, label, count) plus the selected group's series; the request carries an optional GroupKey (falls back to the largest group). The client shows the selector only when a node has more than one group and re-queries on change. One unit is charted at a time so the y-axis stays meaningful. Removes the old "other units not shown" note (superseded). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 20-sensor cap was applied to the first ids by tree order before reading history. For intermittent top-N series (per-process CPU, per-interface traffic) those ids are usually idle in any given window, so nearly all were dropped as empty and the overlay collapsed to a single line even though the group had 50+ sensors. Now the endpoint reads every sensor in the group (bounded per sensor plus a NodeChartMaxSensorsScanned=500 ceiling), keeps only those with data in the window, and caps the display to the 20 highest-peak series. The note reports "20 highest-peak of N with data (M in group)". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quality cleanup (no behavior change): - Move the "effective unit" rule (Rate -> DisplayUnit else SelectedUnit) onto SensorNodeViewModel as EffectiveUnitCode/EffectiveUnitLabel; the grouping code reuses them instead of two private copies. - SelectNode now calls a cheap HasComparableChildGroup (short-circuits on the second member of any group) instead of building, labeling and sorting the full group list just to test Count > 0 on every node selection. - NodeChartHistory: drop the eager per-sensor Peak field and the throwaway scannedIds copy; rank by peak lazily only when the display is actually capped. - Client: delete the initNodeChart pass-through (tab onclick calls loadNodeChart directly) and unify the two show/hide toggles into one setInlineFlex helper. Verified live: Top CPU (1h 15 lines, 24h 20 capped+note), MultiTest group selector + switch, CapTest cap, tab visibility unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — PR #1241: Node chart tab (issue #1235)Overall this is a clean, well-scoped feature. The code is nicely commented, the "why" behind the design choices (peak-based capping, gaps vs zero-fill, cheap-vs-full group computation) is documented, and the server/client contract is coherent. Findings below are mostly medium/low; nothing looks like a hard blocker. 🟠 Medium1. Sequential fan-out of history reads — latency & DB load The endpoint reads every sensor in the group sequentially with foreach (var sensorId in group.SensorIds.Take(NodeChartMaxSensorsScanned))
{
...
var points = await ReadNodeChartPoints(sensor, from, to); // one LevelDB paged read at a time
...
}
Suggestions:
At minimum, please confirm the observed latency on a realistically large group is acceptable. 🟡 Low2. Odd namespace: The file lives in 3. No client-side error handling / The AJAX call has a 4. Custom datetime parsing can produce an invalid date const from = fromVal ? new Date(`${fromVal}:00Z`) : ...
5. Series labels rendered into Plotly without escaping (consistency) Group selector options are correctly escaped via 6. Access control relies on
✅ Things done well
Reviewed via |
Address non-blocking review feedback on PR #1235: - Add a .fail handler to the chart request; on failure (500/network/timeout) the tab now shows a "Failed to load chart data" message and clears the chart instead of leaving a stale plot. Empty and error states share one helper. - Parse the custom datetime-local value robustly: append ":00Z" only when the value has no seconds ("...:SS" in some browsers would otherwise yield an Invalid Date and throw in toISOString()), and fall back on unparseable input. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review — PR #1241: Node "Chart" tab (issue #1235)Nice feature. The implementation is well-factored, the XML/inline comments are genuinely useful, and several sharp edges are already handled well: the stale-response 1. Performance — history reads are fully sequential (medium)
foreach (var sensorId in group.SensorIds.Take(NodeChartMaxSensorsScanned)) // up to 500
{
...
var points = await ReadNodeChartPoints(sensor, from, to); // ~2000 points, hits the cache/DB
...
}With 2. Authorization — no per-user/product access check (security consideration)The endpoint takes an arbitrary if (!_tree.Nodes.TryGetValue(nodeId, out var node)) ...Any authenticated user can request any node id and read its children's data regardless of product permissions. This mirrors the existing 3. Scan ceiling silently drops sensors by tree order (low)The code carefully argues (in the foreach (var sensorId in group.SensorIds.Take(NodeChartMaxSensorsScanned)) // first 500 by tree orderFor a group larger than 500, active-but-late sensors get dropped by the same tree-order logic the display cap was designed to avoid — and unlike the display cap, there is no 4.
|
Follow-up to review feedback on PR #1235: - Read the group's sensors with bounded concurrency (SemaphoreSlim, 8) via Task.WhenAll instead of a sequential await loop. Safe for the comparable types (they exclude File, the only kind with shared read state); WhenAll preserves order so non-capped series stay in tree order. Cuts a large group's tab-load latency from the sum of reads toward the slowest. - Emit a note when the NodeChartMaxSensorsScanned (500) scan ceiling truncates the group, so that truncation isn't silent (matches the display-cap note). - Require a '/' boundary in GetNodeRelativeLabel so "a/b" can't prefix-match "a/bc/d". - Escape series labels before injecting them into Plotly name/hovertemplate, consistent with the group-selector option escaping. - Document that EffectiveUnitCode is only unambiguous when Type is part of the key. Verified live: Top CPU processes (1h 19 lines ~20ms, 24h 20 capped + peak-rank + note). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #1241 Review — Node chart tab (issue #1235)Nice feature. The overlay endpoint is well-bounded (scan/points/concurrency caps), reads are pure/read-only, and client-side escaping is applied before injecting user-influenced sensor names into Plotly and 🐞 Bugs / logic errors1.
// HomeController.cs:122-124
StoredUser.SelectedNode.ConnectNode(node);
StoredUser.SelectedNode.ShowChartTab = _treeViewModel.HasComparableChildGroup(id);
Fix: reset 2. A single failing per-sensor read faults the entire chart request (Medium) The endpoint's stated design is graceful degradation ("children with no data … are omitted"), but the read fan-out has no per-sensor // SensorHistoryController.cs — inside the per-sensor lambda
try {
var points = await ReadNodeChartPoints(sensor, from, to);
...
}
finally { readGate.Release(); } // releases, but does NOT catch
...
var built = (await Task.WhenAll(reads))...If any one sensor's 🧹 Code quality / maintainability3.
const token = (window._nodeChartToken = (window._nodeChartToken || 0) + 1);Multiple opened node panels each get their own 4.
🔒 Security5. No per-user authorization on the resolved node (Low / confirm intended)
Minor notes
VerdictSolid, defensively-written feature. Please fix #1 (folder Chart-tab leak) and #2 (whole-request failure on one bad sensor) before merge; #3 is worth doing while nearby. |
…hart-tab # Conflicts: # aicontext/features/site/node-children-view/feature.md
… token Fixes from the PR #1241 automated review: - ShowChartTab leaked onto folders. The reused SelectedNodeViewModel kept a node's `true` when a folder was then selected, so _ChildrenPanel rendered a Chart tab for the folder; clicking it hit NodeChartHistory with a folder id (not in _tree.Nodes) and returned empty/broken. Reset the flag in Subscribe on every selection change; the node branch re-enables it. - One faulting child sensor 500'd the whole overlay. The per-sensor read lambda had try/finally but no catch, so a single ToDisplayValue/read throw faulted Task.WhenAll and took down the entire node chart. Catch, log, and drop just that series (same outcome as an empty window) so the rest still renders. - The node chart request token was a single global counter shared by every open node panel, so two nodes loading near-simultaneously cancelled each other and one stayed blank. Scope the token per node on the container's dataset. - Guard noteEl/noData in renderNodeChart, matching showNodeChartMessage. Docs: node-children-view feature.md invariants updated for the tab-visibility reset and the drop-on-read-failure behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #1241 Review — Node chart tab (issue #1235)Reviewed Overall this is a well-scoped, carefully defensive change. The new 🔒 Security / access control1. New data-exposure endpoint has no per-product authorization check. (
This is consistent with the existing 🐛 Correctness2. public int? EffectiveUnitCode => Type is SensorType.Rate
? (DisplayUnit.HasValue ? (int)DisplayUnit.Value : null) // ignores SelectedUnit
: (SelectedUnit.HasValue ? (int)SelectedUnit.Value : null);
public string EffectiveUnitLabel => Type is SensorType.Rate && DisplayUnit.HasValue
? DisplayUnit.Value.GetDisplayName()
: SelectedUnit?.GetDisplayName() ?? string.Empty; // falls back to SelectedUnitFor a Rate sensor with 3.
⚡ Performance4. Every chart load reads up to 500 sensors × 2000 points, even though only 20 are shown. ( The design reads all scanned sensors and then caps the display to the 20 highest-peak (the comment explains why capping pre-read would drop intermittent series — that reasoning is sound). But this means each tab open and each period/group change fans out up to 5. It walks all descendant sensors via 🧹 Maintainability6. The entire Each opened node with a chart tab renders its own copy of the IIFE that (re)assigns 7. Reliance on global
8. ✅ Things done well
Nice work overall — items #1 (confirm authz) and #2 (unit code/label consistency) are the ones I'd want resolved before merge; the rest are follow-ups. |
…otly guard Follow-up to the re-review after the merge/parallelize commits. It confirmed the earlier fixes and raised new items on pre-existing code: - EffectiveUnitCode vs EffectiveUnitLabel diverged for a Rate sensor without a DisplayUnit: the code was null (SelectedUnit ignored) but the label fell back to SelectedUnit. Two such sensors with different SelectedUnit shared the null group key yet showed conflicting units, overlaid on one axis with an arbitrary ids[0] label. Branch the label on the same source as the code (Rate -> DisplayUnit only; unit-less when absent). - NaN/Infinity scalars serialized as JSON "NaN"/"Infinity" string literals (AllowNamedFloatingPointLiterals) and Plotly can't plot a string as a number. Drop non-finite points in TryGetScalar (double + double-bar cases) like no data. - renderNodeChart/showNodeChartMessage called window.Plotly.* unguarded: if the bundle failed to load, the ajax .done handler threw uncaught. Guard purge and short-circuit newPlot with a "Chart library failed to load" message. - nodeChartEscape now also escapes ' (defensive; option markup is double-quoted). Deliberately deferred as v1 follow-ups (the review filed them as non-blocking): per-interaction read fan-out (correctness tradeoff for intermittent series), HasComparableChildGroup per selection (measured cheap), and moving the per-node script into a bundle (would require the webpack rebuild this PR avoids by design). Docs: feature.md invariants updated for unit code/label parity and NaN dropping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — PR #1241: Node chart tab (issue #1235)Overall this is a well-structured, well-commented change. The overlay endpoint is defensive (per-sensor 🔐 Security / authorization1. No product-level authorization on if (!_tree.Nodes.TryGetValue(nodeId, out var node)) ...There is no Note this matches the existing 🐞 Correctness2. Nondeterministic default group / selector order on ties. return result.OrderByDescending(group => group.SensorIds.Count)
.ThenBy(group => group.UnitLabel)
.ToList();Two groups with the same count and same ⚡ Performance3. Read amplification: up to 500 full-window reads to display ≤20 series. var scannedIds = group.SensorIds.Take(NodeChartMaxSensorsScanned).ToList(); // up to 500
...
var shown = withData > MaxSensorsPerChart ? built.OrderByDescending(...).Take(20) : built;So for a large group the server issues up to 500 history reads (each up to 2000 points), materializes them all, then discards ~480. The rationale (top-N intermittent series) is sound, but the cost is real and per-request. Consider a cheaper pre-rank — e.g. last-value / recent-activity from the cache — to shortlist candidates before doing full-window reads, or lowering 4. 🧹 Code quality / maintainability5. Inline 6. Minor
✅ Things done well
Automated review — please verify findings against intended behavior before acting. |
Third-round review: GetComparableChildGroups sorted by (count desc, UnitLabel), but groups come from an unordered Dictionary and two groups can tie on both keys (e.g. a unit-less Integer group and a unit-less Double group, both label ""). That left groups[0] — the default overlaid group when the client sends no GroupKey — and the Type-selector order able to flip between requests. Add a final .ThenBy(Key) (unique per (Type, UnitCode)) for a stable total order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — PR #1241: Node Chart tab (overlay comparable child sensors, #1235)Reviewed Security1. No per-user product authorization on
if (!_tree.Nodes.TryGetValue(nodeId, out var node)) return _emptyJsonResult;
var groups = _tree.GetComparableChildGroups(nodeId);
...
_tree.Sensors.TryGetValue(sensorId, out var sensor)The tree view is filtered per user ( Note this is consistent with the existing endpoints in the same controller — Correctness / UX2. Per-sensor truncation to the newest 2000 points is silent (no note). (Low–Medium)
3. "Type" group-selector labels can go stale. (Low) In const sig = groups.map(g => g.key).join('|');
if (sel.dataset.sig !== sig) { /* rebuild options */ }But each option label embeds the group count ( Code quality / maintainability4. 250-line inline
5. Top-20 selection ranks by
6. Traces use Things done well (kept, not asking to change)
No blocking issues. Recommend confirming item 1 (authorization) before merge, and consider item 2's note. The rest are low-priority polish. |
… count change
Follow-ups from the latest review (no blocking bug found):
- The per-sensor 2000-point cap (NodeChartMaxPointsPerSensor) keeps the most-recent
values, so a dense series over a long window silently showed only its tail — unlike
the scan cap and the 20-line cap, which both add a note. ReadNodeChartPoints now
reports whether it hit the cap and the endpoint adds a note when any shown series is
truncated, so long-window partial data is no longer silent.
- The "Type" group selector rebuilt its options only when the set of group keys
changed, but each label embeds the group's live count ("Integer bar, % (7)"). A
count change with an unchanged key set left a stale "(N)". Include the label in the
cache signature so counts refresh.
Deliberately deferred (review filed non-blocking): moving the per-node inline script
into a bundle (needs the webpack rebuild this PR avoids), top-20 peak ranking dropping
low-interesting series (v2 ranking work), and scattergl WebGL contexts (one chart is
live at a time in practice). Authorization model raised again — product access is
enforced at the tree-view layer, not these history read endpoints; consistent with
the existing ChartHistory/TableHistory siblings, pending an explicit product decision.
Docs: feature.md bounds note updated (all three caps now surface a note).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review — PR #1241: Node "Chart" tab (issue #1235)Reviewed Overall this is a high-quality, well-reasoned PR. The code is unusually well-commented, the edge cases that are usually missed are handled deliberately (per-node request token, non-finite value filtering, bounded concurrency with correct semaphore release, per-sensor exception isolation, deterministic group ordering, XSS escaping on all user-influenced strings). I did not find any correctness-breaking bug. The notes below are mostly hardening / maintainability. Security1. No product-level authorization on the endpoint (Low–Medium — confirm tree scoping). This is consistent with the sibling 2. XSS — handled correctly. Sensor labels and group labels are escaped ( Performance3. Heavy, uncached per-request fan-out. 4. 5. Concurrency-safety claim verified. The comment justifying parallel reads ("comparable types exclude File, the only sensor kind with shared read state") is correct — I confirmed in Bugs / LogicNo functional bugs found. Details I checked and that hold up:
6. Minor — "most recent" truncation depends on the shared cache path. Code quality / maintainability7. Large inline 8. Nit — group-selector reset. 9. Nit — doc wording. The comment in VerdictSolid, defensive implementation with excellent inline documentation. No blocking bugs. The one item worth an explicit decision before merge is #1 (authorization) — confirm the global-tree lookup is acceptable given your access model (it matches existing history endpoints, so likely yes). The rest are optional hardening/cleanup. |
…tory The endpoint read every sensor in the comparable group's history (up to a 500 scan ceiling) just to rank by in-window peak and then drop all but 20 — a heavy DB burst on every tab open / period change for a large group (e.g. per-process CPU can be dozens–hundreds of sensors). The review flagged this read amplification repeatedly. Now the endpoint shortlists the top MaxSensorsPerChart children by each child's CURRENT value — already in memory (SensorNodeViewModel.LastValue), no DB read — and reads history ONLY for those. So the DB fan-out is bounded to the shortlist (15), not the whole group. Ranking the shortlist by live value (not tree order) is what still surfaces the hot intermittent series: the earlier tree-order-first cap collapsed the chart to one line because the first ids by path are usually idle in any window. Tradeoff: a child that peaked earlier in the window but is idle now can fall outside the shortlist — fine for the recent-window common case; documented for custom historical windows. - MaxSensorsPerChart 20 -> 15 (per request); it now also bounds the read count. - Removed NodeChartMaxSensorsScanned (500) — no longer read the whole group. - Note reworked: "Group has N sensors; charting the 15 with the highest current value." Docs: feature.md invariant / data-flow / bounds updated for the pre-rank strategy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #1241 Review — Node Chart tab (issue #1235)Reviewed Overall this is a clean, well-documented feature. The hard parts are handled well: per-node request tokens defeat response races, Security1. No product-level authorization on This matches the rest of Correctness / Docs2. Documentation contradicts the shipped behavior ("largest group only"). 3. Maintainability / Performance (minor)4. Palette comment is inaccurate. 5. Duplicated grouping traversal. 6. Per-node 7. Stale Nits
Nice work overall — the concurrency, race-token, and escaping details are done right. The main asks are (2) fixing the "largest group only" docs and (1) confirming the authorization assumption. |
… selector/palette Latest review follow-ups (no blocking bug; these are correctness/doc fixes): - Stale docs said "v1 overlays the node's largest (type, unit) group only" in the NodeChartHistory summary and the NodeChartRequest doc (and thus the regenerated Swagger XML), contradicting the shipped Type/group selector. Reworded both to describe (type, unit) grouping + GroupKey selection + the top-N-by-current-value shortlist; Swagger XML regenerated. - truncated note false-positived when a series had exactly the cap (2000) points in the window (nothing actually dropped). Over-read by one and set truncated only when the count exceeds the cap, then trim the extra so the series still honors the cap. - Palette comment claimed "20 ... matches MaxSensorsPerChart"; the cap is now 15. The 20-colour palette comfortably covers 15, so reworded the comment (palette unchanged). - The Type selector kept stale <option>s/dataset.sig when a node dropped from >1 group to <=1; clear them so re-expanding rebuilds the options. Left as deliberate: authorization model (product-oriented, ship-as-is per owner); the GetComparableChildGroups / HasComparableChildGroup duplication (kept explicit — one early-exits for the cheap existence check, one builds full groups); per-node inline script (moving to a bundle needs the webpack rebuild this PR avoids). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #1241 Review — Node Chart tab (issue #1235)Overall this is a well-crafted, carefully-commented feature. The endpoint bounds its DB fan-out, isolates per-sensor read failures instead of failing the whole overlay, escapes user-influenced labels before injecting into Plotly, and uses a per-node request token to avoid cross-node response clobbering. Nice work. A few things worth addressing before merge, none of them blockers. Security / access control (medium)
var nodeId = request.NodeId.ToGuid();
if (!_tree.Nodes.TryGetValue(nodeId, out var node)) ...
var groups = _tree.GetComparableChildGroups(nodeId);Any authenticated user who supplies a node Guid gets back history and structural metadata (group list, counts, sensor-relative paths) for that node's entire subtree — regardless of whether they have product access to it. This mirrors the pre-existing pattern in There is also no Correctness / UXIntermittent series won't actually render "with gaps". The view sets Ranking candidates by current value is misleading for custom historical windows. The shortlist is chosen as follows (before any history read): .OrderByDescending(s => TryGetScalar(s.LastValue, out var v) ? v : double.NegativeInfinity)
.Take(MaxSensorsPerChart)The inline comment acknowledges this trades away "peaked-earlier-but-idle-now" sensors, which is reasonable for the default recent windows. But for a MaintainabilityMisleading comment in
Things done well
🤖 Generated with Claude Code |
…omment Review follow-ups: - connectgaps:false alone never produced gaps. It only breaks the line at null y-values, but the server omits idle periods instead of sending nulls, so Plotly drew a misleading straight segment across an inactive stretch (e.g. a top-N process that dropped out and came back), contradicting the "drawn with gaps, never interpolated" promise. The client now inserts a null y wherever the gap between consecutive samples exceeds ~4x the series' median spacing, so connectgaps:false breaks the line into a real gap. Verified the transform with a node harness (regular series -> 0 breaks; a long idle stretch -> 1 null placed inside it; single/empty safe). - Corrected the SelectedNodeViewModel.Subscribe comment: it implied Subscribe covers the sensor case, but selecting a sensor never calls Subscribe. It's safe only because the sensor panel doesn't render _ChildrenPanel — now stated. Docs: feature.md gap-rendering invariant updated to describe the null insertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #1241 Review — Node Chart Tab (issue #1235)Reviewed Overall this is a well-scoped, carefully implemented feature. The code is unusually well-commented — most non-obvious decisions (truncation over-read, semaphore bounding, gap insertion, unit-key ambiguity, request-token cancellation) are explained inline and the reasoning holds up. The XSS surface is handled correctly, the DB fan-out is bounded, and failure of a single child read is contained. Below are the issues I found, ordered by severity. Nothing here is a hard blocker, but a couple are worth addressing before merge. 1. No product-level authorization check on
|
Review — PR #1241: Node chart tab (issue #1235)Scope: new Overall this is a well-engineered change. The comments are unusually thorough and correctly capture most of the non-obvious tradeoffs (over-read for truncation detection, per-node request token, gap insertion, non-finite filtering, group-key stability). I verified several things that looked risky and they turned out to be fine — most notably 1. Access control — endpoint returns history for any node GUID, no product-role check (Medium — please confirm)
This mirrors the existing
Suggested: confirm whether product-level read authorization is expected for history endpoints; if so, add the same check used elsewhere for the resolved 2. Full descendant traversal on every node selection (Low–Medium, performance)
Not incorrect, and node selection already does some O(children) work, but consider short-circuiting inside the traversal (a variant of 3. Truncation note can false-positive (Low, cosmetic)
4. Shortlist by current value can hide the true in-window peak (informational)Candidates are picked by 5. Minor / nits
Recommendation: Approve pending a decision on point #1 (access control posture). Points #2–#5 are non-blocking follow-ups. |
Summary
Adds a read-only Chart tab to a tree node that overlays its comparable child sensors' history as one multi-line time chart over an operator-chosen window (last hour / 3h / day / custom). Read-only, derived entirely from stored history — never writes or changes sensor state.
Closes #1235
What the tab does
(SensorType, effective unit)— numeric line-able types only (Integer/Double/Rate/IntegerBar/DoubleBar; excludes enum/bool/version/timespan/string + service status/alive). Bars are flattened to theirMean.(type, unit)group, the operator picks which one to overlay (one unit at a time, so the y-axis stays meaningful). Defaults to the largest group.Implementation
TreeViewModel.GetComparableChildGroups/HasComparableChildGroup;SensorHistoryController.NodeChartHistory(POST, body{ nodeId, groupKey?, from, to }→{ error, unit, note, selectedKey, groups[], series[] });SelectNodesetsSelectedNodeViewModel.ShowChartTab._NodeChartTabContent.cshtmlreuses the already-bundled Plotly.js (palette-cycledscattergl, shared legend) — no webpack rebuild.aicontext/features/site/node-children-view/feature.md.Verified locally (server built from this branch, real + seeded data)
Top CPU processes(52-sensor group): last hour → 15 lines, 24h → 20 (capped, peak-ranked), values match; fixed an earlier collapse-to-one-line bug where the cap picked idle sensors by tree order.Out of scope (tracked for later)
wwwroot/src/ts) and reuseChartHelper.initMultiChart.🤖 Generated with Claude Code