Commit Graph

1526 Commits

Author SHA1 Message Date
Joseph Doherty eb4bce3e49 refactor(kpi): K13/K15 trend review fixups — per-metric isolation, disable-during-load + logging, loading-flag finally, test coverage 2026-06-17 20:44:34 -04:00
Joseph Doherty 7d7c6cbb05 feat(kpi): K16 — Health dashboard per-site trend panel 2026-06-17 20:36:09 -04:00
Joseph Doherty 3595a41349 feat(kpi): K15 — Audit Log trend charts 2026-06-17 20:30:38 -04:00
Joseph Doherty 4a88355098 feat(kpi): K14 — Site Calls trend charts 2026-06-17 20:30:36 -04:00
Joseph Doherty 0dc819f191 feat(kpi): K13 — Notification Outbox trend charts (T11 first consumer) 2026-06-17 20:29:30 -04:00
Joseph Doherty f0177d5073 feat(kpi): K11 — KpiHistoryQueryService (scoped read + bucketing) 2026-06-17 20:21:17 -04:00
Joseph Doherty e14433cd64 feat(kpi): K5 — Host central wiring + KpiHistoryRecorder cluster singleton + appsettings (not readiness-gated)
Wire the M6 KPI History recorder into the central composition path:
- Program.cs: call services.AddKpiHistory(configuration) on the central-only
  branch alongside AddNotificationOutbox/AddAuditLog/AddSiteCallAudit.
- AkkaHostedService.cs: register KpiHistoryRecorderActor as a central,
  non-role-scoped ClusterSingletonManager + ClusterSingletonProxy + a
  PhaseClusterLeave CoordinatedShutdown graceful-stop drain (singleton name
  'kpi-history-recorder'), copied/adapted from the audit-log-purge block.
- appsettings.Central.json (Host + docker + docker-env2 central nodes): add a
  ScadaBridge:KpiHistory section (SampleInterval 00:01:00, RetentionDays 90,
  PurgeInterval 1.00:00:00, DefaultMaxSeriesPoints 200).

KPI history is observability/best-effort and MUST NOT gate readiness: the
recorder is deliberately NOT added to RequiredSingletonsHealthCheck or any
other readiness gate.
2026-06-17 20:20:34 -04:00
Joseph Doherty 601cc6f594 feat(kpi): K9 — SiteHealth sample source (per-site, from aggregator) 2026-06-17 20:20:18 -04:00
Joseph Doherty cb2a516187 refactor(kpi): K4/K10/K12 review fixups — test data-race + faulted-tick liveness, dead-branch/unused removal, NaN-guard assertions, value clamp + doc 2026-06-17 20:15:47 -04:00
Joseph Doherty 5613a5efb7 feat(kpi): K12 — reusable KpiTrendChart SVG component 2026-06-17 20:06:31 -04:00
Joseph Doherty 9c2e7ab4cb feat(kpi): K4 — KpiHistoryRecorderActor (best-effort sampling + daily purge) 2026-06-17 20:06:09 -04:00
Joseph Doherty 76f5ed72e4 feat(kpi): K10 — KpiSeriesBucketer last-per-bucket downsampler 2026-06-17 20:04:51 -04:00
Joseph Doherty e6c15250ce refactor(kpi): K2/K6/K7 review fixups — empty-batch guard + sealed repo + uniform TryAddEnumerable + KPI-age doc fidelity + coverage 2026-06-17 20:00:43 -04:00
Joseph Doherty 456e61dff3 feat(kpi): K7 — SiteCallAudit sample source 2026-06-17 19:53:49 -04:00
Joseph Doherty 6f6157ce89 feat(kpi): K8 — AuditLog sample source 2026-06-17 19:53:41 -04:00
Joseph Doherty 0d6c026dff feat(kpi): K6 — NotificationOutbox sample source (global/site/node) 2026-06-17 19:53:39 -04:00
Joseph Doherty 9ffa34d3e7 feat(kpi): K3 — KpiHistory project + options/validator + AddKpiHistory 2026-06-17 19:48:59 -04:00
Joseph Doherty cabc557629 feat(kpi): K2 — KpiSample EF mapping + KpiHistoryRepository + AddKpiSampleTable migration 2026-06-17 19:44:51 -04:00
Joseph Doherty 460777bffa feat(kpi): K1 — KpiSample + IKpiSampleSource + IKpiHistoryRepository contracts (Commons) 2026-06-17 19:40:17 -04:00
Joseph Doherty 4c6ae9da0e docs(m6): KPI History tasks.json (K1-K17 with blockedBy graph) 2026-06-17 19:32:58 -04:00
Joseph Doherty 1809664bcb docs(m6): KPI History & Trends implementation plan (K1-K17, bite-sized tasks + execution waves) 2026-06-17 19:32:58 -04:00
Joseph Doherty 6084e56c9f docs(m6): KPI History & Trends design — reusable tall/EAV KPI-history backbone + trend charts for all sources; T9/T10 (Teams) deferred to next major version 2026-06-17 19:32:58 -04:00
Joseph Doherty 59e094ada3 test(playwright): DebugViewTree — tolerate empty alarm forest (TreeView renders EmptyContent, not role=tree, when no alarms) 2026-06-17 16:08:37 -04:00
Joseph Doherty e7660134f2 fix(communication): drop IsConfiguredPlaceholder rows in StreamRelayActor before gRPC pack
Placeholder AlarmStateChanged rows are a DebugView snapshot-only concept emitted
by InstanceActor.BuildAlarmStatesSnapshot; they are never a real alarm transition.
Their timestamp may be DateTimeOffset.MinValue (the Protobuf Timestamp lower boundary),
which can throw when packed via Timestamp.FromDateTimeOffset.

