Files
ScadaBridge/docs/plans/2026-06-30-mes-alarm-status-api.md
T
Joseph Doherty 0123b68719 docs(plans): MES alarm-status API — all 7 open design questions decided
Design review 2026-08-01: (1) MES relevance = dedicated severity band 900-999
(script constants, real IsFlaggedForMES predicate); (2) MESReceiver version
DROPPED — CvdReactor is the only implementation, router returns a clean
not-supported error elsewhere; (3) Description = Message else AlarmTypeName;
(4) enrich the native mirror NOW with an additive AckTime (proto +
native_alarm_state + DCL stamping); (5) script names match the endpoints;
(6) shared ReactorAlarms on both sides, suffix behavior as planned;
(7) existing MES API key, no new roles. Plan is now ready to execute.
2026-08-01 10:29:44 -04:00

293 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MES Alarm-Status API — Implementation Plan
**Date:** 2026-06-30
**Status:** Ready to execute — all §6 open questions DECIDED 2026-08-01 (design review with user). Not yet implemented.
**Component touchpoints:** Inbound API (#14), Script Analysis (#25), Site Runtime (#3), Template Engine (#1) — plus deployed config (inbound methods + CvdReactor template scripts).
---
## 1. Goal
Port the legacy **WWSupport / APIServer** MES alarm-status endpoints onto ScadaBridge's Inbound API, mirroring how `MesMoveIn` / `MesMoveOut` were ported:
1. **Update** the existing `SimpleAlarmStatusRequest` inbound method (currently a stub) to do real work.
2. **Create** a new `AlarmStatus` inbound method (the full, filtered endpoint).
3. Both inbound methods **forward to site scripts** (`SAPID → BTDB machine lookup → Route.To(code).Call(...)`), exactly like `MesMoveIn`.
4. ~~The generic version on the `MESReceiver` template queries the BTDB `MachineAlarm` table.~~ **DROPPED (Q2, 2026-08-01):** no MESReceiver version ships — a config-only answer with no live state was judged not worth having. Machines whose template doesn't implement the scripts get a clean `WasSuccessful=false` "not supported on this machine" error from the inbound router.
5. The **`CvdReactor` template provides the (only) implementation**, reading the **alarms actually defined on the CvdReactor object** (its 7 native alarm-source bindings) directly — no `MachineAlarm` lookup anywhere.
Legacy spec being mirrored: [`docs/former-api-specs/mes/Alarm-API.md`](../former-api-specs/mes/Alarm-API.md).
---
## 2. Legacy contract recap (target parity)
Two endpoints, **one** response shape (`AlarmStatusResponse`):
| Legacy route | New inbound method | Request |
|---|---|---|
| `POST /mes/simplealarmstatus` | `SimpleAlarmStatusRequest` (exists, id 9) | `{ "SAPID": "..." }` |
| `POST /mes/alarmstatus` | `AlarmStatus` (NEW) | `{ MachineFilter, AlarmFilter }` |
**`AlarmStatusResponse`** = `{ WasSuccessful: bool, ErrorText: string, Alarms: AlarmInfo[] }`; on failure `WasSuccessful=false` and `Alarms` is **cleared** (all-or-nothing).
**`AlarmInfo`** = `Name`, `HierarchicalName` (`{Code}.{Name}`), `Description`, `IsFlaggedForMES`, `Severity` (0999), `StatusCode` (`"Triggered"` / `"Triggered.Acked"`), `TriggeredDT`, `AckDT?`, `AckComment`.
**Behaviors to preserve:**
- Only **triggered** alarms returned (`InAlarm == true`). No "all configured" mode. `IncludeTriggered` is a no-op (legacy declared-but-unused).
- `/simplealarmstatus`: always **flagged-only**, ack filtering ignored (acked alarms always included, tagged `Triggered.Acked`).
- `/alarmstatus`: `MachineFilter` precedence `SAPID → Code → ZTag → MachineID`; `AlarmFilter` order `FlaggedOnly → MinSeverity → MaxSeverity → NameFilter` (substring, case-insensitive; severity bounds inclusive); `IncludeAcked=false` excludes acked.
---
## 3. Current state in ScadaBridge (verified 2026-06-30 against wonder-app-vd03)
**Inbound methods (`api-method list`):**
- `SimpleAlarmStatusRequest` (id 9) — **stub**: `return new { WasSuccessful = true, Alarms = Array.Empty<Object>() };`. Param: `SAPID` (string). **Return definition already shaped to `AlarmInfo`** (Name/HierarchicalName/Description/IsFlaggedForMES/Severity/StatusCode/TriggeredDT/AckDT/AckComment). ✅ no return-contract change needed.
- No `AlarmStatus` method yet.
- Reference pattern (`MesMoveIn`, id 2): `SAPID → Database.QuerySingleAsync<string>("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s") → Route.To(code).Call("MesMoveIn", {...})`.
**Templates (`template list` / `template get`):**
- `CvdReactor` (id 1, parent=none): root scripts `IpsenMoveIn`, `ProcessRecipeDownload`, `MesMoveIn`, `MesMoveOut`; **7 native alarm sources** (all over `MxGateway`): `ReactorAlarms` (`Z28061.`), `LeftSideAlarms` (`Left_002.`), `RightSideAlarms` (`Right_002.`), `LeftLeakTestAlarms` (`LeakTest_003.`), `RightLeakTestAlarms` (`LeakTest_004.`), `LeftTableAlarms` (`TableAlarms_003.`), `RightTableAlarms` (`TableAlarms_004.`); composes templates 5,6,8,9,13,14,26. No computed `TemplateAlarm`s.
- `MESReceiver` (id 3, parent=none): root scripts `MoveIn`, `MoveOut` (scope-bound to the composed receiver, bare `Attributes`); `Left`/`RightMESReceiver` (5/6) derive from it and are composed into CvdReactor. No alarms, no native sources.
**Established override pattern (MoveIn):** `CvdReactor.MesMoveIn` (root) does side-dispatch off the SAPID suffix and writes **directly** into `Children["LeftMESReceiver"|"RightMESReceiver"].Attributes` via `WriteBatchAndWaitAsync` — it does **not** `CallScript` the base. `MESReceiver.MoveIn` is the generic scope-bound implementation. So "override" here = **CvdReactor supplies its own root-level script that `Route.Call` resolves for CvdReactor instances**; the MESReceiver version is the reusable/generic one. (CvdReactor does **not inherit** MESReceiver — it composes it — so this is parallel-definition, not inheritance-override.)
---
## 4. The blocking gap (drives most of the code work)
**Site `call`-type scripts currently have NO public API to read alarm condition state.**
From the site-script API map:
- The runtime context is `ScriptRuntimeContext` (`src/.../SiteRuntime/Scripts/ScriptRuntimeContext.cs`); compile-time mirror is `ScriptCompileSurface` (`src/.../ScriptAnalysis/ScriptCompileSurface.cs`). Members today: `Attributes`, `Children`, `Parent`, `Instance` (GetAttribute/SetAttribute/CallScript), `Database`, `ExternalSystem`, `Notify`, `Scripts`, `Tracking`, `Parameters`, `Alarm` (on-trigger only), `CancellationToken`.
- Computed alarm state is only exposed via the `Alarm` global **passed into on-trigger scripts**. Native alarm state (`AlarmConditionState`) lives in the Instance Actor's `_latestAlarmEvents`; **no script-facing query method exists**.
⇒ The CvdReactor override ("read the defined alarms directly") **cannot be written as a script today**. It requires a new enabling capability: a script-facing **`Alarms` read accessor**.
**Good news — the data already exists and is local.** `DebugViewSnapshot` (`Commons/Messages/DebugView/DebugViewSnapshot.cs`) carries `IReadOnlyList<AlarmStateChanged> AlarmStates`, and the M7 Alarm Summary page already fans this out per instance. `AlarmStateChanged` (`Commons/Messages/Streaming/AlarmStateChanged.cs`) carries everything we need: `AlarmName`, `Condition` (`AlarmConditionState`: Active/Acknowledged/Confirmed/Shelve/Suppressed/Severity), `Priority`, `Level`, `Message`, `Kind`, `SourceReference`, `AlarmTypeName`, `Category`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime`, `Timestamp`, `NativeSourceCanonicalName`, `IsConfiguredPlaceholder`. The script runs **inside the Instance Actor's context**, so an `Alarms` accessor reads this via a **local Ask** — same mechanism as `GetAttribute`, no cross-cluster hop.
---
## 5. Target architecture (three layers)
```
INBOUND (central) ENABLING CODE (repo) SITE TEMPLATE SCRIPTS (deployed config)
───────────────── ──────────────────── ───────────────────────────────────────
SimpleAlarmStatusRequest ─┐ Alarms script accessor
├─ Route.To(code).Call(...) ─────────► CvdReactor.SimpleAlarmStatus (native sources)
AlarmStatus (new) ─┘ + AckTime mirror enrichment ──► CvdReactor.AlarmStatus (native sources)
(ScriptRuntimeContext +
ScriptCompileSurface) (no MESReceiver version — Q2 DECIDED: dropped)
```
### 5.1 Layer A — Inbound API methods (deployed config; central)
Thin routers, identical shape to `MesMoveIn`. They own machine resolution (the `MachineFilter` precedence) against BTDB, then `Route.To(code).Call(<siteScript>, <filters>)` and return the site result verbatim.
**`SimpleAlarmStatusRequest` (update id 9):** extract the **numeric** SAP id for the BTDB lookup, but forward the **raw** SAPID (with `_A`/`_B`[`_LT`] suffix) to the site so the CvdReactor override can resolve the side — exactly as `MesMoveIn` does.
```csharp
try {
var raw = ((Parameters.ContainsKey("SAPID") ? Parameters["SAPID"]?.ToString() : null) ?? "").Trim();
var m = System.Text.RegularExpressions.Regex.Match(raw, @"^(\d+)");
if (!m.Success)
return new { WasSuccessful = false, ErrorText = $"SAPID does not contain a numeric SAP id: '{raw}'", Alarms = Array.Empty<object>() };
var sap = m.Groups[1].Value; // numeric SAP for the DB lookup
var code = await Database.QuerySingleAsync<string>("BTDB",
"SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = sap });
if (string.IsNullOrEmpty(code))
return new { WasSuccessful = false, ErrorText = $"Failed to find machine with SAPID '{sap}'", Alarms = Array.Empty<object>() };
// SimpleAlarmStatus semantics: flagged-only, acked always included. Pass RAW SAPID for side routing.
return await Route.To(code).Call("SimpleAlarmStatus", new { SAPID = raw, MachineCode = code });
} catch (Exception ex) {
return new { WasSuccessful = false, ErrorText = "SimpleAlarmStatus failed: " + ex.Message, Alarms = Array.Empty<object>() };
}
```
**`AlarmStatus` (new method):** nested `MachineFilter` + `AlarmFilter` params (Inbound API extended type system supports Object). Resolve machine via precedence (all selectors come from `dbo.Machine`), then forward the filter to the site script.
- Param definition (object): `MachineFilter { MachineID:int?, SAPID:string, ZTag:string, Code:string }`, `AlarmFilter { NameFilter:string, MinSeverity:int?, MaxSeverity:int?, IncludeTriggered:bool, IncludeAcked:bool, FlaggedOnly:bool }`.
- Return definition: same `AlarmInfo[]` shape as id 9.
- Body: resolve `code` by `SAPID → Code → ZTag → MachineID` (each its own `SELECT TOP 1 Code FROM dbo.Machine WHERE ...`), selector-specific error text on unmatched, then `Route.To(code).Call("AlarmStatus", new { MachineCode = code, SAPID = <raw MachineFilter.SAPID or "">, ...AlarmFilter fields... })`. The raw `MachineFilter.SAPID` (suffix included) is forwarded so the CvdReactor override can scope by side; when the machine was selected by `Code`/`ZTag`/`MachineID` (no SAPID suffix), side scoping is skipped and all sources are returned (see §6.6).
> **Note:** Machine resolution stays on the inbound side (central) because it is environment/SQL-shaped and identical for both endpoints; site scripts receive an already-resolved `MachineCode` plus the alarm filter.
> **Unsupported machines (Q2 DECIDED):** since only CvdReactor implements the site scripts, `Route.Call` on any other machine fails script-not-found. Both routers catch that case specifically and return `{ WasSuccessful=false, ErrorText = $"Alarm status is not supported on machine '{code}'" }` instead of the raw exception text (verify during impl what exception/message `Route.Call` yields for a missing script so the catch can distinguish it).
### 5.2 Layer B — Enabling code change: script-facing `Alarms` accessor (repo)
New read-only accessor on the site script surface so scripts can enumerate the instance's current alarms.
**Proposed shape** (final names TBD in review):
```csharp
// On ScriptRuntimeContext (runtime) + ScriptCompileSurface (compile-time stub):
public AlarmsAccessor Alarms { get; }
public sealed class AlarmsAccessor {
// Snapshot of the instance's CURRENT alarm conditions (computed + native),
// read locally from the Instance Actor (same Ask path as attribute reads).
Task<IReadOnlyList<ScriptAlarm>> CurrentAsync(CancellationToken ct = default);
}
public sealed record ScriptAlarm(
string Name, string SourceReference, string NativeSourceCanonicalName,
bool Active, bool Acknowledged, bool? Confirmed, bool Shelved, bool Suppressed,
int Severity, string Kind, // "Computed" | "NativeOpcUa" | "NativeMxAccess"
string Message, string AlarmTypeName, string Category,
string OperatorUser, string OperatorComment,
DateTimeOffset? OriginalRaiseTime, DateTimeOffset Timestamp,
DateTimeOffset? AckTime, // Q4 DECIDED: real ack timestamp from the enriched mirror
string CurrentValue, string LimitValue, bool IsConfiguredPlaceholder);
```
**Ack-timestamp enrichment (Q4 DECIDED 2026-08-01 — enrich the mirror now, not a follow-up):**
- Add an additive `AckTime` (`DateTimeOffset?`) to `AlarmStateChanged`, the vendored `AlarmStateUpdate` proto (additive field number, never reuse — manual toggle-build-copy-untoggle regen), and the site `native_alarm_state` persistence so it survives failover.
- Semantics: when the underlying source supplies a true ack time (OPC UA A&C ack transitions do), use it; when it doesn't (MxGateway events without one), stamp the observation time of the ack transition at the DCL — accurate to when the system saw the ack, never fabricated. Null while unacked; cleared on re-raise.
Implementation:
- Runtime: `Alarms.CurrentAsync()` Asks the Instance Actor for its alarm snapshot (reuse the existing internal alarm-state map that feeds `DebugViewSnapshot.AlarmStates`; project each `AlarmStateChanged``ScriptAlarm`). New internal request/response message (or reuse `DebugSnapshotRequest` and project off `AlarmStates`).
- Compile surface: matching stub returning `Task.FromResult(empty)` so design-time compile + Test Run pass.
- `ScriptTrustPolicy` (#25): no new forbidden surface; `Alarms` is an allow-listed context member like `Attributes`. Confirm the validator's allow-list includes it.
- Additive message-contract evolution only.
### 5.3 Layer C — Site template scripts (deployed config)
**MESReceiver version — DROPPED (Q2 DECIDED 2026-08-01).** With no native alarm sources, MESReceiver has no live "triggered" signal; a configured-catalog-only answer was judged misleading rather than useful. No BTDB `MachineAlarm` read ships anywhere in this feature. Machines without the CvdReactor-style scripts get the router's "not supported on machine" error (§5.1).
**CvdReactor (the only implementation — reads native sources directly, no DB):** `SimpleAlarmStatus` and `AlarmStatus` (Q5 DECIDED: names match the endpoints, template-agnostic contract) as **root-level scripts** (mirrors `MesMoveIn`).
The override **routes left vs right off the SAPID suffix**`_A` ⇒ Left, `_B` ⇒ Right — and scopes the returned alarms to that side's native sources plus the shared reactor-wide source. The `_LT` (leak-test) suffix is **ignored** for scoping: it is stripped before reading the side, and both the side's run and leak-test sources are included. Source → side map (the 7 `CvdReactor` native sources):
| Side (`_A`/`_B`) | Native sources included |
|---|---|
| `Left` (`_A`) | `LeftSideAlarms`, `LeftLeakTestAlarms`, `LeftTableAlarms`, **+ shared `ReactorAlarms`** |
| `Right` (`_B`) | `RightSideAlarms`, `RightLeakTestAlarms`, `RightTableAlarms`, **+ shared `ReactorAlarms`** |
```csharp
// CvdReactor.SimpleAlarmStatus (sketch)
// Q1 DECIDED: MES relevance = dedicated severity band. Galaxy alarm priorities for
// MES-relevant alarms are configured into 900-999; nothing else may use that band.
const int MesBandMin = 900;
const int MesBandMax = 999;
try {
var raw = (Parameters["SAPID"] as string) ?? "";
var code = Parameters["MachineCode"]?.ToString() ?? "";
// Side routing from the SAPID suffix: _A => Left, _B => Right. _LT (leak test) is ignored.
var core = raw.EndsWith("_LT") ? raw.Substring(0, raw.Length - 3) : raw;
var side = core.EndsWith("_A") ? "Left" : core.EndsWith("_B") ? "Right" : null;
if (side == null)
return new { WasSuccessful = false, ErrorText = "SAPID missing _A/_B side suffix", Alarms = Array.Empty<object>() };
// Keep this side's sources (Left*/Right*) + the shared reactor-wide source; drop the other side's.
System.Func<string,bool> InScope = src =>
string.IsNullOrEmpty(src) // computed/unbound — keep
|| src.StartsWith(side, System.StringComparison.OrdinalIgnoreCase) // Left* or Right*
|| src.StartsWith("Reactor", System.StringComparison.OrdinalIgnoreCase); // shared ReactorAlarms
var alarms = await Alarms.CurrentAsync(); // native + computed, all 7 sources
var infos = alarms
.Where(a => a.Active && !a.IsConfiguredPlaceholder) // only triggered
.Where(a => InScope(a.NativeSourceCanonicalName)) // _A => Left*, _B => Right*, + ReactorAlarms
// SimpleAlarmStatus semantics: flagged-only (= MES band, Q1) + acked always included
.Where(a => a.Severity >= MesBandMin && a.Severity <= MesBandMax)
.Select(a => new {
Name = a.Name,
HierarchicalName = code + "." + a.Name,
Description = string.IsNullOrEmpty(a.Message) ? a.AlarmTypeName : a.Message, // Q3 DECIDED
IsFlaggedForMES = a.Severity >= MesBandMin && a.Severity <= MesBandMax, // Q1 DECIDED: real predicate
Severity = a.Severity,
StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered",
TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp),
AckDT = a.AckTime, // Q4 DECIDED: enriched mirror
AckComment = a.OperatorComment,
}).ToList();
return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos };
} catch (Exception ex) {
return new { WasSuccessful = false, ErrorText = "SimpleAlarmStatus failed: " + ex.Message, Alarms = Array.Empty<object>() };
}
```
`CvdReactor.AlarmStatus` is the same — same side parsing + `InScope` filter, WITHOUT the always-on band filter — but applies the passed `AlarmFilter` (NameFilter/MinSeverity/MaxSeverity/FlaggedOnly/IncludeAcked) over the scoped native list; `FlaggedOnly=true` means the MES-band predicate (Q1). `IsFlaggedForMES` is always reported per-row from the band predicate. (When `AlarmStatus` was selected by `Code`/`ZTag`/`MachineID` with no SAPID suffix, `side == null`; in that case return all sources instead of erroring — Q6b DECIDED.)
> **Shared `ReactorAlarms` (`Z28061.`) is included on both sides — Q6a DECIDED 2026-08-01:** reactor-wide faults apply regardless of which side MES asks about.
---
## 6. Key design decisions — ALL DECIDED 2026-08-01 (design review with user)
1. **`IsFlaggedForMES` for native alarms — DECIDED: severity band.** MES relevance is encoded in the alarm severity itself: MES-relevant alarms are configured (in the Galaxy alarm priority) into a **dedicated band 900999** reserved exclusively for MES-relevant alarms, so it cannot collide with ordinary criticality tuning. Scripts carry `MesBandMin = 900` / `MesBandMax = 999` as named constants; `IsFlaggedForMES = (Severity in band)` is reported honestly per row; `SimpleAlarmStatus` (always flagged-only) and `AlarmStatus` with `FlaggedOnly=true` filter by the band predicate. Since `Severity` is returned verbatim, MES's own `MinSeverity`/`MaxSeverity` filters compose naturally with the band. No new tables, code flags, or allow-lists. **Operational prerequisite:** the Galaxy alarm priorities for MES-relevant CvdReactor alarms must be set into 900999 before the endpoints are meaningful.
2. **MESReceiver version — DECIDED: dropped entirely.** With no native alarm sources there is no live state; a config-only catalog answer was judged not worth shipping. Only CvdReactor implements the scripts; other machines get the router's clean "not supported on machine" error (§5.1). No BTDB `MachineAlarm` dependency remains.
3. **`Description` mapping — DECIDED:** `Message`, falling back to `AlarmTypeName` when `Message` is empty.
4. **`AckDT` mapping — DECIDED: enrich the native mirror now.** Additive `AckTime` on `AlarmStateChanged` + vendored proto + `native_alarm_state` (survives failover). True source ack time where available (OPC UA A&C); DCL observation time of the ack transition where the source lacks one (MxGateway); null while unacked; cleared on re-raise. See §5.2.
5. **Script naming — DECIDED: `SimpleAlarmStatus` / `AlarmStatus`** (match the endpoint names; template-agnostic contract any future machine template can implement). With the MESReceiver version dropped there is no parallel definition, so no override-resolution concern remains.
6. **Side scoping — DECIDED (all parts).** CvdReactor-only: `_A` ⇒ Left, `_B` ⇒ Right; `_LT` stripped before reading the side (leak-test sources still included). (a) shared `ReactorAlarms` (`Z28061.`) **included on both sides** — reactor-wide faults apply regardless of side. (b) missing suffix: `SimpleAlarmStatus` **errors** on a SAPID without `_A`/`_B` (matches `MesMoveIn`); `AlarmStatus` selected by `Code`/`ZTag`/`MachineID` (no SAPID) **returns all sources**; a SAPID selector without a suffix errors.
7. **Roles / auth — DECIDED:** no new roles; create `AlarmStatus` as Designer via CLI; authorize the **existing MES API key** (the one already calling `MesMoveIn`/`MesMoveOut`) for both alarm endpoints — one key per external system.
---
## 7. Implementation task breakdown (ordered)
> Two artifact classes: **[repo]** = source/tests/docs committed to git; **[deployed]** = inbound methods / template scripts pushed to the cluster via CLI/UI (not in repo). The user said "don't execute yet" — this is the ordered plan only.
**Phase 1 — Enabling `Alarms` script API + AckTime mirror enrichment [repo]**
1. **AckTime enrichment (§6.4):** additive `AckTime` on `AlarmStateChanged`, the vendored `AlarmStateUpdate` proto (manual regen), `native_alarm_state` persistence, and the DCL ack-transition stamping (source ack time where supplied, else observation time).
2. Add `AlarmsAccessor` + `ScriptAlarm` to the runtime context (`ScriptRuntimeContext`) — local Ask to the Instance Actor; project `AlarmStateChanged``ScriptAlarm` (incl. `AckTime`). Add internal request/response message if not reusing `DebugSnapshotRequest`.
3. Mirror the stub on `ScriptCompileSurface` (and confirm `TriggerCompileSurface` not needed — trigger expressions don't read alarms).
4. Confirm/extend `ScriptTrustPolicy` allow-list so `Alarms` is permitted; no new forbidden APIs.
5. Unit tests: runtime accessor projection (active/acked/severity/timestamps/AckTime), AckTime stamping + failover persistence, compile-surface compiles a representative `Alarms.CurrentAsync()` script, trust-policy accepts it.
6. Doc: update `Component-SiteRuntime.md` + `Component-DataConnectionLayer.md` (native-mirror AckTime) + Script Analysis #25 surface list; note the accessor in `Component-InboundAPI.md` routing examples.
**Phase 2 — Inbound methods [deployed] + doc [repo]**
7. Update `SimpleAlarmStatusRequest` (id 9) body to the §5.1 router incl. the not-supported-machine catch (validate via design page first to avoid the stale-handler trap — see memory `inbound-noncompiling-update-keeps-old-handler`).
8. Create `AlarmStatus` method (params/return/script per §5.1); authorize the existing MES API key for both methods (§6.7).
9. Doc the two endpoints in `docs/requirements/Component-InboundAPI.md` (or a dedicated MES-integration note) cross-referencing the legacy spec.
**Phase 3 — Site template scripts [deployed]** *(MESReceiver task removed — §6.2 decided dropped)*
10. Add `SimpleAlarmStatus` + `AlarmStatus` to **CvdReactor** (native sources via `Alarms.CurrentAsync()`, MES band constants), per §5.3.
11. `template validate` (script compilation gate) before redeploy; redeploy affected instances.
**Phase 4 — Verify**
12. Build affected projects + run targeted tests (per memory `targeted-tests-not-full-suite`).
13. Live smoke against a real reactor instance (see §8) — both endpoints, success + machine-not-found + not-supported-machine + filtered. Requires the Galaxy MES-band priorities (§6.1) to be set for a meaningful flagged-only result.
---
## 8. Testing & verification
- **Unit [repo]:** `Alarms` accessor projection; compile-surface acceptance; trust-policy allow; inbound param/return validation for `AlarmStatus` (nested objects).
- **Design-page [deployed]:** paste both site scripts into the api-method/template design pages; confirm no CS errors and Test Run executes (this is the gate that catches `Database.QuerySingle`-style mistakes pre-deploy).
- **Live smoke [deployed]:** `curl` `simplealarmstatus` and `alarmstatus` against the deployed reactor; cross-check returned alarms against the CLI debug snapshot (`debug snapshot --id <instance>`) alarm table for the same instance; verify only `Active` alarms appear, ack mapping, severity filtering, and machine-not-found error text.
- **Caveat:** native MxGateway alarms need a real gateway; the docker cluster can't fully exercise the CvdReactor native path (cf. memory on no-local-gateway). Validate the native-read path on the wonder-app-vd03 / real-gateway environment; unit-test the projection in isolation.
---
## 9. Out of scope / follow-ups
- ~~True `IsFlaggedForMES` allow-list~~ — resolved by the §6.1 severity-band decision (no follow-up needed).
- ~~Precise `AckDT` follow-up~~ — pulled INTO scope by §6.4 (mirror enriched now).
- A live/aggregated central alarm store or stream (the M7 follow-up) — these endpoints stay pull-based, per-instance, like the Alarm Summary page.
- Alarm-status support for non-CvdReactor machine types (any future template just implements `SimpleAlarmStatus`/`AlarmStatus` root scripts against the same contract).
---
## 10. Summary of changes
| Artifact | Type | Change |
|---|---|---|
| `AlarmStateChanged` + vendored `AlarmStateUpdate` proto + `native_alarm_state` + DCL stamping | [repo] | Additive `AckTime` enrichment (§6.4) |
| `ScriptRuntimeContext` | [repo] | New `Alarms` accessor + `ScriptAlarm` (incl. `AckTime`); local alarm-snapshot Ask |
| `ScriptCompileSurface` | [repo] | Mirror `Alarms` stub |
| `ScriptTrustPolicy` (#25) | [repo] | Allow `Alarms` member (verify) |
| Internal alarm-snapshot message (or reuse `DebugSnapshotRequest`) | [repo] | Additive |
| Site Runtime + DCL + Script Analysis + Inbound API docs | [repo] | Document `Alarms` accessor, `AckTime`, endpoints |
| Unit tests (SiteRuntime / ScriptAnalysis / InboundAPI) | [repo] | New |
| `SimpleAlarmStatusRequest` (id 9) | [deployed] | Stub → real router (+ not-supported catch) |
| `AlarmStatus` (new) | [deployed] | New inbound method, existing MES key authorized |
| `CvdReactor.SimpleAlarmStatus` / `.AlarmStatus` | [deployed] | New native-source scripts (MES band 900999) |
| Galaxy alarm priorities | [external] | MES-relevant CvdReactor alarms configured into 900999 (§6.1 prerequisite) |