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
@@ -653,6 +653,7 @@
<th style="width: 220px;">Connection override</th>
<th style="width: 220px;">Source reference override</th>
<th style="width: 170px;">Filter override</th>
<th style="width: 150px;">MES reportable</th>
<th style="width: 140px;">Actions</th>
</tr>
</thead>
@@ -697,6 +698,15 @@
value="@(_nasFilterEdit.GetValueOrDefault(src.Name) ?? "")"
@onchange="e => _nasFilterEdit[src.Name] = string.IsNullOrWhiteSpace((string?)e.Value) ? null : ((string?)e.Value)!.Trim()" />
</td>
<td>
<select class="form-select form-select-sm" data-test="@($"nas-mes-{src.Name}")"
value="@MesText(_nasMesEdit.GetValueOrDefault(src.Name))"
@onchange="e => _nasMesEdit[src.Name] = ParseMes((string?)e.Value)">
<option value="" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == null)">Inherited (@(src.IsMesReportable ? "reportable" : "not reportable"))</option>
<option value="true" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == true)">Reportable</option>
<option value="false" selected="@(_nasMesEdit.GetValueOrDefault(src.Name) == false)">Not reportable</option>
</select>
</td>
<td>
<div class="btn-group btn-group-sm" role="group" aria-label="@($"Override actions for {src.Name}")">
<button class="btn btn-success"
@@ -843,6 +853,9 @@
private Dictionary<string, string?> _nasConnEdit = new();
private Dictionary<string, string?> _nasRefEdit = new();
private Dictionary<string, string?> _nasFilterEdit = new();
private Dictionary<string, bool?> _nasMesEdit = new();
private static string MesText(bool? v) => v switch { true => "true", false => "false", _ => "" };
private static bool? ParseMes(string? v) => v switch { "true" => true, "false" => false, _ => null };
// Native-alarm-source CSV bulk-import result summary — same shape as the
// attribute importer's (_csvImport*) trio. Null until an import runs.
@@ -971,6 +984,7 @@
_nasConnEdit[s.Name] = ovr?.ConnectionNameOverride;
_nasRefEdit[s.Name] = ovr?.SourceReferenceOverride;
_nasFilterEdit[s.Name] = ovr?.ConditionFilterOverride;
_nasMesEdit[s.Name] = ovr?.IsMesReportableOverride;
}
_flattenedAttributes = await BuildFlattenedAttributesAsync();
@@ -1751,16 +1765,17 @@
var conn = Blank(_nasConnEdit.GetValueOrDefault(sourceName));
var sref = Blank(_nasRefEdit.GetValueOrDefault(sourceName));
var filt = Blank(_nasFilterEdit.GetValueOrDefault(sourceName));
var mes = _nasMesEdit.GetValueOrDefault(sourceName);
// All blank → no override; clear any existing row.
if (conn == null && sref == null && filt == null)
if (conn == null && sref == null && filt == null && mes == null)
{
await ClearNativeOverrideCore(sourceName);
_toast.ShowSuccess($"No override on '{sourceName}' (inherited).");
return;
}
await UpsertNativeOverrideCore(sourceName, conn, sref, filt);
await UpsertNativeOverrideCore(sourceName, conn, sref, filt, mes);
_toast.ShowSuccess($"Saved native alarm source override on '{sourceName}'.");
}
catch (Exception ex)
@@ -1791,15 +1806,16 @@
/// so a bulk import commits the whole batch in one go. Shared by the inline Save
/// and the CSV import so both take the identical persistence path.
/// </summary>
private async Task UpsertNativeOverrideCore(string sourceName, string? conn, string? sref, string? filt)
private async Task UpsertNativeOverrideCore(string sourceName, string? conn, string? sref, string? filt, bool? mes = null)
{
var result = await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverrideCommand(Id, sourceName, conn, sref, filt));
var result = await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverrideCommand(Id, sourceName, conn, sref, filt, mes));
if (result is InstanceNativeAlarmSourceOverride saved) _existingNativeOverrides[sourceName] = saved;
// Keep the inline editor showing what was just persisted.
_nasConnEdit[sourceName] = conn;
_nasRefEdit[sourceName] = sref;
_nasFilterEdit[sourceName] = filt;
_nasMesEdit[sourceName] = mes;
}
private async Task ClearNativeOverrideCore(string sourceName)
@@ -1809,6 +1825,7 @@
_nasConnEdit[sourceName] = null;
_nasRefEdit[sourceName] = null;
_nasFilterEdit[sourceName] = null;
_nasMesEdit[sourceName] = null;
}
private static string? Blank(string? v) => string.IsNullOrWhiteSpace(v) ? null : v.Trim();
@@ -1868,7 +1885,8 @@
}
await MutationServices.ExecuteConfigurationAsync(new SetInstanceNativeAlarmSourceOverridesCommand(Id,
outcome.Rows.Select(row => new NativeAlarmSourceOverrideEntry(row.SourceName, row.Connection, row.SourceReference, row.Filter)).ToList()));
outcome.Rows.Select(row => new NativeAlarmSourceOverrideEntry(row.SourceName, row.Connection, row.SourceReference, row.Filter,
_existingNativeOverrides.GetValueOrDefault(row.SourceName)?.IsMesReportableOverride)).ToList()));
await OnInitializedAsync();
_nasCsvImportSucceeded = true;