Compare commits
6 Commits
90331b622c
...
f2a422b02f
| Author | SHA1 | Date | |
|---|---|---|---|
| f2a422b02f | |||
| a5f843cbfb | |||
| c3c603f169 | |||
| a95e510015 | |||
| 47d74032e6 | |||
| 540a1188aa |
@@ -71,7 +71,7 @@ dotnet test src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Test
|
||||
dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
|
||||
|
||||
# API-key admin CLI (same exe, "apikey" subcommand)
|
||||
dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj -- apikey create --display-name "dev" --scopes session,invoke,event,metadata,admin
|
||||
dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj -- apikey create-key --key-id dev --display-name "dev" --scopes session:open,session:close,invoke:read,invoke:write,invoke:secure,events:read,metadata:read,admin
|
||||
```
|
||||
|
||||
Single test by name (xUnit `--filter`):
|
||||
@@ -155,7 +155,7 @@ External analysis sources referenced by design docs:
|
||||
|
||||
## Authentication
|
||||
|
||||
Gateway gRPC clients authenticate with an API key in metadata: `authorization: Bearer mxgw_<key-id>_<secret>`. Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default `C:\ProgramData\MxGateway\gateway-auth.db`). Scopes (`session`, `invoke`, `event`, `metadata`, `admin`) gate specific RPCs; missing → `Unauthenticated`, insufficient → `PermissionDenied`. The `apikey` subcommand on the server exe manages keys; see `src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/`.
|
||||
Gateway gRPC clients authenticate with an API key in metadata: `authorization: Bearer mxgw_<key-id>_<secret>`. Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default `C:\ProgramData\MxGateway\gateway-auth.db`). Scopes (`session:open`, `session:close`, `invoke:read`, `invoke:write`, `invoke:secure`, `events:read`, `metadata:read`, `admin`) gate specific RPCs; missing → `Unauthenticated`, insufficient → `PermissionDenied`. The `apikey` subcommand on the server exe manages keys; see `src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/`.
|
||||
|
||||
Session event streaming is **owner-scoped**: the API key that opened a session is recorded on the session, and every `StreamEvents` attach/reattach is rejected with `PermissionDenied` unless the caller's key id matches the owner. Possessing the `event` scope and knowing a session id is not sufficient — this closes the reconnect/fan-out trust boundary (detach-grace and replay retention are on by default) so an `event`-scoped key cannot attach to another key's retained session.
|
||||
|
||||
|
||||
@@ -355,6 +355,8 @@ If TST-02's interim mitigation (flip retention off) is chosen instead of impleme
|
||||
|
||||
**Verification.** `dotnet build src/ZB.MOM.WW.MxGateway.Server`; test toggling the flag suppresses/shows values; docs match.
|
||||
|
||||
**Closed 2026-08-18 — implemented (the Design's recommended path).** The flag is no longer dead: it now gates value display on all three seams that carry a tag value. `Dashboard/DashboardLiveDataService.cs` substitutes `[redacted]` at the service boundary for each successfully read `/browse` tag (a failed read keeps its `-` placeholder, so the error row and the redaction cannot contradict each other); `Dashboard/Hubs/AlarmsHubPublisher.cs` clears `current_value`/`limit_value` from a **deep-cloned** `AlarmFeedMessage` before broadcasting to `/hubs/alarms`, and `DashboardEventBroadcaster` does the same for the events-hub mirror — clones, because the sources fan out to gRPC `StreamAlarms`/`StreamEvents` and the replay ring, so no gRPC client is affected by this dashboard-display flag. The flag is **kept**, default still `false`. The stale prose the Finding flagged is gone: `docs/GatewayConfiguration.md` and `docs/GatewayDashboardDesign.md` now describe the real behaviour. Status tracked in `00-tracking.md` (already **Done**).
|
||||
|
||||
---
|
||||
|
||||
## TST-17 — Vendor-gated alarm parity residuals silently lossy `Medium` · `—`
|
||||
|
||||
@@ -7342,9 +7342,13 @@ func (x *StreamAlarmsRequest) GetAlarmFilterPrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// One message on the StreamAlarms feed. The stream opens with one
|
||||
// `active_alarm` per currently-active alarm, then a single
|
||||
// `snapshot_complete`, then a `transition` for every subsequent change.
|
||||
// One message on the StreamAlarms feed. The stream opens with a
|
||||
// `provider_status` (the current provider mode), then a `snapshot_status`
|
||||
// (whether the cached active set may be incomplete), then one `active_alarm`
|
||||
// per currently-active alarm, then a single `snapshot_complete`. After that
|
||||
// come the live frames: a `transition` for every subsequent alarm change,
|
||||
// interleaved with a further `provider_status` on each failover/failback and a
|
||||
// further `snapshot_status` on each change of the truncation verdict.
|
||||
type AlarmFeedMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Types that are valid to be assigned to Payload:
|
||||
|
||||
@@ -89203,9 +89203,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile {
|
||||
}
|
||||
/**
|
||||
* <pre>
|
||||
* One message on the StreamAlarms feed. The stream opens with one
|
||||
* `active_alarm` per currently-active alarm, then a single
|
||||
* `snapshot_complete`, then a `transition` for every subsequent change.
|
||||
* One message on the StreamAlarms feed. The stream opens with a
|
||||
* `provider_status` (the current provider mode), then a `snapshot_status`
|
||||
* (whether the cached active set may be incomplete), then one `active_alarm`
|
||||
* per currently-active alarm, then a single `snapshot_complete`. After that
|
||||
* come the live frames: a `transition` for every subsequent alarm change,
|
||||
* interleaved with a further `provider_status` on each failover/failback and a
|
||||
* further `snapshot_status` on each change of the truncation verdict.
|
||||
* </pre>
|
||||
*
|
||||
* Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage}
|
||||
@@ -89739,9 +89743,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile {
|
||||
}
|
||||
/**
|
||||
* <pre>
|
||||
* One message on the StreamAlarms feed. The stream opens with one
|
||||
* `active_alarm` per currently-active alarm, then a single
|
||||
* `snapshot_complete`, then a `transition` for every subsequent change.
|
||||
* One message on the StreamAlarms feed. The stream opens with a
|
||||
* `provider_status` (the current provider mode), then a `snapshot_status`
|
||||
* (whether the cached active set may be incomplete), then one `active_alarm`
|
||||
* per currently-active alarm, then a single `snapshot_complete`. After that
|
||||
* come the live frames: a `transition` for every subsequent alarm change,
|
||||
* interleaved with a further `provider_status` on each failover/failback and a
|
||||
* further `snapshot_status` on each change of the truncation verdict.
|
||||
* </pre>
|
||||
*
|
||||
* Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage}
|
||||
|
||||
Binary file not shown.
@@ -1003,9 +1003,13 @@ message StreamAlarmsRequest {
|
||||
string alarm_filter_prefix = 2;
|
||||
}
|
||||
|
||||
// One message on the StreamAlarms feed. The stream opens with one
|
||||
// `active_alarm` per currently-active alarm, then a single
|
||||
// `snapshot_complete`, then a `transition` for every subsequent change.
|
||||
// One message on the StreamAlarms feed. The stream opens with a
|
||||
// `provider_status` (the current provider mode), then a `snapshot_status`
|
||||
// (whether the cached active set may be incomplete), then one `active_alarm`
|
||||
// per currently-active alarm, then a single `snapshot_complete`. After that
|
||||
// come the live frames: a `transition` for every subsequent alarm change,
|
||||
// interleaved with a further `provider_status` on each failover/failback and a
|
||||
// further `snapshot_status` on each change of the truncation verdict.
|
||||
message AlarmFeedMessage {
|
||||
oneof payload {
|
||||
// Part of the initial active-alarm snapshot (ConditionRefresh).
|
||||
|
||||
@@ -186,8 +186,14 @@ after-reraise TAG=TestMachine_001.TestAlarm001 GUID=2394FEAA94774154AAA97C3AC
|
||||
|
||||
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.
|
||||
does not: the record comes back under a GUID that has never been seen before.
|
||||
|
||||
The two records do **not** coexist — the snapshot carries one record per tag, and the
|
||||
re-raise replaces the returned record rather than joining it. Both fetches above are the
|
||||
full `cap=1024` reply, byte-identical in length (1613) at `@COUNT=3`/`elementCount=3`, the
|
||||
three records being one per `TestMachine_00{1,2,3}`; `72B84A6E…` is simply absent from the
|
||||
later one. So a single poll spanning the re-raise sees the old GUID disappear and the new
|
||||
one appear together.
|
||||
|
||||
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
|
||||
|
||||
@@ -260,7 +260,7 @@ Examples:
|
||||
|
||||
```bash
|
||||
mxgateway apikey init-db
|
||||
mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes read,write
|
||||
mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes invoke:read,invoke:write
|
||||
mxgateway apikey create-key --key-id area1.reader --display-name "Area 1 reader" --scopes invoke:read,metadata:read --read-subtree "Area1/*" --browse-subtree "Area1/*"
|
||||
mxgateway apikey create-key --key-id ops.temp --display-name "Temp contractor" --scopes invoke:read --expires 90d
|
||||
mxgateway apikey create-key --key-id team-a.svc --display-name "Team A service" --scopes session:open,invoke:read --dashboard-tags team-a
|
||||
|
||||
@@ -520,7 +520,9 @@ panel. The panel shows each subscribed tag's live value, MXAccess data type,
|
||||
quality and source timestamp, refreshed every two seconds — but the value column
|
||||
obeys `Dashboard:ShowTagValues` like every other dashboard value surface. With
|
||||
the flag false (the default) `DashboardLiveDataService` hands the page
|
||||
`[redacted]` in place of the formatted value; data type, quality, source
|
||||
`[redacted]` in place of the formatted value of each **successfully read** tag —
|
||||
a failed read keeps its `-` placeholder, since there was no value to suppress —
|
||||
while data type, quality, source
|
||||
timestamp and any read error are untouched, so the panel still answers "is this
|
||||
tag advising and healthy" without disclosing the value. The substitution happens
|
||||
at the service, not in the page: one decision point, and the value never enters
|
||||
|
||||
+5
-4
@@ -114,10 +114,11 @@ message AlarmProviderStatus {
|
||||
}
|
||||
```
|
||||
|
||||
The gateway emits `provider_status` once when a client first subscribes
|
||||
(immediately after the initial snapshot and before the first live transition)
|
||||
and again on every failover or failback. A late-joining client therefore
|
||||
always learns the current provider mode without waiting for the next switch.
|
||||
The gateway emits `provider_status` once when a client first subscribes — as
|
||||
the **first** frame on the stream, before the `snapshot_status` frame and
|
||||
before any cached `active_alarm` — and again on every failover or failback. A
|
||||
late-joining client therefore learns the current provider mode before it sees a
|
||||
single alarm, without waiting for the next switch.
|
||||
|
||||
#### Snapshot completeness on the alarm feed
|
||||
|
||||
|
||||
@@ -416,22 +416,44 @@ gateway 1123/1123, live MXAccess smoke 8/8 — green first try, no stale-obj qui
|
||||
had diverged from Contracts (masked by the in-repo build path); refreshed byte-identical,
|
||||
and the client protoset descriptors were regenerated.
|
||||
|
||||
Follow-ups recorded, not started:
|
||||
- `IGatewayAlarmService.StreamAsync` / `AlarmFeedMessage` does not carry the truncation
|
||||
signal — live central-feed consumers (lmxopcua, ScadaBridge) cannot see snapshot
|
||||
degradation; add if those consumers need completeness reasoning.
|
||||
- No guard keeps `clients/rust/protos/` in sync with Contracts (a `diff` check in
|
||||
`scripts/check-codegen.ps1` would close it).
|
||||
- `EffectiveDashboardConfiguration` (dashboard settings page) doesn't display
|
||||
`GroupToTag` / `UntaggedSessionVisibility`, though it shows `GroupToRole`.
|
||||
- ApiKeysPage's `ConstraintText` neither offers tag input nor lists `DashboardTags`,
|
||||
and since `IsEmpty` now counts tags, a tags-only key renders `-` where a truly
|
||||
unconstrained key renders `unconstrained` — two spellings of one meaning.
|
||||
- `AlarmsHubPublisher` broadcasts alarm transitions with `current_value`/`limit_value`
|
||||
unredacted — the `ShowTagValues` redaction covers only the events hub mirror
|
||||
(pre-existing; now noted in `docs/GatewayConfiguration.md`).
|
||||
- The alarm probes' remaining questions (ack-leg GUID stability, `@COUNT` semantics)
|
||||
unblock via the paths in `docs/AlarmProbeFindings.md`.
|
||||
Follow-ups recorded here — all closed or narrowed on 2026-08-18 (branch
|
||||
`feat/followup-closeout`, plan `docs/plans/2026-08-17-followup-closeout.md`).
|
||||
Each bullet states what it said when written, then its closure:
|
||||
|
||||
- `IGatewayAlarmService.StreamAsync` / `AlarmFeedMessage` did not carry the truncation
|
||||
signal — live central-feed consumers (lmxopcua, ScadaBridge) could not see snapshot
|
||||
degradation. **Closed 2026-08-18 — shipped.** `AlarmSnapshotStatus` with
|
||||
`snapshot_status = 5` on the feed: gateway-side edge emission plus open-time priming
|
||||
(`fccf753`, `312e072`), rolled out to all five clients (`c748361`, `4d4f443`).
|
||||
- "No guard keeps `clients/rust/protos/` in sync with Contracts." **Was stale when
|
||||
written.** `scripts/check-codegen.ps1` Check 3 already existed and already byte-compared
|
||||
by SHA-256 in CI. The real gap was narrower — the sweep ran in one direction only, so a
|
||||
canonical proto with no vendored copy passed. **Closed 2026-08-18** by `711793f`: Check 3
|
||||
now sweeps both directions and a canonical proto missing its vendored copy fails.
|
||||
- `EffectiveDashboardConfiguration` (dashboard settings page) didn't display
|
||||
`GroupToTag` / `UntaggedSessionVisibility`, though it showed `GroupToRole`.
|
||||
**Closed 2026-08-18 — shipped** (`094f2ff`; comparer propagation fixed in `ff16c69`).
|
||||
- ApiKeysPage's `ConstraintText` neither offered tag input nor listed `DashboardTags`,
|
||||
and since `IsEmpty` counts tags, a tags-only key rendered `-` where a truly
|
||||
unconstrained key rendered `unconstrained`. **Closed 2026-08-18 — shipped** (`c037d99`),
|
||||
with the create form de-duplicating typed tags ordinal-ignore-case to match the CLI
|
||||
(`fab600d`).
|
||||
- `AlarmsHubPublisher` broadcast alarm transitions with `current_value`/`limit_value`
|
||||
unredacted — the `ShowTagValues` redaction covered only the events hub mirror.
|
||||
**Closed 2026-08-18** together with the `/browse` residual: `ShowTagValues` now gates
|
||||
all three seams (`eff17d1`, `1ea6f60`, `e50ec49`).
|
||||
- The alarm probes' remaining questions (ack-leg GUID stability, `@COUNT` semantics).
|
||||
**Narrowed 2026-08-18** by a second probe attempt that used the secured-write verb the
|
||||
first attempt lacked (`90331b6`, `540a118`):
|
||||
- `@COUNT` — **answered.** It reports records in the reply, not matching records in the
|
||||
provider, so exact truncation detection through it is permanently refuted; the
|
||||
heuristic in `IsTruncatedFetch` stays as the only available signal.
|
||||
- clear-then-re-raise GUID — **answered.** The re-raise carries a new GUID that replaces
|
||||
the record rather than coexisting with it, which makes the existing `Clear`+`Raise`
|
||||
handling correct as written.
|
||||
- ack leg — **still blocked**, now with a sharper cause: the ack returns `rc=0` and is
|
||||
accepted but inert, so the GUID never moves. Remaining unblock paths are recorded in
|
||||
`docs/AlarmProbeFindings.md`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -448,3 +448,65 @@ clear obj/bin and rebuild, not a regression. Record all counts.
|
||||
payload-case row), closeout-plan follow-ups→Task 9 only.
|
||||
- Task 7/8 run against windev over `ssh windev` (PowerShell); psbridge is fallback.
|
||||
- Do not merge to `main` without user instruction.
|
||||
|
||||
---
|
||||
|
||||
## As-built notes (execution record, 2026-08-18)
|
||||
|
||||
All 9 tasks completed on `feat/followup-closeout`; every classification-driven review
|
||||
chain resolved **Approved**. The final integration review came back *Ready with
|
||||
reservations* — all of its non-blocking findings were fixed in `c3c603f`, leaving no
|
||||
open review item.
|
||||
|
||||
Verification: macOS `NonWindows.slnx` 0W/0E; gateway filtered suites green (dashboard
|
||||
276/276, alarm suites green); `scripts/check-codegen.ps1` 4/4 on macOS. **The full
|
||||
five-language client matrix ran locally for the first time** — dotnet 133 passed /1
|
||||
skipped, Go clean, Rust 108, Python 168 passed /1 skipped, Java 131. Windev at `90331b6`:
|
||||
full `slnx` 0W/0E (one transient MSB4166 node crash, clean on retry), worker x86 523
|
||||
passed / 11 skipped plus **one pre-existing deterministic failure that reproduces on
|
||||
`main`** (`WorkerPipeSessionTests.RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply`),
|
||||
gateway 1151/1151 on rerun (the known windev load flake), live MXAccess smoke 8/8. The
|
||||
comment- and prose-only commits landed after `90331b6` are unverified on windev by
|
||||
design; they need only a cheap tip re-build there.
|
||||
|
||||
- **A session-limit outage interrupted four reviewers mid-run.** All four resumed
|
||||
cleanly on retry; no review was lost or silently truncated.
|
||||
- **Task 2 surfaced two plan defects, both absorbed rather than deferred.** The spec
|
||||
claimed the Java CLI renders alarm frames as generic JSON; it is in fact an exhaustive
|
||||
`switch` that does not compile without an arm for the new case. The spec's work list
|
||||
also omitted the Java generated-bindings tree.
|
||||
- **A reviewer's Minor-2 was refuted, and the doc defect behind it fixed instead.** The
|
||||
finding assumed alarm records coexist after a re-raise; fetch evidence from the probe
|
||||
rig shows the new GUID replaces the record. The prose that implied coexistence was
|
||||
corrected rather than the code.
|
||||
|
||||
Follow-ups recorded, not started:
|
||||
|
||||
- `WorkerPipeSessionTests.RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply`
|
||||
fails deterministically on windev **and on `main`** — pre-existing, needs its own
|
||||
investigation.
|
||||
- `check-codegen.ps1` Check 4 is unrunnable on Windows: the `protoc-gen-go` version banner
|
||||
carries a `.exe` suffix that the exact-string compare in
|
||||
`clients/go/generate-proto.ps1:10,55` does not tolerate.
|
||||
- Windev has `protoc-gen-go-grpc` 1.6.1 against the repo's pinned 1.6.2.
|
||||
- `clients/java`'s `checkGeneratedClean` is dead under Gradle 9 (`Project.exec` was
|
||||
removed); it needs `ExecOperations` injection to work again.
|
||||
- `SettingsPage` renders every `EffectiveDashboardConfiguration` member except
|
||||
`RecentFaultLimit` / `RecentSessionLimit` (pre-existing, predates this branch).
|
||||
- The ack-leg probe stays blocked; unblock paths are in `docs/AlarmProbeFindings.md`.
|
||||
- The dashboard `AlarmsPage` truncation banner is still poll-driven — it could consume
|
||||
the new `snapshot_status` feed frame instead.
|
||||
- **A closed code-review finding regressed, or was never applied.** Server-012
|
||||
(`code-reviews/Server/findings.md:405-412`) is recorded *Resolved 2026-05-18* and claims
|
||||
it corrected two scope lists to the canonical `*:*` strings — the `CLAUDE.md`
|
||||
Build/Test/Run `apikey` sample and the `CLAUDE.md` Authentication-section scope list.
|
||||
Neither correction was present when this branch looked; both were re-fixed here, along
|
||||
with a third instance the finding never covered (`docs/Authentication.md`'s `ops.alice`
|
||||
example). The bookkeeping is the follow-up: other `Server-0xx` entries marked Resolved
|
||||
with documentation-only fixes should be spot-checked for the same pattern, since a
|
||||
finding that reads Resolved is not otherwise re-examined.
|
||||
|
||||
Explicitly decided, not an omission: **`../scadaproj/CLAUDE.md` needs no update.** The
|
||||
umbrella index records the *set* of `.proto` files this repo owns, and that set is
|
||||
unchanged — Task 1 added a message and a field inside an existing proto, not a new
|
||||
contract file.
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
{"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": "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]}
|
||||
{"id": 7, "subject": "Task 7: Wnwrap alarm-probe retry — secured-write path (windev)", "status": "completed", "commits": ["90331b6", "540a118"], "review": "Approved after ack-comment fix; Q2 answered (@COUNT = records-in-reply, exact detection refuted), clear/re-raise GUID answered (new instance), ack leg re-blocked (accepted-but-inert)"},
|
||||
{"id": 8, "subject": "Task 8: Windev full verification", "status": "completed", "verifiedAt": "90331b6", "result": "slnx 0W/0E; worker x86 523 pass + 1 pre-existing deterministic failure (reproduces on main); gateway 1151/1151 on rerun; live smoke 8/8; check-codegen 1-3 pass, Check 4 blocked by pre-existing Windows .exe pin bug (substance validated out-of-band); final comment-only tip re-check pending after bookkeeping", "residual": "pending-external: cheap windev tip re-build covering the comment/prose-only commits landed after 90331b6"},
|
||||
{"id": 9, "subject": "Task 9: Bookkeeping — follow-ups closure + plan record", "status": "completed", "commits": ["a5f843c", "self (follow-up bookkeeping commit, immediately after a5f843c)"], "review": "None (trivial classification); a5f843c closes the prior plan's follow-up block, appends the as-built record, and corrects the CLAUDE.md apikey sample; the follow-up commit closes two adjacent stale scope lists (CLAUDE.md Authentication section, docs/Authentication.md ops.alice example) and records the regressed Server-012 resolution as a follow-up"}
|
||||
],
|
||||
"lastUpdated": "2026-08-17"
|
||||
"lastUpdated": "2026-08-18"
|
||||
}
|
||||
|
||||
@@ -28513,9 +28513,13 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One message on the StreamAlarms feed. The stream opens with one
|
||||
/// `active_alarm` per currently-active alarm, then a single
|
||||
/// `snapshot_complete`, then a `transition` for every subsequent change.
|
||||
/// One message on the StreamAlarms feed. The stream opens with a
|
||||
/// `provider_status` (the current provider mode), then a `snapshot_status`
|
||||
/// (whether the cached active set may be incomplete), then one `active_alarm`
|
||||
/// per currently-active alarm, then a single `snapshot_complete`. After that
|
||||
/// come the live frames: a `transition` for every subsequent alarm change,
|
||||
/// interleaved with a further `provider_status` on each failover/failback and a
|
||||
/// further `snapshot_status` on each change of the truncation verdict.
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
|
||||
public sealed partial class AlarmFeedMessage : pb::IMessage<AlarmFeedMessage>
|
||||
|
||||
@@ -1003,9 +1003,13 @@ message StreamAlarmsRequest {
|
||||
string alarm_filter_prefix = 2;
|
||||
}
|
||||
|
||||
// One message on the StreamAlarms feed. The stream opens with one
|
||||
// `active_alarm` per currently-active alarm, then a single
|
||||
// `snapshot_complete`, then a `transition` for every subsequent change.
|
||||
// One message on the StreamAlarms feed. The stream opens with a
|
||||
// `provider_status` (the current provider mode), then a `snapshot_status`
|
||||
// (whether the cached active set may be incomplete), then one `active_alarm`
|
||||
// per currently-active alarm, then a single `snapshot_complete`. After that
|
||||
// come the live frames: a `transition` for every subsequent alarm change,
|
||||
// interleaved with a further `provider_status` on each failover/failback and a
|
||||
// further `snapshot_status` on each change of the truncation verdict.
|
||||
message AlarmFeedMessage {
|
||||
oneof payload {
|
||||
// Part of the initial active-alarm snapshot (ConditionRefresh).
|
||||
|
||||
@@ -119,13 +119,21 @@ public sealed class AlarmsHubPublisherTests
|
||||
},
|
||||
};
|
||||
|
||||
AlarmFeedMessage snapshotStatus = new()
|
||||
{
|
||||
SnapshotStatus = new AlarmSnapshotStatus { Truncated = true },
|
||||
};
|
||||
|
||||
CapturingHubContext hubContext = await RunPublisherAsync(
|
||||
showTagValues: false,
|
||||
snapshotComplete,
|
||||
providerStatus);
|
||||
providerStatus,
|
||||
snapshotStatus);
|
||||
|
||||
Assert.Same(snapshotComplete, hubContext.Sent[0]);
|
||||
Assert.Same(providerStatus, hubContext.Sent[1]);
|
||||
Assert.Same(snapshotStatus, hubContext.Sent[2]);
|
||||
Assert.True(hubContext.Sent[2].SnapshotStatus.Truncated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -194,11 +194,13 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
|
||||
|
||||
// Use the IwwAlarmConsumer (v1) prefix-named methods for the
|
||||
// lifecycle. Empirically (live dev-rig 2026-05-01) this is the
|
||||
// only path that lets AlarmAckByName succeed afterwards. The
|
||||
// v2 Initialize/Register/Subscribe methods on the class
|
||||
// succeed (return 0) but acks against that consumer state
|
||||
// return -55.
|
||||
// lifecycle. Empirically (live dev-rig 2026-05-01, reconfirmed
|
||||
// 2026-08-18) this is the only path that lets AlarmAckByName
|
||||
// return rc=0 afterwards. The v2 Initialize/Register/Subscribe
|
||||
// methods on the class succeed (return 0) but acks against that
|
||||
// consumer state return -55. Note rc=0 means the call was
|
||||
// accepted, not that an acknowledgement was applied — see
|
||||
// AcknowledgeByName below and docs/AlarmProbeFindings.md.
|
||||
int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName);
|
||||
if (init != 0)
|
||||
{
|
||||
@@ -343,13 +345,28 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
||||
?? throw new InvalidOperationException(
|
||||
"Cannot acknowledge: WnWrapAlarmConsumer was disposed or has not been subscribed yet.");
|
||||
|
||||
// Empirically (live dev-rig 2026-05-01): the IwwAlarmConsumer2
|
||||
// 8-arg AlarmAckByName returns -55 on this AVEVA build (looks like
|
||||
// a stub). The legacy 6-arg IwwAlarmConsumer.AlarmAckByName works
|
||||
// and reaches the alarm-history path correctly. Operator-domain
|
||||
// and operator-full-name fields are accepted by the proto contract
|
||||
// for forward-compat but are not propagated to AVEVA today —
|
||||
// wrapped in the 6-arg call so domain/full-name go to the
|
||||
// The original observation (live dev-rig 2026-05-01) was narrower than
|
||||
// it read: the IwwAlarmConsumer2 8-arg AlarmAckByName returned -55
|
||||
// "and looked like a stub" while the legacy 6-arg
|
||||
// IwwAlarmConsumer.AlarmAckByName returned 0, which is how the 6-arg
|
||||
// overload came to be the one called here. The 2026-08-18 probe
|
||||
// corrected both halves. The -55 tracks the *consumer*, not the
|
||||
// overload: both overloads return -55 against a
|
||||
// SetXmlAlarmQuery-applied consumer and 0 against the ack-only one
|
||||
// used above. And a zero return is NOT evidence the acknowledgement
|
||||
// was applied — the probe acked a real, freshly raised alarm six ways
|
||||
// and watched the snapshot stay UNACK_ALM, OPERATOR_NAME stay empty,
|
||||
// and the alarm extension's own .Acked attribute stay False for 16 s
|
||||
// after each call. On that rig the ack is accepted and then inert, so
|
||||
// whether any wnwrap ack reaches the alarm-history path is
|
||||
// unverified; see docs/AlarmProbeFindings.md and the 2026-08-18
|
||||
// correction in docs/AlarmClientDiscovery.md. The 6-arg call site
|
||||
// below therefore stays as-is for MXAccess parity — the choice is no
|
||||
// longer justified by the rc, and rc semantics are per the probe.
|
||||
//
|
||||
// Operator-domain and operator-full-name fields are accepted by the
|
||||
// proto contract for forward-compat but are not propagated to AVEVA
|
||||
// today — wrapped in the 6-arg call so domain/full-name go to the
|
||||
// alarm-history operator-name field via the szOprName parameter.
|
||||
// Suppress unused-warning explicitly:
|
||||
_ = ackOperatorDomain;
|
||||
|
||||
Reference in New Issue
Block a user