From ce8fc092c8d51fbdcd299e630a04b3896c5e8613 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 11 Sep 2026 10:16:47 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20MES-reportable=20alarm=20flag=20?= =?UTF-8?q?=E2=80=94=20component=20docs,=20CLAUDE.md,=20register=20rows=20?= =?UTF-8?q?121=E2=80=93122?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB --- CLAUDE.md | 2 +- docs/plans/2026-06-30-mes-alarm-status-api.md | 2 +- docs/plans/2026-07-08-deferred-work-register.md | 2 ++ docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md | 2 +- docs/requirements/Component-CLI.md | 6 +++--- docs/requirements/Component-CentralUI.md | 2 ++ docs/requirements/Component-ConfigurationDatabase.md | 1 + docs/requirements/Component-InboundAPI.md | 2 ++ docs/requirements/Component-ScriptAnalysis.md | 2 +- docs/requirements/Component-SiteRuntime.md | 4 +++- docs/requirements/Component-TemplateEngine.md | 2 +- 11 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index df5a4b6a..8e43af15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,7 +111,7 @@ spec for each is `docs/requirements/Component-.md`, and `README.md` carrie - Site nodes carry **two Akka roles**: the base `Site` plus a site-specific `site-{SiteId}` (`AkkaHostedService.BuildRoles`). Singletons scope to the **site-specific** role. - **The gRPC boundary is authenticated (PSK) as of 2026-07-22; Akka remoting still is not, and nothing is encrypted.** Akka remoting sets no `enable-ssl`, no secure cookie, no `trusted-selection-paths` — so intra-cluster Akka remoting remains open to anyone who can reach the remoting port, and that boundary still assumes a trusted network. The gRPC listener stays **h2c**, but `SiteStreamService` is no longer open: `ControlPlaneAuthInterceptor` (`Host/ControlPlaneAuthInterceptor.cs`) gates `/sitestream.SiteStreamService/` — including the `PullAuditEvents`/`PullSiteCalls` RPCs that return audit rows — against a **per-site preshared key**, fail-closed, constant-time compared, alongside the separate `LocalDbSyncAuthInterceptor` on `/localdb_sync.v1.LocalDbSync/` with its own separate key. **Site side:** `ScadaBridge:Communication:GrpcPsk`, in production `${secret:SB-GRPC-PSK-}`, and **`StartupValidator` refuses to boot a site node without it** (an unset key would leave the node healthy-looking but serving nothing). **Central side:** `SitePskProvider` resolves `SB-GRPC-PSK-{siteId}` from the secrets store at channel-build time (sites are added at runtime, so no boot-time expansion is possible), with `ScadaBridge:Communication:SitePsks:{siteId}` as an override for hosts running without a master key — the docker rig uses the latter. One key per site, never fleet-wide. A bearer token over h2c is readable and replayable on-path; TLS is the follow-on hardening and needs no change to this design. Introduced by Phase 0 of the ClusterClient→gRPC migration (`docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`). - gRPC streaming channel — **note the direction is inverted from the data flow**: data moves site→central, but each **site node hosts the gRPC server** (`SiteStreamGrpcServer`, Kestrel h2c, port 8083, mapped **only in the Site branch** of `Program.cs`) and **central is the client**, dialling in. Central creates per-site `SiteStreamGrpcClient` via `SiteStreamGrpcClientFactory`, keyed **`(siteId, endpoint)`** — the key was widened from site-only to fix an arch-review High where one session's NodeA→NodeB flip disposed a channel another session was still using. Proto evolution is **additive only** and field numbers are never reused (`AlarmStateUpdate` grew 7→23 fields for the native-alarm mirror, then 24 for `AckTime`). Generated C# is **vendored** under `Communication/SiteStreamGrpc/` with the `` include commented out — regeneration is a manual toggle-build-copy-untoggle, automated by `docker/regen-proto.sh [sitestream|centralcontrol|sitecommand|all]` (it always restores the csproj; an active `` item must never be committed — it breaks the Docker build). **Stream events are BATCHED as of R2 (2026-08-15)** — a site emitting ~37.5k events/s used to pay one gRPC message per event. Additive wire shape, no new RPC: `batching_supported` on the request (`InstanceStreamRequest` field 3 / `SiteStreamRequest` field 2) plus a `SiteStreamEvent.batch` oneof case (**field 4**) carrying a new `SiteStreamEventBatch { repeated SiteStreamEvent events = 1 }`. **The proto3 default of that flag IS the negotiation, and it is load-bearing** — a batch frame reaches a pre-R2 central as `EventOneofCase.None`, whose `ConvertToDomainEvent` returns null, so the whole batch would vanish with no error anywhere; an old central cannot set the flag, so it never gets one, and an old site ignores the unknown request field and keeps sending per-event frames the new client's `ForEachEvent` unpack handles as the single-event case. Server side is `SiteStreamEventBatcher`, a per-subscriber pump **downstream of `StreamRelayActor`'s bounded DropOldest channel** — so it changes framing only and **does NOT move the burst ceiling** (deferred register row 31; that ceiling lives in the shared publish stage upstream of the BroadcastHub). It never delays a lone event: it drains the already-queued backlog for free and lingers only once a backlog is proven, then emits a single-event buffer as a plain frame. `GrpcStreamBatchMaxEvents` (100, `1` disables) and `GrpcStreamBatchWindow` (25 ms, validated **strictly under 250 ms** = the load test's end-to-end P99 threshold); measured worst case P99 25.4 ms. **Benchmark-proven at target scale 2026-08-18** (the LoadHarness wire path — real Kestrel h2c + real `SiteStreamGrpcClient`, `--wire-probes-per-site`): 899,807 events in 12,462 frames (mean 72.2 ev/frame), wire P99 43.4 ms (5.8× inside the 250 ms budget — a 75-event burst never hits the 100 cap, so every batch pays the full 25 ms window: this workload's worst-case latency posture), zero drops/faults; results doc §8. Histogram `scadabridge.site.stream.batch_size`, recorded only on negotiated streams (`ScadaBridgeTelemetry.MeterName` is already in the `ObservedMeters` allowlist). **The client reconnects on graceful (OK-status) stream completion, not just on fault (arch-review remediation, commit `34a3f4bb`)** — Kestrel's `MaxStreamLifetime`/`Grpc.AspNetCore.Server` max-connection-age periodically ends a healthy stream with a normal completion, which the client used to treat as terminal (no reconnect attempt), silently killing a site's live feed until the next process restart (observed up to ~4h on the rig); live-probed at a 2-minute forced lifetime, reconnect lands within one reconcile tick and `IsLive` reflects the gap in between. -- Native alarms are a **read-only** mirror of OPC UA Alarms & Conditions and MxAccess Gateway alarms — **no ack-back, no central tables**; state lives in the site's `native_alarm_state`, survives failover, and is cleared on redeploy/undeploy (mirrors static overrides). Central's per-site live alarm cache (`ISiteAlarmLiveCache`) is **transient in-memory only** — there is deliberately no persisted central alarm store, so the 15s poll remains the NotReporting authority behind the live stream. See `Component-DataConnectionLayer.md` / `Component-CentralUI.md` for the model and the authoring surface. +- Native alarms are a **read-only** mirror of OPC UA Alarms & Conditions and MxAccess Gateway alarms — **no ack-back, no central tables**; state lives in the site's `native_alarm_state`, survives failover, and is cleared on redeploy/undeploy (mirrors static overrides). Central's per-site live alarm cache (`ISiteAlarmLiveCache`) is **transient in-memory only** — there is deliberately no persisted central alarm store, so the 15s poll remains the NotReporting authority behind the live stream. See `Component-DataConnectionLayer.md` / `Component-CentralUI.md` for the model and the authoring surface. Computed alarms carry an `IsMesReportable` flag (template checkbox, instance-overridable via a nullable override) exposed to scripts as `ScriptAlarm.IsMesReportable` / `Alarms.MesReportableAsync()` (2026-09-11); native alarms have no per-alarm row and keep the 900–999 MES severity band. - **`AckTime` mirror enrichment + the `Alarms` script accessor (MES alarm-status API Phase 1, 2026-08-01).** `AlarmStateChanged` carries an additive `AckTime` (`DateTimeOffset?`), mirrored on the vendored `AlarmStateUpdate` proto as **field 24** and persisted inside `native_alarm_state`'s `metadata_json` — deliberately NOT a new column, because that table is `RegisterReplicated` and LocalDb builds its CDC triggers from the column list at registration time. Set only while a condition is active AND acknowledged (so it is null while unacked and cleared on re-raise); the DCL stamps the source's own ack instant for OPC UA (new SelectClause **index 18** = `AckedState/TransitionTime`) and its observation time of the ack transition for MxGateway, which supplies none. Site `Call` scripts read alarms via the new **`Alarms.CurrentAsync()`** accessor (`ScriptRuntimeContext` + `ScriptGlobals`, local Ask on `GetAlarmSnapshotRequest`, returns `Commons.Types.Scripts.ScriptAlarm`), mirrored on `ScriptCompileSurface` AND the Central UI `SandboxScriptHost` editor surface. The trust model needed no change — it is a deny-list over API roots, not an allow-list of context members. Plan: `docs/plans/2026-06-30-mes-alarm-status-api.md` (Phases 2–4 are deployed config, not repo). - OPC UA cert trust is **site-local and not persisted centrally** (follow-up): the verify-endpoint probe captures an untrusted server cert but **NEVER trusts it**, and DeploymentManager broadcasts `TrustServerCertCommand`/`RemoveServerCertCommand` to **BOTH** site nodes — `CertStoreActor` runs on every site node, not as a singleton, so PKI stores stay consistent across failover. diff --git a/docs/plans/2026-06-30-mes-alarm-status-api.md b/docs/plans/2026-06-30-mes-alarm-status-api.md index 9ffff990..06e8e011 100644 --- a/docs/plans/2026-06-30-mes-alarm-status-api.md +++ b/docs/plans/2026-06-30-mes-alarm-status-api.md @@ -214,7 +214,7 @@ try { ## 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 900–999** 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 900–999 before the endpoints are meaningful. +1. **Superseded for COMPUTED alarms 2026-09-11** by `2026-09-11-mes-reportable-alarm-flag-design.md` — still the rule for native alarms. **`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 900–999** 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 900–999 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. diff --git a/docs/plans/2026-07-08-deferred-work-register.md b/docs/plans/2026-07-08-deferred-work-register.md index 5feefdf6..105c4aee 100644 --- a/docs/plans/2026-07-08-deferred-work-register.md +++ b/docs/plans/2026-07-08-deferred-work-register.md @@ -114,6 +114,8 @@ All 7 fix-now items landed via PLAN-04/05/06/07/08 (verified in review 08 round | 68 | ~~**`deploy.sh` run from a git worktree silently rehomes every node's bind-mount data dir**~~ **RESOLVED 2026-08-15 (guard added in the same close-out).** The compose files' bind mounts are relative (`./site-a-node-a/data`), so a deploy from a worktree checkout mounts the worktree's copies — the rig keeps running and looks healthy, but its SQLite stores split from the main checkout's and everything written there is orphaned by the next deploy from main. Bit the rows-46–53 close-out live: the seeding agent's `rig-probe-1` deploy persisted into worktree-homed dirs and "vanished" after the orchestrator's redeploy (no tombstone, no product bug — LWW/tombstone forensics ruled out deletion; the row sat in `.claude/worktrees/…/docker/site-a-node-a/data/site-localdb.db` the whole time). Both `docker/deploy.sh` and `docker-env2/deploy.sh` now refuse to run from a `.claude/worktrees/` path (override: `SCADABRIDGE_ALLOW_WORKTREE_DEPLOY=1`). | Found during the rows-46–53 close-out | Closed 2026-08-15. | — | | 119 | **Port the QA/prod MES receiver scripts onto the reset-aware `WriteBatchAndWaitAsync` overload (Gitea #36)** *(Medium)*. Template scripts CvdReactor MesMoveIn (13) / MesMoveOut (14) / MesSampleData (36) and base MESReceiver MoveIn/MoveOut (15, 16, 25, 26, 37, 38) still reset `CompleteFlag` with a plain write and rely on `Task.Delay(1000)`; QA carries a config-only `WaitAsync(false, 10 s)` patch on 13/14 (deployment record 110). Rewrite: `var hs = await rx.Attributes.WriteBatchAndWaitAsync(values, "Flag", true, "CompleteFlag", true, 25 s, "CompleteFlag", false, 10 s); if (!hs.Matched) return new { WasSuccessful=false, ErrorText = hs.ResetFailed ? "Timeout waiting for CompleteFlag to reset" : "Timeout waiting for to complete", BatchID=0 };` and delete the delay. Requires the #36 build on the target central + sites first (the overload does not exist on older sites → deploy-gate compile failure, by design). | Gitea #36, design `2026-09-10-handshake-reset-echo-design.md` §8 | Repo-only scope was the user's decision for the #36 session; the script bodies live only in the QA/prod databases. | Next QA deployment of a build ≥ this change (write a deployment record; then prod). | | 120 | **Data-sourced SCALAR writes reach the OPC UA server as String Variants — every non-String node rejects them with BadTypeMismatch (0x80740000)** *(Medium)*. `InstanceActor.HandleSetDataAttribute` / `HandleWriteAttributeBatch` decode only List values before the DCL write ("Scalars are unchanged"), so the codec-encoded string (`"True"`, `"42.5"`) reaches `RealOpcUaClient.WriteValuesAsync`, which wraps it in `new Variant(value)` — a String variant sent to a Boolean/Double node. Found live 2026-09-10 running `docker/seed-handshake-demo.sh` against opc-plc `Motor.Running`/`Pump.Running`/`Motor.Speed` (`SetAttribute('Complete') failed: OPC UA write failed with status: 0x80740000`); the seed was retargeted onto the rig's only two writable String nodes. The MxGateway path is unaffected in practice: prod's Delmia/MES handshakes write the same strings through the gateway today. Fix candidates: (a) decode scalars by declared `DataType` in the InstanceActor before the DCL write — also changes what MxGateway receives, so it needs a gateway-side compatibility check; (b) adapter-local: `RealOpcUaClient` coerces to the node's DataType (it already resolves `DataTypeIds` in `EnrichVariableTypeInfoAsync`), leaving the MxGateway wire untouched — preferred. Pin with a `RealOpcUaClientArrayWriteTests`-style test for a Boolean scalar. | Found live 2026-09-10 during the #36 rig gate | Out of scope for #36 (handshake ordering); touches the production write path and needs the MxGateway compatibility question answered first. | Before any OPC UA-bound template relies on script writes to non-String nodes (none does today — until this gate the rig's script writes to OPC UA were static-only). | +| 121 | **No source-level `IsMesReportable` on native alarm source bindings** *(Low)*. `TemplateNativeAlarmSource` / `InstanceNativeAlarmSourceOverride` carry no per-alarm MES-relevance flag — "every condition under this binding is reportable" is not expressible, only the 900–999 severity-band convention. | `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md` | Native alarms have no per-alarm config row, and the design chose not to invent one for this slice. | A native-source machine needs per-alarm MES relevance beyond the severity band. | +| 122 | **`IsMesReportable` does not ride the wire** *(Low)*. `AlarmStateUpdate` (vendored proto) and `DebugAlarmStateDto` have no `IsMesReportable` field, so central-side surfaces (Debug View live stream, Alarm Summary) always see `false` even though the site-side snapshot stamps the real value. Adding it is additive-only: `AlarmStateUpdate` field 25, `DebugAlarmStateDto` field 20, via a manual `regen-proto.sh` toggle-build-copy-untoggle. | `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md` | No consumer needed it in this slice — the flag exists to serve `Alarms.MesReportableAsync()` for scripts, not a central UI surface. | Operators ask to see the MES-reportable flag live on Debug View / Alarm Summary. | ## Resolved (verified against the code 2026-07-10) Rows removed from the Deferred table above once confirmed shipped. Kept here for traceability. 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 index 3d2c8a93..8c6a77fd 100644 --- a/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md +++ b/docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md @@ -1,7 +1,7 @@ # MES-Reportable Alarm Flag — Design **Date:** 2026-09-11 -**Status:** DRAFT — awaiting user review. Nothing implemented; nothing committed. +**Status:** IMPLEMENTED 2026-09-11 (rig gate pending — see §8 once Task 14 lands). **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). diff --git a/docs/requirements/Component-CLI.md b/docs/requirements/Component-CLI.md index 5264fb22..87b7492e 100644 --- a/docs/requirements/Component-CLI.md +++ b/docs/requirements/Component-CLI.md @@ -68,8 +68,8 @@ scadabridge template delete --id scadabridge template attribute add --template-id --name --data-type [--value ] [--description ] [--data-source ] [--locked] scadabridge template attribute update --id [--name ] [--data-type ] [--value ] [--description ] [--data-source ] [--locked] scadabridge template attribute delete --id -scadabridge template alarm add --template-id --name --trigger-type --priority [--description ] [--trigger-config ] [--locked] [--trigger-kind advisory|strict] -scadabridge template alarm update --id --name --trigger-type --priority [--description ] [--trigger-config ] [--locked] [--trigger-kind advisory|strict] +scadabridge template alarm add --template-id --name --trigger-type --priority [--description ] [--trigger-config ] [--locked] [--trigger-kind advisory|strict] [--mes-reportable] +scadabridge template alarm update --id --name --trigger-type --priority [--description ] [--trigger-config ] [--locked] [--trigger-kind advisory|strict] [--mes-reportable] scadabridge template alarm delete --id scadabridge template script add --template-id --name --code --trigger-type [--trigger-config ] [--locked] [--parameters ] [--return-def ] [--trigger-kind advisory|strict] scadabridge template script update --id --name --code --trigger-type [--trigger-config ] [--locked] [--parameters ] [--return-def ] [--trigger-kind advisory|strict] @@ -93,7 +93,7 @@ scadabridge instance set-overrides --id --overrides scadabridge instance attribute-override delete --instance-id --attribute scadabridge instance native-alarm-source list --instance-id scadabridge instance import-overrides --id --file -scadabridge instance alarm-override set --instance-id --alarm [--trigger-config ] [--priority ] +scadabridge instance alarm-override set --instance-id --alarm [--trigger-config ] [--priority ] [--mes-reportable true|false] scadabridge instance alarm-override delete --instance-id --alarm scadabridge instance alarm-override list --instance-id scadabridge instance native-alarm-source set --instance-id --source [--connection ] [--source-ref ] [--filter ] diff --git a/docs/requirements/Component-CentralUI.md b/docs/requirements/Component-CentralUI.md index 9b9c8c5e..117c9959 100644 --- a/docs/requirements/Component-CentralUI.md +++ b/docs/requirements/Component-CentralUI.md @@ -63,6 +63,7 @@ Central cluster only. Sites have no user interface. - Manage template hierarchy (inheritance) — visual tree of parent/child relationships. - Manage composition — add/remove feature module instances within templates. **Naming collision detection** provides immediate feedback if composed modules introduce duplicate attribute, alarm, or script names. - Define and edit attributes, alarms, and scripts on templates. +- **`TemplateAlarmDialog`** carries an **"MES reportable"** checkbox bound to `TemplateAlarm.IsMesReportable` (default unchecked, 2026-09-11) alongside the existing Name/Trigger Type/Priority/Trigger Definition/Description/Lock fields. - **Member authoring dialogs are hosted, not page-embedded** (M10 residual, 2026-08-01): the template editor's Attribute, Alarm, Native Alarm Source, and Script forms all open through `IDialogService.ShowAsync`, so the single `DialogHost` in `MainLayout` owns the backdrop, focus trap, Escape, and focus restoration. Each form body is its own component beside the page (`TemplateAttributeDialog`, `TemplateAlarmDialog`, `TemplateNativeAlarmSourceDialog`, `TemplateScriptDialog`), following the `MoveDataConnectionDialog` pattern — the body renders validation and server errors **inline and stays open**, closing only on a successful save; persistence stays on the page (which owns `TemplateService`) behind an `OnSaveAsync` delegate. - **Native Alarms tab** (`TemplateEdit`): a tab alongside Attributes / Alarms / Scripts / Compositions that lists the template's **native alarm source bindings** — the OPC UA Alarms & Conditions / MxAccess Gateway sources whose alarm state the instance mirrors. Each binding carries Name, Connection, Source Reference, optional Condition Filter, Description, and a Lock flag. Add / edit / delete go through a **hosted dialog** (`TemplateNativeAlarmSourceDialog`): - **Name** — unique within the template (lock/inherit bookkeeping mirrors `TemplateAlarm`). @@ -149,6 +150,7 @@ Central cluster only. Sites have no user interface. - **Type column** — Variable rows display best-effort type info (data type friendly name, scalar/array value rank, writable flag) read from the server during browse. - Set instance-level attribute overrides (non-locked attributes only). - **Bulk override CSV import** (`InstanceConfigure`): a Blazor `InputFile` upload accepts a CSV of `AttributeName, Value, ElementType?` rows (`ElementType` only for `List` attributes). Each row is validated against the instance's flattened attribute schema (name exists + value type-compatible, reusing the existing override validation); the import is **all-or-nothing** — any per-row error aborts the whole upload with a per-row error summary and nothing is applied. On success the rows are upserted through the same ManagementActor add/update-override handlers used by the inline editor. The same import is available from the CLI (`instance import-overrides --file`, see Component-CLI.md). +- **Alarm Overrides card** (`InstanceConfigure`) gained an **"MES reportable"** tri-state select (Inherit / Yes / No, 2026-09-11) per alarm row, bound to `InstanceAlarmOverride.IsMesReportableOverride` — Inherit maps to `null` (use the template value); the row is disabled for locked alarms, same as the existing Priority/Trigger override fields. `instance alarm-override set --mes-reportable true|false` (omit the flag to inherit) is the CLI equivalent. - **Native Alarm Source Overrides card** (`InstanceConfigure`): a card placed **after the Alarm Overrides card**, listing the template's native alarm sources for per-instance binding. Each row offers **inline override** of the three fields that typically vary per physical instance: - **Connection** — a dropdown (same alarm-capable filtering as the template editor). - **Source Reference** — the concrete native key for this instance. diff --git a/docs/requirements/Component-ConfigurationDatabase.md b/docs/requirements/Component-ConfigurationDatabase.md index c43b603c..ff024549 100644 --- a/docs/requirements/Component-ConfigurationDatabase.md +++ b/docs/requirements/Component-ConfigurationDatabase.md @@ -358,6 +358,7 @@ A CI grep guard (`AuditLogAppendOnlyGuardTests`) fails the build on any `UPDATE` - Each migration is a versioned, incremental schema change. - New tables are introduced as their own migration — for example, the `Notifications` table for the Notification Outbox ships as a dedicated EF Core migration that creates the table, its `Type`/`Status` value conversions, and its dispatcher and KPI indexes. - The native alarm source tables ship in a dedicated `AddNativeAlarmSources` migration, parallel in shape to the existing `AddInstanceAlarmOverrides` migration: it creates `NativeAlarmSources` and `InstanceNativeAlarmSourceOverrides` with their columns, the `TemplateId` → `Templates` and `InstanceId` → `Instances` cascade-delete foreign keys, and the `(TemplateId, Name)` / `(InstanceId, SourceCanonicalName)` unique indexes. +- **`20260911135501_AddAlarmMesReportable`** (2026-09-11) adds `TemplateAlarms.IsMesReportable` (`bit`, default `0`) and `InstanceAlarmOverrides.IsMesReportableOverride` (`bit` NULL, null = inherit the template value). No new tables, no index changes — see `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md`. - The initial `AuditLog` migration creates the monthly partition function `pf_AuditLog_Month` and partition scheme `ps_AuditLog_Month`, then creates the `AuditLog` table aligned to that scheme on `OccurredAtUtc`, along with the indexes listed under Database Schema. The migration also creates the `scadabridge_audit_writer` and `scadabridge_audit_purger` DB roles with the grants described in Database Roles. The ongoing **partition-maintenance job** that rolls the scheme forward each month (creating the next month's partition ahead of time) and switches out expired partitions is owned by the **Audit Log component** (`AuditLogPurgeActor` and its monthly roll-forward step), not by the Configuration Database component — this component is responsible only for the initial schema, roles, and any EF migrations against the table going forward. ### Development Environment diff --git a/docs/requirements/Component-InboundAPI.md b/docs/requirements/Component-InboundAPI.md index 409db0b8..b9f8c2e6 100644 --- a/docs/requirements/Component-InboundAPI.md +++ b/docs/requirements/Component-InboundAPI.md @@ -252,6 +252,8 @@ Inbound API scripts **cannot** call shared scripts directly — shared scripts a - `Route.To("instanceUniqueCode").WaitForAttribute("attributeName", targetValue, timeout)` — Wait, event-driven, until an attribute on a specific instance at any site reaches `targetValue` (value-equality only across the wire), bounded by `timeout`. Returns `true` if matched within the timeout, `false` if it timed out. **The wait is bounded by its own `timeout`, not the generic method-level timeout** — this is the one routed call that may legitimately outlive the method timeout (the site enforces `timeout` and returns `false` when it elapses). A client disconnect still cancels the wait. This is the deliberate exception to the rule below that routed calls inherit the method-level timeout (see "Routing Behavior"): a long event-driven wait is the explicit reason `timeout` governs here. > **Reading alarm state via a routed script.** There is deliberately **no** `Route.To(...).GetAlarms(...)` verb. An inbound method that needs a machine's current alarms resolves the instance and calls a **site script** that uses the Site Runtime `Alarms.CurrentAsync()` accessor — `Route.To(code).Call("SimpleAlarmStatus", new { ... })`. Alarm state is per-instance and lives on the site's Instance Actor; keeping the read inside a site script means the filtering/shaping happens where the data is, and central stays a thin router. This is the shape the MES alarm-status endpoints use (`docs/plans/2026-06-30-mes-alarm-status-api.md` §5.1); see [Component-SiteRuntime.md](Component-SiteRuntime.md) → *Alarm State (read-only)* for the accessor. +> +> **Example — `AlarmStatus`.** The rig demo template `MesAlarmDemo`'s site script (`docker/seed-mes-alarm-demo.sh`) answers `Route.To(MachineCode).Call("AlarmStatus", new { AlarmFilter = new { FlaggedOnly, IncludeAcked, MinSeverity, MaxSeverity, NameFilter } })` with the legacy `AlarmStatusResponse { WasSuccessful, ErrorText, Alarms: AlarmInfo[] }`, setting `IsFlaggedForMES = a.IsMesReportable` per row for **computed** alarms (the template's `TemplateAlarm.IsMesReportable` flag, instance-overridable) and the **900–999 severity band** for **native** alarms, which carry no per-alarm `IsMesReportable` row. `SimpleAlarmStatus` is the same shape with `FlaggedOnly` hard-wired true. #### Input/Output - **Input parameters** are available as defined in the method definition. diff --git a/docs/requirements/Component-ScriptAnalysis.md b/docs/requirements/Component-ScriptAnalysis.md index f2531c50..17463dd6 100644 --- a/docs/requirements/Component-ScriptAnalysis.md +++ b/docs/requirements/Component-ScriptAnalysis.md @@ -148,7 +148,7 @@ Used by the Template Engine deploy gate: ```csharp var errors = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)); ``` -This allows the compile to bind `Attributes["name"]`, `Notify.To("x").Send(...)`, `ExternalSystem.Call(...)`, `Alarms.CurrentAsync()`, and similar API calls against real types, catching undefined-symbol and type-mismatch errors before deployment. +This allows the compile to bind `Attributes["name"]`, `Notify.To("x").Send(...)`, `ExternalSystem.Call(...)`, `Alarms.CurrentAsync()`, `Alarms.MesReportableAsync()` (2026-09-11), and similar API calls against real types, catching undefined-symbol and type-mismatch errors before deployment. The surface also mirrors the **`Log` global** (step logs design `docs/plans/2026-09-09-flow-debugger-step-logs-design.md` D4) as the compile-only `ScriptCompileSurface.CompileLog` — `Debug` / `Info` / `Warn` / `Error(string message, Exception? exception = null)`, including the optional exception parameter, so `Log.Warn("…", ex)` binds at the design-time gate exactly as it does at the site. `Log` needs no `ScriptTrustPolicy` change for the same reason `Alarms` did not: the trust boundary is a deny-list over API roots, not an allow-list of context members. diff --git a/docs/requirements/Component-SiteRuntime.md b/docs/requirements/Component-SiteRuntime.md index a41de720..e30b9a62 100644 --- a/docs/requirements/Component-SiteRuntime.md +++ b/docs/requirements/Component-SiteRuntime.md @@ -510,11 +510,13 @@ Available to all script runs and alarm on-trigger runs: ### Alarm State (read-only) - `Alarms.CurrentAsync()` — Returns a snapshot of the instance's **current** alarm conditions as `IReadOnlyList` (`Commons.Types.Scripts`), covering computed alarms and mirrored native (OPC UA A&C / MxAccess Gateway) conditions alike. Served by a **local Ask** (`GetAlarmSnapshotRequest`/`GetAlarmSnapshotResponse`) to the script's own Instance Actor — the same mechanism attribute reads use, never a cross-cluster hop. +- `Alarms.MesReportableAsync()` — `CurrentAsync()` filtered to rows where `ScriptAlarm.IsMesReportable` is true (2026-09-11). Native alarms carry no per-alarm config row, so they never satisfy the filter — they still convey MES relevance via the 900–999 severity band (see `docs/plans/2026-06-30-mes-alarm-status-api.md`). - Available to **every** script, unlike the `Alarm` global, which exists only inside an on-trigger handler and describes the single alarm that fired. This accessor is what lets an inbound-API-routed `Call` script answer "what is in alarm on this machine right now?"; before it, native mirrored conditions were reachable only from the Debug View. - **Read-only by design** — native alarms are a read-only mirror of the source (no ack-back), so the accessor exposes no acknowledge/shelve operation. - **Not scope-prefixed**, unlike `Attributes`: alarm identity is not a scope-relative attribute name (computed alarms are keyed by configured name, native conditions by a source-supplied reference), so every scope sees the same list rather than a silently truncated one. - The snapshot is built by the same `BuildAlarmStatesSnapshot()` the Debug View uses, so it **includes placeholder rows** for configured-but-quiet native source bindings (`IsConfiguredPlaceholder`). Callers enumerating real alarms filter on `Active && !IsConfiguredPlaceholder`. -- `ScriptAlarm` is a flat projection of the retained `AlarmStateChanged`: `Name`, `SourceReference`, `NativeSourceCanonicalName`, `Active`, `Acknowledged`, `Confirmed?`, `Shelved` (all shelve sub-states collapse to one boolean), `Suppressed`, `Severity` (0–1000), `Kind`, `Message`, `AlarmTypeName`, `Category`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime?`, `Timestamp`, `AckTime?`, `CurrentValue`, `LimitValue`, `IsConfiguredPlaceholder`. The `Condition` is the authority for active/acked/severity, so computed and native alarms read identically from one filter expression. +- `ScriptAlarm` is a flat projection of the retained `AlarmStateChanged`: `Name`, `SourceReference`, `NativeSourceCanonicalName`, `Active`, `Acknowledged`, `Confirmed?`, `Shelved` (all shelve sub-states collapse to one boolean), `Suppressed`, `Severity` (0–1000), `Kind`, `Message`, `AlarmTypeName`, `Category`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime?`, `Timestamp`, `AckTime?`, `CurrentValue`, `LimitValue`, `IsConfiguredPlaceholder`, `IsMesReportable` (trailing, 2026-09-11 — always `false` on native rows). The `Condition` is the authority for active/acked/severity, so computed and native alarms read identically from one filter expression. +- **`IsMesReportable` is stamped in one place.** `InstanceActor.BuildAlarmStatesSnapshot()` sets `AlarmStateChanged.IsMesReportable` from `ResolvedAlarm.IsMesReportable` on every row it produces — retained (fired/active) rows and the fallback/idle rows alike — so `Alarms.CurrentAsync()`/`MesReportableAsync()` and the Debug View snapshot always agree regardless of whether the alarm has fired. The flag is **not** carried on the gRPC stream or `DebugAlarmStateDto` (no proto change in this slice — deferred-work register row 122), so central-side surfaces (Debug View live stream, Alarm Summary) see `false` until that follow-up lands. - The type lives in Commons so the runtime accessor and the Script Analysis compile-only surface (`ScriptCompileSurface.CompileAlarmsAccessor`) project to the **same** type — a script binding at the design-time gate binds identically at the site. Parity is pinned by `CompileSurfaceParityTests`. - Design record: `docs/plans/2026-06-30-mes-alarm-status-api.md` §5.2. diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index f9a9d27a..34e9c812 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -110,7 +110,7 @@ breadcrumb. Override and lock rules apply per entity type at the following granularity: - **Attributes**: Value and Description are overridable. Data Type is fixed by the defining level. `DataSourceReference` on a template attribute defines the **default** physical address for that attribute. Instances may override per attribute via `InstanceConnectionBinding.DataSourceReferenceOverride`; the override replaces the template default at flattening time. When the override is null (the default), the template value is used. Lock applies to the entire attribute (when locked, no fields can be overridden). -- **Alarms**: Priority Level and Trigger Definition (thresholds/ranges/rates) are instance-overridable. Description and On-Trigger Script reference are **not** instance-overridable — `InstanceAlarmOverride` carries only `TriggerConfigurationOverride` and `PriorityLevelOverride`, so an instance cannot re-point an alarm's on-trigger script or reword its description (those are template-level authoring decisions). Name and Trigger Type (Value Match vs. Range vs. Rate of Change) are fixed. Lock applies to the entire alarm. `TriggerConfigurationOverride` is a **partial merge for `HiLo`** triggers — setpoints left unset in the override keep their inherited values — and a **whole-config replacement for every other trigger type**. (Recorded decision, arch-review 05: this narrower granularity is the implemented behavior; adding Description/OnTriggerScript override columns is future feature work, not spec debt.) +- **Alarms**: Priority Level and Trigger Definition (thresholds/ranges/rates) are instance-overridable. Description and On-Trigger Script reference are **not** instance-overridable — `InstanceAlarmOverride` carries only `TriggerConfigurationOverride` and `PriorityLevelOverride`, so an instance cannot re-point an alarm's on-trigger script or reword its description (those are template-level authoring decisions). Name and Trigger Type (Value Match vs. Range vs. Rate of Change) are fixed. Lock applies to the entire alarm. `TriggerConfigurationOverride` is a **partial merge for `HiLo`** triggers — setpoints left unset in the override keep their inherited values — and a **whole-config replacement for every other trigger type**. (Recorded decision, arch-review 05: this narrower granularity is the implemented behavior; adding Description/OnTriggerScript override columns is future feature work, not spec debt.) `TemplateAlarm.IsMesReportable` (bool, default false, "MES reportable" checkbox — `template alarm add|update --mes-reportable`) is instance-overridable via the nullable `InstanceAlarmOverride.IsMesReportableOverride` (null = inherit the template value; `instance alarm-override set --mes-reportable true|false`, omitted = inherit). A locked alarm cannot take this override either, same as every other alarm field. Migration `20260911135501_AddAlarmMesReportable` (Config DB, 2026-09-11); flattening resolves `ResolvedAlarm.IsMesReportable = override ?? template` and ships it in the deployed config JSON — no site schema change. See `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md`. - **Native alarm sources**: An instance overrides a non-locked source via `InstanceNativeAlarmSourceOverride`, keyed by `SourceCanonicalName`. `ConnectionNameOverride`, `SourceReferenceOverride`, and `ConditionFilterOverride` are individually overridable — each is applied only when non-null; a null field **keeps the inherited value**. Name is fixed. Lock applies to the entire source. - **Scripts**: C# source code, Trigger configuration, minimum time between runs, and parameter/return definitions are overridable. Name is fixed. Lock applies to the entire script. - **Composed module members**: A composing template or child template can override non-locked members inside a composed module using the canonical path-qualified name.