docs(alarms): describe the real StreamAlarms open sequence, and close TST-16

The final integration review's non-blocker reservations, all documentation
or comment truth except one test arm.

The alarm feed opens provider_status -> snapshot_status -> cached
active_alarm -> snapshot_complete, which is what GatewayAlarmMonitor has
done since the snapshot_status frame landed. Two places still described
the old order: docs/Grpc.md said provider_status arrived *after* the
initial snapshot, contradicting its own snapshot_status section two
paragraphs down, and AlarmFeedMessage's leading proto comment named
neither status frame at all. Both now state the sequence the monitor
emits, so a client author reading either one gets the frame order right.

The proto comment change flows through the generated trees (Contracts,
Go, Java) and the client descriptor set; the Rust vendored copy stays
byte-identical to canonical. Python's generator does not carry proto
comments into its output, so it has no delta.

AlarmsHubPublisherTests' valueless-payload case covered snapshot_complete
and provider_status but not snapshot_status, leaving the newest arm
unpinned against the redaction switch that must ignore it. Added.

WnWrapAlarmConsumer's ack comment led with the 2026-05-01 reading that
-55 tracks the 8-arg overload, then refuted itself six lines later with
the 2026-08-18 probe. It now leads with the observation labelled as
narrower than it reads -- mirroring the correction already in
docs/AlarmClientDiscovery.md -- so the block argues one thing: the 6-arg
call site stays for parity, and rc semantics are per the probe. A
paragraph orphaned by an earlier splice is rewrapped. Comment interior
only; the file compiles on Windows.

