Commit Graph

155 Commits

Author SHA1 Message Date
Joseph Doherty dc8a2dd52c test(adminui): browse session registry, reaper, service 2026-05-28 15:44:20 -04:00
Joseph Doherty bec2988309 feat(adminui): in-process browse session registry + TTL reaper + service 2026-05-28 15:36:19 -04:00
Joseph Doherty 0d3ec46c14 fix(adminui): capture audit username at click time, not at panel init
v2-ci / build (push) Failing after 48s
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
DriverStatusPanel previously cached the username in a field at
OnInitializedAsync and forwarded the cached value into RestartDriver
/ ReconnectDriver messages. A token refresh or claim change mid-
circuit would land the stale name in the audit ConfigEdit row.
Re-reads AuthenticationStateProvider at button-click time so the
audit entry reflects the current principal.
2026-05-28 11:58:12 -04:00
Joseph Doherty 662f3f9f5c refactor(driver-pages): address Phase 6/8 deep-review findings
v2-ci / build (push) Failing after 32s
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
- Topic-name drift fix: DriverHealthChanged.TopicName and
  DriverControlTopic.Name now live on the message contracts in
  Commons. AkkaDriverHealthPublisher, DriverStatusSignalRBridge,
  DriverHostActor, and AdminOperationsActor all delegate to the
  single constant so a rename can't silently desynchronise
  publisher and subscriber.
- DriverStatusPanel._opResultClearTimer switched from
  System.Timers.Timer to System.Threading.Timer + awaited
  DisposeAsync. Prevents an in-flight 8s clear-callback from
  invoking StateHasChanged on a component whose hub has already
  been released.
- PublishHealthSnapshot deduplicates against the last published
  (state, lastSuccess, lastError, errorCount) fingerprint. The
  30s heartbeat no longer floods the SignalR layer with identical
  Healthy snapshots — newly-joined clients still warm up via the
  snapshot store on JoinDriver.
2026-05-28 11:52:20 -04:00
Joseph Doherty dcd2509548 refactor(driver-pages): address post-review follow-ups
- DriverInstanceSpec carries ClusterId from the deployment artifact;
  DriverHostActor threads the real cluster identity into
  DriverInstanceActor instead of the local NodeId. Old pre-PR
  artifacts without a ClusterId field fall back to the NodeId so
  in-flight deployments keep working.
- DriverHostActor.ChildEntry holds the full DriverInstanceSpec
  (was only carrying DriverType + LastConfigJson). Restart respawns
  preserve RowId, Name, Enabled, ClusterId — no placeholder values.
- Drop the unnecessary _faultLock on DriverInstanceActor — every
  read/write site runs inside an Akka message handler which is
  single-threaded per actor instance.
- DriverStatusPanel.DisposeAsync awaits Timer.DisposeAsync so an
  in-flight 5s tick can't invoke StateHasChanged on a component
  whose hub has already been torn down.
2026-05-28 11:41:46 -04:00
Joseph Doherty 063005fefa feat(adminui): DriverTagPicker modal + 9 static address builders
- DriverTagPicker shell: modal chrome + per-driver picker body
  rendered as ChildContent.
- 9 picker bodies (Modbus/AbCip/AbLegacy/S7/TwinCat/FOCAS/
  OpcUaClient/Galaxy/Historian.Wonderware). 5 have computed
  builder logic + unit tests; 4 are free-text passthroughs
  (live browse for OPC UA + Galaxy is a documented follow-up).
- Each typed driver page gets a "Pick address" button that opens
  the modal with the matching body. Picked address surfaces in
  the modal footer for manual copy — no JS interop in v1.
2026-05-28 11:21:33 -04:00
Joseph Doherty ffcc8d1065 feat(adminui): Reconnect/Restart on DriverStatusPanel (DriverOperator-gated)
- RestartDriver / ReconnectDriver messages + AdminOperationsActor
  handlers (broadcast via driver-control DPS topic; audited via
  ConfigEdits).
- DriverHostActor subscribes to driver-control; locates the
  matching child DriverInstanceActor and stops+respawns it
  (Restart) or sends it a ForceReconnect internal message
  (Reconnect — re-enters Reconnecting state without full stop).
  DriverInstanceSpec constructor call uses named args to handle
  the full 6-parameter signature.
- New DriverOperator authorization policy mapped to DriverOperator
  or FleetAdmin role; documented in docs/security.md. Map LDAP
  group via GroupToRole (e.g. "ot-driver-operator": "DriverOperator").
