Commit Graph

228 Commits

Author SHA1 Message Date
Joseph Doherty 1d7e2a0f8b fix(runtime): reject empty AddComment instead of silently swallowing it
Validate AddComment up-front (IsNullOrWhiteSpace guard + Warning log) so
a blank-comment command is cleanly rejected before reaching the engine
rather than faulting inside ApplyAddComment and being silently swallowed
by the outer catch.  Mirrors the existing TimedShelve missing-UnshelveAtUtc
pattern.

Also fix two stale inline comments: the "async void crash" note on
TimedShelve now correctly says "fault escaping async Task → supervision
restart", and the ownership-filter now documents the benign race with a
concurrent LoadAsync clearing the loaded set.

Tests: AlarmCommand_add_comment_empty_text_is_rejected_not_driven (Theory
— empty string + whitespace) and AlarmCommand_add_comment_nonempty_drives_engine
(positive path, asserts CommentAdded transition on alerts topic).
2026-06-11 06:32:53 -04:00
Joseph Doherty 004558c241 fix(alarms): delta-gate WriteAlarmCondition to suppress inbound ack double-emit (T20)
Inbound client acks now route through the engine (T18/T19). On a successful
Acknowledge the T18 gate returns Good, so the SDK applies the acked state to the
AlarmConditionState node and auto-fires its own condition event (E2) -- directly
on the node, bypassing WriteAlarmCondition. The engine then re-projects that same
transition through WriteAlarmCondition, which fired again (E3): a double-emit.

Gate WriteAlarmCondition's ReportConditionEvent on a genuine delta computed
against the node's CURRENT live state (read before projecting the snapshot), not
a last-written cache (which would be stale, since the SDK-applied ack never went
through this method). For a re-projected ack the snapshot equals the node's
already-applied state -> no delta -> suppress E3. Genuine engine-driven
transitions still differ -> fire.

Compared fields (value-equality via AlarmConditionDelta record): Active, Acked,
Confirmed, Enabled, Shelving (mapped from the shelving state machine), Severity
(mapped through MapSeverity to match the bucket the node stores), Message.
Optional Confirmed/Shelving fold to the node read-back default when the child is
absent so they can't register a phantom delta.

Tests prove both: suppression of the simulated inbound ack re-projection
(EventId unchanged) and that genuine transitions fire while identical
re-projections suppress; plus a direct unit test of the ShouldFireConditionEvent
seam. 102/102 OpcUaServer.Tests green.
2026-06-11 06:26:48 -04:00
Joseph Doherty 4f7999eac2 feat(alarms): consume alarm-commands topic in ScriptedAlarmHostActor (T19)
Subscribe the host to the cluster alarm-commands DPS topic in PreStart and
drive the matching ScriptedAlarmEngine op per inbound AlarmCommand. An
ownership filter (engine.LoadedAlarmIds) ignores commands for alarms this
node does not own; TimedShelve without UnshelveAtUtc and unknown operations
are logged + rejected (never thrown); op failures are caught + logged so a
faulting op can't fault the actor. Re-projection is left to the engine's
existing OnEvent -> OnEngineEmission path.

Handler is a Task-returning ReceiveAsync (the project's AK2003 analyzer
forbids an async-void Receive delegate), giving ordered awaited async on the
actor thread. Adds 3 TestKit tests: ack drives the engine with mapped args,
unowned command ignored, missing-UnshelveAtUtc TimedShelve rejected not
thrown.
2026-06-11 06:23:08 -04:00
Joseph Doherty 1784eedd3f fix(opcua): exempt OnTimedUnshelve from the client AlarmAck gate (system-initiated)
The SDK fires OnTimedUnshelve with the node manager's system context (no
session, no user identity) when a TimedShelve duration expires. Routing
through the shared HandleAlarmCommand hit the AlarmAck gate and returned
BadUserAccessDenied, leaving the alarm permanently shelved.

Replace the delegated HandleAlarmCommand call with an inline lambda that
bypasses the client gate, extracts the AlarmId the same way, and routes an
Unshelve command so the engine clears its shelve state. The manual-client
Unshelve path via OnShelve(shelving:false) remains gated.

