Replace ASCII-art diagrams across the README and docs/ with editable .drawio sources plus exported PNGs, so the diagrams render clearly in rendered markdown and can be maintained/regenerated instead of being hand-edited as fragile text art. Non-diagram blocks (code, folder trees, UI wireframes) were left as text.
74 KiB
Native OPC UA & MxAccess Gateway Alarms — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
Goal: Mirror native alarms from OPC UA Alarms & Conditions servers and the MxAccess Gateway into ScadaBridge as a read-only, unified A&C-style alarm state model (severity + active/acked/shelved/suppressed), discovered at runtime from instance-bound sources, kept site-local and shown live in the DebugView.
Architecture: A new NativeAlarmActor (peer to the computed AlarmActor under InstanceActor) subscribes through a new DCL capability seam (IAlarmSubscribableConnection) implemented by the OPC UA and MxGateway adapters. The DataConnectionActor opens one alarm feed per connection and routes transitions to instances by source reference. State lives in site SQLite, streams to central over the (additively) enriched gRPC AlarmStateUpdate, and is seeded via the existing DebugView snapshot. No central tables; read-only (no ack-back). Design doc: docs/plans/2026-05-29-native-alarms-design.md.
Tech Stack: C#/.NET 10, Akka.NET (TestKit, Become/Stash), EF Core + MS SQL (central config), Microsoft.Data.Sqlite (site state), gRPC/protobuf, Blazor Server, System.CommandLine (CLI), xUnit + NSubstitute + Akka.TestKit.Xunit2 + bUnit.
Conventions: Messages/flattened types are record; entities are class. xUnit [Fact], name Method_Scenario_Expected. Build with dotnet build ZB.MOM.WW.ScadaBridge.slnx. Commit after each task. TDD: failing test → verify fail → implement → verify pass → commit.
Critical gotcha (read before Task 18): src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto is not auto-compiled — the <Protobuf> include is commented out in the .csproj and generated .cs files are checked into SiteStreamGrpc/. Regen is a manual macOS-only step (toggle the include, dotnet build, copy generated files, re-comment). Task 18 documents it.
Task dependency overview
Task 1: Commons alarm core types
Classification: standard Estimated implement time: ~4 min Parallelizable with: Task 3, Task 4
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/AlarmKind.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/AlarmShelveState.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/AlarmTransitionKind.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionState.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/NativeAlarmTransition.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Alarms/AlarmConditionStateTests.cs
Step 1: Write the failing test
// AlarmConditionStateTests.cs
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Alarms;
public class AlarmConditionStateTests
{
[Fact]
public void AlarmConditionState_DefaultsAreNormalUnshelved()
{
var s = new AlarmConditionState(
Active: false, Acknowledged: true, Confirmed: null,
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: 0);
Assert.False(s.Active);
Assert.Equal(AlarmShelveState.Unshelved, s.Shelve);
Assert.Equal(0, s.Severity);
}
[Fact]
public void NativeAlarmTransition_CarriesSourceAndCondition()
{
var t = new NativeAlarmTransition(
SourceReference: "Tank01.Level.HiHi", SourceObjectReference: "Tank01",
AlarmTypeName: "AnalogLimitAlarm.HiHi", Kind: AlarmTransitionKind.Raise,
Condition: new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800),
Category: "Process", Description: "High level", Message: "level high",
OperatorUser: "", OperatorComment: "",
OriginalRaiseTime: null, TransitionTime: DateTimeOffset.UnixEpoch,
CurrentValue: "92.1", LimitValue: "90");
Assert.Equal("Tank01", t.SourceObjectReference);
Assert.Equal(800, t.Condition.Severity);
Assert.Equal(AlarmTransitionKind.Raise, t.Kind);
}
}
Step 2: Run test to verify it fails
Run: dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmConditionStateTests
Expected: FAIL — types do not exist (compile error).
Step 3: Write minimal implementation
// AlarmKind.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
public enum AlarmKind { Computed, NativeOpcUa, NativeMxAccess }
// AlarmShelveState.cs — same namespace
public enum AlarmShelveState { Unshelved, OneShotShelved, TimedShelved, PermanentShelved }
// AlarmTransitionKind.cs — same namespace
public enum AlarmTransitionKind { Snapshot, SnapshotComplete, Raise, Acknowledge, Clear, Retrigger, StateChange }
// AlarmConditionState.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
/// <summary>Orthogonal OPC UA Part 9 sub-conditions + severity. Read-only mirror of the source.</summary>
public record AlarmConditionState(
bool Active,
bool Acknowledged,
bool? Confirmed,
AlarmShelveState Shelve,
bool Suppressed,
int Severity);
// NativeAlarmTransition.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
/// <summary>Protocol-neutral alarm transition emitted by an IAlarmSubscribableConnection adapter.</summary>
public record NativeAlarmTransition(
string SourceReference,
string SourceObjectReference,
string AlarmTypeName,
AlarmTransitionKind Kind,
AlarmConditionState Condition,
string Category,
string Description,
string Message,
string OperatorUser,
string OperatorComment,
DateTimeOffset? OriginalRaiseTime,
DateTimeOffset TransitionTime,
string CurrentValue,
string LimitValue);
Step 4: Run test to verify it passes
Run: dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmConditionStateTests
Expected: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Commons/Types tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Alarms
git commit -m "feat(commons): native alarm core types (AlarmConditionState, NativeAlarmTransition, enums)"
Task 2: Extend AlarmStateChanged + computed-default mapping
Classification: small Estimated implement time: ~4 min Parallelizable with: Task 3, Task 4
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionStateFactory.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/AlarmStateChangedEnrichmentTests.cs
Context: AlarmStateChanged already uses init-properties (Level, Message) so all additions are additive — existing positional callers compile unchanged.
Step 1: Write the failing test
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
public class AlarmStateChangedEnrichmentTests
{
[Fact]
public void Defaults_AreComputedKind_WithAutoAck()
{
var m = new AlarmStateChanged("inst", "HiAlarm", AlarmState.Active, 700, DateTimeOffset.UnixEpoch);
Assert.Equal(AlarmKind.Computed, m.Kind);
Assert.True(m.Condition.Acknowledged); // computed = auto-acked
Assert.Equal(700, m.Condition.Severity); // severity defaults to Priority
Assert.True(m.Condition.Active); // derived from State
Assert.Equal("", m.SourceReference);
}
[Fact]
public void Factory_ForComputed_MapsPriorityAndState()
{
var c = AlarmConditionStateFactory.ForComputed(AlarmState.Normal, priority: 250);
Assert.False(c.Active);
Assert.True(c.Acknowledged);
Assert.Equal(250, c.Severity);
}
}
Step 2: Run test to verify it fails
Run: dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmStateChangedEnrichmentTests
Expected: FAIL — Kind/Condition not defined.
Step 3: Write minimal implementation
Add AlarmConditionStateFactory.cs (namespace ...Types.Alarms):
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
public static class AlarmConditionStateFactory
{
/// <summary>Computed alarms are auto-acked, never shelved/suppressed; severity = priority.</summary>
public static AlarmConditionState ForComputed(AlarmState state, int priority) =>
new(Active: state == AlarmState.Active, Acknowledged: true, Confirmed: null,
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: priority);
}
In AlarmStateChanged.cs, add these init-properties to the record body (keep existing Level, Message):
public AlarmKind Kind { get; init; } = AlarmKind.Computed;
private AlarmConditionState? _condition;
/// <summary>Unified A&C-style condition. Defaults to a computed mapping of State+Priority.</summary>
public AlarmConditionState Condition
{
get => _condition ?? AlarmConditionStateFactory.ForComputed(State, Priority);
init => _condition = value;
}
public string SourceReference { get; init; } = string.Empty;
public string AlarmTypeName { get; init; } = string.Empty;
public string Category { get; init; } = string.Empty;
public string OperatorUser { get; init; } = string.Empty;
public string OperatorComment { get; init; } = string.Empty;
public DateTimeOffset? OriginalRaiseTime { get; init; }
public string CurrentValue { get; init; } = string.Empty;
public string LimitValue { get; init; } = string.Empty;
Add using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; to the file.
Step 4: Run test to verify it passes
Run: dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter AlarmStateChangedEnrichmentTests
Expected: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionStateFactory.cs tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/AlarmStateChangedEnrichmentTests.cs
git commit -m "feat(commons): enrich AlarmStateChanged with unified condition state (additive)"
Task 3: IAlarmSubscribableConnection seam + DCL alarm messages
Classification: small Estimated implement time: ~4 min Parallelizable with: Task 2, Task 4
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IAlarmSubscribableConnection.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection/SubscribeAlarmsRequest.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection/NativeAlarmMessages.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/NativeAlarmMessagesTests.cs
Step 1: Write the failing test
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
public class NativeAlarmMessagesTests
{
[Fact]
public void SubscribeAlarmsRequest_CarriesSourceAndFilter()
{
var r = new SubscribeAlarmsRequest("c1", "inst", "PlantOpcUa", "ns=2;s=Tank01", null, DateTimeOffset.UnixEpoch);
Assert.Equal("ns=2;s=Tank01", r.SourceReference);
Assert.Null(r.ConditionFilter);
}
[Fact]
public void NativeAlarmTransitionUpdate_WrapsTransition()
{
var t = new NativeAlarmTransition("Tank01.Hi", "Tank01", "x", AlarmTransitionKind.Raise,
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 500),
"", "", "", "", "", null, DateTimeOffset.UnixEpoch, "", "");
var u = new NativeAlarmTransitionUpdate("PlantOpcUa", t);
Assert.Equal("PlantOpcUa", u.ConnectionName);
Assert.Equal("Tank01", u.Transition.SourceObjectReference);
}
}
Step 2: Run test — Run: dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter NativeAlarmMessagesTests → FAIL (types missing).
Step 3: Implement
// IAlarmSubscribableConnection.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
/// <summary>Callback invoked when a native alarm transition (incl. snapshot replay) arrives.</summary>
public delegate void AlarmTransitionCallback(NativeAlarmTransition transition);
/// <summary>Optional capability: an IDataConnection that can mirror a source's native alarms.</summary>
public interface IAlarmSubscribableConnection
{
Task<string> SubscribeAlarmsAsync(string sourceReference, string? conditionFilter,
AlarmTransitionCallback callback, CancellationToken cancellationToken = default);
Task UnsubscribeAlarmsAsync(string subscriptionId, CancellationToken cancellationToken = default);
}
// SubscribeAlarmsRequest.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection
public record SubscribeAlarmsRequest(
string CorrelationId, string InstanceUniqueName, string ConnectionName,
string SourceReference, string? ConditionFilter, DateTimeOffset Timestamp);
public record SubscribeAlarmsResponse(
string CorrelationId, string InstanceUniqueName, bool Success, string? ErrorMessage, DateTimeOffset Timestamp);
public record UnsubscribeAlarmsRequest(
string CorrelationId, string InstanceUniqueName, string ConnectionName, string SourceReference, DateTimeOffset Timestamp);
// NativeAlarmMessages.cs — namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
/// <summary>DCL → instance: a native alarm transition routed by source reference.</summary>
public record NativeAlarmTransitionUpdate(string ConnectionName, NativeAlarmTransition Transition);
/// <summary>DCL → instance: the alarm feed for a source is unavailable (connection lost); mark uncertain.</summary>
public record NativeAlarmSourceUnavailable(string ConnectionName, string SourceReference, DateTimeOffset Timestamp);
Step 4: Run test → PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Protocol/IAlarmSubscribableConnection.cs src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/NativeAlarmMessagesTests.cs
git commit -m "feat(commons): IAlarmSubscribableConnection seam + DCL native alarm messages"
Task 4: Entities + flattened type + Template navigation
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 1, Task 2, Task 3
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs - Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceNativeAlarmSourceOverride.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/Template.cs(addNativeAlarmSourcescollection) - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/Instance.cs(addNativeAlarmSourceOverridescollection) - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs(addResolvedNativeAlarmSourcerecord +NativeAlarmSourceslist) - Test:
tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/NativeAlarmSourceEntityTests.cs
Step 1: Write the failing test
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
public class NativeAlarmSourceEntityTests
{
[Fact]
public void TemplateNativeAlarmSource_RequiresName()
{
var s = new TemplateNativeAlarmSource("PressureMon") { ConnectionName = "Plant", SourceReference = "ns=2;s=P1" };
Assert.Equal("PressureMon", s.Name);
Assert.False(s.IsLocked);
}
[Fact]
public void FlattenedConfiguration_HasNativeAlarmSourcesDefaultEmpty()
{
var f = new FlattenedConfiguration();
Assert.Empty(f.NativeAlarmSources);
}
[Fact]
public void ResolvedNativeAlarmSource_DefaultsSourceTemplate()
{
var r = new ResolvedNativeAlarmSource { CanonicalName = "PressureMon", ConnectionName = "Plant", SourceReference = "ns=2;s=P1" };
Assert.Equal("Template", r.Source);
}
}
Step 2: Run test → FAIL (types/members missing).
Step 3: Implement
TemplateNativeAlarmSource.cs — mirror TemplateAlarm (class, ctor takes name):
namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
public class TemplateNativeAlarmSource
{
public int Id { get; set; }
public int TemplateId { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public string ConnectionName { get; set; } = string.Empty;
public string SourceReference { get; set; } = string.Empty;
public string? ConditionFilter { get; set; } // null = mirror all conditions under the source
public bool IsLocked { get; set; }
public bool IsInherited { get; set; }
public bool LockedInDerived { get; set; }
public TemplateNativeAlarmSource(string name) =>
Name = name ?? throw new ArgumentNullException(nameof(name));
}
InstanceNativeAlarmSourceOverride.cs — mirror InstanceAlarmOverride:
namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
public class InstanceNativeAlarmSourceOverride
{
public int Id { get; set; }
public int InstanceId { get; set; }
public string SourceCanonicalName { get; set; }
public string? ConnectionNameOverride { get; set; } // null = keep inherited
public string? SourceReferenceOverride { get; set; }
public string? ConditionFilterOverride { get; set; }
public InstanceNativeAlarmSourceOverride(string sourceCanonicalName) =>
SourceCanonicalName = sourceCanonicalName ?? throw new ArgumentNullException(nameof(sourceCanonicalName));
}
In Template.cs add (mirror the Alarms collection): public ICollection<TemplateNativeAlarmSource> NativeAlarmSources { get; set; } = new List<TemplateNativeAlarmSource>();
In Instance.cs add (mirror AlarmOverrides): public ICollection<InstanceNativeAlarmSourceOverride> NativeAlarmSourceOverrides { get; set; } = new List<InstanceNativeAlarmSourceOverride>();
In FlattenedConfiguration.cs add to the record: public IReadOnlyList<ResolvedNativeAlarmSource> NativeAlarmSources { get; init; } = []; and a new record (mirror ResolvedAlarm):
/// <summary>A fully resolved native alarm source binding (connection + source reference).</summary>
public sealed record ResolvedNativeAlarmSource
{
public string CanonicalName { get; init; } = string.Empty;
public string ConnectionName { get; init; } = string.Empty;
public string SourceReference { get; init; } = string.Empty;
public string? ConditionFilter { get; init; }
public string Source { get; init; } = "Template"; // Template|Inherited|Composed|Override
}
Step 4: Run test → PASS. Also run dotnet build ZB.MOM.WW.ScadaBridge.slnx to confirm nothing referencing Template/Instance broke.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Commons/Entities src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/NativeAlarmSourceEntityTests.cs
git commit -m "feat(commons): native alarm source entities + ResolvedNativeAlarmSource"
Task 5: EF configurations + DbSets
Classification: small Estimated implement time: ~4 min Parallelizable with: Task 8, Task 20
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/TemplateConfiguration.cs(addTemplateNativeAlarmSourceConfigurationclass + relationship onTemplateConfiguration) - Modify:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/InstanceConfiguration.cs(addInstanceNativeAlarmSourceOverrideConfiguration+ relationship) - Modify:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs(add twoDbSets after line ~65) - Test:
tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/NativeAlarmSourceSchemaTests.cs
Step 1: Write the failing test (in-memory SQLite, EnsureCreated, mirrors existing repo-test setup):
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
public class NativeAlarmSourceSchemaTests
{
[Fact]
public async Task TemplateNativeAlarmSource_PersistsAndEnforcesUniqueName()
{
var opts = new DbContextOptionsBuilder<ScadaBridgeDbContext>().UseSqlite("DataSource=:memory:").Options;
using var ctx = new ScadaBridgeDbContext(opts);
ctx.Database.OpenConnection();
ctx.Database.EnsureCreated();
var t = new Template("T1");
t.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src1") { ConnectionName = "C", SourceReference = "r" });
ctx.Templates.Add(t);
await ctx.SaveChangesAsync();
Assert.Single(ctx.TemplateNativeAlarmSources);
}
}
Step 2: Run test → FAIL (TemplateNativeAlarmSources DbSet missing).
Step 3: Implement
In ScadaBridgeDbContext.cs after the template/instance DbSets:
public DbSet<TemplateNativeAlarmSource> TemplateNativeAlarmSources => Set<TemplateNativeAlarmSource>();
public DbSet<InstanceNativeAlarmSourceOverride> InstanceNativeAlarmSourceOverrides => Set<InstanceNativeAlarmSourceOverride>();
(OnModelCreating already calls ApplyConfigurationsFromAssembly — new IEntityTypeConfiguration classes auto-register.)
In TemplateConfiguration.cs, in TemplateConfiguration.Configure add the relationship next to HasMany(t => t.Alarms):
builder.HasMany(t => t.NativeAlarmSources).WithOne().HasForeignKey(s => s.TemplateId).OnDelete(DeleteBehavior.Cascade);
Add a sibling config class (mirror TemplateAlarmConfiguration):
public class TemplateNativeAlarmSourceConfiguration : IEntityTypeConfiguration<TemplateNativeAlarmSource>
{
public void Configure(EntityTypeBuilder<TemplateNativeAlarmSource> builder)
{
builder.HasKey(s => s.Id);
builder.Property(s => s.Name).IsRequired().HasMaxLength(200);
builder.Property(s => s.Description).HasMaxLength(2000);
builder.Property(s => s.ConnectionName).IsRequired().HasMaxLength(200);
builder.Property(s => s.SourceReference).IsRequired().HasMaxLength(1000);
builder.Property(s => s.ConditionFilter).HasMaxLength(1000);
builder.HasIndex(s => new { s.TemplateId, s.Name }).IsUnique();
}
}
In InstanceConfiguration.cs, add relationship next to HasMany(i => i.AlarmOverrides):
builder.HasMany(i => i.NativeAlarmSourceOverrides).WithOne().HasForeignKey(o => o.InstanceId).OnDelete(DeleteBehavior.Cascade);
And a sibling config (mirror InstanceAlarmOverrideConfiguration):
public class InstanceNativeAlarmSourceOverrideConfiguration : IEntityTypeConfiguration<InstanceNativeAlarmSourceOverride>
{
public void Configure(EntityTypeBuilder<InstanceNativeAlarmSourceOverride> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.SourceCanonicalName).IsRequired().HasMaxLength(400);
builder.Property(o => o.ConnectionNameOverride).HasMaxLength(200);
builder.Property(o => o.SourceReferenceOverride).HasMaxLength(1000);
builder.Property(o => o.ConditionFilterOverride).HasMaxLength(1000);
builder.HasIndex(o => new { o.InstanceId, o.SourceCanonicalName }).IsUnique();
}
}
Step 4: Run test → PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/NativeAlarmSourceSchemaTests.cs
git commit -m "feat(configdb): EF mappings + DbSets for native alarm source entities"
Task 6: Repository interface + implementation
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 7, Task 8
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs(add 10 methods) - Modify:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs(implement; mirror alarm-override methods) - Modify:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs— ensureGetTemplateWithChildrenAsync.Include(t => t.NativeAlarmSources)and the instance loader.Include(i => i.NativeAlarmSourceOverrides) - Test:
tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs(add CRUD facts)
Step 1: Write failing tests — add facts:
[Fact]
public async Task AddAndGetNativeAlarmSourcesByTemplateId_RoundTrips()
{
var t = new Template("T"); _context.Templates.Add(t); await _context.SaveChangesAsync();
await _repository.AddTemplateNativeAlarmSourceAsync(
new TemplateNativeAlarmSource("S") { TemplateId = t.Id, ConnectionName = "C", SourceReference = "r" });
await _context.SaveChangesAsync();
var list = await _repository.GetNativeAlarmSourcesByTemplateIdAsync(t.Id);
Assert.Single(list);
}
[Fact]
public async Task GetTemplateWithChildren_IncludesNativeAlarmSources()
{
var t = new Template("T");
t.NativeAlarmSources.Add(new TemplateNativeAlarmSource("S") { ConnectionName = "C", SourceReference = "r" });
_context.Templates.Add(t); await _context.SaveChangesAsync();
var loaded = await _repository.GetTemplateWithChildrenAsync(t.Id);
Assert.Single(loaded!.NativeAlarmSources);
}
Step 2: Run test → FAIL.
Step 3: Implement — add the 10 method signatures to ITemplateEngineRepository and implement them in TemplateEngineRepository exactly mirroring the existing alarm-override CRUD (Get...ByTemplateIdAsync, Get...ByIdAsync, Add...Async, Update...Async, Delete...Async for template; Get...OverridesByInstanceIdAsync, Get...OverrideAsync, Add/Update/Delete...OverrideAsync for instance). Add .Include(t => t.NativeAlarmSources) to GetTemplateWithChildrenAsync and .Include(i => i.NativeAlarmSourceOverrides) to the instance-with-children loader.
Step 4: Run test → PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/TemplateEngineRepositoryTests.cs
git commit -m "feat(configdb): native alarm source repository CRUD + eager-load includes"
Task 7: EF migration
Classification: high-risk Estimated implement time: ~4 min Parallelizable with: Task 6, Task 8
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/<timestamp>_AddNativeAlarmSources.cs(generated) - Modify:
.../Migrations/ScadaBridgeDbContextModelSnapshot.cs(generated)
Step 1: Generate the migration
Run from repo root:
dotnet ef migrations add AddNativeAlarmSources \
--project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase \
--startup-project src/ZB.MOM.WW.ScadaBridge.Host
Step 2: Verify the generated Up() creates Templates.NativeAlarmSources-equivalent table NativeAlarmSources (FK TemplateId → Templates, cascade; unique index (TemplateId, Name)) and InstanceNativeAlarmSourceOverrides (FK InstanceId → Instances, cascade; unique (InstanceId, SourceCanonicalName)). Compare shape against 20260513055537_AddInstanceAlarmOverrides.cs.
Step 3: Apply against a throwaway DB to confirm it runs
dotnet build ZB.MOM.WW.ScadaBridge.slnx
# auto-applies in dev on Host startup; or:
dotnet ef database update --project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase --startup-project src/ZB.MOM.WW.ScadaBridge.Host
Expected: applies with no error; dotnet ef migrations list shows AddNativeAlarmSources pending→applied.
Step 4: Commit
git add src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations
git commit -m "feat(configdb): migration AddNativeAlarmSources"
Task 8: Flattening — ResolveNativeAlarmSources
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 5, Task 6, Task 7
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs(newResolveNativeAlarmSources+ResolveComposedNativeAlarmSources+ApplyInstanceNativeAlarmSourceOverrides; call inFlatten()Step 5 region; attach toFlattenedConfiguration) - Test:
tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs(add facts)
Step 1: Write failing tests (mirror Flatten_SingleTemplate_ResolvesAttributes):
[Fact]
public void Flatten_ResolvesNativeAlarmSources_FromTemplate()
{
var template = CreateTemplate(1, "Base");
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
{ ConnectionName = "Opc", SourceReference = "ns=2;s=P1" });
var result = _sut.Flatten(CreateInstance(), [template],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsSuccess);
Assert.Single(result.Value.NativeAlarmSources);
Assert.Equal("Pressure", result.Value.NativeAlarmSources[0].CanonicalName);
Assert.Equal("ns=2;s=P1", result.Value.NativeAlarmSources[0].SourceReference);
}
[Fact]
public void Flatten_InstanceOverride_ReplacesSourceReference()
{
var template = CreateTemplate(1, "Base");
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
{ ConnectionName = "Opc", SourceReference = "ns=2;s=DEFAULT" });
var instance = CreateInstance();
instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Pressure")
{ SourceReferenceOverride = "ns=2;s=Tank07" });
var result = _sut.Flatten(instance, [template], new(), new(), new());
Assert.Equal("ns=2;s=Tank07", result.Value.NativeAlarmSources[0].SourceReference);
Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source);
}
Step 2: Run test → FAIL.
Step 3: Implement — In Flatten()'s "Step 5" region (after alarms, ~line 97), add:
var nativeAlarmSources = ResolveInheritedNativeAlarmSources(templateChain, prefix: null);
ResolveComposedNativeAlarmSources(templateChain, compositionMap, composedTemplateChains, nativeAlarmSources);
ApplyInstanceNativeAlarmSourceOverrides(instance.NativeAlarmSourceOverrides, nativeAlarmSources);
Attach to the returned config: NativeAlarmSources = nativeAlarmSources.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList().
Implement the three helpers mirroring ResolveInheritedAlarms / ResolveComposedAlarms(Recursive) / ApplyInstanceAlarmOverrides:
- Inherited: walk chain base→derived; derived wins unless
IsLocked; key by bareName; produceResolvedNativeAlarmSource { CanonicalName = prefix-qualified Name, ConnectionName, SourceReference, ConditionFilter, Source = inherited?"Inherited":"Template" }. - Composed: for each composition module, recurse into the composed chain, path-qualify
CanonicalNameto[ModuleInstanceName].[Name], collision-check,Source = "Composed". (No attribute-reference rewriting needed —SourceReferenceis a raw connection address, not an attribute path.) - Overrides: for each
InstanceNativeAlarmSourceOverridematchingSourceCanonicalName, apply non-nullConnectionNameOverride/SourceReferenceOverride/ConditionFilterOverride, setSource = "Override".
Step 4: Run test → PASS. Run full TemplateEngine.Tests.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs
git commit -m "feat(templateengine): flatten native alarm sources (inherit/compose/override)"
Task 9: Semantic validation
Classification: small Estimated implement time: ~4 min Parallelizable with: Task 10, Task 11, Task 12
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs(validation block after line ~215; addValidationCategory.NativeAlarmSourceInvalid) - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/ValidationCategory.cs(add enum member — confirm location during impl) - Test:
tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs
Step 1: Write failing tests:
[Fact]
public void Validate_NativeAlarmSource_UnknownConnection_Errors()
{
var cfg = ConfigWith(new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "Ghost", SourceReference = "x" });
var errors = _sut.Validate(cfg, knownConnections: new HashSet<string> { "RealConn" });
Assert.Contains(errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
}
[Fact]
public void Validate_NativeAlarmSource_EmptySourceRef_Errors()
{
var cfg = ConfigWith(new ResolvedNativeAlarmSource { CanonicalName = "P", ConnectionName = "RealConn", SourceReference = "" });
var errors = _sut.Validate(cfg, knownConnections: new HashSet<string> { "RealConn" });
Assert.Contains(errors, e => e.Category == ValidationCategory.NativeAlarmSourceInvalid);
}
(Confirm Validate's real signature for connection scope during impl; the existing alarm/connection validation already receives connection info — reuse that source rather than adding a new param if available.)
Step 2: Run test → FAIL.
Step 3: Implement — add NativeAlarmSourceInvalid to ValidationCategory; after the on-trigger-script validation loop, iterate config.NativeAlarmSources and errors.Add(ValidationEntry.Error(ValidationCategory.NativeAlarmSourceInvalid, "...", source.CanonicalName)) when: ConnectionName not in the known/alarm-capable connection set, or SourceReference is empty. (Protocol alarm-capability check: connection protocol ∈ {OpcUa, MxGateway}.)
Step 4: Run test → PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/ValidationCategory.cs tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/SemanticValidatorTests.cs
git commit -m "feat(templateengine): validate native alarm source connection + source reference"
Task 10: DataConnectionActor — alarm subscribe/route/unavailable
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: Task 11, Task 12
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/DataConnectionActorAlarmTests.cs
Design: add _alarmSubscribers: Dictionary<string /*sourceObjectRef*/, HashSet<IActorRef>> and _alarmSubByInstance: Dictionary<string, (IActorRef, string sourceRef)>. Handle SubscribeAlarmsRequest in Connected (stash in Connecting/Reconnecting); capability-check _adapter is IAlarmSubscribableConnection (else reply SubscribeAlarmsResponse(false, "not alarm-capable")); call SubscribeAlarmsAsync with a callback closure capturing Self + _adapterGeneration that Self.Tell(new AlarmTransitionReceived(transition, generation)). On AlarmTransitionReceived, drop if stale generation, else route: Self.Tell fan-out to _alarmSubscribers whose key prefixes transition.SourceObjectReference, wrapping as NativeAlarmTransitionUpdate(_connectionName, transition). On entering Reconnecting (existing PushBadQualityForAllTags), also send NativeAlarmSourceUnavailable to alarm subscribers; on reconnect, re-subscribe alarms (the adapter re-emits a snapshot). Handle UnsubscribeAlarmsRequest in all states (mirror HandleUnsubscribe).
Step 1: Write failing test (NSubstitute adapter implementing both IDataConnection and IAlarmSubscribableConnection):
public class DataConnectionActorAlarmTests : TestKit
{
[Fact]
public void SubscribeAlarms_RoutesTransitionToInstanceSubscriber()
{
AlarmTransitionCallback? cb = null;
var adapter = Substitute.For<IDataConnection, IAlarmSubscribableConnection>();
((IAlarmSubscribableConnection)adapter)
.SubscribeAlarmsAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Do<AlarmTransitionCallback>(c => cb = c), Arg.Any<CancellationToken>())
.Returns(Task.FromResult("alarm-sub-1"));
adapter.ConnectAsync(Arg.Any<IDictionary<string,string>>(), Arg.Any<CancellationToken>()).Returns(Task.CompletedTask);
var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor(
"conn", adapter, _options, _health, _factory, "OpcUa")));
actor.Tell(new SubscribeAlarmsRequest("c","inst","conn","Tank01",null,DateTimeOffset.UtcNow));
ExpectMsg<SubscribeAlarmsResponse>(m => m.Success);
var t = new NativeAlarmTransition("Tank01.Hi","Tank01","x",AlarmTransitionKind.Raise,
new AlarmConditionState(true,false,null,AlarmShelveState.Unshelved,false,500),"","","","","",null,DateTimeOffset.UtcNow,"","");
cb!(t); // adapter fires a transition
ExpectMsg<NativeAlarmTransitionUpdate>(u => u.Transition.SourceObjectReference == "Tank01");
}
}
(Wire _options/_health/_factory per the existing DataConnectionActorTests ctor.)
Step 2: Run test → FAIL.
Step 3: Implement the handlers + routing table + stash + unavailable signal as described. Add an internal record AlarmTransitionReceived(NativeAlarmTransition Transition, int Generation).
Step 4: Run test → PASS. Run full DCL actor tests to confirm no regression in tag-subscription paths.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/DataConnectionActorAlarmTests.cs
git commit -m "feat(dcl): DataConnectionActor native alarm subscribe + source-ref routing + unavailable signal"
Task 11: OPC UA A&C adapter
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: Task 10, Task 12
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs(add alarm-subscription method) - Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs(event MonitoredItem + EventFilter + ConditionRefresh + field→transition mapping) - Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs(implementIAlarmSubscribableConnection, map client callback →NativeAlarmTransition) - Create:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs(pure mapping helpers — unit-testable without a server) - Test:
tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs
Strategy: Keep server I/O in RealOpcUaClient, but isolate the field → AlarmConditionState/transition logic in OpcUaAlarmMapper so it is unit-testable without a live server (live behavior is covered by the integration task / SkippableFact). RealOpcUaClient creates one event MonitoredItem (AttributeId = Attributes.EventNotifier) on the Server object (ObjectIds.Server) with an EventFilter selecting EventType, SourceNode, SourceName, Time, Message, Severity, and the ConditionType/AcknowledgeableConditionType/AlarmConditionType state fields; calls the ConditionRefresh method on subscribe to replay active conditions; converts each EventFieldList to the adapter callback shape.
Step 1: Write failing test (mapper only):
public class OpcUaAlarmMapperTests
{
[Fact]
public void MapSeverity_ClampsTo0_1000()
{
Assert.Equal(1000, OpcUaAlarmMapper.NormalizeSeverity(5000));
Assert.Equal(0, OpcUaAlarmMapper.NormalizeSeverity(-1));
Assert.Equal(500, OpcUaAlarmMapper.NormalizeSeverity(500));
}
[Fact]
public void BuildCondition_ActiveUnacked()
{
var c = OpcUaAlarmMapper.BuildCondition(active: true, acked: false, confirmed: null,
shelved: false, suppressed: false, severity: 700);
Assert.True(c.Active);
Assert.False(c.Acknowledged);
Assert.Equal(700, c.Severity);
}
[Fact]
public void DeriveKind_FromStateDelta_AckYieldsAcknowledge()
{
Assert.Equal(AlarmTransitionKind.Acknowledge,
OpcUaAlarmMapper.DeriveKind(prevAcked: false, nowAcked: true, prevActive: true, nowActive: true));
}
}
Step 2: Run test → FAIL.
Step 3: Implement OpcUaAlarmMapper (pure static helpers: NormalizeSeverity, BuildCondition, DeriveKind). Then wire RealOpcUaClient/OpcUaDataConnection: add IOpcUaClient.CreateAlarmSubscriptionAsync(sourceFilter, conditionFilter, callback) returning a handle; implement OpcUaDataConnection.SubscribeAlarmsAsync to call it and translate to NativeAlarmTransition via the mapper; trigger ConditionRefresh then emit Snapshot/SnapshotComplete.
Step 4: Run test → PASS (mapper). Build the project. (Live A&C exercised in Task 28.)
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs
git commit -m "feat(dcl): OPC UA Alarms & Conditions adapter (event subscription + ConditionRefresh)"
Task 12: MxGateway StreamAlarms adapter
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: Task 10, Task 11
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IMxGatewayClient.cs(addRunAlarmStreamAsync) - Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs(consume packageStreamAlarmsAsync; resumable; reconnect → snapshot) - Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs(implementIAlarmSubscribableConnection; background loop;RaiseDisconnectedon fault) - Create:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayAlarmMapper.cs(AlarmFeedMessage/OnAlarmTransitionEvent→NativeAlarmTransition) - Test:
tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/MxGatewayAlarmMapperTests.cs - Verify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.csprojreferences aZB.MOM.WW.MxGateway.Clientversion exposingStreamAlarmsAsync(bump if older; OtOpcUa'sGatewayGalaxyAlarmFeeduses it).
Step 1: Write failing test (mapper, using the generated proto types from ZB.MOM.WW.MxGateway.Contracts.Proto):
public class MxGatewayAlarmMapperTests
{
[Fact]
public void MapTransition_ActiveAcked_FromOnAlarmTransitionEvent()
{
// Build an OnAlarmTransitionEvent (ACK transition) and assert mapping.
var t = MxGatewayAlarmMapper.MapTransition(/* proto OnAlarmTransitionEvent w/ ACKNOWLEDGE */ ...);
Assert.Equal(AlarmTransitionKind.Acknowledge, t.Kind);
Assert.True(t.Condition.Acknowledged);
Assert.Equal("operator1", t.OperatorUser);
}
[Fact]
public void MapState_ActiveAcked_To_ActiveTrue_AckTrue()
{
var c = MxGatewayAlarmMapper.MapConditionState(/* ALARM_CONDITION_STATE_ACTIVE_ACKED */, severity: 600);
Assert.True(c.Active); Assert.True(c.Acknowledged); Assert.Equal(600, c.Severity);
}
}
Step 2: Run test → FAIL.
Step 3: Implement MxGatewayAlarmMapper (proto → NativeAlarmTransition; active_alarm→Snapshot, snapshot_complete→SnapshotComplete, transition→mapped). Add IMxGatewayClient.RunAlarmStreamAsync(prefix, Action<NativeAlarmTransition>, ct) and implement in RealMxGatewayClient over the package StreamAlarmsAsync. Implement MxGatewayDataConnection.SubscribeAlarmsAsync (start background loop; RaiseDisconnected once-only on fault; the resumed stream re-sends a snapshot).
Step 4: Run test → PASS. Build.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IMxGatewayClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealMxGatewayClient.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayAlarmMapper.cs tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/MxGatewayAlarmMapperTests.cs
git commit -m "feat(dcl): MxGateway StreamAlarms adapter (snapshot + live transitions)"
Task 13: SiteRuntimeOptions — alarm cap + retry
Classification: trivial Estimated implement time: ~2 min Parallelizable with: Task 14
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs
Step 1–3: Add:
/// <summary>Max mirrored native alarms retained per source binding before older entries are dropped (logged).</summary>
public int MirroredAlarmCapPerSource { get; set; } = 1000;
/// <summary>Interval to retry a failed native alarm subscription.</summary>
public int NativeAlarmRetryIntervalMs { get; set; } = 5000;
Step 4: Build dotnet build ZB.MOM.WW.ScadaBridge.slnx → OK (bound from ScadaBridge:SiteRuntime).
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs
git commit -m "feat(siteruntime): native alarm cap + retry options"
Task 14: Site SQLite NativeAlarmState store
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 13
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs(create table inInitializeAsync; addUpsertNativeAlarmAsync,DeleteNativeAlarmAsync,GetNativeAlarmsAsync(instance, sourceCanonical),ClearNativeAlarmsForInstanceAsync) - Test:
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/NativeAlarmStateStoreTests.cs
Schema (raw SQL, mirrors static_attribute_overrides): native_alarm_state(instance_unique_name TEXT, source_canonical_name TEXT, source_reference TEXT, condition_json TEXT, last_transition_at TEXT, PRIMARY KEY(instance_unique_name, source_canonical_name, source_reference)).
Step 1: Write failing test:
public class NativeAlarmStateStoreTests : IDisposable
{
private readonly string _db = Path.Combine(Path.GetTempPath(), $"nas-{Guid.NewGuid():N}.db");
private readonly SiteStorageService _s;
public NativeAlarmStateStoreTests()
{ _s = new SiteStorageService($"Data Source={_db}", NullLogger<SiteStorageService>.Instance); _s.InitializeAsync().GetAwaiter().GetResult(); }
[Fact]
public async Task Upsert_Then_Get_RoundTrips()
{
await _s.UpsertNativeAlarmAsync("inst","Src","Tank01.Hi","{\"Active\":true}", DateTimeOffset.UnixEpoch);
var rows = await _s.GetNativeAlarmsAsync("inst","Src");
Assert.Single(rows);
Assert.Equal("Tank01.Hi", rows[0].SourceReference);
}
public void Dispose() { if (File.Exists(_db)) File.Delete(_db); }
}
Step 2: Run test → FAIL.
Step 3: Implement the table creation in InitializeAsync and the four methods following the SetStaticOverrideAsync raw-SQL ON CONFLICT ... DO UPDATE pattern. Return a small NativeAlarmRow(SourceReference, ConditionJson, LastTransitionAt) record.
Step 4: Run test → PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/NativeAlarmStateStoreTests.cs
git commit -m "feat(siteruntime): site SQLite native_alarm_state store"
Task 15: NativeAlarmActor
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: none
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs
Behavior: ctor (ResolvedNativeAlarmSource source, string instanceName, IActorRef instanceActor, IActorRef dclManager, SiteStorageService storage, SiteRuntimeOptions options, ILogger). PreStart: rehydrate _alarms from storage.GetNativeAlarmsAsync(instanceName, source.CanonicalName) (PipeTo), then dclManager.Tell(new SubscribeAlarmsRequest(...)). On NativeAlarmTransitionUpdate: for Snapshot buffer; SnapshotComplete atomic-swap (drop missing → emit return-to-normal AlarmStateChanged); Raise/Ack/Clear/Retrigger/StateChange upsert by SourceReference (ignore older TransitionTime), persist, emit enriched AlarmStateChanged (Kind = NativeOpcUa/NativeMxAccess, Condition, SourceReference, etc.) to instanceActor. Retention: drop when !Active && Acknowledged. On NativeAlarmSourceUnavailable: mark _alarms uncertain (re-emit with a quality flag — represent "uncertain" by leaving values but flipping a derived display; minimal: re-emit current condition unchanged but log + set health). Cap _alarms at options.MirroredAlarmCapPerSource and log when dropping (no silent truncation). On SubscribeAlarmsResponse(false,…): schedule a retry timer at NativeAlarmRetryIntervalMs.
Step 1: Write failing test (probe as instanceActor, NSubstitute/TestProbe as dclManager, temp-file SiteStorageService):
[Fact]
public void Raise_EmitsEnrichedAlarmStateChanged()
{
var instanceProbe = CreateTestProbe();
var dclProbe = CreateTestProbe();
var src = new ResolvedNativeAlarmSource { CanonicalName="Pressure", ConnectionName="Opc", SourceReference="ns=2;s=T01" };
var actor = ActorOf(Props.Create(() => new NativeAlarmActor(src,"inst",instanceProbe.Ref,dclProbe.Ref,_storage,_options,NullLogger<NativeAlarmActor>.Instance)));
dclProbe.ExpectMsg<SubscribeAlarmsRequest>(); // subscribed on start
var t = new NativeAlarmTransition("T01.Hi","T01","AnalogLimit.Hi",AlarmTransitionKind.Raise,
new AlarmConditionState(true,false,null,AlarmShelveState.Unshelved,false,800),"Process","hi","hi","","",null,DateTimeOffset.UtcNow,"92","90");
actor.Tell(new NativeAlarmTransitionUpdate("Opc", t));
var emitted = instanceProbe.ExpectMsg<AlarmStateChanged>();
Assert.Equal(AlarmKind.NativeOpcUa, emitted.Kind);
Assert.Equal("T01.Hi", emitted.SourceReference);
Assert.Equal(800, emitted.Condition.Severity);
}
Step 2: Run test → FAIL.
Step 3: Implement NativeAlarmActor per the behavior above. Derive Kind from the connection protocol (pass protocol in via the resolved source's connection, or set NativeOpcUa when SourceReference looks like a NodeId — simplest: pass a Kind hint through the subscribe response; pragmatic first cut: set Kind = NativeOpcUa and refine in Task 16 once protocol is known from the connection config). Keep MapTransitionToStateChanged a private pure method.
Step 4: Run test → PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/NativeAlarmActor.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/NativeAlarmActorTests.cs
git commit -m "feat(siteruntime): NativeAlarmActor mirrors source alarms (snapshot swap, retention, persistence)"
Task 16: InstanceActor wiring
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: none
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs(createNativeAlarmActorchildren from_configuration.NativeAlarmSources; pass_dclManager; ensureHandleAlarmStateChangedstores enriched state + include native alarms inDebugViewSnapshot) - Test:
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs
Detail: After the foreach (var alarm in _configuration.Alarms) block (~line 748), add a foreach (var src in _configuration.NativeAlarmSources) that does Context.ActorOf(Props.Create(() => new NativeAlarmActor(src, _instanceUniqueName, Self, _dclManager, _storage, _options, _logger)), $"native-alarm-{src.CanonicalName}") and tracks it in a _nativeAlarmActors dictionary. HandleAlarmStateChanged already routes through _alarmStates/_streamManager.PublishAlarmStateChanged; extend it to also retain the latest enriched AlarmStateChanged (store the whole record in a Dictionary<string, AlarmStateChanged> _latestAlarmEvents keyed by AlarmName) so the snapshot can carry enriched fields. In HandleSubscribeDebugView, build alarmStates from _latestAlarmEvents.Values (falling back to the computed projection for alarms with no event yet). Supervision (Resume for children) already covers NativeAlarmActor. On redeploy/stop, ClearNativeAlarmsForInstanceAsync.
Step 1: Write failing test:
[Fact]
public void DebugViewSnapshot_IncludesNativeAlarm_AfterTransition()
{
var config = new FlattenedConfiguration {
InstanceUniqueName = "inst",
NativeAlarmSources = [ new ResolvedNativeAlarmSource { CanonicalName="Pressure", ConnectionName="Opc", SourceReference="ns=2;s=T01" } ]
};
var actor = CreateInstanceActorWithDcl("inst", config, dclProbe.Ref);
// simulate the NativeAlarmActor emitting upward:
actor.Tell(new AlarmStateChanged("inst","T01.Hi",AlarmState.Active,800,DateTimeOffset.UtcNow)
{ Kind = AlarmKind.NativeOpcUa, SourceReference="T01.Hi",
Condition = new AlarmConditionState(true,false,null,AlarmShelveState.Unshelved,false,800) });
actor.Tell(new SubscribeDebugViewRequest("c","inst",DateTimeOffset.UtcNow));
var snap = ExpectMsg<DebugViewSnapshot>();
Assert.Contains(snap.AlarmStates, a => a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa);
}
(Extend the test helper to pass a _dclManager probe — add an InstanceActor ctor overload or thread the probe through CreateInstanceActor.)
Step 2: Run test → FAIL.
Step 3: Implement the wiring + _latestAlarmEvents retention + enriched snapshot.
Step 4: Run test → PASS. Run full SiteRuntime.Tests.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs
git commit -m "feat(siteruntime): InstanceActor spawns NativeAlarmActors + enriched alarm snapshot"
Task 17: Enrich computed AlarmActor emit
Classification: small Estimated implement time: ~3 min Parallelizable with: Task 18
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs(setCondition/Kindon eachAlarmStateChangedit constructs) - Test:
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/AlarmActorTests.cs(add assertion)
Detail: At each new AlarmStateChanged(...) site (binary activate/clear ~lines 207–227; HiLo ~260–266), add the init block:
{ Kind = AlarmKind.Computed, Condition = AlarmConditionStateFactory.ForComputed(state, _priority), Level = ..., Message = ... }
(The Condition getter already defaults to this, so the explicit set is belt-and-suspenders + makes intent clear; minimum change is just confirming Kind = Computed which is the default. Add a regression test asserting Condition.Severity == priority.)
Step 1: Add failing assertion to an existing activation test: Assert.Equal(msg.Priority, msg.Condition.Severity); and Assert.Equal(AlarmKind.Computed, msg.Kind);
Step 2: Run test → likely PASS already via defaults; if so this task reduces to adding the regression test. If a code path constructs AlarmStateChanged in a way that doesn't reflect _priority, fix it.
Step 3–4: Ensure pass.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/AlarmActorTests.cs
git commit -m "test(siteruntime): assert computed alarms carry unified condition state"
Task 18: Extend sitestream.proto + regenerate
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: Task 17
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto(append fields 8–21 toAlarmStateUpdate) - Modify (generated):
src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/Sitestream.cs(+ any*Grpc.cs) - Modify (toggle, then revert):
src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj
Step 1: Edit the proto — append to AlarmStateUpdate (do NOT renumber existing 1–7):
string kind = 8;
bool active = 9;
bool acknowledged = 10;
bool confirmed = 11;
string shelve_state = 12;
bool suppressed = 13;
string source_reference = 14;
string alarm_type_name = 15;
string category = 16;
string operator_user = 17;
string operator_comment = 18;
google.protobuf.Timestamp original_raise_time = 19;
string current_value = 20;
string limit_value = 21;
Step 2: Regenerate (macOS only — the documented manual process):
- In the
.csproj, uncomment the<Protobuf Include="Protos\sitestream.proto" GrpcServices="Both" />block. - Temporarily delete/rename the checked-in generated files under
SiteStreamGrpc/. dotnet build src/ZB.MOM.WW.ScadaBridge.Communication(protoc runs on macOS arm64; fails on arm64 Linux — that's why generated files are vendored).- Copy generated
Sitestream.cs/SitestreamGrpc.csfromobj/Debug/net10.0/Protos/toSiteStreamGrpc/. - Re-comment the
<Protobuf>block in the.csproj.
Step 3: Verify dotnet build ZB.MOM.WW.ScadaBridge.slnx compiles and the generated AlarmStateUpdate exposes the new properties (e.g. SourceReference, Acknowledged).
Step 4: No new unit test here (mapping is Task 19). Build is the gate.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Communication/Protos/sitestream.proto src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc src/ZB.MOM.WW.ScadaBridge.Communication/ZB.MOM.WW.ScadaBridge.Communication.csproj
git commit -m "feat(communication): extend AlarmStateUpdate proto with native alarm fields (regenerated)"
Task 19: gRPC alarm mapping (server + client)
Classification: standard Estimated implement time: ~4 min Parallelizable with: none
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs(HandleAlarmStateChanged— map new fields out) - Modify:
src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs(ConvertToDomainEvent— map new fields back) - Create:
src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AlarmShelveStateCodec.cs(string ↔AlarmShelveState) - Test:
tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs(extend round-trip)
Step 1: Write failing test — extend RelaysAlarmStateChanged_ToProtoEvent:
var domainEvent = new AlarmStateChanged("Site1.Pump01","T01.Hi",AlarmState.Active,2,timestamp)
{
Kind = AlarmKind.NativeMxAccess, SourceReference = "T01.Hi", Category = "Process",
OperatorUser = "op1", OperatorComment = "ack",
Condition = new AlarmConditionState(true, true, null, AlarmShelveState.OneShotShelved, false, 900)
};
actor.Tell(domainEvent);
// ...read protoEvent...
Assert.Equal("T01.Hi", protoEvent.AlarmChanged.SourceReference);
Assert.True(protoEvent.AlarmChanged.Acknowledged);
Assert.Equal(900, protoEvent.AlarmChanged.Suppressed ? 0 : 900); // severity via priority->condition mapping
Assert.Equal("OneShotShelved", protoEvent.AlarmChanged.ShelveState);
Assert.Equal("op1", protoEvent.AlarmChanged.OperatorUser);
Plus a client-side test: build an AlarmStateUpdate with the new fields and assert ConvertToDomainEvent populates Kind, Condition, SourceReference, etc.
Step 2: Run test → FAIL.
Step 3: Implement — In StreamRelayActor.HandleAlarmStateChanged, set the new proto fields from msg.Condition/msg.Kind/msg.SourceReference/… (severity → priority stays; also map active/acknowledged/confirmed/suppressed, shelve_state via codec, original_raise_time via Timestamp.FromDateTimeOffset when non-null). In SiteStreamGrpcClient.ConvertToDomainEvent, build the AlarmStateChanged init block with Kind = ParseKind(...), Condition = new AlarmConditionState(active, acknowledged, confirmed?, codec.Parse(shelve_state), suppressed, priority), and the metadata strings. Add AlarmShelveStateCodec.
Step 4: Run test → PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AlarmShelveStateCodec.cs tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs
git commit -m "feat(communication): map enriched alarm fields across gRPC (server + client)"
Task 20: Management command contracts + registry
Classification: small Estimated implement time: ~4 min Parallelizable with: Task 5, Task 8
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs(add native-alarm-source commands) - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs(add override commands) - Modify: the management command registry (find via
ManagementCommandRegistry— referenced bytests/.../ManagementCommandRegistryTests.cs) to register the new command type names - Test:
tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs(assert new commands resolve)
Contracts (mirror AddTemplateAlarmCommand / SetInstanceAlarmOverrideCommand):
// TemplateCommands.cs
public record AddTemplateNativeAlarmSourceCommand(int TemplateId, string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, bool IsLocked);
public record UpdateTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId, string Name, string ConnectionName, string SourceReference, string? ConditionFilter, string? Description, bool IsLocked);
public record DeleteTemplateNativeAlarmSourceCommand(int NativeAlarmSourceId);
public record ListTemplateNativeAlarmSourcesCommand(int TemplateId);
// InstanceCommands.cs
public record SetInstanceNativeAlarmSourceOverrideCommand(int InstanceId, string SourceCanonicalName, string? ConnectionNameOverride, string? SourceReferenceOverride, string? ConditionFilterOverride);
public record DeleteInstanceNativeAlarmSourceOverrideCommand(int InstanceId, string SourceCanonicalName);
public record ListInstanceNativeAlarmSourceOverridesCommand(int InstanceId);
Step 1: Write failing test asserting each new command name resolves through the registry. Step 2: FAIL. Step 3: add the records + register them. Step 4: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/ManagementCommandRegistryTests.cs
git commit -m "feat(commons): management command contracts for native alarm sources"
Task 21: ManagementActor handlers
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 22, Task 24, Task 25
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.ManagementService/(theManagementActorand/or its command handler partials — find whereAddTemplateAlarmCommand/SetInstanceAlarmOverrideCommandare handled and add the parallel handlers, calling the Task 6 repository methods within the existing unit-of-work) - Test:
tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs(CRUD round-trips, incl. authorization: Design role for template, Deployment for instance override — match the existing alarm commands' role checks)
Step 1: Write failing tests mirroring the existing template-alarm + instance-alarm-override handler tests (Add returns ManagementSuccess; unauthorized role returns ManagementUnauthorized; List returns the rows). Step 2: FAIL. Step 3: implement handlers + role checks + repository calls + SaveChangesAsync. Step 4: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.ManagementService tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs
git commit -m "feat(management): handlers for native alarm source CRUD"
Task 22: CLI commands
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 21, Task 24, Task 25
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs(add anative-alarm-sourcecompound subcommand withadd/list/remove, mirroringBuildAttribute) - Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs(addnative-alarm-source set/clear) - Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/README.md(document new commands) - Test:
tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs(assert tree builds + commands resolve)
Step 1: Write failing test asserting the command tree includes template native-alarm-source add/list/remove and instance native-alarm-source set/clear and the underlying management commands resolve. Step 2: FAIL. Step 3: implement via cmd.SetAction(... CommandHelpers.ExecuteCommandAsync(... AddTemplateNativeAlarmSourceCommand ...)) per the DataConnectionCommands pattern. Step 4: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.CLI tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs
git commit -m "feat(cli): native-alarm-source commands (template + instance)"
Task 23: DebugView alarm table enrichment
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 24, Task 25
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor(add columns/badges; the model already isAlarmStateChanged, now enriched) - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/DebugViewAlarmTableTests.cs(bUnit)
Detail: Add header cells (Severity, State badge set, Kind, Source, Type, Category) and body cells; render a composite condition badge from AlarmConditionState (Active→danger, Acked→muted, Shelved/Suppressed→info), Kind badge (Computed vs Native), and tooltips for operator/comment, raise time, current/limit value. Use the frontend-design skill for styling; Bootstrap only. Keep UpsertWithCap/filter logic; extend FilteredAlarmStates to also match SourceReference.
Step 1: Write failing bUnit test — render DebugView with a seeded snapshot containing one native AlarmStateChanged (Kind=NativeOpcUa, Severity=800, Shelved) and assert the rendered markup shows the severity and a shelved badge. Step 2: FAIL. Step 3: implement. Step 4: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/DebugViewAlarmTableTests.cs
git commit -m "feat(ui): enrich DebugView alarm table with severity + condition state + native metadata"
Task 24: Template editor — Native Alarm Sources subsection
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 23, Task 25
Files:
- Modify: the template detail/edit page (
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razorand/orTemplateEdit.razor— locate the Alarms subsection during impl) - Create (optional):
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/NativeAlarmSourceEditor.razor - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplateNativeAlarmSourceEditorTests.cs
Detail: A subsection listing native alarm sources with add/edit/delete; row fields: Name, Connection (dropdown filtered to alarm-capable protocols — reuse the connection-dropdown pattern from InstanceConfigure), Source Reference, optional Filter, Lock toggle. Wire add/edit/delete to the Task 20 management commands via the existing management HTTP client. Respect lock/inherit display like the Alarms section.
Step 1: Write failing bUnit test (renders the subsection, simulates add, asserts the management command is posted). Step 2: FAIL. Step 3: implement (frontend-design skill). Step 4: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplateNativeAlarmSourceEditorTests.cs
git commit -m "feat(ui): template editor Native Alarm Sources subsection"
Task 25: Instance Configure — native alarm source override panel
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 23, Task 24
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor(new card after Alarm Overrides) - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/InstanceConfigureNativeAlarmTests.cs
Detail: A card listing the instance's resolved native alarm sources with per-row override of Connection/Source Reference/Filter (blank = inherited), saving via SetInstanceNativeAlarmSourceOverrideCommand; clear via DeleteInstanceNativeAlarmSourceOverrideCommand. Mirror the Connection Bindings + Alarm Overrides UX.
Step 1: Write failing bUnit test (renders card, edits a source-ref override, asserts the set command posts). Step 2: FAIL. Step 3: implement. Step 4: PASS.
Step 5: Commit
git add src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/InstanceConfigureNativeAlarmTests.cs
git commit -m "feat(ui): instance configure native alarm source override panel"
Task 26: docker-env2 seed — sample native alarm source
Classification: small Estimated implement time: ~4 min Parallelizable with: Task 27
Files:
- Modify: the
docker-env2seed (the script/CLI calls that created theScadaBridge Site <X>MxGateway connection — find underdocker-env2/) - Modify (if needed):
docker-env2/README.md
Detail: After the existing MxGateway connection seed, add a template native-alarm-source add (connection = the seeded MxGateway connection, a representative source reference / MxAccess area) and deploy an instance, so the feature is manually verifiable end-to-end. Use the CLI commands from Task 22.
Step 1–3: Add the seed lines; bash docker-env2/deploy.sh (or the seed step) runs without error. Step 4: Verify the connection + binding exist via scadabridge ... template native-alarm-source list.
Step 5: Commit
git add docker-env2
git commit -m "chore(docker-env2): seed sample native alarm source binding on site-x"
Task 27: Documentation sync
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 26
Files:
- Modify:
docs/requirements/Component-DataConnectionLayer.md,Component-SiteRuntime.md,Component-TemplateEngine.md,Component-CentralUI.md,Component-CLI.md,Component-Communication.md(a.k.a. Central–Site Communication),Component-ConfigurationDatabase.md - Modify:
README.md(component table notes if needed),CLAUDE.md(add a "Native Alarms" bullet under Key Design Decisions → Data & Communication)
Detail: Document the read-only native alarm mirror: the IAlarmSubscribableConnection seam + per-connection feed + source-ref routing (DCL doc); NativeAlarmActor + native_alarm_state SQLite + enriched AlarmStateChanged (SiteRuntime doc); ResolvedNativeAlarmSource flattening + validation (TemplateEngine doc); enriched AlarmStateUpdate proto (Communication doc); new entities/migration/repo (ConfigurationDatabase doc); DebugView enrichment + authoring panels (CentralUI doc); CLI commands (CLI doc). Keep cross-references consistent per CLAUDE.md editing rules.
Step 1–3: Edit docs. Step 4: Re-read to confirm no stale cross-references. Step 5: Commit
git add docs README.md CLAUDE.md
git commit -m "docs: native alarm ingestion across component docs + CLAUDE.md"
Task 28: Integration / live verification
Classification: standard Estimated implement time: ~5 min (plus manual) Parallelizable with: none
Files:
- Create:
tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmLiveSmokeTests.cs(SkippableFact — skips when no alarm-capable OPC UA server is reachable, mirroring the existing live OPC UA smoke pattern) - Modify (if needed):
docs/test_infra/test_infra.md(note A&C requirement)
Detail: Add a SkippableFact that, against an alarm-capable OPC UA endpoint, subscribes via the adapter and asserts a Snapshot/SnapshotComplete sequence arrives (skip if the infra OPC UA server does not expose Alarms & Conditions). Document in test_infra.md whether the infra OPC UA server exposes A&C; if not, note the simulation/external-server requirement (open item from the design). Then perform the manual end-to-end check:
dotnet build ZB.MOM.WW.ScadaBridge.slnx
bash docker/deploy.sh # or docker-env2/deploy.sh for the MxGateway path
# Open Central UI DebugView for the seeded instance; confirm native alarms appear with severity + condition badges.
Step 5: Commit
git add tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmLiveSmokeTests.cs docs/test_infra/test_infra.md
git commit -m "test(dcl): OPC UA A&C live smoke (skippable) + test-infra note"
Final verification (after all tasks)
dotnet build ZB.MOM.WW.ScadaBridge.slnx
dotnet test ZB.MOM.WW.ScadaBridge.slnx
bash docker/deploy.sh # rebuild cluster image for runtime changes (DCL/SiteRuntime/Communication/Host)
Confirm: native alarms appear in DebugView with severity + condition badges (Active/Acked/Shelved/Suppressed), computed alarms unchanged, no central tables added, and the migration applies cleanly.
Open items carried from design
- MxGateway
StreamAlarmsmust deliver end-to-end (x86 COM-worker caveat from OtOpcUa) — verified in Task 28 manual check. - Infra OPC UA server A&C support — resolved/noted in Task 28.
ZB.MOM.WW.MxGateway.Clientpackage must exposeStreamAlarmsAsync— checked in Task 12.