Added early-return guard at the top of HandleAlarmStateChanged before any timestamp
pack or channel write. Updated the existing NativeBindingLinkage round-trip test to
use a real (non-placeholder) native alarm; added DropsAlarmStateChanged_WhenIsConfiguredPlaceholder
to assert placeholders are silently dropped (15/15 pass).
2026-06-17 15:44:28 -04:00
Joseph Doherty 7f59ae12cb test(playwright): DebugView tabs+trees — assert tabbed layout, tree panes, Alarms-tab switch on connected instance 2026-06-17 15:33:28 -04:00
Joseph Doherty ef86a2db28 refactor(debugview): cosmetic polish — test-seam comment, default-arm comment, tighten severity assertion 2026-06-17 15:30:18 -04:00
Joseph Doherty 50ce26f2e6 feat(centralui): DV-5 — Debug View tabbed composition trees (Attributes/Alarms)
Replace the two flat capped tables with a Bootstrap nav-tabs layout, each
tab hosting a TreeView<DebugTreeNode> built from the live latest-per-name
dictionaries via DebugTreeBuilder. Drop the MaxRows cap, auto-scroll locks,
and Clear buttons (change-feed affordances that don't fit a current-status
tree); HandleStreamEvent now does a plain dictionary upsert. Per-tab filters
ExpandAll on change so matches stay visible. Branch nodes surface roll-up
badges (active-count for alarms, bad-quality for attributes); native binding
nodes show active-count or 'no active conditions'. All existing badge helpers
and ValueFormatter reused. Marshalling/dispose/reconnect contract preserved
(SafeInvokeAsync/_disposed/Dispose unchanged; FilteredAttributeValues kept as
the render-thread dict reader the CentralUI-021 race test exercises).

Rework DebugViewAlarmTableTests for the tabbed-tree DOM: tab presence+default,
computed alarm grouped under its Motor1 branch with the active roll-up badge,
and a native condition nested under its source-binding node with the enriched
kind/severity/Unacked/Shelved badge set.
2026-06-17 15:23:49 -04:00
Joseph Doherty 59f135a4cf docs(DV-6): document Debug View tabbed-tree layout, native placeholders, and new AlarmStateChanged fields
- Component-CentralUI.md: replace flat-table Debug View section with tabbed
  tree layout (Attributes + Alarms tabs, TreeView<TItem> reuse, hierarchy from
  canonical names, branch roll-up, all-configured-alarms rule, native source
  binding nodes with quiet-binding placeholder rows, per-leaf rendering detail)
