From 73f2a032e022121130fd12305ca83b6fdd6a7515 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 11 Sep 2026 09:50:54 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20MES-reportable=20alarm=20flag=20?= =?UTF-8?q?=E2=80=94=20design=20+=2016-task=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the legacy BTDB MachineAlarm.FlaggedForMES lookup onto TemplateAlarm.IsMesReportable (instance-overridable), surfaces it as ScriptAlarm.IsMesReportable / Alarms.MesReportableAsync(), and adds an AlarmStatus inbound method demo on the docker rig. Native alarms stay on the severity band (no per-alarm row). Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB --- ...-09-11-mes-reportable-alarm-flag-design.md | 237 ++++++ .../2026-09-11-mes-reportable-alarm-flag.md | 672 ++++++++++++++++++ ...11-mes-reportable-alarm-flag.md.tasks.json | 22 + 3 files changed, 931 insertions(+) create mode 100644 docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md create mode 100644 docs/plans/2026-09-11-mes-reportable-alarm-flag.md create mode 100644 docs/plans/2026-09-11-mes-reportable-alarm-flag.md.tasks.json diff --git a/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md b/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md new file mode 100644 index 00000000..3d2c8a93 --- /dev/null +++ b/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md @@ -0,0 +1,237 @@ +# MES-Reportable Alarm Flag — Design + +**Date:** 2026-09-11 +**Status:** DRAFT — awaiting user review. Nothing implemented; nothing committed. +**Supersedes (partially):** the severity-band decision in +[`2026-06-30-mes-alarm-status-api.md`](2026-06-30-mes-alarm-status-api.md) §6.1 for **computed** +alarms only (native alarms keep the band — see §2.3). +**Component touchpoints:** Template Engine (#1), Configuration Database, Site Runtime (#3), +Script Analysis (#25), Central UI, CLI, Transport (#24), Inbound API (#14, deployed config only). +**Mockup:** https://claude.ai/code/artifact/02ffcbf9-b87f-4e12-852d-38e2910c6056 (artifact "MES Reportable Alarms"). +**Implementation plan:** [`2026-09-11-mes-reportable-alarm-flag.md`](2026-09-11-mes-reportable-alarm-flag.md) (16 tasks). + +--- + +## 1. Goal + +The legacy WWSupport / APIServer MES alarm endpoints decided which alarms were MES-relevant by a +per-machine row in the BTDB `MachineAlarm` table (`Name`, `Severity`, `FlaggedForMES`). ScadaBridge +already owns the alarm definition on the template, so the flag belongs there too: + +1. Add a **`IsMesReportable` boolean (checkbox, default `false`)** to template alarms, and a + matching **tri-state override** (`inherited` / `true` / `false`) on instance alarm overrides. +2. Carry the flag through flattening to the site so the script engine's alarm read + (`Alarms.CurrentAsync()`) exposes it and a script can **filter by it**. +3. Ship an **inbound API method that routes to the instance and performs the alarm check**, + returning the legacy `AlarmStatusResponse` shape with `IsFlaggedForMES` driven by the flag. +4. Prove all of it on the **local docker rig** (`docker/`) — seed template, instance, method, key, + trip an alarm, call the endpoint. + +No BTDB dependency anywhere. Everything the check needs lives on the deployed configuration. + +--- + +## 2. Decisions and scope + +### 2.1 Where the flag lives (data model) + +| Layer | Change | Semantics | +|---|---|---| +| `TemplateAlarm` (Commons entity) | `bool IsMesReportable` | Default `false`. Inherits down the template chain like `PriorityLevel`. | +| `InstanceAlarmOverride` (Commons entity) | `bool? IsMesReportableOverride` | `null` = inherit from template (the existing nullable-means-inherited convention, same as `PriorityLevelOverride`). | +| `ResolvedAlarm` (flattened DTO) | `bool IsMesReportable` | Effective value after override; rides `deployed_configurations.config_json` unchanged — **no site schema change**. | +| Config DB | migration `AddAlarmMesReportable` | `TemplateAlarms.IsMesReportable bit NOT NULL DEFAULT 0`; `InstanceAlarmOverrides.IsMesReportableOverride bit NULL`. | + +Naming follows the entity idiom already in the file (`IsLocked`, `IsInherited`). The legacy JSON +name `IsFlaggedForMES` is preserved **only on the API response**, where MES clients depend on it. + +**Locking:** the flag is an ordinary overridable field. A `Locked` alarm cannot be overridden at all +(existing rule, `LockEnforcer.ValidateAlarmOverride`); an unlocked alarm's flag can be flipped per +instance. `Name`/`TriggerType` stay the only fixed fields. + +### 2.2 How the site exposes it (script engine) + +The flag is stamped onto the alarm snapshot the script engine already reads: + +- `InstanceActor` records `IsMesReportable` per alarm at Alarm Actor spawn (beside the existing + `_alarmPriorities` map) and **stamps it in `BuildAlarmStatesSnapshot()`** on every row + (`with { IsMesReportable = … }`), retained or fallback. One place, so it cannot drift from an emit + site. `AlarmActor` itself is untouched. +- `AlarmStateChanged` gains an additive init-only `bool IsMesReportable` (default `false`) — the + same additive convention as `AckTime`. **It is deliberately NOT added to the gRPC protos in this + slice**; central never sees it (see §2.4). +- `ScriptAlarm` gains a trailing `bool IsMesReportable` positional parameter. Native rows are + always `false`. +- `AlarmsAccessor` gains **`MesReportableAsync(ct)`** = `CurrentAsync` filtered to + `IsMesReportable`. `CurrentAsync()` is unchanged (an optional bool before the `CancellationToken` + would break positional callers). Scripts can equally `.Where(a => a.IsMesReportable)`. +- The accessor stub is mirrored on both hand-maintained compile surfaces — `ScriptCompileSurface` + (`CompileAlarmsAccessor`) and the Central UI `SandboxScriptHost` (`SandboxAlarmsAccessor`) — and + pinned by `CompileSurfaceParityTests`. `FlowShapePredictor` does not name accessor members, so + it needs no change. + +### 2.3 Native alarms are OUT of scope (and why) + +Native alarms (OPC UA A&C / MxGateway mirrors) have **no per-alarm configuration row** anywhere: +only the *source binding* (`TemplateNativeAlarmSource`) is configured, and the individual conditions +are discovered at runtime. There is nowhere to put a per-alarm checkbox. + +Consequences, stated plainly: + +- This feature covers **computed `TemplateAlarm`s**. The production CvdReactor endpoints, whose + alarms are all native sources, are **not changed** by it and keep the severity-band convention + (900–999) from the 2026-06-30 plan. `IsFlaggedForMES` for a native row therefore stays + `Severity in band`; for a computed row it becomes the flag. +- Follow-up options, recorded in the deferred-work register: (a) a source-level + `IsMesReportable` on `TemplateNativeAlarmSource` + its override ("every condition under this + binding is reportable"), or (b) a new per-condition config entity — the first of its kind. Neither + is designed here. + +### 2.4 Central visibility is a follow-up + +The Alarm Summary / Debug View rows are `AlarmStateChanged` values that arrive over gRPC. Showing a +"MES" badge there needs additive proto fields (`AlarmStateUpdate` field 25, `DebugAlarmStateDto` +field 20, manual `regen-proto.sh`) plus mapper changes. Not needed for the script check or the +inbound API, so it is **deferred** (register row) to keep this slice free of wire changes. +Design-time surfaces (template Alarms tab, Instance Configure overrides) DO show the flag. + +### 2.5 The inbound API method (deployed config, rig) + +`AlarmStatus` — the full-filter endpoint from the legacy spec, minus the BTDB machine lookup +(the rig has no BTDB; `MachineCode` is the instance `UniqueName`, which is exactly what +`Route.To(code)` resolves). Production adds its `SAPID → Code` lookup in front, as `MesMoveIn` does. + +Request (`--parameters`, JSON Schema): +```json +{ "type":"object", + "properties":{ + "MachineCode":{"type":"string"}, + "AlarmFilter":{"type":"object","properties":{ + "FlaggedOnly":{"type":"boolean"}, "IncludeAcked":{"type":"boolean"}, + "MinSeverity":{"type":"integer"}, "MaxSeverity":{"type":"integer"}, + "NameFilter":{"type":"string"}}}}, + "required":["MachineCode"] } +``` +Response (`--return-def`): `{ WasSuccessful, ErrorText, Alarms: AlarmInfo[] }` with +`AlarmInfo = { Name, HierarchicalName, Description, IsFlaggedForMES, Severity, StatusCode, +TriggeredDT, AckDT, AckComment }` — byte-compatible with the legacy contract. + +Router body: `Route.To(MachineCode).Call("AlarmStatus", { MachineCode, ...AlarmFilter })`, +catching `"not found on instance"` → `Alarms status is not supported on machine ''`, any +other exception → `WasSuccessful=false` with `Alarms` cleared (legacy all-or-nothing). + +Site script `AlarmStatus` (Call, root of the demo template): `Alarms.CurrentAsync()` → +`Active && !IsConfiguredPlaceholder` → `FlaggedOnly ? IsMesReportable : true` → severity bounds → +name substring → `IncludeAcked` → project to `AlarmInfo` with `IsFlaggedForMES = a.IsMesReportable`. +Filter order matches the legacy spec. + +A `SimpleAlarmStatus` variant (flagged-only, acked always included) is one extra `api-method create` +with the same site script and `FlaggedOnly=true` hard-wired; included in the seed as the second +endpoint so both legacy routes are demonstrated. + +### 2.6 Rig demo (all local) + +`docker/seed-mes-alarm-demo.sh` (idempotent by name lookup, same style as `seed-flow-log-demo.sh`): + +| Object | Detail | +|---|---| +| Template `MesAlarmDemo` | static `Double` attribute `Temperature` = 20 (static ⇒ a value-change trips alarms synchronously, no device round-trip, no OPC UA String-only trap). | +| Alarm `HighTemp` | HiLo on `Temperature`, hi 80 / hihi 95, priority 500, **`--mes-reportable`**. | +| Alarm `TempSpike` | ValueMatch `Temperature == 100`, priority 300, not reportable. | +| Alarm `SensorFault` | ValueMatch `Temperature == -1`, priority 700, not reportable **on the template**. | +| Script `AlarmStatus` | Call, root; body per §2.5. | +| Instance `MesAlarmDemo01` | site 1; **alarm override `SensorFault` → `IsMesReportableOverride = true`** to prove the override path; deployed. | +| Inbound `AlarmStatus`, `SimpleAlarmStatus` | per §2.5; one API key `mes-alarm-demo` scoped to both. | + +`docker/verify-mes-alarm-demo.sh`: + +1. `flow-debug run-value-change --instance-id … --attribute Temperature --value 96` → HighTemp active. +2. `curl POST /api/AlarmStatus {"MachineCode":"MesAlarmDemo01"}` → 1 row, `IsFlaggedForMES=true`. +3. `--value 100` → TempSpike active; `AlarmStatus` with `FlaggedOnly=true` → still only + HighTemp-style rows (TempSpike excluded); `FlaggedOnly=false` → both, TempSpike `false`. +4. `--value -1` → SensorFault active; `SimpleAlarmStatus` → SensorFault present with + `IsFlaggedForMES=true` (override wins over the template's `false`). +5. `--value 20` → all clear; `AlarmStatus` → empty `Alarms`, `WasSuccessful=true`. +6. `AlarmStatus {"MachineCode":"DelmiaSim01"}` (no script) → `WasSuccessful=false`, "not + supported on machine". +7. Cross-check step 2 against `debug snapshot --id …` and the Flow Debugger replay. + +--- + +## 3. Data flow + +``` +Central UI / CLI ──AddTemplateAlarm(IsMesReportable)──► ManagementActor ► TemplateService ► TemplateAlarms +Central UI / CLI ──SetInstanceAlarmOverride(IsMesReportableOverride?)──► InstanceService ► InstanceAlarmOverrides + │ + FlatteningService.ResolveInheritedAlarms → ResolvedAlarm.IsMesReportable + FlatteningService.ApplyInstanceAlarmOverrides (ovr ?? existing) + │ deploy (config_json, unchanged schema) + ▼ +Site: InstanceActor spawns AlarmActors; _alarmMesReportable[name] = alarm.IsMesReportable + BuildAlarmStatesSnapshot() stamps IsMesReportable on every AlarmStateChanged row + GetAlarmSnapshotResponse ─► ScriptRuntimeContext.ToScriptAlarm ─► ScriptAlarm.IsMesReportable + │ + site script AlarmStatus: Alarms.CurrentAsync() / MesReportableAsync() → AlarmInfo[] + ▲ +Central inbound method AlarmStatus ── Route.To(MachineCode).Call("AlarmStatus", filter) ──┘ + ◄── X-API-Key ── MES client (curl on the rig) +``` + +--- + +## 4. Alternatives considered + +| Option | Verdict | +|---|---| +| **Keep the severity band (900–999) as the flag** (2026-06-30 decision) | Rejected for computed alarms: it couples MES relevance to priority, which operators tune for other reasons, and it is invisible in the template editor. Kept for native alarms only because there is no per-alarm row. | +| **Central lookup table** (a `MesReportableAlarms` table joined at request time, i.e. re-creating BTDB `MachineAlarm`) | Rejected: the check runs at the site (that is the whole point of `Route.To().Call`) and the central table would be a second source of truth beside the template. | +| **Stamp the flag in `AlarmActor` emits** instead of the snapshot | Rejected: three emit sites to keep in sync; the snapshot builder already owns the "fallback row for a quiet alarm" case, so stamping there covers fired and unfired alarms in one place. | +| **`CurrentAsync(bool reportableOnly = false, ct)`** | Rejected: an optional bool before the token breaks existing positional `CurrentAsync(ct)` callers on three mirrored surfaces. A separate `MesReportableAsync` is additive. | +| **Put `IsMesReportable` on the protos now** | Deferred (§2.4): no consumer in this slice; proto regen is manual and adds review surface. | + +--- + +## 5. Error handling + +- **Override on a locked alarm** → existing `LockEnforcer` error; UI hides locked alarms from the + override list already. +- **Override with no delta** — Instance Configure deletes an override when neither config nor + priority is set; the delta check must include the tri-state so a reportable-only override is + **not** silently deleted (known trap, `InstanceConfigure.razor` ~line 1628). +- **Old bundles / old artifacts**: the Transport DTO fields are trailing with defaults; a bundle + without them imports as `false`/`null`. A `FlattenedConfiguration` serialized before the change + deserializes with `IsMesReportable=false`. +- **Mixed version**: a NEW central deploying to an OLD site — the site ignores the unknown JSON + property; scripts there see `false` everywhere. Upgrade sites with the central, as always. +- **Inbound method**: unknown machine → `Instance '…' not found` text passed through as + `ErrorText`; missing script → the "not supported" mapping; everything else → `WasSuccessful=false` + with `Alarms` cleared. Never an HTTP 500 for a handled case. + +--- + +## 6. Testing + +| Layer | Tests | +|---|---| +| Commons | entity + message convention sweeps pass unchanged (plain `bool`, record init-prop); `ScriptAlarm` positional back-compat test. | +| Config DB | migration applies on SQL Server + SQLite test double; `SchemaConfigurationTests` persist round-trip of the two new columns. | +| Template Engine | `FlatteningServiceTests`: template `true` → resolved `true`; override `false` wins; override `null` inherits; inherited (derived-template) alarm carries base value; `LockEnforcerTests` unchanged. | +| Site Runtime | `InstanceActor` snapshot stamps the flag on fired AND unfired alarms; `AlarmsAccessorTests`: `MesReportableAsync` filters; `CompileSurfaceParityTests` sees the new member on all mirrors. | +| Script Analysis / Central UI | compile-surface + `SandboxScriptHost` compile a script using `MesReportableAsync()` and `a.IsMesReportable`. | +| Management | `ManagementActorTests` add/update/override carry the field; authz matrix unchanged (no new command names). | +| CLI | `EditingParityTests` rows for `template alarm add --mes-reportable` and `instance alarm-override set --mes-reportable false`; README updated. | +| Transport | `RoundTripEquivalenceTests` seeds non-default values (the reflection sweep fails otherwise); `TemplateChildEquality`/`ArtifactDiff` tests see the flag as a change. | +| Central UI | structural test on `TemplateAlarmDialog.razor` (`@bind="_isMesReportable"`); bUnit/Playwright override round-trip `AlarmOverride_MesReportable_RoundTrips` beside the priority one. | +| Rig | `docker/verify-mes-alarm-demo.sh` steps 1–7 (§2.6). | + +--- + +## 7. Documents to update in the same change + +`Component-TemplateEngine.md` (alarm fields, override granularity table), `Component-SiteRuntime.md` +(`Alarms` accessor + `MesReportableAsync`), `Component-ScriptAnalysis.md` (surface list), +`Component-InboundAPI.md` (routing example: AlarmStatus), `Component-CentralUI.md` (dialog + override +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). diff --git a/docs/plans/2026-09-11-mes-reportable-alarm-flag.md b/docs/plans/2026-09-11-mes-reportable-alarm-flag.md new file mode 100644 index 00000000..bf9e0493 --- /dev/null +++ b/docs/plans/2026-09-11-mes-reportable-alarm-flag.md @@ -0,0 +1,672 @@ +# MES-Reportable Alarm Flag Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the legacy BTDB `MachineAlarm.FlaggedForMES` lookup with an `IsMesReportable` checkbox on template alarms (instance-overridable), expose it to site scripts through `Alarms.CurrentAsync()` / `Alarms.MesReportableAsync()`, and prove it end-to-end on the docker rig through an `AlarmStatus` inbound API method. + +**Architecture:** One additive `bool` on `TemplateAlarm` + one `bool?` on `InstanceAlarmOverride` (null = inherit), resolved by the flattener into `ResolvedAlarm.IsMesReportable`, which already ships to the site inside `deployed_configurations.config_json`. The site `InstanceActor` stamps the flag on its alarm snapshot in ONE place (`BuildAlarmStatesSnapshot`), so `ScriptAlarm.IsMesReportable` is correct for fired and unfired alarms. No proto/wire change; native alarms (source bindings, no per-alarm row) are out of scope and stay on the 900–999 severity band. The inbound method and site script are deployed config seeded by `docker/seed-mes-alarm-demo.sh`. + +**Tech Stack:** .NET 10, EF Core (SQL Server migrations), Akka.NET, Roslyn scripting, Blazor Server + Bootstrap, System.CommandLine CLI, bash + `scadabridge` CLI on the docker rig. + +**Design:** `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md`. Mockup: artifact "MES Reportable Alarms". + +**Conventions for every task** (from `CLAUDE.md` + memory): +- Work in a dedicated worktree (Task 0). `EnterWorktree` is broken here — use `git worktree add`. +- Run **targeted** tests per task (`dotnet test --filter …`), the affected-project build, and the full solution build only in Task 15. +- Never `git stash` (shared across worktrees). Commit with pathspecs. End every commit message with + `Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB`. +- Every public member needs an XML doc comment (`checkdocs` enforces it). +- Message contracts and Transport DTOs evolve **additively with trailing defaulted parameters** — never reorder. + +--- + +### Task 0: Worktree + +**Classification:** trivial +**Estimated implement time:** ~2 min +**Parallelizable with:** none + +**Files:** none (git only) + +**Step 1:** Create the worktree and confirm a clean build baseline. +```bash +cd /Users/dohertj2/Desktop/ScadaBridge +git worktree add -b feat/mes-reportable-alarm-flag ../ScadaBridge-mes-reportable main +cd ../ScadaBridge-mes-reportable +dotnet build ZB.MOM.WW.ScadaBridge.slnx 2>&1 | tail -3 +``` +Expected: `0 Error(s)`. All later tasks run from `/Users/dohertj2/Desktop/ScadaBridge-mes-reportable` (absolute paths in every command — Bash cwd drifts). + +--- + +### Task 1: Commons — entity, override and resolved-alarm fields + +**Classification:** small +**Estimated implement time:** ~3 min +**Parallelizable with:** none (everything depends on it) + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateAlarm.cs:18` (after `IsLocked`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceAlarmOverride.cs:44` (after `PriorityLevelOverride`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs:125` (after `IsLocked` in `ResolvedAlarm`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/EntityConventionTests.cs` (existing sweep, must stay green) + +**Step 1: Add the three properties** + +`TemplateAlarm.cs`: +```csharp + /// + /// When true, the alarm is reported to MES by the alarm-status API + /// (IsFlaggedForMES on the legacy contract). Default false. Overridable per + /// instance via + /// unless the alarm is locked. + /// + public bool IsMesReportable { get; set; } +``` +`InstanceAlarmOverride.cs`: +```csharp + /// + /// Replaces the alarm's IsMesReportable flag when set. null = keep inherited. + /// + public bool? IsMesReportableOverride { get; set; } +``` +`FlattenedConfiguration.cs` (`ResolvedAlarm`): +```csharp + /// Gets whether the alarm is reported to MES (template value after any instance override). + public bool IsMesReportable { get; init; } +``` + +**Step 2: Build + run the convention sweeps** +```bash +dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter "FullyQualifiedName~EntityConventionTests|FullyQualifiedName~MessageConventionTests" 2>&1 | tail -3 +``` +Expected: all pass. + +**Step 3: Commit** +```bash +git add src/ZB.MOM.WW.ScadaBridge.Commons +git commit -m "feat(commons): IsMesReportable on TemplateAlarm, ResolvedAlarm; IsMesReportableOverride on InstanceAlarmOverride" +``` + +--- + +### Task 2: Configuration Database — migration + +**Classification:** high-risk (migration) +**Estimated implement time:** ~4 min +**Parallelizable with:** Task 3, Task 6, Task 8 (after Task 1) + +**Files:** +- Create: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/_AddAlarmMesReportable.cs` (+ `.Designer.cs`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ScadaBridgeDbContextModelSnapshot.cs` (generated) +- Test: `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SchemaConfigurationTests.cs:144-157` + +**Step 1: Write the failing test** — extend the existing TemplateAlarm persist test (or add beside it): +```csharp + [Fact] + public async Task TemplateAlarm_And_Override_PersistMesReportableFlags() + { + // arrange as the sibling test does (in-memory/SQLite context + a template + an instance) + var alarm = new TemplateAlarm("HighTemp") { TemplateId = template.Id, TriggerType = AlarmTriggerType.HiLo, PriorityLevel = 500, IsMesReportable = true }; + var ovr = new InstanceAlarmOverride("HighTemp") { InstanceId = instance.Id, IsMesReportableOverride = false }; + ctx.TemplateAlarms.Add(alarm); ctx.InstanceAlarmOverrides.Add(ovr); + await ctx.SaveChangesAsync(); + ctx.ChangeTracker.Clear(); + + Assert.True((await ctx.TemplateAlarms.SingleAsync(a => a.Id == alarm.Id)).IsMesReportable); + Assert.False((await ctx.InstanceAlarmOverrides.SingleAsync(o => o.Id == ovr.Id)).IsMesReportableOverride); + } +``` + +**Step 2: Scaffold the migration — BUILD FIRST, no `--no-build`** (memory: `--no-build` scaffolds an empty migration): +```bash +dotnet build src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase 2>&1 | tail -1 +dotnet ef migrations add AddAlarmMesReportable \ + --project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase \ + --startup-project src/ZB.MOM.WW.ScadaBridge.Host +``` +Verify the `Up()` matches the `20260517000628_AddDerivedAlarmFields.cs` shape: +```csharp + migrationBuilder.AddColumn(name: "IsMesReportable", table: "TemplateAlarms", type: "bit", nullable: false, defaultValue: false); + migrationBuilder.AddColumn(name: "IsMesReportableOverride", table: "InstanceAlarmOverrides", type: "bit", nullable: true); +``` +If the file is empty, `rm` the two generated files and re-run the build + add. + +**Step 3: Run the tests** +```bash +dotnet test tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests --filter "FullyQualifiedName~SchemaConfigurationTests" 2>&1 | tail -3 +``` +Expected: PASS. No explicit EF mapping change is needed (`bool`/`bool?` map by convention). + +**Step 4: Commit** +```bash +git add src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests +git commit -m "feat(config-db): AddAlarmMesReportable migration (TemplateAlarms.IsMesReportable, InstanceAlarmOverrides.IsMesReportableOverride)" +``` + +--- + +### Task 3: Template Engine — flattening + +**Classification:** standard +**Estimated implement time:** ~4 min +**Parallelizable with:** Task 2, Task 6, Task 8 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:450-461` (projection) and `:358-363` (override merge) +- Test: `tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs` (model on `Flatten_Alarms_ResolvedFromChain` at :221) + +**Step 1: Write three failing tests** (copy the arrange block of `Flatten_Alarms_ResolvedFromChain`): +- `Flatten_MesReportable_TemplateValueCarried` — template alarm `IsMesReportable = true` → resolved `IsMesReportable == true`; a second alarm left default → `false`. +- `Flatten_MesReportable_OverrideFalseWins` — template `true`, override `IsMesReportableOverride = false` → resolved `false`, `Source == "Override"`. +- `Flatten_MesReportable_OverrideNullInherits` — template `true`, override with only `PriorityLevelOverride = 7` → resolved `IsMesReportable == true`. + +**Step 2: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests --filter "FullyQualifiedName~Flatten_MesReportable"` → 3 FAIL. + +**Step 3: Implement** +In the projection (`:450`): +```csharp + IsLocked = alarm.IsLocked, + IsMesReportable = alarm.IsMesReportable, +``` +In `ApplyInstanceAlarmOverrides` (`:358`): +```csharp + PriorityLevel = ovr.PriorityLevelOverride ?? existing.PriorityLevel, + IsMesReportable = ovr.IsMesReportableOverride ?? existing.IsMesReportable, +``` + +**Step 4: Run** the same filter → 3 PASS; then the whole `Flattening` folder filter to confirm no regression. + +**Step 5: Commit** +```bash +git add src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening +git commit -m "feat(template-engine): flatten IsMesReportable with null-inherits override semantics" +``` + +--- + +### Task 4: Template Engine — service write paths + +**Classification:** standard +**Estimated implement time:** ~4 min +**Parallelizable with:** none (Task 5, 10, 11 depend on the new signature) + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:~619-627` (`UpdateAlarmAsync` field copy block: `existing.PriorityLevel = proposed.PriorityLevel; …`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs:218-280` (`SetAlarmOverrideAsync`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/` (the existing `TemplateService`/`InstanceService` alarm tests — find with `grep -rn "UpdateAlarmAsync\|SetAlarmOverrideAsync" tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests`) + +**Step 1: Failing tests** +- `UpdateAlarmAsync_CopiesIsMesReportable`: existing alarm `false`, proposed `true` → stored `true`. +- `SetAlarmOverrideAsync_StoresIsMesReportableOverride`: call with `isMesReportableOverride: true` → repo receives an override with `IsMesReportableOverride == true`; a second call with `null` clears it. + +**Step 2: Implement** +`TemplateService.UpdateAlarmAsync` — beside `existing.IsLocked = proposed.IsLocked;`: +```csharp + existing.IsMesReportable = proposed.IsMesReportable; +``` +`InstanceService.SetAlarmOverrideAsync` — new trailing optional parameter **after `user`** (all current callers pass five positional args; grep `SetAlarmOverrideAsync(` across `src/` and `tests/` to confirm): +```csharp + string user, + bool? isMesReportableOverride = null, + CancellationToken cancellationToken = default) +``` +and in both branches: +```csharp + existingOverride.IsMesReportableOverride = isMesReportableOverride; +… + PriorityLevelOverride = priorityLevelOverride, + IsMesReportableOverride = isMesReportableOverride +``` +Update the XML `` docs. `AddAlarmAsync` needs nothing — it persists the entity as given. + +**Step 3: Run** the TemplateEngine test project filtered to `Alarm` → PASS. + +**Step 4: Commit** `feat(template-engine): persist IsMesReportable on alarm update; IsMesReportableOverride on SetAlarmOverrideAsync`. + +--- + +### Task 5: Management commands + ManagementActor + +**Classification:** standard +**Estimated implement time:** ~4 min +**Parallelizable with:** none + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs:45-46` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs:39-43` +- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:3173-3201`, `:1171-1182` +- Test: `tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs` + +**Step 1: Failing tests** (model on the existing add-alarm / set-override tests in `ManagementActorTests`): +- `AddTemplateAlarm_CarriesIsMesReportable` — send `new AddTemplateAlarmCommand(1, "HighTemp", "HiLo", 500, null, null, false, IsMesReportable: true)`; assert the `TemplateAlarm` handed to `TemplateService.AddAlarmAsync` has `IsMesReportable == true`. +- `SetInstanceAlarmOverride_PassesIsMesReportableOverride` — `new SetInstanceAlarmOverrideCommand(10, "HighTemp", null, null, IsMesReportableOverride: false)` → `InstanceService.SetAlarmOverrideAsync` receives `false`. + +**Step 2: Implement** — trailing optionals, never reorder: +```csharp +public record AddTemplateAlarmCommand(int TemplateId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked, bool IsMesReportable = false); +public record UpdateTemplateAlarmCommand(int AlarmId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked, bool IsMesReportable = false); +``` +```csharp +public record SetInstanceAlarmOverrideCommand( + int InstanceId, + string AlarmCanonicalName, + string? TriggerConfigurationOverride, + int? PriorityLevelOverride, + bool? IsMesReportableOverride = null); +``` +`HandleAddAlarm` / `HandleUpdateAlarm`: add `IsMesReportable = cmd.IsMesReportable` to both initializers. `HandleSetInstanceAlarmOverride`: pass `cmd.IsMesReportableOverride` after `user.Username`. + +Note the PLAN-08 T9 finding: Newtonsoft ignores C# ctor optional-parameter defaults on the wire → an old CLI that omits the field yields `default(T)` = `false` / `null`, which is exactly the intended inherit/off default. + +**Step 3: Run** +```bash +dotnet test tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests --filter "FullyQualifiedName~ManagementActorTests|FullyQualifiedName~RequiredRoleMatrixTests" 2>&1 | tail -3 +``` +Expected: PASS. `RequiredRoleMatrixTests` is untouched — no new command names. + +**Step 4: Commit** `feat(management): IsMesReportable on alarm commands, IsMesReportableOverride on SetInstanceAlarmOverride`. + +--- + +### Task 6: Site Runtime — stamp the flag on the alarm snapshot + +**Classification:** high-risk (actor) +**Estimated implement time:** ~4 min +**Parallelizable with:** Task 2, Task 3, Task 8 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs:109` (append after `IsConfiguredPlaceholder`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:53` (field), `:1982` (spawn), `:1569-1613` (`BuildAlarmStatesSnapshot`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs` (has the `GetAlarmSnapshotRequest` harness — add beside it) + +**Step 1: Failing test** `AlarmSnapshot_StampsIsMesReportable_OnFiredAndUnfiredAlarms`: deploy a `FlattenedConfiguration` with two computed alarms (`HighTemp` `IsMesReportable=true`, `TempSpike` `false`); drive `HighTemp` active (write the attribute past the setpoint, as the sibling tests do); Ask `GetAlarmSnapshotRequest`; assert the `HighTemp` row (retained event) has `IsMesReportable == true` and the `TempSpike` row (fallback) `false`. + +**Step 2: Implement** +`AlarmStateChanged.cs`: +```csharp + /// + /// True when the alarm's deployed configuration flags it as MES-reportable + /// (ResolvedAlarm.IsMesReportable). Stamped by the Instance Actor on its alarm + /// snapshot; always false for native conditions, which have no per-alarm config row. + /// NOT carried on the gRPC stream in this slice (no proto field) — central sees the + /// default. Additive; default false. + /// + public bool IsMesReportable { get; init; } +``` +`InstanceActor.cs`: +```csharp + private readonly Dictionary _alarmMesReportable = new(); +… + _alarmPriorities[alarm.CanonicalName] = alarm.PriorityLevel; + _alarmMesReportable[alarm.CanonicalName] = alarm.IsMesReportable; +``` +`BuildAlarmStatesSnapshot()` — stamp every row in one place (records support `with`): +```csharp + var states = _latestAlarmEvents.Values + .Select(e => e with { IsMesReportable = _alarmMesReportable.GetValueOrDefault(e.AlarmName) }) + .ToList(); + foreach (var name in _alarmActors.Keys) + { + if (_latestAlarmEvents.ContainsKey(name)) continue; + states.Add(new AlarmStateChanged(/* unchanged five args */) + { + IsMesReportable = _alarmMesReportable.GetValueOrDefault(name) + }); + } +``` +Native placeholder rows are untouched (`false`). Also clear `_alarmMesReportable` wherever `_alarmPriorities` is cleared on undeploy/redeploy (grep `_alarmPriorities.Clear`). + +**Step 3: Run** +```bash +dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests --filter "FullyQualifiedName~InstanceActorNativeAlarmTests|FullyQualifiedName~InstanceActorTests" 2>&1 | tail -3 +dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter "FullyQualifiedName~MessageConventionTests|FullyQualifiedName~AlarmStateChanged" 2>&1 | tail -3 +``` +Expected: PASS (the Commons additive/back-compat tests for `AlarmStateChanged` still deserialize a pre-flag JSON to `false`). + +**Step 4: Commit** `feat(site-runtime): stamp IsMesReportable on the Instance Actor alarm snapshot`. + +--- + +### Task 7: Script surface — `ScriptAlarm.IsMesReportable` + `Alarms.MesReportableAsync()` + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** Task 8, Task 9 (after Task 6) + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs:58-79` (trailing positional `bool IsMesReportable = false` + `` doc) +- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs:1263-1304` (`GetAlarmsAsync`, `ToScriptAlarm`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs:372-401` (`AlarmsAccessor`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs:150-157` (`CompileAlarmsAccessor`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs:151-164` (`SandboxAlarmsAccessor`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs`, `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/CompileSurfaceParityTests.cs`, the ScriptAnalysis compile-surface test that compiles an `Alarms.CurrentAsync()` script (grep `CurrentAsync` under `tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests`), and the CentralUI `SandboxScriptHost` diagnose-clean test (grep `Alarms.CurrentAsync` under `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests`). + +**Step 1: Failing tests** +- `AlarmsAccessorTests.CurrentAsync_ProjectsIsMesReportable` — respond with a row `{ IsMesReportable = true }` → `ScriptAlarm.IsMesReportable == true`. +- `AlarmsAccessorTests.MesReportableAsync_FiltersToFlaggedRows` — respond with one flagged + one unflagged row → exactly the flagged one. +- Compile-surface + sandbox tests: extend the existing script literal to `var r = await Alarms.MesReportableAsync(); var f = r.Where(a => a.IsMesReportable);` → compiles with zero diagnostics. +- `CompileSurfaceParityTests` already pairs `AlarmsAccessor` ↔ `CompileAlarmsAccessor` (`:112`) — it fails automatically until the mirror gains the member. + +**Step 2: Implement** +`ScriptAlarm`: append `bool IsMesReportable = false` as the last positional parameter (existing 21-arg call sites keep compiling). +`ScriptRuntimeContext`: +```csharp + internal async Task> GetMesReportableAlarmsAsync(CancellationToken cancellationToken = default) + => (await GetAlarmsAsync(cancellationToken)).Where(a => a.IsMesReportable).ToList(); +… + IsConfiguredPlaceholder: a.IsConfiguredPlaceholder, + IsMesReportable: a.IsMesReportable); +``` +`AlarmsAccessor`: +```csharp + /// + /// restricted to alarms whose deployed configuration flags + /// them MES-reportable (template checkbox, or an instance override). Native conditions + /// are never included — they carry no per-alarm flag. Placeholder rows are excluded. + /// + public Task> MesReportableAsync(CancellationToken cancellationToken = default) + => _ctx.GetMesReportableAlarmsAsync(cancellationToken); +``` +`CompileAlarmsAccessor`: same signature → `throw new NotSupportedException(CompileOnly)`. +`SandboxAlarmsAccessor`: same signature → `throw new ScriptSandboxException("Alarms.MesReportableAsync() reads the site Instance Actor's live alarm conditions, which aren't available in the central Test Run sandbox — deploy to a site to exercise alarm-reading scripts.")`. + +**Step 3: Run** the four test projects filtered to `Alarms|CompileSurfaceParity|SandboxScriptHost` → PASS. + +**Step 4: Commit** `feat(scripts): ScriptAlarm.IsMesReportable + Alarms.MesReportableAsync() mirrored on both compile surfaces`. + +--- + +### Task 8: Transport bundles + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** Task 2, Task 3, Task 6, Task 7 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs:212-221` (`TemplateAlarmDto` — append `bool IsMesReportable = false`), `:395-398` (`InstanceAlarmOverrideDto` — append `bool? IsMesReportableOverride = null`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs:66-74`, `:285-288`, `:346-359`, `:603-611` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/TemplateChildEquality.cs:57-65` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs:488-496` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:2168-2178`, `:2392-2416`, `:4357-4364`, `:4904-4916` +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/RoundTripEquivalenceTests.cs:233-241`, `:808-812`; `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/ArtifactDiffTests.cs`; `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs` + +**Step 1: Failing tests** +- Round-trip guard: set `IsMesReportable = true` on the seeded `High` alarm (`:233`) and `IsMesReportableOverride = false` on the seeded `HiAlarm` override (`:808`). The reflection sweep now fails until the DTO carries both. +- `ArtifactDiffTests`: an alarm differing only in `IsMesReportable` classifies as Changed (not Identical); same for an override differing only in `IsMesReportableOverride`. +- `EntitySerializerTests`: a bundle JSON lacking the two new keys deserializes to `false` / `null`. + +**Step 2: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests --filter RoundTripEquivalence` → FAIL naming the two properties. + +**Step 3: Implement** — carry the field at every listed site: +- serializer export: `IsMesReportable: a.IsMesReportable` / `IsMesReportableOverride: o.IsMesReportableOverride` +- serializer import + importer entity builds (`:346`, `:603`, `:2168`, `:4357`): `IsMesReportable = al.IsMesReportable` / `IsMesReportableOverride = o.IsMesReportableOverride` +- `SyncTemplateAlarmsAsync` (`:2392`): `current.IsMesReportable = alarmDto.IsMesReportable;` and add `current.IsMesReportable` to the audit payload object +- `TemplateChildEquality.AlarmsEqual`: `&& e.IsMesReportable == i.IsMesReportable` +- `ArtifactDiff` override lambda: `&& e.IsMesReportableOverride == i.IsMesReportableOverride` +- `BundleImporter:4904` (`ResolvedAlarm` for validation): `IsMesReportable = al.IsMesReportable` +- `TemplateChildEquality` also has an override-equality helper if one exists — grep `PriorityLevelOverride` in `Transport/Import` and extend every comparison you find. + +**Step 4: Run** +```bash +dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.Tests 2>&1 | tail -3 +dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests --filter "RoundTripEquivalence|BundleImporterApply|ArtifactDiff" 2>&1 | tail -3 +``` +Expected: PASS. + +**Step 5: Commit** `feat(transport): carry IsMesReportable / IsMesReportableOverride through bundles (round-trip guard extended)`. + +--- + +### Task 9: CLI flags + README + +**Classification:** small +**Estimated implement time:** ~4 min +**Parallelizable with:** Task 7, Task 8, Task 10, Task 11 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs:683-684` (add option), `:709-730` (register on add), `:747-754` (pass), `:758-770` (update option), `:790-797` (pass) +- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs:551-568` +- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/README.md:263-313` (alarm add/update option tables), `:808-821` (alarm-override set) +- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/EditingParityTests.cs:31-36` + +**Step 1: Failing tests** — add `InlineData` rows (exact envelope, camelCase, all fields): +```csharp +[InlineData("template alarm add --template-id 12 --name HighTemp --trigger-type HiLo --priority 500 --mes-reportable", "AddTemplateAlarm", "{\"templateId\":12,\"name\":\"HighTemp\",\"triggerType\":\"HiLo\",\"priorityLevel\":500,\"description\":null,\"triggerConfiguration\":null,\"isLocked\":false,\"isMesReportable\":true}")] +[InlineData("instance alarm-override set --instance-id 6 --alarm HighTemp --mes-reportable false", "SetInstanceAlarmOverride", "{\"instanceId\":6,\"alarmCanonicalName\":\"HighTemp\",\"triggerConfigurationOverride\":null,\"priorityLevelOverride\":null,\"isMesReportableOverride\":false}")] +``` +(If the existing serializer emits `triggerConfiguration` differently for a no-config add, copy the exact string the failing assertion prints.) + +**Step 2: Implement** +Template add + update (mirror `--locked`): +```csharp + var mesReportableOption = new Option("--mes-reportable") { Description = "Report this alarm to MES (IsFlaggedForMES on the alarm-status API)" }; + mesReportableOption.DefaultValueFactory = _ => false; +``` +register on the command, pass `result.GetValue(mesReportableOption)` as the new last ctor arg (a separate `updateMesReportableOption` for `update`, like `updateLockedOption`). +Instance override — nullable means inherit (the `api-method update --trace-all-calls` `Option` idiom, `ApiMethodCommands.cs:98-102`): +```csharp + var setMesReportableOption = new Option("--mes-reportable") { Description = "MES-reportable override: true|false; omit to inherit the template value" }; +``` +README: add the `--mes-reportable` row to the three option tables and the synopsis lines; note "update replaces the whole entity — omit `--mes-reportable` and it resets to false" on `template alarm update` (that is the existing `--locked` behaviour too). + +**Step 3: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CLI.Tests --filter "EditingParity|UpdateCommandContract"` → PASS. + +**Step 4: Commit** `feat(cli): --mes-reportable on template alarm add/update and instance alarm-override set`. + +--- + +### Task 10: Central UI — template alarm dialog + Alarms table + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** Task 9, Task 11 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateAlarmDialog.razor:38-43` (markup), `:68`, `:84`, `:94`, `:106`, `:128-133` +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor:906-934` (table), `:1436-1469` (open/show), `:1518-1540` (save) +- Test: create `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/TemplateAlarmDialogMesReportableTests.cs` (structural, copy the shape of `TemplateNativeAlarmSourceEditorTests.cs:16-67`) + +**Step 1: Failing test** — reads `TemplateAlarmDialog.razor` from disk and asserts it contains `@bind="_isMesReportable"`, `id="alarmMesReportable"`, and the label text `MES reportable`; reads `TemplateEdit.razor` and asserts the Alarms table has a `MES` header and a `data-test="alarm-mes-badge"` cell. + +**Step 2: Implement** +Dialog markup, directly after the Locked block: +```razor +
+
+ + +
Returned by the MES alarm-status API as IsFlaggedForMES. Instances can override unless the alarm is locked.
+
+
+``` +Code: `[Parameter] public bool InitialIsMesReportable { get; set; }`, `private bool _isMesReportable;`, seed in `OnInitialized`, pass in `Submit`, and extend the record: +```csharp + public sealed record AlarmDraft(string Name, AlarmTriggerType TriggerType, int Priority, string? TriggerConfig, bool IsLocked, bool IsMesReportable); +``` +`TemplateEdit.razor`: default draft gets `false`; edit draft passes `alarm.IsMesReportable`; `ShowAlarmDialogAsync` passes `InitialIsMesReportable="initial.IsMesReportable"`; both `proposed`/`alarm` initializers in `SaveAlarmDraftAsync` set `IsMesReportable = draft.IsMesReportable`. Table: header `MES` between Config and Lock; cell +```razor + + @if (alarm.IsMesReportable) { MES } + else { — } + +``` + +**Step 3: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "TemplateAlarmDialogMesReportable|TemplateEdit"` → PASS. `dotnet build src/ZB.MOM.WW.ScadaBridge.CentralUI` → 0 errors. + +**Step 4: Commit** `feat(ui): MES reportable checkbox on the template alarm dialog + MES column on the Alarms tab`. + +--- + +### Task 11: Central UI — instance override modal (tri-state) + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** Task 9, Task 10 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor:441-499` (table), `:531-541` (modal field), `:1532` (`OverrideSummary`), `:1564-1583` (`BeginEditOverride`), `:1628-1660` (delta check + save) +- Test: create `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/InstanceConfigureMesReportableTests.cs` (structural on the razor source, plus a unit test of the delta rule if you extract it into a static helper — recommended: `static bool HasOverrideDelta(string? json, int? priority, bool? mes)`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs` — add `AlarmOverride_MesReportable_RoundTrips` beside `AlarmOverride_SetPriority_ThenClear_RoundTrips` (`:195-249`); runs against the rig in Task 14. + +**Step 1: Failing tests** — structural: `data-test="alarm-mes-select"` exists with three `