Compare commits

...

10 Commits

Author SHA1 Message Date
Joseph Doherty 90331b622c docs(alarms): second wnwrap probe attempt — secured-write verb unblocked the rig, answering @COUNT and the re-raise GUID leg
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m23s
ci / java (push) Successful in 2m25s
ci / portable (push) Successful in 24m49s
AuthenticateUser("Administrator", "") + WriteSecured raises the alarm UDAs that
plain Write could not touch (SecurityError detail=1008), so the 2026-08-17 blocker
was the verb, exactly as that run's own Unblocking list predicted.

Two of the three open questions are now observed rather than assumed:

- ALARM_RECORDS/@COUNT reports the records in the reply, not the total active
  count. With three alarms active it read 1 at cap 1 and 2 at cap 2. There is no
  exact truncation signal to switch to, so IsTruncatedFetch's conservative rule is
  the design rather than a placeholder — behaviour unchanged, only the comments.
- Clear-then-re-raise mints a new GUID; the ALM->RTN leg keeps its GUID
  (reconfirming the 2026-05-01 capture). ComputeTransitions already reads the
  re-raise correctly as one instance ending and another beginning.

The acknowledge leg stays unobserved for a narrower reason: every wnwrap ack
surface is inert on this rig. AlarmAckByName returns 0 from the ack-only consumer
and -55 from the SetXmlAlarmQuery-applied one, for both the 6-arg and 8-arg forms,
and neither the snapshot STATE, OPERATOR_NAME, nor the extension's own .Acked
attribute moves. That corrects AlarmClientDiscovery.md, which read the zero return
as a working ack.

Comment- and prose-only; no behaviour change. The three throwaway probes ran from
the windev CI clone and were deleted; that clone is a clean tree at ab3ff16.
2026-08-18 05:40:11 -04:00
Joseph Doherty ad30adca2f chore(plan): Task 2 complete — five-client snapshot_status rollout approved 2026-08-18 05:33:25 -04:00
Joseph Doherty 4d4f443c27 fix(clients): review nits — alphabetize the Java CLI alarm imports, cover the Rust snapshot-status render
The Java import block's AlarmFeedMessage already sorted after
AlarmProviderStatus before c748361; adding AlarmSnapshotStatus widened the
gap. Order all three alphabetically.

The Rust CLI tests the sibling ProviderStatus render path but not the new
SnapshotStatus arm, so the summary string and the JSON shape were both
uncovered. Add the matching test over alarm_feed_message_summary and
alarm_feed_message_to_json.
2026-08-18 05:33:00 -04:00
Joseph Doherty c7483615cf chore(clients): roll out feed-level snapshot_status to all five clients
Task 1 added AlarmSnapshotStatus and AlarmFeedMessage.snapshot_status = 5.
Carry it downstream from the canonical Contracts protos:

- Rust vendored protos under clients/rust/protos, refreshed byte-identical
  (build.rs falls back to them for out-of-repo tarball builds)
- client descriptor set (protoc 34.1 pin)
- Go (protoc-gen-go v1.36.11 / protoc-gen-go-grpc 1.6.2)
- Python (grpcio-tools 1.80.0 pin)
- Java (gradle generateProto)

.NET needs no regeneration: the client compiles against the Contracts
Generated/ output committed with the proto change.

The hand-written CLI feed renderers switch on the payload oneof, so codegen
alone does not carry the arm. Add snapshot-status to the .NET, Go, Rust, and
Java renderers; the .NET and Go renderers were also missing provider-status,
which has been on the wire since the provider-mode work, so add it there too.
Java's renderer is an exhaustive switch expression and did not compile until
the new case landed. The Python CLI renders generic protobuf-JSON and needs
no change.

Each client README gains a paragraph on the feed-level frame next to its
existing from_truncated_snapshot paragraph: it arrives at stream open after
provider_status and before the cached active_alarm frames, then on every
verdict change including the clearing frame a monitor restart emits, so a
live consumer can track set completeness without polling QueryActiveAlarms.

GatewayDashboardDesign: list the two new payload cases the AlarmsHub forwards,
and — separately — record the GroupToRole / GroupToTag / UntaggedSessionVisibility
rows the settings page already renders but the bullet list omitted.
2026-08-18 05:26:06 -04:00
Joseph Doherty d13144a9d7 chore(plan): Task 3 complete — ShowTagValues coverage approved across all three seams 2026-08-18 05:21:45 -04:00
Joseph Doherty 155c9d619f chore(plan): Task 6 complete — dashboard_tags display and create input approved 2026-08-18 05:21:21 -04:00
Joseph Doherty 51b0a8f55f chore(plan): Task 1 complete — feed-level snapshot_status approved 2026-08-18 05:21:11 -04:00
Joseph Doherty e50ec493cb docs(config): ShowTagValues /browse clause — redaction applies to successful reads only 2026-08-18 05:20:25 -04:00
Joseph Doherty 312e072070 docs(proto): spell out the monitor-restart clearing emission on AlarmSnapshotStatus 2026-08-18 05:19:31 -04:00
Joseph Doherty fab600d3b0 fix(dashboard): de-duplicate dashboard_tags typed into the API-key create form
The form split tags with the shared ParseList and attached the result verbatim,
so "team-a, TEAM-A" persisted as two entries and the constraints column read
dashboard_tags=[team-a, TEAM-A] — one grant reported as two on the page whose
job is to show what a key was granted. Enforcement never saw it (a session holds
its tags in a case-insensitive set), which is exactly why the display was the
only place it could surface.

De-duplicates ordinal-ignore-case at the attach point only, first spelling
winning, matching ApiKeyAdminCommandLineParser.ParseDashboardTags. ParseList is
untouched: the five glob lists are matched literally, so near-duplicates there
are not necessarily the same rule and must survive verbatim — pinned by a test.

