docs(historian-gateway): document gateway backend, config keys, EnsureTags hook, known gates; retire Wonderware from docs
v2-ci / build (pull_request) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped

HistorianGateway is now the sole historian backend (read + alarm SendEvent +
continuous WriteLiveValues). Document the final state and retire the Wonderware
sidecar from the docs/config/labels:

- CLAUDE.md: rewrite the Historian section — ServerHistorian /
  ContinuousHistorization / AlarmHistorian config keys, the IHistorianProvisioning
  EnsureTags hook, the GatewayAlarmHistorianWriter SendEvent path + ReadEvents
  dependency on gateway RuntimeDb:EventReadsEnabled=true, gateway-side
  prerequisites (RuntimeDb flags + historian:read/write/tags:write scopes),
  migration note, and two KNOWN-LIMITATION callouts (live-validation gate +
  empty historized-ref-set recorder follow-on).
- appsettings.json: fix the stale ServerHistorian block (Host/Port/SharedSecret/
  ServerCertThumbprint -> Endpoint/ApiKey/UseTls/AllowUntrustedServerCertificate/
  CaCertificatePath/CallTimeout, keep MaxTieClusterOverfetch); add a disabled
  ContinuousHistorization block; prune the orphaned Wonderware keys from
  AlarmHistorian (keep the SQLite knobs). ApiKey env-supplied via
  ServerHistorian__ApiKey (commented; valid strict JSON via _comment keys).
- README.md + docs (Historian.md, AlarmHistorian.md, Configuration.md,
  ServiceHosting.md, DriverLifecycle.md, drivers/README.md, Uns.md, VirtualTags.md,
  AlarmTracking.md, Client.UI.md, README.md, TestConnectProbes.md): retire the
  Wonderware historian backend from current-backend descriptions; fix the stale
  ServerHistorian/AlarmHistorian config tables (now gateway shape); convert
  drivers/Historian.Wonderware.md to a retired stub pointing at the gateway.
- Source/UI labels (descriptive text only, no behavior change):
  OtOpcUaServerHostedService.cs, HistoryPaging.cs, OtOpcUaSdkServer.cs,
  HistorianAdapterActor.cs, VirtualTagModal.razor, ScriptedAlarmModal.razor,
  AlarmsHistorian.razor now name the HistorianGateway backend.

