feat(instance): native-alarm-source-override CSV bulk import (deferred #12) + doc fixes
Closes the operator parity gap the deferred-work register tracked as #12: native alarm source overrides could only be set one-at-a-time, while attribute overrides had a CSV bulk path. Adds an all-or-nothing CSV import for native sources. - Commons: extract the RFC-4180 line splitter into a shared CsvLineSplitter (refactor OverrideCsvParser onto it — no behavior change, pinned by its tests); new NativeAlarmSourceOverrideCsvParser (header SourceName,Connection, SourceReference,Filter; blank = inherited). - Commons: NativeAlarmSourceOverrideEntry + bulk SetInstanceNativeAlarmSource- OverridesCommand (auto-registered via the reflection command registry). - ManagementService: Deployer-gated handler — flattens once, validates every source resolves + is unlocked + no duplicates up front, then upserts the whole batch under a single SaveChanges (true all-or-nothing txn). Added to the frozen authorization matrix; dispatch-coverage guard passes. - CLI: `instance native-alarm-source import --instance-id --file` (parity with `instance import-overrides`) + README. - Tests: native parser (Commons), CLI parse/entry mapping, 3 bulk-handler tests (happy path single-commit, locked-source reject, unresolved-source reject). Also corrects a stale XML-doc line in ScriptRuntimeContext (WaitForAttribute quality-gated mode is shipped, not "planned") and updates the deferred-work register: marks #7/#13/#15/#16/#20 verified-resolved, #12 as CLI/API-shipped with only the Central UI upload affordance still pending. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -270,6 +270,7 @@ public class ManagementActor : ReceiveActor
|
||||
or SetConnectionBindingsCommand or SetInstanceOverridesCommand or SetInstanceAreaCommand
|
||||
or SetInstanceAlarmOverrideCommand or DeleteInstanceAlarmOverrideCommand
|
||||
or SetInstanceNativeAlarmSourceOverrideCommand or DeleteInstanceNativeAlarmSourceOverrideCommand
|
||||
or SetInstanceNativeAlarmSourceOverridesCommand
|
||||
or GetDeploymentDiffCommand
|
||||
or MgmtDeployArtifactsCommand
|
||||
or QueryDeploymentsCommand
|
||||
@@ -347,6 +348,7 @@ public class ManagementActor : ReceiveActor
|
||||
DeleteInstanceAlarmOverrideCommand cmd => await HandleDeleteInstanceAlarmOverride(sp, cmd, user),
|
||||
ListInstanceAlarmOverridesCommand cmd => await HandleListInstanceAlarmOverrides(sp, cmd, user),
|
||||
SetInstanceNativeAlarmSourceOverrideCommand cmd => await HandleSetInstanceNativeAlarmSourceOverride(sp, cmd, user),
|
||||
SetInstanceNativeAlarmSourceOverridesCommand cmd => await HandleSetInstanceNativeAlarmSourceOverrides(sp, cmd, user),
|
||||
DeleteInstanceNativeAlarmSourceOverrideCommand cmd => await HandleDeleteInstanceNativeAlarmSourceOverride(sp, cmd, user),
|
||||
ListInstanceNativeAlarmSourceOverridesCommand cmd => await HandleListInstanceNativeAlarmSourceOverrides(sp, cmd, user),
|
||||
|
||||
@@ -998,6 +1000,78 @@ public class ManagementActor : ReceiveActor
|
||||
return existing;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleSetInstanceNativeAlarmSourceOverrides(
|
||||
IServiceProvider sp, SetInstanceNativeAlarmSourceOverridesCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId);
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
|
||||
// All-or-nothing (parity with HandleSetInstanceOverrides). Flatten the
|
||||
// instance ONCE (script compilation skipped — a lock/resolve check does not
|
||||
// need it) and validate EVERY requested source up front: a source that does
|
||||
// not resolve would create a dead override the flattener silently drops, and a
|
||||
// template-locked source cannot be overridden. Only once the whole batch is
|
||||
// known valid do we upsert — a mid-batch reject never leaves partial state.
|
||||
var pipeline = sp.GetRequiredService<IFlatteningPipeline>();
|
||||
var flattenResult = await pipeline.FlattenAndValidateAsync(
|
||||
cmd.InstanceId, default, validateScripts: false);
|
||||
if (flattenResult.IsFailure)
|
||||
throw new ManagementCommandException(
|
||||
$"Cannot set native alarm source overrides: the instance could not be flattened ({flattenResult.Error}).");
|
||||
|
||||
var resolvedByName = flattenResult.Value.Configuration.NativeAlarmSources
|
||||
.ToDictionary(s => s.CanonicalName, StringComparer.Ordinal);
|
||||
|
||||
// Reject a batch that names the same source twice — the intended override
|
||||
// would be ambiguous, and the last-writer-wins upsert would silently discard
|
||||
// the earlier row.
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in cmd.Overrides)
|
||||
{
|
||||
if (!resolvedByName.TryGetValue(entry.SourceCanonicalName, out var resolvedSource))
|
||||
throw new ManagementCommandException(
|
||||
$"Native alarm source '{entry.SourceCanonicalName}' does not resolve for this instance " +
|
||||
"and cannot be overridden. No overrides were applied.");
|
||||
if (resolvedSource.IsLocked)
|
||||
throw new ManagementCommandException(
|
||||
$"Native alarm source '{entry.SourceCanonicalName}' is locked at the template level and " +
|
||||
"cannot be overridden. No overrides were applied.");
|
||||
if (!seen.Add(entry.SourceCanonicalName))
|
||||
throw new ManagementCommandException(
|
||||
$"Native alarm source '{entry.SourceCanonicalName}' appears more than once in the batch. " +
|
||||
"No overrides were applied.");
|
||||
}
|
||||
|
||||
var results = new List<InstanceNativeAlarmSourceOverride>();
|
||||
foreach (var entry in cmd.Overrides)
|
||||
{
|
||||
var existing = await repo.GetNativeAlarmSourceOverrideAsync(cmd.InstanceId, entry.SourceCanonicalName);
|
||||
if (existing == null)
|
||||
{
|
||||
var ovr = new InstanceNativeAlarmSourceOverride(entry.SourceCanonicalName)
|
||||
{
|
||||
InstanceId = cmd.InstanceId,
|
||||
ConnectionNameOverride = entry.ConnectionNameOverride,
|
||||
SourceReferenceOverride = entry.SourceReferenceOverride,
|
||||
ConditionFilterOverride = entry.ConditionFilterOverride
|
||||
};
|
||||
await repo.AddInstanceNativeAlarmSourceOverrideAsync(ovr);
|
||||
results.Add(ovr);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.ConnectionNameOverride = entry.ConnectionNameOverride;
|
||||
existing.SourceReferenceOverride = entry.SourceReferenceOverride;
|
||||
existing.ConditionFilterOverride = entry.ConditionFilterOverride;
|
||||
await repo.UpdateInstanceNativeAlarmSourceOverrideAsync(existing);
|
||||
results.Add(existing);
|
||||
}
|
||||
}
|
||||
|
||||
await repo.SaveChangesAsync();
|
||||
return results;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteInstanceNativeAlarmSourceOverride(
|
||||
IServiceProvider sp, DeleteInstanceNativeAlarmSourceOverrideCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user