- DriverStatusPanel renders Reconnect + Restart buttons when the
  user holds the DriverOperator policy (hidden otherwise). Restart
  requires an in-page Razor confirm block (no JS confirm, keeps
  SignalR event loop unblocked). Both buttons show a spinner and
  are disabled during in-flight; result chip auto-clears after 8s.
  Username sourced from AuthenticationStateProvider.

Reconnect resolves to "ForceReconnect" (re-enter Reconnecting,
not full stop+respawn) — transport drops and retries while actor
and in-memory state are preserved. All DriverInstanceActor states
handle ForceReconnect safely (no-op when already in transition).
2026-05-28 11:14:04 -04:00
Joseph Doherty 4b374fd177 feat(adminui): Test Connect button on every typed driver page
- AdminProbeService routes TestDriverConnect through
  IAdminOperationsClient with a 65s outer guard (actor side already
  clamps to [1,60]).
- Added generic AskAsync<T> to IAdminOperationsClient interface and
  AdminOperationsClient impl, delegating straight to the Akka proxy.
- DriverTestConnectButton renders the button + inline result chip,
  auto-clears after 30s, disables during in-flight.
- Wired into all 9 typed driver pages directly under the
  identity section. Sources timeout from the form's
  ProbeTimeoutSeconds; sources config JSON from the form's
  current Options (operator can test BEFORE saving).
2026-05-28 11:02:49 -04:00
Joseph Doherty c19d124e89 feat(drivers): TCP-connect IDriverProbe for all 9 driver types
Cheap-and-fast probe: open TCP socket to the configured endpoint,
close immediately. Surfaces SocketError on failure, latency on
success, "timed out" on caller cancel. Sufficient for the AdminUI
Test Connect "can we reach the host?" question. Richer protocol-
level probes (OPC UA session open, FOCAS handshake, gRPC ping)
are a documented follow-up. Each probe registered as
AddSingleton<IDriverProbe, X> in DriverFactoryBootstrap so they
flow through DI into AdminOperationsActor.

Historian.Wonderware returns a clean "TCP probe not applicable"
result because it communicates over a Windows named pipe, not TCP.
Also adds OpcUaClient + Historian.Wonderware.Client project
references to Host.csproj (both were missing from the driver
ItemGroup).
2026-05-28 10:53:42 -04:00
Joseph Doherty f3f328c25c feat(adminops): IDriverProbe + TestDriverConnect actor handler
- IDriverProbe abstraction in Core.Abstractions; one impl per driver
  type, resolved by DriverType string. Phase 7.3 + 7.4 add concrete
  probes for the 9 supported driver types.
- TestDriverConnect / TestDriverConnectResult messages.
- AdminOperationsActor.HandleTestDriverConnectAsync looks up the probe
  by DriverType, runs it with a [1,60]s clamped timeout, and returns
  success/latency or failure/message. Probes that throw or time out
  surface as soft failures.
2026-05-28 10:44:00 -04:00
Joseph Doherty 4584612a1a feat(adminui): DriverStatusPanel + wire into 9 typed pages
Live panel subscribed to the /hubs/driverstatus SignalR feed —
renders state chip, last-success age, 5-min error count, last
error message. Auto-reconnect; dimmed when no push arrives for 30s.
Hidden for new instances (nothing deployed yet); shown read-only
on every edit-mode page. Reconnect/Restart buttons land in Phase 8.
2026-05-28 10:29:43 -04:00
Joseph Doherty 4203b84d51 feat(runtime): publish DriverHealthChanged via DriverInstanceActor
- IDriverHealthPublisher in Core.Abstractions + NullDriverHealthPublisher
  no-op for tests/dev-stub paths.
- AkkaDriverHealthPublisher in Runtime forwards to the cluster-wide
  `driver-health` DPS topic.
- DriverInstanceActor instrumented to publish snapshots on every
  observable state change + a periodic 30s heartbeat so the AdminUI
  snapshot store warms up for newly-joined SignalR clients.
- Sliding 5-minute Faulted-count tracked per actor via Queue<DateTime>.
- DriverHostActor.SpawnChild threads clusterId (_localNode.Value) and
  the health publisher down to every DriverInstanceActor child.
- ServiceCollectionExtensions.AddOtOpcUaRuntime registers
  AkkaDriverHealthPublisher as IDriverHealthPublisher singleton.