Build clean (0 errors); AdminUI.Tests green (514 passed).

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
Joseph Doherty
2026-06-26 19:46:27 -04:00
parent 0b4b2e4cfd
commit 2124f21ab6
23 changed files with 364 additions and 320 deletions
@@ -18,8 +18,8 @@
<section class="panel notice rise" style="animation-delay:.02s">
Snapshot from the local node's <span class="mono">HistorianAdapterActor</span>. Default sink
is a no-op (<span class="mono">NullAlarmHistorianSink</span>); production wires
<span class="mono">SqliteStoreAndForwardSink</span> with the Wonderware historian sidecar
behind it. Polling every @PollSeconds s.
<span class="mono">SqliteStoreAndForwardSink</span> draining to the HistorianGateway
(<span class="mono">SendEvent</span>) behind it. Polling every @PollSeconds s.
</section>
@if (_status is null)
@@ -74,7 +74,7 @@
<label class="form-label">HistorizeToAveva</label>
<div class="form-check form-switch">
<InputCheckbox @bind-Value="_form.HistorizeToAveva" class="form-check-input" />
<label class="form-check-label">Route to Wonderware sidecar</label>
<label class="form-check-label">Route to HistorianGateway</label>
</div>
</div>
<div class="col-md-4 mb-3">
@@ -136,7 +136,7 @@
<label class="form-label">Historize</label>
<div class="form-check form-switch">
<InputCheckbox @bind-Value="_form.Historize" class="form-check-input" />
<label class="form-check-label">Send to Wonderware historian</label>
<label class="form-check-label">Send to HistorianGateway</label>
</div>
</div>
</div>
@@ -53,7 +53,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
/// <param name="historianDataSource">The server-side HistoryRead backend resolved from DI — the
/// <c>NullHistorianDataSource</c> default seeded by <c>AddOtOpcUaRuntime</c> (which runs on this driver
/// node, the same source the address-space sink + node-write gateway come from), or the configured
/// Wonderware read client when <c>AddServerHistorian</c> enabled it. Wired onto the node manager in
/// HistorianGateway read client when <c>AddServerHistorian</c> enabled it. Wired onto the node manager in
/// <see cref="StartAsync"/>.</param>
/// <param name="configuration">App configuration; the <c>ServerHistorian</c> section is bound here to
/// read <see cref="ServerHistorianOptions.MaxTieClusterOverfetch"/> for the node manager. Bound directly
@@ -211,7 +211,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
logger: _loggerFactory.CreateLogger<ActorNodeWriteGateway>()));
// Wire the server-side read backend resolved from DI — the NullHistorianDataSource default (when
// the ServerHistorian section is disabled) or the configured Wonderware read client (when enabled).
// the ServerHistorian section is disabled) or the configured HistorianGateway read client (when enabled).
// The node manager's HistoryRead overrides block-bridge to whatever source is set here.
_server.SetHistorianDataSource(_historianDataSource);
@@ -11,24 +11,37 @@
"DisableLogin": false
}
},
"ServerHistorian": {
"_comment": "Server-side HistoryRead backend (the ZB.MOM.WW.HistorianGateway gRPC client). Disabled => NullHistorianDataSource (historized nodes return GoodNoData). The gateway must run RuntimeDb:EventReadsEnabled=true for alarm-history ReadEvents, and the API key must carry historian:read + historian:write + historian:tags:write scopes.",
"Enabled": false,
"Endpoint": "",
"ApiKey": "",
"_ApiKeyComment": "NEVER commit a real key. Supply via the environment variable ServerHistorian__ApiKey.",
"UseTls": true,
"AllowUntrustedServerCertificate": false,
"CaCertificatePath": null,
"CallTimeout": "00:00:30",
"MaxTieClusterOverfetch": 65536
},
"ContinuousHistorization": {
"_comment": "Continuous historization of driver (non-Galaxy) tag values: a crash-safe FasterLog outbox + recorder draining to the ServerHistorian gateway's WriteLiveValues. Disabled => no recorder is spawned. Requires ServerHistorian to be configured; the gateway connection (endpoint/key/TLS) is sourced from the ServerHistorian section, not here.",
"Enabled": false,
"OutboxPath": "",
"_OutboxPathComment": "Directory holding the FasterLog segment + commit files. Required when Enabled=true. In production set an ABSOLUTE path on durable storage.",
"CommitMode": "PerEntry",
"CommitIntervalMs": 100,
"DrainBatchSize": 64,
"DrainIntervalSeconds": 2,
"Capacity": 0,
"MinBackoffSeconds": 1,
"MaxBackoffSeconds": 30
},
"AlarmHistorian": {
"_comment": "Durable SQLite store-and-forward alarm sink. Drains alarm events to the ServerHistorian gateway's SendEvent path; the downstream connection (endpoint/key/TLS) is sourced from the ServerHistorian section.",
"Enabled": false,
"DatabasePath": "alarm-historian.db",
"Host": "localhost",
"Port": 32569,
"UseTls": false,
"ServerCertThumbprint": null,
"SharedSecret": "",
"DrainIntervalSeconds": 5,
"Capacity": 1000000,
"DeadLetterRetentionDays": 30
},
"ServerHistorian": {
"Enabled": false,
"Host": "localhost",
"Port": 32569,
"UseTls": false,
"ServerCertThumbprint": null,
"SharedSecret": ""
}
}
@@ -40,7 +40,7 @@ internal sealed record HistoryContinuationState(
/// <summary>
/// Pure server-side continuation-point paging decisions for the count-capped variable-history arms
/// (Raw / Processed). The backend (Wonderware sidecar) does NOT page — it returns up to
/// (Raw / Processed). The backend (the HistorianGateway read client) does NOT page — it returns up to
/// <c>NumValuesPerNode</c> samples with a null continuation point — so paging is synthesised here,
/// time-based:
/// <list type="bullet">
@@ -85,7 +85,7 @@ public sealed class OtOpcUaSdkServer : StandardServer
/// Wire the server-side HistoryRead backend (the <see cref="IHistorianDataSource"/> the node
/// manager's HistoryRead overrides block-bridge to) onto the created
/// <see cref="OtOpcUaNodeManager"/>. The host calls this after start with the DI-resolved source —
/// the <c>NullHistorianDataSource</c> default (GoodNoData-empty reads) or the configured Wonderware
/// the <c>NullHistorianDataSource</c> default (GoodNoData-empty reads) or the configured HistorianGateway
/// read client. Passing <c>null</c> restores the Null default (the property setter null-coalesces),
/// i.e. "no historian". No-op (returns <c>false</c>) when the node manager has not been created yet,
/// so the caller can detect a too-early call (mirrors <see cref="SetNodeWriteGateway"/>).
@@ -16,9 +16,9 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Historian;
/// Galaxy native alarm bridge, AB CIP ALMD reader) tells <see cref="AlarmHistorianEvent"/>s to this
/// actor; the actor enqueues them on the sink fire-and-forget. Production deployments register
/// <see cref="SqliteStoreAndForwardSink"/> against <c>IAlarmHistorianSink</c>; the sink owns the
/// durable queue + drain-to-Wonderware-TCP-channel loop. The actor here owns nothing operational beyond
/// durable queue + drain-to-HistorianGateway-SendEvent loop. The actor here owns nothing operational beyond
/// the message contract — its job is to keep the engine actors on Akka's mailbox without blocking
/// them on disk I/O or TCP channel handshakes.
/// them on disk I/O or gateway round-trips.
///
/// Query queue depth + drain health via <see cref="GetStatus"/>.
/// </summary>