Add UI features, alarm ack, historian UTC fix, and Client.UI documentation

Major changes across the client stack:
- Settings persistence (connection, subscriptions, alarm source)
- Deferred OPC UA SDK init for instant startup
- Array/status code formatting, write value popup, alarm acknowledgment
- Severity-colored alarm rows, condition dedup on server side
- DateTimeRangePicker control with preset buttons and UTC text input
- Historian queries use wwTimezone=UTC and OPCQuality column
- Recursive subscribe from tree, multi-select remove
- Connection panel with expander, folder chooser for cert path
- Dynamic tab headers showing subscription/alarm counts
- Client.UI.md documentation with headless-rendered screenshots

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-31 20:46:45 -04:00
parent 8fae2cb790
commit 188cbf7d24
53 changed files with 2652 additions and 189 deletions

View File

@@ -14,6 +14,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
public ConnectionInfo? ConnectResult { get; set; }
public Exception? ConnectException { get; set; }
public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = [];
public Dictionary<string, IReadOnlyList<BrowseResult>> BrowseResultsByParent { get; set; } = new();
public Exception? BrowseException { get; set; }
public DataValue ReadResult { get; set; } =
@@ -100,6 +101,10 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
BrowseCallCount++;
LastBrowseParentNodeId = parentNodeId;
if (BrowseException != null) throw BrowseException;
if (parentNodeId != null && BrowseResultsByParent.TryGetValue(parentNodeId.ToString(), out var perParent))
return Task.FromResult(perParent);
return Task.FromResult(BrowseResults);
}
@@ -136,6 +141,18 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
return Task.CompletedTask;
}
public StatusCode AcknowledgeResult { get; set; } = StatusCodes.Good;
public Exception? AcknowledgeException { get; set; }
public int AcknowledgeCallCount { get; private set; }
public Task<StatusCode> AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
CancellationToken ct = default)
{
AcknowledgeCallCount++;
if (AcknowledgeException != null) throw AcknowledgeException;
return Task.FromResult(AcknowledgeResult);
}
public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
int maxValues = 1000, CancellationToken ct = default)
{