2026-05-28 10:22:44 -04:00
Joseph Doherty 29370fde3c feat(adminui): add DriverStatusSignalRBridge + InMemory snapshot store 2026-05-28 10:13:30 -04:00
Joseph Doherty 3f23a1acd3 feat(adminui): add DriverStatusHub 2026-05-28 10:13:25 -04:00
Joseph Doherty c4086c243c fix(adminui): S7 typed page no longer wipes Tags on save
- S7DriverPage.FormModel now preserves Tags through Form ↔ Options
  translation (was hard-coding Tags = [] on every save, silently
  destroying any tag list that operators had configured).
- Add FormModel_RoundTrip tests for OpcUaClient and Historian
  mirror classes — both were translating Options ↔ form-model
  entirely untested.
- Surface S7 Tags in the round-trip test so this regression
  can't reach merge again.
2026-05-28 10:06:43 -04:00
Joseph Doherty a971db3ee5 refactor(adminui): retire generic DriverEdit.razor
All 9 driver types now have typed pages; DriverEditRouter dispatches
to them directly. Unknown DriverType strings (e.g. legacy rows) render
an explicit error notice instead of falling through to a generic
editor — the failure mode is now visible, not silent.
2026-05-28 09:59:25 -04:00
Joseph Doherty 5f8fa7004c feat(adminui): wire all 9 typed pages into DriverEditRouter map
DriverEditRouter now dispatches every known DriverType to its typed
page. The legacy DriverEdit fallback remains in ResolveComponentType
for forward-compatibility with as-yet-unknown driver types but is no
longer reached for any current driver.
2026-05-28 09:58:36 -04:00
Joseph Doherty 059a6218f7 feat(adminui): AbLegacy typed driver page 2026-05-28 09:57:07 -04:00
Joseph Doherty 8149739161 feat(adminui): FOCAS typed driver page
Adds FocasDriverPage.razor (route: /clusters/{id}/drivers/new/focas) with
typed sections for timeout, probe, AlarmProjection (enabled + poll interval),
HandleRecycle (enabled + interval in minutes), FixedTree (enabled + axis/
program/timer poll intervals), and read-only JSON views for Devices and Tags.
FormModel uses flat settable properties + FromOptions/ToOptions with
appropriate unit conversions (ms, minutes). Also adds
FocasDriverPageFormSerializationTests (3 tests: JSON round-trip, unknown-field
drop, FormModel round-trip covering all sub-options classes).
2026-05-28 09:56:53 -04:00
Joseph Doherty 2c16062457 feat(adminui): Historian.Wonderware typed driver page 2026-05-28 09:55:15 -04:00
Joseph Doherty dfbf6793de feat(adminui): TwinCat typed driver page
Adds TwinCATDriverPage.razor (route: /clusters/{id}/drivers/new/twincat)
with typed fields for timeout, UseNativeNotifications, EnableControllerBrowse,
NotificationMaxDelayMs, probe sub-options (enabled/interval/timeout/admin
timeout), and read-only JSON views for Devices and Tags collections.
FormModel uses flat settable properties + FromOptions/ToOptions. Also adds
TwinCATDriverPageFormSerializationTests (3 tests). Fixes pre-existing
placeholder syntax error in AbCipDriverPage.razor (@raw_cpu_type in
attribute caused RZ9986).
2026-05-28 09:54:49 -04:00
Joseph Doherty a243cfd126 feat(adminui): Galaxy typed driver page 2026-05-28 09:52:31 -04:00
Joseph Doherty 5cad9b260e feat(adminui): S7 typed driver page
Adds S7DriverPage.razor (route: /clusters/{id}/drivers/new/s7) with
typed fields for host, port, CpuType InputSelect, rack, slot, timeout,
probe sub-options, and read-only JSON tag view. FormModel uses flat
settable properties and FromOptions/ToOptions round-trip; no
init-only bindings in Razor. Also adds
S7DriverPageFormSerializationTests (3 tests: JSON round-trip,
unknown-field drop, FormModel round-trip).
2026-05-28 09:52:10 -04:00
Joseph Doherty a3073d16bf feat(adminui): Modbus typed driver page 2026-05-28 09:52:01 -04:00
Joseph Doherty efcc2311e6 feat(adminui): OpcUaClient typed driver page 2026-05-28 09:50:34 -04:00
Joseph Doherty 7014c9376c feat(adminui): reference all 9 Driver.*.Contracts projects
Wires the POCO-only driver contracts into the AdminUI csproj so the
9 typed *DriverPage.razor components from Phase 4 can compile against
the real Options classes without dragging native driver deps in.
2026-05-28 09:42:12 -04:00
Joseph Doherty 27b3a014da refactor(adminui): hand /drivers routes to DriverTypePicker + DriverEditRouter
Removes both @page directives from DriverEdit.razor. The picker owns
/drivers/new; the router owns /drivers/{id} and dispatches via
DynamicComponent (currently falls back to DriverEdit for every driver
type — Phase 4 populates the type map one driver at a time).
2026-05-28 09:39:49 -04:00
Joseph Doherty 55e8bf70d9 feat(adminui): add DriverEditRouter dispatch page
Falls back to legacy DriverEdit until Phase 4 populates the type-map.
2026-05-28 09:38:35 -04:00
Joseph Doherty c0ce5d02bd feat(adminui): add DriverTypePicker landing page
Adds /clusters/{ClusterId}/drivers/new picker page (Task 3.1). Renders
a 9-card Bootstrap grid — one card per driver type — each linking to
/clusters/{ClusterId}/drivers/new/{slug}. No data fetch; type list is
hardcoded. Route collides with DriverEdit.razor's same directive; Task
3.3 removes the duplicate to resolve the runtime ambiguity.
2026-05-28 09:36:54 -04:00
Joseph Doherty a28f4cdd25 refactor(adminui): drive DriverEdit.razor through shared section components
No functional change — the identity, resilience, and save-bar are now
each in their own reusable component so the typed driver pages (Phase 4)
can share them. The middle "Driver config (JSON)" panel stays inlined
for now — it's replaced wholesale by typed forms in Phase 4.
2026-05-28 09:33:06 -04:00
Joseph Doherty a008530af6 feat(adminui): add DriverResilienceSection shared component 2026-05-28 09:29:41 -04:00
Joseph Doherty 1ff3875a19 feat(adminui): add DriverIdentitySection shared component 2026-05-28 09:28:29 -04:00
Joseph Doherty 85af126406 feat(adminui): add DriverFormShell shared component 2026-05-28 09:26:54 -04:00
Joseph Doherty 64e3fbe035 docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
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
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00
Joseph Doherty f9fc7dd2e1 feat(host): wire UseWindowsService so sc.exe-installed service runs cleanly
v2-ci / build (push) Failing after 45s
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
v2-e2e / e2e (push) Failing after 37s
The v2 plan's blessed install path (scripts/install/Install-Services.ps1)
registers the host via `sc.exe create binPath=...OtOpcUa.Host.exe`, but the
binary never called `UseWindowsService`. Without it, the Service Control
Manager waits ~30s for the process to call SetServiceStatus(Running) and
then kills it — the install script's design was incomplete.

