Design doc and code travel together (CLAUDE.md editing rules), so this records
what the two preceding commits shipped and, more usefully, WHY the non-obvious
choices were made -- the parts a future reader would otherwise re-litigate:
- Component-SiteRuntime.md: the Alarms.CurrentAsync() runtime API entry (why
it is not scope-prefixed, why it is read-only, why placeholder rows are
included), the full ScriptAlarm shape, AckTime on the enriched
AlarmStateChanged, proto field 24, and the metadata_json-vs-new-column
persistence rationale (native_alarm_state is RegisterReplicated; LocalDb
builds its CDC triggers from the column list at registration time).
- Component-DataConnectionLayer.md already carried the AckTime section in the
first commit; this adds the SiteRuntime/ScriptAnalysis/InboundAPI halves.
- Component-ScriptAnalysis.md: accessors returning domain types return the
SAME type on both surfaces, and the trust-model note that a deny-list needs
no entry for a new globals member -- only that its return type resolves in
a permitted namespace.
- Component-InboundAPI.md records the NEGATIVE decision: there is
deliberately no Route.To(...).GetAlarms(...) verb. Alarm state is
per-instance and lives on the site's Instance Actor, so the read goes
through a routed site script and the filtering happens where the data is;
central stays a thin router.
- CLAUDE.md native-alarm bullet gains the enrichment + accessor summary.
- The plan's §7 Phase 1 rows are ticked with 2026-08-01 and annotated with
what was actually built (incl. the two choices that differ from the plan's
"or" options: a dedicated snapshot message rather than DebugSnapshotRequest,
and the extra SandboxScriptHost mirror the plan did not list). Phases 2-4
stay open -- they are deployed config and need a live rig.
31 KiB
MES Alarm-Status API — Implementation Plan
Date: 2026-06-30
Status: Phase 1 [repo] COMPLETE 2026-08-01 — the Alarms.CurrentAsync() script accessor and the AckTime native-mirror enrichment are shipped (see §7). Phases 2–4 are deployed config (inbound methods + CvdReactor template scripts) and still pending; they need a live rig. All §6 open questions DECIDED 2026-08-01 (design review with user).
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:
- Update the existing
SimpleAlarmStatusRequestinbound method (currently a stub) to do real work. - Create a new
AlarmStatusinbound method (the full, filtered endpoint). - Both inbound methods forward to site scripts (
SAPID → BTDB machine lookup → Route.To(code).Call(...)), exactly likeMesMoveIn. The generic version on theDROPPED (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 cleanMESReceivertemplate queries the BTDBMachineAlarmtable.WasSuccessful=false"not supported on this machine" error from the inbound router.- The
CvdReactortemplate provides the (only) implementation, reading the alarms actually defined on the CvdReactor object (its 7 native alarm-source bindings) directly — noMachineAlarmlookup anywhere.
Legacy spec being mirrored: docs/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 (0–999), StatusCode ("Triggered" / "Triggered.Acked"), TriggeredDT, AckDT?, AckComment.
Behaviors to preserve:
- Only triggered alarms returned (
InAlarm == true). No "all configured" mode.IncludeTriggeredis a no-op (legacy declared-but-unused). /simplealarmstatus: always flagged-only, ack filtering ignored (acked alarms always included, taggedTriggered.Acked)./alarmstatus:MachineFilterprecedenceSAPID → Code → ZTag → MachineID;AlarmFilterorderFlaggedOnly → MinSeverity → MaxSeverity → NameFilter(substring, case-insensitive; severity bounds inclusive);IncludeAcked=falseexcludes 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 toAlarmInfo(Name/HierarchicalName/Description/IsFlaggedForMES/Severity/StatusCode/TriggeredDT/AckDT/AckComment). ✅ no return-contract change needed.- No
AlarmStatusmethod 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 scriptsIpsenMoveIn,ProcessRecipeDownload,MesMoveIn,MesMoveOut; 7 native alarm sources (all overMxGateway):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 computedTemplateAlarms.MESReceiver(id 3, parent=none): root scriptsMoveIn,MoveOut(scope-bound to the composed receiver, bareAttributes);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 isScriptCompileSurface(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
Alarmglobal 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.
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
codebySAPID → Code → ZTag → MachineID(each its ownSELECT TOP 1 Code FROM dbo.Machine WHERE ...), selector-specific error text on unmatched, thenRoute.To(code).Call("AlarmStatus", new { MachineCode = code, SAPID = <raw MachineFilter.SAPID or "">, ...AlarmFilter fields... }). The rawMachineFilter.SAPID(suffix included) is forwarded so the CvdReactor override can scope by side; when the machine was selected byCode/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
MachineCodeplus the alarm filter.
Unsupported machines (Q2 DECIDED): since only CvdReactor implements the site scripts,
Route.Callon 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/messageRoute.Callyields 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):
// 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?) toAlarmStateChanged, the vendoredAlarmStateUpdateproto (additive field number, never reuse — manual toggle-build-copy-untoggle regen), and the sitenative_alarm_statepersistence 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 feedsDebugViewSnapshot.AlarmStates; project eachAlarmStateChanged→ScriptAlarm). New internal request/response message (or reuseDebugSnapshotRequestand project offAlarmStates). - Compile surface: matching stub returning
Task.FromResult(empty)so design-time compile + Test Run pass. ScriptTrustPolicy(#25): no new forbidden surface;Alarmsis an allow-listed context member likeAttributes. 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 |
// 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)
-
IsFlaggedForMESfor 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 900–999 reserved exclusively for MES-relevant alarms, so it cannot collide with ordinary criticality tuning. Scripts carryMesBandMin = 900/MesBandMax = 999as named constants;IsFlaggedForMES = (Severity in band)is reported honestly per row;SimpleAlarmStatus(always flagged-only) andAlarmStatuswithFlaggedOnly=truefilter by the band predicate. SinceSeverityis returned verbatim, MES's ownMinSeverity/MaxSeverityfilters 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 900–999 before the endpoints are meaningful. -
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
MachineAlarmdependency remains. -
Descriptionmapping — DECIDED:Message, falling back toAlarmTypeNamewhenMessageis empty. -
AckDTmapping — DECIDED: enrich the native mirror now. AdditiveAckTimeonAlarmStateChanged+ 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. -
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. -
Side scoping — DECIDED (all parts). CvdReactor-only:
_A⇒ Left,_B⇒ Right;_LTstripped before reading the side (leak-test sources still included). (a) sharedReactorAlarms(Z28061.) included on both sides — reactor-wide faults apply regardless of side. (b) missing suffix:SimpleAlarmStatuserrors on a SAPID without_A/_B(matchesMesMoveIn);AlarmStatusselected byCode/ZTag/MachineID(no SAPID) returns all sources; a SAPID selector without a suffix errors. -
Roles / auth — DECIDED: no new roles; create
AlarmStatusas Designer via CLI; authorize the existing MES API key (the one already callingMesMoveIn/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] — ✅ DONE 2026-08-01
- ✅ DONE 2026-08-01 — AckTime enrichment (§6.4): additive
AckTimeonAlarmStateChanged, the vendoredAlarmStateUpdateproto (manual regen),native_alarm_statepersistence, and the DCL ack-transition stamping (source ack time where supplied, else observation time).AlarmStateChanged.AckTime(init-only,nulldefault) +NativeAlarmTransition.AckTime(trailing optional positional — all 14-arg call sites unchanged).- Proto: field 24
google.protobuf.Timestamp ack_timeonAlarmStateUpdate; regenerated withdocker/regen-proto.sh sitestream(csproj diff verified empty). Packed/unpacked byStreamRelayActor/SiteStreamGrpcClient; an absent Timestamp round-trips tonull. - Stamping rule (both protocols): non-null only while the condition is active AND acknowledged — that one predicate yields "null while unacked", "cleared on re-raise", and no phantom ack on the MxGateway return-to-normal (which maps
INACTIVE → Acknowledged = true). Lives in the pureOpcUaAlarmMapper.DeriveAckTime/MxGatewayAlarmMapper.DeriveAckTime. - OPC UA gets a true source ack instant: new SelectClause index 18 =
AcknowledgeableConditionType/AckedState/TransitionTime, appended so indices 0–17 keep their meaning; falls back to the event'sTimewhen the server omits it. MxGateway uses the ack transition's own timestamp (its feed carries no ack time), and anACTIVE_ACKEDre-subscribe snapshot restores one fromLastTransitionTimestamp. - Persistence: rides
native_alarm_state's existingmetadata_jsonblob, not a new column — that table isRegisterReplicatedinSiteLocalDbSetupand LocalDb builds its CDC triggers from the column list at registration time, so an additive JSON property changes no schema, no triggers and no replication contract (metadata_jsonis exactly the extension point UA4 added). Pre-AckTime rows deserialize it asnull.
- ✅ DONE 2026-08-01 — Add
AlarmsAccessor+ScriptAlarmto the runtime context (ScriptRuntimeContext) — local Ask to the Instance Actor; projectAlarmStateChanged→ScriptAlarm(incl.AckTime). Add internal request/response message if not reusingDebugSnapshotRequest.- Dedicated
GetAlarmSnapshotRequest/GetAlarmSnapshotResponse(CommonsMessages/Instance) rather than reusingDebugSnapshotRequest, which would materialise every attribute value on every alarm poll. Served from the sameBuildAlarmStatesSnapshot()the Debug View uses, so the two can never disagree. AlarmsAccessorsits inScopeAccessors.csbeside the other accessors but is not scope-prefixed — alarm identity is not a scope-relative attribute name, so every scope sees the whole list. Exposed asScriptRuntimeContext.Alarmsand the top-levelScriptGlobals.Alarms.
- Dedicated
- ✅ DONE 2026-08-01 — Mirror the stub on
ScriptCompileSurface(and confirmTriggerCompileSurfacenot needed — trigger expressions don't read alarms).CompileAlarmsAccessorreturns the sameScriptAlarmtype as the runtime (Commons is already inDefaultAssemblies), so field access binds identically at the gate and at the site.TriggerCompileSurfaceconfirmed not needed.- Also mirrored on the third hand-maintained surface, the Central UI Test-Run
SandboxScriptHost— without it the design page would false-flag CS1061 on scripts the deploy gate accepts. It throws a labelledScriptSandboxExceptionat run time (no central route to per-instance alarm state) rather than returning an empty list that would read as "nothing is in alarm".
- ✅ DONE 2026-08-01 — Confirm/extend
ScriptTrustPolicyallow-list soAlarmsis permitted; no new forbidden APIs.- No change needed, and the reason is structural: the trust boundary is a deny-list over API roots, not an allow-list of context members. Pinned by a test asserting no
ForbiddenScopesentry prefixes the Commons script-surface namespace, so a future deny-list entry cannot silently makeScriptAlarmuntouchable.
- No change needed, and the reason is structural: the trust boundary is a deny-list over API roots, not an allow-list of context members. Pinned by a test asserting no
- ✅ DONE 2026-08-01 — 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.- New
AlarmsAccessorTests(6),NativeAlarmActorAckTime emit/rehydrate/pre-AckTime-row (3),InstanceActoralarm-snapshot (2), mapper AckTime (4 OPC UA + 6 MxGateway), proto round-trip (1), Commons additive/back-compat (4), compile-surface + trust (4),SandboxScriptHostdiagnose-clean (1).AlarmsAccessoradded to theCompileSurfaceParityTestsmirror pairs; the OPC UA SelectClause count lock-in went 18 → 19 with an index-18 assertion (intended — the clause is appended).
- New
- ✅ DONE 2026-08-01 — Doc: update
Component-SiteRuntime.md+Component-DataConnectionLayer.md(native-mirror AckTime) + Script Analysis #25 surface list; note the accessor inComponent-InboundAPI.mdrouting examples.- Also updated the
CLAUDE.mdnative-alarm bullet. The Inbound API note records the negative decision: noRoute.To(...).GetAlarms(...)verb — alarm reads go through a routed site script so the filtering happens where the data lives.
- Also updated the
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]:
Alarmsaccessor projection; compile-surface acceptance; trust-policy allow; inbound param/return validation forAlarmStatus(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]:
curlsimplealarmstatusandalarmstatusagainst the deployed reactor; cross-check returned alarms against the CLI debug snapshot (debug snapshot --id <instance>) alarm table for the same instance; verify onlyActivealarms 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— resolved by the §6.1 severity-band decision (no follow-up needed).IsFlaggedForMESallow-listPrecise— pulled INTO scope by §6.4 (mirror enriched now).AckDTfollow-up- 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/AlarmStatusroot scripts against the same contract).
10. Summary of changes
| Artifact | Type | Change |
|---|---|---|
AlarmStateChanged + vendored AlarmStateUpdate proto (field 24) + native_alarm_state (metadata_json) + DCL stamping (OPC UA SelectClause index 18) |
[repo] ✅ | Additive AckTime enrichment (§6.4) |
ScriptRuntimeContext + ScriptGlobals |
[repo] ✅ | New Alarms accessor + ScriptAlarm (incl. AckTime); local alarm-snapshot Ask |
ScriptCompileSurface + Central UI SandboxScriptHost |
[repo] ✅ | Mirror Alarms stub on both design-time surfaces |
ScriptTrustPolicy (#25) |
[repo] ✅ | Verified — no change needed (deny-list, not member allow-list); pinned by test |
GetAlarmSnapshotRequest/Response (Commons) |
[repo] ✅ | New, additive — chosen over reusing DebugSnapshotRequest |
Site Runtime + DCL + Script Analysis + Inbound API docs + CLAUDE.md |
[repo] ✅ | Document Alarms accessor, AckTime, endpoints |
| Unit tests (Commons / DCL / SiteRuntime / ScriptAnalysis / Communication / CentralUI) | [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 900–999) |
| Galaxy alarm priorities | [external] | MES-relevant CvdReactor alarms configured into 900–999 (§6.1 prerequisite) |