The help text claimed to mirror the CLI flag; it now claims only the shared
separators and the dedupe, since the form still drops an empty segment silently
where the CLI hard-fails. A browser form has no exit code to fail with, so that
difference stays, and Authorization.md now records it.
2026-08-18 05:19:02 -04:00
27 changed files with 1754 additions and 475 deletions
+10
View File
@@ -156,6 +156,16 @@ poll. Treat the set as possibly incomplete rather than reconciling deletions
from it. It is set-level degraded status, not a comment on the record's own
fidelity, and is distinct from `Degraded` (the subtag fallback provider).
`StreamAlarmsAsync` also carries that completeness verdict at feed level, as an
`AlarmFeedMessage.PayloadOneofCase.SnapshotStatus` frame whose
`SnapshotStatus.Truncated` is true while the monitor's cached set derives from a
truncated fetch. One arrives at stream open (after the `ProviderStatus` frame,
before the cached `ActiveAlarm` frames) so a late joiner learns the current
verdict, then one on every verdict change — including the clearing frame sent
when the gateway's alarm monitor restarts and drops a truncated verdict. Track
it if you need set completeness on a live feed without polling
`QueryActiveAlarmsAsync`.
`MxGatewaySession.CloseAsync` is explicit and idempotent. Repeated calls return
the first `CloseSessionReply` instead of sending another close request.
@@ -1546,8 +1546,9 @@ public static class MxGatewayClientCli
/// <summary>
/// Renders one <see cref="AlarmFeedMessage"/> for the human-readable
/// (non-JSON) stream-alarms output, distinguishing the <c>payload</c> oneof
/// arms: a snapshot active alarm, the snapshot-complete sentinel, or a live
/// transition.
/// arms: a snapshot active alarm, the snapshot-complete sentinel, a live
/// transition, the provider-mode status, or the feed-level
/// snapshot-completeness status.
/// </summary>
private static string FormatAlarmFeedMessage(AlarmFeedMessage feedMessage)
{
@@ -1559,6 +1560,10 @@ public static class MxGatewayClientCli
$"snapshot-complete {feedMessage.SnapshotComplete}",
AlarmFeedMessage.PayloadOneofCase.Transition =>
$"transition {ProtobufJsonFormatter.Format(feedMessage.Transition)}",
AlarmFeedMessage.PayloadOneofCase.ProviderStatus =>
$"provider-status {ProtobufJsonFormatter.Format(feedMessage.ProviderStatus)}",
AlarmFeedMessage.PayloadOneofCase.SnapshotStatus =>
$"snapshot-status {ProtobufJsonFormatter.Format(feedMessage.SnapshotStatus)}",
_ => $"unknown-payload {feedMessage.PayloadCase}",
};
}
+9
View File
@@ -153,6 +153,15 @@ deletions from it. It is set-level degraded status, not a comment on the
record's own fidelity, and is distinct from `Degraded` (the subtag fallback
provider).
`StreamAlarms` also carries that completeness verdict at feed level, as a frame
whose `GetSnapshotStatus()` is non-nil and whose `GetTruncated()` is true while
the monitor's cached set derives from a truncated fetch. One arrives at stream
open (after the `GetProviderStatus()` frame, before the cached
`GetActiveAlarm()` frames) so a late joiner learns the current verdict, then one
on every verdict change — including the clearing frame sent when the gateway's
alarm monitor restarts and drops a truncated verdict. Track it if you need set
completeness on a live feed without polling `QueryActiveAlarms`.
## Write Semantics And Common Pitfalls
These are MXAccess parity behaviors that surprise new callers. The gateway
+7 -1
View File
@@ -1097,7 +1097,8 @@ func runStreamAlarms(ctx context.Context, args []string, stdout, stderr io.Write
// formatAlarmFeedMessage renders one AlarmFeedMessage in the CLI's plain-text
// output style, distinguishing the active-alarm snapshot, snapshot-complete
// sentinel, and transition cases of the message's payload oneof.
// sentinel, transition, provider-status, and snapshot-status cases of the
// message's payload oneof.
func formatAlarmFeedMessage(message *mxgateway.AlarmFeedMessage) string {
switch {
case message.GetActiveAlarm() != nil:
@@ -1108,6 +1109,11 @@ func formatAlarmFeedMessage(message *mxgateway.AlarmFeedMessage) string {
case message.GetTransition() != nil:
transition := message.GetTransition()
return fmt.Sprintf("transition %s kind=%s severity=%d", transition.GetAlarmFullReference(), transition.GetTransitionKind(), transition.GetSeverity())
case message.GetProviderStatus() != nil:
status := message.GetProviderStatus()
return fmt.Sprintf("provider-status mode=%s degraded=%t reason=%q", status.GetMode(), status.GetDegraded(), status.GetReason())
case message.GetSnapshotStatus() != nil:
return fmt.Sprintf("snapshot-status truncated=%t", message.GetSnapshotStatus().GetTruncated())
default:
return "unknown"
}
@@ -7353,6 +7353,7 @@ type AlarmFeedMessage struct {
// *AlarmFeedMessage_SnapshotComplete
// *AlarmFeedMessage_Transition
// *AlarmFeedMessage_ProviderStatus
// *AlarmFeedMessage_SnapshotStatus
Payload isAlarmFeedMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -7431,6 +7432,15 @@ func (x *AlarmFeedMessage) GetProviderStatus() *AlarmProviderStatus {
return nil
}
func (x *AlarmFeedMessage) GetSnapshotStatus() *AlarmSnapshotStatus {
if x != nil {
if x, ok := x.Payload.(*AlarmFeedMessage_SnapshotStatus); ok {
return x.SnapshotStatus
}
}
return nil
}
type isAlarmFeedMessage_Payload interface {
isAlarmFeedMessage_Payload()
}
@@ -7457,6 +7467,13 @@ type AlarmFeedMessage_ProviderStatus struct {
ProviderStatus *AlarmProviderStatus `protobuf:"bytes,4,opt,name=provider_status,json=providerStatus,proto3,oneof"`
}
type AlarmFeedMessage_SnapshotStatus struct {
// Snapshot-completeness status. Emitted once on stream open and again on
// every change of the truncation verdict, so late joiners learn whether the
// feed's active-alarm set may be incomplete.
SnapshotStatus *AlarmSnapshotStatus `protobuf:"bytes,5,opt,name=snapshot_status,json=snapshotStatus,proto3,oneof"`
}
func (*AlarmFeedMessage_ActiveAlarm) isAlarmFeedMessage_Payload() {}
func (*AlarmFeedMessage_SnapshotComplete) isAlarmFeedMessage_Payload() {}
@@ -7465,6 +7482,8 @@ func (*AlarmFeedMessage_Transition) isAlarmFeedMessage_Payload() {}
func (*AlarmFeedMessage_ProviderStatus) isAlarmFeedMessage_Payload() {}
func (*AlarmFeedMessage_SnapshotStatus) isAlarmFeedMessage_Payload() {}
type AlarmProviderStatus struct {
state protoimpl.MessageState `protogen:"open.v1"`
Mode AlarmProviderMode `protobuf:"varint,1,opt,name=mode,proto3,enum=mxaccess_gateway.v1.AlarmProviderMode" json:"mode,omitempty"`
@@ -7533,6 +7552,63 @@ func (x *AlarmProviderStatus) GetSince() *timestamppb.Timestamp {
return nil
}
// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
// (after the initial provider_status frame, before the cached active_alarm
// frames) so late joiners learn the current verdict, and again on every change
// of the truncation verdict — when a reconcile reports a different verdict, and
// when the gateway's alarm monitor restarts and drops a truncated verdict with
// the cache generation it described (feed subscribers outlive that monitor
// session, so they are sent the clearing frame). Mirrors the per-record
// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
// consumers can reason about completeness without polling QueryActiveAlarms.
type AlarmSnapshotStatus struct {
state protoimpl.MessageState `protogen:"open.v1"`
// True while the monitor's cached active-alarm set derives from a truncated
// (capped) worker fetch — the set may be missing alarms. Distinct from
// provider degradation (AlarmProviderStatus.degraded), which describes the
// fidelity of the records rather than the completeness of the set.
Truncated bool `protobuf:"varint,1,opt,name=truncated,proto3" json:"truncated,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AlarmSnapshotStatus) Reset() {
*x = AlarmSnapshotStatus{}
mi := &file_mxaccess_gateway_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AlarmSnapshotStatus) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AlarmSnapshotStatus) ProtoMessage() {}
func (x *AlarmSnapshotStatus) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AlarmSnapshotStatus.ProtoReflect.Descriptor instead.
func (*AlarmSnapshotStatus) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{87}
}
func (x *AlarmSnapshotStatus) GetTruncated() bool {
if x != nil {
return x.Truncated
}
return false
}
type MxStatusProxy struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
@@ -7557,7 +7633,7 @@ type MxStatusProxy struct {
func (x *MxStatusProxy) Reset() {
*x = MxStatusProxy{}
mi := &file_mxaccess_gateway_proto_msgTypes[87]
mi := &file_mxaccess_gateway_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7569,7 +7645,7 @@ func (x *MxStatusProxy) String() string {
func (*MxStatusProxy) ProtoMessage() {}
func (x *MxStatusProxy) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[87]
mi := &file_mxaccess_gateway_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7582,7 +7658,7 @@ func (x *MxStatusProxy) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxStatusProxy.ProtoReflect.Descriptor instead.
func (*MxStatusProxy) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{87}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{88}
}
func (x *MxStatusProxy) GetSuccess() int32 {
@@ -7660,7 +7736,7 @@ type MxValue struct {
func (x *MxValue) Reset() {
*x = MxValue{}
mi := &file_mxaccess_gateway_proto_msgTypes[88]
mi := &file_mxaccess_gateway_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7672,7 +7748,7 @@ func (x *MxValue) String() string {
func (*MxValue) ProtoMessage() {}
func (x *MxValue) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[88]
mi := &file_mxaccess_gateway_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7685,7 +7761,7 @@ func (x *MxValue) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxValue.ProtoReflect.Descriptor instead.
func (*MxValue) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{88}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{89}
}
func (x *MxValue) GetDataType() MxDataType {
@@ -7908,7 +7984,7 @@ type MxArray struct {
func (x *MxArray) Reset() {
*x = MxArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[89]
mi := &file_mxaccess_gateway_proto_msgTypes[90]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -7920,7 +7996,7 @@ func (x *MxArray) String() string {
func (*MxArray) ProtoMessage() {}
func (x *MxArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[89]
mi := &file_mxaccess_gateway_proto_msgTypes[90]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -7933,7 +8009,7 @@ func (x *MxArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxArray.ProtoReflect.Descriptor instead.
func (*MxArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{89}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{90}
}
func (x *MxArray) GetElementDataType() MxDataType {
@@ -8117,7 +8193,7 @@ type MxSparseArray struct {
func (x *MxSparseArray) Reset() {
*x = MxSparseArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[90]
mi := &file_mxaccess_gateway_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8129,7 +8205,7 @@ func (x *MxSparseArray) String() string {
func (*MxSparseArray) ProtoMessage() {}
func (x *MxSparseArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[90]
mi := &file_mxaccess_gateway_proto_msgTypes[91]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8142,7 +8218,7 @@ func (x *MxSparseArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxSparseArray.ProtoReflect.Descriptor instead.
func (*MxSparseArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{90}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{91}
}
func (x *MxSparseArray) GetElementDataType() MxDataType {
@@ -8176,7 +8252,7 @@ type MxSparseElement struct {
func (x *MxSparseElement) Reset() {
*x = MxSparseElement{}
mi := &file_mxaccess_gateway_proto_msgTypes[91]
mi := &file_mxaccess_gateway_proto_msgTypes[92]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8188,7 +8264,7 @@ func (x *MxSparseElement) String() string {
func (*MxSparseElement) ProtoMessage() {}
func (x *MxSparseElement) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[91]
mi := &file_mxaccess_gateway_proto_msgTypes[92]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8201,7 +8277,7 @@ func (x *MxSparseElement) ProtoReflect() protoreflect.Message {
// Deprecated: Use MxSparseElement.ProtoReflect.Descriptor instead.
func (*MxSparseElement) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{91}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{92}
}
func (x *MxSparseElement) GetIndex() uint32 {
@@ -8227,7 +8303,7 @@ type BoolArray struct {
func (x *BoolArray) Reset() {
*x = BoolArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[92]
mi := &file_mxaccess_gateway_proto_msgTypes[93]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8239,7 +8315,7 @@ func (x *BoolArray) String() string {
func (*BoolArray) ProtoMessage() {}
func (x *BoolArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[92]
mi := &file_mxaccess_gateway_proto_msgTypes[93]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8252,7 +8328,7 @@ func (x *BoolArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use BoolArray.ProtoReflect.Descriptor instead.
func (*BoolArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{92}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{93}
}
func (x *BoolArray) GetValues() []bool {
@@ -8271,7 +8347,7 @@ type Int32Array struct {
func (x *Int32Array) Reset() {
*x = Int32Array{}
mi := &file_mxaccess_gateway_proto_msgTypes[93]
mi := &file_mxaccess_gateway_proto_msgTypes[94]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8283,7 +8359,7 @@ func (x *Int32Array) String() string {
func (*Int32Array) ProtoMessage() {}
func (x *Int32Array) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[93]
mi := &file_mxaccess_gateway_proto_msgTypes[94]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8296,7 +8372,7 @@ func (x *Int32Array) ProtoReflect() protoreflect.Message {
// Deprecated: Use Int32Array.ProtoReflect.Descriptor instead.
func (*Int32Array) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{93}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{94}
}
func (x *Int32Array) GetValues() []int32 {
@@ -8315,7 +8391,7 @@ type Int64Array struct {
func (x *Int64Array) Reset() {
*x = Int64Array{}
mi := &file_mxaccess_gateway_proto_msgTypes[94]
mi := &file_mxaccess_gateway_proto_msgTypes[95]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8327,7 +8403,7 @@ func (x *Int64Array) String() string {
func (*Int64Array) ProtoMessage() {}
func (x *Int64Array) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[94]
mi := &file_mxaccess_gateway_proto_msgTypes[95]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8340,7 +8416,7 @@ func (x *Int64Array) ProtoReflect() protoreflect.Message {
// Deprecated: Use Int64Array.ProtoReflect.Descriptor instead.
func (*Int64Array) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{94}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{95}
}
func (x *Int64Array) GetValues() []int64 {
@@ -8359,7 +8435,7 @@ type FloatArray struct {
func (x *FloatArray) Reset() {
*x = FloatArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[95]
mi := &file_mxaccess_gateway_proto_msgTypes[96]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8371,7 +8447,7 @@ func (x *FloatArray) String() string {
func (*FloatArray) ProtoMessage() {}
func (x *FloatArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[95]
mi := &file_mxaccess_gateway_proto_msgTypes[96]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8384,7 +8460,7 @@ func (x *FloatArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use FloatArray.ProtoReflect.Descriptor instead.
func (*FloatArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{95}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{96}
}
func (x *FloatArray) GetValues() []float32 {
@@ -8403,7 +8479,7 @@ type DoubleArray struct {
func (x *DoubleArray) Reset() {
*x = DoubleArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[96]
mi := &file_mxaccess_gateway_proto_msgTypes[97]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8415,7 +8491,7 @@ func (x *DoubleArray) String() string {
func (*DoubleArray) ProtoMessage() {}
func (x *DoubleArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[96]
mi := &file_mxaccess_gateway_proto_msgTypes[97]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8428,7 +8504,7 @@ func (x *DoubleArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use DoubleArray.ProtoReflect.Descriptor instead.
func (*DoubleArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{96}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{97}
}
func (x *DoubleArray) GetValues() []float64 {
@@ -8447,7 +8523,7 @@ type StringArray struct {
func (x *StringArray) Reset() {
*x = StringArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[97]
mi := &file_mxaccess_gateway_proto_msgTypes[98]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8459,7 +8535,7 @@ func (x *StringArray) String() string {
func (*StringArray) ProtoMessage() {}
func (x *StringArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[97]
mi := &file_mxaccess_gateway_proto_msgTypes[98]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8472,7 +8548,7 @@ func (x *StringArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use StringArray.ProtoReflect.Descriptor instead.
func (*StringArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{97}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{98}
}
func (x *StringArray) GetValues() []string {
@@ -8491,7 +8567,7 @@ type TimestampArray struct {
func (x *TimestampArray) Reset() {
*x = TimestampArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[98]
mi := &file_mxaccess_gateway_proto_msgTypes[99]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8503,7 +8579,7 @@ func (x *TimestampArray) String() string {
func (*TimestampArray) ProtoMessage() {}
func (x *TimestampArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[98]
mi := &file_mxaccess_gateway_proto_msgTypes[99]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8516,7 +8592,7 @@ func (x *TimestampArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use TimestampArray.ProtoReflect.Descriptor instead.
func (*TimestampArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{98}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{99}
}
func (x *TimestampArray) GetValues() []*timestamppb.Timestamp {
@@ -8535,7 +8611,7 @@ type RawArray struct {
func (x *RawArray) Reset() {
*x = RawArray{}
mi := &file_mxaccess_gateway_proto_msgTypes[99]
mi := &file_mxaccess_gateway_proto_msgTypes[100]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8547,7 +8623,7 @@ func (x *RawArray) String() string {
func (*RawArray) ProtoMessage() {}
func (x *RawArray) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[99]
mi := &file_mxaccess_gateway_proto_msgTypes[100]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8560,7 +8636,7 @@ func (x *RawArray) ProtoReflect() protoreflect.Message {
// Deprecated: Use RawArray.ProtoReflect.Descriptor instead.
func (*RawArray) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{99}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{100}
}
func (x *RawArray) GetValues() [][]byte {
@@ -8580,7 +8656,7 @@ type ProtocolStatus struct {
func (x *ProtocolStatus) Reset() {
*x = ProtocolStatus{}
mi := &file_mxaccess_gateway_proto_msgTypes[100]
mi := &file_mxaccess_gateway_proto_msgTypes[101]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -8592,7 +8668,7 @@ func (x *ProtocolStatus) String() string {
func (*ProtocolStatus) ProtoMessage() {}
func (x *ProtocolStatus) ProtoReflect() protoreflect.Message {
mi := &file_mxaccess_gateway_proto_msgTypes[100]
mi := &file_mxaccess_gateway_proto_msgTypes[101]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -8605,7 +8681,7 @@ func (x *ProtocolStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProtocolStatus.ProtoReflect.Descriptor instead.
func (*ProtocolStatus) Descriptor() ([]byte, []int) {
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{100}
return file_mxaccess_gateway_proto_rawDescGZIP(), []int{101}
}
func (x *ProtocolStatus) GetCode() ProtocolStatusCode {
@@ -9115,20 +9191,23 @@ const file_mxaccess_gateway_proto_rawDesc = "" +
"session_id\"y\n" +
"\x13StreamAlarmsRequest\x122\n" +
"\x15client_correlation_id\x18\x01 \x01(\tR\x13clientCorrelationId\x12.\n" +
"\x13alarm_filter_prefix\x18\x02 \x01(\tR\x11alarmFilterPrefix\"\xbf\x02\n" +
"\x13alarm_filter_prefix\x18\x02 \x01(\tR\x11alarmFilterPrefix\"\x94\x03\n" +
"\x10AlarmFeedMessage\x12M\n" +
"\factive_alarm\x18\x01 \x01(\v2(.mxaccess_gateway.v1.ActiveAlarmSnapshotH\x00R\vactiveAlarm\x12-\n" +
"\x11snapshot_complete\x18\x02 \x01(\bH\x00R\x10snapshotComplete\x12M\n" +
"\n" +
"transition\x18\x03 \x01(\v2+.mxaccess_gateway.v1.OnAlarmTransitionEventH\x00R\n" +
"transition\x12S\n" +
"\x0fprovider_status\x18\x04 \x01(\v2(.mxaccess_gateway.v1.AlarmProviderStatusH\x00R\x0eproviderStatusB\t\n" +
"\x0fprovider_status\x18\x04 \x01(\v2(.mxaccess_gateway.v1.AlarmProviderStatusH\x00R\x0eproviderStatus\x12S\n" +
"\x0fsnapshot_status\x18\x05 \x01(\v2(.mxaccess_gateway.v1.AlarmSnapshotStatusH\x00R\x0esnapshotStatusB\t\n" +
"\apayload\"\xb7\x01\n" +
"\x13AlarmProviderStatus\x12:\n" +
"\x04mode\x18\x01 \x01(\x0e2&.mxaccess_gateway.v1.AlarmProviderModeR\x04mode\x12\x1a\n" +
"\bdegraded\x18\x02 \x01(\bR\bdegraded\x12\x16\n" +
"\x06reason\x18\x03 \x01(\tR\x06reason\x120\n" +
"\x05since\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x05since\"\xbe\x02\n" +
"\x05since\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x05since\"3\n" +
"\x13AlarmSnapshotStatus\x12\x1c\n" +
"\ttruncated\x18\x01 \x01(\bR\ttruncated\"\xbe\x02\n" +
"\rMxStatusProxy\x12\x18\n" +
"\asuccess\x18\x01 \x01(\x05R\asuccess\x12A\n" +
"\bcategory\x18\x02 \x01(\x0e2%.mxaccess_gateway.v1.MxStatusCategoryR\bcategory\x12D\n" +
@@ -9364,7 +9443,7 @@ func file_mxaccess_gateway_proto_rawDescGZIP() []byte {
}
var file_mxaccess_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 10)
var file_mxaccess_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 101)
var file_mxaccess_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 102)
var file_mxaccess_gateway_proto_goTypes = []any{
(MxCommandKind)(0), // 0: mxaccess_gateway.v1.MxCommandKind
(AlarmProviderMode)(0), // 1: mxaccess_gateway.v1.AlarmProviderMode
@@ -9463,29 +9542,30 @@ var file_mxaccess_gateway_proto_goTypes = []any{
(*StreamAlarmsRequest)(nil), // 94: mxaccess_gateway.v1.StreamAlarmsRequest
(*AlarmFeedMessage)(nil), // 95: mxaccess_gateway.v1.AlarmFeedMessage
(*AlarmProviderStatus)(nil), // 96: mxaccess_gateway.v1.AlarmProviderStatus
(*MxStatusProxy)(nil), // 97: mxaccess_gateway.v1.MxStatusProxy
(*MxValue)(nil), // 98: mxaccess_gateway.v1.MxValue
(*MxArray)(nil), // 99: mxaccess_gateway.v1.MxArray
(*MxSparseArray)(nil), // 100: mxaccess_gateway.v1.MxSparseArray
(*MxSparseElement)(nil), // 101: mxaccess_gateway.v1.MxSparseElement
(*BoolArray)(nil), // 102: mxaccess_gateway.v1.BoolArray
(*Int32Array)(nil), // 103: mxaccess_gateway.v1.Int32Array
(*Int64Array)(nil), // 104: mxaccess_gateway.v1.Int64Array
(*FloatArray)(nil), // 105: mxaccess_gateway.v1.FloatArray
(*DoubleArray)(nil), // 106: mxaccess_gateway.v1.DoubleArray
(*StringArray)(nil), // 107: mxaccess_gateway.v1.StringArray
(*TimestampArray)(nil), // 108: mxaccess_gateway.v1.TimestampArray
(*RawArray)(nil), // 109: mxaccess_gateway.v1.RawArray
(*ProtocolStatus)(nil), // 110: mxaccess_gateway.v1.ProtocolStatus
(*durationpb.Duration)(nil), // 111: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 112: google.protobuf.Timestamp
(*AlarmSnapshotStatus)(nil), // 97: mxaccess_gateway.v1.AlarmSnapshotStatus
(*MxStatusProxy)(nil), // 98: mxaccess_gateway.v1.MxStatusProxy
(*MxValue)(nil), // 99: mxaccess_gateway.v1.MxValue
(*MxArray)(nil), // 100: mxaccess_gateway.v1.MxArray
(*MxSparseArray)(nil), // 101: mxaccess_gateway.v1.MxSparseArray
(*MxSparseElement)(nil), // 102: mxaccess_gateway.v1.MxSparseElement
(*BoolArray)(nil), // 103: mxaccess_gateway.v1.BoolArray
(*Int32Array)(nil), // 104: mxaccess_gateway.v1.Int32Array
(*Int64Array)(nil), // 105: mxaccess_gateway.v1.Int64Array
(*FloatArray)(nil), // 106: mxaccess_gateway.v1.FloatArray
(*DoubleArray)(nil), // 107: mxaccess_gateway.v1.DoubleArray
(*StringArray)(nil), // 108: mxaccess_gateway.v1.StringArray
(*TimestampArray)(nil), // 109: mxaccess_gateway.v1.TimestampArray
(*RawArray)(nil), // 110: mxaccess_gateway.v1.RawArray
(*ProtocolStatus)(nil), // 111: mxaccess_gateway.v1.ProtocolStatus
(*durationpb.Duration)(nil), // 112: google.protobuf.Duration
(*timestamppb.Timestamp)(nil), // 113: google.protobuf.Timestamp
}
var file_mxaccess_gateway_proto_depIdxs = []int32{
111, // 0: mxaccess_gateway.v1.OpenSessionRequest.command_timeout:type_name -> google.protobuf.Duration
111, // 1: mxaccess_gateway.v1.OpenSessionReply.default_command_timeout:type_name -> google.protobuf.Duration
110, // 2: mxaccess_gateway.v1.OpenSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
112, // 0: mxaccess_gateway.v1.OpenSessionRequest.command_timeout:type_name -> google.protobuf.Duration
112, // 1: mxaccess_gateway.v1.OpenSessionReply.default_command_timeout:type_name -> google.protobuf.Duration
111, // 2: mxaccess_gateway.v1.OpenSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
9, // 3: mxaccess_gateway.v1.CloseSessionReply.final_state:type_name -> mxaccess_gateway.v1.SessionState
110, // 4: mxaccess_gateway.v1.CloseSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
111, // 4: mxaccess_gateway.v1.CloseSessionReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
17, // 5: mxaccess_gateway.v1.MxCommandRequest.command:type_name -> mxaccess_gateway.v1.MxCommand
0, // 6: mxaccess_gateway.v1.MxCommand.kind:type_name -> mxaccess_gateway.v1.MxCommandKind
18, // 7: mxaccess_gateway.v1.MxCommand.register:type_name -> mxaccess_gateway.v1.RegisterCommand
@@ -9527,30 +9607,30 @@ var file_mxaccess_gateway_proto_depIdxs = []int32{
60, // 43: mxaccess_gateway.v1.MxCommand.get_worker_info:type_name -> mxaccess_gateway.v1.GetWorkerInfoCommand
61, // 44: mxaccess_gateway.v1.MxCommand.drain_events:type_name -> mxaccess_gateway.v1.DrainEventsCommand
62, // 45: mxaccess_gateway.v1.MxCommand.shutdown_worker:type_name -> mxaccess_gateway.v1.ShutdownWorkerCommand
98, // 46: mxaccess_gateway.v1.WriteCommand.value:type_name -> mxaccess_gateway.v1.MxValue
98, // 47: mxaccess_gateway.v1.Write2Command.value:type_name -> mxaccess_gateway.v1.MxValue
98, // 48: mxaccess_gateway.v1.Write2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
98, // 49: mxaccess_gateway.v1.WriteSecuredCommand.value:type_name -> mxaccess_gateway.v1.MxValue
98, // 50: mxaccess_gateway.v1.WriteSecured2Command.value:type_name -> mxaccess_gateway.v1.MxValue
98, // 51: mxaccess_gateway.v1.WriteSecured2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
99, // 46: mxaccess_gateway.v1.WriteCommand.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 47: mxaccess_gateway.v1.Write2Command.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 48: mxaccess_gateway.v1.Write2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
99, // 49: mxaccess_gateway.v1.WriteSecuredCommand.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 50: mxaccess_gateway.v1.WriteSecured2Command.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 51: mxaccess_gateway.v1.WriteSecured2Command.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
1, // 52: mxaccess_gateway.v1.SubscribeAlarmsCommand.forced_mode:type_name -> mxaccess_gateway.v1.AlarmProviderMode
43, // 53: mxaccess_gateway.v1.SubscribeAlarmsCommand.watch_list:type_name -> mxaccess_gateway.v1.AlarmSubtagTarget
44, // 54: mxaccess_gateway.v1.SubscribeAlarmsCommand.failover:type_name -> mxaccess_gateway.v1.AlarmFailoverConfig
50, // 55: mxaccess_gateway.v1.WriteBulkCommand.entries:type_name -> mxaccess_gateway.v1.WriteBulkEntry
98, // 56: mxaccess_gateway.v1.WriteBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 56: mxaccess_gateway.v1.WriteBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
52, // 57: mxaccess_gateway.v1.Write2BulkCommand.entries:type_name -> mxaccess_gateway.v1.Write2BulkEntry
98, // 58: mxaccess_gateway.v1.Write2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
98, // 59: mxaccess_gateway.v1.Write2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
99, // 58: mxaccess_gateway.v1.Write2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 59: mxaccess_gateway.v1.Write2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
54, // 60: mxaccess_gateway.v1.WriteSecuredBulkCommand.entries:type_name -> mxaccess_gateway.v1.WriteSecuredBulkEntry
98, // 61: mxaccess_gateway.v1.WriteSecuredBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 61: mxaccess_gateway.v1.WriteSecuredBulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
56, // 62: mxaccess_gateway.v1.WriteSecured2BulkCommand.entries:type_name -> mxaccess_gateway.v1.WriteSecured2BulkEntry
98, // 63: mxaccess_gateway.v1.WriteSecured2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
98, // 64: mxaccess_gateway.v1.WriteSecured2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
111, // 65: mxaccess_gateway.v1.ShutdownWorkerCommand.grace_period:type_name -> google.protobuf.Duration
99, // 63: mxaccess_gateway.v1.WriteSecured2BulkEntry.value:type_name -> mxaccess_gateway.v1.MxValue
99, // 64: mxaccess_gateway.v1.WriteSecured2BulkEntry.timestamp_value:type_name -> mxaccess_gateway.v1.MxValue
112, // 65: mxaccess_gateway.v1.ShutdownWorkerCommand.grace_period:type_name -> google.protobuf.Duration
0, // 66: mxaccess_gateway.v1.MxCommandReply.kind:type_name -> mxaccess_gateway.v1.MxCommandKind
110, // 67: mxaccess_gateway.v1.MxCommandReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
98, // 68: mxaccess_gateway.v1.MxCommandReply.return_value:type_name -> mxaccess_gateway.v1.MxValue
97, // 69: mxaccess_gateway.v1.MxCommandReply.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
111, // 67: mxaccess_gateway.v1.MxCommandReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
99, // 68: mxaccess_gateway.v1.MxCommandReply.return_value:type_name -> mxaccess_gateway.v1.MxValue
98, // 69: mxaccess_gateway.v1.MxCommandReply.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
64, // 70: mxaccess_gateway.v1.MxCommandReply.register:type_name -> mxaccess_gateway.v1.RegisterReply
65, // 71: mxaccess_gateway.v1.MxCommandReply.add_item:type_name -> mxaccess_gateway.v1.AddItemReply
66, // 72: mxaccess_gateway.v1.MxCommandReply.add_item2:type_name -> mxaccess_gateway.v1.AddItem2Reply
@@ -9575,24 +9655,24 @@ var file_mxaccess_gateway_proto_depIdxs = []int32{
78, // 91: mxaccess_gateway.v1.MxCommandReply.session_state:type_name -> mxaccess_gateway.v1.SessionStateReply
79, // 92: mxaccess_gateway.v1.MxCommandReply.worker_info:type_name -> mxaccess_gateway.v1.WorkerInfoReply
80, // 93: mxaccess_gateway.v1.MxCommandReply.drain_events:type_name -> mxaccess_gateway.v1.DrainEventsReply
97, // 94: mxaccess_gateway.v1.SuspendReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
97, // 95: mxaccess_gateway.v1.ActivateReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
98, // 94: mxaccess_gateway.v1.SuspendReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
98, // 95: mxaccess_gateway.v1.ActivateReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
72, // 96: mxaccess_gateway.v1.BulkSubscribeReply.results:type_name -> mxaccess_gateway.v1.SubscribeResult
97, // 97: mxaccess_gateway.v1.BulkWriteResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
98, // 97: mxaccess_gateway.v1.BulkWriteResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
74, // 98: mxaccess_gateway.v1.BulkWriteReply.results:type_name -> mxaccess_gateway.v1.BulkWriteResult
98, // 99: mxaccess_gateway.v1.BulkReadResult.value:type_name -> mxaccess_gateway.v1.MxValue
112, // 100: mxaccess_gateway.v1.BulkReadResult.source_timestamp:type_name -> google.protobuf.Timestamp
97, // 101: mxaccess_gateway.v1.BulkReadResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
99, // 99: mxaccess_gateway.v1.BulkReadResult.value:type_name -> mxaccess_gateway.v1.MxValue
113, // 100: mxaccess_gateway.v1.BulkReadResult.source_timestamp:type_name -> google.protobuf.Timestamp
98, // 101: mxaccess_gateway.v1.BulkReadResult.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
76, // 102: mxaccess_gateway.v1.BulkReadReply.results:type_name -> mxaccess_gateway.v1.BulkReadResult
9, // 103: mxaccess_gateway.v1.SessionStateReply.state:type_name -> mxaccess_gateway.v1.SessionState
83, // 104: mxaccess_gateway.v1.DrainEventsReply.events:type_name -> mxaccess_gateway.v1.MxEvent
91, // 105: mxaccess_gateway.v1.QueryActiveAlarmsReplyPayload.snapshots:type_name -> mxaccess_gateway.v1.ActiveAlarmSnapshot
2, // 106: mxaccess_gateway.v1.MxEvent.family:type_name -> mxaccess_gateway.v1.MxEventFamily
98, // 107: mxaccess_gateway.v1.MxEvent.value:type_name -> mxaccess_gateway.v1.MxValue
112, // 108: mxaccess_gateway.v1.MxEvent.source_timestamp:type_name -> google.protobuf.Timestamp
97, // 109: mxaccess_gateway.v1.MxEvent.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
112, // 110: mxaccess_gateway.v1.MxEvent.worker_timestamp:type_name -> google.protobuf.Timestamp
112, // 111: mxaccess_gateway.v1.MxEvent.gateway_receive_timestamp:type_name -> google.protobuf.Timestamp
99, // 107: mxaccess_gateway.v1.MxEvent.value:type_name -> mxaccess_gateway.v1.MxValue
113, // 108: mxaccess_gateway.v1.MxEvent.source_timestamp:type_name -> google.protobuf.Timestamp
98, // 109: mxaccess_gateway.v1.MxEvent.statuses:type_name -> mxaccess_gateway.v1.MxStatusProxy
113, // 110: mxaccess_gateway.v1.MxEvent.worker_timestamp:type_name -> google.protobuf.Timestamp
113, // 111: mxaccess_gateway.v1.MxEvent.gateway_receive_timestamp:type_name -> google.protobuf.Timestamp
84, // 112: mxaccess_gateway.v1.MxEvent.replay_gap:type_name -> mxaccess_gateway.v1.ReplayGap
85, // 113: mxaccess_gateway.v1.MxEvent.on_data_change:type_name -> mxaccess_gateway.v1.OnDataChangeEvent
86, // 114: mxaccess_gateway.v1.MxEvent.on_write_complete:type_name -> mxaccess_gateway.v1.OnWriteCompleteEvent
@@ -9601,68 +9681,69 @@ var file_mxaccess_gateway_proto_depIdxs = []int32{
89, // 117: mxaccess_gateway.v1.MxEvent.on_alarm_transition:type_name -> mxaccess_gateway.v1.OnAlarmTransitionEvent
90, // 118: mxaccess_gateway.v1.MxEvent.on_alarm_provider_mode_changed:type_name -> mxaccess_gateway.v1.OnAlarmProviderModeChangedEvent
7, // 119: mxaccess_gateway.v1.OnBufferedDataChangeEvent.data_type:type_name -> mxaccess_gateway.v1.MxDataType
99, // 120: mxaccess_gateway.v1.OnBufferedDataChangeEvent.quality_values:type_name -> mxaccess_gateway.v1.MxArray
99, // 121: mxaccess_gateway.v1.OnBufferedDataChangeEvent.timestamp_values:type_name -> mxaccess_gateway.v1.MxArray
100, // 120: mxaccess_gateway.v1.OnBufferedDataChangeEvent.quality_values:type_name -> mxaccess_gateway.v1.MxArray
100, // 121: mxaccess_gateway.v1.OnBufferedDataChangeEvent.timestamp_values:type_name -> mxaccess_gateway.v1.MxArray
3, // 122: mxaccess_gateway.v1.OnAlarmTransitionEvent.transition_kind:type_name -> mxaccess_gateway.v1.AlarmTransitionKind
112, // 123: mxaccess_gateway.v1.OnAlarmTransitionEvent.original_raise_timestamp:type_name -> google.protobuf.Timestamp
112, // 124: mxaccess_gateway.v1.OnAlarmTransitionEvent.transition_timestamp:type_name -> google.protobuf.Timestamp
98, // 125: mxaccess_gateway.v1.OnAlarmTransitionEvent.current_value:type_name -> mxaccess_gateway.v1.MxValue
98, // 126: mxaccess_gateway.v1.OnAlarmTransitionEvent.limit_value:type_name -> mxaccess_gateway.v1.MxValue
113, // 123: mxaccess_gateway.v1.OnAlarmTransitionEvent.original_raise_timestamp:type_name -> google.protobuf.Timestamp
113, // 124: mxaccess_gateway.v1.OnAlarmTransitionEvent.transition_timestamp:type_name -> google.protobuf.Timestamp
99, // 125: mxaccess_gateway.v1.OnAlarmTransitionEvent.current_value:type_name -> mxaccess_gateway.v1.MxValue
99, // 126: mxaccess_gateway.v1.OnAlarmTransitionEvent.limit_value:type_name -> mxaccess_gateway.v1.MxValue
1, // 127: mxaccess_gateway.v1.OnAlarmTransitionEvent.source_provider:type_name -> mxaccess_gateway.v1.AlarmProviderMode
1, // 128: mxaccess_gateway.v1.OnAlarmProviderModeChangedEvent.mode:type_name -> mxaccess_gateway.v1.AlarmProviderMode
112, // 129: mxaccess_gateway.v1.OnAlarmProviderModeChangedEvent.at:type_name -> google.protobuf.Timestamp
112, // 130: mxaccess_gateway.v1.ActiveAlarmSnapshot.original_raise_timestamp:type_name -> google.protobuf.Timestamp
113, // 129: mxaccess_gateway.v1.OnAlarmProviderModeChangedEvent.at:type_name -> google.protobuf.Timestamp
113, // 130: mxaccess_gateway.v1.ActiveAlarmSnapshot.original_raise_timestamp:type_name -> google.protobuf.Timestamp
4, // 131: mxaccess_gateway.v1.ActiveAlarmSnapshot.current_state:type_name -> mxaccess_gateway.v1.AlarmConditionState
112, // 132: mxaccess_gateway.v1.ActiveAlarmSnapshot.last_transition_timestamp:type_name -> google.protobuf.Timestamp
98, // 133: mxaccess_gateway.v1.ActiveAlarmSnapshot.current_value:type_name -> mxaccess_gateway.v1.MxValue
98, // 134: mxaccess_gateway.v1.ActiveAlarmSnapshot.limit_value:type_name -> mxaccess_gateway.v1.MxValue
113, // 132: mxaccess_gateway.v1.ActiveAlarmSnapshot.last_transition_timestamp:type_name -> google.protobuf.Timestamp
99, // 133: mxaccess_gateway.v1.ActiveAlarmSnapshot.current_value:type_name -> mxaccess_gateway.v1.MxValue
99, // 134: mxaccess_gateway.v1.ActiveAlarmSnapshot.limit_value:type_name -> mxaccess_gateway.v1.MxValue
1, // 135: mxaccess_gateway.v1.ActiveAlarmSnapshot.source_provider:type_name -> mxaccess_gateway.v1.AlarmProviderMode
110, // 136: mxaccess_gateway.v1.AcknowledgeAlarmReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
97, // 137: mxaccess_gateway.v1.AcknowledgeAlarmReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
111, // 136: mxaccess_gateway.v1.AcknowledgeAlarmReply.protocol_status:type_name -> mxaccess_gateway.v1.ProtocolStatus
98, // 137: mxaccess_gateway.v1.AcknowledgeAlarmReply.status:type_name -> mxaccess_gateway.v1.MxStatusProxy
91, // 138: mxaccess_gateway.v1.AlarmFeedMessage.active_alarm:type_name -> mxaccess_gateway.v1.ActiveAlarmSnapshot
89, // 139: mxaccess_gateway.v1.AlarmFeedMessage.transition:type_name -> mxaccess_gateway.v1.OnAlarmTransitionEvent
96, // 140: mxaccess_gateway.v1.AlarmFeedMessage.provider_status:type_name -> mxaccess_gateway.v1.AlarmProviderStatus
1, // 141: mxaccess_gateway.v1.AlarmProviderStatus.mode:type_name -> mxaccess_gateway.v1.AlarmProviderMode
112, // 142: mxaccess_gateway.v1.AlarmProviderStatus.since:type_name -> google.protobuf.Timestamp
5, // 143: mxaccess_gateway.v1.MxStatusProxy.category:type_name -> mxaccess_gateway.v1.MxStatusCategory
6, // 144: mxaccess_gateway.v1.MxStatusProxy.detected_by:type_name -> mxaccess_gateway.v1.MxStatusSource
7, // 145: mxaccess_gateway.v1.MxValue.data_type:type_name -> mxaccess_gateway.v1.MxDataType
112, // 146: mxaccess_gateway.v1.MxValue.timestamp_value:type_name -> google.protobuf.Timestamp
99, // 147: mxaccess_gateway.v1.MxValue.array_value:type_name -> mxaccess_gateway.v1.MxArray
100, // 148: mxaccess_gateway.v1.MxValue.sparse_array_value:type_name -> mxaccess_gateway.v1.MxSparseArray
7, // 149: mxaccess_gateway.v1.MxArray.element_data_type:type_name -> mxaccess_gateway.v1.MxDataType
102, // 150: mxaccess_gateway.v1.MxArray.bool_values:type_name -> mxaccess_gateway.v1.BoolArray
103, // 151: mxaccess_gateway.v1.MxArray.int32_values:type_name -> mxaccess_gateway.v1.Int32Array
104, // 152: mxaccess_gateway.v1.MxArray.int64_values:type_name -> mxaccess_gateway.v1.Int64Array
105, // 153: mxaccess_gateway.v1.MxArray.float_values:type_name -> mxaccess_gateway.v1.FloatArray
106, // 154: mxaccess_gateway.v1.MxArray.double_values:type_name -> mxaccess_gateway.v1.DoubleArray
107, // 155: mxaccess_gateway.v1.MxArray.string_values:type_name -> mxaccess_gateway.v1.StringArray
108, // 156: mxaccess_gateway.v1.MxArray.timestamp_values:type_name -> mxaccess_gateway.v1.TimestampArray
109, // 157: mxaccess_gateway.v1.MxArray.raw_values:type_name -> mxaccess_gateway.v1.RawArray
7, // 158: mxaccess_gateway.v1.MxSparseArray.element_data_type:type_name -> mxaccess_gateway.v1.MxDataType
101, // 159: mxaccess_gateway.v1.MxSparseArray.elements:type_name -> mxaccess_gateway.v1.MxSparseElement
98, // 160: mxaccess_gateway.v1.MxSparseElement.value:type_name -> mxaccess_gateway.v1.MxValue
112, // 161: mxaccess_gateway.v1.TimestampArray.values:type_name -> google.protobuf.Timestamp
8, // 162: mxaccess_gateway.v1.ProtocolStatus.code:type_name -> mxaccess_gateway.v1.ProtocolStatusCode
11, // 163: mxaccess_gateway.v1.MxAccessGateway.OpenSession:input_type -> mxaccess_gateway.v1.OpenSessionRequest
13, // 164: mxaccess_gateway.v1.MxAccessGateway.CloseSession:input_type -> mxaccess_gateway.v1.CloseSessionRequest
16, // 165: mxaccess_gateway.v1.MxAccessGateway.Invoke:input_type -> mxaccess_gateway.v1.MxCommandRequest
15, // 166: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:input_type -> mxaccess_gateway.v1.StreamEventsRequest
92, // 167: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:input_type -> mxaccess_gateway.v1.AcknowledgeAlarmRequest
94, // 168: mxaccess_gateway.v1.MxAccessGateway.StreamAlarms:input_type -> mxaccess_gateway.v1.StreamAlarmsRequest
10, // 169: mxaccess_gateway.v1.MxAccessGateway.QueryActiveAlarms:input_type -> mxaccess_gateway.v1.QueryActiveAlarmsRequest
12, // 170: mxaccess_gateway.v1.MxAccessGateway.OpenSession:output_type -> mxaccess_gateway.v1.OpenSessionReply
14, // 171: mxaccess_gateway.v1.MxAccessGateway.CloseSession:output_type -> mxaccess_gateway.v1.CloseSessionReply
63, // 172: mxaccess_gateway.v1.MxAccessGateway.Invoke:output_type -> mxaccess_gateway.v1.MxCommandReply
83, // 173: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:output_type -> mxaccess_gateway.v1.MxEvent
93, // 174: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:output_type -> mxaccess_gateway.v1.AcknowledgeAlarmReply
95, // 175: mxaccess_gateway.v1.MxAccessGateway.StreamAlarms:output_type -> mxaccess_gateway.v1.AlarmFeedMessage
91, // 176: mxaccess_gateway.v1.MxAccessGateway.QueryActiveAlarms:output_type -> mxaccess_gateway.v1.ActiveAlarmSnapshot
170, // [170:177] is the sub-list for method output_type
163, // [163:170] is the sub-list for method input_type
163, // [163:163] is the sub-list for extension type_name
163, // [163:163] is the sub-list for extension extendee
0, // [0:163] is the sub-list for field type_name
97, // 141: mxaccess_gateway.v1.AlarmFeedMessage.snapshot_status:type_name -> mxaccess_gateway.v1.AlarmSnapshotStatus
1, // 142: mxaccess_gateway.v1.AlarmProviderStatus.mode:type_name -> mxaccess_gateway.v1.AlarmProviderMode
113, // 143: mxaccess_gateway.v1.AlarmProviderStatus.since:type_name -> google.protobuf.Timestamp
5, // 144: mxaccess_gateway.v1.MxStatusProxy.category:type_name -> mxaccess_gateway.v1.MxStatusCategory
6, // 145: mxaccess_gateway.v1.MxStatusProxy.detected_by:type_name -> mxaccess_gateway.v1.MxStatusSource
7, // 146: mxaccess_gateway.v1.MxValue.data_type:type_name -> mxaccess_gateway.v1.MxDataType
113, // 147: mxaccess_gateway.v1.MxValue.timestamp_value:type_name -> google.protobuf.Timestamp
100, // 148: mxaccess_gateway.v1.MxValue.array_value:type_name -> mxaccess_gateway.v1.MxArray
101, // 149: mxaccess_gateway.v1.MxValue.sparse_array_value:type_name -> mxaccess_gateway.v1.MxSparseArray
7, // 150: mxaccess_gateway.v1.MxArray.element_data_type:type_name -> mxaccess_gateway.v1.MxDataType
103, // 151: mxaccess_gateway.v1.MxArray.bool_values:type_name -> mxaccess_gateway.v1.BoolArray
104, // 152: mxaccess_gateway.v1.MxArray.int32_values:type_name -> mxaccess_gateway.v1.Int32Array
105, // 153: mxaccess_gateway.v1.MxArray.int64_values:type_name -> mxaccess_gateway.v1.Int64Array
106, // 154: mxaccess_gateway.v1.MxArray.float_values:type_name -> mxaccess_gateway.v1.FloatArray
107, // 155: mxaccess_gateway.v1.MxArray.double_values:type_name -> mxaccess_gateway.v1.DoubleArray
108, // 156: mxaccess_gateway.v1.MxArray.string_values:type_name -> mxaccess_gateway.v1.StringArray
109, // 157: mxaccess_gateway.v1.MxArray.timestamp_values:type_name -> mxaccess_gateway.v1.TimestampArray
110, // 158: mxaccess_gateway.v1.MxArray.raw_values:type_name -> mxaccess_gateway.v1.RawArray
7, // 159: mxaccess_gateway.v1.MxSparseArray.element_data_type:type_name -> mxaccess_gateway.v1.MxDataType
102, // 160: mxaccess_gateway.v1.MxSparseArray.elements:type_name -> mxaccess_gateway.v1.MxSparseElement
99, // 161: mxaccess_gateway.v1.MxSparseElement.value:type_name -> mxaccess_gateway.v1.MxValue
113, // 162: mxaccess_gateway.v1.TimestampArray.values:type_name -> google.protobuf.Timestamp
8, // 163: mxaccess_gateway.v1.ProtocolStatus.code:type_name -> mxaccess_gateway.v1.ProtocolStatusCode
11, // 164: mxaccess_gateway.v1.MxAccessGateway.OpenSession:input_type -> mxaccess_gateway.v1.OpenSessionRequest
13, // 165: mxaccess_gateway.v1.MxAccessGateway.CloseSession:input_type -> mxaccess_gateway.v1.CloseSessionRequest
16, // 166: mxaccess_gateway.v1.MxAccessGateway.Invoke:input_type -> mxaccess_gateway.v1.MxCommandRequest
15, // 167: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:input_type -> mxaccess_gateway.v1.StreamEventsRequest
92, // 168: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:input_type -> mxaccess_gateway.v1.AcknowledgeAlarmRequest
94, // 169: mxaccess_gateway.v1.MxAccessGateway.StreamAlarms:input_type -> mxaccess_gateway.v1.StreamAlarmsRequest
10, // 170: mxaccess_gateway.v1.MxAccessGateway.QueryActiveAlarms:input_type -> mxaccess_gateway.v1.QueryActiveAlarmsRequest
12, // 171: mxaccess_gateway.v1.MxAccessGateway.OpenSession:output_type -> mxaccess_gateway.v1.OpenSessionReply
14, // 172: mxaccess_gateway.v1.MxAccessGateway.CloseSession:output_type -> mxaccess_gateway.v1.CloseSessionReply
63, // 173: mxaccess_gateway.v1.MxAccessGateway.Invoke:output_type -> mxaccess_gateway.v1.MxCommandReply
83, // 174: mxaccess_gateway.v1.MxAccessGateway.StreamEvents:output_type -> mxaccess_gateway.v1.MxEvent
93, // 175: mxaccess_gateway.v1.MxAccessGateway.AcknowledgeAlarm:output_type -> mxaccess_gateway.v1.AcknowledgeAlarmReply
95, // 176: mxaccess_gateway.v1.MxAccessGateway.StreamAlarms:output_type -> mxaccess_gateway.v1.AlarmFeedMessage
91, // 177: mxaccess_gateway.v1.MxAccessGateway.QueryActiveAlarms:output_type -> mxaccess_gateway.v1.ActiveAlarmSnapshot
171, // [171:178] is the sub-list for method output_type
164, // [164:171] is the sub-list for method input_type
164, // [164:164] is the sub-list for extension type_name
164, // [164:164] is the sub-list for extension extendee
0, // [0:164] is the sub-list for field type_name
}
func init() { file_mxaccess_gateway_proto_init() }
@@ -9752,8 +9833,9 @@ func file_mxaccess_gateway_proto_init() {
(*AlarmFeedMessage_SnapshotComplete)(nil),
(*AlarmFeedMessage_Transition)(nil),
(*AlarmFeedMessage_ProviderStatus)(nil),
(*AlarmFeedMessage_SnapshotStatus)(nil),
}
file_mxaccess_gateway_proto_msgTypes[88].OneofWrappers = []any{
file_mxaccess_gateway_proto_msgTypes[89].OneofWrappers = []any{
(*MxValue_BoolValue)(nil),
(*MxValue_Int32Value)(nil),
(*MxValue_Int64Value)(nil),
@@ -9765,7 +9847,7 @@ func file_mxaccess_gateway_proto_init() {
(*MxValue_RawValue)(nil),
(*MxValue_SparseArrayValue)(nil),
}
file_mxaccess_gateway_proto_msgTypes[89].OneofWrappers = []any{
file_mxaccess_gateway_proto_msgTypes[90].OneofWrappers = []any{
(*MxArray_BoolValues)(nil),
(*MxArray_Int32Values)(nil),
(*MxArray_Int64Values)(nil),
@@ -9781,7 +9863,7 @@ func file_mxaccess_gateway_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_mxaccess_gateway_proto_rawDesc), len(file_mxaccess_gateway_proto_rawDesc)),
NumEnums: 10,
NumMessages: 101,
NumMessages: 102,
NumExtensions: 0,
NumServices: 1,
},
+10
View File
@@ -125,6 +125,16 @@ deletions from it. It is set-level degraded status, not a comment on the
record's own fidelity, and is distinct from `getDegraded()` (the subtag
fallback provider).
`streamAlarms` also carries that completeness verdict at feed level, as a
message whose `getPayloadCase()` is `SNAPSHOT_STATUS` and whose
`getSnapshotStatus().getTruncated()` is true while the monitor's cached set
derives from a truncated fetch. One arrives at stream open (after the
`PROVIDER_STATUS` frame, before the cached `ACTIVE_ALARM` frames) so a late
joiner learns the current verdict, then one on every verdict change — including
the clearing frame sent when the gateway's alarm monitor restarts and drops a
truncated verdict. Track it if you need set completeness on a live feed without
polling `queryActiveAlarms`.
## Write Semantics And Common Pitfalls
These are MXAccess parity behaviors that surprise new callers. The gateway
File diff suppressed because it is too large Load Diff
@@ -47,8 +47,9 @@ import java.util.function.Consumer;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
import mxaccess_gateway.v1.MxaccessGateway.AlarmProviderStatus;
import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
import mxaccess_gateway.v1.MxaccessGateway.AlarmProviderStatus;
import mxaccess_gateway.v1.MxaccessGateway.AlarmSnapshotStatus;
import mxaccess_gateway.v1.MxaccessGateway.BulkReadResult;
import mxaccess_gateway.v1.MxaccessGateway.BulkWriteResult;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
@@ -2282,7 +2283,8 @@ public final class MxGatewayCli implements Callable<Integer> {
/**
* Renders one {@link AlarmFeedMessage} in the CLI's plain-text output
* style, distinguishing the active-alarm snapshot, snapshot-complete
* sentinel, and transition cases of the message's {@code payload} oneof.
* sentinel, transition, provider-status, and snapshot-status cases of the
* message's {@code payload} oneof.
*/
private static String formatAlarmFeedMessage(AlarmFeedMessage message) {
return switch (message.getPayloadCase()) {
@@ -2307,6 +2309,10 @@ public final class MxGatewayCli implements Callable<Integer> {
"provider-status mode=%s degraded=%b reason=%s",
status.getMode().name(), status.getDegraded(), status.getReason());
}
case SNAPSHOT_STATUS -> {
AlarmSnapshotStatus status = message.getSnapshotStatus();
yield String.format("snapshot-status truncated=%b", status.getTruncated());
}
case PAYLOAD_NOT_SET -> "unknown";
};
}
+10
View File
@@ -122,6 +122,16 @@ poll. Treat the set as possibly incomplete rather than reconciling deletions
from it. It is set-level degraded status, not a comment on the record's own
fidelity, and is distinct from `degraded` (the subtag fallback provider).
`stream_alarms` also carries that completeness verdict at feed level, as a
message whose `WhichOneof("payload")` is `snapshot_status` and whose
`snapshot_status.truncated` is true while the monitor's cached set derives from
a truncated fetch. One arrives at stream open (after the `provider_status`
frame, before the cached `active_alarm` frames) so a late joiner learns the
current verdict, then one on every verdict change — including the clearing frame
sent when the gateway's alarm monitor restarts and drops a truncated verdict.
Track it if you need set completeness on a live feed without polling
`query_active_alarms`.
Canceling a Python task cancels the client-side gRPC call or stream wait. It
does not abort an in-flight MXAccess COM call inside the worker process.
File diff suppressed because one or more lines are too long
+9
View File
@@ -132,6 +132,15 @@ poll. Treat the set as possibly incomplete rather than reconciling deletions
from it. It is set-level degraded status, not a comment on the record's own
fidelity, and is distinct from `degraded` (the subtag fallback provider).
`AlarmFeedStream` also carries that completeness verdict at feed level, as an
`alarm_feed_message::Payload::SnapshotStatus` frame whose `truncated` field is
true while the monitor's cached set derives from a truncated fetch. One arrives
at stream open (after the `ProviderStatus` frame, before the cached
`ActiveAlarm` frames) so a late joiner learns the current verdict, then one on
every verdict change — including the clearing frame sent when the gateway's
alarm monitor restarts and drops a truncated verdict. Track it if you need set
completeness on a live feed without polling `query_active_alarms`.
The session helpers keep MXAccess handles visible:
```rust
+28 -1
View File
@@ -2231,7 +2231,7 @@ fn event_value_to_json(value: &ProtoMxValue) -> Value {
}
/// Render a streamed [`AlarmFeedMessage`] as a terse one-line summary that
/// distinguishes the four `payload` oneof cases.
/// distinguishes the five `payload` oneof cases.
fn alarm_feed_message_summary(message: &AlarmFeedMessage) -> String {
match &message.payload {
Some(alarm_feed_message::Payload::ActiveAlarm(snapshot)) => {
@@ -2259,6 +2259,9 @@ fn alarm_feed_message_summary(message: &AlarmFeedMessage) -> String {
status.reason
)
}
Some(alarm_feed_message::Payload::SnapshotStatus(status)) => {
format!("snapshot-status truncated={}", status.truncated)
}
None => "(empty)".to_owned(),
}
}
@@ -2308,6 +2311,11 @@ fn alarm_feed_message_to_json(message: &AlarmFeedMessage) -> Value {
})),
}
}),
Some(alarm_feed_message::Payload::SnapshotStatus(status)) => json!({
"snapshotStatus": {
"truncated": status.truncated,
}
}),
None => Value::Null,
}
}
@@ -2961,4 +2969,23 @@ mod tests {
assert_eq!(provider["reason"], "alarmmgr unavailable");
assert_eq!(provider["since"]["seconds"], 1_777_995_000_i64);
}
#[test]
fn alarm_feed_snapshot_status_renders_in_summary_and_json() {
use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
alarm_feed_message, AlarmFeedMessage, AlarmSnapshotStatus,
};
let message = AlarmFeedMessage {
payload: Some(alarm_feed_message::Payload::SnapshotStatus(
AlarmSnapshotStatus { truncated: true },
)),
};
let summary = super::alarm_feed_message_summary(&message);
assert_eq!(summary, "snapshot-status truncated=true");
let value = super::alarm_feed_message_to_json(&message);
assert_eq!(value["snapshotStatus"]["truncated"], true);
}
}
@@ -1018,6 +1018,10 @@ message AlarmFeedMessage {
// Provider-mode status. Emitted once on stream open and again on every
// failover/failback so late joiners learn the current mode immediately.
AlarmProviderStatus provider_status = 4;
// Snapshot-completeness status. Emitted once on stream open and again on
// every change of the truncation verdict, so late joiners learn whether the
// feed's active-alarm set may be incomplete.
AlarmSnapshotStatus snapshot_status = 5;
}
}
@@ -1028,6 +1032,23 @@ message AlarmProviderStatus {
google.protobuf.Timestamp since = 4;
}
// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
// (after the initial provider_status frame, before the cached active_alarm
// frames) so late joiners learn the current verdict, and again on every change
// of the truncation verdict — when a reconcile reports a different verdict, and
// when the gateway's alarm monitor restarts and drops a truncated verdict with
// the cache generation it described (feed subscribers outlive that monitor
// session, so they are sent the clearing frame). Mirrors the per-record
// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
// consumers can reason about completeness without polling QueryActiveAlarms.
message AlarmSnapshotStatus {
// True while the monitor's cached active-alarm set derives from a truncated
// (capped) worker fetch — the set may be missing alarms. Distinct from
// provider degradation (AlarmProviderStatus.degraded), which describes the
// fidelity of the records rather than the completeness of the set.
bool truncated = 1;
}
message MxStatusProxy {
// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
// (a 16-bit signed value in the COM struct, widened to int32 on the
+11
View File
@@ -764,6 +764,17 @@ The v2 8-arg method returns -55 on this AVEVA build regardless of
operator-identity inputs — looks like a stub. The v1 6-arg method
works. Production `WnWrapAlarmConsumer.AcknowledgeByName` calls the
6-arg overload and discards the proto's `domain` + `full_name` fields.
**Correction, 2026-08-18** (`docs/AlarmProbeFindings.md`). Both halves
of the paragraph above are narrower than they read. The -55 tracks the
*consumer*, not the overload: against the `SetXmlAlarmQuery`-applied
reader both the 6-arg and 8-arg forms return -55, and against the
ack-only consumer both return 0. And "works" means "returns 0" only —
a probe that raised a real alarm and then acked it six ways 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 this rig the wnwrap ack is accepted and then inert, so a zero
return code must not be read as an applied acknowledgement.
The proto contract keeps the 8 fields for forward compatibility if
AVEVA fixes the v2 method later.
+176 -32
View File
@@ -1,7 +1,8 @@
# Alarm Probe Findings
`WnWrapAlarmConsumer` rests on two assumptions that no unit test can settle, because both
are properties of AVEVA's alarm provider rather than of our code:
`WnWrapAlarmConsumer` rested on two assumptions that no unit test can settle, because both
are properties of AVEVA's alarm provider rather than of our code. The second is now settled
and one half of the first is; the questions are stated here as they were asked:
1. **GUID identity.** The snapshot diff in `ComputeTransitions` keys on the alarm record's
`GUID`. If wnwrap mints a fresh GUID when an alarm changes state, a single
@@ -13,23 +14,27 @@ are properties of AVEVA's alarm provider rather than of our code:
rather than the records-in-reply count, truncation detection can become exact instead of
conservative, and the bounded staleness `ApplySnapshotUpdate` accepts goes away.
This document records what a live probe run against the dev rig (`DESKTOP-6JL3KKO`,
2026-08-17) could and could not establish, so the next attempt starts from the blocker
rather than rediscovering it.
This document records what live probe runs against the dev rig (`DESKTOP-6JL3KKO`,
2026-08-17 and 2026-08-18) could and could not establish, so the next attempt starts from
the blocker rather than rediscovering it.
## Outcome
| Question | Status |
|---|---|
| GUID stable across polls and `ALM → RTN` | Answered — yes, by the 2026-05-01 capture in `AlarmClientDiscovery.md` |
| GUID stable across `UNACK → ACK`, and across clear-then-re-raise | **Open** |
| `COUNT` = total active vs records-in-reply under a capped fetch | **Open** |
| GUID stable across polls and `ALM → RTN` | Answered — yes (2026-05-01 capture in `AlarmClientDiscovery.md`, reconfirmed 2026-08-18) |
| GUID stable across clear-then-re-raise | Answered 2026-08-18 — **no**, a re-raise mints a new GUID |
| GUID stable across `UNACK → ACK` | **Open** — the rig cannot be driven into an acknowledged state at all |
| `COUNT` = total active vs records-in-reply under a capped fetch | Answered 2026-08-18 — **records in the reply** |
Both open questions are blocked by the same thing: the rig has no active alarm and cannot
be driven into one over MXAccess, so there is no alarm instance whose GUID can be followed
through an acknowledge and no population large enough to overflow a capped fetch.
The 2026-08-17 run below is kept because it is the record of the wrong-verb blocker. The
2026-08-18 run cleared that blocker with `AuthenticateUser` + `WriteSecured` and answered
two of the three questions; the acknowledge leg is now blocked on something narrower and
different, described in "Second attempt".
## Why The Rig Cannot Raise An Alarm
## First attempt (2026-08-17): plain `Write`
### Why The Rig Could Not Raise An Alarm
The rig is otherwise healthy, which is what makes the blocker specific rather than a
general "nothing works":
@@ -64,7 +69,8 @@ for the whole probe session.
### Unblocking
Any one of these makes both questions answerable, in rough order of cost:
Any one of these makes both questions answerable, in rough order of cost. The second one
is what the 2026-08-18 run did, and it worked:
- Re-enable the System Platform script that flips `TestMachine_001.TestAlarm001`
(referenced throughout `AlarmClientDiscovery.md`). It writes from inside the engine, so
@@ -78,7 +84,7 @@ Three separate objects are wired to the same alarm UDA name, so once writes land
`maxAlmCnt` of 1 or 2 forces truncation against three active alarms and answers the `COUNT`
question in the same run.
## Evidence
### Evidence
Snapshot payload, identical at every cap (1, 2, and 1024) and at every poll across the
~100-second session:
@@ -98,26 +104,164 @@ The probe used for the run was a throwaway file in the windev CI clone
harness it was modelled on is
`src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs`.
## Implications
## Second attempt (2026-08-18): secured-write verb
### Transition identity
The findings above named the fix in their own "Unblocking" list, and it holds:
`AuthenticateUser` + `WriteSecured` raises the alarms that plain `Write` could not touch.
The rig's alarm attributes are not unreachable — they are secured, and the 2026-08-17 probe
used the wrong verb.
`ComputeTransitions` keying on GUID is safe for the raise and clear legs, which is the
evidence `AlarmClientDiscovery.md` already carries. The acknowledge leg — the one where a
re-minted GUID would corrupt the feed, because an ack is the state change most likely to
create a new record in a provider that models acknowledgement as a separate event — is
still assumed rather than observed. Nothing here justifies changing the diff, but the
assumption should not be described in code as established.
### Method
### Truncation detection
Three throwaway probes in the windev CI clone (`C:\build\mxaccessgw-ci`, detached at
`ab3ff16`), each an x86 net48 xUnit fact running on a dedicated STA with a
`PeekMessage`/`DispatchMessage` pump so MXAccess events deliver:
`IsTruncatedFetch` stays as written. Tightening it to an exact test requires knowing that
`COUNT` reports the total, and this run cannot show that. The conservative rule keeps its
justification: at the cap, treating a complete fetch as truncated costs one poll of
staleness, while treating a truncated fetch as complete broadcasts clears for every alarm
past the cap.
1. secured write + both GUID legs + the capped-fetch question,
2. the acknowledge surface, exhaustively,
3. the acknowledge question re-asked through an independent observer.
The one substantive correction is to the phrasing rather than the logic. The reply is not
featureless — it carries a `COUNT` attribute the parser currently ignores. Whether that
attribute is a usable "more available" signal is unverified, not absent, and the comments
in `WnWrapAlarmConsumer` now say so.
All three used `LMXProxyServerClass` directly for the write path and a
`wwAlarmConsumerClass` pair — a reader with `SetXmlAlarmQuery` applied and an ack-only
consumer without it — mirroring `WnWrapAlarmConsumer`'s own two-consumer arrangement. The
subscription was `\\DESKTOP-6JL3KKO\Galaxy!TestArea`, matching the 2026-08-17 run.
`MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER` / `_PASSWORD` are set at neither machine nor
user scope on the box, so the probes used the default rig identity, `Administrator` with an
empty password. All three files were deleted afterwards and the clone is a clean tree.
### The secured write lands
Same session, same item, one after the other — the control reproduces 2026-08-17 exactly
and the secured verb succeeds:
```
Write 'TestMachine_001.TestAlarm001' <- True
WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=0 category=SecurityError detectedBy=RespondingAutomationObject detail=1008 text=]
observed 'TestMachine_001.TestAlarm001' = False
AuthenticateUser -> userId=1
WriteSecured 'TestMachine_001.TestAlarm001' <- True currentUserId=1 verifierUserId=0
DATA-CHANGE hItem=1 value=True quality=192
WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=-1 category=Ok detectedBy=RespondingAutomationObject detail=0 text=]
observed 'TestMachine_001.TestAlarm001' = True
```
`AuthenticateUser("Administrator", "")` resolves to user id 1, matching the value
`WorkerLiveMxAccessSmokeTests` records. No verifier is needed: `verifierUserId=0` is
accepted. All six subsequent raise/clear writes across the three objects behaved the same
way, so the path is reliable rather than a one-off.
### Q2 — `ALARM_RECORDS/@COUNT` reports the reply, not the total
With all three `TestMachine_00{1,2,3}.TestAlarm001` alarms active, the same subscription
fetched at three caps back to back:
```
FETCH three-active cap=1024 ALARM_RECORDS/@COUNT=3 elementCount=3
FETCH three-active cap=1 ALARM_RECORDS/@COUNT=1 elementCount=1
FETCH three-active cap=2 ALARM_RECORDS/@COUNT=2 elementCount=2
```
`@COUNT` tracked the cap, not the population. It equals the element count in every reply,
including the two that are provably truncated — the galaxy held three active alarms while
`@COUNT` read 1 and 2. The attribute therefore carries no "more available" information, and
the hypothesis that it might report the total is refuted rather than merely untested.
`IsTruncatedFetch` stays exactly as written, and this is now settled rather than deferred:
there is no exact truncation signal to switch to. Tightening it is **not** a future task.
The conservative rule keeps its original justification — at the cap, treating a complete
fetch as truncated costs one poll of staleness, while treating a truncated fetch as
complete broadcasts clears for every alarm past the cap.
### Q1 — clear-then-re-raise mints a new GUID
`TestMachine_001.TestAlarm001` was raised, cleared, and re-raised through `WriteSecured`,
with a full snapshot fetch after each leg:
```
after-raise TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_ALM VALUE=true
after-clear-001 TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_RTN VALUE=false
after-reraise TAG=TestMachine_001.TestAlarm001 GUID=2394FEAA94774154AAA97C3AC29CA51E STATE=UNACK_ALM VALUE=true
```
Two facts, one confirming and one new. The `ALM → RTN` leg holds its GUID, reconfirming the
2026-05-01 capture against a write-driven rather than script-driven transition. The re-raise
does not: the returned record keeps the old GUID and the new active condition arrives as a
separate record under a fresh one.
That is the correct reading for `ComputeTransitions` rather than a problem for it. The old
GUID leaving the active set is a real Clear and the new GUID is a real Raise, because they
are two alarm instances. The diff needed no change.
### Q1 — the acknowledge leg is still unobserved, for a new reason
The blocker moved. The rig can now be driven into an alarm; it cannot be driven out of
`UNACK_ALM`. Every acknowledge surface `wwAlarmConsumerClass` exposes was tried against a
freshly raised alarm, each followed by 16 seconds of polling:
| Attempt | Result |
|---|---|
| `AlarmAckByName` 6-arg on the ack-only consumer, node = machine | `rc=0`, state unchanged |
| same, node empty | `rc=0`, state unchanged |
| same, name as `Galaxy!TestArea.TestMachine_001.TestAlarm001` | `rc=0`, state unchanged |
| same, operator name `Administrator` | `rc=0`, state unchanged |
| `AlarmAckByName` 6-arg on the reader consumer (`SetXmlAlarmQuery` applied) | `rc=-55`, state unchanged |
| `AlarmAckByName` 8-arg (v2) on the ack-only consumer | `rc=0`, state unchanged |
The `rc=-55` on the `SetXmlAlarmQuery`-applied consumer reproduces the 2026-05-01 finding
that motivated the two-consumer split, so the ack-only consumer was correctly provisioned;
the `rc=0` returns are the ones that go nowhere.
A third probe removed the last doubt by watching the alarm extension's own `.Acked`
attribute over MXAccess — an observer entirely independent of the wnwrap snapshot:
```
VALUES after-raise: 'TestMachine_001.TestAlarm001.Acked' = False
AlarmAckByName -> rc=0
VALUES after-wnwrap-ack: 'TestMachine_001.TestAlarm001.Acked' = False
XML after-wnwrap-ack: STATE=UNACK_ALM OPRNAME= OPRNODE=
```
Nothing moves: not the extension attribute, not the snapshot `STATE`, not `OPERATOR_NAME`.
`rc=0` from wnwrap means the call was accepted, not that an acknowledgement was applied.
Acknowledging by writing the extension attribute directly is not an alternative — the
attribute is not writable, and says so with an operational rather than a security failure,
for both verbs:
```
WriteSecured 'TestMachine_001.TestAlarm001.Acked' <- true
WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007]
Write 'TestMachine_001.TestAlarm001.Acked' <- true
WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007]
```
`detail=1007` from the responding automation object, unchanged by authentication, is a
read-only attribute rather than a permission refusal — the `1008` `SecurityError` the alarm
UDA itself returns is what a permission refusal looks like on this rig. So the ack has no
MXAccess-side entry point and the wnwrap-side entry point is inert.
#### Remaining unblock paths for the acknowledge leg
- Acknowledge from the System Platform side — the IDE's alarm client, InTouch, or an
ArchestrA graphic bound to the alarm — and watch the snapshot from a running probe. This
proves whether wnwrap's `STATE` ever reports `ACK_ALM` at all, which is the actual
question; the ack API being inert may be a wnwrap defect layered on top of a snapshot
that would report the state correctly.
- Check whether `alarmmgr` on this rig is configured with an alarm-acknowledgement security
requirement that the wnwrap consumer, which passes an operator *name* string and no
authenticated identity, cannot meet. If so, ack over wnwrap is not merely untested here
but unavailable by configuration, and the gateway's `AcknowledgeByName` path needs the
same treatment on any customer galaxy configured that way.
- If neither lands, the acknowledge leg stays assumed. It is worth restating that this is a
documentation gap, not a correctness one: a re-minted GUID on acknowledge would produce a
spurious Clear plus a spurious Raise, which is the same shape the now-observed re-raise
behaviour produces and which `ComputeTransitions` already handles as two instances.
### Rig state left behind
The three `TestMachine_00{1,2,3}.TestAlarm001` UDAs are back to `false` and their
`.InAlarm` subtags read `false`, but each leaves a `UNACK_RTN` record in the wnwrap
snapshot, since nothing can acknowledge them away. `SnapshotActiveAlarms` counts only
`UNACK_ALM` and `ACK_ALM` as active, so these are inert for the gateway; they will clear on
the next `alarmmgr` restart.
+4 -1
View File
@@ -197,7 +197,10 @@ Tags are set at key creation with
`apikey create-key --dashboard-tags team-a,team-b` (repeatable; segments are
trimmed and de-duplicated ordinal-ignore-case). The dashboard API Keys page sets
them too: its create form has a **Dashboard tags** field alongside the data-access
constraints, split on the same separators the other constraint fields use.
constraints, split on the same separators the other constraint fields use and
de-duplicated ordinal-ignore-case as the CLI does. The two differ on one point: a
stray separator drops an empty segment silently on the form rather than failing
the command, because a browser form has no exit code to fail with.
That page's constraints column names `dashboard_tags` like any other member. It
has to: `IsEmpty` counts the tags, so a key whose only recorded policy is a
+8 -7
View File
@@ -226,13 +226,14 @@ payload carries the flag as well because a prefix filter (or an empty galaxy) ca
leave zero records, and a truncated fetch with nothing to report still has to say
so.
The **detection heuristic is unchanged**: `IsTruncatedFetch` remains
`fetchedRecordCount >= maxAlarmsPerFetch`. The live probe run for this work could
not verify whether `ALARM_RECORDS/@COUNT` reports the total active count or only
the records in the reply (`docs/AlarmProbeFindings.md`), and an exact-looking
signal derived from an unverified attribute is worse than an honest heuristic —
it would read as precise while being wrong in the one direction that matters.
Switching to `@COUNT` stays blocked on probe evidence.
The **detection heuristic is unchanged, and now permanently**: `IsTruncatedFetch`
remains `fetchedRecordCount >= maxAlarmsPerFetch`. The 2026-08-18 probe settled
what the earlier run could not: with three alarms active, `ALARM_RECORDS/@COUNT`
read 1 at a cap of 1 and 2 at a cap of 2, so it reports the records in the reply
rather than the total active count (`docs/AlarmProbeFindings.md`). There is no
exact signal to switch to — `@COUNT` cannot distinguish a capped fetch from a
complete one — so the heuristic is the design rather than a placeholder waiting
on evidence.
The flag is **not latched**. It is replaced by each fetch's verdict, so the first
sub-cap fetch clears it, and `GatewayAlarmMonitor.ClearCache` drops it with the
+1 -1
View File
@@ -192,7 +192,7 @@ events (a "gap") and must re-snapshot; whatever is still retained is replayed.
| `MxGateway:Dashboard:SnapshotIntervalMilliseconds` | `1000` | Dashboard snapshot refresh interval used by the snapshot SignalR hub and the pages that subscribe to it. |
| `MxGateway:Dashboard:RecentFaultLimit` | `100` | Maximum number of fault summaries projected into each dashboard snapshot. |
| `MxGateway:Dashboard:RecentSessionLimit` | `200` | Maximum number of session summaries projected into each dashboard snapshot. |
| `MxGateway:Dashboard:ShowTagValues` | `false` | Controls whether tag values reach the dashboard at all. `false` (the default) suppresses them on all three seams that carry one: (1) the **events hub mirror** — `DashboardEventBroadcaster` blanks `MxEvent.value` plus the alarm body's `current_value`/`limit_value` from a deep-cloned copy before it reaches any hub subscriber (see `docs/GatewayDashboardDesign.md`'s `EventsHub` row); (2) the **alarms hub** — `AlarmsHubPublisher` clears `current_value`/`limit_value` from a deep-cloned copy of each `AlarmFeedMessage`, on both value-bearing payload arms (`transition` and `active_alarm`), before broadcasting to `/hubs/alarms`; (3) the **`/browse` live-value panel** — `DashboardLiveDataService` substitutes the literal `[redacted]` for each tag's value text. Both hub redactions clone: the source `MxEvent` is shared with the gRPC event stream and the replay ring, and the source `AlarmFeedMessage` fans out to gRPC `StreamAlarms` subscribers, so neither is mutated in place and **no gRPC client is affected by this flag** — it is a dashboard-display control only. Everything that is not the value survives on every seam: tag reference, alarm reference/severity/state/operator, data type, quality, status, and timestamps still render, so the dashboard stays diagnostic without disclosing process values. This is one of two independent layers: `IDashboardSessionAcl` decides *which* sessions a caller may subscribe to at all (see `GroupToTag` / `UntaggedSessionVisibility` below), while this flag decides what a permitted subscriber sees. Setting it `true` exposes tag values to everyone the ACL admits — every Administrator, plus each Viewer holding a matching tag — and, because the alarms hub is session-less, exposes alarm values to every dashboard client that can reach `/hubs/alarms`. |
| `MxGateway:Dashboard:ShowTagValues` | `false` | Controls whether tag values reach the dashboard at all. `false` (the default) suppresses them on all three seams that carry one: (1) the **events hub mirror** — `DashboardEventBroadcaster` blanks `MxEvent.value` plus the alarm body's `current_value`/`limit_value` from a deep-cloned copy before it reaches any hub subscriber (see `docs/GatewayDashboardDesign.md`'s `EventsHub` row); (2) the **alarms hub** — `AlarmsHubPublisher` clears `current_value`/`limit_value` from a deep-cloned copy of each `AlarmFeedMessage`, on both value-bearing payload arms (`transition` and `active_alarm`), before broadcasting to `/hubs/alarms`; (3) the **`/browse` live-value panel** — `DashboardLiveDataService` substitutes the literal `[redacted]` for the value text of each **successfully read** tag; a failed read keeps its `-` placeholder (there was no value to suppress, and the row's error is left untouched, so the two cannot contradict each other). Both hub redactions clone: the source `MxEvent` is shared with the gRPC event stream and the replay ring, and the source `AlarmFeedMessage` fans out to gRPC `StreamAlarms` subscribers, so neither is mutated in place and **no gRPC client is affected by this flag** — it is a dashboard-display control only. Everything that is not the value survives on every seam: tag reference, alarm reference/severity/state/operator, data type, quality, status, and timestamps still render, so the dashboard stays diagnostic without disclosing process values. This is one of two independent layers: `IDashboardSessionAcl` decides *which* sessions a caller may subscribe to at all (see `GroupToTag` / `UntaggedSessionVisibility` below), while this flag decides what a permitted subscriber sees. Setting it `true` exposes tag values to everyone the ACL admits — every Administrator, plus each Viewer holding a matching tag — and, because the alarms hub is session-less, exposes alarm values to every dashboard client that can reach `/hubs/alarms`. |
| `MxGateway:Dashboard:GroupToRole` | _(empty)_ | LDAP group → dashboard role mapping. Keys are LDAP group names (short CN or full DN — leading-RDN match). Values must be `Administrator` (read/write, API-key CRUD) or `Viewer` (read-only) — matched ordinally by the startup validator, so the spelling is exact and `Admin` is rejected. A user whose LDAP groups don't intersect this map cannot sign in; with no mapping at all, only the loopback bypass admits anyone. |
| `MxGateway:Dashboard:GroupToTag` | _(empty)_ | LDAP group → dashboard visibility tags. Keys follow the same convention as `GroupToRole` (short CN or full DN — leading-RDN match, case-insensitive); values are tag lists. A dashboard user's granted tag set is the union over the groups they belong to; an unmapped group contributes nothing. **Visibility only:** tags scope which sessions' event streams a Viewer may observe on the dashboard — they never grant or deny data access, which stays with the API key's scopes and constraints. Independent of `GroupToRole`: a group may appear in either map, both, or neither. Empty (the default) means Viewers hold no tags, so under the default `UntaggedSessionVisibility` they observe no session's events. |
| `MxGateway:Dashboard:UntaggedSessionVisibility` | `AdminOnly` | Who may observe a session that carries no tags (its owning API key declared none). `AdminOnly` (default, fail-closed) restricts untagged sessions to dashboard Administrators. `AllViewers` shows them to every Viewer — opt-in for a single-tenant deployment that wants the pre-tag behaviour. Administrators always see every session regardless of tags. |
+6 -1
View File
@@ -273,7 +273,7 @@ bearer). Each hub class is `[Authorize(Policy = HubClientsPolicy)]`.
| Hub | Path | Producer | Payload | Routing |
|---|---|---|---|---|
| `DashboardSnapshotHub` | `/hubs/snapshot` | `DashboardSnapshotPublisher` (BackgroundService consuming `IDashboardSnapshotService.WatchSnapshotsAsync`) | `DashboardSnapshot` | Sent to all connected clients on every snapshot tick, but only while at least one client is connected (see "Idle gating" below); new connections receive the current snapshot synchronously in `OnConnectedAsync`. |
| `AlarmsHub` | `/hubs/alarms` | `AlarmsHubPublisher` (BackgroundService consuming `IGatewayAlarmService.StreamAsync(filter: null)`) | `AlarmFeedMessage` (`active_alarm` / `snapshot_complete` / `transition`) | Connected clients auto-join `__alarms__`; all clients receive every message. Publisher auto-reconnects every 5s on stream faults. The alarm value fields (`current_value` / `limit_value`) are stripped from a deep-cloned copy of the message when `Dashboard:ShowTagValues` is false (the default) — on both value-bearing payload arms, so neither a live transition nor a snapshot record leaks a process value; every other field still renders. The source message is never mutated: it fans out from the same feed to gRPC `StreamAlarms` subscribers, which this dashboard-display flag does not govern. Arms carrying no value are forwarded as-is, uncloned. |
| `AlarmsHub` | `/hubs/alarms` | `AlarmsHubPublisher` (BackgroundService consuming `IGatewayAlarmService.StreamAsync(filter: null)`) | `AlarmFeedMessage` (`active_alarm` / `snapshot_complete` / `transition` / `provider_status` / `snapshot_status`) | Connected clients auto-join `__alarms__`; all clients receive every message. Publisher auto-reconnects every 5s on stream faults. The alarm value fields (`current_value` / `limit_value`) are stripped from a deep-cloned copy of the message when `Dashboard:ShowTagValues` is false (the default) — on both value-bearing payload arms, so neither a live transition nor a snapshot record leaks a process value; every other field still renders. The source message is never mutated: it fans out from the same feed to gRPC `StreamAlarms` subscribers, which this dashboard-display flag does not govern. Arms carrying no value are forwarded as-is, uncloned. |
| `EventsHub` | `/hubs/events` | `DashboardEventBroadcaster` invoked by each session's internal dashboard-mirror subscriber on its `SessionEventDistributor` (registered when the session becomes Ready) | `MxEvent` | Clients call `SubscribeSession(sessionId)` to join `session:{id}`, which also registers them in `EventsHubViewerRegistry` — the mirror is gated on that registry, which counts hub and in-process viewers alike (see "Mirror gating" below). The dashboard is a first-class distributor subscriber, so it receives the session's events whether or not a gRPC client is streaming. It sees RAW session events — not the per-gRPC-subscriber `AfterWorkerSequence` filtering that `EventStreamService` applies at its own boundary — because the dashboard is a separate LDAP-authenticated monitoring view meant to show the session's full event activity. Tag values are stripped from the mirrored `MxEvent` copy by `DashboardEventBroadcaster` when `Dashboard:ShowTagValues` is false (the default) — event metadata (tag reference, quality, status, timestamps) still renders, but the value fields are blanked, so no value leaks through this seam. `SubscribeSession` is gated by `IDashboardSessionAcl` (SEC-25 / TST-15): a denied caller gets a `HubException`, is not joined to the group, and is not registered as a viewer, so the mirror stays off for a session nobody is legitimately watching. The same ACL gates the in-process seam the session-details page uses, so neither path is the weaker one. Value redaction remains an independent layer — it bounds what a *permitted* subscriber sees. |
### Default cadences
@@ -652,6 +652,11 @@ Show read-only effective configuration:
- auth mode,
- SQLite auth database path with sensitive parts redacted if needed,
- dashboard enabled state,
- the LDAP group mappings that decide what a signed-in user may see —
`Dashboard:GroupToRole` (group → `Administrator` / `Viewer`) and
`Dashboard:GroupToTag` (group → the session tags it grants),
- `Dashboard:UntaggedSessionVisibility`, which decides who sees a session whose
owning API key carries no dashboard tags,
- protocol version.
Do not show API key secrets or pepper values.
@@ -1,12 +1,12 @@
{
"planPath": "docs/plans/2026-08-17-followup-closeout.md",
"tasks": [
{"id": 1, "subject": "Task 1: Feed-level alarm-truncation signal (proto + gateway)", "status": "pending"},
{"id": 2, "subject": "Task 2: Five-client rollout of snapshot_status", "status": "pending", "blockedBy": [1, 3, 4]},
{"id": 3, "subject": "Task 3: Complete ShowTagValues coverage — alarms hub + /browse", "status": "pending"},
{"id": 1, "subject": "Task 1: Feed-level alarm-truncation signal (proto + gateway)", "status": "completed", "commits": ["fccf753", "312e072"], "review": "Spec compliant; code review Approved, proto-comment Minor fixed"},
{"id": 2, "subject": "Task 2: Five-client rollout of snapshot_status", "status": "completed", "commits": ["c748361", "4d4f443"], "review": "Spec compliant; code review Approved, Java import + Rust render-test nits fixed; full five-toolchain matrix ran locally"},
{"id": 3, "subject": "Task 3: Complete ShowTagValues coverage — alarms hub + /browse", "status": "completed", "commits": ["eff17d1", "1ea6f60", "e50ec49"], "review": "Spec compliant; code review Approved, failed-read placeholder fix re-review Approved"},
{"id": 4, "subject": "Task 4: Codegen guard — reverse-direction Check 3 + stale-note correction", "status": "completed", "commit": "711793f", "review": "Approved (code review, no issues)"},
{"id": 5, "subject": "Task 5: Settings page — GroupToTag + UntaggedSessionVisibility rows", "status": "completed", "commits": ["094f2ff", "ff16c69"], "review": "Approved; comparer-propagation fix applied and re-verified"},
{"id": 6, "subject": "Task 6: ApiKeysPage — DashboardTags in ConstraintText + create-form input", "status": "pending"},
{"id": 6, "subject": "Task 6: ApiKeysPage — DashboardTags in ConstraintText + create-form input", "status": "completed", "commits": ["c037d99", "fab600d"], "review": "Spec compliant; code review Approved after dedupe/help-text fix, re-review Approved"},
{"id": 7, "subject": "Task 7: Wnwrap alarm-probe retry — secured-write path (windev)", "status": "pending"},
{"id": 8, "subject": "Task 8: Windev full verification", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7]},
{"id": 9, "subject": "Task 9: Bookkeeping — follow-ups closure + plan record", "status": "pending", "blockedBy": [8]}
+4 -3
View File
@@ -265,9 +265,10 @@ means "this active set may be incomplete", not "this record is unreliable" —
it is independent of the subtag-fallback `degraded` field above. It is not
latched: the first fetch that comes back under the cap is complete, restores
absence authority, and clears it. Detection remains the record-count heuristic;
the reply's `ALARM_RECORDS/@COUNT` attribute would make the test exact only if
it reported the total active count rather than the records in the reply, which
a live probe could not discriminate (see `docs/AlarmProbeFindings.md`).
the reply's `ALARM_RECORDS/@COUNT` attribute would have made the test exact only
if it reported the total active count, and the 2026-08-18 live probe observed it
reporting the records in the reply instead (see `docs/AlarmProbeFindings.md`), so
there is no exact alternative.
Forced modes are available via `MxGateway:Alarms:Fallback:Mode`:
`ForceAlarmManager` disables failover; `ForceSubtag` forces the standby
@@ -29318,7 +29318,10 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
/// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
/// (after the initial provider_status frame, before the cached active_alarm
/// frames) so late joiners learn the current verdict, and again on every change
/// of the truncation verdict observed at reconcile. Mirrors the per-record
/// of the truncation verdict — when a reconcile reports a different verdict, and
/// when the gateway's alarm monitor restarts and drops a truncated verdict with
/// the cache generation it described (feed subscribers outlive that monitor
/// session, so they are sent the clearing frame). Mirrors the per-record
/// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
/// consumers can reason about completeness without polling QueryActiveAlarms.
/// </summary>
@@ -1035,7 +1035,10 @@ message AlarmProviderStatus {
// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
// (after the initial provider_status frame, before the cached active_alarm
// frames) so late joiners learn the current verdict, and again on every change
// of the truncation verdict observed at reconcile. Mirrors the per-record
// of the truncation verdict — when a reconcile reports a different verdict, and
// when the gateway's alarm monitor restarts and drops a truncated verdict with
// the cache generation it described (feed subscribers outlive that monitor
// session, so they are sent the clearing frame). Mirrors the per-record
// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
// consumers can reason about completeness without polling QueryActiveAlarms.
message AlarmSnapshotStatus {
@@ -121,7 +121,8 @@ else
aria-describedby="dashboardTagsHelp"
@bind="CreateModel.DashboardTags" @bind:event="oninput"></textarea>
<div id="dashboardTagsHelp" class="form-text small">
Comma- or newline-separated; mirrors <code>apikey create-key --dashboard-tags</code>.
Split on the same separators as the constraint fields above, and
de-duplicated case-insensitively — two spellings of a tag are one grant.
Matched case-insensitively against the viewer grants in
<code>Dashboard:GroupToTag</code>. Scopes dashboard event visibility only —
never what the key may read, write, or browse. Empty leaves the key's sessions
@@ -478,7 +479,7 @@ else
{
// Init-only rather than positional (it was bolted onto the record after the
// eight-member constructor shipped), so it is attached here instead.
DashboardTags = ParseList(CreateModel.DashboardTags),
DashboardTags = ParseDashboardTags(CreateModel.DashboardTags),
});
return true;
@@ -575,6 +576,18 @@ else
}
}
// Dashboard tags alone are de-duplicated ordinal-ignore-case, which is how the enforcement site
// compares them — two spellings are one grant, and ApiKeyAdminCommandLineParser.ParseDashboardTags
// collapses them the same way, first spelling winning. Persisting both would not change who can
// see what (GatewaySession holds its tags in a case-insensitive set) but the constraints column
// would report one grant twice, and a security grant that reads as two is the wrong kind of
// wrong on an audit surface. The other five list fields keep ParseList's verbatim behaviour:
// their globs are matched literally, so near-duplicates there are not necessarily the same rule.
private static IReadOnlyList<string> ParseDashboardTags(string? value)
{
return [.. ParseList(value).Distinct(StringComparer.OrdinalIgnoreCase)];
}
private static IReadOnlyList<string> ParseList(string? value)
{
return (value ?? string.Empty)
@@ -111,6 +111,72 @@ public sealed class ApiKeysPageDashboardTagsTests
Assert.False(request.Constraints.HasWriteConstraints);
}
/// <summary>
/// Two spellings of one tag persist as one grant, matching how
/// <c>ApiKeyAdminCommandLineParser.ParseDashboardTags</c> collapses them and how the
/// enforcement site compares them.
/// </summary>
/// <remarks>
/// Enforcement would survive the duplicate — a session holds its tags in a
/// case-insensitive set — but the API Keys page's constraints column would render
/// <c>dashboard_tags=[team-a, TEAM-A]</c> and report one grant as two, which is the
/// reading an audit surface can least afford.
/// </remarks>
[Fact]
public void TryBuildCreateRequest_DeduplicatesDashboardTagsIgnoringCase()
{
ApiKeysPage page = new();
page.CreateModel.DashboardTags = "team-a, TEAM-A, team-a";
bool built = page.TryBuildCreateRequest(out DashboardApiKeyManagementRequest? request, out string? error);
Assert.True(built, error);
Assert.NotNull(request);
// First spelling typed wins, so what the operator wrote is what the audit surface shows.
Assert.Equal(["team-a"], request.Constraints.DashboardTags);
}
/// <summary>
/// A dedupe that spanned fields would be a bug of its own: the five glob lists are matched
/// literally, so near-duplicates there are not necessarily the same rule and must survive
/// verbatim.
/// </summary>
[Fact]
public void TryBuildCreateRequest_DoesNotDeduplicateTheOtherConstraintLists()
{
ApiKeysPage page = new();
page.CreateModel.ReadSubtrees = "Area1, AREA1";
bool built = page.TryBuildCreateRequest(out DashboardApiKeyManagementRequest? request, out string? error);
Assert.True(built, error);
Assert.NotNull(request);
Assert.Equal(["Area1", "AREA1"], request.Constraints.ReadSubtrees);
}
/// <summary>
/// <c>Reset</c> clears the new field, so the next key minted in the same dialog does not
/// inherit the previous key's tag grant.
/// </summary>
[Fact]
public void Reset_ClearsDashboardTagsAlongsideTheOtherConstraintFields()
{
ApiKeysPage page = new();
page.CreateModel.DashboardTags = "team-a";
// A sibling field pins the assertion to Reset itself: were Reset a no-op, both would
// survive and the failure would name the method rather than the one line.
page.CreateModel.ReadSubtrees = "Area1";
page.CreateModel.KeyId = "leftover";
page.CreateModel.Reset();
Assert.Equal(string.Empty, page.CreateModel.DashboardTags);
Assert.Equal(string.Empty, page.CreateModel.ReadSubtrees);
Assert.Equal(string.Empty, page.CreateModel.KeyId);
}
/// <summary>An empty field leaves the key untagged rather than inventing a tag.</summary>
[Fact]
public void TryBuildCreateRequest_WhenDashboardTagsIsBlank_LeavesTheKeyUntagged()
@@ -429,10 +429,12 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
// have exactly that many active alarms. Treat the ambiguous case as
// truncated: the false-positive cost is a snapshot that stays stale for
// one poll, the false-negative cost is every alarm past the cap reading
// as cleared. (The reply's ALARM_RECORDS/@COUNT attribute is a
// candidate exact signal, but only if it reports the total rather than
// the records in the reply — untested on a live rig, see
// docs/AlarmProbeFindings.md.)
// as cleared. (The reply's ALARM_RECORDS/@COUNT attribute was the
// candidate exact signal; the 2026-08-18 live probe settled it — with
// three alarms active, @COUNT read 3, 1, and 2 at caps 1024, 1, and 2,
// matching the records in the reply every time. It reports the reply,
// not the total, so it cannot tell a capped fetch from a complete one.
// See docs/AlarmProbeFindings.md.)
bool truncated = IsTruncatedFetch(fetchedRecordCount, maxAlarmsPerFetch);
IReadOnlyList<MxAlarmTransitionEvent> transitions =
@@ -491,12 +493,14 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// offers no confirmed "more available" flag, so a reply at exactly the
/// cap is indistinguishable from a galaxy that happens to hold exactly
/// that many active alarms; both are treated as truncated. The reply
/// root carries an <c>ALARM_RECORDS/@COUNT</c> attribute that would make
/// the test exact if it reported the total active count rather than the
/// records in this reply; a live probe could not discriminate the two
/// (see <c>docs/AlarmProbeFindings.md</c>), so the count is deliberately
/// not trusted here. Exposed as <c>internal static</c> so the rule is
/// unit-testable without the wnwrapConsumer COM object.
/// root carries an <c>ALARM_RECORDS/@COUNT</c> attribute that would have
/// made the test exact had it reported the total active count; the
/// 2026-08-18 live probe observed it tracking the records in the reply
/// instead (three alarms active returned @COUNT 1 at cap 1 and 2 at cap
/// 2), so it carries no "more available" information and is deliberately
/// not trusted here (see <c>docs/AlarmProbeFindings.md</c>). Exposed as
/// <c>internal static</c> so the rule is unit-testable without the
/// wnwrapConsumer COM object.
/// </summary>
/// <param name="fetchedRecordCount">ALARM records the reply carried.</param>
/// <param name="maxAlarmsPerFetch">The cap that was passed to the fetch.</param>
@@ -660,10 +664,17 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// <em>instance</em> rather than the state it is in: a re-minted
/// GUID would read as the old alarm vanishing and a new one
/// appearing, i.e. a spurious Clear plus a spurious Raise. Live
/// capture confirms stability across the active→returned leg only
/// (<c>docs/AlarmClientDiscovery.md</c>); the acknowledge leg and
/// re-raise-after-clear are assumed, not observed, because the dev
/// rig's alarm attributes reject unauthenticated writes — see
/// capture confirms stability across the active→returned leg
/// (<c>docs/AlarmClientDiscovery.md</c>, reconfirmed 2026-08-18).
/// Re-raise-after-clear was observed on 2026-08-18 and does
/// <em>not</em> reuse the GUID: clearing left the returned record in
/// place under its original GUID and the re-raise replaced it with a
/// freshly minted one. That is the intended reading — the old
/// instance ended and a new one began — so the diff emits a Clear
/// plus a Raise, which is what happened. The acknowledge leg remains
/// assumed rather than observed: on the dev rig every ack surface is
/// inert (<c>AlarmAckByName</c> returns 0 and nothing moves), so no
/// record can be driven into an acknowledged state — see
/// <c>docs/AlarmProbeFindings.md</c>.
/// </para>
/// </remarks>