Two changes:

- Host.csproj: drop the `IsOSPlatform('Windows')` condition on the
  Microsoft.Extensions.Hosting.WindowsServices package reference so the
  package is always available. The runtime helper used by
  UseWindowsService gates on WindowsServiceHelpers.IsWindowsService()
  internally, so it's a no-op when running as a console app or under
  Linux/macOS — the binary stays cross-platform-buildable.

- Program.cs: call builder.Host.UseWindowsService(options =>
  options.ServiceName = "OtOpcUaHost") immediately after CreateBuilder.
  When the host is launched by SCM, WindowsServiceLifetime takes over
  the IHostLifetime slot and reports START/STOP correctly. When launched
  by `dotnet run` or `OtOpcUa.Host.exe` from a console, it's a no-op.

Verified end-to-end on wonder-app-vd03.zmr.zimmer.com: `sc.exe create`
followed by `sc.exe start OtOpcUaHost` transitions from START_PENDING to
RUNNING; /login + /health/ready + /health/active all return 200; service
survives SSH session close and auto-starts on boot per the AUTO_START
flag set by the installer script.
2026-05-26 17:07:52 -04:00
Joseph Doherty 7dfbca6469 feat(opcua): materialise SystemPlatform tags (Galaxy) as OPC UA variables
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
Closes the gap where Tag rows with EquipmentId=NULL + Namespace.Kind=SystemPlatform
(Galaxy hierarchy) existed in ConfigDb but were never surfaced in the OPC UA
address space. Now they materialise as Variable nodes under a folder named for
their FolderPath, browseable through any OPC UA client.

Layers touched:

- IOpcUaAddressSpaceSink: new EnsureVariable(nodeId, parentFolderId, displayName,
  dataType) signature on the sink interface, NullSink, DeferredSink, SdkSink.
