feat(alarms): HiLo trigger type with per-band level, hysteresis, messages, overrides
Adds a new HiLo alarm trigger type with four configurable setpoints
(LoLo / Lo / Hi / HiHi). Each setpoint carries an optional priority,
deadband (for hysteresis), and operator message. The site runtime emits
AlarmStateChanged with an AlarmLevel field so consumers can differentiate
warning vs critical bands.
Plumbing:
- new AlarmLevel enum + AlarmStateChanged.Level/Message init properties
- AlarmTriggerEditor (Blazor) gets a HiLo render with severity tinting
- AlarmTriggerConfigCodec extracted from the editor for testability
- sitestream.proto carries level + message over gRPC
- SemanticValidator enforces numeric attribute, setpoint ordering,
non-negative deadband
- on-trigger scripts get an Alarm global (Name/Level/Priority/Message)
so notification routing can branch by severity
- per-instance InstanceAlarmOverride entity + EF migration + flattening
step + CLI commands; HiLo overrides merge setpoint-by-setpoint, binary
types whole-replace
- DebugView shows a Level badge + per-band message tooltip
- App.razor auto-reloads on permanent Blazor circuit failure
- docker/regen-proto.sh automates the proto regen workflow (the linux/arm64
protoc segfault means generated files are checked in for now)
This commit is contained in:
@@ -169,6 +169,94 @@ public class InstanceService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a per-instance alarm override. The alarm must exist on the
|
||||
/// template and must not be locked. For HiLo alarms, the override JSON
|
||||
/// merges into the inherited TriggerConfiguration setpoint-by-setpoint;
|
||||
/// for binary trigger types, it replaces the whole config.
|
||||
/// </summary>
|
||||
public async Task<Result<InstanceAlarmOverride>> SetAlarmOverrideAsync(
|
||||
int instanceId,
|
||||
string alarmCanonicalName,
|
||||
string? triggerConfigurationOverride,
|
||||
int? priorityLevelOverride,
|
||||
string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
|
||||
if (instance == null)
|
||||
return Result<InstanceAlarmOverride>.Failure($"Instance with ID {instanceId} not found.");
|
||||
|
||||
// Verify alarm exists in the template and is not locked. Only direct
|
||||
// template alarms are checked here — composed-member overrides go
|
||||
// through but are silently ignored at runtime if the name doesn't
|
||||
// match (same behavior as attribute overrides).
|
||||
var templateAlarms = await _repository.GetAlarmsByTemplateIdAsync(instance.TemplateId, cancellationToken);
|
||||
var templateAlarm = templateAlarms.FirstOrDefault(a => a.Name == alarmCanonicalName);
|
||||
if (templateAlarm != null && templateAlarm.IsLocked)
|
||||
{
|
||||
return Result<InstanceAlarmOverride>.Failure(
|
||||
$"Alarm '{alarmCanonicalName}' is locked and cannot be overridden.");
|
||||
}
|
||||
|
||||
var existingOverride = await _repository.GetAlarmOverrideAsync(
|
||||
instanceId, alarmCanonicalName, cancellationToken);
|
||||
|
||||
if (existingOverride != null)
|
||||
{
|
||||
existingOverride.TriggerConfigurationOverride = triggerConfigurationOverride;
|
||||
existingOverride.PriorityLevelOverride = priorityLevelOverride;
|
||||
await _repository.UpdateInstanceAlarmOverrideAsync(existingOverride, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await _auditService.LogAsync(user, "UpdateAlarmOverride", "InstanceAlarmOverride",
|
||||
existingOverride.Id.ToString(), alarmCanonicalName, existingOverride, cancellationToken);
|
||||
|
||||
return Result<InstanceAlarmOverride>.Success(existingOverride);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newOverride = new InstanceAlarmOverride(alarmCanonicalName)
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
TriggerConfigurationOverride = triggerConfigurationOverride,
|
||||
PriorityLevelOverride = priorityLevelOverride
|
||||
};
|
||||
|
||||
await _repository.AddInstanceAlarmOverrideAsync(newOverride, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await _auditService.LogAsync(user, "CreateAlarmOverride", "InstanceAlarmOverride",
|
||||
newOverride.Id.ToString(), alarmCanonicalName, newOverride, cancellationToken);
|
||||
|
||||
return Result<InstanceAlarmOverride>.Success(newOverride);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a per-instance alarm override. After removal the instance
|
||||
/// inherits the template alarm config unchanged.
|
||||
/// </summary>
|
||||
public async Task<Result<bool>> DeleteAlarmOverrideAsync(
|
||||
int instanceId,
|
||||
string alarmCanonicalName,
|
||||
string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await _repository.GetAlarmOverrideAsync(
|
||||
instanceId, alarmCanonicalName, cancellationToken);
|
||||
if (existing == null)
|
||||
return Result<bool>.Failure($"No alarm override for '{alarmCanonicalName}' on instance {instanceId}.");
|
||||
|
||||
await _repository.DeleteInstanceAlarmOverrideAsync(existing.Id, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await _auditService.LogAsync(user, "DeleteAlarmOverride", "InstanceAlarmOverride",
|
||||
existing.Id.ToString(), alarmCanonicalName, existing, cancellationToken);
|
||||
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets connection bindings for an instance in bulk.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user