Update the AlarmCommandRouterTests OnTimedUnshelve test to use a real
system context (no UserIdentity) — reproducing the actual SDK invocation
path — and assert Good, AlarmId, Operation==Unshelve, User==empty.

Add a doc note to AlarmCommand.Operation that Enable/Disable are in the
vocabulary but not yet wired at the node-manager seam.
2026-06-11 06:16:30 -04:00
Joseph Doherty 63289d377c feat(alarms): route inbound Part 9 alarm methods through AlarmAck gate (T18)
Wire the materialised AlarmConditionState method handlers so a client calling
Acknowledge/Confirm/Shelve/AddComment is gated on the AlarmAck data-plane role
and, when allowed, routed back to the scripted-alarm engine via a new
`alarm-commands` DistributedPubSub topic.

- Commons: new AlarmCommand DTO (AlarmId/Operation/User/Comment/UnshelveAtUtc).
- ScriptedAlarmHostActor: add AlarmCommandsTopic const.
- OtOpcUaNodeManager: settable AlarmCommandRouter + wire OnAcknowledge/OnConfirm/
  OnAddComment/OnShelve/OnTimedUnshelve. Each resolves the principal off
  ISessionOperationContext.UserIdentity as RoleCarryingUserIdentity, fails closed
  (BadUserAccessDenied) when the AlarmAck role is absent or no identity, else maps
  + routes an AlarmCommand and returns Good. OnShelve discriminates OneShotShelve/
  TimedShelve/Unshelve from the SDK flags; TimedShelve expiry = UtcNow + ms.
  No Akka/IActorRef handle — only the Action<AlarmCommand> delegate. T20 de-dup
  note left; WriteAlarmCondition untouched.
- OpcUaServer.Security: OpcUaDataPlaneRoles.AlarmAck shared const (the role was a
  bare string everywhere; introduced one symbol for the gate + tests).
- OtOpcUaSdkServer: SetAlarmCommandRouter pass-through.
- Host: boot wiring publishes each command via mediator.Tell(Publish(...)) using a
  lazy ActorSystem accessor (mirrors DpsScriptLogPublisher).
- Tests: 11 new gate + mapping tests (OpcUaServer.Tests 88->99, all green).
2026-06-11 06:05:39 -04:00
Joseph Doherty a6fed85ac9 feat(security): carry LDAP roles onto session identity (T17)
Stop discarding the authenticator's resolved roles during impersonation.
HandleImpersonation now sets args.Identity to a RoleCarryingUserIdentity
(: UserIdentity) that carries result.Roles, so a downstream method handler
can read them off context.UserIdentity for the inbound AlarmAck gate (T18).