- OtOpcUaNodeManager.EnsureVariable: creates a BaseDataVariableState parented
  under the named folder (or root), initial Value=null +
  StatusCode=BadWaitingForInitialData; resolves Tag.DataType strings to the
  matching OPC UA built-in NodeId. Idempotent.
- Phase7CompositionResult: new GalaxyTags collection of GalaxyTagPlan records
  carrying (TagId, DriverInstanceId, FolderPath, DisplayName, DataType,
  MxAccessRef). Constructor overloads keep existing call sites compiling.
- Phase7Composer.Compose: now takes Tag + Namespace inputs, filters for
  SystemPlatform-namespace tags with EquipmentId=NULL, emits GalaxyTagPlan
  rows with MXAccess ref "FolderPath.Name".
- Phase7Plan: new AddedGalaxyTags / RemovedGalaxyTags / ChangedGalaxyTags
  collections + GalaxyTagDelta record; IsEmpty + needsRebuild updated.
- Phase7Planner.Compute: diffs GalaxyTags by TagId via existing DiffById helper.
- DeploymentArtifact.ParseComposition: reads the Tags + Namespaces +
  DriverInstances arrays the ConfigComposer already emits, applies the same
  SystemPlatform filter, returns the same GalaxyTagPlan list as the composer
  so artifact-side and compose-side plans agree.
- Phase7Applier: new MaterialiseGalaxyTags pass that ensures one folder per
  distinct FolderPath then one Variable per tag. NodeId for the variable is
  "<FolderPath>.<Name>" matching the MXAccess ref so the future Galaxy
  SubscribeBulk wiring can address them directly.
- OpcUaPublishActor.RebuildAddressSpace: invokes MaterialiseGalaxyTags after
  MaterialiseHierarchy. _lastApplied initialiser updated for the new ctor.
- seed-clusters.sql: pre-existing TestMachine_001.TestAlarm001..003 rows
  needed no change — the composer/applier now picks them up automatically.

Verified end-to-end via docker-dev: deploy click → driver-a logs
"Phase7Applier: Galaxy tags materialised (tags=3, folders=1)" → OPC UA Client
CLI browses the three Variable nodes under TestMachine_001 folder. Reads
return BadWaitingForInitialData status (expected — Galaxy driver's
SubscribeBulk wiring to push values into the nodes is the remaining
follow-up).
2026-05-26 15:43:22 -04:00
Joseph Doherty 60beb9128e feat(deploy,runtime): wire mxaccessgw connection — endpoint, key, seed row
v2-ci / build (push) Failing after 37s
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
User confirmed the mxaccessgw client (Galaxy driver) doesn't need Windows
— only the gateway worker has that constraint. This wires the Galaxy
driver into the docker-dev fleet:

- docker-compose.yml: GALAXY_MXGW_API_KEY env var on every host service
  (admin nodes harmlessly ignore it; driver-role nodes pick it up when
  the seeded DriverInstance resolves ApiKeySecretRef=env:GALAXY_MXGW_API_KEY).
  Default value matches the key the operator provided; override via shell
  env (GALAXY_MXGW_API_KEY=... docker compose up -d) to rotate without
  editing compose.
- seed-clusters.sql: now creates a SystemPlatform Namespace
  (MAIN-galaxy, urn:zb:docker-dev:galaxy) plus a GalaxyMxGateway
  DriverInstance (MAIN-galaxy-mxgw) in the MAIN cluster pointing at
  http://10.100.0.48:5120 with UseTls=false. Idempotent via IF NOT EXISTS.
- DriverInstanceActor.ShouldStub: clarified the doc comment — only the
  legacy "Galaxy" type name and "Historian.Wonderware" are Windows-only;
  the v2 "GalaxyMxGateway" driver is .NET 10 cross-platform (gRPC to an
  external gateway) and is NOT stubbed.
- README: documents the final operator step — sign in, click "Deploy
  current configuration" on /deployments to materialise the seeded
  Galaxy driver into a running gRPC connection. Raw DriverInstance rows
  don't spawn drivers on their own; the v2 lifecycle requires a sealed
  Deployment first.
2026-05-26 14:58:02 -04:00
Joseph Doherty 6884de9774 revert(adminui): restore 'OtOpcUa Admin' login title
v2-ci / build (push) Failing after 44s
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
User chose to revert the MxAccess Gateway rebrand on the login card. Keep
the layout fix from c064ec1 (no panel-head top strip; inline h1.login-title)
and just put the original product name back.
2026-05-26 14:50:06 -04:00
Joseph Doherty c064ec16cf fix(security,adminui): logout redirects to /login + restyle login card
v2-ci / build (push) Failing after 41s
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
Two small UX fixes:

