feat(docker): seed + verify scripts for the MES-reportable alarm demo — rig live gate PASS

Task 13/14. seed-mes-alarm-demo.sh builds a template whose three computed alarms differ in
how they acquire the flag (template --mes-reportable, nothing, instance override over a
template false), a site Call script projecting them through Alarms.CurrentAsync() /
MesReportableAsync(), and two inbound methods + a scoped API key. Idempotent by name lookup.

verify-mes-alarm-demo.sh drives the real inbound HTTP surface: PASS 14/14 on the docker rig
(migration 20260911135501_AddAlarmMesReportable applied at boot). Playwright
AlarmOverride_MesReportable_RoundTrips passed. Results + the facts the gate established are
in design doc section 8.

Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
Joseph Doherty
2026-09-11 10:33:40 -04:00
parent 17d9a4cd2c
commit 421d2d305e
3 changed files with 498 additions and 0 deletions
@@ -235,3 +235,80 @@ Central inbound method AlarmStatus ── Route.To(MachineCode).Call("AlarmStatu
editor), `Component-ConfigurationDatabase.md` (migration), CLI `README.md`, `CLAUDE.md` native-alarm
bullet (flag vs band), `2026-06-30-mes-alarm-status-api.md` §6.1 (note: superseded for computed
alarms), deferred-work register (native source-level flag; central badge/proto).
---
## 8. Rig live gate results (2026-09-11)
Seeded by `docker/seed-mes-alarm-demo.sh`, gated by `docker/verify-mes-alarm-demo.sh` against the
`docker/` rig (8 nodes, rebuilt from this branch). Config DB migration applied on boot:
```
[14:19:43 INF] [Central/scadabridge-central-a] Applying migration '20260911135501_AddAlarmMesReportable'.
```
### 8.1 Seeded objects
| Object | Id | Notes |
|---|---|---|
| Template `MesAlarmDemo` | 5255 | `Temperature` Double **static**, value 20 |
| Alarm `HighTemp` | — | HiLo hi=80 hihi=95, priority 500, **`--mes-reportable`** |
| Alarm `TempSpike` | — | ValueMatch `100`, priority 300, not reportable |
| Alarm `SensorFault` | — | ValueMatch `-1`, priority 700, **not reportable on the template** |
| Site script `AlarmStatus` (Call) | 1045 | `Alarms.CurrentAsync()` / `Alarms.MesReportableAsync()` + the filter params |
| Instance `MesAlarmDemo01` (site-a) | 3847 | `instance alarm-override set --alarm SensorFault --mes-reportable true` |
| Inbound method `AlarmStatus` | 2224 | nested `AlarmFilter` object param, routes via `Route.To(MachineCode)` |
| Inbound method `SimpleAlarmStatus` | 2225 | flagged-only shorthand |
| API key `mes-alarm-demo` | — | scoped to both methods; token printed once |
Second seed run reported `exists` for template/attribute/all three alarms/instance, `updated` for
both scripts and both methods, and refused to mint a second API key — **idempotent**, same ids.
### 8.2 Gate: `PASS 14/14`
| Step | Stimulus | Result |
|---|---|---|
| A1 | `96` → `AlarmStatus` | `WasSuccessful=true`, exactly 1 row, `HighTemp` |
| A2 | same | `IsFlaggedForMES=true` (template flag), `Severity=500`, `StatusCode="Triggered.Acked"`, `AckDT=null` |
| A3 | same | `HierarchicalName="MesAlarmDemo01.HighTemp"` |
| H | same moment | `debug snapshot --id 3847` shows `HighTemp` Active (`condition.active=true`) |
| B1/B2 | `100` → `FlaggedOnly=false` | 2 rows: `HighTemp` **true**, `TempSpike` **false** (HiHi still active at 100, so both) |
| B3 | `100` → `FlaggedOnly=true` | 1 row, `HighTemp`; `TempSpike` excluded by `MesReportableAsync()` |
| C1/C2 | `-1` → `SimpleAlarmStatus` | 1 row, `SensorFault`, `IsFlaggedForMES=true`, `Severity=700` — **the instance override beat the template's `false`** |
| C3 | `-1` → `AlarmStatus` | `SensorFault` only; `HighTemp` absent (−1 trips no high band) |
| D1 | `-1` + `MinSeverity=600` | `SensorFault` only (700 ≥ 600; HighTemp 500 and TempSpike 300 filtered) |
| E1 | `20` | `WasSuccessful=true`, `Alarms=[]` |
| F1 | `MachineCode=DelmiaSim01` | `WasSuccessful=false`, `ErrorText` contains "not supported on machine" (`not found on instance` catch) |
| G1 | `MachineCode=NoSuchMachine` | `WasSuccessful=false`, `ErrorText` contains "not found" |
Browser round-trip: `dotnet test … --filter AlarmOverride_MesReportable_RoundTrips` →
**Passed: 1, Failed: 0** (12 s).
### 8.3 Facts the gate established (read before extending the demo)
- **`StatusCode` is `"Triggered.Acked"` for every computed alarm, and `AckDT` is always `null`.**
`AlarmConditionStateFactory.ForComputed` reports `Acknowledged: true` — computed alarms have no
ack-back, so there is nothing to be unacknowledged about. A filter such as the script's
`IncludeAcked=false` therefore drops *every* computed alarm; it is only meaningful over mirrored
native conditions. The plan's expected `"Triggered"` was wrong and the assertion was corrected.
- **`Severity` is the alarm's effective priority**, and for a HiLo alarm it is the *band* priority
(`LevelPriority`), falling back to the alarm priority when no per-band priority is configured —
which is why `HighTemp` reads 500 at HiHi and `MinSeverity=600` cleanly isolates `SensorFault`.
- **The `debug snapshot` / stream DTO reports `isMesReportable: false` for every alarm, including
`HighTemp`.** That is §2.2 working as specified — the flag is deliberately not on the gRPC protos
in this slice, so central never sees it and only the site-local script snapshot is stamped. Step H
therefore cross-checks alarm *activity*, not the flag. (Register item: central badge/proto.)
- **`ValueMatch` compares `value?.ToString()` as an ordinal string**, so `-1` and `100` matched the
Double attribute's rendered form on this rig. A ValueMatch alarm over a Double is a string
comparison, not a numeric one — `RangeViolation` is the numeric trigger.
- `--match-value=-1` must use the `=` form; a space-separated `-1` is parsed as another option.
### 8.4 Deviations from the plan
1. Step A's `StatusCode` expectation changed from `"Triggered"` to `"Triggered.Acked"` (§8.3).
2. Step H parses `alarmStates[].condition.active` (the snapshot has no textual state field; the
`state` enum is `Active=0`, `Normal=1`).
3. `SensorFault` is seeded with `--attribute Temperature` — a `ValueMatch` trigger needs the watched
attribute, which the plan's sketch omitted.
4. `site deploy-artifacts` is not called: the demo has no shared scripts or system-wide artifacts,
and `instance deploy` after the override is what puts the alarms and the script on site-a.