- Component-SiteRuntime.md (Instance Actor Wiring): add idle-binding placeholder
  emission via BuildAlarmStatesSnapshot(), _nativeAlarmKinds map, and
  NativeSourceCanonicalName stamping on live native events
- Component-SiteRuntime.md (Enriched AlarmStateChanged): document two new
  additive fields — NativeSourceCanonicalName (string?) and
  IsConfiguredPlaceholder (bool) — plus their gRPC proto fields 22/23 and
  StreamRelayActor/SiteStreamGrpcClient pack/unpack
- Component-Commons.md (Attribute Stream DTOs): extend AlarmStateChanged bullet
  with the same two additive fields and proto field numbers
2026-06-17 15:21:42 -04:00
Joseph Doherty c1e786e3fc refactor(DebugTreeBuilder): DRY BuildAttributeTree via WalkBranches; guard empty NativeSourceCanonicalName 2026-06-17 15:18:09 -04:00
Joseph Doherty 5f387ef3e3 feat(debugview): DV-4 implement BuildAlarmTree (computed leaves, native binding nodes, roll-up, filter)
Computed alarms place as leaves at their path-qualified AlarmName; native conditions group under a deduped IsNativeBinding branch keyed by NativeSourceCanonicalName with condition children keyed canonical::sourceRef. Configured-placeholder events materialise a childless binding node. Alarm roll-up (WorstState/ActiveCount) excludes placeholders. Filter matches AlarmName/SourceReference/NativeSourceCanonicalName (OrdinalIgnoreCase) and retains ancestor + binding branches. 20 new TDD cases; 18 attribute cases stay green. No DebugTreeNode model changes.
2026-06-17 15:12:57 -04:00
Joseph Doherty 69b83379d5 test(dv-3): add 4-level roll-up + deep-leaf filter tests; return AsReadOnly; add caller-contract remark
Fix 1 (Important): RollUp_FourLevelDeepBadQuality_ReachesRoot — proves bad quality at a
4-segment-deep leaf propagates HasBadQuality up every ancestor to the root.

Fix 2 (Important): Filter_DeepLeafMatch_RetainsAllAncestorBranches — proves filtering on
a terminal segment of a 3-level path retains all ancestor branches.

Fix 3 (Minor): BuildAttributeTree now returns roots.AsReadOnly() so the returned
IReadOnlyList<DebugTreeNode> reference is not a mutable list.

Fix 4 (Minor): Added <remarks> XML doc to BuildAttributeTree noting the caller-contract
that at most one AttributeValueChanged per AttributeName should be passed.

All 18 DebugTreeBuilder tests pass.
2026-06-17 15:09:01 -04:00
Joseph Doherty b5347faf44 fix(DV-2): clarify placeholder comment, stable MinValue timestamp, drain DCL probe in new tests
- Replace placeholder-loop comment with the double-render guard explanation
- Use _alarmTimestamps.GetValueOrDefault(binding, DateTimeOffset.MinValue) so the
  placeholder timestamp is stable/idempotent across snapshot calls (was UtcNow)
- Add dcl.ExpectMsg<SubscribeAlarmsRequest>() drain in Snapshot_QuietNativeBinding_EmitsPlaceholder
  and Snapshot_NativeBindingWithLiveCondition_NoPlaceholder to consume the DCL message
  the NativeAlarmActor sends at startup