TST-16 gets a dated closure note rather than a rewrite: the flag it
called dead was implemented 2026-08-18. GatewayDashboardDesign's /browse
paragraph gains the failed-read carve-out GatewayConfiguration already
documented, so the two agree that a failed read keeps its - placeholder.
This commit is contained in:
Joseph Doherty
2026-08-18 06:09:06 -04:00
parent a95e510015
commit c3c603f169
11 changed files with 83 additions and 41 deletions
@@ -355,6 +355,8 @@ If TST-02's interim mitigation (flip retention off) is chosen instead of impleme
**Verification.** `dotnet build src/ZB.MOM.WW.MxGateway.Server`; test toggling the flag suppresses/shows values; docs match.
**Closed 2026-08-18 — implemented (the Design's recommended path).** The flag is no longer dead: it now gates value display on all three seams that carry a tag value. `Dashboard/DashboardLiveDataService.cs` substitutes `[redacted]` at the service boundary for each successfully read `/browse` tag (a failed read keeps its `-` placeholder, so the error row and the redaction cannot contradict each other); `Dashboard/Hubs/AlarmsHubPublisher.cs` clears `current_value`/`limit_value` from a **deep-cloned** `AlarmFeedMessage` before broadcasting to `/hubs/alarms`, and `DashboardEventBroadcaster` does the same for the events-hub mirror — clones, because the sources fan out to gRPC `StreamAlarms`/`StreamEvents` and the replay ring, so no gRPC client is affected by this dashboard-display flag. The flag is **kept**, default still `false`. The stale prose the Finding flagged is gone: `docs/GatewayConfiguration.md` and `docs/GatewayDashboardDesign.md` now describe the real behaviour. Status tracked in `00-tracking.md` (already **Done**).
---
## TST-17 — Vendor-gated alarm parity residuals silently lossy `Medium` · `—`
@@ -7342,9 +7342,13 @@ func (x *StreamAlarmsRequest) GetAlarmFilterPrefix() string {
return ""
}
// One message on the StreamAlarms feed. The stream opens with one
// `active_alarm` per currently-active alarm, then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
// One message on the StreamAlarms feed. The stream opens with a
// `provider_status` (the current provider mode), then a `snapshot_status`
// (whether the cached active set may be incomplete), then one `active_alarm`
// per currently-active alarm, then a single `snapshot_complete`. After that
// come the live frames: a `transition` for every subsequent alarm change,
// interleaved with a further `provider_status` on each failover/failback and a
// further `snapshot_status` on each change of the truncation verdict.
type AlarmFeedMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
@@ -89203,9 +89203,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile {
}
/**
* <pre>
* One message on the StreamAlarms feed. The stream opens with one
* `active_alarm` per currently-active alarm, then a single
* `snapshot_complete`, then a `transition` for every subsequent change.
* One message on the StreamAlarms feed. The stream opens with a
* `provider_status` (the current provider mode), then a `snapshot_status`
* (whether the cached active set may be incomplete), then one `active_alarm`
* per currently-active alarm, then a single `snapshot_complete`. After that
* come the live frames: a `transition` for every subsequent alarm change,
* interleaved with a further `provider_status` on each failover/failback and a
* further `snapshot_status` on each change of the truncation verdict.
* </pre>
*
* Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage}
@@ -89739,9 +89743,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile {
}
/**
* <pre>
* One message on the StreamAlarms feed. The stream opens with one
* `active_alarm` per currently-active alarm, then a single
* `snapshot_complete`, then a `transition` for every subsequent change.
* One message on the StreamAlarms feed. The stream opens with a
* `provider_status` (the current provider mode), then a `snapshot_status`
* (whether the cached active set may be incomplete), then one `active_alarm`
* per currently-active alarm, then a single `snapshot_complete`. After that
* come the live frames: a `transition` for every subsequent alarm change,
* interleaved with a further `provider_status` on each failover/failback and a
* further `snapshot_status` on each change of the truncation verdict.
* </pre>
*
* Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage}
+7 -3
View File
@@ -1003,9 +1003,13 @@ message StreamAlarmsRequest {
string alarm_filter_prefix = 2;
}
// One message on the StreamAlarms feed. The stream opens with one
// `active_alarm` per currently-active alarm, then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
// One message on the StreamAlarms feed. The stream opens with a
// `provider_status` (the current provider mode), then a `snapshot_status`
// (whether the cached active set may be incomplete), then one `active_alarm`
// per currently-active alarm, then a single `snapshot_complete`. After that
// come the live frames: a `transition` for every subsequent alarm change,
// interleaved with a further `provider_status` on each failover/failback and a
// further `snapshot_status` on each change of the truncation verdict.
message AlarmFeedMessage {
oneof payload {
// Part of the initial active-alarm snapshot (ConditionRefresh).
+3 -1
View File
@@ -520,7 +520,9 @@ panel. The panel shows each subscribed tag's live value, MXAccess data type,
quality and source timestamp, refreshed every two seconds — but the value column
obeys `Dashboard:ShowTagValues` like every other dashboard value surface. With
the flag false (the default) `DashboardLiveDataService` hands the page
`[redacted]` in place of the formatted value; data type, quality, source
`[redacted]` in place of the formatted value of each **successfully read** tag —
a failed read keeps its `-` placeholder, since there was no value to suppress —
while data type, quality, source
timestamp and any read error are untouched, so the panel still answers "is this
tag advising and healthy" without disclosing the value. The substitution happens
at the service, not in the page: one decision point, and the value never enters
+5 -4
View File
@@ -114,10 +114,11 @@ message AlarmProviderStatus {
}
```
The gateway emits `provider_status` once when a client first subscribes
(immediately after the initial snapshot and before the first live transition)
and again on every failover or failback. A late-joining client therefore
always learns the current provider mode without waiting for the next switch.
The gateway emits `provider_status` once when a client first subscribes — as
the **first** frame on the stream, before the `snapshot_status` frame and
before any cached `active_alarm`and again on every failover or failback. A
late-joining client therefore learns the current provider mode before it sees a
single alarm, without waiting for the next switch.
#### Snapshot completeness on the alarm feed
@@ -28513,9 +28513,13 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
/// <summary>
/// One message on the StreamAlarms feed. The stream opens with one
/// `active_alarm` per currently-active alarm, then a single
/// `snapshot_complete`, then a `transition` for every subsequent change.
/// One message on the StreamAlarms feed. The stream opens with a
/// `provider_status` (the current provider mode), then a `snapshot_status`
/// (whether the cached active set may be incomplete), then one `active_alarm`
/// per currently-active alarm, then a single `snapshot_complete`. After that
/// come the live frames: a `transition` for every subsequent alarm change,
/// interleaved with a further `provider_status` on each failover/failback and a
/// further `snapshot_status` on each change of the truncation verdict.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class AlarmFeedMessage : pb::IMessage<AlarmFeedMessage>
@@ -1003,9 +1003,13 @@ message StreamAlarmsRequest {
string alarm_filter_prefix = 2;
}
// One message on the StreamAlarms feed. The stream opens with one
// `active_alarm` per currently-active alarm, then a single
// `snapshot_complete`, then a `transition` for every subsequent change.
// One message on the StreamAlarms feed. The stream opens with a
// `provider_status` (the current provider mode), then a `snapshot_status`
// (whether the cached active set may be incomplete), then one `active_alarm`
// per currently-active alarm, then a single `snapshot_complete`. After that
// come the live frames: a `transition` for every subsequent alarm change,
// interleaved with a further `provider_status` on each failover/failback and a
// further `snapshot_status` on each change of the truncation verdict.
message AlarmFeedMessage {
oneof payload {
// Part of the initial active-alarm snapshot (ConditionRefresh).
@@ -119,13 +119,21 @@ public sealed class AlarmsHubPublisherTests
},
};
AlarmFeedMessage snapshotStatus = new()
{
SnapshotStatus = new AlarmSnapshotStatus { Truncated = true },
};
CapturingHubContext hubContext = await RunPublisherAsync(
showTagValues: false,
snapshotComplete,
providerStatus);
providerStatus,
snapshotStatus);
Assert.Same(snapshotComplete, hubContext.Sent[0]);
Assert.Same(providerStatus, hubContext.Sent[1]);
Assert.Same(snapshotStatus, hubContext.Sent[2]);
Assert.True(hubContext.Sent[2].SnapshotStatus.Truncated);
}
/// <summary>
@@ -345,23 +345,28 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
?? throw new InvalidOperationException(
"Cannot acknowledge: WnWrapAlarmConsumer was disposed or has not been subscribed yet.");
// Empirically (live dev-rig 2026-05-01): the IwwAlarmConsumer2
// 8-arg AlarmAckByName returns -55 on this AVEVA build (looks like
// a stub) and the legacy 6-arg IwwAlarmConsumer.AlarmAckByName
// returns 0, which is why the 6-arg overload is the one called here.
// A zero return is NOT evidence the acknowledgement was applied: the
// 2026-08-18 probe acked a real, freshly raised alarm six ways and
// watched the snapshot stay UNACK_ALM, OPERATOR_NAME stay empty, and
// the alarm extension's own .Acked attribute stay False for 16 s after
// each call. On that rig the ack is accepted and then inert, and the
// -55 tracks the consumer rather than the overload — both overloads
// return -55 against a SetXmlAlarmQuery-applied consumer and 0 against
// the ack-only one. Whether any wnwrap ack reaches the alarm-history
// path is therefore unverified; see docs/AlarmProbeFindings.md.
// Operator-domain
// and operator-full-name fields are accepted by the proto contract
// for forward-compat but are not propagated to AVEVA today —
// wrapped in the 6-arg call so domain/full-name go to the
// The original observation (live dev-rig 2026-05-01) was narrower than
// it read: the IwwAlarmConsumer2 8-arg AlarmAckByName returned -55
// "and looked like a stub" while the legacy 6-arg
// IwwAlarmConsumer.AlarmAckByName returned 0, which is how the 6-arg
// overload came to be the one called here. The 2026-08-18 probe
// corrected both halves. The -55 tracks the *consumer*, not the
// overload: both overloads return -55 against a
// SetXmlAlarmQuery-applied consumer and 0 against the ack-only one
// used above. And a zero return is NOT evidence the acknowledgement
// was applied — the probe acked a real, freshly raised alarm six ways
// and watched the snapshot stay UNACK_ALM, OPERATOR_NAME stay empty,
// and the alarm extension's own .Acked attribute stay False for 16 s
// after each call. On that rig the ack is accepted and then inert, so
// whether any wnwrap ack reaches the alarm-history path is
// unverified; see docs/AlarmProbeFindings.md and the 2026-08-18
// correction in docs/AlarmClientDiscovery.md. The 6-arg call site
// below therefore stays as-is for MXAccess parity — the choice is no
// longer justified by the rc, and rc semantics are per the probe.
//
// Operator-domain and operator-full-name fields are accepted by the
// proto contract for forward-compat but are not propagated to AVEVA
// today — wrapped in the 6-arg call so domain/full-name go to the
// alarm-history operator-name field via the szOprName parameter.
// Suppress unused-warning explicitly:
_ = ackOperatorDomain;