docs: complete XML doc coverage (returns, summaries, inheritdoc)

Resolve all 622 issues flagged by the enhanced CommentChecker: add missing
<returns> tags (incl. the standard phrasing on non-generic Task methods),
add missing <summary> tags, and replace misused/redundant <inheritdoc/> on
members that override or implement nothing with real documentation.
Documentation-only — no behavior change; solution builds clean.
This commit is contained in:
Joseph Doherty
2026-06-03 11:39:32 -04:00
parent a050170414
commit eabf270d71
208 changed files with 867 additions and 114 deletions
@@ -76,7 +76,12 @@ public sealed class AuditLogPartitionMaintenanceService : IHostedService, IDispo
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
/// <inheritdoc /> /// <summary>
/// Starts the background maintenance loop, firing an immediate first tick and then
/// repeating every <see cref="AuditLogPartitionMaintenanceOptions.IntervalSeconds"/>.
/// </summary>
/// <param name="ct">Cancellation token provided by the host.</param>
/// <returns>A completed task; the loop runs independently on a background thread.</returns>
public Task StartAsync(CancellationToken ct) public Task StartAsync(CancellationToken ct)
{ {
// Linked CTS lets StopAsync's cancellation AND the host's shutdown // Linked CTS lets StopAsync's cancellation AND the host's shutdown
@@ -136,14 +141,21 @@ public sealed class AuditLogPartitionMaintenanceService : IHostedService, IDispo
} }
} }
/// <inheritdoc /> /// <summary>
/// Signals the maintenance loop to stop by cancelling its linked token,
/// then returns the loop task so the host can await its completion.
/// </summary>
/// <param name="ct">Cancellation token provided by the host (unused — the internal CTS is cancelled directly).</param>
/// <returns>The background loop task, or a completed task if the loop was never started.</returns>
public Task StopAsync(CancellationToken ct) public Task StopAsync(CancellationToken ct)
{ {
_cts?.Cancel(); _cts?.Cancel();
return _loop ?? Task.CompletedTask; return _loop ?? Task.CompletedTask;
} }
/// <inheritdoc /> /// <summary>
/// Disposes the internal <see cref="CancellationTokenSource"/> used to stop the maintenance loop.
/// </summary>
public void Dispose() public void Dispose()
{ {
_cts?.Dispose(); _cts?.Dispose();
@@ -41,6 +41,7 @@ public interface IPullAuditEventsClient
/// <param name="sinceUtc">Only events with an <c>OccurredAtUtc</c> at or after this cursor time are returned.</param> /// <param name="sinceUtc">Only events with an <c>OccurredAtUtc</c> at or after this cursor time are returned.</param>
/// <param name="batchSize">Maximum number of events to return per call.</param> /// <param name="batchSize">Maximum number of events to return per call.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the next reconciliation batch with a <c>MoreAvailable</c> flag.</returns>
Task<PullAuditEventsResponse> PullAsync( Task<PullAuditEventsResponse> PullAsync(
string siteId, string siteId,
DateTime sinceUtc, DateTime sinceUtc,
@@ -23,6 +23,7 @@ public interface ISiteEnumerator
/// — the actor calls this once per tick. /// — the actor calls this once per tick.
/// </summary> /// </summary>
/// <param name="ct">Cancellation token for the async enumeration.</param> /// <param name="ct">Cancellation token for the async enumeration.</param>
/// <returns>A task that resolves to the current set of site entries to poll on the next reconciliation tick.</returns>
Task<IReadOnlyList<SiteEntry>> EnumerateAsync(CancellationToken ct = default); Task<IReadOnlyList<SiteEntry>> EnumerateAsync(CancellationToken ct = default);
} }
@@ -133,6 +133,7 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
/// Returns a defensive copy of the per-site latched stalled state. /// Returns a defensive copy of the per-site latched stalled state.
/// Absent sites are interpreted as <c>Stalled=false</c> by consumers. /// Absent sites are interpreted as <c>Stalled=false</c> by consumers.
/// </summary> /// </summary>
/// <returns>A snapshot dictionary mapping each known site ID to its current stalled state.</returns>
public IReadOnlyDictionary<string, bool> Snapshot() => public IReadOnlyDictionary<string, bool> Snapshot() =>
new Dictionary<string, bool>(_state); new Dictionary<string, bool>(_state);
@@ -71,6 +71,11 @@ internal static class AuditRedactionPrimitives
/// field is over-redacted with <see cref="RedactorErrorMarker"/> and /// field is over-redacted with <see cref="RedactorErrorMarker"/> and
/// <paramref name="onFailure"/> is invoked. /// <paramref name="onFailure"/> is invoked.
/// </summary> /// </summary>
/// <param name="json">The raw JSON string to redact; null passes through as null.</param>
/// <param name="redactList">Header names (case-insensitive) whose values should be replaced.</param>
/// <param name="logger">Logger for warning diagnostics on redactor faults.</param>
/// <param name="onFailure">Callback invoked when the redactor stage faults; used to increment health counters.</param>
/// <returns>The re-serialized JSON with redacted header values, the original string if nothing was redacted, or <see cref="RedactorErrorMarker"/> on fault.</returns>
public static string? RedactHeaders( public static string? RedactHeaders(
string? json, string? json,
IList<string> redactList, IList<string> redactList,
@@ -152,6 +157,11 @@ internal static class AuditRedactionPrimitives
/// with <see cref="RedactorErrorMarker"/> and <paramref name="onFailure"/> /// with <see cref="RedactorErrorMarker"/> and <paramref name="onFailure"/>
/// is invoked — the user-facing action is never aborted. /// is invoked — the user-facing action is never aborted.
/// </summary> /// </summary>
/// <param name="value">The string to redact; null passes through as null.</param>
/// <param name="regexes">Compiled body-redaction regexes applied in order.</param>
/// <param name="logger">Logger for warning diagnostics on redactor faults.</param>
/// <param name="onFailure">Callback invoked when a regex match faults; used to increment health counters.</param>
/// <returns>The value with all regex matches replaced by <see cref="RedactedMarker"/>, or <see cref="RedactorErrorMarker"/> on fault.</returns>
public static string? RedactBody( public static string? RedactBody(
string? value, string? value,
IReadOnlyList<Regex> regexes, IReadOnlyList<Regex> regexes,
@@ -192,6 +202,11 @@ internal static class AuditRedactionPrimitives
/// is over-redacted with <see cref="RedactorErrorMarker"/> and /// is over-redacted with <see cref="RedactorErrorMarker"/> and
/// <paramref name="onFailure"/> is invoked. /// <paramref name="onFailure"/> is invoked.
/// </summary> /// </summary>
/// <param name="json">The raw JSON string to redact; null passes through as null.</param>
/// <param name="paramNameRegex">Compiled regex matched against each SQL parameter name.</param>
/// <param name="logger">Logger for warning diagnostics on redactor faults.</param>
/// <param name="onFailure">Callback invoked when the redactor stage faults; used to increment health counters.</param>
/// <returns>The re-serialized JSON with matched parameter values replaced by <see cref="RedactedMarker"/>, the original string if no parameters matched, or <see cref="RedactorErrorMarker"/> on fault.</returns>
public static string? RedactSqlParameters( public static string? RedactSqlParameters(
string? json, string? json,
Regex paramNameRegex, Regex paramNameRegex,
@@ -277,6 +292,10 @@ internal static class AuditRedactionPrimitives
/// setting <paramref name="truncated"/> to <c>true</c> when the value was /// setting <paramref name="truncated"/> to <c>true</c> when the value was
/// shortened. Null passes through as null. /// shortened. Null passes through as null.
/// </summary> /// </summary>
/// <param name="value">The string to truncate; null passes through as null.</param>
/// <param name="cap">Maximum number of UTF-8 bytes to retain.</param>
/// <param name="truncated">Set to <c>true</c> when the value was shortened; unchanged otherwise.</param>
/// <returns>The truncated string, the original string if within the cap, or <c>null</c> if the input was null.</returns>
public static string? TruncateField(string? value, int cap, ref bool truncated) public static string? TruncateField(string? value, int cap, ref bool truncated)
{ {
if (value is null) if (value is null)
@@ -299,6 +318,9 @@ internal static class AuditRedactionPrimitives
/// (<c>byte &amp; 0xC0 == 0x80</c>), and decodes the resulting prefix — /// (<c>byte &amp; 0xC0 == 0x80</c>), and decodes the resulting prefix —
/// guaranteeing the returned string never splits a multi-byte sequence. /// guaranteeing the returned string never splits a multi-byte sequence.
/// </summary> /// </summary>
/// <param name="value">The string to truncate.</param>
/// <param name="capBytes">Maximum number of UTF-8 bytes in the returned string.</param>
/// <returns>The truncated string guaranteed not to split a multi-byte UTF-8 sequence, or the original string if within the cap.</returns>
public static string TruncateUtf8(string value, int capBytes) public static string TruncateUtf8(string value, int capBytes)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
@@ -35,6 +35,8 @@ internal sealed class AuditRegexCache
private readonly ConcurrentDictionary<string, CompiledRegex> _cache = new(); private readonly ConcurrentDictionary<string, CompiledRegex> _cache = new();
private readonly ILogger _logger; private readonly ILogger _logger;
/// <summary>Initializes the cache with the logger used to report compile failures.</summary>
/// <param name="logger">Logger for recording invalid or slow-compile pattern warnings.</param>
public AuditRegexCache(ILogger logger) => _logger = logger; public AuditRegexCache(ILogger logger) => _logger = logger;
/// <summary> /// <summary>
@@ -44,6 +46,9 @@ internal sealed class AuditRegexCache
/// compile time "invalid"); the failure is logged once and the sentinel /// compile time "invalid"); the failure is logged once and the sentinel
/// cache entry prevents repeat compile attempts. /// cache entry prevents repeat compile attempts.
/// </summary> /// </summary>
/// <param name="pattern">The regex pattern string to look up or compile.</param>
/// <param name="regex">The compiled <see cref="Regex"/>, or <c>null</c> if the pattern is invalid.</param>
/// <returns><c>true</c> if the pattern compiled successfully; <c>false</c> if it is invalid or too slow to compile.</returns>
public bool TryGet(string pattern, out Regex? regex) public bool TryGet(string pattern, out Regex? regex)
{ {
var entry = _cache.GetOrAdd(pattern, Compile); var entry = _cache.GetOrAdd(pattern, Compile);
@@ -88,8 +93,11 @@ internal sealed class AuditRegexCache
{ {
public static readonly CompiledRegex Invalid = new(null); public static readonly CompiledRegex Invalid = new(null);
/// <summary>The compiled regex, or <c>null</c> when this entry represents an invalid pattern.</summary>
public Regex? Regex { get; } public Regex? Regex { get; }
/// <summary>Initializes the entry with the compiled regex (or <c>null</c> for the invalid sentinel).</summary>
/// <param name="regex">The compiled <see cref="Regex"/>, or <c>null</c> for a failed compile.</param>
public CompiledRegex(Regex? regex) => Regex = regex; public CompiledRegex(Regex? regex) => Regex = regex;
} }
} }
@@ -37,7 +37,15 @@ public sealed class SafeDefaultAuditRedactor : IAuditRedactor
private SafeDefaultAuditRedactor() { } private SafeDefaultAuditRedactor() { }
/// <inheritdoc /> /// <summary>
/// Applies line-oriented header redaction to the default sensitive headers
/// (<c>Authorization</c>, <c>X-Api-Key</c>, <c>Cookie</c>, <c>Set-Cookie</c>)
/// found in <c>RequestSummary</c> and <c>ResponseSummary</c> inside
/// <paramref name="rawEvent"/>.<c>DetailsJson</c>. Never throws; over-redacts on
/// any internal failure.
/// </summary>
/// <param name="rawEvent">The audit event whose details JSON is to be redacted.</param>
/// <returns>A new <see cref="AuditEvent"/> with sensitive headers replaced by the redacted marker, or an over-redacted sentinel on failure.</returns>
public AuditEvent Apply(AuditEvent rawEvent) public AuditEvent Apply(AuditEvent rawEvent)
{ {
ArgumentNullException.ThrowIfNull(rawEvent); ArgumentNullException.ThrowIfNull(rawEvent);
@@ -73,7 +73,12 @@ public sealed class ScadaBridgeAuditRedactor : IAuditRedactor
_regexCache = new AuditRegexCache(_logger); _regexCache = new AuditRegexCache(_logger);
} }
/// <inheritdoc /> /// <summary>
/// Applies the full redaction pipeline to <paramref name="rawEvent"/> and returns a
/// filtered copy; returns the same instance unchanged on the fast path. Never throws.
/// </summary>
/// <param name="rawEvent">The raw audit event to redact.</param>
/// <returns>A redacted copy of <paramref name="rawEvent"/>, or the original instance when no changes are needed.</returns>
public AuditEvent Apply(AuditEvent rawEvent) public AuditEvent Apply(AuditEvent rawEvent)
{ {
try try
@@ -96,6 +96,7 @@ public sealed class RingBufferFallback
/// must call <see cref="Complete"/> first. /// must call <see cref="Complete"/> first.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancellation token to abort the async enumeration.</param> /// <param name="cancellationToken">Cancellation token to abort the async enumeration.</param>
/// <returns>An async sequence of buffered <see cref="AuditEvent"/> values in FIFO order.</returns>
public async IAsyncEnumerable<AuditEvent> DrainAsync( public async IAsyncEnumerable<AuditEvent> DrainAsync(
[EnumeratorCancellation] CancellationToken cancellationToken) [EnumeratorCancellation] CancellationToken cancellationToken)
{ {
@@ -69,7 +69,9 @@ public sealed class SiteAuditBacklogReporter : IHostedService, IDisposable
_refreshInterval = refreshInterval ?? DefaultRefreshInterval; _refreshInterval = refreshInterval ?? DefaultRefreshInterval;
} }
/// <inheritdoc /> /// <summary>Starts the background polling loop, running an immediate first probe before entering the timed cycle.</summary>
/// <param name="ct">Cancellation token signalling host shutdown.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task StartAsync(CancellationToken ct) public Task StartAsync(CancellationToken ct)
{ {
// Linked CTS lets StopAsync's cancellation AND the host's shutdown // Linked CTS lets StopAsync's cancellation AND the host's shutdown
@@ -123,14 +125,16 @@ public sealed class SiteAuditBacklogReporter : IHostedService, IDisposable
} }
} }
/// <inheritdoc /> /// <summary>Signals the polling loop to stop and waits for it to complete.</summary>
/// <param name="ct">Cancellation token (not used; the internal CTS governs shutdown).</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task StopAsync(CancellationToken ct) public Task StopAsync(CancellationToken ct)
{ {
_cts?.Cancel(); _cts?.Cancel();
return _loop ?? Task.CompletedTask; return _loop ?? Task.CompletedTask;
} }
/// <inheritdoc /> /// <summary>Releases the internal <see cref="CancellationTokenSource"/> used to stop the polling loop.</summary>
public void Dispose() public void Dispose()
{ {
_cts?.Dispose(); _cts?.Dispose();
@@ -244,7 +244,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
/// <inheritdoc /> /// <summary>
/// Enqueues an audit event for asynchronous batched persistence to SQLite.
/// Back-pressure is applied when the write channel is full.
/// </summary>
/// <param name="evt">The audit event to persist.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that completes when the event has been persisted.</returns>
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default) public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{ {
ArgumentNullException.ThrowIfNull(evt); ArgumentNullException.ThrowIfNull(evt);
@@ -469,7 +475,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
return CachedTelemetryKinds.Contains(kind); return CachedTelemetryKinds.Contains(kind);
} }
/// <inheritdoc /> /// <summary>
/// Returns up to <paramref name="limit"/> non-cached pending audit events, oldest first.
/// Cached-lifecycle kinds are excluded; use <see cref="ReadPendingCachedTelemetryAsync"/> for those.
/// </summary>
/// <param name="limit">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of pending audit events.</returns>
public Task<IReadOnlyList<AuditEvent>> ReadPendingAsync(int limit, CancellationToken ct = default) public Task<IReadOnlyList<AuditEvent>> ReadPendingAsync(int limit, CancellationToken ct = default)
{ {
if (limit <= 0) if (limit <= 0)
@@ -512,7 +524,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
} }
} }
/// <inheritdoc /> /// <summary>
/// Returns up to <paramref name="limit"/> pending cached-lifecycle audit events, oldest first.
/// Only rows with cached-call kinds (CachedSubmit, ApiCallCached, DbWriteCached, CachedResolve) are included.
/// </summary>
/// <param name="limit">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of pending cached-telemetry audit events.</returns>
public Task<IReadOnlyList<AuditEvent>> ReadPendingCachedTelemetryAsync( public Task<IReadOnlyList<AuditEvent>> ReadPendingCachedTelemetryAsync(
int limit, CancellationToken ct = default) int limit, CancellationToken ct = default)
{ {
@@ -560,6 +578,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
/// </summary> /// </summary>
/// <param name="limit">Maximum number of rows to return.</param> /// <param name="limit">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of forwarded audit events.</returns>
public Task<IReadOnlyList<AuditEvent>> ReadForwardedAsync(int limit, CancellationToken ct = default) public Task<IReadOnlyList<AuditEvent>> ReadForwardedAsync(int limit, CancellationToken ct = default)
{ {
if (limit <= 0) if (limit <= 0)
@@ -645,7 +664,15 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
} }
} }
/// <inheritdoc /> /// <summary>
/// Returns up to <paramref name="batchSize"/> pending or forwarded audit events
/// with <see cref="AuditEvent.OccurredAtUtc"/> &gt;= <paramref name="sinceUtc"/>, oldest first.
/// Used by the M6 reconciliation-pull handler.
/// </summary>
/// <param name="sinceUtc">Lower bound timestamp (UTC) for event occurrence.</param>
/// <param name="batchSize">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of audit events since the given timestamp.</returns>
public Task<IReadOnlyList<AuditEvent>> ReadPendingSinceAsync( public Task<IReadOnlyList<AuditEvent>> ReadPendingSinceAsync(
DateTime sinceUtc, int batchSize, CancellationToken ct = default) DateTime sinceUtc, int batchSize, CancellationToken ct = default)
{ {
@@ -867,6 +894,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
} }
/// <summary>Asynchronously disposes the audit writer and releases resources.</summary> /// <summary>Asynchronously disposes the audit writer and releases resources.</summary>
/// <returns>A <see cref="ValueTask"/> that completes when all resources have been released.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
Task? writerLoop; Task? writerLoop;
@@ -44,6 +44,9 @@ public sealed class ClusterClientSiteAuditClient : ISiteStreamAuditClient
private readonly IActorRef _siteCommunicationActor; private readonly IActorRef _siteCommunicationActor;
private readonly TimeSpan _askTimeout; private readonly TimeSpan _askTimeout;
/// <summary>
/// Initializes a new instance that forwards audit telemetry to central via the site's <c>SiteCommunicationActor</c>.
/// </summary>
/// <param name="siteCommunicationActor"> /// <param name="siteCommunicationActor">
/// The site's <c>SiteCommunicationActor</c> — it forwards the ingest command /// The site's <c>SiteCommunicationActor</c> — it forwards the ingest command
/// over the registered central ClusterClient and routes the reply back to /// over the registered central ClusterClient and routes the reply back to
@@ -22,6 +22,7 @@ public interface ISiteStreamAuditClient
/// </summary> /// </summary>
/// <param name="batch">The batch of audit events to forward.</param> /// <param name="batch">The batch of audit events to forward.</param>
/// <param name="ct">Cancellation token for the operation.</param> /// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A task that resolves to the ingest acknowledgement containing accepted event IDs.</returns>
Task<IngestAck> IngestAuditEventsAsync(AuditEventBatch batch, CancellationToken ct); Task<IngestAck> IngestAuditEventsAsync(AuditEventBatch batch, CancellationToken ct);
/// <summary> /// <summary>
@@ -42,5 +43,6 @@ public interface ISiteStreamAuditClient
/// </remarks> /// </remarks>
/// <param name="batch">The batch of cached-call telemetry packets to forward.</param> /// <param name="batch">The batch of cached-call telemetry packets to forward.</param>
/// <param name="ct">Cancellation token for the operation.</param> /// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A task that resolves to the ingest acknowledgement containing accepted event IDs.</returns>
Task<IngestAck> IngestCachedTelemetryAsync(CachedTelemetryBatch batch, CancellationToken ct); Task<IngestAck> IngestCachedTelemetryAsync(CachedTelemetryBatch batch, CancellationToken ct);
} }
@@ -13,6 +13,7 @@ public static class ApiMethodCommands
/// <param name="formatOption">Global option for the output format.</param> /// <param name="formatOption">Global option for the output format.</param>
/// <param name="usernameOption">Global option for the authentication username.</param> /// <param name="usernameOption">Global option for the authentication username.</param>
/// <param name="passwordOption">Global option for the authentication password.</param> /// <param name="passwordOption">Global option for the authentication password.</param>
/// <returns>The configured <c>api-method</c> command with all subcommands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("api-method") { Description = "Manage inbound API methods" }; var command = new Command("api-method") { Description = "Manage inbound API methods" };
@@ -18,6 +18,7 @@ public static class AuditCommands
/// <param name="formatOption">Global <c>--format</c> option for output format.</param> /// <param name="formatOption">Global <c>--format</c> option for output format.</param>
/// <param name="usernameOption">Global <c>--username</c> option for authentication.</param> /// <param name="usernameOption">Global <c>--username</c> option for authentication.</param>
/// <param name="passwordOption">Global <c>--password</c> option for authentication.</param> /// <param name="passwordOption">Global <c>--password</c> option for authentication.</param>
/// <returns>The configured <c>audit</c> <see cref="Command"/> with all sub-commands attached.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("audit") { Description = "Query and export the centralized audit log" }; var command = new Command("audit") { Description = "Query and export the centralized audit log" };
@@ -74,6 +74,7 @@ public static class AuditExportHelpers
/// </summary> /// </summary>
/// <param name="args">The export arguments containing filters and format.</param> /// <param name="args">The export arguments containing filters and format.</param>
/// <param name="now">The current time for resolving relative time specifications.</param> /// <param name="now">The current time for resolving relative time specifications.</param>
/// <returns>The full query string (including the leading <c>?</c>) for the export endpoint.</returns>
public static string BuildQueryString(AuditExportArgs args, DateTimeOffset now) public static string BuildQueryString(AuditExportArgs args, DateTimeOffset now)
{ {
var parts = new List<string>(); var parts = new List<string>();
@@ -116,6 +117,7 @@ public static class AuditExportHelpers
/// <param name="args">The export arguments containing filters and output file path.</param> /// <param name="args">The export arguments containing filters and output file path.</param>
/// <param name="output">Text writer for command output messages.</param> /// <param name="output">Text writer for command output messages.</param>
/// <param name="now">The current time for resolving relative time specifications.</param> /// <param name="now">The current time for resolving relative time specifications.</param>
/// <returns>0 on success, 1 on general error, or 2 on authorization failure.</returns>
public static async Task<int> RunExportAsync( public static async Task<int> RunExportAsync(
ManagementHttpClient client, AuditExportArgs args, TextWriter output, DateTimeOffset now) ManagementHttpClient client, AuditExportArgs args, TextWriter output, DateTimeOffset now)
{ {
@@ -178,6 +180,8 @@ public static class AuditExportHelpers
/// to extract the <c>code</c> field. Returns null if the body is empty, not valid JSON, or /// to extract the <c>code</c> field. Returns null if the body is empty, not valid JSON, or
/// has no <c>code</c> property — callers fall back to "ERROR" in that case. /// has no <c>code</c> property — callers fall back to "ERROR" in that case.
/// </summary> /// </summary>
/// <param name="body">The HTTP response body string to parse for an error code.</param>
/// <returns>The <c>code</c> string from the JSON error envelope, or null if absent or unparseable.</returns>
internal static string? TryExtractErrorCode(string body) internal static string? TryExtractErrorCode(string body)
{ {
if (string.IsNullOrWhiteSpace(body)) if (string.IsNullOrWhiteSpace(body))
@@ -43,6 +43,7 @@ public static class AuditFormatterFactory
/// </summary> /// </summary>
/// <param name="format">Format name; <c>table</c> selects the table formatter, any other value selects JSONL.</param> /// <param name="format">Format name; <c>table</c> selects the table formatter, any other value selects JSONL.</param>
/// <param name="notices">Writer for notice messages emitted during formatting.</param> /// <param name="notices">Writer for notice messages emitted during formatting.</param>
/// <returns>The <see cref="IAuditFormatter"/> appropriate for the requested format.</returns>
public static IAuditFormatter Create(string format, TextWriter notices) public static IAuditFormatter Create(string format, TextWriter notices)
{ {
if (string.Equals(format, "table", StringComparison.OrdinalIgnoreCase)) if (string.Equals(format, "table", StringComparison.OrdinalIgnoreCase))
@@ -50,6 +50,7 @@ public static class AuditLogCommands
/// <param name="formatOption">Global output format option.</param> /// <param name="formatOption">Global output format option.</param>
/// <param name="usernameOption">Global username option.</param> /// <param name="usernameOption">Global username option.</param>
/// <param name="passwordOption">Global password option.</param> /// <param name="passwordOption">Global password option.</param>
/// <returns>The configured <c>audit-config</c> command with all sub-commands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("audit-config") { Description = "Query the configuration-change audit log" }; var command = new Command("audit-config") { Description = "Query the configuration-change audit log" };
@@ -61,6 +61,7 @@ public static class AuditQueryHelpers
/// <param name="spec">The time specification string.</param> /// <param name="spec">The time specification string.</param>
/// <param name="now">The current time used as reference for relative specs.</param> /// <param name="now">The current time used as reference for relative specs.</param>
/// <exception cref="FormatException">The spec is neither a known relative form nor a parseable ISO-8601 timestamp.</exception> /// <exception cref="FormatException">The spec is neither a known relative form nor a parseable ISO-8601 timestamp.</exception>
/// <returns>The resolved absolute <see cref="DateTimeOffset"/> in UTC.</returns>
public static DateTimeOffset ResolveTimeSpec(string spec, DateTimeOffset now) public static DateTimeOffset ResolveTimeSpec(string spec, DateTimeOffset now)
{ {
if (string.IsNullOrWhiteSpace(spec)) if (string.IsNullOrWhiteSpace(spec))
@@ -103,6 +104,7 @@ public static class AuditQueryHelpers
/// <param name="now">The current time for resolving relative time specs.</param> /// <param name="now">The current time for resolving relative time specs.</param>
/// <param name="afterOccurredAtUtc">Optional keyset cursor timestamp.</param> /// <param name="afterOccurredAtUtc">Optional keyset cursor timestamp.</param>
/// <param name="afterEventId">Optional keyset cursor event ID.</param> /// <param name="afterEventId">Optional keyset cursor event ID.</param>
/// <returns>A URL query string (starting with <c>?</c>) containing the encoded filter parameters, or an empty string if no parameters are set.</returns>
public static string BuildQueryString( public static string BuildQueryString(
AuditQueryArgs args, DateTimeOffset now, DateTimeOffset? afterOccurredAtUtc, string? afterEventId) AuditQueryArgs args, DateTimeOffset now, DateTimeOffset? afterOccurredAtUtc, string? afterEventId)
{ {
@@ -169,6 +171,7 @@ public static class AuditQueryHelpers
/// <param name="formatter">The audit result formatter.</param> /// <param name="formatter">The audit result formatter.</param>
/// <param name="output">The output writer for results.</param> /// <param name="output">The output writer for results.</param>
/// <param name="now">The current time for resolving relative time specs.</param> /// <param name="now">The current time for resolving relative time specs.</param>
/// <returns>A task that resolves to <c>0</c> on success, <c>1</c> on HTTP/transport error, or <c>2</c> on authorization failure.</returns>
public static async Task<int> RunQueryAsync( public static async Task<int> RunQueryAsync(
ManagementHttpClient client, ManagementHttpClient client,
AuditQueryArgs args, AuditQueryArgs args,
@@ -14,6 +14,7 @@ public static class AuditVerifyChainHelpers
/// with a real month (01-12). A malformed month (e.g. <c>2026-13</c>) is rejected. /// with a real month (01-12). A malformed month (e.g. <c>2026-13</c>) is rejected.
/// </summary> /// </summary>
/// <param name="month">The month string to validate in YYYY-MM format.</param> /// <param name="month">The month string to validate in YYYY-MM format.</param>
/// <returns><c>true</c> if the string is a well-formed YYYY-MM value with a real month; otherwise <c>false</c>.</returns>
public static bool IsValidMonth(string? month) public static bool IsValidMonth(string? month)
=> !string.IsNullOrWhiteSpace(month) => !string.IsNullOrWhiteSpace(month)
&& DateTime.TryParseExact(month, "yyyy-MM", CultureInfo.InvariantCulture, && DateTime.TryParseExact(month, "yyyy-MM", CultureInfo.InvariantCulture,
@@ -307,6 +307,13 @@ public static class BundleCommands
// for the post-write summary line. // for the post-write summary line.
internal const int Base64StreamChunkChars = 1024 * 1024; // 1 MB of base64 chars ≈ 768 KB decoded internal const int Base64StreamChunkChars = 1024 * 1024; // 1 MB of base64 chars ≈ 768 KB decoded
/// <summary>
/// Decodes a base64 string into <paramref name="outputPath"/> in chunked fashion to avoid
/// large intermediate allocations. Returns the total number of decoded bytes written.
/// </summary>
/// <param name="base64">The base64-encoded content to decode and write.</param>
/// <param name="outputPath">Destination file path; created or overwritten.</param>
/// <returns>Total number of bytes written to the output file.</returns>
internal static long StreamBase64ToFile(string base64, string outputPath) internal static long StreamBase64ToFile(string base64, string outputPath)
{ {
if (base64 is null) throw new ArgumentNullException(nameof(base64)); if (base64 is null) throw new ArgumentNullException(nameof(base64));
@@ -17,6 +17,7 @@ internal static class CliOptions
/// typo (e.g. <c>--format tabel</c>) is rejected with a clear parse error rather /// typo (e.g. <c>--format tabel</c>) is rejected with a clear parse error rather
/// than silently falling through to JSON. /// than silently falling through to JSON.
/// </summary> /// </summary>
/// <returns>The configured <c>--format</c> option constrained to "json" or "table".</returns>
internal static Option<string> CreateFormatOption() internal static Option<string> CreateFormatOption()
{ {
var formatOption = new Option<string>("--format") var formatOption = new Option<string>("--format")
@@ -30,6 +30,7 @@ internal static class CommandHelpers
/// (<see cref="IsAuthorizationFailure"/>) is preserved on the error path either way, /// (<see cref="IsAuthorizationFailure"/>) is preserved on the error path either way,
/// closing CLI-017's regression. /// closing CLI-017's regression.
/// </param> /// </param>
/// <returns>A task that resolves to the process exit code (0 = success, 1 = error, 2 = authorization failure).</returns>
internal static async Task<int> ExecuteCommandAsync( internal static async Task<int> ExecuteCommandAsync(
ParseResult result, ParseResult result,
Option<string> urlOption, Option<string> urlOption,
@@ -110,6 +111,7 @@ internal static class CommandHelpers
/// <param name="result">Parsed command-line result.</param> /// <param name="result">Parsed command-line result.</param>
/// <param name="formatOption">The <c>--format</c> option definition.</param> /// <param name="formatOption">The <c>--format</c> option definition.</param>
/// <param name="config">Loaded CLI configuration providing the default format fallback.</param> /// <param name="config">Loaded CLI configuration providing the default format fallback.</param>
/// <returns>The resolved format string (e.g. <c>"json"</c> or <c>"table"</c>).</returns>
internal static string ResolveFormat(ParseResult result, Option<string> formatOption, CliConfig config) internal static string ResolveFormat(ParseResult result, Option<string> formatOption, CliConfig config)
{ {
// GetResult returns non-null only when the option was actually present on the // GetResult returns non-null only when the option was actually present on the
@@ -130,6 +132,7 @@ internal static class CommandHelpers
/// </summary> /// </summary>
/// <param name="commandLineValue">Value supplied on the command line, or null if absent.</param> /// <param name="commandLineValue">Value supplied on the command line, or null if absent.</param>
/// <param name="envValue">Fallback value from the config file or environment variable.</param> /// <param name="envValue">Fallback value from the config file or environment variable.</param>
/// <returns>The command-line value when non-empty; otherwise the environment fallback (may be null).</returns>
internal static string? ResolveCredential(string? commandLineValue, string? envValue) internal static string? ResolveCredential(string? commandLineValue, string? envValue)
=> string.IsNullOrWhiteSpace(commandLineValue) ? envValue : commandLineValue; => string.IsNullOrWhiteSpace(commandLineValue) ? envValue : commandLineValue;
@@ -140,6 +143,7 @@ internal static class CommandHelpers
/// an unhandled <see cref="UriFormatException"/>. /// an unhandled <see cref="UriFormatException"/>.
/// </summary> /// </summary>
/// <param name="url">URL string to validate.</param> /// <param name="url">URL string to validate.</param>
/// <returns><c>true</c> when the URL is an absolute http or https URL; otherwise <c>false</c>.</returns>
internal static bool IsValidManagementUrl(string? url) internal static bool IsValidManagementUrl(string? url)
{ {
if (string.IsNullOrWhiteSpace(url)) if (string.IsNullOrWhiteSpace(url))
@@ -154,6 +158,7 @@ internal static class CommandHelpers
/// </summary> /// </summary>
/// <param name="response">Response received from the management API.</param> /// <param name="response">Response received from the management API.</param>
/// <param name="format">Output format (<c>json</c> or <c>table</c>).</param> /// <param name="format">Output format (<c>json</c> or <c>table</c>).</param>
/// <returns>The process exit code (0 = success, 1 = error, 2 = authorization failure).</returns>
internal static int HandleResponse(ManagementResponse response, string format) internal static int HandleResponse(ManagementResponse response, string format)
{ {
if (response.JsonData != null) if (response.JsonData != null)
@@ -192,6 +197,8 @@ internal static class CommandHelpers
/// both channels are honoured. (Authentication failure — HTTP 401 / bad credentials /// both channels are honoured. (Authentication failure — HTTP 401 / bad credentials
/// — is deliberately <em>not</em> treated as authorization failure; it is exit 1.) /// — is deliberately <em>not</em> treated as authorization failure; it is exit 1.)
/// </summary> /// </summary>
/// <param name="response">The management response to inspect for authorization failure signals.</param>
/// <returns><c>true</c> when the response signals an authorization failure (HTTP 403 or FORBIDDEN/UNAUTHORIZED code).</returns>
internal static bool IsAuthorizationFailure(ManagementResponse response) internal static bool IsAuthorizationFailure(ManagementResponse response)
{ {
if (response.StatusCode == 403) if (response.StatusCode == 403)
@@ -13,6 +13,7 @@ public static class DataConnectionCommands
/// <param name="formatOption">Global output format option.</param> /// <param name="formatOption">Global output format option.</param>
/// <param name="usernameOption">Global username option.</param> /// <param name="usernameOption">Global username option.</param>
/// <param name="passwordOption">Global password option.</param> /// <param name="passwordOption">Global password option.</param>
/// <returns>The configured <c>data-connection</c> <see cref="Command"/> with all subcommands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("data-connection") { Description = "Manage data connections" }; var command = new Command("data-connection") { Description = "Manage data connections" };
@@ -15,6 +15,7 @@ public static class DebugCommands
/// <param name="formatOption">Shared output format option.</param> /// <param name="formatOption">Shared output format option.</param>
/// <param name="usernameOption">Shared username option for authentication.</param> /// <param name="usernameOption">Shared username option for authentication.</param>
/// <param name="passwordOption">Shared password option for authentication.</param> /// <param name="passwordOption">Shared password option for authentication.</param>
/// <returns>The configured <c>debug</c> command with snapshot and stream subcommands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("debug") { Description = "Runtime debugging" }; var command = new Command("debug") { Description = "Runtime debugging" };
@@ -27,6 +27,7 @@ internal static class DebugStreamHelpers
/// </summary> /// </summary>
/// <param name="ex">The exception thrown by HubConnection.StartAsync.</param> /// <param name="ex">The exception thrown by HubConnection.StartAsync.</param>
/// <param name="cancellationRequested">True when the user requested cancellation (Ctrl+C) before the exception was thrown.</param> /// <param name="cancellationRequested">True when the user requested cancellation (Ctrl+C) before the exception was thrown.</param>
/// <returns>A <see cref="ConnectFailure"/> describing whether the failure was a cancellation and the appropriate exit code.</returns>
internal static ConnectFailure ClassifyConnectFailure(Exception ex, bool cancellationRequested) internal static ConnectFailure ClassifyConnectFailure(Exception ex, bool cancellationRequested)
{ {
if (cancellationRequested && ex is OperationCanceledException) if (cancellationRequested && ex is OperationCanceledException)
@@ -43,6 +44,7 @@ internal static class DebugStreamHelpers
/// result is ever produced (pure Ctrl+C), the stream ended gracefully — exit 0. /// result is ever produced (pure Ctrl+C), the stream ended gracefully — exit 0.
/// </summary> /// </summary>
/// <param name="exitTask">The task whose result is the intended exit code, set by OnStreamTerminated or the Closed handler.</param> /// <param name="exitTask">The task whose result is the intended exit code, set by OnStreamTerminated or the Closed handler.</param>
/// <returns>A task that resolves to the process exit code (0 for graceful exit or pure Ctrl+C, non-zero for error).</returns>
internal static async Task<int> ResolveStreamExitCodeAsync(Task<int> exitTask) internal static async Task<int> ResolveStreamExitCodeAsync(Task<int> exitTask)
{ {
if (exitTask.IsCompletedSuccessfully) if (exitTask.IsCompletedSuccessfully)
@@ -13,6 +13,7 @@ public static class DeployCommands
/// <param name="formatOption">Global output format option.</param> /// <param name="formatOption">Global output format option.</param>
/// <param name="usernameOption">Global username option.</param> /// <param name="usernameOption">Global username option.</param>
/// <param name="passwordOption">Global password option.</param> /// <param name="passwordOption">Global password option.</param>
/// <returns>The configured <c>deploy</c> <see cref="Command"/> with all sub-commands attached.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("deploy") { Description = "Deployment operations" }; var command = new Command("deploy") { Description = "Deployment operations" };
@@ -13,6 +13,7 @@ public static class ExternalSystemCommands
/// <param name="formatOption">Global option for the output format.</param> /// <param name="formatOption">Global option for the output format.</param>
/// <param name="usernameOption">Global option for the authentication username.</param> /// <param name="usernameOption">Global option for the authentication username.</param>
/// <param name="passwordOption">Global option for the authentication password.</param> /// <param name="passwordOption">Global option for the authentication password.</param>
/// <returns>The fully configured <c>external-system</c> <see cref="Command"/> with all subcommands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("external-system") { Description = "Manage external systems" }; var command = new Command("external-system") { Description = "Manage external systems" };
@@ -13,6 +13,7 @@ public static class HealthCommands
/// <param name="formatOption">Global <c>--format</c> option for output format.</param> /// <param name="formatOption">Global <c>--format</c> option for output format.</param>
/// <param name="usernameOption">Global <c>--username</c> option for authentication.</param> /// <param name="usernameOption">Global <c>--username</c> option for authentication.</param>
/// <param name="passwordOption">Global <c>--password</c> option for authentication.</param> /// <param name="passwordOption">Global <c>--password</c> option for authentication.</param>
/// <returns>The configured <c>health</c> command with all sub-commands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("health") { Description = "Health monitoring" }; var command = new Command("health") { Description = "Health monitoring" };
@@ -13,6 +13,7 @@ public static class NotificationCommands
/// <param name="formatOption">Global <c>--format</c> option for output format.</param> /// <param name="formatOption">Global <c>--format</c> option for output format.</param>
/// <param name="usernameOption">Global <c>--username</c> option for authentication.</param> /// <param name="usernameOption">Global <c>--username</c> option for authentication.</param>
/// <param name="passwordOption">Global <c>--password</c> option for authentication.</param> /// <param name="passwordOption">Global <c>--password</c> option for authentication.</param>
/// <returns>The configured <c>notification</c> command with all sub-commands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("notification") { Description = "Manage notification lists" }; var command = new Command("notification") { Description = "Manage notification lists" };
@@ -131,6 +132,7 @@ public static class NotificationCommands
/// null when omitted so the server-side handler preserves the existing values. /// null when omitted so the server-side handler preserves the existing values.
/// </summary> /// </summary>
/// <param name="result">The parsed command-line result from the <c>smtp update</c> invocation.</param> /// <param name="result">The parsed command-line result from the <c>smtp update</c> invocation.</param>
/// <returns>An <see cref="UpdateSmtpConfigCommand"/> populated from the parsed result.</returns>
internal static UpdateSmtpConfigCommand BuildUpdateSmtpConfigCommand(ParseResult result) internal static UpdateSmtpConfigCommand BuildUpdateSmtpConfigCommand(ParseResult result)
{ {
var id = result.GetValue(SmtpIdOption); var id = result.GetValue(SmtpIdOption);
@@ -13,6 +13,7 @@ public static class SecurityCommands
/// <param name="formatOption">Shared output format option.</param> /// <param name="formatOption">Shared output format option.</param>
/// <param name="usernameOption">Shared username option for authentication.</param> /// <param name="usernameOption">Shared username option for authentication.</param>
/// <param name="passwordOption">Shared password option for authentication.</param> /// <param name="passwordOption">Shared password option for authentication.</param>
/// <returns>The configured <c>security</c> command with all subcommands attached.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("security") { Description = "Manage security settings" }; var command = new Command("security") { Description = "Manage security settings" };
@@ -125,6 +126,7 @@ public static class SecurityCommands
/// The advisory line is written to stderr so that piping stdout captures only the token. /// The advisory line is written to stderr so that piping stdout captures only the token.
/// </summary> /// </summary>
/// <param name="json">The JSON success body returned by the management API.</param> /// <param name="json">The JSON success body returned by the management API.</param>
/// <returns>Exit code 0.</returns>
internal static int PrintCreatedKey(string json) internal static int PrintCreatedKey(string json)
{ {
using var doc = System.Text.Json.JsonDocument.Parse(json); using var doc = System.Text.Json.JsonDocument.Parse(json);
@@ -13,6 +13,7 @@ public static class SiteCommands
/// <param name="formatOption">Global output format option.</param> /// <param name="formatOption">Global output format option.</param>
/// <param name="usernameOption">Global username option.</param> /// <param name="usernameOption">Global username option.</param>
/// <param name="passwordOption">Global password option.</param> /// <param name="passwordOption">Global password option.</param>
/// <returns>The configured <c>site</c> command with all subcommands attached.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("site") { Description = "Manage sites" }; var command = new Command("site") { Description = "Manage sites" };
@@ -11,6 +11,7 @@ public static class TemplateCommands
/// <param name="formatOption">Shared output format option.</param> /// <param name="formatOption">Shared output format option.</param>
/// <param name="usernameOption">Shared username option for authentication.</param> /// <param name="usernameOption">Shared username option for authentication.</param>
/// <param name="passwordOption">Shared password option for authentication.</param> /// <param name="passwordOption">Shared password option for authentication.</param>
/// <returns>The fully configured <c>template</c> command with all its subcommands.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption) public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{ {
var command = new Command("template") { Description = "Manage templates" }; var command = new Command("template") { Description = "Manage templates" };
@@ -61,6 +61,7 @@ public static class AuditExportEndpoints
/// </summary> /// </summary>
/// <param name="context">The HTTP context for the current request.</param> /// <param name="context">The HTTP context for the current request.</param>
/// <param name="exportService">The export service used to stream audit rows as CSV.</param> /// <param name="exportService">The export service used to stream audit rows as CSV.</param>
/// <returns>A task representing the asynchronous export streaming operation.</returns>
internal static async Task HandleExportAsync(HttpContext context, IAuditLogExportService exportService) internal static async Task HandleExportAsync(HttpContext context, IAuditLogExportService exportService)
{ {
var filter = ParseFilter(context.Request.Query); var filter = ParseFilter(context.Request.Query);
@@ -94,6 +95,7 @@ public static class AuditExportEndpoints
/// its own CLI / UI URL builder — so do NOT "fix" the two to one key name. /// its own CLI / UI URL builder — so do NOT "fix" the two to one key name.
/// </remarks> /// </remarks>
/// <param name="query">The query string parameters from the HTTP request.</param> /// <param name="query">The query string parameters from the HTTP request.</param>
/// <returns>An <see cref="AuditLogQueryFilter"/> populated from the query string values.</returns>
internal static AuditLogQueryFilter ParseFilter(IQueryCollection query) internal static AuditLogQueryFilter ParseFilter(IQueryCollection query)
{ {
var channels = AuditQueryParamParsers.ParseEnumList<AuditChannel>(query["channel"]); var channels = AuditQueryParamParsers.ParseEnumList<AuditChannel>(query["channel"]);
@@ -19,6 +19,7 @@ public static class AuthEndpoints
{ {
/// <summary>Registers the <c>/auth/login</c>, <c>/auth/logout</c>, and <c>/auth/ping</c> endpoints on the given route builder.</summary> /// <summary>Registers the <c>/auth/login</c>, <c>/auth/logout</c>, and <c>/auth/ping</c> endpoints on the given route builder.</summary>
/// <param name="endpoints">The route builder to add the endpoints to.</param> /// <param name="endpoints">The route builder to add the endpoints to.</param>
/// <returns>The same <paramref name="endpoints"/> instance, for call chaining.</returns>
public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder endpoints) public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder endpoints)
{ {
endpoints.MapPost("/auth/login", async (HttpContext context) => endpoints.MapPost("/auth/login", async (HttpContext context) =>
@@ -198,6 +199,7 @@ public static class AuthEndpoints
/// server-side. See CentralUI-020. /// server-side. See CentralUI-020.
/// </summary> /// </summary>
/// <param name="context">The current HTTP context used to check authentication state and write the response.</param> /// <param name="context">The current HTTP context used to check authentication state and write the response.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public static Task HandlePing(HttpContext context) public static Task HandlePing(HttpContext context)
{ {
context.Response.StatusCode = context.User.Identity?.IsAuthenticated == true context.Response.StatusCode = context.User.Identity?.IsAuthenticated == true
@@ -219,6 +221,7 @@ public static class AuthEndpoints
/// <see cref="AuthenticationProperties.AllowRefresh"/> is left unset (null) /// <see cref="AuthenticationProperties.AllowRefresh"/> is left unset (null)
/// so the middleware is free to slide the expiry on activity. /// so the middleware is free to slide the expiry on activity.
/// </summary> /// </summary>
/// <returns>An <see cref="AuthenticationProperties"/> instance with <see cref="AuthenticationProperties.IsPersistent"/> set to <c>true</c> and no fixed expiry.</returns>
public static AuthenticationProperties BuildSignInProperties() => new() public static AuthenticationProperties BuildSignInProperties() => new()
{ {
IsPersistent = true IsPersistent = true
@@ -20,6 +20,7 @@ public static class ClaimsPrincipalExtensions
/// <see cref="UnknownUser"/> when the claim is absent. /// <see cref="UnknownUser"/> when the claim is absent.
/// </summary> /// </summary>
/// <param name="principal">The claims principal to read the username from.</param> /// <param name="principal">The claims principal to read the username from.</param>
/// <returns>The username claim value, or <see cref="UnknownUser"/> if absent.</returns>
public static string GetUsername(this ClaimsPrincipal principal) public static string GetUsername(this ClaimsPrincipal principal)
=> principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? UnknownUser; => principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? UnknownUser;
@@ -28,6 +29,7 @@ public static class ClaimsPrincipalExtensions
/// the claim is absent. /// the claim is absent.
/// </summary> /// </summary>
/// <param name="principal">The claims principal to read the display name from.</param> /// <param name="principal">The claims principal to read the display name from.</param>
/// <returns>The display name claim value, or <c>null</c> if the claim is absent.</returns>
public static string? GetDisplayName(this ClaimsPrincipal principal) public static string? GetDisplayName(this ClaimsPrincipal principal)
=> principal.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value; => principal.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value;
@@ -37,6 +39,7 @@ public static class ClaimsPrincipalExtensions
/// ten components (CentralUI-024). /// ten components (CentralUI-024).
/// </summary> /// </summary>
/// <param name="authStateProvider">The Blazor authentication state provider to read from.</param> /// <param name="authStateProvider">The Blazor authentication state provider to read from.</param>
/// <returns>A task that resolves to the current user's audit username, or <see cref="UnknownUser"/> if not authenticated.</returns>
public static async Task<string> GetCurrentUsernameAsync( public static async Task<string> GetCurrentUsernameAsync(
this AuthenticationStateProvider authStateProvider) this AuthenticationStateProvider authStateProvider)
{ {
@@ -38,6 +38,7 @@ public sealed class SiteScopeService
/// True when the user is not restricted to a site subset (no <c>SiteId</c> /// True when the user is not restricted to a site subset (no <c>SiteId</c>
/// claims). System-wide users see and act on every site. /// claims). System-wide users see and act on every site.
/// </summary> /// </summary>
/// <returns>A task that resolves to <c>true</c> if the user has no site-scope restriction.</returns>
public async Task<bool> IsSystemWideAsync() public async Task<bool> IsSystemWideAsync()
=> (await ResolveAsync()).IsSystemWide; => (await ResolveAsync()).IsSystemWide;
@@ -46,6 +47,7 @@ public sealed class SiteScopeService
/// system-wide user (callers should consult <see cref="IsSystemWideAsync"/> /// system-wide user (callers should consult <see cref="IsSystemWideAsync"/>
/// or use the filter/allowed helpers, which already account for that). /// or use the filter/allowed helpers, which already account for that).
/// </summary> /// </summary>
/// <returns>A task that resolves to the set of permitted site IDs (empty for system-wide users).</returns>
public async Task<IReadOnlySet<int>> PermittedSiteIdsAsync() public async Task<IReadOnlySet<int>> PermittedSiteIdsAsync()
=> (await ResolveAsync()).Sites; => (await ResolveAsync()).Sites;
@@ -54,6 +56,7 @@ public sealed class SiteScopeService
/// see. A system-wide user gets the full list back unchanged. /// see. A system-wide user gets the full list back unchanged.
/// </summary> /// </summary>
/// <param name="sites">The full set of sites to filter.</param> /// <param name="sites">The full set of sites to filter.</param>
/// <returns>A task that resolves to the filtered list of sites the user is permitted to see.</returns>
public async Task<List<Site>> FilterSitesAsync(IEnumerable<Site> sites) public async Task<List<Site>> FilterSitesAsync(IEnumerable<Site> sites)
{ {
var (isSystemWide, allowed) = await ResolveAsync(); var (isSystemWide, allowed) = await ResolveAsync();
@@ -67,6 +70,7 @@ public sealed class SiteScopeService
/// Must be re-checked server-side before any mutating cross-site command. /// Must be re-checked server-side before any mutating cross-site command.
/// </summary> /// </summary>
/// <param name="siteId">The <c>Site.Id</c> to check.</param> /// <param name="siteId">The <c>Site.Id</c> to check.</param>
/// <returns>A task that resolves to <c>true</c> when the user may operate on the given site.</returns>
public async Task<bool> IsSiteAllowedAsync(int siteId) public async Task<bool> IsSiteAllowedAsync(int siteId)
{ {
var (isSystemWide, allowed) = await ResolveAsync(); var (isSystemWide, allowed) = await ResolveAsync();
@@ -114,6 +114,7 @@ public sealed class AuditQueryModel
/// With one or more Channels selected, the union of the channel-specific kind /// With one or more Channels selected, the union of the channel-specific kind
/// lists is returned (deduplicated and order-stable on first-seen). /// lists is returned (deduplicated and order-stable on first-seen).
/// </summary> /// </summary>
/// <returns>The deduplicated, order-stable list of <see cref="AuditKind"/> values applicable to the selected channels.</returns>
public IReadOnlyList<AuditKind> VisibleKinds() public IReadOnlyList<AuditKind> VisibleKinds()
{ {
if (Channels.Count == 0) if (Channels.Count == 0)
@@ -411,6 +411,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// </summary> /// </summary>
/// <param name="columnKey">The stable key of the resized column.</param> /// <param name="columnKey">The stable key of the resized column.</param>
/// <param name="widthPx">The new column width in pixels.</param> /// <param name="widthPx">The new column width in pixels.</param>
/// <returns>A task that completes when the column width has been persisted and the component re-rendered.</returns>
[JSInvokable] [JSInvokable]
public async Task OnColumnResized(string columnKey, int widthPx) public async Task OnColumnResized(string columnKey, int widthPx)
{ {
@@ -431,6 +432,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// </summary> /// </summary>
/// <param name="fromKey">The stable key of the column being dragged.</param> /// <param name="fromKey">The stable key of the column being dragged.</param>
/// <param name="toKey">The stable key of the target column drop slot.</param> /// <param name="toKey">The stable key of the target column drop slot.</param>
/// <returns>A task that completes when the column order has been persisted and the component re-rendered.</returns>
[JSInvokable] [JSInvokable]
public async Task OnColumnReordered(string fromKey, string toKey) public async Task OnColumnReordered(string fromKey, string toKey)
{ {
@@ -472,6 +474,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// <summary> /// <summary>
/// Releases the .NET object reference held for JS interop callbacks. /// Releases the .NET object reference held for JS interop callbacks.
/// </summary> /// </summary>
/// <returns>A completed value task.</returns>
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
_selfRef?.Dispose(); _selfRef?.Dispose();
@@ -254,6 +254,7 @@ public partial class AuditLogPage : IDisposable
/// Builds the CSV export URL for the given filter, encoding all active filter dimensions as query parameters. /// Builds the CSV export URL for the given filter, encoding all active filter dimensions as query parameters.
/// </summary> /// </summary>
/// <param name="filter">Currently applied filter; null returns the bare export endpoint.</param> /// <param name="filter">Currently applied filter; null returns the bare export endpoint.</param>
/// <returns>The relative URL with encoded filter dimensions as query parameters.</returns>
internal static string BuildExportUrl(AuditLogQueryFilter? filter) internal static string BuildExportUrl(AuditLogQueryFilter? filter)
{ {
const string basePath = "/api/centralui/audit/export"; const string basePath = "/api/centralui/audit/export";
@@ -182,6 +182,7 @@ public partial class TransportExport : ComponentBase
/// importer enforces its own strength + lockout policies. /// importer enforces its own strength + lockout policies.
/// </summary> /// </summary>
/// <param name="s">The passphrase string to score.</param> /// <param name="s">The passphrase string to score.</param>
/// <returns>An integer from 0 (blank) to 4 (long, mixed case, digits, and symbols).</returns>
internal static int PassphraseStrength(string s) internal static int PassphraseStrength(string s)
{ {
if (string.IsNullOrEmpty(s)) return 0; if (string.IsNullOrEmpty(s)) return 0;
@@ -261,6 +262,7 @@ public partial class TransportExport : ComponentBase
/// knows exactly what an unencrypted export would leak. /// knows exactly what an unencrypted export would leak.
/// </summary> /// </summary>
/// <param name="resolved">The resolved export closure whose secret fields are counted.</param> /// <param name="resolved">The resolved export closure whose secret fields are counted.</param>
/// <returns>The total number of non-empty secret fields across all external systems, SMTP configs, and database connections.</returns>
internal static int CountSecrets(ResolvedExport resolved) internal static int CountSecrets(ResolvedExport resolved)
{ {
var count = 0; var count = 0;
@@ -367,6 +369,7 @@ public partial class TransportExport : ComponentBase
/// </summary> /// </summary>
/// <param name="sourceEnvironment">The environment label to embed in the filename (sanitised to filename-safe characters).</param> /// <param name="sourceEnvironment">The environment label to embed in the filename (sanitised to filename-safe characters).</param>
/// <param name="nowUtc">Timestamp to use for the datetime segment; defaults to <see cref="DateTimeOffset.UtcNow"/> when null.</param> /// <param name="nowUtc">Timestamp to use for the datetime segment; defaults to <see cref="DateTimeOffset.UtcNow"/> when null.</param>
/// <returns>A filename of the form <c>scadabundle-{env}-{yyyy-MM-dd-HHmmss}.scadabundle</c>.</returns>
internal static string BuildFilename(string sourceEnvironment, DateTimeOffset? nowUtc = null) internal static string BuildFilename(string sourceEnvironment, DateTimeOffset? nowUtc = null)
{ {
var safe = SanitizeForFilename(sourceEnvironment); var safe = SanitizeForFilename(sourceEnvironment);
@@ -427,6 +430,7 @@ public partial class TransportExport : ComponentBase
/// <param name="all">The full resolved list including both seed and auto-included items.</param> /// <param name="all">The full resolved list including both seed and auto-included items.</param>
/// <param name="seed">The set of explicitly selected item ids.</param> /// <param name="seed">The set of explicitly selected item ids.</param>
/// <param name="idOf">Function that extracts the integer id from an item.</param> /// <param name="idOf">Function that extracts the integer id from an item.</param>
/// <returns>Items from <paramref name="all"/> whose ids are not in <paramref name="seed"/> (auto-included dependencies).</returns>
internal static IReadOnlyList<T> AutoIncluded<T>(IReadOnlyList<T> all, IReadOnlyCollection<int> seed, Func<T, int> idOf) internal static IReadOnlyList<T> AutoIncluded<T>(IReadOnlyList<T> all, IReadOnlyCollection<int> seed, Func<T, int> idOf)
{ {
return all.Where(x => !seed.Contains(idOf(x))).ToList(); return all.Where(x => !seed.Contains(idOf(x))).ToList();
@@ -18,6 +18,7 @@ internal static class DurationInput
/// <c>sec</c> unit. /// <c>sec</c> unit.
/// </summary> /// </summary>
/// <param name="duration">The duration to split, or null for unset.</param> /// <param name="duration">The duration to split, or null for unset.</param>
/// <returns>A tuple of the numeric string and unit token (ms/sec/min), or <c>(null, "sec")</c> for null or non-positive input.</returns>
internal static (string? Value, string Unit) Split(TimeSpan? duration) internal static (string? Value, string Unit) Split(TimeSpan? duration)
{ {
if (duration is not { } d || d <= TimeSpan.Zero) return (null, "sec"); if (duration is not { } d || d <= TimeSpan.Zero) return (null, "sec");
@@ -34,6 +35,7 @@ internal static class DurationInput
/// </summary> /// </summary>
/// <param name="value">The numeric string entered by the user.</param> /// <param name="value">The numeric string entered by the user.</param>
/// <param name="unit">The selected unit token (ms, sec, or min).</param> /// <param name="unit">The selected unit token (ms, sec, or min).</param>
/// <returns>The composed <see cref="TimeSpan"/>, or <c>null</c> for blank, unparseable, or non-positive input.</returns>
internal static TimeSpan? Compose(string? value, string unit) internal static TimeSpan? Compose(string? value, string unit)
{ {
if (!long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) if (!long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n)
@@ -18,6 +18,7 @@ public interface IDialogService
/// <param name="danger">When <c>true</c>, the confirm button renders in /// <param name="danger">When <c>true</c>, the confirm button renders in
/// <c>btn-danger</c> styling with the label "Delete"; otherwise a primary /// <c>btn-danger</c> styling with the label "Delete"; otherwise a primary
/// "Confirm" button is shown.</param> /// "Confirm" button is shown.</param>
/// <returns>A task that resolves to <c>true</c> when the user confirms, or <c>false</c> when cancelled.</returns>
Task<bool> ConfirmAsync(string title, string message, bool danger = false); Task<bool> ConfirmAsync(string title, string message, bool danger = false);
/// <summary> /// <summary>
@@ -28,5 +29,6 @@ public interface IDialogService
/// <param name="label">Label rendered above the input field.</param> /// <param name="label">Label rendered above the input field.</param>
/// <param name="initialValue">Pre-populated value for the input field.</param> /// <param name="initialValue">Pre-populated value for the input field.</param>
/// <param name="placeholder">Optional placeholder shown when the input is empty.</param> /// <param name="placeholder">Optional placeholder shown when the input is empty.</param>
/// <returns>A task that resolves to the entered string, or <c>null</c> if the user cancels.</returns>
Task<string?> PromptAsync(string title, string label, string initialValue = "", string? placeholder = null); Task<string?> PromptAsync(string title, string label, string initialValue = "", string? placeholder = null);
} }
@@ -46,6 +46,7 @@ internal static class SchemaBuilderModel
/// </summary> /// </summary>
/// <param name="json">JSON Schema string to parse, or null/empty to return the fallback.</param> /// <param name="json">JSON Schema string to parse, or null/empty to return the fallback.</param>
/// <param name="fallback">The <see cref="SchemaNode"/> to return when the input cannot be parsed.</param> /// <param name="fallback">The <see cref="SchemaNode"/> to return when the input cannot be parsed.</param>
/// <returns>The parsed <see cref="SchemaNode"/> tree, or <paramref name="fallback"/> if the input is empty or malformed.</returns>
public static SchemaNode Parse(string? json, SchemaNode fallback) public static SchemaNode Parse(string? json, SchemaNode fallback)
{ {
if (string.IsNullOrWhiteSpace(json)) return fallback; if (string.IsNullOrWhiteSpace(json)) return fallback;
@@ -66,15 +67,18 @@ internal static class SchemaBuilderModel
} }
/// <summary>Default empty object schema (parameters mode default).</summary> /// <summary>Default empty object schema (parameters mode default).</summary>
/// <returns>A new <see cref="SchemaNode"/> with type <c>object</c>.</returns>
public static SchemaNode NewObject() => new() { Type = "object" }; public static SchemaNode NewObject() => new() { Type = "object" };
/// <summary>Default scalar schema (return mode default).</summary> /// <summary>Default scalar schema (return mode default).</summary>
/// <returns>A new <see cref="SchemaNode"/> with type <c>string</c>.</returns>
public static SchemaNode NewValue() => new() { Type = "string" }; public static SchemaNode NewValue() => new() { Type = "string" };
/// <summary> /// <summary>
/// Serializes a <see cref="SchemaNode"/> tree to its canonical JSON Schema string. /// Serializes a <see cref="SchemaNode"/> tree to its canonical JSON Schema string.
/// </summary> /// </summary>
/// <param name="node">The schema node to serialize.</param> /// <param name="node">The schema node to serialize.</param>
/// <returns>The canonical JSON Schema string representing the node tree.</returns>
public static string Serialize(SchemaNode node) public static string Serialize(SchemaNode node)
{ {
using var stream = new System.IO.MemoryStream(); using var stream = new System.IO.MemoryStream();
@@ -13,6 +13,7 @@ public static class ScriptParameterNames
/// Parses a parameter definitions JSON Schema and returns the declared parameter names. /// Parses a parameter definitions JSON Schema and returns the declared parameter names.
/// </summary> /// </summary>
/// <param name="json">JSON Schema or legacy flat-array string; null/empty returns an empty list.</param> /// <param name="json">JSON Schema or legacy flat-array string; null/empty returns an empty list.</param>
/// <returns>A read-only list of declared parameter names.</returns>
public static IReadOnlyList<string> Parse(string? json) => public static IReadOnlyList<string> Parse(string? json) =>
JsonSchemaShapeParser.ParseParameters(json) JsonSchemaShapeParser.ParseParameters(json)
.Select(p => p.Name) .Select(p => p.Name)
@@ -23,6 +24,7 @@ public static class ScriptParameterNames
/// Parses a parameter definitions JSON Schema and returns the full parameter shape objects. /// Parses a parameter definitions JSON Schema and returns the full parameter shape objects.
/// </summary> /// </summary>
/// <param name="json">JSON Schema or legacy flat-array string; null/empty returns an empty list.</param> /// <param name="json">JSON Schema or legacy flat-array string; null/empty returns an empty list.</param>
/// <returns>A read-only list of parameter shape objects with name and type information.</returns>
public static IReadOnlyList<ParameterShape> ParseShapes(string? json) => public static IReadOnlyList<ParameterShape> ParseShapes(string? json) =>
JsonSchemaShapeParser.ParseParameters(json); JsonSchemaShapeParser.ParseParameters(json);
} }
@@ -68,6 +68,7 @@ internal static class ScriptTriggerConfigCodec
/// <summary>Classifies a raw <c>TriggerType</c> string (case-insensitive).</summary> /// <summary>Classifies a raw <c>TriggerType</c> string (case-insensitive).</summary>
/// <param name="triggerType">The raw trigger type string from the template script entity.</param> /// <param name="triggerType">The raw trigger type string from the template script entity.</param>
/// <returns>The matching <see cref="ScriptTriggerKind"/>, or <see cref="ScriptTriggerKind.None"/> for null/empty.</returns>
internal static ScriptTriggerKind ParseKind(string? triggerType) internal static ScriptTriggerKind ParseKind(string? triggerType)
{ {
if (string.IsNullOrWhiteSpace(triggerType)) return ScriptTriggerKind.None; if (string.IsNullOrWhiteSpace(triggerType)) return ScriptTriggerKind.None;
@@ -89,6 +90,7 @@ internal static class ScriptTriggerConfigCodec
/// (invoked explicitly, never throttled), and None/Unknown. /// (invoked explicitly, never throttled), and None/Unknown.
/// </summary> /// </summary>
/// <param name="triggerType">The raw trigger type string to classify.</param> /// <param name="triggerType">The raw trigger type string to classify.</param>
/// <returns><see langword="true"/> if the trigger honours <c>MinTimeBetweenRuns</c>; otherwise <see langword="false"/>.</returns>
internal static bool SupportsMinTimeBetweenRuns(string? triggerType) => internal static bool SupportsMinTimeBetweenRuns(string? triggerType) =>
ParseKind(triggerType) is ScriptTriggerKind.ValueChange ParseKind(triggerType) is ScriptTriggerKind.ValueChange
or ScriptTriggerKind.Conditional or ScriptTriggerKind.Conditional
@@ -96,6 +98,7 @@ internal static class ScriptTriggerConfigCodec
/// <summary>Canonical <c>TriggerType</c> string for a kind; null for None/Unknown.</summary> /// <summary>Canonical <c>TriggerType</c> string for a kind; null for None/Unknown.</summary>
/// <param name="kind">The trigger kind to convert.</param> /// <param name="kind">The trigger kind to convert.</param>
/// <returns>The canonical trigger-type string, or null for <see cref="ScriptTriggerKind.None"/>/<see cref="ScriptTriggerKind.Unknown"/>.</returns>
internal static string? KindToString(ScriptTriggerKind kind) => kind switch internal static string? KindToString(ScriptTriggerKind kind) => kind switch
{ {
ScriptTriggerKind.Interval => "Interval", ScriptTriggerKind.Interval => "Interval",
@@ -113,6 +116,7 @@ internal static class ScriptTriggerConfigCodec
/// </summary> /// </summary>
/// <param name="json">The raw JSON trigger configuration string.</param> /// <param name="json">The raw JSON trigger configuration string.</param>
/// <param name="kind">The trigger kind, used to determine which fields to parse.</param> /// <param name="kind">The trigger kind, used to determine which fields to parse.</param>
/// <returns>A <see cref="ScriptTriggerModel"/> populated from the JSON; defaults are used for absent or malformed fields.</returns>
internal static ScriptTriggerModel Parse(string? json, ScriptTriggerKind kind) internal static ScriptTriggerModel Parse(string? json, ScriptTriggerKind kind)
{ {
var model = new ScriptTriggerModel(); var model = new ScriptTriggerModel();
@@ -161,6 +165,7 @@ internal static class ScriptTriggerConfigCodec
/// </summary> /// </summary>
/// <param name="model">The trigger model to serialize.</param> /// <param name="model">The trigger model to serialize.</param>
/// <param name="kind">The trigger kind, used to determine which fields to emit.</param> /// <param name="kind">The trigger kind, used to determine which fields to emit.</param>
/// <returns>The JSON configuration string, or null for <see cref="ScriptTriggerKind.None"/>/<see cref="ScriptTriggerKind.Unknown"/>.</returns>
internal static string? Serialize(ScriptTriggerModel model, ScriptTriggerKind kind) internal static string? Serialize(ScriptTriggerModel model, ScriptTriggerKind kind)
{ {
if (kind is ScriptTriggerKind.None or ScriptTriggerKind.Unknown) return null; if (kind is ScriptTriggerKind.None or ScriptTriggerKind.Unknown) return null;
@@ -214,6 +219,7 @@ internal static class ScriptTriggerConfigCodec
/// <summary>Returns <paramref name="raw"/> if it is a recognized operator, else "&gt;".</summary> /// <summary>Returns <paramref name="raw"/> if it is a recognized operator, else "&gt;".</summary>
/// <param name="raw">The raw operator string to normalize.</param> /// <param name="raw">The raw operator string to normalize.</param>
/// <returns>A valid operator string from <see cref="Operators"/>; defaults to "&gt;" for unrecognized input.</returns>
internal static string NormalizeOperator(string? raw) internal static string NormalizeOperator(string? raw)
{ {
var op = raw?.Trim(); var op = raw?.Trim();
@@ -19,6 +19,7 @@ public static class TriggerAttributeMapper
{ {
/// <summary>Direct and inherited attributes, exposed as <c>Attributes["..."]</c>.</summary> /// <summary>Direct and inherited attributes, exposed as <c>Attributes["..."]</c>.</summary>
/// <param name="choices">The full flattened attribute choice list from the trigger editor.</param> /// <param name="choices">The full flattened attribute choice list from the trigger editor.</param>
/// <returns>The list of <see cref="AttributeShape"/>s for Direct and Inherited attributes.</returns>
public static IReadOnlyList<AttributeShape> SelfAttributes( public static IReadOnlyList<AttributeShape> SelfAttributes(
IReadOnlyList<AlarmAttributeChoice> choices) => IReadOnlyList<AlarmAttributeChoice> choices) =>
choices choices
@@ -32,6 +33,7 @@ public static class TriggerAttributeMapper
/// are skipped (no child scope to attach them to). /// are skipped (no child scope to attach them to).
/// </summary> /// </summary>
/// <param name="choices">The full flattened attribute choice list from the trigger editor.</param> /// <param name="choices">The full flattened attribute choice list from the trigger editor.</param>
/// <returns>The list of <see cref="CompositionContext"/>s, one per distinct composition-instance name.</returns>
public static IReadOnlyList<CompositionContext> Children( public static IReadOnlyList<CompositionContext> Children(
IReadOnlyList<AlarmAttributeChoice> choices) => IReadOnlyList<AlarmAttributeChoice> choices) =>
choices choices
@@ -15,6 +15,7 @@ public static class EndpointExtensions
/// </summary> /// </summary>
/// <typeparam name="TApp">The root Blazor App component type, supplied by the Host assembly.</typeparam> /// <typeparam name="TApp">The root Blazor App component type, supplied by the Host assembly.</typeparam>
/// <param name="endpoints">The endpoint route builder to register routes on.</param> /// <param name="endpoints">The endpoint route builder to register routes on.</param>
/// <returns>The same <paramref name="endpoints"/> instance, for call chaining.</returns>
public static IEndpointRouteBuilder MapCentralUI<TApp>(this IEndpointRouteBuilder endpoints) public static IEndpointRouteBuilder MapCentralUI<TApp>(this IEndpointRouteBuilder endpoints)
where TApp : Microsoft.AspNetCore.Components.IComponent where TApp : Microsoft.AspNetCore.Components.IComponent
{ {
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
public interface ISharedScriptCatalog public interface ISharedScriptCatalog
{ {
/// <summary>Returns the parameter and return shapes for all registered shared scripts.</summary> /// <summary>Returns the parameter and return shapes for all registered shared scripts.</summary>
/// <returns>A task that resolves to the list of all shared script shapes.</returns>
Task<IReadOnlyList<ScriptShape>> GetShapesAsync(); Task<IReadOnlyList<ScriptShape>> GetShapesAsync();
/// <summary> /// <summary>
@@ -18,6 +19,7 @@ public interface ISharedScriptCatalog
/// </summary> /// </summary>
/// <param name="name">Name of the shared script to retrieve.</param> /// <param name="name">Name of the shared script to retrieve.</param>
/// <param name="cancellationToken">Cancellation token for the async lookup.</param> /// <param name="cancellationToken">Cancellation token for the async lookup.</param>
/// <returns>A task that resolves to the matching shared script source, or null if none exists.</returns>
Task<SharedScriptSource?> GetByNameAsync(string name, CancellationToken cancellationToken = default); Task<SharedScriptSource?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
} }
@@ -32,6 +32,7 @@ public class InboundScriptHost
/// Targets a specific instance for method invocation. /// Targets a specific instance for method invocation.
/// </summary> /// </summary>
/// <param name="instanceCode">The instance code to target.</param> /// <param name="instanceCode">The instance code to target.</param>
/// <returns>A <see cref="RouteTarget"/> scoped to the specified instance.</returns>
public RouteTarget To(string instanceCode) => new(); public RouteTarget To(string instanceCode) => new();
} }
@@ -44,6 +45,7 @@ public class InboundScriptHost
/// <param name="scriptName">The name of the script to call.</param> /// <param name="scriptName">The name of the script to call.</param>
/// <param name="parameters">Optional parameters to pass to the script.</param> /// <param name="parameters">Optional parameters to pass to the script.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the script's return value, or null.</returns>
public System.Threading.Tasks.Task<object?> Call( public System.Threading.Tasks.Task<object?> Call(
string scriptName, string scriptName,
object? parameters = null, object? parameters = null,
@@ -55,6 +57,7 @@ public class InboundScriptHost
/// </summary> /// </summary>
/// <param name="attributeName">The name of the attribute to retrieve.</param> /// <param name="attributeName">The name of the attribute to retrieve.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the attribute value, or null if not found.</returns>
public System.Threading.Tasks.Task<object?> GetAttribute( public System.Threading.Tasks.Task<object?> GetAttribute(
string attributeName, string attributeName,
System.Threading.CancellationToken cancellationToken = default) => System.Threading.CancellationToken cancellationToken = default) =>
@@ -65,6 +68,7 @@ public class InboundScriptHost
/// </summary> /// </summary>
/// <param name="attributeNames">The names of the attributes to retrieve.</param> /// <param name="attributeNames">The names of the attributes to retrieve.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a dictionary mapping attribute names to their values.</returns>
public System.Threading.Tasks.Task<IReadOnlyDictionary<string, object?>> GetAttributes( public System.Threading.Tasks.Task<IReadOnlyDictionary<string, object?>> GetAttributes(
IEnumerable<string> attributeNames, IEnumerable<string> attributeNames,
System.Threading.CancellationToken cancellationToken = default) => System.Threading.CancellationToken cancellationToken = default) =>
@@ -77,6 +81,7 @@ public class InboundScriptHost
/// <param name="attributeName">The name of the attribute to set.</param> /// <param name="attributeName">The name of the attribute to set.</param>
/// <param name="value">The value to set.</param> /// <param name="value">The value to set.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public System.Threading.Tasks.Task SetAttribute( public System.Threading.Tasks.Task SetAttribute(
string attributeName, string attributeName,
string value, string value,
@@ -88,6 +93,7 @@ public class InboundScriptHost
/// </summary> /// </summary>
/// <param name="attributeValues">Dictionary of attribute names to values.</param> /// <param name="attributeValues">Dictionary of attribute names to values.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public System.Threading.Tasks.Task SetAttributes( public System.Threading.Tasks.Task SetAttributes(
IReadOnlyDictionary<string, string> attributeValues, IReadOnlyDictionary<string, string> attributeValues,
System.Threading.CancellationToken cancellationToken = default) => System.Threading.CancellationToken cancellationToken = default) =>
@@ -20,6 +20,7 @@ public static class JsonSchemaShapeParser
{ {
/// <summary>Parses a JSON Schema or legacy flat-array parameters definition and returns the resulting parameter shapes.</summary> /// <summary>Parses a JSON Schema or legacy flat-array parameters definition and returns the resulting parameter shapes.</summary>
/// <param name="json">The JSON string to parse; <c>null</c> or whitespace returns an empty list.</param> /// <param name="json">The JSON string to parse; <c>null</c> or whitespace returns an empty list.</param>
/// <returns>A read-only list of <see cref="ParameterShape"/> instances parsed from the definition; empty on null, whitespace, or malformed input.</returns>
public static IReadOnlyList<ParameterShape> ParseParameters(string? json) public static IReadOnlyList<ParameterShape> ParseParameters(string? json)
{ {
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<ParameterShape>(); if (string.IsNullOrWhiteSpace(json)) return Array.Empty<ParameterShape>();
@@ -41,6 +42,7 @@ public static class JsonSchemaShapeParser
/// <summary>Parses a JSON Schema or legacy return-type definition and returns the normalised type name, or <c>null</c> if absent or unrecognised.</summary> /// <summary>Parses a JSON Schema or legacy return-type definition and returns the normalised type name, or <c>null</c> if absent or unrecognised.</summary>
/// <param name="json">The JSON string to parse; <c>null</c> or whitespace returns <c>null</c>.</param> /// <param name="json">The JSON string to parse; <c>null</c> or whitespace returns <c>null</c>.</param>
/// <returns>The normalised type name string (e.g. <c>"Boolean"</c>, <c>"List&lt;Integer&gt;"</c>), or <c>null</c> if the input is null, whitespace, malformed, or unrecognised.</returns>
public static string? ParseReturnType(string? json) public static string? ParseReturnType(string? json)
{ {
if (string.IsNullOrWhiteSpace(json)) return null; if (string.IsNullOrWhiteSpace(json)) return null;
@@ -41,6 +41,7 @@ internal sealed class SandboxConsoleCapture : TextWriter
/// <see cref="Console.Error"/> once for the process. Idempotent and /// <see cref="Console.Error"/> once for the process. Idempotent and
/// thread-safe. Subsequent calls return the already-installed instances. /// thread-safe. Subsequent calls return the already-installed instances.
/// </summary> /// </summary>
/// <returns>The installed <see cref="SandboxConsoleCapture"/> instances for stdout and stderr.</returns>
public static (SandboxConsoleCapture Out, SandboxConsoleCapture Error) Install() public static (SandboxConsoleCapture Out, SandboxConsoleCapture Error) Install()
{ {
if (_outInstance != null && _errorInstance != null) if (_outInstance != null && _errorInstance != null)
@@ -72,6 +73,7 @@ internal sealed class SandboxConsoleCapture : TextWriter
/// other call-trees are unaffected. /// other call-trees are unaffected.
/// </summary> /// </summary>
/// <param name="buffer">The writer that receives console output for this scope.</param> /// <param name="buffer">The writer that receives console output for this scope.</param>
/// <returns>A <see cref="CaptureScope"/> that, when disposed, restores the previous capture state.</returns>
public CaptureScope BeginCapture(StringWriter buffer) public CaptureScope BeginCapture(StringWriter buffer)
{ {
var previous = _current.Value; var previous = _current.Value;
@@ -32,6 +32,7 @@ public class SandboxExternalHelper
/// <param name="methodName">The method name to invoke.</param> /// <param name="methodName">The method name to invoke.</param>
/// <param name="parameters">Optional method parameters.</param> /// <param name="parameters">Optional method parameters.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the <see cref="ExternalCallResult"/> from the external system.</returns>
public Task<ExternalCallResult> Call( public Task<ExternalCallResult> Call(
string systemName, string systemName,
string methodName, string methodName,
@@ -49,6 +50,7 @@ public class SandboxExternalHelper
/// <param name="methodName">The method name to invoke.</param> /// <param name="methodName">The method name to invoke.</param>
/// <param name="parameters">Optional method parameters.</param> /// <param name="parameters">Optional method parameters.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the <see cref="ExternalCallResult"/> from the external system.</returns>
public Task<ExternalCallResult> CachedCall( public Task<ExternalCallResult> CachedCall(
string systemName, string systemName,
string methodName, string methodName,
@@ -80,6 +82,7 @@ public class SandboxDatabaseHelper
/// <summary>Gets a database connection by name.</summary> /// <summary>Gets a database connection by name.</summary>
/// <param name="name">The database connection name.</param> /// <param name="name">The database connection name.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the open <see cref="DbConnection"/> for the named database.</returns>
public Task<DbConnection> Connection(string name, CancellationToken cancellationToken = default) public Task<DbConnection> Connection(string name, CancellationToken cancellationToken = default)
{ {
if (_gateway == null) if (_gateway == null)
@@ -93,6 +96,7 @@ public class SandboxDatabaseHelper
/// <param name="sql">The SQL statement to execute.</param> /// <param name="sql">The SQL statement to execute.</param>
/// <param name="parameters">Optional SQL parameters.</param> /// <param name="parameters">Optional SQL parameters.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task CachedWrite( public Task CachedWrite(
string name, string name,
string sql, string sql,
@@ -123,6 +127,7 @@ public class SandboxNotifyHelper
{ {
/// <summary>Selects the notification list to send to.</summary> /// <summary>Selects the notification list to send to.</summary>
/// <param name="listName">The notification list name.</param> /// <param name="listName">The notification list name.</param>
/// <returns>A <see cref="SandboxNotifyTarget"/> for the specified list.</returns>
public SandboxNotifyTarget To(string listName) => public SandboxNotifyTarget To(string listName) =>
new(); new();
@@ -133,6 +138,7 @@ public class SandboxNotifyHelper
/// <c>NotifyHelper.Status</c>. /// <c>NotifyHelper.Status</c>.
/// </summary> /// </summary>
/// <param name="notificationId">The notification ID to check status for.</param> /// <param name="notificationId">The notification ID to check status for.</param>
/// <returns>A task that resolves to a placeholder <see cref="NotificationDeliveryStatus"/> with status "Unknown".</returns>
public Task<NotificationDeliveryStatus> Status(string notificationId) => public Task<NotificationDeliveryStatus> Status(string notificationId) =>
Task.FromResult(new NotificationDeliveryStatus("Unknown", 0, null, null)); Task.FromResult(new NotificationDeliveryStatus("Unknown", 0, null, null));
} }
@@ -156,6 +162,7 @@ public class SandboxNotifyTarget
/// <param name="subject">The notification subject.</param> /// <param name="subject">The notification subject.</param>
/// <param name="message">The notification message.</param> /// <param name="message">The notification message.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a fake notification ID string (a random GUID).</returns>
public Task<string> Send(string subject, string message, CancellationToken cancellationToken = default) => public Task<string> Send(string subject, string message, CancellationToken cancellationToken = default) =>
Task.FromResult(Guid.NewGuid().ToString("N")); Task.FromResult(Guid.NewGuid().ToString("N"));
} }
@@ -30,6 +30,7 @@ public class SandboxInboundScriptHost
/// Creates a sandbox route target that throws on every operation. /// Creates a sandbox route target that throws on every operation.
/// </summary> /// </summary>
/// <param name="instanceCode">The instance code (used only in the exception message).</param> /// <param name="instanceCode">The instance code (used only in the exception message).</param>
/// <returns>A sandbox route target bound to <paramref name="instanceCode"/>.</returns>
public RouteTarget To(string instanceCode) => new(instanceCode); public RouteTarget To(string instanceCode) => new(instanceCode);
} }
@@ -50,6 +51,7 @@ public class SandboxInboundScriptHost
/// <param name="scriptName">Script name (included in the exception message).</param> /// <param name="scriptName">Script name (included in the exception message).</param>
/// <param name="parameters">Unused parameters.</param> /// <param name="parameters">Unused parameters.</param>
/// <param name="cancellationToken">Unused token.</param> /// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task<object?> Call( public Task<object?> Call(
string scriptName, string scriptName,
object? parameters = null, object? parameters = null,
@@ -61,6 +63,7 @@ public class SandboxInboundScriptHost
/// </summary> /// </summary>
/// <param name="attributeName">Attribute name (included in the exception message).</param> /// <param name="attributeName">Attribute name (included in the exception message).</param>
/// <param name="cancellationToken">Unused token.</param> /// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task<object?> GetAttribute( public Task<object?> GetAttribute(
string attributeName, string attributeName,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
@@ -71,6 +74,7 @@ public class SandboxInboundScriptHost
/// </summary> /// </summary>
/// <param name="attributeNames">Attribute names (unused).</param> /// <param name="attributeNames">Attribute names (unused).</param>
/// <param name="cancellationToken">Unused token.</param> /// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task<IReadOnlyDictionary<string, object?>> GetAttributes( public Task<IReadOnlyDictionary<string, object?>> GetAttributes(
IEnumerable<string> attributeNames, IEnumerable<string> attributeNames,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
@@ -82,6 +86,7 @@ public class SandboxInboundScriptHost
/// <param name="attributeName">Attribute name (included in the exception message).</param> /// <param name="attributeName">Attribute name (included in the exception message).</param>
/// <param name="value">Unused value.</param> /// <param name="value">Unused value.</param>
/// <param name="cancellationToken">Unused token.</param> /// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task SetAttribute( public Task SetAttribute(
string attributeName, string attributeName,
string value, string value,
@@ -93,6 +98,7 @@ public class SandboxInboundScriptHost
/// </summary> /// </summary>
/// <param name="attributeValues">Unused attribute values.</param> /// <param name="attributeValues">Unused attribute values.</param>
/// <param name="cancellationToken">Unused token.</param> /// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task SetAttributes( public Task SetAttributes(
IReadOnlyDictionary<string, string> attributeValues, IReadOnlyDictionary<string, string> attributeValues,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
@@ -102,6 +102,7 @@ public interface ISandboxInstanceGateway
/// <param name="canonicalName">The canonical name of the attribute.</param> /// <param name="canonicalName">The canonical name of the attribute.</param>
/// <param name="value">The value to set.</param> /// <param name="value">The value to set.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct); Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct);
/// <summary> /// <summary>
@@ -12,6 +12,7 @@ public static class ScriptShapeParser
/// <param name="name">The canonical script name.</param> /// <param name="name">The canonical script name.</param>
/// <param name="parametersJson">The JSON Schema or legacy flat-array parameters definition, or <c>null</c> for parameterless scripts.</param> /// <param name="parametersJson">The JSON Schema or legacy flat-array parameters definition, or <c>null</c> for parameterless scripts.</param>
/// <param name="returnJson">The JSON Schema or legacy return-type definition, or <c>null</c> for void scripts.</param> /// <param name="returnJson">The JSON Schema or legacy return-type definition, or <c>null</c> for void scripts.</param>
/// <returns>A <see cref="ScriptShape"/> describing the script's parameter list and return type.</returns>
public static ScriptShape Parse(string name, string? parametersJson, string? returnJson) public static ScriptShape Parse(string name, string? parametersJson, string? returnJson)
{ {
var parameters = JsonSchemaShapeParser.ParseParameters(parametersJson); var parameters = JsonSchemaShapeParser.ParseParameters(parametersJson);
@@ -14,6 +14,7 @@ public static class ServiceCollectionExtensions
/// Registers all Central UI services including Blazor, auth state, dialogs, audit query, and script analysis. /// Registers all Central UI services including Blazor, auth state, dialogs, audit query, and script analysis.
/// </summary> /// </summary>
/// <param name="services">The service collection to configure.</param> /// <param name="services">The service collection to configure.</param>
/// <returns>The <paramref name="services"/> instance for chaining.</returns>
public static IServiceCollection AddCentralUI(this IServiceCollection services) public static IServiceCollection AddCentralUI(this IServiceCollection services)
{ {
services.AddRazorComponents() services.AddRazorComponents()
@@ -45,6 +45,7 @@ public static class ApiMethodKeyScopeReconciler
/// <param name="currentMethodsByKey">Each affected key's CURRENT full scope set, keyed by KeyId. /// <param name="currentMethodsByKey">Each affected key's CURRENT full scope set, keyed by KeyId.
/// Read fresh from the seam right before reconciling so concurrent edits do not get clobbered.</param> /// Read fresh from the seam right before reconciling so concurrent edits do not get clobbered.</param>
/// <param name="keyNamesById">Display names by KeyId, for human-readable empty-scope messages.</param> /// <param name="keyNamesById">Display names by KeyId, for human-readable empty-scope messages.</param>
/// <returns>A <see cref="ReconcileResult"/> with the scope updates to apply and any empty-scope warnings.</returns>
public static ReconcileResult Reconcile( public static ReconcileResult Reconcile(
string methodName, string methodName,
IReadOnlySet<string> selectedKeyIds, IReadOnlySet<string> selectedKeyIds,
@@ -70,6 +70,8 @@ public sealed record AuditEventView
/// <summary> /// <summary>
/// Decomposes a canonical <see cref="AuditEvent"/> into a flat view for the UI. /// Decomposes a canonical <see cref="AuditEvent"/> into a flat view for the UI.
/// </summary> /// </summary>
/// <param name="evt">The canonical audit event to decompose.</param>
/// <returns>A flat <see cref="AuditEventView"/> populated from the event's top-level and details fields.</returns>
public static AuditEventView From(AuditEvent evt) public static AuditEventView From(AuditEvent evt)
{ {
var r = AuditRowProjection.Decompose(evt); var r = AuditRowProjection.Decompose(evt);
@@ -42,6 +42,7 @@ public interface IAuditLogExportService
/// enough to amortise the per-query overhead, small enough that one page in /// enough to amortise the per-query overhead, small enough that one page in
/// memory is bounded. /// memory is bounded.
/// </param> /// </param>
/// <returns>A task that completes when all matching rows (up to <paramref name="maxRows"/>) have been written to <paramref name="output"/>.</returns>
Task ExportAsync( Task ExportAsync(
AuditLogQueryFilter filter, AuditLogQueryFilter filter,
int maxRows, int maxRows,
@@ -176,6 +177,7 @@ public sealed class AuditLogExportService : IAuditLogExportService
/// cleanly on another. /// cleanly on another.
/// </summary> /// </summary>
/// <param name="evt">The audit event view to format as a CSV row.</param> /// <param name="evt">The audit event view to format as a CSV row.</param>
/// <returns>An RFC 4180 CSV row string (no trailing newline) for the supplied event.</returns>
internal static string FormatCsvRow(AuditEventView evt) internal static string FormatCsvRow(AuditEventView evt)
{ {
var sb = new StringBuilder(256); var sb = new StringBuilder(256);
@@ -28,6 +28,7 @@ public interface IAuditLogQueryService
/// <param name="filter">Filter criteria applied to the audit log query.</param> /// <param name="filter">Filter criteria applied to the audit log query.</param>
/// <param name="paging">Optional paging cursor; defaults to first page when null.</param> /// <param name="paging">Optional paging cursor; defaults to first page when null.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a page of audit event views matching the filter.</returns>
Task<IReadOnlyList<AuditEventView>> QueryAsync( Task<IReadOnlyList<AuditEventView>> QueryAsync(
AuditLogQueryFilter filter, AuditLogQueryFilter filter,
AuditLogPaging? paging = null, AuditLogPaging? paging = null,
@@ -57,6 +58,7 @@ public interface IAuditLogQueryService
/// dashboard. /// dashboard.
/// </remarks> /// </remarks>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the current audit KPI snapshot.</returns>
Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(CancellationToken ct = default); Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(CancellationToken ct = default);
/// <summary> /// <summary>
@@ -76,6 +78,7 @@ public interface IAuditLogQueryService
/// </remarks> /// </remarks>
/// <param name="executionId">Any execution id in the chain to look up.</param> /// <param name="executionId">Any execution id in the chain to look up.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the flat list of execution tree nodes in the chain.</returns>
Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync( Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
Guid executionId, Guid executionId,
CancellationToken ct = default); CancellationToken ct = default);
@@ -90,5 +93,6 @@ public interface IAuditLogQueryService
/// filter affordance. /// filter affordance.
/// </summary> /// </summary>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the distinct non-null source node names present in the audit log.</returns>
Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default); Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default);
} }
@@ -31,6 +31,7 @@ public interface IBindingTester
/// <param name="connectionName">Name of the site-local data connection — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param> /// <param name="connectionName">Name of the site-local data connection — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param>
/// <param name="tagPaths">Tag paths to read.</param> /// <param name="tagPaths">Tag paths to read.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the read outcomes for all requested tag paths.</returns>
Task<ReadTagValuesResult> ReadAsync( Task<ReadTagValuesResult> ReadAsync(
string siteId, string siteId,
string connectionName, string connectionName,
@@ -29,6 +29,7 @@ public interface IBrowseService
/// <param name="connectionName">Name of the site-local data connection to browse against — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param> /// <param name="connectionName">Name of the site-local data connection to browse against — the site's <c>DataConnectionManagerActor</c> indexes its children by name.</param>
/// <param name="parentNodeId">Node to browse, or <c>null</c> to browse from the server root.</param> /// <param name="parentNodeId">Node to browse, or <c>null</c> to browse from the server root.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a <see cref="BrowseNodeResult"/> containing child nodes or a <see cref="BrowseFailure"/> on error.</returns>
Task<BrowseNodeResult> BrowseChildrenAsync( Task<BrowseNodeResult> BrowseChildrenAsync(
string siteId, string siteId,
string connectionName, string connectionName,
@@ -18,11 +18,7 @@ public sealed class ClusterOptionsValidator : OptionsValidatorBase<ClusterOption
"keep-oldest" "keep-oldest"
}; };
/// <summary> /// <inheritdoc />
/// Validates the cluster options, recording a failure if any critical settings are misconfigured.
/// </summary>
/// <param name="builder">The accumulator to record failures on.</param>
/// <param name="options">The cluster options to validate.</param>
protected override void Validate(ValidationBuilder builder, ClusterOptions options) protected override void Validate(ValidationBuilder builder, ClusterOptions options)
{ {
// CI-012: design doc states "both nodes are seed nodes — each node lists // CI-012: design doc states "both nodes are seed nodes — each node lists
@@ -23,6 +23,7 @@ public static class ServiceCollectionExtensions
/// </para> /// </para>
/// </summary> /// </summary>
/// <param name="services">The service collection to register into.</param> /// <param name="services">The service collection to register into.</param>
/// <returns>The same <paramref name="services"/> instance, for call chaining.</returns>
public static IServiceCollection AddClusterInfrastructure(this IServiceCollection services) public static IServiceCollection AddClusterInfrastructure(this IServiceCollection services)
{ {
services.TryAddEnumerable( services.TryAddEnumerable(
@@ -20,9 +20,17 @@ public interface IAlarmSubscribableConnection
/// currently-active conditions (Snapshot…SnapshotComplete) on every /// currently-active conditions (Snapshot…SnapshotComplete) on every
/// (re)subscribe. Returns a subscription id for <see cref="UnsubscribeAlarmsAsync"/>. /// (re)subscribe. Returns a subscription id for <see cref="UnsubscribeAlarmsAsync"/>.
/// </summary> /// </summary>
/// <param name="sourceReference">The source object reference to subscribe alarms for.</param>
/// <param name="conditionFilter">Optional condition name filter; <c>null</c> subscribes to all conditions.</param>
/// <param name="callback">Delegate invoked on each incoming alarm transition.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the subscription id for use with <see cref="UnsubscribeAlarmsAsync"/>.</returns>
Task<string> SubscribeAlarmsAsync(string sourceReference, string? conditionFilter, Task<string> SubscribeAlarmsAsync(string sourceReference, string? conditionFilter,
AlarmTransitionCallback callback, CancellationToken cancellationToken = default); AlarmTransitionCallback callback, CancellationToken cancellationToken = default);
/// <summary>Cancels an active alarm subscription by its id.</summary> /// <summary>Cancels an active alarm subscription by its id.</summary>
/// <param name="subscriptionId">The subscription id returned by <see cref="SubscribeAlarmsAsync"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UnsubscribeAlarmsAsync(string subscriptionId, CancellationToken cancellationToken = default); Task UnsubscribeAlarmsAsync(string subscriptionId, CancellationToken cancellationToken = default);
} }
@@ -14,6 +14,7 @@ public interface IBrowsableDataConnection
/// </summary> /// </summary>
/// <param name="parentNodeId">Node id whose children to browse, or null for the server root (OPC UA ObjectsFolder).</param> /// <param name="parentNodeId">Node id whose children to browse, or null for the server root (OPC UA ObjectsFolder).</param>
/// <param name="cancellationToken">Cancellation token; on cancellation the implementation should throw <see cref="OperationCanceledException"/>.</param> /// <param name="cancellationToken">Cancellation token; on cancellation the implementation should throw <see cref="OperationCanceledException"/>.</param>
/// <returns>A task that resolves to the child nodes and a flag indicating whether results were truncated.</returns>
Task<BrowseChildrenResult> BrowseChildrenAsync( Task<BrowseChildrenResult> BrowseChildrenAsync(
string? parentNodeId, string? parentNodeId,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -44,5 +45,7 @@ public enum BrowseNodeClass { Object, Variable, Method, Other }
/// </summary> /// </summary>
public sealed class ConnectionNotConnectedException : InvalidOperationException public sealed class ConnectionNotConnectedException : InvalidOperationException
{ {
/// <summary>Initializes a new <see cref="ConnectionNotConnectedException"/> with the specified error message.</summary>
/// <param name="message">Message describing why the connection is not currently connected.</param>
public ConnectionNotConnectedException(string message) : base(message) { } public ConnectionNotConnectedException(string message) : base(message) { }
} }
@@ -18,9 +18,11 @@ public interface IDataConnection : IAsyncDisposable
/// <summary>Establishes the protocol connection using the provided connection details.</summary> /// <summary>Establishes the protocol connection using the provided connection details.</summary>
/// <param name="connectionDetails">Protocol-specific key-value configuration pairs.</param> /// <param name="connectionDetails">Protocol-specific key-value configuration pairs.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ConnectAsync(IDictionary<string, string> connectionDetails, CancellationToken cancellationToken = default); Task ConnectAsync(IDictionary<string, string> connectionDetails, CancellationToken cancellationToken = default);
/// <summary>Gracefully terminates the protocol connection.</summary> /// <summary>Gracefully terminates the protocol connection.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DisconnectAsync(CancellationToken cancellationToken = default); Task DisconnectAsync(CancellationToken cancellationToken = default);
/// <summary>Subscribes to value-change notifications for a tag path; returns a subscription ID.</summary> /// <summary>Subscribes to value-change notifications for a tag path; returns a subscription ID.</summary>
/// <param name="tagPath">The tag path to subscribe to.</param> /// <param name="tagPath">The tag path to subscribe to.</param>
@@ -31,6 +33,7 @@ public interface IDataConnection : IAsyncDisposable
/// <summary>Cancels an active subscription by its ID.</summary> /// <summary>Cancels an active subscription by its ID.</summary>
/// <param name="subscriptionId">The subscription ID returned by <see cref="SubscribeAsync"/>.</param> /// <param name="subscriptionId">The subscription ID returned by <see cref="SubscribeAsync"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default); Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default);
/// <summary>Reads the current value of a single tag.</summary> /// <summary>Reads the current value of a single tag.</summary>
/// <param name="tagPath">The tag path to read.</param> /// <param name="tagPath">The tag path to read.</param>
@@ -31,6 +31,7 @@ public interface IAuditLogRepository
/// </summary> /// </summary>
/// <param name="evt">The audit event to insert.</param> /// <param name="evt">The audit event to insert.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default); Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -44,6 +45,7 @@ public interface IAuditLogRepository
/// <param name="filter">Filter criteria to apply to the query.</param> /// <param name="filter">Filter criteria to apply to the query.</param>
/// <param name="paging">Paging cursor and page size.</param> /// <param name="paging">Paging cursor and page size.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the matching audit events for the requested page, ordered by <c>(OccurredAtUtc DESC, EventId DESC)</c>.</returns>
Task<IReadOnlyList<AuditEvent>> QueryAsync( Task<IReadOnlyList<AuditEvent>> QueryAsync(
AuditLogQueryFilter filter, AuditLogQueryFilter filter,
AuditLogPaging paging, AuditLogPaging paging,
@@ -82,6 +84,7 @@ public interface IAuditLogRepository
/// </remarks> /// </remarks>
/// <param name="monthBoundary">Lower-bound datetime of the monthly partition to switch out.</param> /// <param name="monthBoundary">Lower-bound datetime of the monthly partition to switch out.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the approximate number of rows discarded by the partition switch.</returns>
Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default); Task<long> SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -94,6 +97,7 @@ public interface IAuditLogRepository
/// </summary> /// </summary>
/// <param name="threshold">Only partitions whose data is entirely older than this UTC datetime are returned.</param> /// <param name="threshold">Only partitions whose data is entirely older than this UTC datetime are returned.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the list of partition lower-bound boundaries eligible for purge.</returns>
Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync( Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, DateTime threshold,
CancellationToken ct = default); CancellationToken ct = default);
@@ -183,6 +187,7 @@ public interface IAuditLogRepository
/// </remarks> /// </remarks>
/// <param name="executionId">Any execution id in the chain; the implementation walks to the root and back down.</param> /// <param name="executionId">Any execution id in the chain; the implementation walks to the root and back down.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the full execution tree rooted at the topmost ancestor, one node per distinct execution.</returns>
Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync( Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
Guid executionId, Guid executionId,
CancellationToken ct = default); CancellationToken ct = default);
@@ -194,5 +199,6 @@ public interface IAuditLogRepository
/// for ~60s so the repository is hit at most once per minute per circuit. /// for ~60s so the repository is hit at most once per minute per circuit.
/// </summary> /// </summary>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the distinct, non-null source node names in ascending order.</returns>
Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default); Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default);
} }
@@ -10,30 +10,37 @@ public interface ICentralUiRepository
{ {
/// <summary>Returns all configured sites.</summary> /// <summary>Returns all configured sites.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all sites.</returns>
Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default);
/// <summary>Returns all data connections for the specified site.</summary> /// <summary>Returns all data connections for the specified site.</summary>
/// <param name="siteId">The site database ID to filter by.</param> /// <param name="siteId">The site database ID to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of data connections for the site.</returns>
Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Returns all data connections across all sites.</summary> /// <summary>Returns all data connections across all sites.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all data connections.</returns>
Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default);
/// <summary>Returns the full template tree including folders and templates.</summary> /// <summary>Returns the full template tree including folders and templates.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of templates.</returns>
Task<IReadOnlyList<Template>> GetTemplateTreeAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<Template>> GetTemplateTreeAsync(CancellationToken cancellationToken = default);
/// <summary>Returns instances filtered by optional site, template, or search term.</summary> /// <summary>Returns instances filtered by optional site, template, or search term.</summary>
/// <param name="siteId">Optional site ID to filter by.</param> /// <param name="siteId">Optional site ID to filter by.</param>
/// <param name="templateId">Optional template ID to filter by.</param> /// <param name="templateId">Optional template ID to filter by.</param>
/// <param name="searchTerm">Optional keyword to filter instance names.</param> /// <param name="searchTerm">Optional keyword to filter instance names.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of matching instances.</returns>
Task<IReadOnlyList<Instance>> GetInstancesFilteredAsync(int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default); Task<IReadOnlyList<Instance>> GetInstancesFilteredAsync(int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default);
/// <summary>Returns the most recent deployment records up to the specified count.</summary> /// <summary>Returns the most recent deployment records up to the specified count.</summary>
/// <param name="count">Maximum number of records to return.</param> /// <param name="count">Maximum number of records to return.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of recent deployment records.</returns>
Task<IReadOnlyList<DeploymentRecord>> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default); Task<IReadOnlyList<DeploymentRecord>> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default);
/// <summary>Returns the area tree for the specified site.</summary> /// <summary>Returns the area tree for the specified site.</summary>
/// <param name="siteId">The site database ID.</param> /// <param name="siteId">The site database ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of areas for the site.</returns>
Task<IReadOnlyList<Area>> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); Task<IReadOnlyList<Area>> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
// Audit log queries // Audit log queries
@@ -51,6 +58,7 @@ public interface ICentralUiRepository
/// <param name="page">One-based page number.</param> /// <param name="page">One-based page number.</param>
/// <param name="pageSize">Number of entries per page.</param> /// <param name="pageSize">Number of entries per page.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a tuple of the matching entries and the total count.</returns>
Task<(IReadOnlyList<AuditLogEntry> Entries, int TotalCount)> GetAuditLogEntriesAsync( Task<(IReadOnlyList<AuditLogEntry> Entries, int TotalCount)> GetAuditLogEntriesAsync(
string? user = null, string? user = null,
string? entityType = null, string? entityType = null,
@@ -66,5 +74,6 @@ public interface ICentralUiRepository
/// <summary>Persists pending changes to the underlying store.</summary> /// <summary>Persists pending changes to the underlying store.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the number of entities written.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
} }
@@ -36,6 +36,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="definition">The external system definition to add.</param> /// <param name="definition">The external system definition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default); Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -43,6 +44,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="definition">The external system definition to update.</param> /// <param name="definition">The external system definition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default); Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -50,6 +52,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="id">The external system ID.</param> /// <param name="id">The external system ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default); Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default);
// ExternalSystemMethod // ExternalSystemMethod
@@ -86,6 +89,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="method">The external system method to add.</param> /// <param name="method">The external system method to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default); Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -93,6 +97,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="method">The external system method to update.</param> /// <param name="method">The external system method to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default); Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -100,6 +105,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="id">The method ID.</param> /// <param name="id">The method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default); Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default);
// DatabaseConnectionDefinition // DatabaseConnectionDefinition
@@ -135,6 +141,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="definition">The database connection definition to add.</param> /// <param name="definition">The database connection definition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default); Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -142,6 +149,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="definition">The database connection definition to update.</param> /// <param name="definition">The database connection definition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default); Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -149,6 +157,7 @@ public interface IExternalSystemRepository
/// </summary> /// </summary>
/// <param name="id">The database connection ID.</param> /// <param name="id">The database connection ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default); Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -14,28 +14,35 @@ public interface IInboundApiRepository
/// <summary>Retrieves an API method by ID.</summary> /// <summary>Retrieves an API method by ID.</summary>
/// <param name="id">The API method ID.</param> /// <param name="id">The API method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching <see cref="ApiMethod"/>, or <c>null</c> if not found.</returns>
Task<ApiMethod?> GetApiMethodByIdAsync(int id, CancellationToken cancellationToken = default); Task<ApiMethod?> GetApiMethodByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all API methods.</summary> /// <summary>Retrieves all API methods.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all <see cref="ApiMethod"/> entities.</returns>
Task<IReadOnlyList<ApiMethod>> GetAllApiMethodsAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<ApiMethod>> GetAllApiMethodsAsync(CancellationToken cancellationToken = default);
/// <summary>Retrieves an API method by name.</summary> /// <summary>Retrieves an API method by name.</summary>
/// <param name="name">The API method name.</param> /// <param name="name">The API method name.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching <see cref="ApiMethod"/>, or <c>null</c> if not found.</returns>
Task<ApiMethod?> GetMethodByNameAsync(string name, CancellationToken cancellationToken = default); Task<ApiMethod?> GetMethodByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>Adds a new API method.</summary> /// <summary>Adds a new API method.</summary>
/// <param name="method">The API method to add.</param> /// <param name="method">The API method to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default); Task AddApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
/// <summary>Updates an existing API method.</summary> /// <summary>Updates an existing API method.</summary>
/// <param name="method">The API method to update.</param> /// <param name="method">The API method to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default); Task UpdateApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
/// <summary>Deletes an API method by ID.</summary> /// <summary>Deletes an API method by ID.</summary>
/// <param name="id">The API method ID.</param> /// <param name="id">The API method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteApiMethodAsync(int id, CancellationToken cancellationToken = default); Task DeleteApiMethodAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the database.</summary> /// <summary>Saves pending changes to the database.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the number of state entries written to the database.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
} }
@@ -51,6 +51,7 @@ public interface INotificationOutboxRepository
/// </summary> /// </summary>
/// <param name="n">The notification to update.</param> /// <param name="n">The notification to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that completes when the notification has been persisted.</returns>
Task UpdateAsync(Notification n, CancellationToken cancellationToken = default); Task UpdateAsync(Notification n, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
@@ -25,16 +25,19 @@ public interface INotificationRepository
/// <summary>Adds a new notification list.</summary> /// <summary>Adds a new notification list.</summary>
/// <param name="list">The notification list to add.</param> /// <param name="list">The notification list to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default); Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
/// <summary>Updates an existing notification list.</summary> /// <summary>Updates an existing notification list.</summary>
/// <param name="list">The notification list to update.</param> /// <param name="list">The notification list to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default); Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
/// <summary>Deletes a notification list by ID.</summary> /// <summary>Deletes a notification list by ID.</summary>
/// <param name="id">The notification list ID.</param> /// <param name="id">The notification list ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default); Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default);
// NotificationRecipient // NotificationRecipient
@@ -53,16 +56,19 @@ public interface INotificationRepository
/// <summary>Adds a new notification recipient.</summary> /// <summary>Adds a new notification recipient.</summary>
/// <param name="recipient">The recipient to add.</param> /// <param name="recipient">The recipient to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default); Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
/// <summary>Updates an existing notification recipient.</summary> /// <summary>Updates an existing notification recipient.</summary>
/// <param name="recipient">The recipient to update.</param> /// <param name="recipient">The recipient to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default); Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
/// <summary>Deletes a notification recipient by ID.</summary> /// <summary>Deletes a notification recipient by ID.</summary>
/// <param name="id">The recipient ID.</param> /// <param name="id">The recipient ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default); Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default);
// SmtpConfiguration // SmtpConfiguration
@@ -80,16 +86,19 @@ public interface INotificationRepository
/// <summary>Adds a new SMTP configuration.</summary> /// <summary>Adds a new SMTP configuration.</summary>
/// <param name="configuration">The SMTP configuration to add.</param> /// <param name="configuration">The SMTP configuration to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default); Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
/// <summary>Updates an existing SMTP configuration.</summary> /// <summary>Updates an existing SMTP configuration.</summary>
/// <param name="configuration">The SMTP configuration to update.</param> /// <param name="configuration">The SMTP configuration to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default); Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
/// <summary>Deletes an SMTP configuration by ID.</summary> /// <summary>Deletes an SMTP configuration by ID.</summary>
/// <param name="id">The SMTP configuration ID.</param> /// <param name="id">The SMTP configuration ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default); Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the repository.</summary> /// <summary>Saves pending changes to the repository.</summary>
@@ -39,6 +39,7 @@ public interface ISiteCallAuditRepository
/// </summary> /// </summary>
/// <param name="siteCall">The site call row to insert or monotonically update.</param> /// <param name="siteCall">The site call row to insert or monotonically update.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default); Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -46,6 +47,7 @@ public interface ISiteCallAuditRepository
/// </summary> /// </summary>
/// <param name="id">The tracked operation id to look up.</param> /// <param name="id">The tracked operation id to look up.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the matching <see cref="SiteCall"/>, or <c>null</c> if no row exists.</returns>
Task<SiteCall?> GetAsync(TrackedOperationId id, CancellationToken ct = default); Task<SiteCall?> GetAsync(TrackedOperationId id, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -58,6 +60,7 @@ public interface ISiteCallAuditRepository
/// <param name="filter">Filter criteria for the query.</param> /// <param name="filter">Filter criteria for the query.</param>
/// <param name="paging">Keyset paging parameters.</param> /// <param name="paging">Keyset paging parameters.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a page of <see cref="SiteCall"/> rows matching the filter, ordered by creation time descending.</returns>
Task<IReadOnlyList<SiteCall>> QueryAsync( Task<IReadOnlyList<SiteCall>> QueryAsync(
SiteCallQueryFilter filter, SiteCallQueryFilter filter,
SiteCallPaging paging, SiteCallPaging paging,
@@ -71,6 +74,7 @@ public interface ISiteCallAuditRepository
/// </summary> /// </summary>
/// <param name="olderThanUtc">UTC cutoff; terminal rows older than this are deleted.</param> /// <param name="olderThanUtc">UTC cutoff; terminal rows older than this are deleted.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the number of rows deleted.</returns>
Task<int> PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default); Task<int> PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -84,6 +88,7 @@ public interface ISiteCallAuditRepository
/// <param name="stuckCutoff">UTC threshold for classifying a row as stuck.</param> /// <param name="stuckCutoff">UTC threshold for classifying a row as stuck.</param>
/// <param name="intervalSince">UTC start of the delivered/failed interval window.</param> /// <param name="intervalSince">UTC start of the delivered/failed interval window.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a global <see cref="SiteCallKpiSnapshot"/> computed from the current table state.</returns>
Task<SiteCallKpiSnapshot> ComputeKpisAsync( Task<SiteCallKpiSnapshot> ComputeKpisAsync(
DateTime stuckCutoff, DateTime stuckCutoff,
DateTime intervalSince, DateTime intervalSince,
@@ -97,6 +102,7 @@ public interface ISiteCallAuditRepository
/// <param name="stuckCutoff">UTC threshold for classifying a row as stuck.</param> /// <param name="stuckCutoff">UTC threshold for classifying a row as stuck.</param>
/// <param name="intervalSince">UTC start of the delivered/failed interval window.</param> /// <param name="intervalSince">UTC start of the delivered/failed interval window.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a per-site KPI list; sites with no rows are omitted.</returns>
Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync( Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
DateTime stuckCutoff, DateTime stuckCutoff,
DateTime intervalSince, DateTime intervalSince,
@@ -12,59 +12,73 @@ public interface ISiteRepository
/// <summary>Retrieves a site by its ID.</summary> /// <summary>Retrieves a site by its ID.</summary>
/// <param name="id">The site primary key.</param> /// <param name="id">The site primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching <see cref="Site"/>, or <c>null</c> if not found.</returns>
Task<Site?> GetSiteByIdAsync(int id, CancellationToken cancellationToken = default); Task<Site?> GetSiteByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves a site by its identifier.</summary> /// <summary>Retrieves a site by its identifier.</summary>
/// <param name="siteIdentifier">The unique site identifier string.</param> /// <param name="siteIdentifier">The unique site identifier string.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching <see cref="Site"/>, or <c>null</c> if not found.</returns>
Task<Site?> GetSiteByIdentifierAsync(string siteIdentifier, CancellationToken cancellationToken = default); Task<Site?> GetSiteByIdentifierAsync(string siteIdentifier, CancellationToken cancellationToken = default);
/// <summary>Retrieves all sites.</summary> /// <summary>Retrieves all sites.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all <see cref="Site"/> entities.</returns>
Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default);
/// <summary>Adds a new site.</summary> /// <summary>Adds a new site.</summary>
/// <param name="site">The site entity to add.</param> /// <param name="site">The site entity to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddSiteAsync(Site site, CancellationToken cancellationToken = default); Task AddSiteAsync(Site site, CancellationToken cancellationToken = default);
/// <summary>Updates an existing site.</summary> /// <summary>Updates an existing site.</summary>
/// <param name="site">The site entity with updated values.</param> /// <param name="site">The site entity with updated values.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateSiteAsync(Site site, CancellationToken cancellationToken = default); Task UpdateSiteAsync(Site site, CancellationToken cancellationToken = default);
/// <summary>Deletes a site.</summary> /// <summary>Deletes a site.</summary>
/// <param name="id">The site primary key.</param> /// <param name="id">The site primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteSiteAsync(int id, CancellationToken cancellationToken = default); Task DeleteSiteAsync(int id, CancellationToken cancellationToken = default);
// Data Connections // Data Connections
/// <summary>Retrieves a data connection by its ID.</summary> /// <summary>Retrieves a data connection by its ID.</summary>
/// <param name="id">The data connection primary key.</param> /// <param name="id">The data connection primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching <see cref="DataConnection"/>, or <c>null</c> if not found.</returns>
Task<DataConnection?> GetDataConnectionByIdAsync(int id, CancellationToken cancellationToken = default); Task<DataConnection?> GetDataConnectionByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all data connections.</summary> /// <summary>Retrieves all data connections.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all <see cref="DataConnection"/> entities.</returns>
Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default);
/// <summary>Retrieves all data connections for a site.</summary> /// <summary>Retrieves all data connections for a site.</summary>
/// <param name="siteId">The site primary key to filter by.</param> /// <param name="siteId">The site primary key to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of <see cref="DataConnection"/> entities for the given site.</returns>
Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Adds a new data connection.</summary> /// <summary>Adds a new data connection.</summary>
/// <param name="connection">The data connection entity to add.</param> /// <param name="connection">The data connection entity to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default); Task AddDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default);
/// <summary>Updates an existing data connection.</summary> /// <summary>Updates an existing data connection.</summary>
/// <param name="connection">The data connection entity with updated values.</param> /// <param name="connection">The data connection entity with updated values.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default); Task UpdateDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default);
/// <summary>Deletes a data connection.</summary> /// <summary>Deletes a data connection.</summary>
/// <param name="id">The data connection primary key.</param> /// <param name="id">The data connection primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteDataConnectionAsync(int id, CancellationToken cancellationToken = default); Task DeleteDataConnectionAsync(int id, CancellationToken cancellationToken = default);
// Instances (for deletion constraint checks) // Instances (for deletion constraint checks)
/// <summary>Retrieves all instances deployed to a site.</summary> /// <summary>Retrieves all instances deployed to a site.</summary>
/// <param name="siteId">The site primary key to filter by.</param> /// <param name="siteId">The site primary key to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of <see cref="Instance"/> entities for the given site.</returns>
Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Saves all pending changes to the database.</summary> /// <summary>Saves all pending changes to the database.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the number of state entries written to the database.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
} }
@@ -10,10 +10,12 @@ public interface ITemplateEngineRepository
/// <summary>Retrieves a template by ID.</summary> /// <summary>Retrieves a template by ID.</summary>
/// <param name="id">The template ID.</param> /// <param name="id">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching template, or <see langword="null"/> if not found.</returns>
Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default); Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves a template with its child entities by ID.</summary> /// <summary>Retrieves a template with its child entities by ID.</summary>
/// <param name="id">The template ID.</param> /// <param name="id">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching template with children loaded, or <see langword="null"/> if not found.</returns>
Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default); Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Bulk variant of <see cref="GetTemplateWithChildrenAsync(int, CancellationToken)"/> /// Bulk variant of <see cref="GetTemplateWithChildrenAsync(int, CancellationToken)"/>
@@ -26,9 +28,11 @@ public interface ITemplateEngineRepository
/// </summary> /// </summary>
/// <param name="names">Template names to load. Duplicate / null / empty names are filtered out.</param> /// <param name="names">Template names to load. Duplicate / null / empty names are filtered out.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of matched templates with children loaded.</returns>
Task<IReadOnlyList<Template>> GetTemplatesWithChildrenAsync(IEnumerable<string> names, CancellationToken cancellationToken = default); Task<IReadOnlyList<Template>> GetTemplatesWithChildrenAsync(IEnumerable<string> names, CancellationToken cancellationToken = default);
/// <summary>Retrieves all templates.</summary> /// <summary>Retrieves all templates.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all templates.</returns>
Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Returns every template that contains a composition referencing /// Returns every template that contains a composition referencing
@@ -38,293 +42,386 @@ public interface ITemplateEngineRepository
/// </summary> /// </summary>
/// <param name="composedTemplateId">The composed template ID.</param> /// <param name="composedTemplateId">The composed template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of parent templates that reference the composed template.</returns>
Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default); Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template.</summary> /// <summary>Adds a new template.</summary>
/// <param name="template">The template to add.</param> /// <param name="template">The template to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default); Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template.</summary> /// <summary>Updates an existing template.</summary>
/// <param name="template">The template to update.</param> /// <param name="template">The template to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default); Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default);
/// <summary>Deletes a template by ID.</summary> /// <summary>Deletes a template by ID.</summary>
/// <param name="id">The template ID.</param> /// <param name="id">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default); Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default);
// TemplateAttribute // TemplateAttribute
/// <summary>Retrieves a template attribute by ID.</summary> /// <summary>Retrieves a template attribute by ID.</summary>
/// <param name="id">The attribute ID.</param> /// <param name="id">The attribute ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching attribute, or <see langword="null"/> if not found.</returns>
Task<TemplateAttribute?> GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default); Task<TemplateAttribute?> GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves attributes for a template.</summary> /// <summary>Retrieves attributes for a template.</summary>
/// <param name="templateId">The template ID.</param> /// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of attributes for the specified template.</returns>
Task<IReadOnlyList<TemplateAttribute>> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TemplateAttribute>> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template attribute.</summary> /// <summary>Adds a new template attribute.</summary>
/// <param name="attribute">The attribute to add.</param> /// <param name="attribute">The attribute to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default); Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template attribute.</summary> /// <summary>Updates an existing template attribute.</summary>
/// <param name="attribute">The attribute to update.</param> /// <param name="attribute">The attribute to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default); Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
/// <summary>Deletes a template attribute by ID.</summary> /// <summary>Deletes a template attribute by ID.</summary>
/// <param name="id">The attribute ID.</param> /// <param name="id">The attribute ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default); Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default);
// TemplateAlarm // TemplateAlarm
/// <summary>Retrieves a template alarm by ID.</summary> /// <summary>Retrieves a template alarm by ID.</summary>
/// <param name="id">The alarm ID.</param> /// <param name="id">The alarm ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching alarm, or <see langword="null"/> if not found.</returns>
Task<TemplateAlarm?> GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default); Task<TemplateAlarm?> GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves alarms for a template.</summary> /// <summary>Retrieves alarms for a template.</summary>
/// <param name="templateId">The template ID.</param> /// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of alarms for the specified template.</returns>
Task<IReadOnlyList<TemplateAlarm>> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TemplateAlarm>> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template alarm.</summary> /// <summary>Adds a new template alarm.</summary>
/// <param name="alarm">The alarm to add.</param> /// <param name="alarm">The alarm to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default); Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template alarm.</summary> /// <summary>Updates an existing template alarm.</summary>
/// <param name="alarm">The alarm to update.</param> /// <param name="alarm">The alarm to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default); Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
/// <summary>Deletes a template alarm by ID.</summary> /// <summary>Deletes a template alarm by ID.</summary>
/// <param name="id">The alarm ID.</param> /// <param name="id">The alarm ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default); Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default);
// TemplateNativeAlarmSource // TemplateNativeAlarmSource
/// <summary>Retrieves a template native alarm source by ID.</summary> /// <summary>Retrieves a template native alarm source by ID.</summary>
/// <param name="id">The native alarm source ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching native alarm source, or <see langword="null"/> if not found.</returns>
Task<TemplateNativeAlarmSource?> GetTemplateNativeAlarmSourceByIdAsync(int id, CancellationToken cancellationToken = default); Task<TemplateNativeAlarmSource?> GetTemplateNativeAlarmSourceByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves native alarm sources for a template.</summary> /// <summary>Retrieves native alarm sources for a template.</summary>
/// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of native alarm sources for the specified template.</returns>
Task<IReadOnlyList<TemplateNativeAlarmSource>> GetNativeAlarmSourcesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TemplateNativeAlarmSource>> GetNativeAlarmSourcesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template native alarm source.</summary> /// <summary>Adds a new template native alarm source.</summary>
/// <param name="source">The native alarm source to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default); Task AddTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template native alarm source.</summary> /// <summary>Updates an existing template native alarm source.</summary>
/// <param name="source">The native alarm source to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default); Task UpdateTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default);
/// <summary>Deletes a template native alarm source by ID.</summary> /// <summary>Deletes a template native alarm source by ID.</summary>
/// <param name="id">The native alarm source ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateNativeAlarmSourceAsync(int id, CancellationToken cancellationToken = default); Task DeleteTemplateNativeAlarmSourceAsync(int id, CancellationToken cancellationToken = default);
// TemplateScript // TemplateScript
/// <summary>Retrieves a template script by ID.</summary> /// <summary>Retrieves a template script by ID.</summary>
/// <param name="id">The script ID.</param> /// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching script, or <see langword="null"/> if not found.</returns>
Task<TemplateScript?> GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default); Task<TemplateScript?> GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves scripts for a template.</summary> /// <summary>Retrieves scripts for a template.</summary>
/// <param name="templateId">The template ID.</param> /// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of scripts for the specified template.</returns>
Task<IReadOnlyList<TemplateScript>> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TemplateScript>> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template script.</summary> /// <summary>Adds a new template script.</summary>
/// <param name="script">The script to add.</param> /// <param name="script">The script to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default); Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template script.</summary> /// <summary>Updates an existing template script.</summary>
/// <param name="script">The script to update.</param> /// <param name="script">The script to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default); Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
/// <summary>Deletes a template script by ID.</summary> /// <summary>Deletes a template script by ID.</summary>
/// <param name="id">The script ID.</param> /// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default); Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default);
// TemplateComposition // TemplateComposition
/// <summary>Retrieves a template composition by ID.</summary> /// <summary>Retrieves a template composition by ID.</summary>
/// <param name="id">The composition ID.</param> /// <param name="id">The composition ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching composition, or <see langword="null"/> if not found.</returns>
Task<TemplateComposition?> GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default); Task<TemplateComposition?> GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves compositions for a template.</summary> /// <summary>Retrieves compositions for a template.</summary>
/// <param name="templateId">The template ID.</param> /// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of compositions for the specified template.</returns>
Task<IReadOnlyList<TemplateComposition>> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); Task<IReadOnlyList<TemplateComposition>> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template composition.</summary> /// <summary>Adds a new template composition.</summary>
/// <param name="composition">The composition to add.</param> /// <param name="composition">The composition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default); Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template composition.</summary> /// <summary>Updates an existing template composition.</summary>
/// <param name="composition">The composition to update.</param> /// <param name="composition">The composition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default); Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
/// <summary>Deletes a template composition by ID.</summary> /// <summary>Deletes a template composition by ID.</summary>
/// <param name="id">The composition ID.</param> /// <param name="id">The composition ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default); Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default);
// Instance // Instance
/// <summary>Retrieves an instance by ID.</summary> /// <summary>Retrieves an instance by ID.</summary>
/// <param name="id">The instance ID.</param> /// <param name="id">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching instance, or <see langword="null"/> if not found.</returns>
Task<Instance?> GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default); Task<Instance?> GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all instances.</summary> /// <summary>Retrieves all instances.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all instances.</returns>
Task<IReadOnlyList<Instance>> GetAllInstancesAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<Instance>> GetAllInstancesAsync(CancellationToken cancellationToken = default);
/// <summary>Retrieves instances for a template.</summary> /// <summary>Retrieves instances for a template.</summary>
/// <param name="templateId">The template ID.</param> /// <param name="templateId">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of instances for the specified template.</returns>
Task<IReadOnlyList<Instance>> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default); Task<IReadOnlyList<Instance>> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Retrieves instances for a site.</summary> /// <summary>Retrieves instances for a site.</summary>
/// <param name="siteId">The site ID.</param> /// <param name="siteId">The site ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of instances for the specified site.</returns>
Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Retrieves an instance by unique name.</summary> /// <summary>Retrieves an instance by unique name.</summary>
/// <param name="uniqueName">The unique instance name.</param> /// <param name="uniqueName">The unique instance name.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching instance, or <see langword="null"/> if not found.</returns>
Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default); Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance.</summary> /// <summary>Adds a new instance.</summary>
/// <param name="instance">The instance to add.</param> /// <param name="instance">The instance to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default); Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance.</summary> /// <summary>Updates an existing instance.</summary>
/// <param name="instance">The instance to update.</param> /// <param name="instance">The instance to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default); Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance by ID.</summary> /// <summary>Deletes an instance by ID.</summary>
/// <param name="id">The instance ID.</param> /// <param name="id">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default); Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default);
// InstanceAttributeOverride // InstanceAttributeOverride
/// <summary>Retrieves attribute overrides for an instance.</summary> /// <summary>Retrieves attribute overrides for an instance.</summary>
/// <param name="instanceId">The instance ID.</param> /// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of attribute overrides for the specified instance.</returns>
Task<IReadOnlyList<InstanceAttributeOverride>> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default); Task<IReadOnlyList<InstanceAttributeOverride>> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance attribute override.</summary> /// <summary>Adds a new instance attribute override.</summary>
/// <param name="attributeOverride">The override to add.</param> /// <param name="attributeOverride">The override to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default); Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance attribute override.</summary> /// <summary>Updates an existing instance attribute override.</summary>
/// <param name="attributeOverride">The override to update.</param> /// <param name="attributeOverride">The override to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default); Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance attribute override by ID.</summary> /// <summary>Deletes an instance attribute override by ID.</summary>
/// <param name="id">The override ID.</param> /// <param name="id">The override ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default); Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default);
// InstanceAlarmOverride // InstanceAlarmOverride
/// <summary>Retrieves alarm overrides for an instance.</summary> /// <summary>Retrieves alarm overrides for an instance.</summary>
/// <param name="instanceId">The instance ID.</param> /// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of alarm overrides for the specified instance.</returns>
Task<IReadOnlyList<InstanceAlarmOverride>> GetAlarmOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default); Task<IReadOnlyList<InstanceAlarmOverride>> GetAlarmOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Retrieves an alarm override by instance and alarm name.</summary> /// <summary>Retrieves an alarm override by instance and alarm name.</summary>
/// <param name="instanceId">The instance ID.</param> /// <param name="instanceId">The instance ID.</param>
/// <param name="alarmCanonicalName">The alarm canonical name.</param> /// <param name="alarmCanonicalName">The alarm canonical name.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching alarm override, or <see langword="null"/> if not found.</returns>
Task<InstanceAlarmOverride?> GetAlarmOverrideAsync(int instanceId, string alarmCanonicalName, CancellationToken cancellationToken = default); Task<InstanceAlarmOverride?> GetAlarmOverrideAsync(int instanceId, string alarmCanonicalName, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance alarm override.</summary> /// <summary>Adds a new instance alarm override.</summary>
/// <param name="alarmOverride">The override to add.</param> /// <param name="alarmOverride">The override to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default); Task AddInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance alarm override.</summary> /// <summary>Updates an existing instance alarm override.</summary>
/// <param name="alarmOverride">The override to update.</param> /// <param name="alarmOverride">The override to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default); Task UpdateInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance alarm override by ID.</summary> /// <summary>Deletes an instance alarm override by ID.</summary>
/// <param name="id">The override ID.</param> /// <param name="id">The override ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteInstanceAlarmOverrideAsync(int id, CancellationToken cancellationToken = default); Task DeleteInstanceAlarmOverrideAsync(int id, CancellationToken cancellationToken = default);
// InstanceNativeAlarmSourceOverride // InstanceNativeAlarmSourceOverride
/// <summary>Retrieves native alarm source overrides for an instance.</summary> /// <summary>Retrieves native alarm source overrides for an instance.</summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of native alarm source overrides for the specified instance.</returns>
Task<IReadOnlyList<InstanceNativeAlarmSourceOverride>> GetNativeAlarmSourceOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default); Task<IReadOnlyList<InstanceNativeAlarmSourceOverride>> GetNativeAlarmSourceOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Retrieves a single native alarm source override by instance + source canonical name.</summary> /// <summary>Retrieves a single native alarm source override by instance + source canonical name.</summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="sourceCanonicalName">The canonical name of the native alarm source.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching override, or <see langword="null"/> if not found.</returns>
Task<InstanceNativeAlarmSourceOverride?> GetNativeAlarmSourceOverrideAsync(int instanceId, string sourceCanonicalName, CancellationToken cancellationToken = default); Task<InstanceNativeAlarmSourceOverride?> GetNativeAlarmSourceOverrideAsync(int instanceId, string sourceCanonicalName, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance native alarm source override.</summary> /// <summary>Adds a new instance native alarm source override.</summary>
/// <param name="ovr">The override to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default); Task AddInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance native alarm source override.</summary> /// <summary>Updates an existing instance native alarm source override.</summary>
/// <param name="ovr">The override to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default); Task UpdateInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance native alarm source override by ID.</summary> /// <summary>Deletes an instance native alarm source override by ID.</summary>
/// <param name="id">The override ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteInstanceNativeAlarmSourceOverrideAsync(int id, CancellationToken cancellationToken = default); Task DeleteInstanceNativeAlarmSourceOverrideAsync(int id, CancellationToken cancellationToken = default);
// InstanceConnectionBinding // InstanceConnectionBinding
/// <summary>Retrieves connection bindings for an instance.</summary> /// <summary>Retrieves connection bindings for an instance.</summary>
/// <param name="instanceId">The instance ID.</param> /// <param name="instanceId">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of connection bindings for the specified instance.</returns>
Task<IReadOnlyList<InstanceConnectionBinding>> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default); Task<IReadOnlyList<InstanceConnectionBinding>> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance connection binding.</summary> /// <summary>Adds a new instance connection binding.</summary>
/// <param name="binding">The binding to add.</param> /// <param name="binding">The binding to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default); Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance connection binding.</summary> /// <summary>Updates an existing instance connection binding.</summary>
/// <param name="binding">The binding to update.</param> /// <param name="binding">The binding to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default); Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance connection binding by ID.</summary> /// <summary>Deletes an instance connection binding by ID.</summary>
/// <param name="id">The binding ID.</param> /// <param name="id">The binding ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default); Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default);
// Area // Area
/// <summary>Retrieves an area by ID.</summary> /// <summary>Retrieves an area by ID.</summary>
/// <param name="id">The area ID.</param> /// <param name="id">The area ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching area, or <see langword="null"/> if not found.</returns>
Task<Area?> GetAreaByIdAsync(int id, CancellationToken cancellationToken = default); Task<Area?> GetAreaByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves areas for a site.</summary> /// <summary>Retrieves areas for a site.</summary>
/// <param name="siteId">The site ID.</param> /// <param name="siteId">The site ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of areas for the specified site.</returns>
Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
/// <summary>Adds a new area.</summary> /// <summary>Adds a new area.</summary>
/// <param name="area">The area to add.</param> /// <param name="area">The area to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddAreaAsync(Area area, CancellationToken cancellationToken = default); Task AddAreaAsync(Area area, CancellationToken cancellationToken = default);
/// <summary>Updates an existing area.</summary> /// <summary>Updates an existing area.</summary>
/// <param name="area">The area to update.</param> /// <param name="area">The area to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default); Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default);
/// <summary>Deletes an area by ID.</summary> /// <summary>Deletes an area by ID.</summary>
/// <param name="id">The area ID.</param> /// <param name="id">The area ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default); Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default);
// SharedScript // SharedScript
/// <summary>Retrieves a shared script by ID.</summary> /// <summary>Retrieves a shared script by ID.</summary>
/// <param name="id">The script ID.</param> /// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching shared script, or <see langword="null"/> if not found.</returns>
Task<SharedScript?> GetSharedScriptByIdAsync(int id, CancellationToken cancellationToken = default); Task<SharedScript?> GetSharedScriptByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves a shared script by name.</summary> /// <summary>Retrieves a shared script by name.</summary>
/// <param name="name">The script name.</param> /// <param name="name">The script name.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching shared script, or <see langword="null"/> if not found.</returns>
Task<SharedScript?> GetSharedScriptByNameAsync(string name, CancellationToken cancellationToken = default); Task<SharedScript?> GetSharedScriptByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>Retrieves all shared scripts.</summary> /// <summary>Retrieves all shared scripts.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all shared scripts.</returns>
Task<IReadOnlyList<SharedScript>> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<SharedScript>> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default);
/// <summary>Adds a new shared script.</summary> /// <summary>Adds a new shared script.</summary>
/// <param name="sharedScript">The script to add.</param> /// <param name="sharedScript">The script to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default); Task AddSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default);
/// <summary>Updates an existing shared script.</summary> /// <summary>Updates an existing shared script.</summary>
/// <param name="sharedScript">The script to update.</param> /// <param name="sharedScript">The script to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default); Task UpdateSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default);
/// <summary>Deletes a shared script by ID.</summary> /// <summary>Deletes a shared script by ID.</summary>
/// <param name="id">The script ID.</param> /// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteSharedScriptAsync(int id, CancellationToken cancellationToken = default); Task DeleteSharedScriptAsync(int id, CancellationToken cancellationToken = default);
// TemplateFolder // TemplateFolder
/// <summary>Retrieves a template folder by ID.</summary> /// <summary>Retrieves a template folder by ID.</summary>
/// <param name="id">The folder ID.</param> /// <param name="id">The folder ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the matching folder, or <see langword="null"/> if not found.</returns>
Task<TemplateFolder?> GetFolderByIdAsync(int id, CancellationToken cancellationToken = default); Task<TemplateFolder?> GetFolderByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Retrieves all template folders.</summary> /// <summary>Retrieves all template folders.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a read-only list of all template folders.</returns>
Task<IReadOnlyList<TemplateFolder>> GetAllFoldersAsync(CancellationToken cancellationToken = default); Task<IReadOnlyList<TemplateFolder>> GetAllFoldersAsync(CancellationToken cancellationToken = default);
/// <summary>Adds a new template folder.</summary> /// <summary>Adds a new template folder.</summary>
/// <param name="folder">The folder to add.</param> /// <param name="folder">The folder to add.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default); Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template folder.</summary> /// <summary>Updates an existing template folder.</summary>
/// <param name="folder">The folder to update.</param> /// <param name="folder">The folder to update.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default); Task UpdateFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default);
/// <summary>Deletes a template folder by ID.</summary> /// <summary>Deletes a template folder by ID.</summary>
/// <param name="id">The folder ID.</param> /// <param name="id">The folder ID.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteFolderAsync(int id, CancellationToken cancellationToken = default); Task DeleteFolderAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the database.</summary> /// <summary>Saves pending changes to the database.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the number of rows written to the database.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
} }
@@ -45,31 +45,54 @@ public interface IInboundApiKeyAdmin
{ {
/// <summary>Creates a new key scoped to <paramref name="methods"/> and returns its /// <summary>Creates a new key scoped to <paramref name="methods"/> and returns its
/// identifier plus the bearer token (shown once).</summary> /// identifier plus the bearer token (shown once).</summary>
/// <param name="name">Operator-facing display name for the new key.</param>
/// <param name="methods">API method names the key is permitted to call.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the new key identifier and the one-time bearer token.</returns>
Task<InboundApiKeyCreated> CreateAsync( Task<InboundApiKeyCreated> CreateAsync(
string name, IReadOnlyCollection<string> methods, CancellationToken ct = default); string name, IReadOnlyCollection<string> methods, CancellationToken ct = default);
/// <summary>Lists all inbound keys (hash-free projection).</summary> /// <summary>Lists all inbound keys (hash-free projection).</summary>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the full list of inbound API key records.</returns>
Task<IReadOnlyList<InboundApiKeyInfo>> ListAsync(CancellationToken ct = default); Task<IReadOnlyList<InboundApiKeyInfo>> ListAsync(CancellationToken ct = default);
/// <summary>Enables or disables a key without changing its secret. Returns false if /// <summary>Enables or disables a key without changing its secret. Returns false if
/// the key does not exist.</summary> /// the key does not exist.</summary>
/// <param name="keyId">Identifier of the key to update.</param>
/// <param name="enabled">True to enable the key; false to disable it.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to <c>true</c> if the key was updated; <c>false</c> if the key does not exist.</returns>
Task<bool> SetEnabledAsync(string keyId, bool enabled, CancellationToken ct = default); Task<bool> SetEnabledAsync(string keyId, bool enabled, CancellationToken ct = default);
/// <summary>Replaces the method-scope set on a key without changing its secret. /// <summary>Replaces the method-scope set on a key without changing its secret.
/// Returns false if the key does not exist.</summary> /// Returns false if the key does not exist.</summary>
/// <param name="keyId">Identifier of the key to update.</param>
/// <param name="methods">Replacement set of API method names the key may call.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to <c>true</c> if the key's method scope was replaced; <c>false</c> if the key does not exist.</returns>
Task<bool> SetMethodsAsync( Task<bool> SetMethodsAsync(
string keyId, IReadOnlyCollection<string> methods, CancellationToken ct = default); string keyId, IReadOnlyCollection<string> methods, CancellationToken ct = default);
/// <summary>Removes a key (revoke-then-delete). Returns false if the key could not be /// <summary>Removes a key (revoke-then-delete). Returns false if the key could not be
/// deleted.</summary> /// deleted.</summary>
/// <param name="keyId">Identifier of the key to delete.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to <c>true</c> if the key was deleted; <c>false</c> if it could not be deleted.</returns>
Task<bool> DeleteAsync(string keyId, CancellationToken ct = default); Task<bool> DeleteAsync(string keyId, CancellationToken ct = default);
/// <summary>Returns the method-scope set for a key, or an empty list if not found.</summary> /// <summary>Returns the method-scope set for a key, or an empty list if not found.</summary>
/// <remarks>Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths.</remarks> /// <remarks>Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths.</remarks>
/// <param name="keyId">Identifier of the key whose method scope to retrieve.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the method names the key is scoped to, or an empty list if the key does not exist.</returns>
Task<IReadOnlyList<string>> GetMethodsForKeyAsync(string keyId, CancellationToken ct = default); Task<IReadOnlyList<string>> GetMethodsForKeyAsync(string keyId, CancellationToken ct = default);
/// <summary>Returns the identifiers of all keys whose scopes contain /// <summary>Returns the identifiers of all keys whose scopes contain
/// <paramref name="methodName"/>.</summary> /// <paramref name="methodName"/>.</summary>
/// <remarks>Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths.</remarks> /// <remarks>Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths.</remarks>
/// <param name="methodName">API method name to search for across all key scopes.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the identifiers of all keys whose scopes include <paramref name="methodName"/>.</returns>
Task<IReadOnlyList<string>> GetKeysForMethodAsync(string methodName, CancellationToken ct = default); Task<IReadOnlyList<string>> GetKeysForMethodAsync(string methodName, CancellationToken ct = default);
} }
@@ -12,5 +12,6 @@ public interface IAuditService
/// <param name="entityName">The display name of the affected entity.</param> /// <param name="entityName">The display name of the affected entity.</param>
/// <param name="afterState">The entity state after the action; may be null for deletes.</param> /// <param name="afterState">The entity state after the action; may be null for deletes.</param>
/// <param name="cancellationToken">Cancellation token for the log write.</param> /// <param name="cancellationToken">Cancellation token for the log write.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task LogAsync(string user, string action, string entityType, string entityId, string entityName, object? afterState, CancellationToken cancellationToken = default); Task LogAsync(string user, string action, string entityType, string entityId, string entityName, object? afterState, CancellationToken cancellationToken = default);
} }
@@ -21,5 +21,6 @@ public interface IAuditWriter
/// </summary> /// </summary>
/// <param name="evt">The audit event to persist.</param> /// <param name="evt">The audit event to persist.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task representing the asynchronous write operation.</returns>
Task WriteAsync(AuditEvent evt, CancellationToken ct = default); Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
} }
@@ -36,6 +36,7 @@ public interface ICachedCallLifecycleObserver
/// </summary> /// </summary>
/// <param name="context">Per-attempt context including the tracking id, outcome, and audit provenance fields.</param> /// <param name="context">Per-attempt context including the tracking id, outcome, and audit provenance fields.</param>
/// <param name="ct">Cancellation token for the observation operation.</param> /// <param name="ct">Cancellation token for the observation operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task OnAttemptCompletedAsync(CachedCallAttemptContext context, CancellationToken ct = default); Task OnAttemptCompletedAsync(CachedCallAttemptContext context, CancellationToken ct = default);
} }
@@ -32,5 +32,6 @@ public interface ICachedCallTelemetryForwarder
/// </summary> /// </summary>
/// <param name="telemetry">The combined-telemetry packet to fan out.</param> /// <param name="telemetry">The combined-telemetry packet to fan out.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default); Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default);
} }
@@ -14,5 +14,6 @@ public interface ICentralAuditWriter
/// </summary> /// </summary>
/// <param name="evt">The audit event to persist.</param> /// <param name="evt">The audit event to persist.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task WriteAsync(AuditEvent evt, CancellationToken ct = default); Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
} }
@@ -16,6 +16,7 @@ public interface IDatabaseGateway
/// </summary> /// </summary>
/// <param name="connectionName">Name of the configured database connection to open.</param> /// <param name="connectionName">Name of the configured database connection to open.</param>
/// <param name="cancellationToken">Cancellation token for the async open operation.</param> /// <param name="cancellationToken">Cancellation token for the async open operation.</param>
/// <returns>A task that resolves to an open <see cref="DbConnection"/>; the caller is responsible for disposing it.</returns>
Task<DbConnection> GetConnectionAsync( Task<DbConnection> GetConnectionAsync(
string connectionName, string connectionName,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -55,6 +56,7 @@ public interface IDatabaseGateway
/// <param name="parameters">Optional SQL parameters for the statement.</param> /// <param name="parameters">Optional SQL parameters for the statement.</param>
/// <param name="originInstanceName">Optional name of the instance that originated the write.</param> /// <param name="originInstanceName">Optional name of the instance that originated the write.</param>
/// <param name="cancellationToken">Cancellation token for the buffering operation.</param> /// <param name="cancellationToken">Cancellation token for the buffering operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task CachedWriteAsync( Task CachedWriteAsync(
string connectionName, string connectionName,
string sql, string sql,
@@ -12,6 +12,7 @@ public interface IInstanceLocator
/// </summary> /// </summary>
/// <param name="instanceUniqueName">System-wide unique name of the instance to look up.</param> /// <param name="instanceUniqueName">System-wide unique name of the instance to look up.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the site identifier for the instance, or <c>null</c> if the instance is not found.</returns>
Task<string?> GetSiteIdForInstanceAsync( Task<string?> GetSiteIdForInstanceAsync(
string instanceUniqueName, string instanceUniqueName,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
@@ -47,6 +47,7 @@ public interface IOperationTrackingStore
/// <param name="sourceScript">Optional name of the source script.</param> /// <param name="sourceScript">Optional name of the source script.</param>
/// <param name="sourceNode">Optional source node identifier.</param> /// <param name="sourceNode">Optional source node identifier.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecordEnqueueAsync( Task RecordEnqueueAsync(
TrackedOperationId id, TrackedOperationId id,
string kind, string kind,
@@ -68,6 +69,7 @@ public interface IOperationTrackingStore
/// <param name="lastError">Optional error message from the last attempt.</param> /// <param name="lastError">Optional error message from the last attempt.</param>
/// <param name="httpStatus">Optional HTTP status code from the last attempt.</param> /// <param name="httpStatus">Optional HTTP status code from the last attempt.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecordAttemptAsync( Task RecordAttemptAsync(
TrackedOperationId id, TrackedOperationId id,
string status, string status,
@@ -86,6 +88,7 @@ public interface IOperationTrackingStore
/// <param name="lastError">Optional final error message.</param> /// <param name="lastError">Optional final error message.</param>
/// <param name="httpStatus">Optional final HTTP status code.</param> /// <param name="httpStatus">Optional final HTTP status code.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecordTerminalAsync( Task RecordTerminalAsync(
TrackedOperationId id, TrackedOperationId id,
string status, string status,
@@ -111,6 +114,7 @@ public interface IOperationTrackingStore
/// </summary> /// </summary>
/// <param name="olderThanUtc">Cutoff timestamp; rows terminal before this are deleted.</param> /// <param name="olderThanUtc">Cutoff timestamp; rows terminal before this are deleted.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task PurgeTerminalAsync( Task PurgeTerminalAsync(
DateTime olderThanUtc, DateTime olderThanUtc,
CancellationToken ct = default); CancellationToken ct = default);
@@ -44,6 +44,7 @@ public interface IPartitionMaintenance
/// </summary> /// </summary>
/// <param name="lookaheadMonths">Number of future monthly boundaries to ensure exist.</param> /// <param name="lookaheadMonths">Number of future monthly boundaries to ensure exist.</param>
/// <param name="ct">Cancellation token for the SQL operation.</param> /// <param name="ct">Cancellation token for the SQL operation.</param>
/// <returns>A task that resolves to the list of boundary values actually added, in chronological order.</returns>
Task<IReadOnlyList<DateTime>> EnsureLookaheadAsync(int lookaheadMonths, CancellationToken ct = default); Task<IReadOnlyList<DateTime>> EnsureLookaheadAsync(int lookaheadMonths, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -53,5 +54,6 @@ public interface IPartitionMaintenance
/// has no boundaries. /// has no boundaries.
/// </summary> /// </summary>
/// <param name="ct">Cancellation token for the SQL operation.</param> /// <param name="ct">Cancellation token for the SQL operation.</param>
/// <returns>A task that resolves to the highest boundary value, or <c>null</c> when the partition function has no boundaries.</returns>
Task<DateTime?> GetMaxBoundaryAsync(CancellationToken ct = default); Task<DateTime?> GetMaxBoundaryAsync(CancellationToken ct = default);
} }
@@ -47,6 +47,7 @@ public interface ISiteAuditQueue
/// </remarks> /// </remarks>
/// <param name="limit">Maximum number of rows to return.</param> /// <param name="limit">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the oldest pending non-cached audit events, up to <paramref name="limit"/>.</returns>
Task<IReadOnlyList<AuditEvent>> ReadPendingAsync(int limit, CancellationToken ct = default); Task<IReadOnlyList<AuditEvent>> ReadPendingAsync(int limit, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -77,6 +78,7 @@ public interface ISiteAuditQueue
/// </remarks> /// </remarks>
/// <param name="limit">Maximum number of rows to return.</param> /// <param name="limit">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the oldest pending cached-lifecycle audit events, up to <paramref name="limit"/>.</returns>
Task<IReadOnlyList<AuditEvent>> ReadPendingCachedTelemetryAsync(int limit, CancellationToken ct = default); Task<IReadOnlyList<AuditEvent>> ReadPendingCachedTelemetryAsync(int limit, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -87,6 +89,7 @@ public interface ISiteAuditQueue
/// </summary> /// </summary>
/// <param name="eventIds">Event IDs to mark as forwarded.</param> /// <param name="eventIds">Event IDs to mark as forwarded.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task MarkForwardedAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default); Task MarkForwardedAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -107,6 +110,7 @@ public interface ISiteAuditQueue
/// <param name="sinceUtc">Lower bound timestamp (UTC).</param> /// <param name="sinceUtc">Lower bound timestamp (UTC).</param>
/// <param name="batchSize">Maximum number of rows to return.</param> /// <param name="batchSize">Maximum number of rows to return.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to audit events at or after <paramref name="sinceUtc"/> in pending or forwarded state, up to <paramref name="batchSize"/>.</returns>
Task<IReadOnlyList<AuditEvent>> ReadPendingSinceAsync( Task<IReadOnlyList<AuditEvent>> ReadPendingSinceAsync(
DateTime sinceUtc, int batchSize, CancellationToken ct = default); DateTime sinceUtc, int batchSize, CancellationToken ct = default);
@@ -121,6 +125,7 @@ public interface ISiteAuditQueue
/// </summary> /// </summary>
/// <param name="eventIds">Event IDs to mark as reconciled.</param> /// <param name="eventIds">Event IDs to mark as reconciled.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task MarkReconciledAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default); Task MarkReconciledAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -135,5 +140,6 @@ public interface ISiteAuditQueue
/// the hot-path INSERT batch and the drain queries. /// the hot-path INSERT batch and the drain queries.
/// </summary> /// </summary>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a point-in-time snapshot of the site audit queue's pending count, oldest timestamp, and on-disk file size.</returns>
Task<SiteAuditBacklogSnapshot> GetBacklogStatsAsync(CancellationToken ct = default); Task<SiteAuditBacklogSnapshot> GetBacklogStatsAsync(CancellationToken ct = default);
} }
@@ -12,6 +12,7 @@ public interface IBundleExporter
/// <param name="sourceEnvironment">Environment label stamped in the bundle manifest.</param> /// <param name="sourceEnvironment">Environment label stamped in the bundle manifest.</param>
/// <param name="passphrase">Optional passphrase to encrypt the bundle; null produces an unencrypted bundle.</param> /// <param name="passphrase">Optional passphrase to encrypt the bundle; null produces an unencrypted bundle.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a seeked-to-start stream containing the bundle ZIP archive.</returns>
Task<Stream> ExportAsync( Task<Stream> ExportAsync(
ExportSelection selection, ExportSelection selection,
string user, string user,
@@ -10,6 +10,7 @@ public interface IBundleImporter
/// <param name="bundleStream">Stream containing the bundle zip archive.</param> /// <param name="bundleStream">Stream containing the bundle zip archive.</param>
/// <param name="passphrase">Optional passphrase for decrypting an encrypted bundle.</param> /// <param name="passphrase">Optional passphrase for decrypting an encrypted bundle.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to session metadata for the loaded bundle.</returns>
Task<BundleSession> LoadAsync(Stream bundleStream, string? passphrase, CancellationToken ct = default); Task<BundleSession> LoadAsync(Stream bundleStream, string? passphrase, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -17,6 +18,7 @@ public interface IBundleImporter
/// </summary> /// </summary>
/// <param name="sessionId">Session id returned by <see cref="LoadAsync"/>.</param> /// <param name="sessionId">Session id returned by <see cref="LoadAsync"/>.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to a per-artifact import preview with conflict details.</returns>
Task<ImportPreview> PreviewAsync(Guid sessionId, CancellationToken ct = default); Task<ImportPreview> PreviewAsync(Guid sessionId, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -26,6 +28,7 @@ public interface IBundleImporter
/// <param name="resolutions">Per-artifact conflict resolutions from the preview step.</param> /// <param name="resolutions">Per-artifact conflict resolutions from the preview step.</param>
/// <param name="user">Username of the operator performing the import, stamped in audit rows.</param> /// <param name="user">Username of the operator performing the import, stamped in audit rows.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the result of the committed import transaction.</returns>
Task<ImportResult> ApplyAsync( Task<ImportResult> ApplyAsync(
Guid sessionId, Guid sessionId,
IReadOnlyList<ImportResolution> resolutions, IReadOnlyList<ImportResolution> resolutions,
@@ -6,9 +6,11 @@ public interface IBundleSessionStore
{ {
/// <summary>Stores the session and returns it; overwrites any existing session with the same id.</summary> /// <summary>Stores the session and returns it; overwrites any existing session with the same id.</summary>
/// <param name="session">The session to store.</param> /// <param name="session">The session to store.</param>
/// <returns>The stored session (same reference as <paramref name="session"/>).</returns>
BundleSession Open(BundleSession session); BundleSession Open(BundleSession session);
/// <summary>Returns the session for the given id, or null if not found or expired.</summary> /// <summary>Returns the session for the given id, or null if not found or expired.</summary>
/// <param name="sessionId">The session identifier to look up.</param> /// <param name="sessionId">The session identifier to look up.</param>
/// <returns>The matching <see cref="BundleSession"/>, or null if not found or expired.</returns>
BundleSession? Get(Guid sessionId); BundleSession? Get(Guid sessionId);
/// <summary>Removes the session for the given id, if present.</summary> /// <summary>Removes the session for the given id, if present.</summary>
/// <param name="sessionId">The session identifier to remove.</param> /// <param name="sessionId">The session identifier to remove.</param>
@@ -31,6 +33,7 @@ public interface IBundleSessionStore
/// against identical bundle bytes are throttled regardless of client. /// against identical bundle bytes are throttled regardless of client.
/// </summary> /// </summary>
/// <param name="bundleContentHash">SHA-256 hex from <c>BundleManifest.ContentHash</c>.</param> /// <param name="bundleContentHash">SHA-256 hex from <c>BundleManifest.ContentHash</c>.</param>
/// <returns>The new unlock-failure count after incrementing.</returns>
int IncrementUnlockFailureCount(string bundleContentHash); int IncrementUnlockFailureCount(string bundleContentHash);
/// <summary> /// <summary>
@@ -29,6 +29,7 @@ public static class ManagementCommandRegistry
/// Resolves a management command wire name to its CLR type, or null if not registered. /// Resolves a management command wire name to its CLR type, or null if not registered.
/// </summary> /// </summary>
/// <param name="commandName">The wire name of the management command (without the "Command" suffix).</param> /// <param name="commandName">The wire name of the management command (without the "Command" suffix).</param>
/// <returns>The CLR <see cref="Type"/> for the command, or <c>null</c> if not registered.</returns>
public static Type? Resolve(string commandName) public static Type? Resolve(string commandName)
{ {
return Commands.GetValueOrDefault(commandName); return Commands.GetValueOrDefault(commandName);
@@ -45,6 +46,7 @@ public static class ManagementCommandRegistry
/// symmetric with <see cref="Resolve"/>: it never yields a name that /// symmetric with <see cref="Resolve"/>: it never yields a name that
/// <see cref="Resolve"/> cannot turn back into the same type. /// <see cref="Resolve"/> cannot turn back into the same type.
/// </exception> /// </exception>
/// <returns>The registered wire name for <paramref name="commandType"/>.</returns>
public static string GetCommandName(Type commandType) public static string GetCommandName(Type commandType)
{ {
ArgumentNullException.ThrowIfNull(commandType); ArgumentNullException.ThrowIfNull(commandType);
@@ -21,11 +21,13 @@ public static class MxGatewayEndpointConfigSerializer
/// <summary>Serializes a config to the typed JSON shape.</summary> /// <summary>Serializes a config to the typed JSON shape.</summary>
/// <param name="config">The endpoint configuration to serialize.</param> /// <param name="config">The endpoint configuration to serialize.</param>
/// <returns>A camelCase JSON string representing the endpoint configuration.</returns>
public static string Serialize(MxGatewayEndpointConfig config) public static string Serialize(MxGatewayEndpointConfig config)
=> JsonSerializer.Serialize(config, JsonOpts); => JsonSerializer.Serialize(config, JsonOpts);
/// <summary>Parses stored config JSON; null/blank/malformed yields a default config.</summary> /// <summary>Parses stored config JSON; null/blank/malformed yields a default config.</summary>
/// <param name="json">The stored JSON string.</param> /// <param name="json">The stored JSON string.</param>
/// <returns>The deserialized <see cref="MxGatewayEndpointConfig"/>, or a default instance if the input is null, blank, or malformed.</returns>
public static MxGatewayEndpointConfig Deserialize(string? json) public static MxGatewayEndpointConfig Deserialize(string? json)
{ {
if (string.IsNullOrWhiteSpace(json)) return new MxGatewayEndpointConfig(); if (string.IsNullOrWhiteSpace(json)) return new MxGatewayEndpointConfig();
@@ -35,6 +37,7 @@ public static class MxGatewayEndpointConfigSerializer
/// <summary>Flattens the typed config to the key-value shape the adapter consumes.</summary> /// <summary>Flattens the typed config to the key-value shape the adapter consumes.</summary>
/// <param name="c">The endpoint configuration to flatten.</param> /// <param name="c">The endpoint configuration to flatten.</param>
/// <returns>A string-keyed dictionary containing all endpoint configuration properties.</returns>
public static IDictionary<string, string> ToFlatDict(MxGatewayEndpointConfig c) => new Dictionary<string, string> public static IDictionary<string, string> ToFlatDict(MxGatewayEndpointConfig c) => new Dictionary<string, string>
{ {
["Endpoint"] = c.Endpoint, ["Endpoint"] = c.Endpoint,
@@ -49,6 +52,7 @@ public static class MxGatewayEndpointConfigSerializer
/// <summary>Reconstructs a config from the flat key-value shape; invalid numerics fall back to defaults.</summary> /// <summary>Reconstructs a config from the flat key-value shape; invalid numerics fall back to defaults.</summary>
/// <param name="d">The flat dictionary.</param> /// <param name="d">The flat dictionary.</param>
/// <returns>A <see cref="MxGatewayEndpointConfig"/> populated from the dictionary; missing or invalid entries use default values.</returns>
public static MxGatewayEndpointConfig FromFlatDict(IDictionary<string, string> d) public static MxGatewayEndpointConfig FromFlatDict(IDictionary<string, string> d)
{ {
var c = new MxGatewayEndpointConfig(); var c = new MxGatewayEndpointConfig();
@@ -118,6 +118,7 @@ public static class OpcUaEndpointConfigSerializer
/// </list> /// </list>
/// </summary> /// </summary>
/// <param name="json">The stored JSON string to parse; null or blank yields a default typed result.</param> /// <param name="json">The stored JSON string to parse; null or blank yields a default typed result.</param>
/// <returns>An <see cref="OpcUaConfigParseResult"/> containing the parsed config and the detected parse status.</returns>
public static OpcUaConfigParseResult Deserialize(string? json) public static OpcUaConfigParseResult Deserialize(string? json)
{ {
if (string.IsNullOrWhiteSpace(json)) if (string.IsNullOrWhiteSpace(json))
@@ -175,6 +176,7 @@ public static class OpcUaEndpointConfigSerializer
/// used by OpcUaDataConnection so the adapter can keep that interface. /// used by OpcUaDataConnection so the adapter can keep that interface.
/// </summary> /// </summary>
/// <param name="config">The endpoint configuration to flatten.</param> /// <param name="config">The endpoint configuration to flatten.</param>
/// <returns>A dictionary mapping connection-parameter key names to their string values.</returns>
public static IDictionary<string, string> ToFlatDict(OpcUaEndpointConfig config) public static IDictionary<string, string> ToFlatDict(OpcUaEndpointConfig config)
{ {
var dict = new Dictionary<string, string> var dict = new Dictionary<string, string>
@@ -10,6 +10,9 @@ public static class AlarmConditionStateFactory
/// auto-acked, never shelved or suppressed, not confirmable, and their /// auto-acked, never shelved or suppressed, not confirmable, and their
/// severity is the configured priority. Active mirrors the alarm State. /// severity is the configured priority. Active mirrors the alarm State.
/// </summary> /// </summary>
/// <param name="state">The current alarm state used to derive the Active flag.</param>
/// <param name="priority">Configured priority mapped to the Severity field (01000).</param>
/// <returns>An <see cref="AlarmConditionState"/> reflecting the computed alarm's lifecycle (auto-acked, unshelved, not suppressed).</returns>
public static AlarmConditionState ForComputed(AlarmState state, int priority) => public static AlarmConditionState ForComputed(AlarmState state, int priority) =>
new(Active: state == AlarmState.Active, Acknowledged: true, Confirmed: null, new(Active: state == AlarmState.Active, Acknowledged: true, Confirmed: null,
Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: priority); Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: priority);
@@ -46,6 +46,8 @@ public static class AuditDetailsCodec
/// Serializes <paramref name="details"/> to a compact, deterministic JSON string /// Serializes <paramref name="details"/> to a compact, deterministic JSON string
/// suitable for storage in <c>AuditEvent.DetailsJson</c>. /// suitable for storage in <c>AuditEvent.DetailsJson</c>.
/// </summary> /// </summary>
/// <param name="details">The audit details instance to serialize.</param>
/// <returns>A compact, deterministic JSON string representing the audit details.</returns>
public static string Serialize(AuditDetails details) public static string Serialize(AuditDetails details)
=> JsonSerializer.Serialize(details, Options); => JsonSerializer.Serialize(details, Options);
@@ -54,6 +56,8 @@ public static class AuditDetailsCodec
/// Returns an empty (all-null) <see cref="AuditDetails"/> when <paramref name="json"/> /// Returns an empty (all-null) <see cref="AuditDetails"/> when <paramref name="json"/>
/// is <c>null</c>, empty, or whitespace — never throws. /// is <c>null</c>, empty, or whitespace — never throws.
/// </summary> /// </summary>
/// <param name="json">The JSON string to deserialize; null or whitespace returns an empty instance.</param>
/// <returns>The deserialized <see cref="AuditDetails"/>, or an empty instance on null/invalid input.</returns>
public static AuditDetails Deserialize(string? json) public static AuditDetails Deserialize(string? json)
{ {
if (string.IsNullOrWhiteSpace(json)) if (string.IsNullOrWhiteSpace(json))
@@ -22,12 +22,17 @@ public static class AuditFieldBuilders
/// <summary> /// <summary>
/// Returns the canonical <c>Action</c> string: <c>"{channel}.{kind}"</c>. /// Returns the canonical <c>Action</c> string: <c>"{channel}.{kind}"</c>.
/// </summary> /// </summary>
/// <param name="channel">The audit channel (e.g. ExternalSystem, Notification).</param>
/// <param name="kind">The audit kind (e.g. Sync, Cached, InboundAuthFailure).</param>
/// <returns>A dot-separated string in the form <c>"{channel}.{kind}"</c>.</returns>
public static string BuildAction(AuditChannel channel, AuditKind kind) public static string BuildAction(AuditChannel channel, AuditKind kind)
=> $"{channel}.{kind}"; => $"{channel}.{kind}";
/// <summary> /// <summary>
/// Returns the canonical <c>Category</c> string: the channel name. /// Returns the canonical <c>Category</c> string: the channel name.
/// </summary> /// </summary>
/// <param name="channel">The audit channel whose name becomes the category.</param>
/// <returns>The channel enum name as a string (e.g. <c>"ApiOutbound"</c>).</returns>
public static string BuildCategory(AuditChannel channel) public static string BuildCategory(AuditChannel channel)
=> channel.ToString(); => channel.ToString();
} }
@@ -26,6 +26,9 @@ public static class AuditOutcomeProjector
/// Projects <paramref name="status"/> + <paramref name="kind"/> onto the canonical /// Projects <paramref name="status"/> + <paramref name="kind"/> onto the canonical
/// <see cref="AuditOutcome"/>. /// <see cref="AuditOutcome"/>.
/// </summary> /// </summary>
/// <param name="status">The audit status of the operation.</param>
/// <param name="kind">The audit kind; <see cref="AuditKind.InboundAuthFailure"/> takes precedence over status.</param>
/// <returns>The projected <see cref="AuditOutcome"/> value.</returns>
public static AuditOutcome Project(AuditStatus status, AuditKind kind) public static AuditOutcome Project(AuditStatus status, AuditKind kind)
{ {
// Auth-failure kind takes absolute precedence — checked before any status rule. // Auth-failure kind takes absolute precedence — checked before any status rule.
@@ -64,6 +64,8 @@ public static class AuditRowProjection
/// <see cref="ScadaBridgeAuditEventFactory"/>); a missing/unparseable discriminator /// <see cref="ScadaBridgeAuditEventFactory"/>); a missing/unparseable discriminator
/// falls back to the first enum member (defensive — production rows always carry them). /// falls back to the first enum member (defensive — production rows always carry them).
/// </summary> /// </summary>
/// <param name="evt">The canonical audit event to decompose.</param>
/// <returns>An <see cref="AuditRowValues"/> struct containing the typed column values extracted from the event.</returns>
public static AuditRowValues Decompose(AuditEvent evt) public static AuditRowValues Decompose(AuditEvent evt)
{ {
ArgumentNullException.ThrowIfNull(evt); ArgumentNullException.ThrowIfNull(evt);
@@ -115,6 +117,8 @@ public static class AuditRowProjection
/// are rebuilt via the field builders / outcome projector, and every domain field is /// are rebuilt via the field builders / outcome projector, and every domain field is
/// re-serialized into <c>DetailsJson</c> via <see cref="AuditDetailsCodec"/>. /// re-serialized into <c>DetailsJson</c> via <see cref="AuditDetailsCodec"/>.
/// </summary> /// </summary>
/// <param name="v">The typed column values to recompose into a canonical event.</param>
/// <returns>A reconstructed canonical <see cref="AuditEvent"/> with domain fields re-serialized into <c>DetailsJson</c>.</returns>
public static AuditEvent Recompose(in AuditRowValues v) public static AuditEvent Recompose(in AuditRowValues v)
{ {
var details = new AuditDetails var details = new AuditDetails
@@ -163,6 +167,9 @@ public static class AuditRowProjection
/// record, so the central ingest paths stamp it here rather than on a top-level /// record, so the central ingest paths stamp it here rather than on a top-level
/// property as the legacy bespoke record allowed. /// property as the legacy bespoke record allowed.
/// </summary> /// </summary>
/// <param name="evt">The canonical audit event to update.</param>
/// <param name="ingestedAtUtc">The central-side ingest timestamp to stamp into the event.</param>
/// <returns>A new <see cref="AuditEvent"/> with <c>IngestedAtUtc</c> set inside <c>DetailsJson</c>.</returns>
public static AuditEvent WithIngestedAtUtc(AuditEvent evt, DateTimeOffset ingestedAtUtc) public static AuditEvent WithIngestedAtUtc(AuditEvent evt, DateTimeOffset ingestedAtUtc)
{ {
ArgumentNullException.ThrowIfNull(evt); ArgumentNullException.ThrowIfNull(evt);
@@ -179,6 +186,10 @@ public static class AuditRowProjection
/// or does not match any declared member name — so callers never throw on an /// or does not match any declared member name — so callers never throw on an
/// unknown/renamed enum string (legacy or corrupt rows degrade gracefully). /// unknown/renamed enum string (legacy or corrupt rows degrade gracefully).
/// </summary> /// </summary>
/// <typeparam name="TEnum">The enum type to parse into.</typeparam>
/// <param name="value">The string to parse; null or empty triggers the fallback.</param>
/// <param name="fallback">Value returned when <paramref name="value"/> is null, empty, or unrecognised.</param>
/// <returns>The parsed <typeparamref name="TEnum"/> value, or <paramref name="fallback"/> when the input is null, empty, or unrecognised.</returns>
public static TEnum ParseEnum<TEnum>(string? value, TEnum fallback) where TEnum : struct, Enum public static TEnum ParseEnum<TEnum>(string? value, TEnum fallback) where TEnum : struct, Enum
=> !string.IsNullOrEmpty(value) && Enum.TryParse<TEnum>(value, ignoreCase: false, out var parsed) => !string.IsNullOrEmpty(value) && Enum.TryParse<TEnum>(value, ignoreCase: false, out var parsed)
? parsed ? parsed
@@ -194,6 +205,8 @@ public static class AuditRowProjection
public static class AuditEventRowExtensions public static class AuditEventRowExtensions
{ {
/// <summary>Decomposes this canonical record into its typed 24-field view.</summary> /// <summary>Decomposes this canonical record into its typed 24-field view.</summary>
/// <param name="evt">The canonical audit event to decompose.</param>
/// <returns>An <see cref="AuditRowProjection.AuditRowValues"/> struct with all domain fields extracted from the event.</returns>
public static AuditRowProjection.AuditRowValues AsRow(this AuditEvent evt) public static AuditRowProjection.AuditRowValues AsRow(this AuditEvent evt)
=> AuditRowProjection.Decompose(evt); => AuditRowProjection.Decompose(evt);
} }
@@ -59,6 +59,7 @@ public static class ScadaBridgeAuditEventFactory
/// <param name="payloadTruncated">True when summaries were truncated to the payload cap (DetailsJson).</param> /// <param name="payloadTruncated">True when summaries were truncated to the payload cap (DetailsJson).</param>
/// <param name="extra">Free-form JSON extension for channel-specific extras (DetailsJson).</param> /// <param name="extra">Free-form JSON extension for channel-specific extras (DetailsJson).</param>
/// <param name="ingestedAtUtc">UTC ingest timestamp (central-set; DetailsJson).</param> /// <param name="ingestedAtUtc">UTC ingest timestamp (central-set; DetailsJson).</param>
/// <returns>A fully-populated <see cref="AuditEvent"/> with the top-level fields and serialized <c>DetailsJson</c> set.</returns>
public static AuditEvent Create( public static AuditEvent Create(
AuditChannel channel, AuditChannel channel,
AuditKind kind, AuditKind kind,

Some files were not shown because too many files have changed in this diff Show More