2026-06-17 15:08:37 -04:00
Joseph Doherty cc017aabfc feat(debugview): DV-3 DebugTreeNode model + attribute tree builder
Pure path-split composition forest from streamed AttributeValueChanged: branch dedupe by accumulated prefix, ordinal child sort, post-order bad-quality roll-up, case-insensitive name-contains filter (keeps ancestors). BuildAlarmTree left as a NotImplementedException stub for DV-4. 16 unit tests cover structure + roll-up + filter.
2026-06-17 15:01:02 -04:00
Joseph Doherty 5d07ac24cb feat(debugview): DV-2 emit placeholder rows for quiet native alarm bindings
InstanceActor.BuildAlarmStatesSnapshot now adds an IsConfiguredPlaceholder
row per configured native source binding that currently has no live
condition, so the Debug View tree can show the binding node even when
quiet. A binding is "quiet" when no retained AlarmStateChanged carries its
NativeSourceCanonicalName (DV-1).

Kind derivation: reuses the exact nativeKind value already computed via
ResolveNativeKind(nativeSource.ConnectionName) at the NativeAlarmActor
creation site and stored in a new _nativeAlarmKinds dictionary -- the
accurate per-binding kind (NativeOpcUa vs NativeMxAccess), not the
NativeOpcUa default.

Tests: Snapshot_QuietNativeBinding_EmitsPlaceholder,
Snapshot_NativeBindingWithLiveCondition_NoPlaceholder.
2026-06-17 15:00:20 -04:00
Joseph Doherty 899ad6e106 feat(debugview): DV-1 native-binding linkage on AlarmStateChanged contract chain
Add two additive init-only fields to AlarmStateChanged so the Debug View can
nest live native conditions under their configured source-binding node:
  - NativeSourceCanonicalName (binding canonical name, e.g. "Motor1.MotorAlarms")
  - IsConfiguredPlaceholder (quiet-binding placeholder flag; default false)

Flow on BOTH cross-process paths:
  - Live: proto AlarmStateUpdate fields 22/23 -> StreamRelayActor packs ->
    SiteStreamGrpcClient unpacks (regenerated SiteStreamGrpc/Sitestream.cs).
  - Snapshot (Newtonsoft): record defaults carry through; no special handling.

NativeAlarmActor.Emit now stamps NativeSourceCanonicalName = _source.CanonicalName.
Additive-only: no existing positional constructor or wire frame changed.

