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:
Joseph Doherty
2026-07-10 08:52:12 -04:00
parent 5a878b78d4
commit f480a56737
13 changed files with 748 additions and 106 deletions
@@ -477,6 +477,53 @@ public static class InstanceCommands
});
group.Add(clearCmd);
// import (CSV bulk apply — parity with `instance import-overrides`)
var importIdOption = new Option<int>("--instance-id") { Description = "Instance ID", Required = true };
var importFileOption = new Option<string>("--file")
{
Description = "Path to the override CSV file (columns: SourceName,Connection,SourceReference,Filter; blank field = inherited)",
Required = true
};
var importCmd = new Command("import")
{
Description = "Apply native alarm source overrides from a CSV file (all-or-nothing)"
};
importCmd.Add(importIdOption);
importCmd.Add(importFileOption);
importCmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(importIdOption);
var filePath = result.GetValue(importFileOption)!;
string csvText;
try
{
csvText = File.ReadAllText(filePath);
}
catch (Exception ex)
{
OutputFormatter.WriteError($"Cannot read file '{filePath}': {ex.Message}", "FILE_READ_ERROR");
return 1;
}
var parseResult = NativeAlarmSourceOverrideCsvParser.Parse(csvText);
if (parseResult.Errors.Count > 0)
{
foreach (var error in parseResult.Errors)
OutputFormatter.WriteError(error, "INVALID_CSV");
return 1;
}
var overrides = parseResult.Rows
.Select(r => new NativeAlarmSourceOverrideEntry(
r.SourceName, r.Connection, r.SourceReference, r.Filter))
.ToList();
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new SetInstanceNativeAlarmSourceOverridesCommand(id, overrides));
});
group.Add(importCmd);
return group;
}
+25
View File
@@ -503,6 +503,31 @@ Clear an instance's native alarm source override, reverting to the inherited bin
scadabridge --url <url> instance native-alarm-source clear --instance-id <int> --source <string>
```
#### `instance native-alarm-source import`
Bulk-apply native alarm source overrides for a single instance from a CSV file
(all-or-nothing — the native-source parity of `instance import-overrides`). The CSV
header is required: `SourceName,Connection,SourceReference,Filter`; a blank override
field keeps the inherited value. Every named source must resolve for the instance and
be unlocked, or the whole batch is rejected before any write.
```sh
scadabridge --url <url> instance native-alarm-source import --instance-id <int> --file <path>
```
| Option | Required | Description |
|--------|----------|-------------|
| `--instance-id` | yes | Instance ID |
| `--file` | yes | Path to the override CSV (`SourceName,Connection,SourceReference,Filter`; blank field = inherited) |
Example CSV:
```csv
SourceName,Connection,SourceReference,Filter
Pressure,OpcB,ns=2;s=Pump.Alarm,Active
Module.Temp,,,
```
#### `instance deploy`
Deploy an instance to its site. Acquires the per-instance operation lock.