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));
}
/// <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)
{
// 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)
{
_cts?.Cancel();
return _loop ?? Task.CompletedTask;
}
/// <inheritdoc />
/// <summary>
/// Disposes the internal <see cref="CancellationTokenSource"/> used to stop the maintenance loop.
/// </summary>
public void 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="batchSize">Maximum number of events to return per call.</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(
string siteId,
DateTime sinceUtc,
@@ -23,6 +23,7 @@ public interface ISiteEnumerator
/// — the actor calls this once per tick.
/// </summary>
/// <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);
}
@@ -133,6 +133,7 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
/// Returns a defensive copy of the per-site latched stalled state.
/// Absent sites are interpreted as <c>Stalled=false</c> by consumers.
/// </summary>
/// <returns>A snapshot dictionary mapping each known site ID to its current stalled state.</returns>
public IReadOnlyDictionary<string, bool> Snapshot() =>
new Dictionary<string, bool>(_state);
@@ -71,6 +71,11 @@ internal static class AuditRedactionPrimitives
/// field is over-redacted with <see cref="RedactorErrorMarker"/> and
/// <paramref name="onFailure"/> is invoked.
/// </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(
string? json,
IList<string> redactList,
@@ -152,6 +157,11 @@ internal static class AuditRedactionPrimitives
/// with <see cref="RedactorErrorMarker"/> and <paramref name="onFailure"/>
/// is invoked — the user-facing action is never aborted.
/// </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(
string? value,
IReadOnlyList<Regex> regexes,
@@ -192,6 +202,11 @@ internal static class AuditRedactionPrimitives
/// is over-redacted with <see cref="RedactorErrorMarker"/> and
/// <paramref name="onFailure"/> is invoked.
/// </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(
string? json,
Regex paramNameRegex,
@@ -277,6 +292,10 @@ internal static class AuditRedactionPrimitives
/// setting <paramref name="truncated"/> to <c>true</c> when the value was
/// shortened. Null passes through as null.
/// </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)
{
if (value is null)
@@ -299,6 +318,9 @@ internal static class AuditRedactionPrimitives
/// (<c>byte &amp; 0xC0 == 0x80</c>), and decodes the resulting prefix —
/// guaranteeing the returned string never splits a multi-byte sequence.
/// </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)
{
if (string.IsNullOrEmpty(value))
@@ -35,6 +35,8 @@ internal sealed class AuditRegexCache
private readonly ConcurrentDictionary<string, CompiledRegex> _cache = new();
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;
/// <summary>
@@ -44,6 +46,9 @@ internal sealed class AuditRegexCache
/// compile time "invalid"); the failure is logged once and the sentinel
/// cache entry prevents repeat compile attempts.
/// </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)
{
var entry = _cache.GetOrAdd(pattern, Compile);
@@ -88,8 +93,11 @@ internal sealed class AuditRegexCache
{
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; }
/// <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;
}
}
@@ -37,7 +37,15 @@ public sealed class SafeDefaultAuditRedactor : IAuditRedactor
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)
{
ArgumentNullException.ThrowIfNull(rawEvent);
@@ -73,7 +73,12 @@ public sealed class ScadaBridgeAuditRedactor : IAuditRedactor
_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)
{
try
@@ -96,6 +96,7 @@ public sealed class RingBufferFallback
/// must call <see cref="Complete"/> first.
/// </summary>
/// <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(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -69,7 +69,9 @@ public sealed class SiteAuditBacklogReporter : IHostedService, IDisposable
_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)
{
// 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)
{
_cts?.Cancel();
return _loop ?? Task.CompletedTask;
}
/// <inheritdoc />
/// <summary>Releases the internal <see cref="CancellationTokenSource"/> used to stop the polling loop.</summary>
public void Dispose()
{
_cts?.Dispose();
@@ -244,7 +244,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
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)
{
ArgumentNullException.ThrowIfNull(evt);
@@ -469,7 +475,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
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)
{
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(
int limit, CancellationToken ct = default)
{
@@ -560,6 +578,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
/// </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 forwarded audit events.</returns>
public Task<IReadOnlyList<AuditEvent>> ReadForwardedAsync(int limit, CancellationToken ct = default)
{
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(
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>
/// <returns>A <see cref="ValueTask"/> that completes when all resources have been released.</returns>
public async ValueTask DisposeAsync()
{
Task? writerLoop;
@@ -44,6 +44,9 @@ public sealed class ClusterClientSiteAuditClient : ISiteStreamAuditClient
private readonly IActorRef _siteCommunicationActor;
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">
/// The site's <c>SiteCommunicationActor</c> — it forwards the ingest command
/// over the registered central ClusterClient and routes the reply back to
@@ -22,6 +22,7 @@ public interface ISiteStreamAuditClient
/// </summary>
/// <param name="batch">The batch of audit events to forward.</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);
/// <summary>
@@ -42,5 +43,6 @@ public interface ISiteStreamAuditClient
/// </remarks>
/// <param name="batch">The batch of cached-call telemetry packets to forward.</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);
}
@@ -13,6 +13,7 @@ public static class ApiMethodCommands
/// <param name="formatOption">Global option for the output format.</param>
/// <param name="usernameOption">Global option for the authentication username.</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)
{
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="usernameOption">Global <c>--username</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)
{
var command = new Command("audit") { Description = "Query and export the centralized audit log" };
@@ -74,6 +74,7 @@ public static class AuditExportHelpers
/// </summary>
/// <param name="args">The export arguments containing filters and format.</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)
{
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="output">Text writer for command output messages.</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(
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
/// has no <c>code</c> property — callers fall back to "ERROR" in that case.
/// </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)
{
if (string.IsNullOrWhiteSpace(body))
@@ -43,6 +43,7 @@ public static class AuditFormatterFactory
/// </summary>
/// <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>
/// <returns>The <see cref="IAuditFormatter"/> appropriate for the requested format.</returns>
public static IAuditFormatter Create(string format, TextWriter notices)
{
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="usernameOption">Global username 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)
{
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="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>
/// <returns>The resolved absolute <see cref="DateTimeOffset"/> in UTC.</returns>
public static DateTimeOffset ResolveTimeSpec(string spec, DateTimeOffset now)
{
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="afterOccurredAtUtc">Optional keyset cursor timestamp.</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(
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="output">The output writer for results.</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(
ManagementHttpClient client,
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.
/// </summary>
/// <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)
=> !string.IsNullOrWhiteSpace(month)
&& DateTime.TryParseExact(month, "yyyy-MM", CultureInfo.InvariantCulture,
@@ -307,6 +307,13 @@ public static class BundleCommands
// for the post-write summary line.
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)
{
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
/// than silently falling through to JSON.
/// </summary>
/// <returns>The configured <c>--format</c> option constrained to "json" or "table".</returns>
internal static Option<string> CreateFormatOption()
{
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,
/// closing CLI-017's regression.
/// </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(
ParseResult result,
Option<string> urlOption,
@@ -110,6 +111,7 @@ internal static class CommandHelpers
/// <param name="result">Parsed command-line result.</param>
/// <param name="formatOption">The <c>--format</c> option definition.</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)
{
// GetResult returns non-null only when the option was actually present on the
@@ -130,6 +132,7 @@ internal static class CommandHelpers
/// </summary>
/// <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>
/// <returns>The command-line value when non-empty; otherwise the environment fallback (may be null).</returns>
internal static string? ResolveCredential(string? commandLineValue, string? envValue)
=> string.IsNullOrWhiteSpace(commandLineValue) ? envValue : commandLineValue;
@@ -140,6 +143,7 @@ internal static class CommandHelpers
/// an unhandled <see cref="UriFormatException"/>.
/// </summary>
/// <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)
{
if (string.IsNullOrWhiteSpace(url))
@@ -154,6 +158,7 @@ internal static class CommandHelpers
/// </summary>
/// <param name="response">Response received from the management API.</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)
{
if (response.JsonData != null)
@@ -192,6 +197,8 @@ internal static class CommandHelpers
/// both channels are honoured. (Authentication failure — HTTP 401 / bad credentials
/// — is deliberately <em>not</em> treated as authorization failure; it is exit 1.)
/// </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)
{
if (response.StatusCode == 403)
@@ -13,6 +13,7 @@ public static class DataConnectionCommands
/// <param name="formatOption">Global output format option.</param>
/// <param name="usernameOption">Global username 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)
{
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="usernameOption">Shared username 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)
{
var command = new Command("debug") { Description = "Runtime debugging" };
@@ -27,6 +27,7 @@ internal static class DebugStreamHelpers
/// </summary>
/// <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>
/// <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)
{
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.
/// </summary>
/// <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)
{
if (exitTask.IsCompletedSuccessfully)
@@ -13,6 +13,7 @@ public static class DeployCommands
/// <param name="formatOption">Global output format option.</param>
/// <param name="usernameOption">Global username 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)
{
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="usernameOption">Global option for the authentication username.</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)
{
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="usernameOption">Global <c>--username</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)
{
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="usernameOption">Global <c>--username</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)
{
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.
/// </summary>
/// <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)
{
var id = result.GetValue(SmtpIdOption);
@@ -13,6 +13,7 @@ public static class SecurityCommands
/// <param name="formatOption">Shared output format option.</param>
/// <param name="usernameOption">Shared username 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)
{
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.
/// </summary>
/// <param name="json">The JSON success body returned by the management API.</param>
/// <returns>Exit code 0.</returns>
internal static int PrintCreatedKey(string 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="usernameOption">Global username 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)
{
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="usernameOption">Shared username 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)
{
var command = new Command("template") { Description = "Manage templates" };
@@ -61,6 +61,7 @@ public static class AuditExportEndpoints
/// </summary>
/// <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>
/// <returns>A task representing the asynchronous export streaming operation.</returns>
internal static async Task HandleExportAsync(HttpContext context, IAuditLogExportService exportService)
{
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.
/// </remarks>
/// <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)
{
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>
/// <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)
{
endpoints.MapPost("/auth/login", async (HttpContext context) =>
@@ -198,6 +199,7 @@ public static class AuthEndpoints
/// server-side. See CentralUI-020.
/// </summary>
/// <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)
{
context.Response.StatusCode = context.User.Identity?.IsAuthenticated == true
@@ -219,6 +221,7 @@ public static class AuthEndpoints
/// <see cref="AuthenticationProperties.AllowRefresh"/> is left unset (null)
/// so the middleware is free to slide the expiry on activity.
/// </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()
{
IsPersistent = true
@@ -20,6 +20,7 @@ public static class ClaimsPrincipalExtensions
/// <see cref="UnknownUser"/> when the claim is absent.
/// </summary>
/// <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)
=> principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? UnknownUser;
@@ -28,6 +29,7 @@ public static class ClaimsPrincipalExtensions
/// the claim is absent.
/// </summary>
/// <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)
=> principal.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value;
@@ -37,6 +39,7 @@ public static class ClaimsPrincipalExtensions
/// ten components (CentralUI-024).
/// </summary>
/// <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(
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>
/// claims). System-wide users see and act on every site.
/// </summary>
/// <returns>A task that resolves to <c>true</c> if the user has no site-scope restriction.</returns>
public async Task<bool> IsSystemWideAsync()
=> (await ResolveAsync()).IsSystemWide;
@@ -46,6 +47,7 @@ public sealed class SiteScopeService
/// system-wide user (callers should consult <see cref="IsSystemWideAsync"/>
/// or use the filter/allowed helpers, which already account for that).
/// </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()
=> (await ResolveAsync()).Sites;
@@ -54,6 +56,7 @@ public sealed class SiteScopeService
/// see. A system-wide user gets the full list back unchanged.
/// </summary>
/// <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)
{
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.
/// </summary>
/// <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)
{
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
/// lists is returned (deduplicated and order-stable on first-seen).
/// </summary>
/// <returns>The deduplicated, order-stable list of <see cref="AuditKind"/> values applicable to the selected channels.</returns>
public IReadOnlyList<AuditKind> VisibleKinds()
{
if (Channels.Count == 0)
@@ -411,6 +411,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// </summary>
/// <param name="columnKey">The stable key of the resized column.</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]
public async Task OnColumnResized(string columnKey, int widthPx)
{
@@ -431,6 +432,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// </summary>
/// <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>
/// <returns>A task that completes when the column order has been persisted and the component re-rendered.</returns>
[JSInvokable]
public async Task OnColumnReordered(string fromKey, string toKey)
{
@@ -472,6 +474,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// <summary>
/// Releases the .NET object reference held for JS interop callbacks.
/// </summary>
/// <returns>A completed value task.</returns>
public ValueTask DisposeAsync()
{
_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.
/// </summary>
/// <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)
{
const string basePath = "/api/centralui/audit/export";
@@ -182,6 +182,7 @@ public partial class TransportExport : ComponentBase
/// importer enforces its own strength + lockout policies.
/// </summary>
/// <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)
{
if (string.IsNullOrEmpty(s)) return 0;
@@ -261,6 +262,7 @@ public partial class TransportExport : ComponentBase
/// knows exactly what an unencrypted export would leak.
/// </summary>
/// <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)
{
var count = 0;
@@ -367,6 +369,7 @@ public partial class TransportExport : ComponentBase
/// </summary>
/// <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>
/// <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)
{
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="seed">The set of explicitly selected item ids.</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)
{
return all.Where(x => !seed.Contains(idOf(x))).ToList();
@@ -18,6 +18,7 @@ internal static class DurationInput
/// <c>sec</c> unit.
/// </summary>
/// <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)
{
if (duration is not { } d || d <= TimeSpan.Zero) return (null, "sec");
@@ -34,6 +35,7 @@ internal static class DurationInput
/// </summary>
/// <param name="value">The numeric string entered by the user.</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)
{
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
/// <c>btn-danger</c> styling with the label "Delete"; otherwise a primary
/// "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);
/// <summary>
@@ -28,5 +29,6 @@ public interface IDialogService
/// <param name="label">Label rendered above 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>
/// <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);
}
@@ -46,6 +46,7 @@ internal static class SchemaBuilderModel
/// </summary>
/// <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>
/// <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)
{
if (string.IsNullOrWhiteSpace(json)) return fallback;
@@ -66,15 +67,18 @@ internal static class SchemaBuilderModel
}
/// <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" };
/// <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" };
/// <summary>
/// Serializes a <see cref="SchemaNode"/> tree to its canonical JSON Schema string.
/// </summary>
/// <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)
{
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.
/// </summary>
/// <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) =>
JsonSchemaShapeParser.ParseParameters(json)
.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.
/// </summary>
/// <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) =>
JsonSchemaShapeParser.ParseParameters(json);
}
@@ -68,6 +68,7 @@ internal static class ScriptTriggerConfigCodec
/// <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>
/// <returns>The matching <see cref="ScriptTriggerKind"/>, or <see cref="ScriptTriggerKind.None"/> for null/empty.</returns>
internal static ScriptTriggerKind ParseKind(string? triggerType)
{
if (string.IsNullOrWhiteSpace(triggerType)) return ScriptTriggerKind.None;
@@ -89,6 +90,7 @@ internal static class ScriptTriggerConfigCodec
/// (invoked explicitly, never throttled), and None/Unknown.
/// </summary>
/// <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) =>
ParseKind(triggerType) is ScriptTriggerKind.ValueChange
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>
/// <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
{
ScriptTriggerKind.Interval => "Interval",
@@ -113,6 +116,7 @@ internal static class ScriptTriggerConfigCodec
/// </summary>
/// <param name="json">The raw JSON trigger configuration string.</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)
{
var model = new ScriptTriggerModel();
@@ -161,6 +165,7 @@ internal static class ScriptTriggerConfigCodec
/// </summary>
/// <param name="model">The trigger model to serialize.</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)
{
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>
/// <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)
{
var op = raw?.Trim();
@@ -19,6 +19,7 @@ public static class TriggerAttributeMapper
{
/// <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>
/// <returns>The list of <see cref="AttributeShape"/>s for Direct and Inherited attributes.</returns>
public static IReadOnlyList<AttributeShape> SelfAttributes(
IReadOnlyList<AlarmAttributeChoice> choices) =>
choices
@@ -32,6 +33,7 @@ public static class TriggerAttributeMapper
/// are skipped (no child scope to attach them to).
/// </summary>
/// <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(
IReadOnlyList<AlarmAttributeChoice> choices) =>
choices
@@ -15,6 +15,7 @@ public static class EndpointExtensions
/// </summary>
/// <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>
/// <returns>The same <paramref name="endpoints"/> instance, for call chaining.</returns>
public static IEndpointRouteBuilder MapCentralUI<TApp>(this IEndpointRouteBuilder endpoints)
where TApp : Microsoft.AspNetCore.Components.IComponent
{
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
public interface ISharedScriptCatalog
{
/// <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();
/// <summary>
@@ -18,6 +19,7 @@ public interface ISharedScriptCatalog
/// </summary>
/// <param name="name">Name of the shared script to retrieve.</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);
}
@@ -32,6 +32,7 @@ public class InboundScriptHost
/// Targets a specific instance for method invocation.
/// </summary>
/// <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();
}
@@ -44,6 +45,7 @@ public class InboundScriptHost
/// <param name="scriptName">The name of the script to call.</param>
/// <param name="parameters">Optional parameters to pass to the script.</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(
string scriptName,
object? parameters = null,
@@ -55,6 +57,7 @@ public class InboundScriptHost
/// </summary>
/// <param name="attributeName">The name of the attribute to retrieve.</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(
string attributeName,
System.Threading.CancellationToken cancellationToken = default) =>
@@ -65,6 +68,7 @@ public class InboundScriptHost
/// </summary>
/// <param name="attributeNames">The names of the attributes to retrieve.</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(
IEnumerable<string> attributeNames,
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="value">The value to set.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public System.Threading.Tasks.Task SetAttribute(
string attributeName,
string value,
@@ -88,6 +93,7 @@ public class InboundScriptHost
/// </summary>
/// <param name="attributeValues">Dictionary of attribute names to values.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public System.Threading.Tasks.Task SetAttributes(
IReadOnlyDictionary<string, string> attributeValues,
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>
/// <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)
{
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>
/// <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)
{
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
/// thread-safe. Subsequent calls return the already-installed instances.
/// </summary>
/// <returns>The installed <see cref="SandboxConsoleCapture"/> instances for stdout and stderr.</returns>
public static (SandboxConsoleCapture Out, SandboxConsoleCapture Error) Install()
{
if (_outInstance != null && _errorInstance != null)
@@ -72,6 +73,7 @@ internal sealed class SandboxConsoleCapture : TextWriter
/// other call-trees are unaffected.
/// </summary>
/// <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)
{
var previous = _current.Value;
@@ -32,6 +32,7 @@ public class SandboxExternalHelper
/// <param name="methodName">The method name to invoke.</param>
/// <param name="parameters">Optional method parameters.</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(
string systemName,
string methodName,
@@ -49,6 +50,7 @@ public class SandboxExternalHelper
/// <param name="methodName">The method name to invoke.</param>
/// <param name="parameters">Optional method parameters.</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(
string systemName,
string methodName,
@@ -80,6 +82,7 @@ public class SandboxDatabaseHelper
/// <summary>Gets a database connection by name.</summary>
/// <param name="name">The database connection name.</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)
{
if (_gateway == null)
@@ -93,6 +96,7 @@ public class SandboxDatabaseHelper
/// <param name="sql">The SQL statement to execute.</param>
/// <param name="parameters">Optional SQL parameters.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task CachedWrite(
string name,
string sql,
@@ -123,6 +127,7 @@ public class SandboxNotifyHelper
{
/// <summary>Selects the notification list to send to.</summary>
/// <param name="listName">The notification list name.</param>
/// <returns>A <see cref="SandboxNotifyTarget"/> for the specified list.</returns>
public SandboxNotifyTarget To(string listName) =>
new();
@@ -133,6 +138,7 @@ public class SandboxNotifyHelper
/// <c>NotifyHelper.Status</c>.
/// </summary>
/// <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) =>
Task.FromResult(new NotificationDeliveryStatus("Unknown", 0, null, null));
}
@@ -156,6 +162,7 @@ public class SandboxNotifyTarget
/// <param name="subject">The notification subject.</param>
/// <param name="message">The notification message.</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) =>
Task.FromResult(Guid.NewGuid().ToString("N"));
}
@@ -30,6 +30,7 @@ public class SandboxInboundScriptHost
/// Creates a sandbox route target that throws on every operation.
/// </summary>
/// <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);
}
@@ -50,6 +51,7 @@ public class SandboxInboundScriptHost
/// <param name="scriptName">Script name (included in the exception message).</param>
/// <param name="parameters">Unused parameters.</param>
/// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task<object?> Call(
string scriptName,
object? parameters = null,
@@ -61,6 +63,7 @@ public class SandboxInboundScriptHost
/// </summary>
/// <param name="attributeName">Attribute name (included in the exception message).</param>
/// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task<object?> GetAttribute(
string attributeName,
CancellationToken cancellationToken = default) =>
@@ -71,6 +74,7 @@ public class SandboxInboundScriptHost
/// </summary>
/// <param name="attributeNames">Attribute names (unused).</param>
/// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task<IReadOnlyDictionary<string, object?>> GetAttributes(
IEnumerable<string> attributeNames,
CancellationToken cancellationToken = default) =>
@@ -82,6 +86,7 @@ public class SandboxInboundScriptHost
/// <param name="attributeName">Attribute name (included in the exception message).</param>
/// <param name="value">Unused value.</param>
/// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task SetAttribute(
string attributeName,
string value,
@@ -93,6 +98,7 @@ public class SandboxInboundScriptHost
/// </summary>
/// <param name="attributeValues">Unused attribute values.</param>
/// <param name="cancellationToken">Unused token.</param>
/// <returns>Never returns; always throws <see cref="ScriptSandboxException"/>.</returns>
public Task SetAttributes(
IReadOnlyDictionary<string, string> attributeValues,
CancellationToken cancellationToken = default) =>
@@ -102,6 +102,7 @@ public interface ISandboxInstanceGateway
/// <param name="canonicalName">The canonical name of the attribute.</param>
/// <param name="value">The value to set.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct);
/// <summary>
@@ -12,6 +12,7 @@ public static class ScriptShapeParser
/// <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="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)
{
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.
/// </summary>
/// <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)
{
services.AddRazorComponents()
@@ -45,6 +45,7 @@ public static class ApiMethodKeyScopeReconciler
/// <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>
/// <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(
string methodName,
IReadOnlySet<string> selectedKeyIds,
@@ -70,6 +70,8 @@ public sealed record AuditEventView
/// <summary>
/// Decomposes a canonical <see cref="AuditEvent"/> into a flat view for the UI.
/// </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)
{
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
/// memory is bounded.
/// </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(
AuditLogQueryFilter filter,
int maxRows,
@@ -176,6 +177,7 @@ public sealed class AuditLogExportService : IAuditLogExportService
/// cleanly on another.
/// </summary>
/// <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)
{
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="paging">Optional paging cursor; defaults to first page when null.</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(
AuditLogQueryFilter filter,
AuditLogPaging? paging = null,
@@ -57,6 +58,7 @@ public interface IAuditLogQueryService
/// dashboard.
/// </remarks>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the current audit KPI snapshot.</returns>
Task<AuditLogKpiSnapshot> GetKpiSnapshotAsync(CancellationToken ct = default);
/// <summary>
@@ -76,6 +78,7 @@ public interface IAuditLogQueryService
/// </remarks>
/// <param name="executionId">Any execution id in the chain to look up.</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(
Guid executionId,
CancellationToken ct = default);
@@ -90,5 +93,6 @@ public interface IAuditLogQueryService
/// filter affordance.
/// </summary>
/// <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);
}
@@ -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="tagPaths">Tag paths to read.</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(
string siteId,
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="parentNodeId">Node to browse, or <c>null</c> to browse from the server root.</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(
string siteId,
string connectionName,
@@ -18,11 +18,7 @@ public sealed class ClusterOptionsValidator : OptionsValidatorBase<ClusterOption
"keep-oldest"
};
/// <summary>
/// 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>
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, ClusterOptions options)
{
// CI-012: design doc states "both nodes are seed nodes — each node lists
@@ -23,6 +23,7 @@ public static class ServiceCollectionExtensions
/// </para>
/// </summary>
/// <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)
{
services.TryAddEnumerable(
@@ -20,9 +20,17 @@ public interface IAlarmSubscribableConnection
/// currently-active conditions (Snapshot…SnapshotComplete) on every
/// (re)subscribe. Returns a subscription id for <see cref="UnsubscribeAlarmsAsync"/>.
/// </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,
AlarmTransitionCallback callback, CancellationToken cancellationToken = default);
/// <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);
}
@@ -14,6 +14,7 @@ public interface IBrowsableDataConnection
/// </summary>
/// <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>
/// <returns>A task that resolves to the child nodes and a flag indicating whether results were truncated.</returns>
Task<BrowseChildrenResult> BrowseChildrenAsync(
string? parentNodeId,
CancellationToken cancellationToken = default);
@@ -44,5 +45,7 @@ public enum BrowseNodeClass { Object, Variable, Method, Other }
/// </summary>
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) { }
}
@@ -18,9 +18,11 @@ public interface IDataConnection : IAsyncDisposable
/// <summary>Establishes the protocol connection using the provided connection details.</summary>
/// <param name="connectionDetails">Protocol-specific key-value configuration pairs.</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);
/// <summary>Gracefully terminates the protocol connection.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DisconnectAsync(CancellationToken cancellationToken = default);
/// <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>
@@ -31,6 +33,7 @@ public interface IDataConnection : IAsyncDisposable
/// <summary>Cancels an active subscription by its ID.</summary>
/// <param name="subscriptionId">The subscription ID returned by <see cref="SubscribeAsync"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default);
/// <summary>Reads the current value of a single tag.</summary>
/// <param name="tagPath">The tag path to read.</param>
@@ -31,6 +31,7 @@ public interface IAuditLogRepository
/// </summary>
/// <param name="evt">The audit event to insert.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default);
/// <summary>
@@ -44,6 +45,7 @@ public interface IAuditLogRepository
/// <param name="filter">Filter criteria to apply to the query.</param>
/// <param name="paging">Paging cursor and page size.</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(
AuditLogQueryFilter filter,
AuditLogPaging paging,
@@ -82,6 +84,7 @@ public interface IAuditLogRepository
/// </remarks>
/// <param name="monthBoundary">Lower-bound datetime of the monthly partition to switch out.</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);
/// <summary>
@@ -94,6 +97,7 @@ public interface IAuditLogRepository
/// </summary>
/// <param name="threshold">Only partitions whose data is entirely older than this UTC datetime are returned.</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(
DateTime threshold,
CancellationToken ct = default);
@@ -183,6 +187,7 @@ public interface IAuditLogRepository
/// </remarks>
/// <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>
/// <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(
Guid executionId,
CancellationToken ct = default);
@@ -194,5 +199,6 @@ public interface IAuditLogRepository
/// for ~60s so the repository is hit at most once per minute per circuit.
/// </summary>
/// <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);
}
@@ -10,30 +10,37 @@ public interface ICentralUiRepository
{
/// <summary>Returns all configured sites.</summary>
/// <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);
/// <summary>Returns all data connections for the specified site.</summary>
/// <param name="siteId">The site database ID to filter by.</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);
/// <summary>Returns all data connections across all sites.</summary>
/// <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);
/// <summary>Returns the full template tree including folders and templates.</summary>
/// <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);
/// <summary>Returns instances filtered by optional site, template, or search term.</summary>
/// <param name="siteId">Optional site 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="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);
/// <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="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);
/// <summary>Returns the area tree for the specified site.</summary>
/// <param name="siteId">The site database ID.</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);
// Audit log queries
@@ -51,6 +58,7 @@ public interface ICentralUiRepository
/// <param name="page">One-based page number.</param>
/// <param name="pageSize">Number of entries per page.</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(
string? user = null,
string? entityType = null,
@@ -66,5 +74,6 @@ public interface ICentralUiRepository
/// <summary>Persists pending changes to the underlying store.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the number of entities written.</returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -36,6 +36,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="definition">The external system definition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
@@ -43,6 +44,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="definition">The external system definition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
@@ -50,6 +52,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="id">The external system ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default);
// ExternalSystemMethod
@@ -86,6 +89,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="method">The external system method to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
/// <summary>
@@ -93,6 +97,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="method">The external system method to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
/// <summary>
@@ -100,6 +105,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="id">The method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default);
// DatabaseConnectionDefinition
@@ -135,6 +141,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="definition">The database connection definition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
@@ -142,6 +149,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="definition">The database connection definition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
/// <summary>
@@ -149,6 +157,7 @@ public interface IExternalSystemRepository
/// </summary>
/// <param name="id">The database connection ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
@@ -14,28 +14,35 @@ public interface IInboundApiRepository
/// <summary>Retrieves an API method by ID.</summary>
/// <param name="id">The API method ID.</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);
/// <summary>Retrieves all API methods.</summary>
/// <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);
/// <summary>Retrieves an API method by name.</summary>
/// <param name="name">The API method name.</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);
/// <summary>Adds a new API method.</summary>
/// <param name="method">The API method to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
/// <summary>Updates an existing API method.</summary>
/// <param name="method">The API method to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
/// <summary>Deletes an API method by ID.</summary>
/// <param name="id">The API method ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteApiMethodAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the database.</summary>
/// <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);
}
@@ -51,6 +51,7 @@ public interface INotificationOutboxRepository
/// </summary>
/// <param name="n">The notification to update.</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);
/// <summary>
@@ -25,16 +25,19 @@ public interface INotificationRepository
/// <summary>Adds a new notification list.</summary>
/// <param name="list">The notification list to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
/// <summary>Updates an existing notification list.</summary>
/// <param name="list">The notification list to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
/// <summary>Deletes a notification list by ID.</summary>
/// <param name="id">The notification list ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default);
// NotificationRecipient
@@ -53,16 +56,19 @@ public interface INotificationRepository
/// <summary>Adds a new notification recipient.</summary>
/// <param name="recipient">The recipient to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
/// <summary>Updates an existing notification recipient.</summary>
/// <param name="recipient">The recipient to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
/// <summary>Deletes a notification recipient by ID.</summary>
/// <param name="id">The recipient ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default);
// SmtpConfiguration
@@ -80,16 +86,19 @@ public interface INotificationRepository
/// <summary>Adds a new SMTP configuration.</summary>
/// <param name="configuration">The SMTP configuration to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
/// <summary>Updates an existing SMTP configuration.</summary>
/// <param name="configuration">The SMTP configuration to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
/// <summary>Deletes an SMTP configuration by ID.</summary>
/// <param name="id">The SMTP configuration ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the repository.</summary>
@@ -39,6 +39,7 @@ public interface ISiteCallAuditRepository
/// </summary>
/// <param name="siteCall">The site call row to insert or monotonically update.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default);
/// <summary>
@@ -46,6 +47,7 @@ public interface ISiteCallAuditRepository
/// </summary>
/// <param name="id">The tracked operation id to look up.</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);
/// <summary>
@@ -58,6 +60,7 @@ public interface ISiteCallAuditRepository
/// <param name="filter">Filter criteria for the query.</param>
/// <param name="paging">Keyset paging parameters.</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(
SiteCallQueryFilter filter,
SiteCallPaging paging,
@@ -71,6 +74,7 @@ public interface ISiteCallAuditRepository
/// </summary>
/// <param name="olderThanUtc">UTC cutoff; terminal rows older than this are deleted.</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);
/// <summary>
@@ -84,6 +88,7 @@ public interface ISiteCallAuditRepository
/// <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="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(
DateTime stuckCutoff,
DateTime intervalSince,
@@ -97,6 +102,7 @@ public interface ISiteCallAuditRepository
/// <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="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(
DateTime stuckCutoff,
DateTime intervalSince,
@@ -12,59 +12,73 @@ public interface ISiteRepository
/// <summary>Retrieves a site by its ID.</summary>
/// <param name="id">The site primary key.</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);
/// <summary>Retrieves a site by its identifier.</summary>
/// <param name="siteIdentifier">The unique site identifier string.</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);
/// <summary>Retrieves all sites.</summary>
/// <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);
/// <summary>Adds a new site.</summary>
/// <param name="site">The site entity to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddSiteAsync(Site site, CancellationToken cancellationToken = default);
/// <summary>Updates an existing site.</summary>
/// <param name="site">The site entity with updated values.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateSiteAsync(Site site, CancellationToken cancellationToken = default);
/// <summary>Deletes a site.</summary>
/// <param name="id">The site primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteSiteAsync(int id, CancellationToken cancellationToken = default);
// Data Connections
/// <summary>Retrieves a data connection by its ID.</summary>
/// <param name="id">The data connection primary key.</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);
/// <summary>Retrieves all data connections.</summary>
/// <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);
/// <summary>Retrieves all data connections for a site.</summary>
/// <param name="siteId">The site primary key to filter by.</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);
/// <summary>Adds a new data connection.</summary>
/// <param name="connection">The data connection entity to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default);
/// <summary>Updates an existing data connection.</summary>
/// <param name="connection">The data connection entity with updated values.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default);
/// <summary>Deletes a data connection.</summary>
/// <param name="id">The data connection primary key.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteDataConnectionAsync(int id, CancellationToken cancellationToken = default);
// Instances (for deletion constraint checks)
/// <summary>Retrieves all instances deployed to a site.</summary>
/// <param name="siteId">The site primary key to filter by.</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);
/// <summary>Saves all pending changes to the database.</summary>
/// <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);
}
@@ -10,10 +10,12 @@ public interface ITemplateEngineRepository
/// <summary>Retrieves a template by ID.</summary>
/// <param name="id">The template ID.</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);
/// <summary>Retrieves a template with its child entities by ID.</summary>
/// <param name="id">The template ID.</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);
/// <summary>
/// Bulk variant of <see cref="GetTemplateWithChildrenAsync(int, CancellationToken)"/>
@@ -26,9 +28,11 @@ public interface ITemplateEngineRepository
/// </summary>
/// <param name="names">Template names to load. Duplicate / null / empty names are filtered out.</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);
/// <summary>Retrieves all templates.</summary>
/// <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);
/// <summary>
/// Returns every template that contains a composition referencing
@@ -38,293 +42,386 @@ public interface ITemplateEngineRepository
/// </summary>
/// <param name="composedTemplateId">The composed template ID.</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);
/// <summary>Adds a new template.</summary>
/// <param name="template">The template to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template.</summary>
/// <param name="template">The template to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default);
/// <summary>Deletes a template by ID.</summary>
/// <param name="id">The template ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default);
// TemplateAttribute
/// <summary>Retrieves a template attribute by ID.</summary>
/// <param name="id">The attribute ID.</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);
/// <summary>Retrieves attributes 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 attributes for the specified template.</returns>
Task<IReadOnlyList<TemplateAttribute>> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template attribute.</summary>
/// <param name="attribute">The attribute to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template attribute.</summary>
/// <param name="attribute">The attribute to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
/// <summary>Deletes a template attribute by ID.</summary>
/// <param name="id">The attribute ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default);
// TemplateAlarm
/// <summary>Retrieves a template alarm by ID.</summary>
/// <param name="id">The alarm ID.</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);
/// <summary>Retrieves alarms 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 alarms for the specified template.</returns>
Task<IReadOnlyList<TemplateAlarm>> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template alarm.</summary>
/// <param name="alarm">The alarm to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template alarm.</summary>
/// <param name="alarm">The alarm to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
/// <summary>Deletes a template alarm by ID.</summary>
/// <param name="id">The alarm ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default);
// TemplateNativeAlarmSource
/// <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);
/// <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);
/// <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);
/// <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);
/// <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);
// TemplateScript
/// <summary>Retrieves a template script by ID.</summary>
/// <param name="id">The script ID.</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);
/// <summary>Retrieves scripts 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 scripts for the specified template.</returns>
Task<IReadOnlyList<TemplateScript>> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template script.</summary>
/// <param name="script">The script to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template script.</summary>
/// <param name="script">The script to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
/// <summary>Deletes a template script by ID.</summary>
/// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default);
// TemplateComposition
/// <summary>Retrieves a template composition by ID.</summary>
/// <param name="id">The composition ID.</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);
/// <summary>Retrieves compositions 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 compositions for the specified template.</returns>
Task<IReadOnlyList<TemplateComposition>> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Adds a new template composition.</summary>
/// <param name="composition">The composition to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template composition.</summary>
/// <param name="composition">The composition to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
/// <summary>Deletes a template composition by ID.</summary>
/// <param name="id">The composition ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default);
// Instance
/// <summary>Retrieves an instance by ID.</summary>
/// <param name="id">The instance ID.</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);
/// <summary>Retrieves all instances.</summary>
/// <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);
/// <summary>Retrieves instances 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 instances for the specified template.</returns>
Task<IReadOnlyList<Instance>> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
/// <summary>Retrieves instances for a site.</summary>
/// <param name="siteId">The site ID.</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);
/// <summary>Retrieves an instance by unique name.</summary>
/// <param name="uniqueName">The unique instance name.</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);
/// <summary>Adds a new instance.</summary>
/// <param name="instance">The instance to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance.</summary>
/// <param name="instance">The instance to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance by ID.</summary>
/// <param name="id">The instance ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default);
// InstanceAttributeOverride
/// <summary>Retrieves attribute 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 attribute overrides for the specified instance.</returns>
Task<IReadOnlyList<InstanceAttributeOverride>> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance attribute override.</summary>
/// <param name="attributeOverride">The override to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance attribute override.</summary>
/// <param name="attributeOverride">The override to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance attribute 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 DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default);
// InstanceAlarmOverride
/// <summary>Retrieves alarm 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 alarm overrides for the specified instance.</returns>
Task<IReadOnlyList<InstanceAlarmOverride>> GetAlarmOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Retrieves an alarm override by instance and alarm name.</summary>
/// <param name="instanceId">The instance ID.</param>
/// <param name="alarmCanonicalName">The alarm canonical name.</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);
/// <summary>Adds a new instance alarm override.</summary>
/// <param name="alarmOverride">The override to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance alarm override.</summary>
/// <param name="alarmOverride">The override to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance alarm 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 DeleteInstanceAlarmOverrideAsync(int id, CancellationToken cancellationToken = default);
// InstanceNativeAlarmSourceOverride
/// <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);
/// <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);
/// <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);
/// <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);
/// <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);
// InstanceConnectionBinding
/// <summary>Retrieves connection bindings 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 connection bindings for the specified instance.</returns>
Task<IReadOnlyList<InstanceConnectionBinding>> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
/// <summary>Adds a new instance connection binding.</summary>
/// <param name="binding">The binding to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
/// <summary>Updates an existing instance connection binding.</summary>
/// <param name="binding">The binding to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
/// <summary>Deletes an instance connection binding by ID.</summary>
/// <param name="id">The binding ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default);
// Area
/// <summary>Retrieves an area by ID.</summary>
/// <param name="id">The area ID.</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);
/// <summary>Retrieves areas for a site.</summary>
/// <param name="siteId">The site ID.</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);
/// <summary>Adds a new area.</summary>
/// <param name="area">The area to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddAreaAsync(Area area, CancellationToken cancellationToken = default);
/// <summary>Updates an existing area.</summary>
/// <param name="area">The area to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default);
/// <summary>Deletes an area by ID.</summary>
/// <param name="id">The area ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default);
// SharedScript
/// <summary>Retrieves a shared script by ID.</summary>
/// <param name="id">The script ID.</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);
/// <summary>Retrieves a shared script by name.</summary>
/// <param name="name">The script name.</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);
/// <summary>Retrieves all shared scripts.</summary>
/// <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);
/// <summary>Adds a new shared script.</summary>
/// <param name="sharedScript">The script to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default);
/// <summary>Updates an existing shared script.</summary>
/// <param name="sharedScript">The script to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default);
/// <summary>Deletes a shared script by ID.</summary>
/// <param name="id">The script ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteSharedScriptAsync(int id, CancellationToken cancellationToken = default);
// TemplateFolder
/// <summary>Retrieves a template folder by ID.</summary>
/// <param name="id">The folder ID.</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);
/// <summary>Retrieves all template folders.</summary>
/// <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);
/// <summary>Adds a new template folder.</summary>
/// <param name="folder">The folder to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default);
/// <summary>Updates an existing template folder.</summary>
/// <param name="folder">The folder to update.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UpdateFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default);
/// <summary>Deletes a template folder by ID.</summary>
/// <param name="id">The folder ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteFolderAsync(int id, CancellationToken cancellationToken = default);
/// <summary>Saves pending changes to the database.</summary>
/// <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);
}
@@ -45,31 +45,54 @@ public interface IInboundApiKeyAdmin
{
/// <summary>Creates a new key scoped to <paramref name="methods"/> and returns its
/// 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(
string name, IReadOnlyCollection<string> methods, CancellationToken ct = default);
/// <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);
/// <summary>Enables or disables a key without changing its secret. Returns false if
/// 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);
/// <summary>Replaces the method-scope set on a key without changing its secret.
/// 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(
string keyId, IReadOnlyCollection<string> methods, CancellationToken ct = default);
/// <summary>Removes a key (revoke-then-delete). Returns false if the key could not be
/// 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);
/// <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>
/// <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);
/// <summary>Returns the identifiers of all keys whose scopes contain
/// <paramref name="methodName"/>.</summary>
/// <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);
}
@@ -12,5 +12,6 @@ public interface IAuditService
/// <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="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);
}
@@ -21,5 +21,6 @@ public interface IAuditWriter
/// </summary>
/// <param name="evt">The audit event to persist.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task representing the asynchronous write operation.</returns>
Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
}
@@ -36,6 +36,7 @@ public interface ICachedCallLifecycleObserver
/// </summary>
/// <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>
/// <returns>A task that represents the asynchronous operation.</returns>
Task OnAttemptCompletedAsync(CachedCallAttemptContext context, CancellationToken ct = default);
}
@@ -32,5 +32,6 @@ public interface ICachedCallTelemetryForwarder
/// </summary>
/// <param name="telemetry">The combined-telemetry packet to fan out.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default);
}
@@ -14,5 +14,6 @@ public interface ICentralAuditWriter
/// </summary>
/// <param name="evt">The audit event to persist.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task WriteAsync(AuditEvent evt, CancellationToken ct = default);
}
@@ -16,6 +16,7 @@ public interface IDatabaseGateway
/// </summary>
/// <param name="connectionName">Name of the configured database connection to open.</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(
string connectionName,
CancellationToken cancellationToken = default);
@@ -55,6 +56,7 @@ public interface IDatabaseGateway
/// <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="cancellationToken">Cancellation token for the buffering operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task CachedWriteAsync(
string connectionName,
string sql,
@@ -12,6 +12,7 @@ public interface IInstanceLocator
/// </summary>
/// <param name="instanceUniqueName">System-wide unique name of the instance to look up.</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(
string instanceUniqueName,
CancellationToken cancellationToken = default);
@@ -47,6 +47,7 @@ public interface IOperationTrackingStore
/// <param name="sourceScript">Optional name of the source script.</param>
/// <param name="sourceNode">Optional source node identifier.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecordEnqueueAsync(
TrackedOperationId id,
string kind,
@@ -68,6 +69,7 @@ public interface IOperationTrackingStore
/// <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="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecordAttemptAsync(
TrackedOperationId id,
string status,
@@ -86,6 +88,7 @@ public interface IOperationTrackingStore
/// <param name="lastError">Optional final error message.</param>
/// <param name="httpStatus">Optional final HTTP status code.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecordTerminalAsync(
TrackedOperationId id,
string status,
@@ -111,6 +114,7 @@ public interface IOperationTrackingStore
/// </summary>
/// <param name="olderThanUtc">Cutoff timestamp; rows terminal before this are deleted.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task PurgeTerminalAsync(
DateTime olderThanUtc,
CancellationToken ct = default);
@@ -44,6 +44,7 @@ public interface IPartitionMaintenance
/// </summary>
/// <param name="lookaheadMonths">Number of future monthly boundaries to ensure exist.</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);
/// <summary>
@@ -53,5 +54,6 @@ public interface IPartitionMaintenance
/// has no boundaries.
/// </summary>
/// <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);
}
@@ -47,6 +47,7 @@ public interface ISiteAuditQueue
/// </remarks>
/// <param name="limit">Maximum number of rows to return.</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);
/// <summary>
@@ -77,6 +78,7 @@ public interface ISiteAuditQueue
/// </remarks>
/// <param name="limit">Maximum number of rows to return.</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);
/// <summary>
@@ -87,6 +89,7 @@ public interface ISiteAuditQueue
/// </summary>
/// <param name="eventIds">Event IDs to mark as forwarded.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task MarkForwardedAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
/// <summary>
@@ -107,6 +110,7 @@ public interface ISiteAuditQueue
/// <param name="sinceUtc">Lower bound timestamp (UTC).</param>
/// <param name="batchSize">Maximum number of rows to return.</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(
DateTime sinceUtc, int batchSize, CancellationToken ct = default);
@@ -121,6 +125,7 @@ public interface ISiteAuditQueue
/// </summary>
/// <param name="eventIds">Event IDs to mark as reconciled.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task MarkReconciledAsync(IReadOnlyList<Guid> eventIds, CancellationToken ct = default);
/// <summary>
@@ -135,5 +140,6 @@ public interface ISiteAuditQueue
/// the hot-path INSERT batch and the drain queries.
/// </summary>
/// <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);
}
@@ -12,6 +12,7 @@ public interface IBundleExporter
/// <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="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to a seeked-to-start stream containing the bundle ZIP archive.</returns>
Task<Stream> ExportAsync(
ExportSelection selection,
string user,
@@ -10,6 +10,7 @@ public interface IBundleImporter
/// <param name="bundleStream">Stream containing the bundle zip archive.</param>
/// <param name="passphrase">Optional passphrase for decrypting an encrypted bundle.</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);
/// <summary>
@@ -17,6 +18,7 @@ public interface IBundleImporter
/// </summary>
/// <param name="sessionId">Session id returned by <see cref="LoadAsync"/>.</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);
/// <summary>
@@ -26,6 +28,7 @@ public interface IBundleImporter
/// <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="ct">Cancellation token.</param>
/// <returns>A task that resolves to the result of the committed import transaction.</returns>
Task<ImportResult> ApplyAsync(
Guid sessionId,
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>
/// <param name="session">The session to store.</param>
/// <returns>The stored session (same reference as <paramref name="session"/>).</returns>
BundleSession Open(BundleSession session);
/// <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>
/// <returns>The matching <see cref="BundleSession"/>, or null if not found or expired.</returns>
BundleSession? Get(Guid sessionId);
/// <summary>Removes the session for the given id, if present.</summary>
/// <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.
/// </summary>
/// <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);
/// <summary>
@@ -29,6 +29,7 @@ public static class ManagementCommandRegistry
/// Resolves a management command wire name to its CLR type, or null if not registered.
/// </summary>
/// <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)
{
return Commands.GetValueOrDefault(commandName);
@@ -45,6 +46,7 @@ public static class ManagementCommandRegistry
/// symmetric with <see cref="Resolve"/>: it never yields a name that
/// <see cref="Resolve"/> cannot turn back into the same type.
/// </exception>
/// <returns>The registered wire name for <paramref name="commandType"/>.</returns>
public static string GetCommandName(Type commandType)
{
ArgumentNullException.ThrowIfNull(commandType);
@@ -21,11 +21,13 @@ public static class MxGatewayEndpointConfigSerializer
/// <summary>Serializes a config to the typed JSON shape.</summary>
/// <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)
=> JsonSerializer.Serialize(config, JsonOpts);
/// <summary>Parses stored config JSON; null/blank/malformed yields a default config.</summary>
/// <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)
{
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>
/// <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>
{
["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>
/// <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)
{
var c = new MxGatewayEndpointConfig();
@@ -118,6 +118,7 @@ public static class OpcUaEndpointConfigSerializer
/// </list>
/// </summary>
/// <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)
{
if (string.IsNullOrWhiteSpace(json))
@@ -175,6 +176,7 @@ public static class OpcUaEndpointConfigSerializer
/// used by OpcUaDataConnection so the adapter can keep that interface.
/// </summary>
/// <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)
{
var dict = new Dictionary<string, string>
@@ -10,6 +10,9 @@ public static class AlarmConditionStateFactory
/// auto-acked, never shelved or suppressed, not confirmable, and their
/// severity is the configured priority. Active mirrors the alarm State.
/// </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) =>
new(Active: state == AlarmState.Active, Acknowledged: true, Confirmed: null,
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
/// suitable for storage in <c>AuditEvent.DetailsJson</c>.
/// </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)
=> JsonSerializer.Serialize(details, Options);
@@ -54,6 +56,8 @@ public static class AuditDetailsCodec
/// Returns an empty (all-null) <see cref="AuditDetails"/> when <paramref name="json"/>
/// is <c>null</c>, empty, or whitespace — never throws.
/// </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)
{
if (string.IsNullOrWhiteSpace(json))
@@ -22,12 +22,17 @@ public static class AuditFieldBuilders
/// <summary>
/// Returns the canonical <c>Action</c> string: <c>"{channel}.{kind}"</c>.
/// </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)
=> $"{channel}.{kind}";
/// <summary>
/// Returns the canonical <c>Category</c> string: the channel name.
/// </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)
=> channel.ToString();
}
@@ -26,6 +26,9 @@ public static class AuditOutcomeProjector
/// Projects <paramref name="status"/> + <paramref name="kind"/> onto the canonical
/// <see cref="AuditOutcome"/>.
/// </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)
{
// 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
/// falls back to the first enum member (defensive — production rows always carry them).
/// </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)
{
ArgumentNullException.ThrowIfNull(evt);
@@ -115,6 +117,8 @@ public static class AuditRowProjection
/// are rebuilt via the field builders / outcome projector, and every domain field is
/// re-serialized into <c>DetailsJson</c> via <see cref="AuditDetailsCodec"/>.
/// </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)
{
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
/// property as the legacy bespoke record allowed.
/// </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)
{
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
/// unknown/renamed enum string (legacy or corrupt rows degrade gracefully).
/// </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
=> !string.IsNullOrEmpty(value) && Enum.TryParse<TEnum>(value, ignoreCase: false, out var parsed)
? parsed
@@ -194,6 +205,8 @@ public static class AuditRowProjection
public static class AuditEventRowExtensions
{
/// <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)
=> 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="extra">Free-form JSON extension for channel-specific extras (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(
AuditChannel channel,
AuditKind kind,

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