diff --git a/CLAUDE.md b/CLAUDE.md index 84097cd5..78e076d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,29 +8,12 @@ When a change is requested, the default assumption is: update the design doc *an ### Top-level directories -- `src/` — C#/.NET implementation, one project per component (e.g. `ZB.MOM.WW.ScadaBridge.AuditLog`, `ZB.MOM.WW.ScadaBridge.NotificationOutbox`, `ZB.MOM.WW.ScadaBridge.SiteCallAudit`, `ZB.MOM.WW.ScadaBridge.CentralUI`, `ZB.MOM.WW.ScadaBridge.Host`, …). Solution file: `ZB.MOM.WW.ScadaBridge.slnx`. -- `tests/` — Test projects (unit + integration). -- `docs/` — Design documentation: `docs/requirements/` (high-level + per-component specs), `docs/test_infra/` (test infrastructure), `docs/plans/` (design-decision and implementation-plan docs). The spec the code implements. -- `docker/` — 8-node cluster topology (2 central + 3 sites), `deploy.sh`, per-node `appsettings.*.json`. See [`docker/README.md`](docker/README.md) for setup, ports, and management commands. Rebuild + redeploy with `bash docker/deploy.sh`. -- `docker-env2/` — Minimal second cluster topology (2 central + 1 site × 2 nodes), runs concurrently with `docker/` on host ports 91XX. Built specifically for testing the Transport (#24) feature with two real environments. See [`docker-env2/README.md`](docker-env2/README.md). Rebuild + redeploy with `bash docker-env2/deploy.sh`. -- `infra/` — Docker Compose for local test services (MS SQL, OPC UA, SMTP, REST API, Traefik). **LDAP is no longer started here** — dev/test LDAP is the shared GLAuth on `10.100.0.35:3893` (source of truth: `scadaproj/infra/glauth/`). -- `deploy/` — Production/on-host deployment artifacts (e.g. `deploy/wonder-app-vd03/`: `appsettings.Central.json`, `appsettings.Site.json`, `install.ps1`/`uninstall.ps1`, `RUNBOOK.md`). -- `deployments/` — Deployment topology notes (`docker-cluster.md`, `docker-cluster-env2.md`, `README.md`). -- `code-reviews/` — Per-component code-review notes (one folder per component, plus `_template`). -- `tools/` — Repo maintenance/utility scripts (e.g. `rename-to-scadabridge.sh`). -- `AkkaDotNet/` — Akka.NET reference documentation and best-practices notes. -- `deprecated/` — Retired docs/notes kept for reference. -- `logs/` — Local runtime log output. -- `vendor/` — Vendored third-party assets (currently an empty placeholder). -- `.claude/` — Claude Code project config (settings, skills, agents). +Layout is self-describing (`ls`, plus each directory's own README). Only the non-obvious parts are recorded here: -### Key documents - -- `README.md` — Master index with component table and architecture diagrams. -- `docs/requirements/HighLevelReqs.md` — Complete high-level requirements covering all functional areas. -- `docs/requirements/Component-*.md` — Individual component design documents (one per component) — the spec the code implements. -- `docs/test_infra/test_infra.md` — Master test infrastructure doc (OPC UA, MS SQL, SMTP, REST API, Traefik). LDAP is the shared GLAuth on `10.100.0.35:3893` (not a local infra container; see `scadaproj/infra/glauth/`). -- `docs/plans/` — Design decision and implementation-plan documents from refinement sessions. +- `docker/` — the primary 8-node cluster topology (2 central + 3 sites). Rebuild + redeploy with `bash docker/deploy.sh`. +- `docker-env2/` — minimal second cluster (2 central + 1 site × 2 nodes) that runs **concurrently** with `docker/` on host ports 91XX; built for testing Transport (#24) against two real environments. Rebuild with `bash docker-env2/deploy.sh`. +- `infra/` — Docker Compose for local test services (MS SQL, OPC UA, SMTP, REST API, Traefik). **LDAP is no longer started here** — dev/test LDAP is the shared GLAuth on `10.100.0.35:3893` (source of truth: `scadaproj/infra/glauth/`), and the same applies to `docs/test_infra/test_infra.md`, which still reads as if it were a local container. +- `deprecated/` — retired docs/notes kept only for reference; not current. ## Sister Projects @@ -70,35 +53,11 @@ Other peers in the `scadaproj` family (see `scadaproj/CLAUDE.md` for details): ` - Run tests with `dotnet test ZB.MOM.WW.ScadaBridge.slnx`. `ZB.MOM.WW.ScadaBridge.CLI.Tests` is now a member of the slnx (added 2026-07; arch-review 08 §2.4) — the old "silently skipped" gotcha no longer applies, so a solution-level test run exercises the CLI suite too. - **Propagate cross-repo changes to the umbrella index.** This repo is indexed by the parent workspace `~/Desktop/scadaproj`. When a fact its index records changes here — remote/push status, stack/component summary, cross-project wire relationships (OPC UA → OtOpcUa, gRPC → mxaccessgw), or the solution/namespace shape — update the **ScadaBridge entry in [`../scadaproj/CLAUDE.md`](../scadaproj/CLAUDE.md)** in the same change so the umbrella index never drifts from this repo. (Mirrors the same rule in the peer repos, e.g. `MxAccessGateway`/`HistorianGateway`.) -## Current Component List (27 components) +## Component List -1. Template Engine — Template modeling, inheritance, composition, validation, flattening, diffs. -2. Deployment Manager — Central-side deployment pipeline, system-wide artifact deployment, instance lifecycle. -3. Site Runtime — Site-side actor hierarchy (Deployment Manager singleton, Instance/Script/Alarm Actors), script compilation, Akka stream. -4. Data Connection Layer — Protocol abstraction (OPC UA, custom), subscription management, clean data pipe. -5. Central–Site Communication — Akka.NET ClusterClient (command/control) + gRPC server-streaming (real-time data), message patterns, debug streaming. -6. Store-and-Forward Engine — Buffering, fixed-interval retry, parking, SQLite persistence, replication. -7. External System Gateway — External system definitions, API method invocation, database connections. -8. Notification Service — Central-only notification-list and SMTP definitions, per-type delivery adapters (sites no longer deliver notifications). -9. Central UI — Web-based management interface, all workflows. -10. Security & Auth — LDAP/AD authentication, role-based authorization, site-scoped permissions. -11. Health Monitoring — Site health metrics collection and central reporting. -12. Site Event Logging — Local operational event logs at sites with central query access. -13. Cluster Infrastructure — Akka.NET cluster setup, active/standby failover, singleton support. -14. Inbound API — Web API for external systems, API key auth, script-based implementations. -15. Host — Single deployable binary, role-based component registration, Akka.NET bootstrap. -16. Commons — Shared types, POCO entity classes, repository interfaces, message contracts. -17. Configuration Database — EF Core data access layer, repositories, unit-of-work, audit logging (IAuditService), migrations. -18. Management Service — Akka.NET actor providing programmatic access to all admin operations, ClusterClientReceptionist registration. -19. CLI — Command-line tool using HTTP Management API, System.CommandLine, JSON/table output. -20. Traefik Proxy — Reverse proxy/load balancer fronting central cluster, active node routing via `/health/active`, automatic failover. -21. Notification Outbox — Central component ingesting store-and-forwarded notifications, `Notifications` audit table, dispatcher loop, retry/parking, delivery KPIs. -22. Site Call Audit — Central component auditing site cached calls (`CachedCall`/`CachedWrite`); `SiteCalls` audit table, telemetry ingest, reconciliation, KPIs, central→site Retry/Discard relay; sites remain the source of truth. -23. Audit Log — Central append-only AuditLog table spanning every script-trust-boundary action (outbound API sync+cached, outbound DB sync+cached, notifications, inbound API). Site SQLite hot-path + gRPC telemetry + reconciliation; combined telemetry with Site Call Audit; central direct-write for Notification Outbox dispatch + Inbound API; monthly partitioning, 365-day retention. -24. Transport — File-based, encrypted bundle export/import via Central UI. Templates (with **all** child collections — attributes/alarms/scripts/compositions/**native-alarm-sources**, field-complete incl. `LockedInDerived` + script cadence/timeout), system artifacts, central-only configuration, plus site/instance-scoped config (`Site`s, site `DataConnection`s, `Instance`s + real `AreaName` by name) reconciled across environments by a `BundleNameMap` name-mapping subsystem. Per-conflict resolution with a per-line Myers diff. Runs the **script trust gate** at import review (5th `ScriptTrustValidator` call site — forbidden-API scripts rejected pre-runtime; covers template/shared/ApiMethod bodies, template script + alarm Expression-trigger bodies, **and instance alarm-override trigger expressions** — all hard-block; template-script name-resolution findings are advisory warnings, ApiMethod findings hard-block). Correlated audit via `BundleImportId`. Never touches site runtime nodes (imported instances land `NotDeployed`). -25. Script Analysis — Shared authoritative script-trust analyzer: unified forbidden-API deny-list (`ScriptTrustPolicy`), fused semantic+syntactic validator (`ScriptTrustValidator`), Roslyn compile wrapper (`RoslynScriptCompiler`), and compile-only globals stubs (`ScriptCompileSurface`/`TriggerCompileSurface`); consumed by Template Engine, Site Runtime, Inbound API, and Central UI. -26. KPI History — Reusable central KPI-history backbone: tall/EAV `KpiSample` store in central MS SQL, `KpiHistoryRecorderActor` cluster singleton (`kpi-history-recorder`, not readiness-gated) sampling DI-registered `IKpiSampleSource`s every minute, bucketed query (`GetRawSeriesAsync` + `KpiSeriesBucketer`) + scoped `KpiHistoryQueryService`, and a reusable custom-SVG `KpiTrendChart`; ships trends for Notification Outbox, Site Call Audit, Audit Log, and Site Health. -27. DelmiaNotifier — Standalone external client tool (NOT a cluster component, NOT in the Host): a compact Native-AOT (`win-x64`) console app (`WWNotifier.exe`) that DELMIA Apriso shells out to per recipe download. POSTs to the Inbound API `DelmiaRecipeDownload` method (`X-API-Key`, key from `SCADABRIDGE_API_KEY`), with connect-failure-only failover across a comma-list of base URLs, and reports the legacy `YES`/`NO` + exit-code stdout contract — a drop-in replacement for the legacy `WWNotifier` (see `docs/former-api-specs/dnc/`). Zero-dependency BCL-only, `System.Text.Json` source-gen. Project README: `src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/README.md`; design: `docs/plans/2026-06-26-delmia-recipe-notifier-design.md`. +27 components. The catalog — what each one is and its key design commitments — lives in the +`scadabridge-components` skill (`.claude/skills/scadabridge-components/SKILL.md`); the authoritative +spec for each is `docs/requirements/Component-.md`, and `README.md` carries the component table. ## Key Design Decisions (for context across sessions) @@ -120,29 +79,26 @@ Other peers in the `scadaproj` family (see `scadaproj/CLAUDE.md` for details): ` - DCL write failures returned synchronously to calling script. - Tag path resolution retried periodically for devices still booting. - Static attribute writes persisted to local SQLite (survive restart/failover, reset on redeployment). -- **Consolidated site database (LocalDb Phase 1 + 2, complete 2026-07-20).** **Ten** tables now live in ONE `ZB.MOM.WW.LocalDb`-managed SQLite file — the Phase 1 pair (`OperationTracking`, `site_events`) plus Phase 2's `sf_messages` and the seven site config tables (`deployed_configurations`, `static_attribute_overrides`, `shared_scripts`, `external_systems`, `database_connections`, `data_connection_definitions`, `native_alarm_state`), configured by the **required** `LocalDb:Path` (`/app/data/site-localdb.db` on the rig; validated with `ValidateOnStart`, so a site config missing it fails to boot). Both are `RegisterReplicated` tables. Consequences worth knowing: +- **Consolidated site database (LocalDb Phase 1 + 2, complete 2026-07-20).** Ten tables now live in ONE `ZB.MOM.WW.LocalDb`-managed SQLite file (enumerate them with `grep -r RegisterReplicated src/`), configured by the **required** `LocalDb:Path` (`/app/data/site-localdb.db` on the rig; validated with `ValidateOnStart`, so a site config missing it fails to boot). Consequences worth knowing: - `site_events.id` changed from autoincrement INTEGER to an application-minted **GUID**. Last-writer-wins keys on the primary key, so two nodes independently minting `id=1,2,3…` would destroy each other's events rather than merge them. The event-log read path uses a composite `(timestamp, id)` keyset cursor with an **opaque string** continuation token; `EventLogEntry.Id` and both `ContinuationToken`s are `string`/`string?` on the site↔central Akka DTOs. - `ScadaBridge:OperationTracking:ConnectionString`, `ScadaBridge:SiteEventLog:DatabasePath` and — as of Phase 2 — `ScadaBridge:StoreAndForward:SqliteDbPath` + `ScadaBridge:Database:SiteDbPath` are all **migration-only** — nothing reads them but `SiteLocalDbLegacyMigrator`, which copies a pre-Phase-1 file in once (deterministic `mig-{NodeName}-{legacyId}` event ids, `INSERT OR IGNORE`, runs AFTER `RegisterReplicated` so migrated rows replicate) and renames it `.migrated`. Delete the keys once a node has migrated. - This incidentally fixes a data-loss bug: both legacy databases defaulted to CWD-relative paths **outside** the mounted volume and were discarded on every container recreate. - **Replication is default-OFF and opt-in** via `LocalDb:Replication:PeerAddress` + a matching `ApiKey` on both nodes. `LocalDbSyncAuthInterceptor` is **fail-closed**: no configured key means no sync stream is accepted at all, so a key typo does not degrade to unauthenticated replication — the pair simply stops converging. The sync endpoint shares the existing site gRPC h2c listener (8083); no new port. Rig posture: **site-a replicated, site-b/site-c deliberately not**, so both states are proven side-by-side. Status surfaces on the site health report as `LocalDbReplicationConnected` / `LocalDbOplogBacklog` (both nullable — null means "no data", NOT "disconnected with an empty backlog") and as `localdb_*` Prometheus series. Note `ZbTelemetryOptions.Meters` is an **allowlist** (`SiteServiceRegistration.ObservedMeters`); an unlisted meter exports nothing, silently. - - Design: **scadaproj** `docs/plans/2026-07-19-scadabridge-localdb-design.md` (that doc lives in the umbrella repo, not here); Phase 1 plan + Phase 2 gate are here under `docs/plans/`. **Phase 2 is COMPLETE** (branch `feat/localdb-phase2`, live gate PASS 2026-07-20 — all 10 checks, evidence in `docs/plans/2026-07-19-localdb-phase2-live-gate.md`). It moved the config tables + `sf_messages` in and **deleted** `SiteReplicationActor`, its `ReplicationMessages`, StoreAndForward's `ReplicationService`, and `StoreAndForwardStorage.ReplaceAllAsync`. What those did, and why nothing replaced them: + - Design: **scadaproj** `docs/plans/2026-07-19-scadabridge-localdb-design.md` (that doc lives in the umbrella repo, not here); Phase 1 plan + Phase 2 gate are here under `docs/plans/`. Phase 2 **deleted** `SiteReplicationActor`, its `ReplicationMessages`, StoreAndForward's `ReplicationService`, and `StoreAndForwardStorage.ReplaceAllAsync` — do not reintroduce them: - - `SiteReplicationActor` pushed config deploys to the peer and ran a **notify-and-fetch** exchange (tell the standby a deploy happened; it HTTP-fetches the config itself, with retries and a superseded-404 path). Config rows now simply replicate — **the standby makes no fetch at all** during a deploy (verified live). `SiteReconciliationActor` still fetches at node STARTUP when central reports gaps; that path survives and is a different thing. - - `ReplicationService` fanned each buffer mutation (add/remove/park/requeue) to the standby by hand. CDC triggers on `sf_messages` do it now. - - `ReplaceAllAsync` was a destructive delete-all-then-insert-all resync. It was not merely unused after the cutover but **unsafe to keep**: a mass DELETE on a now-replicated table would be captured and shipped to the peer. LocalDb's snapshot resync merges per row under LWW and never deletes, which is also why the old N1 directional-authority guard is gone — there is no wipe left to gate. + - CDC replication does all three jobs now: config deploys reach the standby as ordinary row changes — **the standby makes no fetch at all** during a deploy (`SiteReconciliationActor`'s node-STARTUP fetch when central reports gaps is a different, surviving path) — and buffer mutations replicate via triggers on `sf_messages`. `ReplaceAllAsync` was a destructive delete-all-then-insert-all resync and is **unsafe to reintroduce**: a mass DELETE on a replicated table would be captured and shipped to the peer. LocalDb's snapshot resync merges per row under LWW and never deletes, which is also why the old N1 directional-authority guard is gone — there is no wipe left to gate. - **`notification_lists` and `smtp_configurations` are created but deliberately NOT registered.** They are permanently empty on a site (no writer since 2026-07-10, the migrator skips them, the active-node purge keeps them empty), and registering them would open a standing replication channel whose only historical payload was plaintext SMTP passwords. Pinned by a security-named test, and verified live: those two tables have **no CDC triggers** on either rig node. - **Operational constraints (read before upgrading a site pair):** stop and start both nodes TOGETHER — rolling one at a time is no longer supported, since the legacy `SfBufferSnapshot` compatibility handler went with the replicator. And a node offline longer than `LocalDb:Replication:TombstoneRetention` (default 7 days) can resurrect deleted rows on rejoin. See `docs/deployment/topology-guide.md`. - `LocalDb:Replication:MaxBatchSize` batches by ROW COUNT, not bytes, against a 4 MB gRPC cap — the rig pins it to **16** (~70 KB worst-case `config_json` x 16 ~= 1.1 MB). The 500 default would allow ~35 MB. - All timestamps are UTC throughout the system. - Inter-cluster communication uses **three** transports, not two — **all cross-cluster command/control and data now rides gRPC** after the ClusterClient→gRPC migration's Phase 4 (`docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`) deleted Akka `ClusterClient`/`ClusterClientReceptionist`: (1) **gRPC command/control** — site→central over the central-hosted `CentralControlService` (`GrpcCentralTransport`, sticky central-a→central-b channel pair; deployments/notifications/health/heartbeat/audit-ingest/reconcile), and central→site over the site-hosted `SiteCommandService` (`GrpcSiteTransport`, per-site NodeA→NodeB channel pair; the 28 lifecycle/OPC-UA/query/parked/route/failover commands); (2) **gRPC** server-streaming for real-time data (attribute values, alarm states, `SiteStreamService`); and (3) **plain token-gated HTTP** for the deployment config itself — notify-and-fetch, the site pulls the config from `DeploymentConfigEndpoints` (`ManagementService/DeploymentConfigEndpoints.cs`) with an `X-Deployment-Token` header, `AllowAnonymous` with the per-deployment token as the entire security boundary. The gRPC boundary is per-site PSK-authenticated (`ControlPlaneAuthInterceptor`, unchanged). There is **no receptionist registration** — discovery is by dialling configured endpoints; central builds one `SitePairChannelProvider` per site (addresses from `Site.GrpcNodeAAddress`/`GrpcNodeBAddress`, refreshed from the DB every 60s and on admin changes), sites dial `ScadaBridge:Communication:CentralGrpcEndpoints` (both central nodes, h2c on `CentralGrpcPort` 8083, **NOT** via Traefik). **Discovery is asymmetric by design:** central discovers site gRPC addresses from the *database* (refreshable at runtime), sites discover central from *appsettings* (`CentralGrpcEndpoints`, static — restart required; `StartupValidator` requires a Site node to list at least one). `Akka.Cluster.Tools` stays for ClusterSingleton; only the ClusterClient part is gone. **Central never buffers for an unreachable site** — the send fails with the caller's Ask/deadline timing out; a `ConnectionStateChanged` mechanism built for this was deleted as dead code. -- **All clusters share ONE ActorSystem name**, `"scadabridge"` — hardcoded at `AkkaHostedService.cs:191`. Central and each site are separate clusters *only* by seed-node partitioning. This is required, not incidental: Akka.Remote address matching means a ClusterClient could not reach a differently-named system. +- **All clusters share ONE ActorSystem name**, `"scadabridge"` — hardcoded in `AkkaHostedService` at the `ActorSystem.Create` call. Central and each site are separate clusters *only* by seed-node partitioning. The constraint originated with ClusterClient (Akka.Remote address matching meant it could not reach a differently-named system); whether it is still load-bearing after the gRPC migration has **not** been re-verified, so treat the name as fixed until someone checks. - **`ActiveNodeEvaluator.SelfIsOldestUp` is THE single definition of "active node"** (`Communication/ClusterState/ActiveNodeEvaluator.cs`) — the **oldest Up member** in a role scope, and explicitly **never `cluster.State.Leader`**: leadership (lowest address) is an Akka-internal concept that diverges from singleton placement permanently once the original first node restarts and rejoins, and both sides claim it during a partition. The equivalence *oldest-Up == where `ClusterSingletonManager` places singletons* **is** the design. `ClusterActivityEvaluator.SelfIsOldest`, the S&F delivery gate, `/health/active` and the heartbeat `IsActive` stamp all delegate here. -- Site nodes carry **two Akka roles**: the base `Site` plus a site-specific `site-{SiteId}` (`BuildRoles`, `AkkaHostedService.cs:386`). Singletons scope to the **site-specific** role. -- **The gRPC boundary is authenticated (PSK) as of 2026-07-22; Akka remoting still is not, and nothing is encrypted.** Akka remoting sets no `enable-ssl`, no secure cookie, no `trusted-selection-paths` — so the ClusterClient command/control path remains open to anyone who can reach the remoting port, and the boundary still assumes a trusted network. The gRPC listener stays **h2c**, but `SiteStreamService` is no longer open: `ControlPlaneAuthInterceptor` (`Host/ControlPlaneAuthInterceptor.cs`) gates `/sitestream.SiteStreamService/` — including the `PullAuditEvents`/`PullSiteCalls` RPCs that return audit rows — against a **per-site preshared key**, fail-closed, constant-time compared, alongside the separate `LocalDbSyncAuthInterceptor` on `/localdb_sync.v1.LocalDbSync/` with its own separate key. **Site side:** `ScadaBridge:Communication:GrpcPsk`, in production `${secret:SB-GRPC-PSK-}`, and **`StartupValidator` refuses to boot a site node without it** (an unset key would leave the node healthy-looking but serving nothing). **Central side:** `SitePskProvider` resolves `SB-GRPC-PSK-{siteId}` from the secrets store at channel-build time (sites are added at runtime, so no boot-time expansion is possible), with `ScadaBridge:Communication:SitePsks:{siteId}` as an override for hosts running without a master key — the docker rig uses the latter. One key per site, never fleet-wide. A bearer token over h2c is readable and replayable on-path; TLS is the follow-on hardening and needs no change to this design. Introduced by Phase 0 of the ClusterClient→gRPC migration (`docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`). -- **Akka frame size is the default 128 KB with `log-frame-size-exceeding` off**, and no custom serializer is configured — so payload carried over ClusterClient is JSON-escaped a second time by the default Newtonsoft serializer, roughly doubling it. Over the limit the transport drops **that one message** without tearing down the association (heartbeats keep flowing, the site still reports healthy) and the central Ask simply times out. See `docs/known-issues/2026-06-26-deploy-config-exceeds-akka-frame-size.md`; `DeployArtifactsCommand` still carries payload and remains exposed. -- gRPC streaming channel — **note the direction is inverted from the data flow**: data moves site→central, but each **site node hosts the gRPC server** (`SiteStreamGrpcServer`, Kestrel h2c, port 8083, mapped **only in the Site branch** of `Program.cs`) and **central is the client**, dialling in. There is **no gRPC server on central at all**, which is why the two `Ingest*` unary RPCs — documented as a "central-side ingest surface" — are dead in practice (acknowledged in `AkkaHostedService.cs:510-519`); sites reach central over ClusterClient instead. Central creates per-site `SiteStreamGrpcClient` via `SiteStreamGrpcClientFactory`, keyed **`(siteId, endpoint)`** — the key was widened from site-only to fix an arch-review High where one session's NodeA→NodeB flip disposed a channel another session was still using. Site entity has GrpcNodeAAddress/GrpcNodeBAddress fields. Proto: `sitestream.proto`, **6 RPCs** (2 server-streaming: `SubscribeInstance`, `SubscribeSite` (site-wide, alarm-only); 4 unary: `IngestAuditEvents`, `IngestCachedTelemetry`, `PullAuditEvents`, `PullSiteCalls`), `SiteStreamEvent` (oneof: AttributeValueUpdate, AlarmStateUpdate). Field numbers are never reused; evolution is additive only (`AlarmStateUpdate` grew 7→23 fields for the native-alarm mirror). Generated C# is **vendored** under `Communication/SiteStreamGrpc/` with the `` include commented out — regeneration is a manual toggle-build-copy-untoggle. DebugStreamEvent message removed (no longer flows through ClusterClient). -- Native alarms: a read-only mirror of native alarms from OPC UA Alarms & Conditions servers and the MxAccess Gateway, unified onto an A&C-style condition model (`AlarmConditionState`: orthogonal Active/Acked/Confirmed/Shelved/Suppressed + 0–1000 severity) plus an `AlarmKind` discriminator (Computed/NativeOpcUa/NativeMxAccess). New DCL capability seam `IAlarmSubscribableConnection` (implemented by the OPC UA and MxGateway adapters); the `DataConnectionActor` opens ONE alarm feed per connection and routes transitions to instances by source-object reference. A `NativeAlarmActor` (peer to the computed `AlarmActor` under `InstanceActor`) mirrors one source binding: snapshot atomic-swap on (re)subscribe, retention (drops once inactive+acked), per-source cap, and site SQLite persistence (`native_alarm_state`, survives failover, cleared on redeploy/undeploy — mirrors static overrides). State streams to central over the additively-enriched gRPC `AlarmStateUpdate` (the existing computed `AlarmStateChanged` was enriched additively) and seeds via the DebugView snapshot. Authoring: `TemplateNativeAlarmSource` / `InstanceNativeAlarmSourceOverride` entities flatten to `ResolvedNativeAlarmSource` (inherit/compose/override); management commands + ManagementActor handlers + CLI (`template/instance native-alarm-source`) + Central UI (template editor tab + instance override panel) + enriched DebugView alarm table. Read-only — no ack-back; no central tables. -- OPC UA / MxGateway UX (M7): operator **Alarm Summary** page (`/monitoring/alarms`, RequireDeployment, read-only) fans out the existing per-instance `DebugViewSnapshot` Ask (SemaphoreSlim-capped, partial-results tolerant) and aggregates client-side — no central alarm store; shared `AlarmStateBadges` component. **Aggregated live stream shipped 2026-07-10** (`docs/plans/2026-07-10-aggregated-live-alarm-stream-plan.md`): a transient in-memory per-site central live cache (`ISiteAlarmLiveCache`) fed by a site-wide, alarm-only `SubscribeSite` gRPC stream (seed-then-stream), pushing near-real-time deltas to the page over the Blazor circuit with the 15s poll kept as fallback + NotReporting authority — still no persisted central alarm store. OPC UA node browser gains `BrowseNext` continuation paging ("Load more"), a bounded recursive address-space **search** (`IAddressSpaceSearchable` seam; depth + result caps; substring on DisplayName/path), and **type-info** (DataType/ValueRank/Writable on `BrowseNode` for Variables). Attribute-override **CSV bulk import** (`OverrideCsvParser`, all-or-nothing) via InstanceConfigure `InputFile` + CLI `instance import-overrides --file` (native-alarm-source-override CSV deferred). **Verify-endpoint** probe (temporary `RealOpcUaClient`, short timeout, captures an untrusted server cert but NEVER trusts it) + **site-local cert trust**: per-node `CertStoreActor` (runs on every site node, not a singleton) writing the `.der` into the node's OPC UA trusted-peer PKI store; DeploymentManager broadcasts `TrustServerCertCommand`/`RemoveServerCertCommand` to BOTH site nodes so PKI stores stay consistent across failover; Admin-gated cert-management UI (`/design/connections/{id}/certificates`). No central persistence of cert trust (follow-up). +- Site nodes carry **two Akka roles**: the base `Site` plus a site-specific `site-{SiteId}` (`AkkaHostedService.BuildRoles`). Singletons scope to the **site-specific** role. +- **The gRPC boundary is authenticated (PSK) as of 2026-07-22; Akka remoting still is not, and nothing is encrypted.** Akka remoting sets no `enable-ssl`, no secure cookie, no `trusted-selection-paths` — so intra-cluster Akka remoting remains open to anyone who can reach the remoting port, and that boundary still assumes a trusted network. The gRPC listener stays **h2c**, but `SiteStreamService` is no longer open: `ControlPlaneAuthInterceptor` (`Host/ControlPlaneAuthInterceptor.cs`) gates `/sitestream.SiteStreamService/` — including the `PullAuditEvents`/`PullSiteCalls` RPCs that return audit rows — against a **per-site preshared key**, fail-closed, constant-time compared, alongside the separate `LocalDbSyncAuthInterceptor` on `/localdb_sync.v1.LocalDbSync/` with its own separate key. **Site side:** `ScadaBridge:Communication:GrpcPsk`, in production `${secret:SB-GRPC-PSK-}`, and **`StartupValidator` refuses to boot a site node without it** (an unset key would leave the node healthy-looking but serving nothing). **Central side:** `SitePskProvider` resolves `SB-GRPC-PSK-{siteId}` from the secrets store at channel-build time (sites are added at runtime, so no boot-time expansion is possible), with `ScadaBridge:Communication:SitePsks:{siteId}` as an override for hosts running without a master key — the docker rig uses the latter. One key per site, never fleet-wide. A bearer token over h2c is readable and replayable on-path; TLS is the follow-on hardening and needs no change to this design. Introduced by Phase 0 of the ClusterClient→gRPC migration (`docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`). +- gRPC streaming channel — **note the direction is inverted from the data flow**: data moves site→central, but each **site node hosts the gRPC server** (`SiteStreamGrpcServer`, Kestrel h2c, port 8083, mapped **only in the Site branch** of `Program.cs`) and **central is the client**, dialling in. Central creates per-site `SiteStreamGrpcClient` via `SiteStreamGrpcClientFactory`, keyed **`(siteId, endpoint)`** — the key was widened from site-only to fix an arch-review High where one session's NodeA→NodeB flip disposed a channel another session was still using. Proto evolution is **additive only** and field numbers are never reused (`AlarmStateUpdate` grew 7→23 fields for the native-alarm mirror). Generated C# is **vendored** under `Communication/SiteStreamGrpc/` with the `` include commented out — regeneration is a manual toggle-build-copy-untoggle. +- Native alarms are a **read-only** mirror of OPC UA Alarms & Conditions and MxAccess Gateway alarms — **no ack-back, no central tables**; state lives in the site's `native_alarm_state`, survives failover, and is cleared on redeploy/undeploy (mirrors static overrides). Central's per-site live alarm cache (`ISiteAlarmLiveCache`) is **transient in-memory only** — there is deliberately no persisted central alarm store, so the 15s poll remains the NotReporting authority behind the live stream. See `Component-DataConnectionLayer.md` / `Component-CentralUI.md` for the model and the authoring surface. +- OPC UA cert trust is **site-local and not persisted centrally** (follow-up): the verify-endpoint probe captures an untrusted server cert but **NEVER trusts it**, and DeploymentManager broadcasts `TrustServerCertCommand`/`RemoveServerCertCommand` to **BOTH** site nodes — `CertStoreActor` runs on every site node, not as a singleton, so PKI stores stay consistent across failover. ### External Integrations - External System Gateway: HTTP/REST only, JSON serialization, API key + Basic Auth. @@ -218,14 +174,14 @@ Other peers in the `scadaproj` family (see `scadaproj/CLAUDE.md` for details): ` - Failure detection: 2s heartbeat, 10s threshold. Total failover ~25s (drill-measured 2026-07-21 under auto-down: active-crash TAKEOVER in 28s, standby-crash removal in 27s with 0 routing blips — `docker/failover-drill.sh`). - CoordinatedShutdown for graceful singleton handover. - Automatic dual-node recovery from persistent storage. -- **Active/standby is decided by `ActiveNodeEvaluator.SelfIsOldestUp`, never by cluster leadership** — see the Architecture note above. `/health/active` is **central-only** (site nodes map no `/health/*` at all) and backs both Traefik's active-node routing and `IActiveNodeGate`, so the proxy and the Inbound API always agree on which node is active. Central never needs to know which *site* node is active: ClusterClient contact rotation reaches either receptionist and the site-internal `ClusterSingletonProxy` lands the work on the active node for free. The **exception is gRPC**, which picks `GrpcNodeAAddress`/`GrpcNodeBAddress` explicitly and flips on error. +- **Active/standby is decided by `ActiveNodeEvaluator.SelfIsOldestUp`, never by cluster leadership** — see the Architecture note above. `/health/active` is **central-only** (site nodes map no `/health/*` at all) and backs both Traefik's active-node routing and `IActiveNodeGate`, so the proxy and the Inbound API always agree on which node is active. Central reaches a site by dialling `GrpcNodeAAddress`/`GrpcNodeBAddress` explicitly and flipping on error; whichever node answers, the site-internal `ClusterSingletonProxy` lands the work on the active node for free, so central still never needs to track which *site* node is active. - **Seed-node ordering: every node lists ITSELF first (decision 2026-07-22) — the boot-alone gap is CLOSED.** Only `seed-nodes[0]` may self-join to form a new cluster (Akka runs `FirstSeedNodeProcess` for it, `JoinSeedNodeProcess` — which can never form one — for everyone else). All 14 shipped node appsettings now lead with the node's own address, so any node can cold-start alone and become operational unattended (~5s, `seed-node-timeout`); `StartupValidator` fails the boot if the ordering is broken (compares host AND port; Akka does no DNS canonicalisation). Two nodes cold-starting together while mutually reachable converge on ONE cluster via the `InitJoin` handshake — they split only under a genuine boot-time partition, the same class auto-down accepts. **An external self-form timer (`Cluster.Join(SelfAddress)` after a window) was implemented and REJECTED:** it sits outside the join handshake, so on a routine standby restart — where the peer is alive but the join is stalled behind removal of the node's own stale incarnation — it fires mid-join and permanently splits the pair (measured: still split after 90s). Regression tests: `SelfFirstSeedBootstrapTests`. The keep-oldest active-crash total outage was separately closed by the auto-down decision. See `docs/requirements/Component-ClusterInfrastructure.md` → Seed Node Ordering. - **Simultaneous-cold-start split-brain guard (opt-in, Gitea #33) — the residual self-first cost, closed.** Self-first-on-both means a *truly simultaneous* cold start (shared power/hypervisor event) races `FirstSeedNodeProcess` on BOTH nodes → two 1-node clusters that never merge (in-process loopback tests converge and hide it; real parallel VM starts don't — OtOpcUa reproduced it live). Ported from OtOpcUa (`lmxopcua` `d1dac87f`): a **dark switch `ScadaBridge:Cluster:BootstrapGuard:Enabled` (default OFF, guard-off byte-identical)**. On: `BuildHocon` emits an EMPTY seed list (Akka does not auto-join) and `ClusterBootstrapCoordinator` (`IHostedService`, registered in BOTH the Central and Site composition roots) issues ONE reachability-gated `JoinSeedNodes` — the pure `ClusterBootstrapGuard` core makes the lower canonical `host:port` the **founder** (self-first, forms immediately), the higher node TCP-probes the founder up to `PartnerProbeSeconds` (25s) and joins **peer-first** if reachable else **self-first** (cold-start-alone preserved). Decided BEFORE a single join, **never re-forms mid-handshake** (that was the rejected self-form-timer's flaw); case-insensitive founder tie-break; probe timings validated `>0` when enabled. Residual accepted trade: founder dies in the probe→join window → higher node hangs unjoined, coordinator WARNs, a restart recovers it. Tests: `ClusterBootstrapGuardTests` (pure) + `ClusterBootstrapCoordinatorTests` (real-ActorSystem, incl. both-cold-start-together-form-one-cluster). See `docs/requirements/Component-ClusterInfrastructure.md` → Simultaneous cold start. ### UI & Monitoring - Central UI: Blazor Server (ASP.NET Core + SignalR) with Bootstrap CSS. No third-party component frameworks (no Blazorise, MudBlazor, Radzen, etc.). Build custom Blazor components for tables, grids, forms, etc. - UI design: Clean, corporate, internal-use aesthetic. Not flashy. Use the `frontend-design` skill when designing UI pages/components. -- Debug view: real-time streaming via DebugStreamBridgeActor + gRPC (events via SiteStreamGrpcClient, snapshot via ClusterClient). Health dashboard: 10s polling timer. Deployment status: real-time push via SignalR. +- Debug view: real-time streaming via DebugStreamBridgeActor + gRPC (events via `SiteStreamGrpcClient`, snapshot via the `SiteCommandService` query surface — `QueryReply.DebugViewSnapshot`). Health dashboard: 10s polling timer. Deployment status: real-time push via SignalR. - Health reports: 30s interval, 60s offline threshold, monotonic sequence numbers, raw error counts per interval. - Dead letter monitoring as a health metric. - Site Event Logging: 30-day retention, 1GB storage cap, daily purge, paginated queries with keyword search. @@ -254,14 +210,9 @@ Other peers in the `scadaproj` family (see `scadaproj/CLAUDE.md` for details): ` ## Tool Usage -- When consulting with the Codex MCP tool, use model `gpt-5.4`. - When a task requires setting up or controlling system state (sites, templates, instances, data connections, deployments, security, etc.) and the Central UI is not needed, prefer the ScadaBridge CLI over manual DB edits or UI navigation. See [`src/ZB.MOM.WW.ScadaBridge.CLI/README.md`](src/ZB.MOM.WW.ScadaBridge.CLI/README.md) for the full command reference. ### CLI Quick Reference (Docker / OrbStack) -- **Management URL**: `http://localhost:9000` — the CLI connects via the Traefik load balancer, which routes to the active central node. Direct access: central-a on port 9001, central-b on port 9002. -- **Test user**: `--username multi-role --password password` — has Admin, Design, and Deployment roles. The `admin` user only has the Admin role and cannot create templates, data connections, or deploy. -- **Config file**: `~/.scadabridge/config.json` — stores `managementUrl` and default format. See `docker/README.md` for a ready-to-use test config. -- **Rebuild cluster**: `bash docker/deploy.sh` — builds the `scadabridge:latest` image and recreates all containers. Run this after code changes to ManagementActor, Host, or any server-side component. -- **Infrastructure services**: `cd infra && docker compose up -d` — starts MS SQL, OPC UA, SMTP, and REST API. These are separate from the cluster containers in `docker/`. **LDAP is NOT started here** — it is the shared GLAuth on `10.100.0.35:3893` (dc=zb,dc=local); source of truth and config: `scadaproj/infra/glauth/`. -- **All test LDAP passwords**: `password` (see `scadaproj/infra/glauth/config.toml` for users and groups; canonical cross-app login: `multi-role`). +Management URL, test credentials, rebuild/redeploy commands and the infra services are in the +`scadabridge-cluster-ops` skill (`.claude/skills/scadabridge-cluster-ops/SKILL.md`). diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs index 31cfbfcd..e60f4331 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs @@ -144,8 +144,8 @@ public static class ServiceCollectionExtensions // ISiteStreamAuditClient: NoOp default. This binding remains correct for // central/test composition roots that have no SiteCommunicationActor. - // The real implementation is ClusterClientSiteAuditClient, which pushes - // audit telemetry to central over Akka ClusterClient via the site's + // The real implementation is SiteCommunicationAuditClient, which pushes + // audit telemetry to central via the site's // SiteCommunicationActor — the Host wires it directly into the // SiteAuditTelemetryActor's Props.Create call for site roles (it cannot // be a DI singleton because it needs the SiteCommunicationActor IActorRef, diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs index 7df5588c..2e638029 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs @@ -37,7 +37,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry; /// Local-write only — the wire push is the drain actor's job. This /// forwarder is deliberately synchronous against the two site-local SQLite /// stores and never pushes to central itself. The site→central transport is -/// now live: ClusterClientSiteAuditClient is the production binding of +/// now live: SiteCommunicationAuditClient is the production binding of /// on site roles (with /// NoOpSiteStreamAuditClient retained only for central/test composition /// roots). The push happens out-of-band: diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs index 26d3c111..6f831470 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs @@ -6,9 +6,9 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry; /// Mockable abstraction over the central site-audit push surface that /// uses to forward /// payloads. The production implementation is -/// — a ClusterClient-based client, -/// wired in the Host for site roles, that forwards batches to central via the -/// site's SiteCommunicationActor. Unit tests substitute via NSubstitute +/// — wired in the Host for site +/// roles, it forwards batches to central via the site's +/// SiteCommunicationActor. Unit tests substitute via NSubstitute /// against this interface so the actor never needs a live transport. /// public interface ISiteStreamAuditClient @@ -36,8 +36,8 @@ public interface ISiteStreamAuditClient /// once central has acknowledged them. /// /// - /// The production forwards over - /// the ClusterClient transport; the + /// The production forwards over + /// the site's central transport; the /// DI default (used by central and test composition roots) returns an empty /// ack so no rows are flipped. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/NoOpSiteStreamAuditClient.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/NoOpSiteStreamAuditClient.cs index ff25ab79..a9935f30 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/NoOpSiteStreamAuditClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/NoOpSiteStreamAuditClient.cs @@ -7,9 +7,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry; /// . /// It is a no-op binding for composition roots that have no /// SiteCommunicationActor — central and test roots. Site roles override -/// it in the Host with the ClusterClient-based -/// , which actually forwards audit -/// telemetry to central. +/// it in the Host with , which +/// actually forwards audit telemetry to central. /// /// /// @@ -33,7 +32,7 @@ public sealed class NoOpSiteStreamAuditClient : ISiteStreamAuditClient { ArgumentNullException.ThrowIfNull(batch); // Empty ack — no EventIds will be flipped to Forwarded, so rows stay - // Pending until the real ClusterClientSiteAuditClient (or a test stub) + // Pending until the real SiteCommunicationAuditClient (or a test stub) // takes over. return Task.FromResult(EmptyAck); } @@ -45,7 +44,7 @@ public sealed class NoOpSiteStreamAuditClient : ISiteStreamAuditClient // Empty ack — same rationale as IngestAuditEventsAsync. The site still // writes the audit + tracking rows to its SQLite stores authoritatively; // central-side state only materialises once the real - // ClusterClientSiteAuditClient (or a test stub) is wired in. + // SiteCommunicationAuditClient (or a test stub) is wired in. return Task.FromResult(EmptyAck); } } diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ClusterClientSiteAuditClient.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SiteCommunicationAuditClient.cs similarity index 86% rename from src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ClusterClientSiteAuditClient.cs rename to src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SiteCommunicationAuditClient.cs index dec53865..a56b5e90 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ClusterClientSiteAuditClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SiteCommunicationAuditClient.cs @@ -7,11 +7,11 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry; /// /// Production binding for site composition -/// roots: pushes audit telemetry to central over Akka ClusterClient via -/// the site's SiteCommunicationActor. The actor forwards the command to -/// /user/central-communication and the central -/// CentralCommunicationActor Asks the AuditLogIngestActor proxy — -/// the same command/control transport notifications already use. Wired by the +/// roots: pushes audit telemetry to central via the site's +/// SiteCommunicationActor, which owns the transport choice (today +/// GrpcCentralTransport, over the central-hosted CentralControlService). +/// The central CentralCommunicationActor Asks the AuditLogIngestActor +/// proxy — the same command/control path notifications already use. Wired by the /// Host for site roles; central and test composition roots keep the /// DI default (they have no /// SiteCommunicationActor). @@ -34,12 +34,12 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry; /// ) because the /// builds them with /// . This client converts them back into -/// the / entities the Akka -/// command messages carry — the same DTO→entity translation the +/// the / entities the command +/// messages carry — the same DTO→entity translation the /// SiteStreamGrpcServer performs for the gRPC reconciliation path. /// /// -public sealed class ClusterClientSiteAuditClient : ISiteStreamAuditClient +public sealed class SiteCommunicationAuditClient : ISiteStreamAuditClient { private readonly IActorRef _siteCommunicationActor; private readonly TimeSpan _askTimeout; @@ -49,15 +49,15 @@ public sealed class ClusterClientSiteAuditClient : ISiteStreamAuditClient /// /// /// The site's SiteCommunicationActor — it forwards the ingest command - /// over the registered central ClusterClient and routes the reply back to - /// this client's Ask. + /// to central over its configured ICentralTransport and routes the + /// reply back to this client's Ask. /// /// /// Ask timeout for the round-trip to central. On expiry the Ask throws /// , which the drain loop treats /// as transient (rows stay Pending). /// - public ClusterClientSiteAuditClient(IActorRef siteCommunicationActor, TimeSpan askTimeout) + public SiteCommunicationAuditClient(IActorRef siteCommunicationActor, TimeSpan askTimeout) { ArgumentNullException.ThrowIfNull(siteCommunicationActor); _siteCommunicationActor = siteCommunicationActor; diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Sites/Site.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Sites/Site.cs index 90b081b9..16f7f0e0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Sites/Site.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Sites/Site.cs @@ -10,9 +10,9 @@ public class Site public string SiteIdentifier { get; set; } /// Optional description of the site. public string? Description { get; set; } - /// Akka remote address for site node A (ClusterClient contact point). + /// Akka remote address for site node A. public string? NodeAAddress { get; set; } - /// Akka remote address for site node B (ClusterClient contact point). + /// Akka remote address for site node B. public string? NodeBAddress { get; set; } /// gRPC endpoint for site node A used by the central SiteStreamGrpcClient. public string? GrpcNodeAAddress { get; set; } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Audit/SiteCallRelayMessages.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Audit/SiteCallRelayMessages.cs index a41090d0..081edb61 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Audit/SiteCallRelayMessages.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Audit/SiteCallRelayMessages.cs @@ -28,7 +28,7 @@ public enum SiteCallRelayOutcome NotParked, /// - /// The owning site could not be reached (offline / no ClusterClient route / + /// The owning site could not be reached (offline / no route to the site / /// relay timed out). The action was NOT applied; the operator may retry once /// the site is back online. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs index 37f7771c..360b4c96 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugViewSnapshot.cs @@ -19,8 +19,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView; /// /// /// A new dedicated message type (DebugViewInstanceNotFound) was -/// considered but rejected: the ClusterClient / ClusterClientReceptionist -/// channel is typed on the request side and the bridge actor is already +/// considered but rejected: the site↔central channel is typed on the request +/// side and the bridge actor is already /// pattern-matching on DebugViewSnapshot for the initial-snapshot TCS /// in DebugStreamService. Introducing a second reply type would require /// every consumer to handle an additional Ask result union — more change diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Deployment/ReconcileSiteRequest.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Deployment/ReconcileSiteRequest.cs index c9cc6ef1..02a9eb57 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Deployment/ReconcileSiteRequest.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Deployment/ReconcileSiteRequest.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment; /// -/// Site→central (over ClusterClient) on node startup: the node's local deployed inventory, +/// Site→central on node startup: the node's local deployed inventory, /// so central can reply with fetch tokens for whatever the node is missing or stale /// (self-heal a node that was down during a deploy). /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs index 5e498b78..8306ffdc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs @@ -108,8 +108,8 @@ public record SiteHealthReport( /// /// Broadcast wrapper used between central nodes to keep per-node -/// CentralHealthAggregator state in sync. ClusterClient load-balances each -/// incoming SiteHealthReport to one central node; that node re-publishes +/// CentralHealthAggregator state in sync. A site delivers each +/// SiteHealthReport to ONE central node; that node re-publishes /// this wrapper on a DistributedPubSub topic so the peer node's aggregator /// also processes the report (idempotently — sequence numbers guard against /// double-counting). diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WaitForAttribute.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WaitForAttribute.cs index 57480e02..99b29fe9 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WaitForAttribute.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WaitForAttribute.cs @@ -9,7 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance; /// Site-local only. The optional is a non-serializable /// in-process delegate, so this message MUST flow only within a single site node's /// actor system (script execution → Instance Actor). It is never sent across the -/// ClusterClient / gRPC boundary. The value-equality form () +/// site↔central gRPC boundary. The value-equality form () /// would serialize, but the routed/inbound variant is deliberately out of scope here. /// /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs index 0f3a0ec6..9653bbed 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; /// Bidirectional name registry for management command records. The registry contains /// exactly the non-abstract *Command types declared in the /// ZB.MOM.WW.ScadaBridge.Commons.Messages.Management namespace; these are the commands that -/// travel over the HTTP / ClusterClient management boundary. +/// travel over the HTTP management boundary. /// /// /// and are symmetric: diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SchemaLibraryCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SchemaLibraryCommands.cs index 54df037a..02e98747 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SchemaLibraryCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SchemaLibraryCommands.cs @@ -2,7 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; // Schema-library authoring commands. The reusable named JSON-Schema // library (the SharedSchema entity + ISharedSchemaRepository) gains its CRUD -// surface here. These records travel the same HTTP / ClusterClient management +// surface here. These records travel the same HTTP management // boundary as every other *Command and are auto-discovered by reflection in // ManagementCommandRegistry (no manual registry entry needed). // diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/RemoteQuery/SiteFailoverRelayMessages.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/RemoteQuery/SiteFailoverRelayMessages.cs index 0aaf9eda..c4bce8f3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/RemoteQuery/SiteFailoverRelayMessages.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/RemoteQuery/SiteFailoverRelayMessages.cs @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery; /// /// /// Either site node may receive this: SiteCommunicationActor is registered per node -/// (not as a singleton), and ClusterClient contact rotation reaches whichever answers. That is +/// (not as a singleton), and central's gRPC dial reaches whichever node answers. That is /// fine — Cluster.Leave(address) is valid from any member, and the target is resolved /// from cluster state rather than from who received the message. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs index f7310aac..b2cbc71f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs @@ -89,7 +89,7 @@ public class CentralCommunicationActor : ReceiveActor /// Default Ask timeout for routing audit ingest commands to the /// Effective Ask timeout for audit ingest routing. Defaults to /// (30 s) — the two - /// audit-ingest transports (gRPC vs ClusterClient) now share one source of truth + /// audit-ingest entry points (the site stream server and the control plane) share one source of truth /// for the timeout. Overridable via the constructor so tests can exercise the /// timeout/fault path without waiting 30 s. When the window is exceeded the Ask /// faults and that fault is piped back to the caller as a @@ -167,8 +167,8 @@ public class CentralCommunicationActor : ReceiveActor }); // Notification Outbox ingest: a site forwards a buffered NotificationSubmit to the - // central cluster via ClusterClient. Forward to the outbox proxy so the original - // Sender (the site's ClusterClient path) is preserved and the NotificationSubmitAck + // central cluster. Forward to the outbox proxy so the original + // Sender (the site's transport path) is preserved and the NotificationSubmitAck // routes straight back to the site. Receive(HandleNotificationSubmit); @@ -186,9 +186,9 @@ public class CentralCommunicationActor : ReceiveActor }); // Audit Log site→central ingest: a site forwards a batch of audit - // events to the central cluster via ClusterClient. Ask the ingest proxy + // events to the central cluster. Ask the ingest proxy // and pipe the IngestAuditEventsReply back to the original Sender (the - // site's ClusterClient path) so the site can flip its rows to Forwarded. + // site's transport path) so the site can flip its rows to Forwarded. Receive(HandleIngestAuditEvents); // Audit Log combined-telemetry ingest: routes to the same proxy @@ -196,9 +196,9 @@ public class CentralCommunicationActor : ReceiveActor Receive(HandleIngestCachedTelemetry); // Startup reconciliation: a site node forwards its local deployed inventory on - // startup via ClusterClient. Resolve the scoped ReconcileService, diff the + // startup. Resolve the scoped ReconcileService, diff the // inventory against central's expected set, and pipe the ReconcileSiteResponse - // (gap fetch tokens + orphans) straight back to the site node's ClusterClient. + // (gap fetch tokens + orphans) straight back to the site node. Receive(HandleReconcileSiteRequest); } @@ -254,7 +254,7 @@ public class CentralCommunicationActor : ReceiveActor } // Capture Sender before the async/PipeTo — Akka resets Sender between - // dispatches. The reply is piped straight back to the site's ClusterClient. + // dispatches. The reply is piped straight back to the calling site node. // On an Ask timeout or a faulted reply, PipeTo delivers a Status.Failure to // replyTo: the fault propagates to the caller rather than being swallowed. // The site's own Ask through this path then faults, and the site drain loop @@ -285,10 +285,10 @@ public class CentralCommunicationActor : ReceiveActor } /// - /// Startup reconciliation (site→central over ClusterClient): resolve the scoped + /// Startup reconciliation (site→central): resolve the scoped /// in a DI scope, diff the node's reported inventory /// against central's expected set, and pipe the - /// back to the site node's ClusterClient path. The actor stays thin — all the diff + /// back to the site node's transport path. The actor stays thin — all the diff /// and staging logic lives in the service. Mirrors the DB-access pattern used by /// (Task.Run + CreateScope + PipeTo) and the /// Sender-preservation pattern of . @@ -331,7 +331,7 @@ public class CentralCommunicationActor : ReceiveActor MarkHeartbeatLocally(heartbeat); // Fan the heartbeat out to the peer central node so BOTH aggregators mark - // it, regardless of which central node the site's ClusterClient delivered + // it, regardless of which central node the site delivered // to. Without this, a heartbeat that only ever reaches one node leaves the // other node's aggregator blind to that site's liveness after a failover // (arch review 02, Low). MarkHeartbeat is idempotent (timestamp overwrite), @@ -358,7 +358,7 @@ public class CentralCommunicationActor : ReceiveActor } /// - /// Handles a report delivered directly from a site (via ClusterClient): + /// Handles a report delivered directly from a site: /// process locally, then fan out to the peer central node so its /// aggregator stays in sync. /// @@ -407,7 +407,7 @@ public class CentralCommunicationActor : ReceiveActor // HandleConnectionStateChanged removed — no production // caller emitted ConnectionStateChanged, so the workflow ran only in tests. // Disconnect detection is owned by the transport layers (gRPC keepalive + - // ClusterClient/Ask timeout). + // Ask timeout). private void HandleSiteEnvelope(SiteEnvelope envelope) { @@ -491,8 +491,8 @@ public class CentralCommunicationActor : ReceiveActor private void HandleSiteAddressCacheLoaded(SiteAddressCacheLoaded msg) { - // Per-transport per-site resource reconciliation (create/stop ClusterClients, or - // build/drop gRPC channel pairs). Runs on the actor thread each refresh tick. + // Per-site transport resource reconciliation (build/drop gRPC channel + // pairs). Runs on the actor thread each refresh tick. _transport.ReconcileSites(msg); // Self-healing eviction: a site deleted from configuration would otherwise @@ -525,7 +525,7 @@ public class CentralCommunicationActor : ReceiveActor // Subscribe to the peer-replication topic so we receive health reports // delivered to the other central node and keep our local aggregator - // in sync (ClusterClient load-balances reports across nodes). + // in sync (a site may deliver its report to either central node). // Tolerant of non-clustered hosts (TestKit) where the extension is absent. try { @@ -581,7 +581,7 @@ public record RefreshSiteAddresses; /// discipline. The producer wraps the constructed buckets with /// List<T>.AsReadOnly() before piping to Self. /// -/// Akka ClusterClient contact addresses per site (from NodeA/NodeBAddress). +/// Akka remote addresses per site (from NodeA/NodeBAddress). /// Every configured site id, address-bearing or not, for aggregator pruning. /// /// gRPC endpoint pairs per site (from GrpcNodeA/GrpcNodeBAddress) — the streaming path's columns, @@ -603,8 +603,8 @@ public readonly record struct SiteGrpcEndpoints(string? NodeA, string? NodeB); /// /// Peer-replication envelope for a site heartbeat, fanned out over the same /// DistributedPubSub topic as so both -/// central aggregators mark heartbeats regardless of which node the site's -/// ClusterClient delivered to. Only ever travels central↔central (same assembly +/// central aggregators mark heartbeats regardless of which node the site +/// delivered to. Only ever travels central↔central (same assembly /// version, rolled together), so the default reflective serializer is safe. /// public sealed record SiteHeartbeatReplica(HeartbeatMessage Heartbeat); diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs index 4b4170c7..7496b053 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs @@ -94,10 +94,10 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers /// /// Initializes the debug stream bridge actor and registers message handlers. /// - /// Site identifier for targeting ClusterClient messages and logging. + /// Site identifier for targeting site-addressed messages and logging. /// Unique name of the instance whose debug stream is being bridged. /// Correlation id for the debug session. - /// Actor used to forward ClusterClient messages to the site. + /// Actor used to forward site-addressed messages to the site. /// Callback invoked on each received debug event. /// Callback invoked when the stream terminates. /// Factory for creating gRPC streaming clients. @@ -124,7 +124,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers _grpcNodeAAddress = grpcNodeAAddress; _grpcNodeBAddress = grpcNodeBAddress; - // Initial snapshot response from the site (via ClusterClient). + // Initial snapshot response from the site. // If the site reports InstanceNotFound=true the instance is not // deployed there. Under the stream-first lifecycle the gRPC stream // was already opened in PreStart, so the not-found path must tear it down diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/ISiteCommandTransport.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/ISiteCommandTransport.cs index 189c6c4e..89545aa6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/ISiteCommandTransport.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/ISiteCommandTransport.cs @@ -4,13 +4,11 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors; /// /// The central→site command-send seam, injected into -/// below the handler. Exactly one implementation is active per node, -/// chosen by ScadaBridge:Communication:SiteTransport: -/// -/// — today's per-site ClusterClient path (default). -/// — the site -/// SiteCommandService gRPC plane. -/// +/// below the handler. The sole implementation is +/// , over the site +/// SiteCommandService gRPC plane. The seam predates that: it was introduced so the Akka +/// ClusterClient path and the gRPC plane could run side by side during the migration, and +/// is kept because it is the natural substitution point for tests. /// The producers above the seam (CommunicationService's 27 commands, SiteCallAuditActor's /// 2 parked relays, and DebugStreamBridgeActor's subscribe/unsubscribe) are unchanged — they /// still Ask/Tell a to the actor, which delegates here. @@ -27,14 +25,14 @@ public interface ISiteCommandTransport /// delivered to — for an Ask that is the temporary ask actor /// (completing the caller's task); for a Tell-with-sender (the debug bridge) that is the /// originating actor. A message with no route (an unknown site) is warned and dropped so the - /// caller's Ask times out, exactly as today's ClusterClient path behaves. + /// caller's Ask times out — central never buffers for an unreachable site. /// /// The site-addressed command envelope. /// Where a reply (or a ) is delivered. void Send(SiteEnvelope envelope, IActorRef replyTo); /// - /// Reconciles per-site transport resources (ClusterClients for Akka, channel pairs for gRPC) + /// Reconciles per-site transport resources (gRPC channel pairs) /// against the freshly loaded site set. Called once per DB refresh tick with the same cache /// message the actor already receives — the ONE DB-poll loop feeds both transports. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs index c1c53432..a320479a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs @@ -536,7 +536,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers if (_retryCount > MaxRetries) { // Give up the stream, but do NOT stop the aggregator: the periodic reconcile - // still refreshes the cache from ClusterClient snapshots, so the page keeps a + // still refreshes the cache from site snapshots, so the page keeps a // (slower) live-ish view rather than going dark. A later reconcile-triggered // reconnect is not attempted here; the stream is simply left down. _log.Error("Site-alarm gRPC stream for {0} exceeded max retries ({1}); leaving stream down, " + diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommandDispatcher.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommandDispatcher.cs index 1e46d086..ed627e19 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommandDispatcher.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommandDispatcher.cs @@ -21,8 +21,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors; /// /// /// -/// Two transports call this one unit: the Akka -/// (ClusterClient) and the new SiteCommandGrpcService (gRPC). Centralising the +/// Two entry points call this one unit: the site-side +/// and SiteCommandGrpcService. Centralising the /// table means the two can never drift on where a command goes. Each transport keeps /// its own send mechanics — the actor Forwards (preserving the Ask sender), the /// gRPC service Asks and encodes the reply — but both read the same @@ -219,9 +219,9 @@ public sealed class SiteCommandDispatcher private static Route Immediate(object reply) => new(RouteDisposition.ImmediateReply, null, reply); /// - /// The actor's failover path: resolve the standby AND issue the leave in one step (today's - /// coupled behaviour over ClusterClient, where Tell merely enqueues the ack so leave - /// order is immaterial), then return the ack. + /// The actor's failover path: resolve the standby AND issue the leave in one step (coupled, + /// because Tell merely enqueues the ack so leave order is immaterial here), then + /// return the ack. /// /// The failover command. /// The ack to send back. @@ -283,8 +283,8 @@ public sealed class SiteCommandDispatcher } catch (Exception ex) { - // A fault must be reported to the operator, never thrown into supervision (over - // ClusterClient) or surfaced as a broken stream (over gRPC). + // A fault must be reported to the operator, never thrown into supervision or + // surfaced as a broken stream (over gRPC). return new FailoverOutcome( new SiteFailoverAck( msg.CorrelationId, Accepted: false, TargetAddress: null, ErrorMessage: ex.Message), diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs index f8fd7853..b70f3d83 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs @@ -16,9 +16,10 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery; namespace ZB.MOM.WW.ScadaBridge.Communication.Actors; /// -/// Site-side actor that receives messages from central via ClusterClient and routes -/// them to the appropriate local actors. Also sends heartbeats and health reports -/// to central via the registered ClusterClient. +/// Site-side actor that receives messages from central over the site-hosted +/// SiteCommandService gRPC plane and routes them to the appropriate local actors. +/// Also sends heartbeats and health reports to central over its +/// ( in production). /// /// Routes all 8 message patterns to local handlers. /// @@ -157,15 +158,15 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers // the SITE-SPECIFIC role, because that is what site singletons (the Deployment // Manager) are placed on. Either node may receive this (the actor is per-node, not a // singleton, and contact rotation picks whichever answers); the target is resolved - // from cluster state, not from who received the message. Over ClusterClient the ack - // Tell merely enqueues, so the dispatcher resolves-and-leaves in one step (the gRPC - // transport defers the leave to keep ack-before-Leave — see PrepareFailover). + // from cluster state, not from who received the message. The gRPC transport defers the + // leave to keep ack-before-Leave — see PrepareFailover. (Under the removed ClusterClient + // path the ack Tell merely enqueued, so the dispatcher resolved-and-left in one step.) Receive(msg => Sender.Tell(_dispatcher.HandleFailover(msg))); - // The seven site→central sends now delegate to the injected transport (ClusterClient by - // default, gRPC when configured). Each handler captures the current Sender as the reply - // target so central's reply routes straight back to the waiting Ask, not through this - // actor — the exact sender-forwarding the ClusterClient path relied on. The per-message + // The seven site→central sends delegate to the injected transport (GrpcCentralTransport in + // production). Each handler captures the current Sender as the reply target so central's + // reply routes straight back to the waiting Ask, not through this actor — preserving the + // sender-forwarding the original ClusterClient path relied on. The per-message // "no transport / not-accepted" fallbacks live inside the transport now. // Notification Outbox: forward a buffered notification (S&F forwarder's Ask → ack back). @@ -229,9 +230,9 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers /// Executes a resolved command route within the actor: Forward to the target (preserving the /// central Ask sender so the reply routes straight back to the waiting Ask), or — when a /// null-guarded handler is unregistered — Tell the caller the dispatcher's synthetic reply. - /// The fire-and-forget disposition (UnsubscribeDebugView) is a plain Forward here, exactly as - /// before: over ClusterClient the site never acked it, so the synthetic ack is a gRPC-only - /// concern. + /// The fire-and-forget disposition (UnsubscribeDebugView) is a plain Forward here: the site + /// never acked it under the original ClusterClient path either, so the synthetic ack is a + /// gRPC-only concern. /// /// The migrated central→site command to route. private void DispatchCommand(object command) diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/CentralControlGrpc/CentralControlGrpc.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/CentralControlGrpc/CentralControlGrpc.cs index e0275fc7..e99f14c9 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/CentralControlGrpc/CentralControlGrpc.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/CentralControlGrpc/CentralControlGrpc.cs @@ -13,7 +13,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc { /// /// Direction: SITE is the client, CENTRAL is the server — the inverse of /// SiteStreamService, where central dials the site. That asymmetry is deliberate - /// and mirrors the direction the Akka ClusterClient traffic flows today: these + /// and mirrors the direction the Akka ClusterClient traffic used to flow: these /// seven calls are exactly the seven messages SiteCommunicationActor sends to /// /user/central-communication. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs index 158a3b35..c2465d52 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs @@ -139,7 +139,7 @@ public class CommunicationService /// Queries a site for the currently-applied deployment /// identity of a single instance. Used by the Deployment Manager before a /// re-deploy to reconcile against the site's actual state. Sent over the - /// existing ClusterClient command/control transport; the Ask times out (no + /// gRPC command/control transport; the Ask times out (no /// central buffering) if the site is unreachable, and the caller falls /// through to a normal deploy. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs index 53c3568a..2393cff1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs @@ -16,8 +16,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// The seven pairs mirror, one for one, the seven messages /// SiteCommunicationActor forwards to /user/central-communication over -/// Akka ClusterClient today. Both transports carry the SAME message types -/// end-to-end — central's handlers are untouched by the migration — so this mapper is +/// the control plane. The in-process and wire representations carry the SAME message types +/// end-to-end — central's handlers were untouched by the migration — so this mapper is /// the only place the two representations meet, and a field that does not survive a /// round-trip here is a field the gRPC transport silently drops. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs index 5ffcba68..e0b821bf 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// Central-hosted gRPC face of the seven site→central control messages /// (Protos/central_control.proto). Decodes each request onto the SAME in-process -/// message type the Akka ClusterClient path already carries, Asks +/// message type the removed Akka ClusterClient path used to carry, Asks /// , and encodes the reply back. /// /// @@ -26,8 +26,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// /// Zero handler logic lives here. Every RPC lands on a receive -/// CentralCommunicationActor already implements for the ClusterClient path, so the two -/// transports cannot drift in behaviour: the actor is the single implementation, and this class +/// CentralCommunicationActor already implements, so transport and handler cannot drift +/// in behaviour: the actor is the single implementation, and this class /// is a codec plus an Ask. That is also why the service takes the actor through /// rather than resolving anything from DI — the actor is created by the /// host's Akka bootstrap, not by the container. @@ -36,8 +36,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// Fault semantics deliberately differ from 's ingest /// RPCs. That server answers a failed audit ingest with an EMPTY IngestAck; this one /// fails the call with a non-OK status. Both leave the site's rows Pending for the next -/// drain, so the outcome is the same — but this service replaces the ClusterClient path, whose -/// documented behaviour is to propagate the fault (CentralCommunicationActor's +/// drain, so the outcome is the same — but this service replaced the ClusterClient path, whose +/// documented behaviour was to propagate the fault (CentralCommunicationActor's /// HandleIngestAuditEvents pipes a Status.Failure back), and preserving that keeps /// a lost batch visible as a failure rather than as a successful call that acked nothing. /// @@ -94,8 +94,8 @@ public sealed class CentralControlGrpcService : CentralControlService.CentralCon /// actor exists and can receive, NOT that every downstream singleton proxy /// (notification-outbox, audit-log-ingest) has registered itself yet. Those /// register moments later in the same startup path, and the actor already answers a call - /// that beats them with the same "not available, retry" reply it gives on the ClusterClient - /// path — so gating readiness on them would add nothing but a longer window in which sites + /// that beats them with a "not available, retry" reply + /// — so gating readiness on them would add nothing but a longer window in which sites /// see . /// /// The central communication actor. diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcCentralTransport.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcCentralTransport.cs index 5695c7b2..5904c6e2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcCentralTransport.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcCentralTransport.cs @@ -11,8 +11,8 @@ using AkkaStatus = Akka.Actor.Status; namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// -/// The that carries the seven site→central sends over gRPC — the -/// migration target for the Akka ClusterClient path. Each method encodes the message with +/// The that carries the seven site→central sends over gRPC — +/// the replacement for the removed Akka ClusterClient path. Each method encodes the message with /// , dials CentralControlService through the sticky /// , and delivers the decoded reply (or a transient-failure /// signal) to the waiting Ask. diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs index 18bfef06..33322fe3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs @@ -81,7 +81,7 @@ public sealed class GrpcSiteTransport : ISiteCommandTransport } catch (SiteChannelUnavailableException) { - // Parity with the Akka "no ClusterClient for site" path: warn and drop, so the caller's + // Parity with the removed Akka "no ClusterClient for site" path: warn and drop, so the caller's // Ask times out. Central never buffers. _logger.LogWarning( "No gRPC channel for site {SiteId}; dropping {Message} (caller's Ask will time out)", diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandGrpcService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandGrpcService.cs index b0fc482d..1320ac7d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandGrpcService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandGrpcService.cs @@ -22,7 +22,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// /// Coexistence — server-side only. This makes the site ALSO listen on gRPC for commands; -/// nothing central flips to gRPC here (that is T1B.3). Central still dials sites via ClusterClient. +/// this was the server half, landed before central was flipped to dial it (T1B.3). /// /// /// Fire-and-forget. The Akka path never acks UnsubscribeDebugView, but a unary RPC diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs index ee94a6a2..f2cf359e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs @@ -9,7 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// Raised when a site has no usable gRPC channel — an unknown site, or a site with neither /// GrpcNodeAAddress nor GrpcNodeBAddress configured. The gRPC transport treats this -/// the way the Akka path treats "no ClusterClient for site": warn and drop, so the caller's Ask +/// the way the removed Akka path treated "no ClusterClient for site": warn and drop, so the caller's Ask /// times out (central never buffers). /// public sealed class SiteChannelUnavailableException(string siteId) diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/central_control.proto b/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/central_control.proto index 94f0c104..97d4daf1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/central_control.proto +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Protos/central_control.proto @@ -17,7 +17,7 @@ import "Protos/sitestream.proto"; // // Direction: SITE is the client, CENTRAL is the server — the inverse of // SiteStreamService, where central dials the site. That asymmetry is deliberate -// and mirrors the direction the Akka ClusterClient traffic flows today: these +// and mirrors the direction the Akka ClusterClient traffic used to flow: these // seven calls are exactly the seven messages SiteCommunicationActor sends to // /user/central-communication. // diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/ReconcileService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/ReconcileService.cs index e8bae4df..28e440a4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/ReconcileService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/ReconcileService.cs @@ -8,8 +8,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication; /// /// Central-side startup-reconciliation handler. A site node, on startup, reports its -/// local deployed inventory via (delivered over -/// ClusterClient to ); this service diffs +/// local deployed inventory via (delivered over the +/// control plane to ); this service diffs /// it against central's expected deployed set and replies with fresh fetch tokens for the /// gap — instances the node is missing or has at a stale revision — plus the orphan names /// (present locally but no longer deployed centrally, which the node only logs). diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/IHealthReportTransport.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/IHealthReportTransport.cs index 4c79a5e0..4112d2ac 100644 --- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/IHealthReportTransport.cs +++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/IHealthReportTransport.cs @@ -4,7 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring; /// /// Abstraction for sending health reports to central. -/// In production, implemented over Akka ClusterClient with an acked round-trip +/// In production, implemented over the gRPC central transport with an acked round-trip /// (review 01 [Medium]): the transport Asks and awaits a /// , so a lost/rejected report surfaces as a /// fault the sender can observe (and its per-interval counter-restore fires). diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs index d2d8bfa5..bc9fdc2e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs @@ -556,7 +556,7 @@ akka {{ _logger); // Hand the audit-ingest proxy to the CentralCommunicationActor so audit - // ingest commands forwarded by sites over ClusterClient are routed to the + // ingest commands forwarded by sites are routed to the // singleton. Mirrors the RegisterNotificationOutbox wiring above. centralCommActor.Tell(new RegisterAuditIngest(auditIngest.Proxy)); @@ -628,7 +628,7 @@ akka {{ // Hand the CentralCommunicationActor to the SiteCallAudit // actor so it can relay operator Retry/Discard on parked cached calls to - // the owning site (over the per-site ClusterClient via SiteEnvelope). + // the owning site (over the per-site gRPC transport via SiteEnvelope). // Mirrors the RegisterAuditIngest / RegisterNotificationOutbox wiring; // the message is sent to the singleton proxy so it reaches whichever // central node currently hosts the singleton. @@ -896,7 +896,7 @@ akka {{ // Event log handler — cluster singleton so queries always reach the // active node. The event log is node-local SQLite and is not // replicated; only the active node records events. A per-node handler - // would let a ClusterClient query land on the standby and find nothing. + // would let a central query land on the standby and find nothing. var eventLogQueryService = _serviceProvider.GetService(); if (eventLogQueryService != null) { @@ -1048,23 +1048,23 @@ akka {{ // collaborators through its constructor, so we resolve them from DI // and pass them in via Props.Create rather than relying on a future // FactoryProvider. The real site→central client is constructed and - // wired immediately below: a ClusterClientSiteAuditClient (ClusterClient - // transport, not gRPC) replaces the DI-default NoOpSiteStreamAuditClient + // wired immediately below: a SiteCommunicationAuditClient (routing through + // the SiteCommunicationActor's central transport) replaces the DI-default NoOpSiteStreamAuditClient // for site roles, without disturbing the rest of this wiring. var siteAuditOptions = _serviceProvider .GetRequiredService>(); var siteAuditQueue = _serviceProvider .GetRequiredService(); // Audit Log follow-up: the production site→central audit - // push uses the ClusterClient transport via the SiteCommunicationActor, + // push routes through the SiteCommunicationActor's central transport, // not the DI-resolved NoOpSiteStreamAuditClient. The NoOp default stays // correct for central/test composition roots (no SiteCommunicationActor); - // a site role wires the real ClusterClient-based client here so the + // a site role wires the real client here so the // SQLite Pending backlog actually drains to central. The forward Ask // reuses NotificationForwardTimeout — the same site→central command // forward bound notifications already use over this transport. ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry.ISiteStreamAuditClient siteAuditClient = - new ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry.ClusterClientSiteAuditClient( + new ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry.SiteCommunicationAuditClient( siteCommActor, _communicationOptions.NotificationForwardTimeout); var siteAuditLogger = _serviceProvider.GetRequiredService() @@ -1100,10 +1100,10 @@ akka {{ // SetReady asserts a deliberately narrow contract. By this point the // actor system exists, SiteStreamManager.Initialize has run, and every // role actor (SiteCommunicationActor, deployment-manager singleton, - // the ClusterClient) has been created with ActorOf — + // the central transport) has been created with ActorOf — // creation and the registration Tells are synchronous and strictly ordered. // What is NOT guaranteed is completion of each actor's PreStart or the - // ClusterClient's initial-contact handshake with central: those are + // the central transport's initial connection to central: those are // intentionally asynchronous. Gating readiness on the central handshake would // be wrong — a site must come up and stream locally even while central is // briefly unreachable. gRPC readiness therefore guarantees "the site actor diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs index e6752c06..d00e1274 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs @@ -160,7 +160,7 @@ try // which would let DI hand the instance back and bypass Grpc.AspNetCore's own activation // path (the shape that once hid a two-public-constructor defect until the rig caught it). // CentralControlGrpcService decodes each request onto the same in-process message the - // ClusterClient path carries and Asks CentralCommunicationActor — zero handler logic here. + // removed ClusterClient path used to carry, and Asks CentralCommunicationActor — zero handler logic here. builder.Services.AddGrpc(options => { options.Interceptors.Add(); @@ -638,7 +638,7 @@ try builder.Services.AddSingleton(); // Site command plane (central→site) — the gRPC peer of SiteStreamGrpcServer. Both share // the h2c listener and the ControlPlaneAuthInterceptor PSK gate; both are readiness-gated - // (SetReady, below). Central still dials over ClusterClient until T1B.3. + // (SetReady, below). builder.Services.AddSingleton(); // Existing site service registrations (this is also where LocalDb and its diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs index 63872f72..f4deebb6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs @@ -388,7 +388,7 @@ public class RouteTarget /// /// N3: awaits a routed Ask and TYPES unreachability. AskTimeoutException means - /// the site never answered (no ClusterClient contact, site down, partition) — + /// the site never answered (no route to the site, site down, partition) — /// the communication layer's own typed signal (CentralCommunicationActor drops /// envelopes for contactless sites so the Ask expires caller-side) — and /// surfaces as SiteUnreachableException so the executor maps it to the spec's diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs index 1fe68472..e16d9b0f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs @@ -38,7 +38,7 @@ using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services; namespace ZB.MOM.WW.ScadaBridge.ManagementService; /// -/// Central actor that handles all management commands from the CLI (via ClusterClient). +/// Central actor that handles all management commands from the CLI (via the HTTP /management endpoint). /// Receives ManagementEnvelope messages, authorizes based on roles, then delegates to /// the appropriate service or repository using scoped DI. /// @@ -477,7 +477,7 @@ public class ManagementActor : ReceiveActor // NOTE: ResolveRolesCommand is intentionally NOT dispatched. The two-step // "ResolveRoles + command" flow is retired — the HTTP endpoint performs LDAP // auth and role resolution itself before sending a single envelope. Leaving a - // handler would expose role-mapping data to any raw ClusterClient sender with + // handler would expose role-mapping data to any raw in-cluster sender with // no role requirement; the command now falls through to the default below. _ => throw new NotSupportedException($"Unknown command type: {command.GetType().Name}") }; diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs index 05d0e421..398c712f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs @@ -164,7 +164,7 @@ public class SiteCallAuditActor : ReceiveActor /// /// The central→site command transport — the /// CentralCommunicationActor, which owns the per-site - /// ClusterClient map and routes a to the + /// transport map and routes a to the /// owning site. Set via by the /// Host after both actors exist (this actor is a cluster singleton; the /// transport actor is created separately). Null until registration @@ -1079,7 +1079,7 @@ public class SiteCallAuditActor : ReceiveActor /// SiteCalls mirror row. It wraps a /// in a addressed to SourceSite, Asks the /// CentralCommunicationActor (which routes it over the per-site - /// ClusterClient), and maps the site's + /// gRPC transport), and maps the site's /// — or an Ask timeout — onto a /// . A timeout / no-route is reported as /// the distinct outcome, diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditOptions.cs b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditOptions.cs index dd78d3ac..bbeb479c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditOptions.cs @@ -30,7 +30,7 @@ public class SiteCallAuditOptions /// Ask timeout for the central→site Retry/Discard relay. When /// the owning site does not ack a RetryParkedOperation / /// DiscardParkedOperation within this window — site offline, no - /// ClusterClient route, or central buffering deliberately absent — the relay + /// route to the site, or central buffering deliberately absent — the relay /// reports a SiteUnreachable outcome. Default 10 seconds: long enough /// to absorb a healthy cross-cluster round-trip, short enough that an /// operator clicking Retry on an offline site gets a fast, honest answer. diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReconciliationActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReconciliationActor.cs index 9051bc4b..557569b8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReconciliationActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReconciliationActor.cs @@ -27,8 +27,8 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; /// crashes on these failures. /// /// -/// The pass runs after a small startup delay (so the central ClusterClient has time to -/// register) and is driven entirely off the actor thread: the Ask + fetch + write happen in +/// The pass runs after a small startup delay (so the site's central transport has time to +/// connect) and is driven entirely off the actor thread: the Ask + fetch + write happen in /// an awaited continuation whose summary is captured in an internal message /// piped back to Self. The actor thread never blocks. /// @@ -60,14 +60,14 @@ public sealed class SiteReconciliationActor : ReceiveActor, IWithTimers /// Fetches a deployment's flattened config JSON from central over HTTP. /// /// The site's SiteCommunicationActor; it forwards the - /// over the registered central ClusterClient and routes + /// over the site's central transport and routes /// the back to this actor's Ask. /// /// This node's site identifier (resolved by central). /// This node's semantic id (e.g. node-a/node-b), for logging/diagnostics. /// Logger. /// - /// Delay before the single startup pass, giving the central ClusterClient time to register. + /// Delay before the single startup pass, giving the site's central transport time to connect. /// Defaults to 5 seconds. /// /// Round-trip timeout for the reconcile Ask to central. Defaults to 30 seconds. @@ -105,7 +105,7 @@ public sealed class SiteReconciliationActor : ReceiveActor, IWithTimers protected override void PreStart() { base.PreStart(); - // One-shot pass after a small delay so the central ClusterClient can register first. + // One-shot pass after a small delay so the site's central transport can connect first. // Non-blocking: the timer fires RunReconcile back onto this actor's mailbox. Timers.StartSingleTimer(StartupTimerKey, RunReconcile.Instance, _initialDelay); _logger.LogInformation( diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs index 082ac684..115abf75 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs @@ -74,7 +74,7 @@ public class ScriptRuntimeContext /// /// Notification Outbox: the site communication actor that Notify.Status - /// queries central through (via the ClusterClient command/control transport). + /// queries central through (via the gRPC command/control transport). /// private readonly ICanTell? _siteCommunicationActor; @@ -175,7 +175,7 @@ public class ScriptRuntimeContext /// Optional client for external system API calls. /// Optional gateway for database connection and cached write access. /// Optional store-and-forward service for notification delivery. - /// Optional actor for site-to-central communication (ClusterClient). + /// Optional actor for site-to-central communication. /// Identifier of the site where this instance is running. /// Optional name of the source script for audit trail identification. /// Optional writer for audit log entries. diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/NotificationForwarder.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/NotificationForwarder.cs index da56cdbe..b3ccbebc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/NotificationForwarder.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/NotificationForwarder.cs @@ -26,10 +26,10 @@ namespace ZB.MOM.WW.ScadaBridge.StoreAndForward; /// for operator forensics. /// /// -/// The forward travels over the ClusterClient command/control transport: the handler +/// The forward travels over the gRPC command/control transport: the handler /// Asks /// the site communication actor, which wraps the message in a -/// ClusterClient.Send("/user/central-communication", …) and routes central's +/// SubmitNotification call on CentralControlService and routes central's /// reply straight back to this Ask. /// public sealed class NotificationForwarder @@ -45,7 +45,7 @@ public sealed class NotificationForwarder /// /// /// The site communication actor. It forwards a to - /// central via the registered ClusterClient and replies with the + /// central over the site's central transport and replies with the /// . /// /// This site's identifier, stamped on every submit. diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/Telemetry/ClusterClientSiteAuditClientTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/Telemetry/SiteCommunicationAuditClientTests.cs similarity index 92% rename from tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/Telemetry/ClusterClientSiteAuditClientTests.cs rename to tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/Telemetry/SiteCommunicationAuditClientTests.cs index 404f5a10..fbb4b062 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/Telemetry/ClusterClientSiteAuditClientTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/Telemetry/SiteCommunicationAuditClientTests.cs @@ -11,13 +11,13 @@ using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site.Telemetry; /// -/// Tests for — the production +/// Tests for — the production /// binding wired by the Host for site /// roles. The client maps the proto-DTO batches produced by /// into the Akka /// / -/// messages, Asks the site's SiteCommunicationActor (which forwards over -/// ClusterClient to central), and maps the reply back into an +/// messages, Asks the site's SiteCommunicationActor (which forwards to +/// central over its central transport), and maps the reply back into an /// . /// /// @@ -25,7 +25,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site.Telemetry; /// it lets the tests assert the exact command shape AND drive the reply (or /// withhold one to exercise the Ask-timeout path). /// -public class ClusterClientSiteAuditClientTests : TestKit +public class SiteCommunicationAuditClientTests : TestKit { /// Short Ask timeout so the timeout test completes quickly. private static readonly TimeSpan AskTimeout = TimeSpan.FromMilliseconds(500); @@ -65,7 +65,7 @@ public class ClusterClientSiteAuditClientTests : TestKit public async Task IngestAuditEventsAsync_FullAck_MapsAllAcceptedIdsOntoAck() { var probe = CreateTestProbe(); - var sut = new ClusterClientSiteAuditClient(probe.Ref, AskTimeout); + var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout); var events = Enumerable.Range(0, 3).Select(_ => NewEvent()).ToList(); var batch = BatchOf(events); @@ -92,7 +92,7 @@ public class ClusterClientSiteAuditClientTests : TestKit public async Task IngestAuditEventsAsync_PartialAck_OnlyAcceptedIdsAppearOnAck() { var probe = CreateTestProbe(); - var sut = new ClusterClientSiteAuditClient(probe.Ref, AskTimeout); + var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout); var events = Enumerable.Range(0, 5).Select(_ => NewEvent()).ToList(); var accepted = events.Take(3).Select(e => e.EventId).ToList(); @@ -114,7 +114,7 @@ public class ClusterClientSiteAuditClientTests : TestKit public async Task IngestAuditEventsAsync_AskTimeout_Throws_SoDrainLoopKeepsRowsPending() { var probe = CreateTestProbe(); - var sut = new ClusterClientSiteAuditClient(probe.Ref, AskTimeout); + var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout); var batch = BatchOf(new[] { NewEvent() }); @@ -131,7 +131,7 @@ public class ClusterClientSiteAuditClientTests : TestKit public async Task IngestAuditEventsAsync_FaultedReply_Throws() { var probe = CreateTestProbe(); - var sut = new ClusterClientSiteAuditClient(probe.Ref, AskTimeout); + var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout); var task = sut.IngestAuditEventsAsync(BatchOf(new[] { NewEvent() }), CancellationToken.None); probe.ExpectMsg(TimeSpan.FromSeconds(3)); @@ -147,7 +147,7 @@ public class ClusterClientSiteAuditClientTests : TestKit public async Task IngestCachedTelemetryAsync_RoutesCommand_AndMapsReply() { var probe = CreateTestProbe(); - var sut = new ClusterClientSiteAuditClient(probe.Ref, AskTimeout); + var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout); var events = Enumerable.Range(0, 2).Select(_ => NewEvent()).ToList(); var batch = new CachedTelemetryBatch(); @@ -182,7 +182,7 @@ public class ClusterClientSiteAuditClientTests : TestKit public async Task IngestCachedTelemetryAsync_AskTimeout_Throws() { var probe = CreateTestProbe(); - var sut = new ClusterClientSiteAuditClient(probe.Ref, AskTimeout); + var sut = new SiteCommunicationAuditClient(probe.Ref, AskTimeout); var batch = new CachedTelemetryBatch(); batch.Packets.Add(new CachedTelemetryPacket diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs index 31b36b64..a27674f4 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTests.cs @@ -28,7 +28,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment; /// Tolerant terminal-state assertion (validation-behavior protocol): whether /// Connect reaches a Live badge depends on the owning site returning a /// snapshot for the instance. Reading DebugView.razor::Connect: it awaits -/// DebugStreamService.StartStreamAsync (a ClusterClient snapshot round-trip); +/// DebugStreamService.StartStreamAsync (a site snapshot round-trip); /// on success it flips _connected = true (Live badge + success toast), and on /// any exception it surfaces a Connect failed: … error .toast. Either /// way _connecting resets, so the click always resolves — it never hangs. The diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs index 0fd056f8..189ca20d 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DebugViewTreeTests.cs @@ -19,13 +19,13 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment; /// and would never appear, so the test mints an instance via the shared /// and drives it to Enabled over the CLI /// (instance deploy) before selecting it. The fixture provisions onto the -/// real, running site-a so the ClusterClient snapshot round-trip resolves -/// fast (an unknown site has no registered ClusterClient and only times out). +/// real, running site-a so the snapshot round-trip resolves +/// fast (an unknown site has no reachable gRPC endpoint and only times out). /// /// /// /// Tolerant terminal-state wait, then the tabs/trees assertions — Connect -/// awaits a ClusterClient snapshot round-trip; on success the page flips to the +/// awaits a site snapshot round-trip; on success the page flips to the /// Live badge and renders the tabbed card, on failure it surfaces a /// Connect failed: … .toast (see DebugView.razor::Connect). /// The test first waits on a TERMINAL state (Live badge OR error toast) within a diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentFixture.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentFixture.cs index dfac4df3..17e76027 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentFixture.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/DeploymentFixture.cs @@ -11,8 +11,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment; /// /// /// Why site-a and not a throwaway site: the deploy / enable / disable -/// actions relay to the owning site over the Akka ClusterClient. An unknown -/// site identifier has no registered ClusterClient, so the relay only +/// actions relay to the owning site over the gRPC site command plane. An unknown +/// site identifier has no reachable gRPC endpoint, so the relay only /// resolves on a slow 10-second timeout (and never surfaces a fast failure toast). /// To exercise the real action path, the ephemeral instances must therefore live /// on a real, running site — site-a — rather than a fixture-created diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCalls/SiteCallsPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCalls/SiteCallsPageTests.cs index 90cf0fae..7ce46b15 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCalls/SiteCallsPageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/SiteCalls/SiteCallsPageTests.cs @@ -282,8 +282,8 @@ public class SiteCallsPageTests // be relayed to the owning site. Unlike the display-only tests above, // this one actually relays to the owning site, so the SourceSite must // be a *real* site identifier from the running cluster (site-a) and - // not the cosmetic "plant-a" label: an unknown site has no registered - // ClusterClient, so CentralCommunicationActor drops the envelope + // not the cosmetic "plant-a" label: an unknown site has no reachable + // gRPC endpoint, so CentralCommunicationActor drops the envelope // without replying and the relay only resolves on the 10s inner Ask // timeout — too slow for the toast assertion below. Relayed to a live // site, the site finds no parked S&F message for this freshly-seeded @@ -319,7 +319,7 @@ public class SiteCallsPageTests // We only assert that an outcome toast appears (exactly one — the // single-toast contract), not which one, since the live cluster // state determines the outcome. The wait is generous (15s): the - // relay round-trips to the site over ClusterClient, and a worst-case + // relay round-trips to the site over gRPC, and a worst-case // path can sit on the 10s inner relay timeout before the response — // and the toast itself auto-dismisses 5s after it appears, so the // assertion must catch it inside that window. diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ClusterClientContractLockTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ClusterClientContractLockTests.cs index a24b27dc..c8ab1f6e 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ClusterClientContractLockTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ClusterClientContractLockTests.cs @@ -8,10 +8,10 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages; /// -/// Contract-lock tests for the highest-traffic ClusterClient record contracts. -/// ClusterClient traffic uses Akka.NET's default Newtonsoft-JSON serializer and the -/// design supports system-wide artifact version skew across sites, so these records -/// must obey additive-only evolution: +/// Contract-lock tests for the highest-traffic cross-boundary record contracts. +/// These predate the ClusterClient→gRPC migration, when this traffic rode Akka.NET's +/// default Newtonsoft-JSON serializer; the design still supports system-wide artifact +/// version skew across sites, so these records must still obey additive-only evolution: /// (a) an old-shape payload (missing newer trailing optional fields) still /// deserializes, with the missing fields taking their constructor defaults; /// (b) an unknown field from a *newer* peer is ignored (never throws); diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs index e4a12c89..ac9fc060 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs @@ -94,7 +94,7 @@ public class ManagementCommandRegistryTests /// /// M9-T32c: the schema-library CRUD commands must auto-register by reflection - /// (no manual registry entry) so the HTTP / ClusterClient boundary can route them. + /// (no manual registry entry) so the HTTP management boundary can route them. /// [Fact] public void Resolve_SchemaLibraryCommands_AllRegistered() diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Actors/SiteCommunicationActorTransportTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Actors/SiteCommunicationActorTransportTests.cs index 0432a702..d414f325 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Actors/SiteCommunicationActorTransportTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Actors/SiteCommunicationActorTransportTests.cs @@ -104,7 +104,7 @@ public class SiteCommunicationActorTransportTests : TestKit // The seam preserves the reply-routing the S&F layer depends on: when the transport // answers the captured replyTo with a Status.Failure (its transient-failure signal on a // non-OK status), that failure reaches the original sender — here the test actor — so the - // waiting Ask faults, exactly as it did on the ClusterClient path. + // waiting Ask faults, exactly as it did on the removed ClusterClient path. var transport = Substitute.For(); transport .When(t => t.SubmitNotification(Arg.Any(), Arg.Any())) diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorAuditTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorAuditTests.cs index 0d4c7f26..8cc6ab5f 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorAuditTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorAuditTests.cs @@ -15,10 +15,10 @@ using ZB.MOM.WW.ScadaBridge.Communication.Actors; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests; /// -/// Tests for the Audit Log (#23) site→central ClusterClient ingest routing on -/// . A site ClusterClient delivers +/// Tests for the Audit Log (#23) site→central ingest routing on +/// . A site delivers /// / -/// to the receptionist-registered actor, which forwards to the registered +/// to the actor, which forwards to the registered /// AuditLogIngestActor proxy and routes the reply back to the site. /// Mirrors the NotificationSubmit / RegisterNotificationOutbox pattern. /// diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorReconcileTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorReconcileTests.cs index 91e12370..423fbed1 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorReconcileTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CentralCommunicationActorReconcileTests.cs @@ -17,7 +17,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Tests; /// Tests that routes a site→central /// through the scoped /// and pipes the resulting back to the original -/// sender (the site's ClusterClient path). Mirrors the audit-ingest routing tests. +/// sender (the site's transport path). Mirrors the audit-ingest routing tests. /// public class CentralCommunicationActorReconcileTests : TestKit { diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/DebugStreamBridgeActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/DebugStreamBridgeActorTests.cs index 6068f628..7f79e3b7 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/DebugStreamBridgeActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/DebugStreamBridgeActorTests.cs @@ -293,7 +293,7 @@ public class DebugStreamBridgeActorTests : TestKit ctx.BridgeActor.Tell(new StopDebugStream()); - // Should send ClusterClient unsubscribe + // Should send the site-addressed unsubscribe var envelope = ctx.CommProbe.ExpectMsg(); Assert.IsType(envelope.Message); diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/SiteCommunicationActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/SiteCommunicationActorTests.cs index 28acc06b..76b71d38 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/SiteCommunicationActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/SiteCommunicationActorTests.cs @@ -60,7 +60,7 @@ public class SiteCommunicationActorTests : TestKit public void DeploymentStateQuery_ForwardedToDeploymentManager() { // DeploymentManager-006: the site-before-redeploy query travels over the - // ClusterClient command/control transport and is routed to the local + // gRPC command/control transport and is routed to the local // Deployment Manager, which owns the deployed-config store. var dmProbe = CreateTestProbe(); var siteActor = Sys.ActorOf(Props.Create(() => diff --git a/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/AuditLog/SiteAuditPushFlowTests.cs b/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/AuditLog/SiteAuditPushFlowTests.cs index 9f36f2ac..f77ec70f 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/AuditLog/SiteAuditPushFlowTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/AuditLog/SiteAuditPushFlowTests.cs @@ -24,14 +24,14 @@ namespace ZB.MOM.WW.ScadaBridge.IntegrationTests.AuditLog; /// /// End-to-end integration test for the Audit Log (#23) site→central push path -/// introduced by the "real ClusterClient-based site audit push client" follow-up. +/// introduced by the "real site audit push client" follow-up. /// /// /// /// Exercises the full production chain in one actor system: the real /// site SQLite hot-path, the real /// drain loop, the real -/// , the real +/// , the real /// forward, the real /// routing, and the real /// AuditLogIngestActor ingest — only the cross-cluster gRPC transport itself is @@ -180,7 +180,7 @@ public class SiteAuditPushFlowTests : TestKit new BridgeCentralTransport(centralCommActor)))); // The production site audit push client — the unit under integration. - var auditClient = new ClusterClientSiteAuditClient( + var auditClient = new SiteCommunicationAuditClient( siteCommActor, TimeSpan.FromSeconds(5)); // Real SiteAuditTelemetryActor drains the writer's Pending queue and diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/DispatchCoverageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/DispatchCoverageTests.cs index 859031b6..184cf116 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/DispatchCoverageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/DispatchCoverageTests.cs @@ -47,7 +47,7 @@ public class DispatchCoverageTests : TestKit, IDisposable { // The two-step "ResolveRoles + command" flow is retired — the HTTP endpoint // resolves roles itself and sends a single envelope. Leaving a handler would - // expose role-mapping data to a raw ClusterClient sender with no role gate. + // expose role-mapping data to a raw in-cluster sender with no role gate. // See ManagementActor.DispatchCommand (the comment above the default arm, ~line 472). typeof(ResolveRolesCommand), diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs index 669d8059..41f298b0 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs @@ -1089,7 +1089,7 @@ public class ManagementActorTests : TestKit, IDisposable // // The two-step ResolveRoles + command flow is retired: the HTTP endpoint does // LDAP auth and role resolution itself. The actor must no longer dispatch - // ResolveRolesCommand — a stray ClusterClient sender hitting it gets a uniform + // ResolveRolesCommand — a stray in-cluster sender hitting it gets a uniform // ManagementError rather than an unauthenticated role-mapping enumeration. // ======================================================================== diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteCallAudit.Tests/SiteCallRelayTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteCallAudit.Tests/SiteCallRelayTests.cs index 3ccfd3dc..ddea1ce7 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteCallAudit.Tests/SiteCallRelayTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteCallAudit.Tests/SiteCallRelayTests.cs @@ -164,7 +164,7 @@ public class SiteCallRelayTests : TestKit public void RetrySiteCall_SiteNeverReplies_ReportsSiteUnreachable() { // A central comm probe that silently drops the relay — models an offline - // site / no ClusterClient route: the Ask times out. + // site / no route to the site: the Ask times out. var central = CreateTestProbe(); var actor = CreateActor(central.Ref);