- AuthEndpoints.LogoutAsync now redirects browser callers to /login after
  SignOutAsync instead of returning 204 NoContent. 204 was correct for the
  REST contract but left browsers stuck on the page they came from (the
  cookie was cleared but no navigation happened, so "Sign out" appeared
  to do nothing). API callers can still opt into the status-only behavior
  by sending `Accept: application/json`.

- Login.razor drops the .panel-head top strip; the sign-in card now reads
  as a self-contained form with an inline title "MxAccess Gateway Admin —
  sign in". Added a .login-title CSS class to site.css that matches the
  panel-head's typographic weight without the bar.
2026-05-26 14:47:53 -04:00
Joseph Doherty ed1c17bc7b fix(deploy,host): docker-dev bring-up — anon health probes, robust seeder
v2-ci / build (push) Failing after 32s
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
Two fixes surfaced while bringing up the docker-dev stack end-to-end:

- HealthEndpoints.MapOtOpcUaHealth now calls .AllowAnonymous() on /health/ready,
  /health/active, /healthz. Without it the AddOtOpcUaAuth fallback policy 401s
  every probe and Traefik marks every backend unhealthy → all three cluster
  routes return 503.

- cluster-seed entrypoint no longer attempts to apply Migrate-To-V2.sql via
  sqlcmd. The EF-generated idempotent script puts CREATE PROCEDURE inside
  IF NOT EXISTS BEGIN ... END blocks (procs must be first in their batch),
  so sqlcmd fails with "Must declare the scalar variable @FromGenerationId".
  EF's own runner handles this; sqlcmd doesn't. The seed now just waits for
  the schema and applies row inserts. Migrations remain the operator's job:
      dotnet ef database update --project src/Core/.../Configuration \
                                --startup-project src/Server/.../Host

Also: LDAP service removed (bitnami/openldap:2.6 image retired, legacy tag
crashes mid-setup with exit 68); every host now runs with
Authentication__Ldap__DevStubMode=true. Bumped LDAP+Traefik dashboard host
ports to avoid collisions with the sister scadalink dev stack (3893→3894,
8080→8089).

Confirmed working end-to-end: all three Traefik routes return HTTP 200,
cluster-seed populates ServerCluster (MAIN/SITE-A/SITE-B) + ClusterNode
(driver-a/b, site-a-1/2, site-b-1/2) rows on first boot.
2026-05-26 14:37:01 -04:00
Joseph Doherty a1a7646b33 fix(adminui): refresh stale F9 stub copy on /alerts page
ScriptedAlarmActor (Runtime/ScriptedAlarms) shipped a while back — the
"Engine wiring (F9 ScriptedAlarmActor) is pending" stub message was
misleading. Also drop the matching "(F9)" / "(future)" parentheticals
in the intro panel and frame the empty state as a current-window
condition, not a missing feature.
2026-05-26 13:53:09 -04:00
Joseph Doherty e4d0d82f7f feat(adminui): collapsible nav sidebar with cookie state + LoginLayout
Port the ScadaLink CentralUI sidebar pattern into the OtOpcUa AdminUI:

- Drop the top app-bar. Brand moves into the side rail's header — same
  visual rhythm as ScadaLink's NavMenu.
- New NavSection.razor: collapsible eyebrow toggle (rail-eyebrow-toggle CSS)
  with a chevron + label. Mirrors ScadaLink/Components/Layout/NavSection.
- New NavSidebar.razor: interactive island carrying the three section
  groups (Navigation / Scripting / Live) + session block. Marked
  @rendermode InteractiveServer; MainLayout itself stays static-rendered
  because layouts can't take a RenderFragment Body across an interactive
  boundary.
- New wwwroot/js/nav-state.js: window.navState.get/.set persists the
  expanded-section list to the otopcua_nav cookie (one-year lifetime,
  SameSite=Lax). Same shape as ScadaLink's scadabridge_nav.
- New LoginLayout.razor + @layout LoginLayout on Login.razor: the login
  page now renders without the side rail — clean centred card.
- MainLayout.razor: slimmed down to the d-flex shell + hamburger toggle +
  <NavSidebar/> + @Body.
- Login.razor: also drops the trailing "LDAP bind against the configured
  directory..." footer that the user asked to remove.