Verified via the decompiled SDK (1.5.378.106) that the instance we assign to
ImpersonateEventArgs.Identity is stored by reference onto Session.Identity /
EffectiveIdentity and surfaced unchanged on OperationContext.UserIdentity --
the custom subclass survives the round-trip. No auth-decision logic changes.
2026-06-11 05:42:27 -04:00
Joseph Doherty 2ad1dbc894 fix(security): AutoLoginAuthenticationHandler no-op sign-in/out (avoid 500 on /auth/logout when flag on) 2026-06-11 04:45:29 -04:00
Joseph Doherty 82fec753c8 feat(security): wire Security:Auth:DisableLogin into AddOtOpcUaAuth 2026-06-11 04:39:23 -04:00
Joseph Doherty caeaae21f9 feat(security): AutoLoginAuthenticationHandler for dev login bypass 2026-06-11 04:31:07 -04:00
Joseph Doherty a92ba6a10b feat(security): add AuthDisableLoginOptions + DevAuthRoles for dev login bypass 2026-06-11 04:26:20 -04:00
Joseph Doherty 295bb55dc6 feat(scripted-alarms): fire Part 9 condition events on transition (T16) 2026-06-10 19:50:09 -04:00
Joseph Doherty 4eb1d65e2b feat(scripted-alarms): richer AlarmConditionState bridge to the OPC UA node (T15) 2026-06-10 19:41:16 -04:00
Joseph Doherty 60d48a2a0a feat(scripted-alarms): materialise real Part 9 AlarmConditionState nodes (T14) 2026-06-10 19:19:10 -04:00
Joseph Doherty a8640a9331 test(scripted-alarms): cover bootstrap-restore path forwarding alarms (T10 review) 2026-06-10 15:24:39 -04:00
Joseph Doherty fc0d43a3dc refactor(scripted-alarms): retire orphaned ScriptedAlarmActor + F9b evaluator (T11) 2026-06-10 15:22:26 -04:00
Joseph Doherty 5256761368 feat(scripted-alarms): spawn + apply ScriptedAlarmHostActor in DriverHostActor (T10) 2026-06-10 15:17:29 -04:00
Joseph Doherty dafaf2faec fix(scripted-alarms): ScriptedAlarmHostActor review fixes — load-gen guard, quiet cancel, parse guard (T9 review) 2026-06-10 15:08:54 -04:00
Joseph Doherty 3b418a54f1 feat(scripted-alarms): ScriptedAlarmHostActor — engine runtime host (T9) 2026-06-10 14:57:42 -04:00
Joseph Doherty 8e8ca9efe8 feat(scripted-alarms): DeploymentArtifact byte-parity for the alarm plan (T6) 2026-06-10 14:41:46 -04:00
Joseph Doherty b28c6bdb62 feat(scripted-alarms): EquipmentScriptedAlarmPlan + Phase7Composer enrichment (T5) 2026-06-10 14:21:28 -04:00
Joseph Doherty 1c96fe0be0 feat(scripted-alarms): EfAlarmConditionStateStore (T8) 2026-06-10 14:21:19 -04:00
Joseph Doherty 945ccd0b85 feat(scripted-alarms): DependencyMuxTagUpstreamSource (T7)
Concrete ITagUpstreamSource the scripted-alarm host actor pushes
DependencyValueChanged values into and ScriptedAlarmEngine reads/subscribes
from. Thread-safe: ConcurrentDictionary value cache + per-path ImmutableList
observer lists with atomic add/remove and capture-then-invoke fan-out.
ReadTag of an unknown path returns a Bad-quality (0x80000000) snapshot stamped
via the injected clock. Adds the Core.ScriptedAlarms project reference Runtime
needs to see the interface.
2026-06-10 14:20:02 -04:00
Joseph Doherty b5748288df test(scriptlog): prove bridge→broadcaster delivery off the script-logs DPS topic
Composes the one Layer-0 hop existing tests left uncovered together:
ScriptLogSignalRBridge subscribing to the script-logs DPS topic and
fanning a ScriptLogEntry out to the IInProcessBroadcaster<ScriptLogEntry>
singleton resolved from the SAME DI container the /script-log page injects.
Mirrors DriverStatusHubE2eTests. Confirms the server-side topic→page chain
delivers end-to-end (only the live Blazor circuit remains manual).
2026-06-10 13:53:34 -04:00
Joseph Doherty bd2dd05a0c feat(scripting): evaluators log through root script logger → script-log page (F8) 2026-06-10 12:03:51 -04:00
Joseph Doherty 73014258ef feat(scripting): root script logger + DPS publisher wired in Host 2026-06-10 11:50:50 -04:00
Joseph Doherty 6c23a6763a test(adminui): DB-backed tests for ScriptTagCatalog.GetEquipmentRelativeLeavesAsync 2026-06-10 08:12:52 -04:00
Joseph Doherty a7c1d7f7cb test(adminui): cover divergent-prefix {{equip}} rejection; use EquipToken constant in message 2026-06-10 08:08:13 -04:00
Joseph Doherty c7041a24e7 feat(adminui): {{equip}}-aware hover + {{equip}}. leaf completion in the script editor 2026-06-10 08:04:51 -04:00
Joseph Doherty cadd6c60b7 feat(adminui): reject {{equip}} virtual tags whose equipment has no derivable base 2026-06-10 07:58:38 -04:00
Joseph Doherty 66ea9c56f6 feat(runtime): DeploymentArtifact substitutes {{equip}} (parity with composer) 2026-06-10 07:53:20 -04:00
Joseph Doherty a4b36c54ba feat(opcuaserver): Phase7Composer substitutes {{equip}} per equipment 2026-06-10 07:49:28 -04:00
Joseph Doherty 142635b402 feat(adminui): tag-path hover (tag kind/type/driver inside ctx.GetTag literals)
v2-ci / build (push) Failing after 47s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
2026-06-10 05:48:02 -04:00
Joseph Doherty fc7dc3b57d feat(adminui): inline script-source editor in the virtual-tag modal 2026-06-09 15:23:35 -04:00
Joseph Doherty 4a2f7e37e5 feat(adminui): script document formatting (NormalizeWhitespace) 2026-06-09 15:06:40 -04:00
Joseph Doherty 9104b6c614 feat(adminui): script hover + signature help 2026-06-09 15:03:40 -04:00
Joseph Doherty 521fb61e44 feat(adminui): tag-path completion inside ctx.GetTag/SetVirtualTag literals 2026-06-09 14:53:15 -04:00
Joseph Doherty d1434933b4 feat(adminui): IScriptTagCatalog for tag-path completion 2026-06-09 14:49:19 -04:00
Joseph Doherty 93f5a745a3 feat(adminui): scope + dot-member script completions 2026-06-09 14:36:28 -04:00
Joseph Doherty 6a9b052fc7 feat(adminui): script diagnostics (Roslyn + forbidden-type + dynamic-path) 2026-06-09 14:29:08 -04:00
Joseph Doherty fa96989e2a feat(uns): validate typed TagConfig before save (F-uns-2 / #156)
v2-ci / build (push) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The per-driver editor models expose Validate() (required-field checks) but the
TagModal never called them, so a blank required field (e.g. S7 address, AbCip
tag path) saved silently and only failed at deploy/connect. Add a
TagConfigValidator registry (DriverType -> model.FromJson(json).Validate(),
parallel to TagConfigEditorMap) and call it in SaveAsync before the service
call — a non-null result sets the modal error and blocks save. Unmapped drivers
(no typed editor) and Modbus (no required field) return null. Editors untouched.

AdminUI.Tests 307/307 (12 new validator tests); build clean.
2026-06-09 11:45:35 -04:00
Joseph Doherty c0afecda50 fix(uns): omit blank optional keys from TagConfig + add omission tests; drop unused ParseInt (T4-T8 review) 2026-06-09 09:49:33 -04:00
Joseph Doherty 75021fa2c9 feat(uns): S7/AbCip/AbLegacy/TwinCAT/Focas typed tag-config editors (F-uns-1 T4-T8) 2026-06-09 09:42:40 -04:00
Joseph Doherty 5990b673cc feat(uns): Modbus typed tag-config editor (F-uns-1 T3) 2026-06-09 09:36:19 -04:00
Joseph Doherty fd9fa75d0e feat(uns): TagConfig JSON helper + editor map + TagModal dispatch scaffold (F-uns-1 T2) 2026-06-09 09:26:50 -04:00
Joseph Doherty d9dbd7917a feat(uns): surface DriverType to the TagModal driver dropdown (F-uns-1 T1) 2026-06-09 09:16:07 -04:00
Joseph Doherty 8ba64b1d99 fix(uns): enforce #122 on line reparent across clusters (final review)
v2-ci / build (push) Failing after 4m38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
2026-06-08 14:12:46 -04:00
Joseph Doherty 7db9a24403 feat(uns): equipment CSV import folded into the tree toolbar 2026-06-08 13:56:01 -04:00
Joseph Doherty c0346f14ce feat(uns): tag + virtual-tag modals wired into the tree 2026-06-08 13:47:34 -04:00
Joseph Doherty d637b834b9 fix(uns): reject equipment bind to non-existent driver + modal-xl (review) 2026-06-08 13:38:33 -04:00
Joseph Doherty 2beaa43d60 feat(uns): equipment modal wired into the tree 2026-06-08 13:31:14 -04:00