feat(native-alarms): per-binding IsMesReportable on native alarm source bindings (ScadaBridge#59)

TemplateNativeAlarmSource.IsMesReportable + tri-state InstanceNativeAlarmSourceOverride
.IsMesReportableOverride flatten onto ResolvedNativeAlarmSource; NativeAlarmActor stamps every
mirrored condition and InstanceActor the quiet-binding placeholder, so ScriptAlarm,
Alarms.MesReportableAsync() and the #60 wire fields carry it. Additive to the 900-999 severity band.
Migration AddNativeAlarmSourceMesReportable. Management commands, CLI --mes-reportable, Central UI
(template dialog checkbox, instance override select), Transport DTOs/importer/diff and the revision
hash (true-only, so existing hashes are unchanged) carry it.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:09:54 -05:00
parent 69a2017000
commit f98369a786
38 changed files with 2579 additions and 43 deletions
@@ -59,9 +59,16 @@ public class InstanceActor : ReceiveActor
/// children are created and read back in <see cref="BuildAlarmStatesSnapshot"/>, which
/// is the single place the flag is stamped — so a fired alarm (served from the retained
/// event) and an unfired one (served from the bare fallback projection) cannot drift.
/// Native conditions have no entry here and correctly resolve to false.
/// Native conditions have no entry here (their flag is the binding's, set by NativeAlarmActor).
/// </summary>
private readonly Dictionary<string, bool> _alarmMesReportable = new();
/// <summary>
/// Native binding canonical name → <c>ResolvedNativeAlarmSource.IsMesReportable</c> (Gitea #59),
/// used to stamp the quiet-binding placeholder rows. Live condition rows are stamped by the
/// <see cref="NativeAlarmActor"/> that owns the binding.
/// </summary>
private readonly Dictionary<string, bool> _nativeAlarmMesReportable = new();
private readonly Dictionary<string, IActorRef> _scriptActors = new();
private readonly Dictionary<string, IActorRef> _alarmActors = new();
private readonly Dictionary<string, IActorRef> _nativeAlarmActors = new();
@@ -1775,6 +1782,7 @@ public class InstanceActor : ReceiveActor
{
Kind = _nativeAlarmKinds.GetValueOrDefault(binding, AlarmKind.NativeOpcUa),
NativeSourceCanonicalName = binding,
IsMesReportable = _nativeAlarmMesReportable.GetValueOrDefault(binding),
IsConfiguredPlaceholder = true
});
}
@@ -2250,6 +2258,7 @@ public class InstanceActor : ReceiveActor
// Same kind handed to the NativeAlarmActor — used to stamp the
// (cosmetic) kind badge on placeholder rows for quiet bindings.
_nativeAlarmKinds[nativeSource.CanonicalName] = nativeKind;
_nativeAlarmMesReportable[nativeSource.CanonicalName] = nativeSource.IsMesReportable;
}
_logger.LogInformation(
@@ -496,6 +496,9 @@ public class NativeAlarmActor : ReceiveActor
CurrentValue = t.CurrentValue,
LimitValue = t.LimitValue,
NativeSourceCanonicalName = _source.CanonicalName,
// Gitea #59: the binding-level flag ("every condition under this source is
// MES-reportable"). Additive to the 900-999 severity band the consumer applies.
IsMesReportable = _source.IsMesReportable,
// MES alarm-status API §6.4: carried verbatim from the transition — the DCL
// adapter already decided whether an ack instant applies (null while unacked,
// cleared on re-raise), so the mirror never invents or suppresses one. A