Tests: StreamRelayActorTests round-trips both fields pack->unpack;
NativeAlarmActorTests asserts the emitted event carries the binding canonical name.
2026-06-17 14:52:03 -04:00
Joseph Doherty 1045e7966d docs(plans): implementation plan for Debug View tabs + hierarchy trees
7 tasks (DV-1..DV-7): additive AlarmStateChanged native-binding contract
chain, site snapshot native placeholders, DebugTreeNode + pure builder
(attribute + alarm trees with roll-up/filter), DebugView tabs reusing
TreeView<TItem>, docs, and integration (build + docker + Playwright).
2026-06-17 14:14:58 -04:00
Joseph Doherty 811d72255c docs(plans): design for Debug View tabs + hierarchy trees
Tabbed Attributes/Alarms view with collapsible composition trees derived
from path-qualified canonical names; all configured alarms (computed +
native) shown with current status; branch-level status roll-up; native
source bindings as nodes with conditions nested. Site snapshot enriched
with placeholder rows for idle native sources via an additive
IsConfiguredPlaceholder field on AlarmStateChanged.
2026-06-17 14:06:08 -04:00
Joseph Doherty 670b607acb fix(templateengine): SemanticValidator accepts composition-delegated CallScript (Children[x].CallScript leaf-name match) 2026-06-17 12:43:17 -04:00
Joseph Doherty 0e989c867d feat(siteruntime): event-driven Attributes.WriteBatchAndWaitAsync (batched DCL write + trigger + existing WaitForAttribute waiter) + compile mirror 2026-06-17 12:13:02 -04:00
Joseph Doherty e8db6c71a8 docs(plans): mark WaitAsync compile-surface tasks complete in tasks.json 2026-06-17 11:24:03 -04:00
Joseph Doherty 8dcc55f633 test: address review feedback — cover WaitForAsync predicate overload; clarify ChildrenAccessor parity row; dedup object-method exclusion 2026-06-17 11:16:33 -04:00
Joseph Doherty b88f04ec2d fix(siteruntime): normalize routed WaitForAttribute response value for cross-process transport 2026-06-17 11:10:17 -04:00
Joseph Doherty adc8ee4afa test(scriptanalysis): parity test fails on any unmirrored runtime accessor method 2026-06-17 11:09:00 -04:00
Joseph Doherty bee295d3ee fix(central-ui): mirror WaitForAttribute on inbound-script analysis RouteTarget
Add WaitForAttribute(attributeName, targetValue, timeout, cancellationToken)
to InboundScriptHost.RouteTarget and SandboxInboundScriptHost.RouteTarget,
mirroring the shipped runtime signature in RouteHelper. Eliminates the false
CS error the editor raised against valid Route.To("X").WaitForAttribute(...)
calls in inbound API method scripts. Test asserts the call diagnoses clean
under ScriptKind.InboundApi.
2026-06-17 11:04:13 -04:00
Joseph Doherty a1186685a9 fix(scriptanalysis): mirror WaitAsync/WaitForAsync on CompileAttributeAccessor
Adds the four missing overloads (value + predicate × WaitAsync + WaitForAsync)
to CompileAttributeAccessor so template/call scripts that use Attributes.WaitAsync
or Attributes.WaitForAsync pass design-time Roslyn validation.  Covers both root
scope and composed/child scope (Children["x"].Attributes.WaitAsync) automatically
since CompileCompositionAccessor.Attributes already returns CompileAttributeAccessor.
2026-06-17 11:03:24 -04:00
Joseph Doherty dc43a3f0f6 docs(plans): add WaitAsync compile-surface mirror-gaps plan + tasks 2026-06-17 11:00:41 -04:00
Joseph Doherty c2e89e9d40 fix(central-ui): never render DB connection strings on Integration Definitions list
Connection strings carry credentials; the Database Connections tab rendered the
full string (text + title tooltip) for any Design/Admin user. Replace with a
non-sensitive 'hidden — edit to view' hint so it never reaches the browser DOM.
Connection strings remain editable on the create/edit form. Adds a bUnit
regression guard asserting the seeded secret is absent from the rendered list.
2026-06-17 10:51:18 -04:00
Joseph Doherty af54c8ad11 merge: integrate WaitAsync/M5-audit (parallel session) with galaxy array-write + inbound-timeout fixes 2026-06-17 09:28:15 -04:00
Joseph Doherty bf2f481bb4 fix(siteruntime): normalize routed script return value for cross-process transport
A routed inbound-API call (Route.To(inst).Call(script)) runs the script on
the Site and returns its value to Central inside RouteToCallResponse, which
crosses the Central<->Site PROCESS boundary. A script's natural
'return new { ... }' is a compiler-generated anonymous type that Akka's
cross-process serializer cannot reconstruct on the receiving node, so the
reply was silently dropped and the caller's Route.To().Call() Ask timed out
at 30s with 'Script execution timed out' -- even though the script completed
and all device writes committed.

DeploymentManagerActor.RouteInboundApiCall now projects the routed return
value to a plain CLR graph (Dictionary/List/string/long/double/bool/null)
via a JSON round-trip before placing it in RouteToCallResponse. The graph
round-trips the wire and re-serializes to the same JSON shape the inbound
API expects for the HTTP body / ReturnDefinition validation.

Diagnosed live: IpsenMESMoveIn writes committed + site_events showed the
IpsenMoveIn script completed in ~0.6s, yet the inbound POST returned 500 at
30s; Central's Akka serializer logged 'Writing value of type
<>f__AnonymousType0`1 as Json' at the timeout moment.

379/379 SiteRuntime tests green.
2026-06-17 09:19:12 -04:00
Joseph Doherty 11534089b9 docs(siteruntime): mark WaitAsync deferred items implemented (§3/§4.2/§6) + fast-path throwing-predicate test 2026-06-17 09:15:42 -04:00