Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs
Joseph Doherty be1003c53e Phase 7 Stream E — Config DB schema for scripts, virtual tags, scripted alarms, and alarm state
Adds the four tables Streams B/C/F consume — Script (generation-scoped source code),
VirtualTag (generation-scoped calculated-tag config), ScriptedAlarm (generation-scoped
alarm config), and ScriptedAlarmState (logical-id-keyed persistent runtime state).

## New entities (net10, EF Core)

- Script — stable logical ScriptId carries across generations; SourceHash is the
  compile-cache key (matches Core.Scripting's CompiledScriptCache).
- VirtualTag — mandatory EquipmentId FK (plan decision #2, unified Equipment tree);
  ChangeTriggered/TimerIntervalMs + Historize flags; check constraints enforce
  "at least one trigger" + "timer >= 50ms".
- ScriptedAlarm — required AlarmType ('AlarmCondition'/'LimitAlarm'/'OffNormalAlarm'/
  'DiscreteAlarm'); Severity 1..1000 range check; HistorizeToAveva default true per
  plan decision #15.
- ScriptedAlarmState — keyed ONLY on ScriptedAlarmId (NOT generation-scoped) per plan
  decision #14 — ack state + audit trail must follow alarm identity across Modified
  generations. CommentsJson has ISJSON check for GxP audit.

## Migration

EF-generated 20260420231641_AddPhase7ScriptingTables covers all 4 tables + indexes +
check constraints + FKs to ConfigGeneration. sp_PublishGeneration required no changes —
it only flips Draft->Published status; the new entities already carry GenerationId so
they publish atomically with the rest of the config.

## Tests — 12/12 (design-time model introspection)

Phase7ScriptingEntitiesTests covers: table registration, column maxlength + column
types, unique indexes (Generation+LogicalId, Generation+EquipmentPath for VirtualTag
and ScriptedAlarm), secondary indexes (SourceHash for cache lookup), check constraints
(trigger-required, timer-min, severity-range, alarm-type-enum, CommentsJson-IsJson),
ScriptedAlarmState PK is alarm-id not generation-scoped, ScriptedAlarm defaults
(HistorizeToAveva=true, Retain=true, Severity=500, Enabled=true), DbSets wired, and
the generated migration type exists for rollforward.
2026-04-20 19:22:45 -04:00

63 lines
2.8 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>
/// Per Phase 7 plan decision #14 — persistent runtime state for each scripted alarm.
/// Survives process restart so operators don't re-ack and ack history survives for
/// GxP / 21 CFR Part 11 compliance. Keyed on <c>ScriptedAlarmId</c> logically (not
/// per-generation) because ack state follows the alarm's stable identity across
/// generations — a Modified alarm keeps its ack history.
/// </summary>
/// <remarks>
/// <para>
/// <c>ActiveState</c> is deliberately NOT persisted — it rederives from the current
/// predicate evaluation on startup. Only operator-supplied state (<see cref="AckedState"/>,
/// <see cref="ConfirmedState"/>, <see cref="ShelvingState"/>) + audit trail persist.
/// </para>
/// <para>
/// <see cref="CommentsJson"/> is an append-only JSON array of <c>{user, utc, text}</c>
/// tuples — one per operator comment. Core.ScriptedAlarms' <c>AlarmConditionState.Comments</c>
/// serializes directly into this column.
/// </para>
/// </remarks>
public sealed class ScriptedAlarmState
{
/// <summary>Logical FK — matches <see cref="ScriptedAlarm.ScriptedAlarmId"/>. One row per alarm identity.</summary>
public required string ScriptedAlarmId { get; set; }
/// <summary>Enabled/Disabled. Persists across restart per plan decision #14.</summary>
public required string EnabledState { get; set; } = "Enabled";
/// <summary>Unacknowledged / Acknowledged.</summary>
public required string AckedState { get; set; } = "Unacknowledged";
/// <summary>Unconfirmed / Confirmed.</summary>
public required string ConfirmedState { get; set; } = "Unconfirmed";
/// <summary>Unshelved / OneShotShelved / TimedShelved.</summary>
public required string ShelvingState { get; set; } = "Unshelved";
/// <summary>When a TimedShelve expires — null if not shelved or OneShotShelved.</summary>
public DateTime? ShelvingExpiresUtc { get; set; }
/// <summary>User who last acknowledged. Null if never acked.</summary>
public string? LastAckUser { get; set; }
/// <summary>Operator-supplied ack comment. Null if no comment or never acked.</summary>
public string? LastAckComment { get; set; }
public DateTime? LastAckUtc { get; set; }
/// <summary>User who last confirmed.</summary>
public string? LastConfirmUser { get; set; }
public string? LastConfirmComment { get; set; }
public DateTime? LastConfirmUtc { get; set; }
/// <summary>JSON array of operator comments, append-only (GxP audit).</summary>
public string CommentsJson { get; set; } = "[]";
/// <summary>Row write timestamp — tracks last state change.</summary>
public DateTime UpdatedAtUtc { get; set; }
}