- site.css: adds .side-rail .brand styles (mirrored from ScadaLink) and
  the .rail-eyebrow-toggle / .rail-eyebrow-chevron / .rail-section-body
  styles for the new collapsible UI.

Auto-expand on page load: NavSidebar seeds the expanded set from the
current URL's first path segment (in OnInitialized so it works even on
the very first server render) and from the cookie (in OnAfterRenderAsync
once JS interop is available). LocationChanged hooks keep the expanded
state in sync as the user navigates between sections.
2026-05-26 13:48:35 -04:00
Joseph Doherty 2915755a7c fix(host,security): wire static assets, DI lifetimes, form login, dev-stub LDAP
Six interlocking fixes surfaced while smoke-testing the fused Host in a browser:

- Host/Program.cs: UseStaticWebAssets() opts into the RCL static-asset pipeline
  in any environment (auto-only in Development), MapStaticAssets().AllowAnonymous()
  exempts CSS/JS from the AddOtOpcUaAuth fallback policy, and
  AddCascadingAuthenticationState() lets <AuthorizeView/> work inside interactive
  components (NavSidebar's session block).
- Security/ServiceCollectionExtensions: ILdapAuthService Scoped → Singleton —
  consumed by the Singleton LdapOpcUaUserAuthenticator on driver-role nodes.
  Crash only surfaced in Development (ValidateOnBuild=true).
- Security/Endpoints/AuthEndpoints: /auth/login now dispatches on Content-Type —
  application/json keeps the original 204/401/503 contract for tests, and
  application/x-www-form-urlencoded (the browser <form>) gets a redirect dance.
  DisableAntiforgery on the login endpoint (it's the entry point, no prior session)
  and AllowAnonymous to override the fallback policy.
- Security/Ldap/LdapOptions + LdapAuthService: real DevStubMode property; when
  true the auth service bypasses the LDAP bind and returns a FleetAdmin role so
  dev/test can navigate the full Admin UI without GLAuth running.
- AdminUI/EndpointRouteBuilderExtensions: doc-comment update about static-asset
  flow (the actual MapStaticAssets call lives in Host/Program.cs).
2026-05-26 13:48:18 -04:00
Joseph Doherty cb936db7d6 fix(opcua): PopulateServerArray writes IServerInternal.ServerUris so clients see peers 2026-05-26 11:39:44 -04:00
Joseph Doherty 70ffd2849d feat(opcua): OpcUaApplicationHost publishes peer URIs in Server.ServerArray 2026-05-26 11:21:11 -04:00
Joseph Doherty 898a47746d feat(host): add per-role appsettings overlays for admin/driver/admin-driver 2026-05-26 11:19:10 -04:00
Joseph Doherty 05a0596fb1 feat(host): F9b RoslynScriptedAlarmEvaluator + #107 close engine DI
v2-ci / build (push) Failing after 39s
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 (push) Has been skipped
RoslynScriptedAlarmEvaluator mirrors F8b's pattern for alarm predicates:
caches a compiled ScriptEvaluator<AlarmPredicateContext, bool> per unique
predicate, runs against the dependency dictionary with a 2s timeout, and
turns every failure (compile error, sandbox violation, runtime throw,
ctx.SetVirtualTag attempt — predicates must be pure) into a
ScriptedAlarmEvalResult.Failure. ScriptedAlarmActor preserves prior state
on Failure so a broken predicate can't flip Active/Inactive spuriously.

Program.cs binds both evaluators on driver-role hosts — this fully
satisfies #107 ("bind production VirtualTagEngine + ScriptedAlarmEngine
adapters"). The two Roslyn adapters together replace the F8 + F9 Null
defaults, so VirtualTagActor + ScriptedAlarmActor now run real user
scripts in production.

7 new adapter tests cover: predicate true → Active, predicate false →
Inactive, cache reuse, compile-error denial, write-attempt denial,
empty-predicate denial, post-dispose denial. Host.IntegrationTests now
17/17 green.

Closes #80 + #107. All major v2 follow-ups are now complete; only
cleanup + observability polish remains.
2026-05-26 10:58:04 -04:00
Joseph Doherty 219d10a22d feat(host): F8b RoslynVirtualTagEvaluator — production virtual-tag eval
RoslynVirtualTagEvaluator wraps Core.Scripting.ScriptEvaluator + Core
.VirtualTags.VirtualTagContext into a single-tag IVirtualTagEvaluator
adapter. Caches the compiled ScriptEvaluator per unique expression so
the second-and-onwards Evaluate is an in-process method call against the
dependency dictionary. Compile/sandbox/runtime errors all surface as
VirtualTagEvalResult.Failure rather than propagating exceptions through
the VirtualTagActor message loop.

Single-tag scope: cross-tag ctx.SetVirtualTag writes are dropped + logged
because fan-out between actors is owned by DependencyMuxActor. Cycle
detection + cascade ordering stay in Core.VirtualTags.VirtualTagEngine
where they belong (loaded fleet-wide); this adapter keeps the actor
message handler simple.

Host adds Core.Scripting + Core.VirtualTags project refs, plus a
TargetWarningsAsErrors NU1608 suppression — Microsoft.CodeAnalysis.CSharp
.Scripting 4.12.0 pins Common to 4.12.0 but ASP.NET Core transitively
brings Microsoft.CodeAnalysis.Common 5.0.0; the surface we use is stable
across the drift (verified by Core.Scripting.Tests).

Program.cs binds RoslynVirtualTagEvaluator → IVirtualTagEvaluator on
driver-role hosts, replacing the F8-default NullVirtualTagEvaluator so
VirtualTagActor evaluates real user scripts at runtime.

6 new adapter tests cover: simple expression sums, cache reuse across
calls, compile-error denial, runtime-throw denial, empty-expression
denial, post-dispose denial. Host.IntegrationTests now 10/10 green.

Closes #79. F9b + #107 next.
2026-05-26 10:55:56 -04:00
Joseph Doherty 607dc51dec feat(opcua): #85 UNS Area/Line/Equipment folder hierarchy in SDK
v2-ci / build (push) Failing after 42s
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 (push) Has been skipped
Phase7Composer now carries UnsAreaProjection + UnsLineProjection lists so
the applier can materialise the full UNS topology in the OPC UA address
space. New IOpcUaAddressSpaceSink.EnsureFolder(folderNodeId, parentNodeId,
displayName) seam (no-op default, recorded in tests, forwarded by
DeferredAddressSpaceSink, implemented by SdkAddressSpaceSink). The SDK-
side OtOpcUaNodeManager gains an EnsureFolder API that creates
FolderState nodes with proper parent linkage; RebuildAddressSpace now
clears folders too so re-applies don't accumulate stale topology.

Phase7Applier.MaterialiseHierarchy walks composition.UnsAreas →
composition.UnsLines → composition.EquipmentNodes, calling EnsureFolder
with the correct parent at each level. Idempotent — calling twice with
the same composition is a no-op. OpcUaPublishActor.HandleRebuild invokes
it after Phase7Applier.Apply so OPC UA clients browsing the server now
see Area/Line/Equipment as proper folders rather than flat tag ids.

DeploymentArtifact.ParseComposition reads UnsAreas + UnsLines from the
JSON snapshot the ControlPlane emits, populating the new fields when
present.

Phase7Composer.Compose now accepts UnsAreas + UnsLines; a 3-arg overload
preserves the old signature for legacy callers + existing tests. The
Phase7CompositionResult convenience ctor likewise keeps the planner
tests working without UNS data.

3 new hierarchy tests (pure unit + boot-verify against a real
OtOpcUaSdkServer); OpcUaServer suite is 48/48 green (was 45, +3),
Runtime 74/74 unchanged.

Closes #85.
2026-05-26 10:48:56 -04:00
Joseph Doherty 2697af31d1 feat(opcua,host): #81 ServiceLevel SDK publisher
SdkServiceLevelPublisher writes Server.ServiceLevel through the SDK's
ServerObjectState — the standard OPC UA non-transparent-redundancy signal
clients use to pick a primary. Writes are guarded by DiagnosticsLock so
concurrent SDK diagnostics scans don't fight with our updates.

DeferredServiceLevelPublisher mirrors the DeferredAddressSpaceSink late-
binding pattern: Akka actors resolve IServiceLevelPublisher at construction,
hosted service swaps the SDK publisher in after StandardServer.Start. Host
Program.cs registers DeferredServiceLevelPublisher as the singleton bound
to IServiceLevelPublisher; OtOpcUaServerHostedService gets it injected and
fills it once IServerInternal is available.

Tests boot a real StandardServer on a free port (cross-platform), call
Publish, then verify ServerObject.ServiceLevel.Value reflects the write.
5 new tests; OpcUaServer suite now 45/45 green (was 40, +5).

Closes #81 residual. Unblocks Task 60 (OPC UA dual-endpoint + ServiceLevel
tests).
2026-05-26 10:37:42 -04:00