diff --git a/.gitignore b/.gitignore
index d6410ba9..aec5aeb6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,9 @@ data/
# Docker env2 runtime data
docker-env2/*/logs/
docker-env2/*/data/
+
+# Local credentials / login captures — never commit
+*login*.txt
+
+# Sister-project deployment artifacts (not part of this solution)
+/deploy/
diff --git a/CLAUDE.md b/CLAUDE.md
index 89403b0d..3f1ab46b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,6 +18,13 @@ When a change is requested, the default assumption is: update the design doc *an
- `docs/plans/` — Design decision and implementation-plan documents from refinement sessions.
- `AkkaDotNet/` — Akka.NET reference documentation and best practices notes.
+## Sister Projects
+
+Related repos cloned as sibling directories under `~/Desktop/` — referenced for context, not part of this solution:
+
+- `~/Desktop/MxAccessGateway` — MxAccess Gateway (`https://gitea.dohertylan.com/dohertj2/mxaccessgw`).
+- `~/Desktop/OtOpcUa` — OtOpcUa (`https://gitea.dohertylan.com/dohertj2/lmxopcua`).
+
## Document Conventions
- Requirements documents (high-level and component-level) live in `docs/requirements/`.
diff --git a/src/ScadaLink.AuditLog/Central/AuditCentralHealthSnapshot.cs b/src/ScadaLink.AuditLog/Central/AuditCentralHealthSnapshot.cs
index e728c51c..699d508a 100644
--- a/src/ScadaLink.AuditLog/Central/AuditCentralHealthSnapshot.cs
+++ b/src/ScadaLink.AuditLog/Central/AuditCentralHealthSnapshot.cs
@@ -64,6 +64,7 @@ public sealed class AuditCentralHealthSnapshot
/// later from the Akka host) can push without a friend reference;
/// readers should call .
///
+ /// The event carrying the site ID and new stalled state.
public void ApplyStalled(SiteAuditTelemetryStalledChanged evt)
{
if (evt is null) return;
diff --git a/src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs b/src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs
index 61a6dafc..ae5e7ed9 100644
--- a/src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs
+++ b/src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs
@@ -52,6 +52,8 @@ public class AuditLogIngestActor : ReceiveActor
/// lifetime exceeds the test, so the actor reuses the same instance across
/// every message. Used by Bundle D's MSSQL-backed TestKit fixture.
///
+ /// Audit log repository instance shared across all messages.
+ /// Logger for ingest diagnostics.
public AuditLogIngestActor(
IAuditLogRepository repository,
ILogger logger)
@@ -77,6 +79,8 @@ public class AuditLogIngestActor : ReceiveActor
/// is a long-lived cluster singleton, so it cannot hold a scope across
/// messages.
///
+ /// Root service provider used to open a fresh scope per message.
+ /// Logger for ingest diagnostics.
public AuditLogIngestActor(
IServiceProvider serviceProvider,
ILogger logger)
@@ -91,12 +95,7 @@ public class AuditLogIngestActor : ReceiveActor
ReceiveAsync(OnCachedTelemetryAsync);
}
- ///
- /// Audit-write failures are best-effort by design (see alog.md §13): a
- /// thrown exception in the ingest pipeline must not crash the actor.
- /// Resume keeps the actor's state intact so the next batch is processed
- /// against the same repository instance.
- ///
+ ///
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(maxNrOfRetries: 0, withinTimeRange: TimeSpan.Zero, decider:
diff --git a/src/ScadaLink.AuditLog/Central/AuditLogPartitionMaintenanceService.cs b/src/ScadaLink.AuditLog/Central/AuditLogPartitionMaintenanceService.cs
index 2aa02f81..39d99d8c 100644
--- a/src/ScadaLink.AuditLog/Central/AuditLogPartitionMaintenanceService.cs
+++ b/src/ScadaLink.AuditLog/Central/AuditLogPartitionMaintenanceService.cs
@@ -60,6 +60,12 @@ public sealed class AuditLogPartitionMaintenanceService : IHostedService, IDispo
private CancellationTokenSource? _cts;
private Task? _loop;
+ ///
+ /// Initializes the maintenance service with its required dependencies.
+ ///
+ /// Scope factory used to open DI scopes for each maintenance run.
+ /// Partition maintenance options (retention period, purge interval, etc.).
+ /// Logger for this service.
public AuditLogPartitionMaintenanceService(
IServiceScopeFactory scopeFactory,
IOptions options,
diff --git a/src/ScadaLink.AuditLog/Central/AuditLogPurgeActor.cs b/src/ScadaLink.AuditLog/Central/AuditLogPurgeActor.cs
index 153e2382..a0768513 100644
--- a/src/ScadaLink.AuditLog/Central/AuditLogPurgeActor.cs
+++ b/src/ScadaLink.AuditLog/Central/AuditLogPurgeActor.cs
@@ -61,6 +61,11 @@ public class AuditLogPurgeActor : ReceiveActor
private readonly ILogger _logger;
private ICancelable? _timer;
+ /// Initializes a new instance of and registers the tick handler.
+ /// DI service provider used to create scoped repository instances per tick.
+ /// Options controlling the purge interval.
+ /// Options controlling retention policy (RetentionDays).
+ /// Logger instance.
public AuditLogPurgeActor(
IServiceProvider services,
IOptions purgeOptions,
@@ -80,6 +85,7 @@ public class AuditLogPurgeActor : ReceiveActor
ReceiveAsync(_ => OnTickAsync());
}
+ ///
protected override void PreStart()
{
base.PreStart();
@@ -92,17 +98,14 @@ public class AuditLogPurgeActor : ReceiveActor
sender: Self);
}
+ ///
protected override void PostStop()
{
_timer?.Cancel();
base.PostStop();
}
- ///
- /// Resume keeps the singleton alive across any leaked exception. Restart
- /// would re-run PreStart and reschedule the timer (harmless but wasteful);
- /// Stop is wrong because the singleton must keep ticking until shutdown.
- ///
+ ///
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
diff --git a/src/ScadaLink.AuditLog/Central/CentralAuditRedactionFailureCounter.cs b/src/ScadaLink.AuditLog/Central/CentralAuditRedactionFailureCounter.cs
index 102b6d93..a60ffe66 100644
--- a/src/ScadaLink.AuditLog/Central/CentralAuditRedactionFailureCounter.cs
+++ b/src/ScadaLink.AuditLog/Central/CentralAuditRedactionFailureCounter.cs
@@ -47,6 +47,10 @@ public sealed class CentralAuditRedactionFailureCounter : IAuditRedactionFailure
{
private readonly AuditCentralHealthSnapshot _snapshot;
+ ///
+ /// Initializes a new backed by the supplied snapshot.
+ ///
+ /// The central health snapshot that accumulates the redaction failure count.
public CentralAuditRedactionFailureCounter(AuditCentralHealthSnapshot snapshot)
{
_snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot));
diff --git a/src/ScadaLink.AuditLog/Central/CentralAuditWriter.cs b/src/ScadaLink.AuditLog/Central/CentralAuditWriter.cs
index 9f079681..7dec9b38 100644
--- a/src/ScadaLink.AuditLog/Central/CentralAuditWriter.cs
+++ b/src/ScadaLink.AuditLog/Central/CentralAuditWriter.cs
@@ -66,6 +66,11 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
/// provider simply leaves SourceNode at whatever the caller set (often
/// null, which is the legacy behaviour).
///
+ /// Service provider used to open a per-call scope for the scoped repository.
+ /// Logger for swallowed write-failure diagnostics.
+ /// Optional payload filter for truncation and redaction; defaults to a pass-through.
+ /// Optional counter incremented on swallowed repository failures; defaults to a no-op.
+ /// Optional node identity provider for stamping SourceNode on central-origin rows.
public CentralAuditWriter(
IServiceProvider services,
ILogger logger,
@@ -80,12 +85,7 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
_nodeIdentity = nodeIdentity;
}
- ///
- /// Persists into the central AuditLog table
- /// idempotently on . Stamps
- /// from the central-side clock.
- /// Internal failures are logged and swallowed — never thrown.
- ///
+ ///
public async Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
if (evt is null)
diff --git a/src/ScadaLink.AuditLog/Central/IPullAuditEventsClient.cs b/src/ScadaLink.AuditLog/Central/IPullAuditEventsClient.cs
index e094e48c..172174c1 100644
--- a/src/ScadaLink.AuditLog/Central/IPullAuditEventsClient.cs
+++ b/src/ScadaLink.AuditLog/Central/IPullAuditEventsClient.cs
@@ -37,6 +37,10 @@ public interface IPullAuditEventsClient
/// rows ordered oldest-first AND a MoreAvailable flag the actor
/// uses to decide whether to fire another pull immediately.
///
+ /// The identifier of the site to pull audit events from.
+ /// Only events with an OccurredAtUtc at or after this cursor time are returned.
+ /// Maximum number of events to return per call.
+ /// Cancellation token.
Task PullAsync(
string siteId,
DateTime sinceUtc,
diff --git a/src/ScadaLink.AuditLog/Central/ISiteEnumerator.cs b/src/ScadaLink.AuditLog/Central/ISiteEnumerator.cs
index 9e9607cd..357ebae7 100644
--- a/src/ScadaLink.AuditLog/Central/ISiteEnumerator.cs
+++ b/src/ScadaLink.AuditLog/Central/ISiteEnumerator.cs
@@ -22,6 +22,7 @@ public interface ISiteEnumerator
/// on the next tick. Implementations should reflect adds/removes promptly
/// — the actor calls this once per tick.
///
+ /// Cancellation token for the async enumeration.
Task> EnumerateAsync(CancellationToken ct = default);
}
diff --git a/src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs b/src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs
index e38e6d2c..737be9fb 100644
--- a/src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs
+++ b/src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs
@@ -95,6 +95,14 @@ public class SiteAuditReconciliationActor : ReceiveActor
private ICancelable? _timer;
+ ///
+ /// Initializes the reconciliation actor with its dependencies and registers the tick handler.
+ ///
+ /// Enumerates the known sites to reconcile.
+ /// Client used to pull audit events from individual sites.
+ /// Root service provider for opening a per-tick DI scope.
+ /// Reconciliation configuration (interval, page size).
+ /// Logger for reconciliation diagnostics.
public SiteAuditReconciliationActor(
ISiteEnumerator sites,
IPullAuditEventsClient client,
@@ -117,6 +125,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
ReceiveAsync(_ => OnTickAsync());
}
+ ///
protected override void PreStart()
{
base.PreStart();
@@ -129,6 +138,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
sender: Self);
}
+ ///
protected override void PostStop()
{
_timer?.Cancel();
@@ -301,11 +311,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
}
}
- ///
- /// Resume on any unhandled exception inside the receive — the singleton
- /// MUST stay alive even if the per-tick try/catch leaks. Restart would
- /// reset the cursors (safe but wasteful); Resume preserves them.
- ///
+ ///
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
diff --git a/src/ScadaLink.AuditLog/Central/SiteAuditTelemetryStalledTracker.cs b/src/ScadaLink.AuditLog/Central/SiteAuditTelemetryStalledTracker.cs
index e1ed0fd2..849a1647 100644
--- a/src/ScadaLink.AuditLog/Central/SiteAuditTelemetryStalledTracker.cs
+++ b/src/ScadaLink.AuditLog/Central/SiteAuditTelemetryStalledTracker.cs
@@ -67,6 +67,7 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
/// SiteAuditTelemetryStalledTrackerTests use the ActorSystem ctor
/// via Akka.TestKit so they exercise the production subscribe path.
///
+ /// The actor system event stream to observe.
public SiteAuditTelemetryStalledTracker(EventStream eventStream)
: this(eventStream, snapshot: null)
{
@@ -80,6 +81,8 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
/// subscribe (no actor system), but tests that drive the tracker via
/// get the snapshot push for free.
///
+ /// The actor system event stream to observe.
+ /// Optional central health snapshot to mirror stalled-state changes into.
public SiteAuditTelemetryStalledTracker(EventStream eventStream, AuditCentralHealthSnapshot? snapshot)
{
_eventStream = eventStream ?? throw new ArgumentNullException(nameof(eventStream));
@@ -94,6 +97,7 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
/// updates the latched
/// per-site map. tears the subscriber down.
///
+ /// The actor system whose EventStream will be subscribed.
public SiteAuditTelemetryStalledTracker(ActorSystem actorSystem)
: this(actorSystem, snapshot: null)
{
@@ -105,6 +109,8 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
/// shared so the central health
/// surface sees per-site stalled state without re-reading the tracker.
///
+ /// The actor system whose EventStream will be subscribed.
+ /// Optional central health snapshot to mirror stalled-state changes into.
public SiteAuditTelemetryStalledTracker(ActorSystem actorSystem, AuditCentralHealthSnapshot? snapshot)
{
ArgumentNullException.ThrowIfNull(actorSystem);
@@ -136,6 +142,7 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
/// internally so tests against the bare-stream ctor can still drive the
/// tracker, but the production path always goes through the actor.
///
+ /// The stalled-state change event to apply.
internal void Apply(SiteAuditTelemetryStalledChanged evt)
{
if (evt is null) return;
@@ -147,6 +154,9 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
_snapshot?.ApplyStalled(evt);
}
+ ///
+ /// Disposes the tracker and tears down the internal subscriber actor.
+ ///
public void Dispose()
{
if (_disposed) return;
@@ -173,12 +183,17 @@ public sealed class SiteAuditTelemetryStalledTracker : IDisposable
{
private readonly SiteAuditTelemetryStalledTracker _parent;
+ ///
+ /// Initializes a new subscriber actor that forwards events to the given tracker.
+ ///
+ /// The parent tracker whose method will be called for each event.
public StalledChangedSubscriber(SiteAuditTelemetryStalledTracker parent)
{
_parent = parent;
Receive(evt => _parent.Apply(evt));
}
+ ///
protected override void PostStop()
{
Context.System.EventStream.Unsubscribe(Self, typeof(SiteAuditTelemetryStalledChanged));
diff --git a/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs b/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs
index 0d22447f..dbf8bd8d 100644
--- a/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs
+++ b/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs
@@ -103,6 +103,9 @@ public sealed class DefaultAuditPayloadFilter : IAuditPayloadFilter
/// counter from the container; a NoOp default is registered in
/// .
///
+ /// Live-reloadable audit log options.
+ /// Logger for redaction diagnostics.
+ /// Optional counter incremented when a redaction operation fails; defaults to a no-op.
public DefaultAuditPayloadFilter(
IOptionsMonitor options,
ILogger logger,
@@ -113,6 +116,7 @@ public sealed class DefaultAuditPayloadFilter : IAuditPayloadFilter
_failureCounter = failureCounter ?? new NoOpAuditRedactionFailureCounter();
}
+ ///
public AuditEvent Apply(AuditEvent rawEvent)
{
try
@@ -573,8 +577,11 @@ public sealed class DefaultAuditPayloadFilter : IAuditPayloadFilter
{
public static readonly CompiledRegex Invalid = new(null);
+ /// Gets the compiled , or null when the pattern was invalid.
public Regex? Regex { get; }
+ /// Initializes a new wrapping the given compiled regex instance.
+ /// The pre-compiled regex, or null to represent an invalid pattern.
public CompiledRegex(Regex? regex) => Regex = regex;
}
}
diff --git a/src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs b/src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs
index 45b7ee21..d9d410af 100644
--- a/src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs
+++ b/src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs
@@ -26,5 +26,6 @@ public interface IAuditPayloadFilter
/// and return a filtered copy. MUST NOT throw — on internal failure, over-redact
/// and surface the failure via the audit-redaction-failure health metric.
///
+ /// The unfiltered audit event to process.
AuditEvent Apply(AuditEvent rawEvent);
}
diff --git a/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs b/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs
index d97917fe..05aafce1 100644
--- a/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs
+++ b/src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs
@@ -53,6 +53,9 @@ public static class ServiceCollectionExtensions
/// and the site-→central telemetry collaborators. Idempotent re-registration
/// is not supported; call this exactly once per .
///
+ /// The service collection to register into.
+ /// Application configuration used to bind and related options sections.
+ /// The same for chaining.
public static IServiceCollection AddAuditLog(this IServiceCollection services, IConfiguration config)
{
ArgumentNullException.ThrowIfNull(services);
@@ -252,6 +255,8 @@ public static class ServiceCollectionExtensions
/// ships in M6.
///
///
+ /// The service collection to register into.
+ /// The same for chaining.
public static IServiceCollection AddAuditLogHealthMetricsBridge(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
@@ -290,6 +295,9 @@ public static class ServiceCollectionExtensions
/// from any composition root" invariant.
///
///
+ /// The service collection to register into.
+ /// Application configuration used to bind partition maintenance options.
+ /// The same for chaining.
public static IServiceCollection AddAuditLogCentralMaintenance(
this IServiceCollection services,
IConfiguration config)
diff --git a/src/ScadaLink.AuditLog/Site/FallbackAuditWriter.cs b/src/ScadaLink.AuditLog/Site/FallbackAuditWriter.cs
index 18511f12..321d6374 100644
--- a/src/ScadaLink.AuditLog/Site/FallbackAuditWriter.cs
+++ b/src/ScadaLink.AuditLog/Site/FallbackAuditWriter.cs
@@ -44,6 +44,11 @@ public sealed class FallbackAuditWriter : IAuditWriter
/// registration
/// always passes the real filter through.
///
+ /// The primary audit writer (typically the SQLite writer).
+ /// Drop-oldest ring buffer used to stash events when the primary fails.
+ /// Counter incremented on each primary failure for health reporting.
+ /// Logger for diagnostics.
+ /// Optional payload filter applied before writing; null means no filtering.
public FallbackAuditWriter(
IAuditWriter primary,
RingBufferFallback ring,
@@ -58,6 +63,7 @@ public sealed class FallbackAuditWriter : IAuditWriter
_filter = filter; // null = no-op pass-through; see WriteAsync.
}
+ ///
public async Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(evt);
diff --git a/src/ScadaLink.AuditLog/Site/HealthMetricsAuditRedactionFailureCounter.cs b/src/ScadaLink.AuditLog/Site/HealthMetricsAuditRedactionFailureCounter.cs
index 78454e3a..5365f56f 100644
--- a/src/ScadaLink.AuditLog/Site/HealthMetricsAuditRedactionFailureCounter.cs
+++ b/src/ScadaLink.AuditLog/Site/HealthMetricsAuditRedactionFailureCounter.cs
@@ -38,6 +38,8 @@ public sealed class HealthMetricsAuditRedactionFailureCounter : IAuditRedactionF
{
private readonly ISiteHealthCollector _collector;
+ /// Initializes the counter with the site health collector it bridges into.
+ /// The site health collector that receives the incremented redaction-failure count.
public HealthMetricsAuditRedactionFailureCounter(ISiteHealthCollector collector)
{
_collector = collector ?? throw new ArgumentNullException(nameof(collector));
diff --git a/src/ScadaLink.AuditLog/Site/HealthMetricsAuditWriteFailureCounter.cs b/src/ScadaLink.AuditLog/Site/HealthMetricsAuditWriteFailureCounter.cs
index 7284727b..9d4cf666 100644
--- a/src/ScadaLink.AuditLog/Site/HealthMetricsAuditWriteFailureCounter.cs
+++ b/src/ScadaLink.AuditLog/Site/HealthMetricsAuditWriteFailureCounter.cs
@@ -23,6 +23,10 @@ public sealed class HealthMetricsAuditWriteFailureCounter : IAuditWriteFailureCo
{
private readonly ISiteHealthCollector _collector;
+ ///
+ /// Initializes a new backed by the given health collector.
+ ///
+ /// The site health collector to increment on each audit write failure.
public HealthMetricsAuditWriteFailureCounter(ISiteHealthCollector collector)
{
_collector = collector ?? throw new ArgumentNullException(nameof(collector));
diff --git a/src/ScadaLink.AuditLog/Site/RingBufferFallback.cs b/src/ScadaLink.AuditLog/Site/RingBufferFallback.cs
index cf38dcdd..ac33e5e0 100644
--- a/src/ScadaLink.AuditLog/Site/RingBufferFallback.cs
+++ b/src/ScadaLink.AuditLog/Site/RingBufferFallback.cs
@@ -37,6 +37,8 @@ public sealed class RingBufferFallback
///
public event Action? RingBufferOverflowed;
+ /// Initializes the ring buffer with the specified fixed capacity.
+ /// Maximum number of events to buffer; must be greater than zero. Default is 1024.
public RingBufferFallback(int capacity = 1024)
{
if (capacity <= 0)
@@ -62,6 +64,8 @@ public sealed class RingBufferFallback
/// only when the ring has been
/// -d.
///
+ /// The audit event to enqueue.
+ /// if enqueued (or enqueued with overflow); when the channel is completed.
public bool TryEnqueue(AuditEvent evt)
{
ArgumentNullException.ThrowIfNull(evt);
@@ -91,6 +95,7 @@ public sealed class RingBufferFallback
/// been called. Callers that only want to drain what's currently buffered
/// must call first.
///
+ /// Cancellation token to abort the async enumeration.
public async IAsyncEnumerable DrainAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -105,6 +110,8 @@ public sealed class RingBufferFallback
/// recovery path. Returns
/// when the ring is empty.
///
+ /// When this returns , contains the dequeued event.
+ /// if an event was dequeued; if the ring is empty.
public bool TryDequeue(out AuditEvent evt) => _channel.Reader.TryRead(out evt!);
///
diff --git a/src/ScadaLink.AuditLog/Site/SiteAuditBacklogReporter.cs b/src/ScadaLink.AuditLog/Site/SiteAuditBacklogReporter.cs
index 955832a9..95823b01 100644
--- a/src/ScadaLink.AuditLog/Site/SiteAuditBacklogReporter.cs
+++ b/src/ScadaLink.AuditLog/Site/SiteAuditBacklogReporter.cs
@@ -52,6 +52,11 @@ public sealed class SiteAuditBacklogReporter : IHostedService, IDisposable
private CancellationTokenSource? _cts;
private Task? _loop;
+ /// Initializes a new instance of .
+ /// The site audit queue used to probe the backlog count.
+ /// The site health collector that receives the backlog snapshot.
+ /// Logger instance.
+ /// Poll interval override; defaults to (30 s).
public SiteAuditBacklogReporter(
ISiteAuditQueue queue,
ISiteHealthCollector collector,
diff --git a/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs b/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs
index 3e3ed440..9173af6c 100644
--- a/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs
+++ b/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs
@@ -48,6 +48,11 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
private readonly Task _writerLoop;
private bool _disposed;
+ /// Initializes a new instance of the SqliteAuditWriter class.
+ /// Configuration options for the audit writer.
+ /// Logger instance.
+ /// Node identity provider.
+ /// Optional connection string override.
public SqliteAuditWriter(
IOptions options,
ILogger logger,
@@ -186,14 +191,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
alter.ExecuteNonQuery();
}
- ///
- /// Enqueue an event for durable persistence. The returned
- /// completes once the event has been INSERTed (or, in the duplicate-EventId
- /// case, recognised as already present); it faults only if the writer loop
- /// itself collapses. The enqueue side never blocks on disk I/O — it only
- /// awaits the bounded channel's back-pressure when the writer is briefly
- /// behind.
- ///
+ ///
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(evt);
@@ -386,12 +384,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
}
}
- ///
- /// Returns up to rows in ,
- /// oldest first, with
- /// as the deterministic tiebreaker. Called by Bundle D's site telemetry
- /// actor to build a batch for the gRPC push.
- ///
+ ///
public Task> ReadPendingAsync(int limit, CancellationToken ct = default)
{
if (limit <= 0)
@@ -443,6 +436,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
/// , which also returns
/// rows).
///
+ /// Maximum number of rows to return.
+ /// Cancellation token.
public Task> ReadForwardedAsync(int limit, CancellationToken ct = default)
{
if (limit <= 0)
@@ -481,11 +476,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
}
}
- ///
- /// Flips the supplied EventIds from to
- /// in a single UPDATE. Non-existent
- /// or already-forwarded ids are no-ops.
- ///
+ ///
public Task MarkForwardedAsync(IReadOnlyList eventIds, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(eventIds);
@@ -520,15 +511,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
}
}
- ///
- /// M6 reconciliation-pull read: returns up to rows
- /// whose OccurredAtUtc >= sinceUtc and whose
- /// is still or
- /// . Forwarded rows are included so the
- /// brief race window between a site-Forwarded ack and central ingest cannot
- /// silently drop rows; central dedups on .
- /// Ordered oldest first, EventId tiebreaker.
- ///
+ ///
public Task> ReadPendingSinceAsync(
DateTime sinceUtc, int batchSize, CancellationToken ct = default)
{
@@ -575,13 +558,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
}
}
- ///
- /// M6 reconciliation-pull commit: flips the supplied EventIds to
- /// , but ONLY for rows currently in
- /// or .
- /// Rows already in are left untouched
- /// (idempotent re-call). Non-existent ids are silent no-ops.
- ///
+ ///
public Task MarkReconciledAsync(IReadOnlyList eventIds, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(eventIds);
@@ -616,22 +593,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
}
}
- ///
- /// M6 Bundle E (T6) health-metric surface: returns a point-in-time snapshot
- /// of the site queue's pending count, the oldest pending row's
- /// , and the on-disk file size. Called
- /// by the site-side SiteAuditBacklogReporter hosted service on its
- /// 30 s tick to refresh the SiteHealthReport.SiteAuditBacklog field.
- ///
- ///
- /// The pending-count + oldest-row queries run inside the same write lock as
- /// the hot-path INSERT batch so the snapshot is consistent against the
- /// connection's view (no torn read of an in-flight transaction). The on-disk
- /// size lookup happens OUTSIDE the lock — it's a stat() call on the file
- /// path and doesn't touch the connection. In-memory and missing files
- /// return 0 bytes (the snapshot is for ops dashboards, not a correctness
- /// invariant).
- ///
+ ///
public Task GetBacklogStatsAsync(CancellationToken ct = default)
{
int pendingCount;
@@ -731,11 +693,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
};
}
+ /// Disposes the audit writer and releases resources.
public void Dispose()
{
DisposeAsync().AsTask().GetAwaiter().GetResult();
}
+ /// Asynchronously disposes the audit writer and releases resources.
public async ValueTask DisposeAsync()
{
Task? writerLoop;
@@ -779,13 +743,17 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
/// An audit event awaiting persistence by the background writer.
private sealed class PendingAuditEvent
{
+ /// Initializes a new instance of the PendingAuditEvent class.
+ /// The audit event to persist.
public PendingAuditEvent(AuditEvent evt)
{
Event = evt;
Completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
}
+ /// The audit event to persist.
public AuditEvent Event { get; }
+ /// Task completion source for write completion signaling.
public TaskCompletionSource Completion { get; }
}
}
diff --git a/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
index 299f7ce1..09be7ab0 100644
--- a/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
+++ b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallLifecycleBridge.cs
@@ -48,6 +48,10 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
///
private readonly INodeIdentityProvider? _nodeIdentity;
+ /// Initializes a new with the given telemetry forwarder, logger, and optional node identity provider.
+ /// The telemetry forwarder used to ship cached-call lifecycle events to central.
+ /// Logger for bridge diagnostics.
+ /// Optional node identity provider used to stamp SourceNode on emitted telemetry rows.
public CachedCallLifecycleBridge(
ICachedCallTelemetryForwarder forwarder,
ILogger logger,
diff --git a/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs
index 897f047d..dd7f8812 100644
--- a/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs
+++ b/src/ScadaLink.AuditLog/Site/Telemetry/CachedCallTelemetryForwarder.cs
@@ -70,6 +70,10 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
/// registration. Production site nodes wire both — the central lazy
/// resolution is a no-op path kept symmetric with the M2 writer chain.
///
+ /// Writer used to persist audit events from the telemetry packet.
+ /// Optional store for updating operation tracking state; null on central nodes.
+ /// Logger for this forwarder.
+ /// Optional provider of the current node name stamped on emitted rows.
public CachedCallTelemetryForwarder(
IAuditWriter auditWriter,
IOperationTrackingStore? trackingStore,
@@ -82,13 +86,7 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
_nodeIdentity = nodeIdentity;
}
- ///
- /// Fan out one combined-telemetry packet to the audit writer and the
- /// tracking store. Returns once both halves have been attempted (success
- /// OR logged failure). NEVER throws — exceptions are caught per-half and
- /// logged at warning level so the calling script's outbound action is not
- /// disturbed.
- ///
+ ///
public async Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(telemetry);
diff --git a/src/ScadaLink.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs b/src/ScadaLink.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs
index b6b27f54..07e58e5d 100644
--- a/src/ScadaLink.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs
+++ b/src/ScadaLink.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs
@@ -20,6 +20,8 @@ public interface ISiteStreamAuditClient
///
/// in the site SQLite queue.
///
+ /// The batch of audit events to forward.
+ /// Cancellation token for the operation.
Task IngestAuditEventsAsync(AuditEventBatch batch, CancellationToken ct);
///
@@ -38,5 +40,7 @@ public interface ISiteStreamAuditClient
/// DI default (used by central and test composition roots) returns an empty
/// ack so no rows are flipped.
///
+ /// The batch of cached-call telemetry packets to forward.
+ /// Cancellation token for the operation.
Task IngestCachedTelemetryAsync(CachedTelemetryBatch batch, CancellationToken ct);
}
diff --git a/src/ScadaLink.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs b/src/ScadaLink.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs
index e903e0a4..68cc3cd7 100644
--- a/src/ScadaLink.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs
+++ b/src/ScadaLink.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs
@@ -43,6 +43,11 @@ public class SiteAuditTelemetryActor : ReceiveActor
private readonly ILogger _logger;
private ICancelable? _pendingTick;
+ /// Initializes the actor with its drain queue, gRPC client, options, and logger.
+ /// The site-local SQLite audit queue to drain.
+ /// The gRPC client used to push audit events to central.
+ /// Telemetry options controlling drain intervals and batch size.
+ /// Logger instance.
public SiteAuditTelemetryActor(
ISiteAuditQueue queue,
ISiteStreamAuditClient client,
@@ -62,6 +67,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
ReceiveAsync(_ => OnDrainAsync());
}
+ ///
protected override void PreStart()
{
base.PreStart();
@@ -71,6 +77,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
ScheduleNext(TimeSpan.FromSeconds(_options.BusyIntervalSeconds));
}
+ ///
protected override void PostStop()
{
_pendingTick?.Cancel();
diff --git a/src/ScadaLink.CLI/CliConfig.cs b/src/ScadaLink.CLI/CliConfig.cs
index 91fff5c0..dd59e585 100644
--- a/src/ScadaLink.CLI/CliConfig.cs
+++ b/src/ScadaLink.CLI/CliConfig.cs
@@ -2,9 +2,14 @@ using System.Text.Json;
namespace ScadaLink.CLI;
+///
+/// Resolved CLI configuration combining config file values, environment variable overrides, and per-invocation credentials.
+///
public class CliConfig
{
+ /// Base URL of the ScadaLink Management API (e.g. http://localhost:9000).
public string? ManagementUrl { get; set; }
+ /// Default output format for CLI commands; defaults to "json".
public string DefaultFormat { get; set; } = "json";
///
@@ -21,6 +26,10 @@ public class CliConfig
///
public string? Password { get; set; }
+ ///
+ /// Loads CLI configuration by merging the config file, environment variables, and credential env vars.
+ ///
+ /// A populated instance.
public static CliConfig Load()
{
var config = new CliConfig();
@@ -66,7 +75,9 @@ public class CliConfig
private class CliConfigFile
{
+ /// Management API URL from the config file.
public string? ManagementUrl { get; set; }
+ /// Default output format from the config file.
public string? DefaultFormat { get; set; }
}
}
diff --git a/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs b/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs
index cde69a64..eb79980a 100644
--- a/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs
+++ b/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class ApiMethodCommands
{
+ ///
+ /// Builds the api-method CLI command group with subcommands for managing inbound API methods.
+ ///
+ /// Global option for the management URL.
+ /// Global option for the output format.
+ /// Global option for the authentication username.
+ /// Global option for the authentication password.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("api-method") { Description = "Manage inbound API methods" };
diff --git a/src/ScadaLink.CLI/Commands/AuditCommandHelpers.cs b/src/ScadaLink.CLI/Commands/AuditCommandHelpers.cs
index 876db62b..706ad97a 100644
--- a/src/ScadaLink.CLI/Commands/AuditCommandHelpers.cs
+++ b/src/ScadaLink.CLI/Commands/AuditCommandHelpers.cs
@@ -9,12 +9,37 @@ namespace ScadaLink.CLI.Commands;
///
public sealed class AuditConnection
{
+ ///
+ /// The management URL, or null if resolution failed.
+ ///
public string? Url { get; init; }
+
+ ///
+ /// The username for authentication, or null if resolution failed.
+ ///
public string? Username { get; init; }
+
+ ///
+ /// The password for authentication, or null if resolution failed.
+ ///
public string? Password { get; init; }
+
+ ///
+ /// Error message if resolution failed, or null.
+ ///
public string? Error { get; init; }
+
+ ///
+ /// Error code if resolution failed, or null.
+ ///
public string? ErrorCode { get; init; }
+ ///
+ /// Creates a failed connection with an error message and code.
+ ///
+ /// The error message.
+ /// The error code.
+ /// A failed AuditConnection.
public static AuditConnection Fail(string error, string code)
=> new() { Error = error, ErrorCode = code };
}
@@ -28,6 +53,14 @@ public sealed class AuditConnection
///
public static class AuditCommandHelpers
{
+ ///
+ /// Resolves management API connection details from command line arguments, config file, or environment variables.
+ ///
+ /// The parsed command line arguments.
+ /// The URL option.
+ /// The username option.
+ /// The password option.
+ /// The resolved connection details, or a failure result.
public static AuditConnection ResolveConnection(
ParseResult result,
Option urlOption,
@@ -67,6 +100,12 @@ public static class AuditCommandHelpers
return new AuditConnection { Url = url, Username = username, Password = password };
}
+ ///
+ /// Resolves the output format from command line arguments, config file, or defaults to "table".
+ ///
+ /// The parsed command line arguments.
+ /// The format option.
+ /// The resolved format string.
public static string ResolveFormat(ParseResult result, Option formatOption)
=> CommandHelpers.ResolveFormat(result, formatOption, CliConfig.Load());
}
diff --git a/src/ScadaLink.CLI/Commands/AuditCommands.cs b/src/ScadaLink.CLI/Commands/AuditCommands.cs
index 2951e59e..1c7b0d13 100644
--- a/src/ScadaLink.CLI/Commands/AuditCommands.cs
+++ b/src/ScadaLink.CLI/Commands/AuditCommands.cs
@@ -11,6 +11,13 @@ namespace ScadaLink.CLI.Commands;
///
public static class AuditCommands
{
+ ///
+ /// Builds the audit command group with query, export, and verify-chain sub-commands.
+ ///
+ /// Global --url option for the management API endpoint.
+ /// Global --format option for output format.
+ /// Global --username option for authentication.
+ /// Global --password option for authentication.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("audit") { Description = "Query and export the centralized audit log" };
diff --git a/src/ScadaLink.CLI/Commands/AuditExportHelpers.cs b/src/ScadaLink.CLI/Commands/AuditExportHelpers.cs
index 4a36fc53..b693d4aa 100644
--- a/src/ScadaLink.CLI/Commands/AuditExportHelpers.cs
+++ b/src/ScadaLink.CLI/Commands/AuditExportHelpers.cs
@@ -13,15 +13,45 @@ namespace ScadaLink.CLI.Commands;
///
public sealed class AuditExportArgs
{
+ ///
+ /// Start timestamp for the export time window.
+ ///
public string Since { get; set; } = string.Empty;
+ ///
+ /// End timestamp for the export time window.
+ ///
public string Until { get; set; } = string.Empty;
+ ///
+ /// Export format (e.g., 'json', 'csv', 'parquet').
+ ///
public string Format { get; set; } = string.Empty;
+ ///
+ /// Output file path for the exported audit log.
+ ///
public string Output { get; set; } = string.Empty;
+ ///
+ /// Channel filter values (repeated query parameter).
+ ///
public string[] Channel { get; set; } = Array.Empty();
+ ///
+ /// Kind filter values (repeated query parameter).
+ ///
public string[] Kind { get; set; } = Array.Empty();
+ ///
+ /// Status filter values (repeated query parameter).
+ ///
public string[] Status { get; set; } = Array.Empty();
+ ///
+ /// Site identifier filter values (repeated query parameter).
+ ///
public string[] Site { get; set; } = Array.Empty();
+ ///
+ /// Optional target system filter.
+ ///
public string? Target { get; set; }
+ ///
+ /// Optional actor/user filter.
+ ///
public string? Actor { get; set; }
}
@@ -41,6 +71,8 @@ public static class AuditExportHelpers
/// server's multi-value IN (…) filter receives the full set — mirroring
/// .
///
+ /// The export arguments containing filters and format.
+ /// The current time for resolving relative time specifications.
public static string BuildQueryString(AuditExportArgs args, DateTimeOffset now)
{
var parts = new List();
@@ -79,6 +111,10 @@ public static class AuditExportHelpers
/// A 501 Not Implemented (parquet not yet supported server-side) prints the
/// server message and returns a non-zero exit code.
///
+ /// The management HTTP client for API communication.
+ /// The export arguments containing filters and output file path.
+ /// Text writer for command output messages.
+ /// The current time for resolving relative time specifications.
public static async Task RunExportAsync(
ManagementHttpClient client, AuditExportArgs args, TextWriter output, DateTimeOffset now)
{
diff --git a/src/ScadaLink.CLI/Commands/AuditFormatter.cs b/src/ScadaLink.CLI/Commands/AuditFormatter.cs
index ebfb600c..9cb053eb 100644
--- a/src/ScadaLink.CLI/Commands/AuditFormatter.cs
+++ b/src/ScadaLink.CLI/Commands/AuditFormatter.cs
@@ -10,6 +10,8 @@ namespace ScadaLink.CLI.Commands;
public interface IAuditFormatter
{
/// Renders one page of events. Called once per fetched page.
+ /// The audit events on this page.
+ /// Writer to render the formatted output to.
void WritePage(IReadOnlyList events, TextWriter output);
}
@@ -21,6 +23,7 @@ public sealed class JsonLinesAuditFormatter : IAuditFormatter
{
private static readonly JsonSerializerOptions Compact = new() { WriteIndented = false };
+ ///
public void WritePage(IReadOnlyList events, TextWriter output)
{
foreach (var evt in events)
@@ -35,6 +38,11 @@ public sealed class JsonLinesAuditFormatter : IAuditFormatter
///
public static class AuditFormatterFactory
{
+ ///
+ /// Returns an for the given format name.
+ ///
+ /// Format name; table selects the table formatter, any other value selects JSONL.
+ /// Writer for notice messages emitted during formatting.
public static IAuditFormatter Create(string format, TextWriter notices)
{
if (string.Equals(format, "table", StringComparison.OrdinalIgnoreCase))
diff --git a/src/ScadaLink.CLI/Commands/AuditLogCommands.cs b/src/ScadaLink.CLI/Commands/AuditLogCommands.cs
index eee61435..72551dab 100644
--- a/src/ScadaLink.CLI/Commands/AuditLogCommands.cs
+++ b/src/ScadaLink.CLI/Commands/AuditLogCommands.cs
@@ -32,6 +32,8 @@ public static class AuditLogCommands
/// an alias of audit-config — so this only adds the migration warning.
/// Factored out of Program.cs so it is unit-testable without spawning a process.
///
+ /// The raw command-line arguments passed to the CLI.
+ /// The text writer to emit the deprecation warning to.
public static void WriteDeprecationWarningIfNeeded(string[] args, TextWriter stderr)
{
if (args.Length > 0
@@ -41,6 +43,13 @@ public static class AuditLogCommands
}
}
+ ///
+ /// Builds the audit-config command (with the deprecated audit-log alias) and its subcommands.
+ ///
+ /// Global management URL option.
+ /// Global output format option.
+ /// Global username option.
+ /// Global password option.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("audit-config") { Description = "Query the configuration-change audit log" };
diff --git a/src/ScadaLink.CLI/Commands/AuditQueryHelpers.cs b/src/ScadaLink.CLI/Commands/AuditQueryHelpers.cs
index f8640ebc..173cce1e 100644
--- a/src/ScadaLink.CLI/Commands/AuditQueryHelpers.cs
+++ b/src/ScadaLink.CLI/Commands/AuditQueryHelpers.cs
@@ -15,18 +15,31 @@ namespace ScadaLink.CLI.Commands;
///
public sealed class AuditQueryArgs
{
+ /// Start time spec (relative like 1h, or absolute ISO-8601).
public string? Since { get; set; }
+ /// End time spec (relative like 7d, or absolute ISO-8601).
public string? Until { get; set; }
+ /// Multi-valued channel filter.
public string[] Channel { get; set; } = Array.Empty();
+ /// Multi-valued audit event kind filter.
public string[] Kind { get; set; } = Array.Empty();
+ /// Multi-valued status filter.
public string[] Status { get; set; } = Array.Empty();
+ /// Multi-valued site ID filter.
public string[] Site { get; set; } = Array.Empty();
+ /// Target system or service filter.
public string? Target { get; set; }
+ /// Actor (user or system) filter.
public string? Actor { get; set; }
+ /// Operation correlation ID filter.
public string? CorrelationId { get; set; }
+ /// Script execution ID filter.
public string? ExecutionId { get; set; }
+ /// Parent execution ID filter.
public string? ParentExecutionId { get; set; }
+ /// Filter for errors only (status=Failed).
public bool ErrorsOnly { get; set; }
+ /// Page size for pagination.
public int PageSize { get; set; } = 100;
}
@@ -45,6 +58,8 @@ public static class AuditQueryHelpers
/// relative offset (30s, 15m, 1h, 7d) interpreted as
/// minus the offset, or an absolute ISO-8601 timestamp.
///
+ /// The time specification string.
+ /// The current time used as reference for relative specs.
/// The spec is neither a known relative form nor a parseable ISO-8601 timestamp.
public static DateTimeOffset ResolveTimeSpec(string spec, DateTimeOffset now)
{
@@ -84,6 +99,10 @@ public static class AuditQueryHelpers
/// server's multi-value IN (…) filter receives the full set. --errors-only
/// maps to a single status=Failed and overrides any explicit --status.
///
+ /// The audit query arguments.
+ /// The current time for resolving relative time specs.
+ /// Optional keyset cursor timestamp.
+ /// Optional keyset cursor event ID.
public static string BuildQueryString(
AuditQueryArgs args, DateTimeOffset now, DateTimeOffset? afterOccurredAtUtc, string? afterEventId)
{
@@ -144,6 +163,12 @@ public static class AuditQueryHelpers
/// follows nextCursor until the server returns a null cursor. Returns the
/// process exit code (0 success, non-zero on HTTP/transport error).
///
+ /// The management HTTP client.
+ /// The audit query arguments.
+ /// Whether to follow pagination cursors.
+ /// The audit result formatter.
+ /// The output writer for results.
+ /// The current time for resolving relative time specs.
public static async Task RunQueryAsync(
ManagementHttpClient client,
AuditQueryArgs args,
diff --git a/src/ScadaLink.CLI/Commands/AuditVerifyChainHelpers.cs b/src/ScadaLink.CLI/Commands/AuditVerifyChainHelpers.cs
index 45aa9642..12cf41b5 100644
--- a/src/ScadaLink.CLI/Commands/AuditVerifyChainHelpers.cs
+++ b/src/ScadaLink.CLI/Commands/AuditVerifyChainHelpers.cs
@@ -13,6 +13,7 @@ public static class AuditVerifyChainHelpers
/// Returns true if is a well-formed YYYY-MM value
/// with a real month (01-12). A malformed month (e.g. 2026-13) is rejected.
///
+ /// The month string to validate in YYYY-MM format.
public static bool IsValidMonth(string? month)
=> !string.IsNullOrWhiteSpace(month)
&& DateTime.TryParseExact(month, "yyyy-MM", CultureInfo.InvariantCulture,
diff --git a/src/ScadaLink.CLI/Commands/BundleCommands.cs b/src/ScadaLink.CLI/Commands/BundleCommands.cs
index 0eb595ac..eb2002e8 100644
--- a/src/ScadaLink.CLI/Commands/BundleCommands.cs
+++ b/src/ScadaLink.CLI/Commands/BundleCommands.cs
@@ -15,6 +15,12 @@ public static class BundleCommands
{
private static readonly TimeSpan BundleCommandTimeout = TimeSpan.FromMinutes(5);
+ /// Builds the bundle command group with export, preview, and import sub-commands.
+ /// Shared management URL option.
+ /// Shared output format option.
+ /// Shared username option.
+ /// Shared password option.
+ /// The configured for the bundle group.
public static Command Build(
Option urlOption, Option formatOption,
Option usernameOption, Option passwordOption)
diff --git a/src/ScadaLink.CLI/Commands/CommandHelpers.cs b/src/ScadaLink.CLI/Commands/CommandHelpers.cs
index ccb73ed1..f1689fa8 100644
--- a/src/ScadaLink.CLI/Commands/CommandHelpers.cs
+++ b/src/ScadaLink.CLI/Commands/CommandHelpers.cs
@@ -7,6 +7,16 @@ namespace ScadaLink.CLI.Commands;
internal static class CommandHelpers
{
+ ///
+ /// Resolves the management URL, credentials, and output format, then sends
+ /// to the management API and returns the process exit code.
+ ///
+ /// Parsed command-line result from which option values are read.
+ /// Option that supplies the management URL override.
+ /// Option that supplies the output format override.
+ /// Option that supplies the username override.
+ /// Option that supplies the password override.
+ /// The management command object to send.
internal static async Task ExecuteCommandAsync(
ParseResult result,
Option urlOption,
@@ -69,6 +79,9 @@ internal static class CommandHelpers
/// is used, otherwise json. The --format option must not declare a
/// DefaultValueFactory — that would mask whether the flag was supplied.
///
+ /// Parsed command-line result.
+ /// The --format option definition.
+ /// Loaded CLI configuration providing the default format fallback.
internal static string ResolveFormat(ParseResult result, Option formatOption, CliConfig config)
{
// GetResult returns non-null only when the option was actually present on the
@@ -87,6 +100,8 @@ internal static class CommandHelpers
/// Resolves a single credential: an explicit command-line value wins, otherwise the
/// environment-variable fallback (from ) is used.
///
+ /// Value supplied on the command line, or null if absent.
+ /// Fallback value from the config file or environment variable.
internal static string? ResolveCredential(string? commandLineValue, string? envValue)
=> string.IsNullOrWhiteSpace(commandLineValue) ? envValue : commandLineValue;
@@ -96,6 +111,7 @@ internal static class CommandHelpers
/// new Uri(...) in the constructor and throw
/// an unhandled .
///
+ /// URL string to validate.
internal static bool IsValidManagementUrl(string? url)
{
if (string.IsNullOrWhiteSpace(url))
@@ -105,6 +121,11 @@ internal static class CommandHelpers
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}
+ ///
+ /// Writes the management response to stdout and returns the appropriate process exit code.
+ ///
+ /// Response received from the management API.
+ /// Output format (json or table).
internal static int HandleResponse(ManagementResponse response, string format)
{
if (response.JsonData != null)
diff --git a/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs b/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs
index e9e66846..059bed60 100644
--- a/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs
+++ b/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class DataConnectionCommands
{
+ ///
+ /// Builds the data-connection command group and all its subcommands.
+ ///
+ /// Global management URL option.
+ /// Global output format option.
+ /// Global username option.
+ /// Global password option.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("data-connection") { Description = "Manage data connections" };
diff --git a/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs b/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs
index 4bbe2342..af77f32b 100644
--- a/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs
+++ b/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs
@@ -4,8 +4,17 @@ using ScadaLink.Commons.Messages.Management;
namespace ScadaLink.CLI.Commands;
+///
+/// CLI commands for managing database connection definitions.
+///
public static class DbConnectionCommands
{
+ /// Builds the db-connection command with list, get, create, update, and delete sub-commands.
+ /// Global URL option.
+ /// Global output format option.
+ /// Global username option.
+ /// Global password option.
+ /// The configured db-connection command.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("db-connection") { Description = "Manage database connections" };
diff --git a/src/ScadaLink.CLI/Commands/DebugCommands.cs b/src/ScadaLink.CLI/Commands/DebugCommands.cs
index e080df7b..ba6b55b1 100644
--- a/src/ScadaLink.CLI/Commands/DebugCommands.cs
+++ b/src/ScadaLink.CLI/Commands/DebugCommands.cs
@@ -10,6 +10,11 @@ namespace ScadaLink.CLI.Commands;
public static class DebugCommands
{
+ /// Builds the debug command with its subcommands using the given shared CLI options.
+ /// Shared management URL option.
+ /// Shared output format option.
+ /// Shared username option for authentication.
+ /// Shared password option for authentication.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("debug") { Description = "Runtime debugging" };
diff --git a/src/ScadaLink.CLI/Commands/DebugStreamHelpers.cs b/src/ScadaLink.CLI/Commands/DebugStreamHelpers.cs
index 845e428c..1a6bac1d 100644
--- a/src/ScadaLink.CLI/Commands/DebugStreamHelpers.cs
+++ b/src/ScadaLink.CLI/Commands/DebugStreamHelpers.cs
@@ -25,6 +25,8 @@ internal static class DebugStreamHelpers
/// (Ctrl+C during connect) is a graceful shutdown — exit 0, no error printed.
/// Anything else is a genuine connection failure — exit 1.
///
+ /// The exception thrown by HubConnection.StartAsync.
+ /// True when the user requested cancellation (Ctrl+C) before the exception was thrown.
internal static ConnectFailure ClassifyConnectFailure(Exception ex, bool cancellationRequested)
{
if (cancellationRequested && ex is OperationCanceledException)
@@ -40,6 +42,7 @@ internal static class DebugStreamHelpers
/// a brief grace period covers a termination that races with cancellation. If no
/// result is ever produced (pure Ctrl+C), the stream ended gracefully — exit 0.
///
+ /// The task whose result is the intended exit code, set by OnStreamTerminated or the Closed handler.
internal static async Task ResolveStreamExitCodeAsync(Task exitTask)
{
if (exitTask.IsCompletedSuccessfully)
diff --git a/src/ScadaLink.CLI/Commands/DeployCommands.cs b/src/ScadaLink.CLI/Commands/DeployCommands.cs
index 5957aac0..6e587e88 100644
--- a/src/ScadaLink.CLI/Commands/DeployCommands.cs
+++ b/src/ScadaLink.CLI/Commands/DeployCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class DeployCommands
{
+ ///
+ /// Builds the deploy command group with all sub-commands.
+ ///
+ /// Global management URL option.
+ /// Global output format option.
+ /// Global username option.
+ /// Global password option.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("deploy") { Description = "Deployment operations" };
diff --git a/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs b/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs
index 30a58956..4a32a3bb 100644
--- a/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs
+++ b/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class ExternalSystemCommands
{
+ ///
+ /// Builds the external-system CLI command group with subcommands for managing external systems.
+ ///
+ /// Global option for the management URL.
+ /// Global option for the output format.
+ /// Global option for the authentication username.
+ /// Global option for the authentication password.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("external-system") { Description = "Manage external systems" };
diff --git a/src/ScadaLink.CLI/Commands/HealthCommands.cs b/src/ScadaLink.CLI/Commands/HealthCommands.cs
index f3ac6e45..c387345a 100644
--- a/src/ScadaLink.CLI/Commands/HealthCommands.cs
+++ b/src/ScadaLink.CLI/Commands/HealthCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class HealthCommands
{
+ ///
+ /// Builds the health command group with summary, site, event-log, and parked-message sub-commands.
+ ///
+ /// Global --url option for the management API endpoint.
+ /// Global --format option for output format.
+ /// Global --username option for authentication.
+ /// Global --password option for authentication.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("health") { Description = "Health monitoring" };
diff --git a/src/ScadaLink.CLI/Commands/InstanceCommands.cs b/src/ScadaLink.CLI/Commands/InstanceCommands.cs
index fd77b261..955b256e 100644
--- a/src/ScadaLink.CLI/Commands/InstanceCommands.cs
+++ b/src/ScadaLink.CLI/Commands/InstanceCommands.cs
@@ -6,6 +6,14 @@ namespace ScadaLink.CLI.Commands;
public static class InstanceCommands
{
+ ///
+ /// Builds the instance command and its subcommands.
+ ///
+ /// The URL option.
+ /// The format option.
+ /// The username option.
+ /// The password option.
+ /// The instance command with all subcommands.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("instance") { Description = "Manage instances" };
@@ -71,6 +79,10 @@ public static class InstanceCommands
/// throwing when the JSON is malformed, a pair has the wrong arity, or an element
/// has the wrong type.
///
+ /// The JSON string to parse.
+ /// The parsed bindings list, or null if parsing fails.
+ /// The error message if parsing fails, or null on success.
+ /// True if parsing succeeded; false otherwise.
internal static bool TryParseBindings(
string json,
out List? bindings,
@@ -126,6 +138,10 @@ public static class InstanceCommands
/// false with a descriptive instead of throwing
/// when the JSON is malformed or null.
///
+ /// The JSON string to parse.
+ /// The parsed overrides dictionary, or null if parsing fails.
+ /// The error message if parsing fails, or null on success.
+ /// True if parsing succeeded; false otherwise.
internal static bool TryParseOverrides(
string json,
out Dictionary? overrides,
diff --git a/src/ScadaLink.CLI/Commands/NotificationCommands.cs b/src/ScadaLink.CLI/Commands/NotificationCommands.cs
index 685599b3..7cddf94b 100644
--- a/src/ScadaLink.CLI/Commands/NotificationCommands.cs
+++ b/src/ScadaLink.CLI/Commands/NotificationCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class NotificationCommands
{
+ ///
+ /// Builds the notification command group with sub-commands for managing notification lists and SMTP configuration.
+ ///
+ /// Global --url option for the management API endpoint.
+ /// Global --format option for output format.
+ /// Global --username option for authentication.
+ /// Global --password option for authentication.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("notification") { Description = "Manage notification lists" };
@@ -123,6 +130,7 @@ public static class NotificationCommands
/// invocation. The optional --tls-mode / --credentials flags map to
/// null when omitted so the server-side handler preserves the existing values.
///
+ /// The parsed command-line result from the smtp update invocation.
internal static UpdateSmtpConfigCommand BuildUpdateSmtpConfigCommand(ParseResult result)
{
var id = result.GetValue(SmtpIdOption);
diff --git a/src/ScadaLink.CLI/Commands/SecurityCommands.cs b/src/ScadaLink.CLI/Commands/SecurityCommands.cs
index deea7e65..68b2b729 100644
--- a/src/ScadaLink.CLI/Commands/SecurityCommands.cs
+++ b/src/ScadaLink.CLI/Commands/SecurityCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class SecurityCommands
{
+ ///
+ /// Builds the security command group with API key, role mapping, and scope rule subcommands.
+ ///
+ /// Shared management URL option.
+ /// Shared output format option.
+ /// Shared username option for authentication.
+ /// Shared password option for authentication.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("security") { Description = "Manage security settings" };
diff --git a/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs b/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs
index e64767af..c64577c3 100644
--- a/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs
+++ b/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs
@@ -6,6 +6,12 @@ namespace ScadaLink.CLI.Commands;
public static class SharedScriptCommands
{
+ /// Builds the shared-script command group with list, get, create, update, and delete sub-commands.
+ /// Shared management URL option.
+ /// Shared output format option.
+ /// Shared username option.
+ /// Shared password option.
+ /// The configured for the shared-script group.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("shared-script") { Description = "Manage shared scripts" };
diff --git a/src/ScadaLink.CLI/Commands/SiteCommands.cs b/src/ScadaLink.CLI/Commands/SiteCommands.cs
index ff43eff4..a8cf32e4 100644
--- a/src/ScadaLink.CLI/Commands/SiteCommands.cs
+++ b/src/ScadaLink.CLI/Commands/SiteCommands.cs
@@ -6,6 +6,13 @@ namespace ScadaLink.CLI.Commands;
public static class SiteCommands
{
+ ///
+ /// Builds the site command group and all its subcommands.
+ ///
+ /// Global management URL option.
+ /// Global output format option.
+ /// Global username option.
+ /// Global password option.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("site") { Description = "Manage sites" };
diff --git a/src/ScadaLink.CLI/Commands/TableAuditFormatter.cs b/src/ScadaLink.CLI/Commands/TableAuditFormatter.cs
index 98c9b6c5..dbf85b15 100644
--- a/src/ScadaLink.CLI/Commands/TableAuditFormatter.cs
+++ b/src/ScadaLink.CLI/Commands/TableAuditFormatter.cs
@@ -28,6 +28,7 @@ public sealed class TableAuditFormatter : IAuditFormatter
("httpStatus", "HttpStatus", 10),
};
+ ///
public void WritePage(IReadOnlyList events, TextWriter output)
{
// Build every cell first so column widths account for the actual data.
diff --git a/src/ScadaLink.CLI/Commands/TemplateCommands.cs b/src/ScadaLink.CLI/Commands/TemplateCommands.cs
index dd9a4cd6..69fd92c8 100644
--- a/src/ScadaLink.CLI/Commands/TemplateCommands.cs
+++ b/src/ScadaLink.CLI/Commands/TemplateCommands.cs
@@ -6,6 +6,11 @@ namespace ScadaLink.CLI.Commands;
public static class TemplateCommands
{
+ /// Builds the template command with its subcommands using the given shared CLI options.
+ /// Shared management URL option.
+ /// Shared output format option.
+ /// Shared username option for authentication.
+ /// Shared password option for authentication.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("template") { Description = "Manage templates" };
diff --git a/src/ScadaLink.CLI/ManagementHttpClient.cs b/src/ScadaLink.CLI/ManagementHttpClient.cs
index ce8e02bd..6f8882f2 100644
--- a/src/ScadaLink.CLI/ManagementHttpClient.cs
+++ b/src/ScadaLink.CLI/ManagementHttpClient.cs
@@ -8,6 +8,12 @@ public class ManagementHttpClient : IDisposable
{
private readonly HttpClient _httpClient;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The base URL for the management API.
+ /// The username for HTTP Basic authentication.
+ /// The password for HTTP Basic authentication.
public ManagementHttpClient(string baseUrl, string username, string password)
: this(new HttpClient(), baseUrl, username, password)
{
@@ -18,6 +24,10 @@ public class ManagementHttpClient : IDisposable
/// over a stub ) so the request/response handling can
/// be exercised without a live server.
///
+ /// The HTTP client to use for requests.
+ /// The base URL for the management API.
+ /// The username for HTTP Basic authentication.
+ /// The password for HTTP Basic authentication.
internal ManagementHttpClient(HttpClient httpClient, string baseUrl, string username, string password)
{
_httpClient = httpClient;
@@ -27,6 +37,13 @@ public class ManagementHttpClient : IDisposable
new AuthenticationHeaderValue("Basic", credentials);
}
+ ///
+ /// Sends a management command to the management API.
+ ///
+ /// The command name to execute.
+ /// The command payload.
+ /// The request timeout.
+ /// A management response containing status and data.
public async Task SendCommandAsync(string commandName, object payload, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
@@ -82,6 +99,8 @@ public class ManagementHttpClient : IDisposable
/// REST resources. Authentication (HTTP Basic) and the base address are shared.
///
/// Path relative to the base URL, with query string.
+ /// The request timeout.
+ /// A management response containing status and data.
public async Task SendGetAsync(string relativePath, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
@@ -130,9 +149,15 @@ public class ManagementHttpClient : IDisposable
/// disposing the returned message. The
/// option ensures the body is not pre-buffered.
///
+ /// Path relative to the base URL, with query string.
+ /// A cancellation token that can be used to cancel the operation.
+ /// The raw HTTP response message for streaming.
public async Task SendGetStreamAsync(string relativePath, CancellationToken cancellationToken)
=> await _httpClient.GetAsync(relativePath, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ ///
+ /// Disposes the underlying HTTP client.
+ ///
public void Dispose() => _httpClient.Dispose();
}
diff --git a/src/ScadaLink.CLI/OutputFormatter.cs b/src/ScadaLink.CLI/OutputFormatter.cs
index 46342895..220e24c6 100644
--- a/src/ScadaLink.CLI/OutputFormatter.cs
+++ b/src/ScadaLink.CLI/OutputFormatter.cs
@@ -12,16 +12,24 @@ public static class OutputFormatter
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
+ /// Serializes to indented JSON and writes it to standard output.
+ /// The object to serialize; null is serialized as JSON null.
public static void WriteJson(object? data)
{
Console.WriteLine(JsonSerializer.Serialize(data, JsonOptions));
}
+ /// Writes a JSON error envelope with the given message and code to standard error.
+ /// Human-readable error description.
+ /// Machine-readable error code.
public static void WriteError(string message, string code)
{
Console.Error.WriteLine(JsonSerializer.Serialize(new { error = message, code }, JsonOptions));
}
+ /// Writes a plain-text padded table to standard output with the given column headers and data rows.
+ /// Data rows; each inner array corresponds to a column in the same order as .
+ /// Column header labels.
public static void WriteTable(IEnumerable rows, string[] headers)
{
var allRows = new List { headers };
diff --git a/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs b/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs
index a497dcb9..14206cc9 100644
--- a/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs
+++ b/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs
@@ -43,6 +43,9 @@ public static class AuditExportEndpoints
///
public const int DefaultMaxRows = 100_000;
+ /// Registers the audit log CSV export endpoint on the given route builder.
+ /// The endpoint route builder to register against.
+ /// The same instance for chaining.
public static IEndpointRouteBuilder MapAuditExportEndpoints(this IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/api/centralui/audit/export", HandleExportAsync)
@@ -56,6 +59,8 @@ public static class AuditExportEndpoints
/// tests can call it directly when desirable; the live wire-up goes
/// through the minimal-API map above.
///
+ /// The HTTP context for the current request.
+ /// The export service used to stream audit rows as CSV.
internal static async Task HandleExportAsync(HttpContext context, IAuditLogExportService exportService)
{
var filter = ParseFilter(context.Request.Query);
@@ -88,6 +93,7 @@ public static class AuditExportEndpoints
/// sourceSiteId. The divergence is deliberate — each endpoint matches
/// its own CLI / UI URL builder — so do NOT "fix" the two to one key name.
///
+ /// The query string parameters from the HTTP request.
internal static AuditLogQueryFilter ParseFilter(IQueryCollection query)
{
var channels = AuditQueryParamParsers.ParseEnumList(query["channel"]);
diff --git a/src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs b/src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs
index db1f09be..2990fa3b 100644
--- a/src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs
+++ b/src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs
@@ -15,6 +15,8 @@ namespace ScadaLink.CentralUI.Auth;
///
public static class AuthEndpoints
{
+ /// Registers the /auth/login, /auth/logout, and /auth/ping endpoints on the given route builder.
+ /// The route builder to add the endpoints to.
public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder endpoints)
{
endpoints.MapPost("/auth/login", async (HttpContext context) =>
@@ -155,6 +157,7 @@ public static class AuthEndpoints
/// cookie session is still valid and 401 once it has lapsed
/// server-side. See CentralUI-020.
///
+ /// The current HTTP context used to check authentication state and write the response.
public static Task HandlePing(HttpContext context)
{
context.Response.StatusCode = context.User.Identity?.IsAuthenticated == true
diff --git a/src/ScadaLink.CentralUI/Auth/ClaimsPrincipalExtensions.cs b/src/ScadaLink.CentralUI/Auth/ClaimsPrincipalExtensions.cs
index 4cd3ec58..e4c0b5c7 100644
--- a/src/ScadaLink.CentralUI/Auth/ClaimsPrincipalExtensions.cs
+++ b/src/ScadaLink.CentralUI/Auth/ClaimsPrincipalExtensions.cs
@@ -19,6 +19,7 @@ public static class ClaimsPrincipalExtensions
/// The audit username for , or
/// when the claim is absent.
///
+ /// The claims principal to read the username from.
public static string GetUsername(this ClaimsPrincipal principal)
=> principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? UnknownUser;
@@ -26,6 +27,7 @@ public static class ClaimsPrincipalExtensions
/// The display name for , or null when
/// the claim is absent.
///
+ /// The claims principal to read the display name from.
public static string? GetDisplayName(this ClaimsPrincipal principal)
=> principal.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value;
@@ -34,6 +36,7 @@ public static class ClaimsPrincipalExtensions
/// Replaces the GetCurrentUserAsync helper that was copy-pasted into
/// ten components (CentralUI-024).
///
+ /// The Blazor authentication state provider to read from.
public static async Task GetCurrentUsernameAsync(
this AuthenticationStateProvider authStateProvider)
{
diff --git a/src/ScadaLink.CentralUI/Auth/CookieAuthenticationStateProvider.cs b/src/ScadaLink.CentralUI/Auth/CookieAuthenticationStateProvider.cs
index 361cd09d..9c3d5ac4 100644
--- a/src/ScadaLink.CentralUI/Auth/CookieAuthenticationStateProvider.cs
+++ b/src/ScadaLink.CentralUI/Auth/CookieAuthenticationStateProvider.cs
@@ -28,6 +28,10 @@ public class CookieAuthenticationStateProvider : ServerAuthenticationStateProvid
{
private readonly Task _circuitAuthState;
+ ///
+ /// Snapshots the authenticated principal from the current HTTP context for use throughout the circuit lifetime.
+ ///
+ /// Accessor used to read the initial HTTP context principal.
public CookieAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
{
// Snapshot the principal at circuit-construction time. HttpContext is
@@ -38,6 +42,7 @@ public class CookieAuthenticationStateProvider : ServerAuthenticationStateProvid
_circuitAuthState = Task.FromResult(new AuthenticationState(user));
}
+ ///
public override Task GetAuthenticationStateAsync()
=> _circuitAuthState;
}
diff --git a/src/ScadaLink.CentralUI/Auth/SiteScopeService.cs b/src/ScadaLink.CentralUI/Auth/SiteScopeService.cs
index 44ba11d0..d3ab48c5 100644
--- a/src/ScadaLink.CentralUI/Auth/SiteScopeService.cs
+++ b/src/ScadaLink.CentralUI/Auth/SiteScopeService.cs
@@ -27,6 +27,8 @@ public sealed class SiteScopeService
private readonly AuthenticationStateProvider _authStateProvider;
private (bool IsSystemWide, IReadOnlySet Sites)? _cached;
+ /// Initializes a new instance of .
+ /// The Blazor authentication state provider used to read the current user's claims.
public SiteScopeService(AuthenticationStateProvider authStateProvider)
{
_authStateProvider = authStateProvider;
@@ -51,6 +53,7 @@ public sealed class SiteScopeService
/// Returns the subset of the user is permitted to
/// see. A system-wide user gets the full list back unchanged.
///
+ /// The full set of sites to filter.
public async Task> FilterSitesAsync(IEnumerable sites)
{
var (isSystemWide, allowed) = await ResolveAsync();
@@ -63,6 +66,7 @@ public sealed class SiteScopeService
/// True when the user may operate on the site with the given Site.Id.
/// Must be re-checked server-side before any mutating cross-site command.
///
+ /// The Site.Id to check.
public async Task IsSiteAllowedAsync(int siteId)
{
var (isSystemWide, allowed) = await ResolveAsync();
diff --git a/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs b/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs
index 451bccf1..c4a15522 100644
--- a/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs
@@ -63,6 +63,7 @@ public partial class AuditFilterBar
///
[Parameter] public string? InitialInstanceSearch { get; set; }
+ ///
protected override async Task OnInitializedAsync()
{
// One-shot prefill from a drill-in deep link. Subsequent parameter changes
diff --git a/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs b/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs
index 3864e5b1..84509e29 100644
--- a/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs
+++ b/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs
@@ -33,9 +33,13 @@ namespace ScadaLink.CentralUI.Components.Audit;
///
public sealed class AuditQueryModel
{
+ /// Selected channel filter chips; empty means all channels.
public HashSet Channels { get; } = new();
+ /// Selected kind filter chips; empty means all kinds.
public HashSet Kinds { get; } = new();
+ /// Selected status filter chips; empty means all statuses.
public HashSet Statuses { get; } = new();
+ /// Selected source-site identifier chips; empty means all sites.
public HashSet SiteIdentifiers { get; } = new(StringComparer.OrdinalIgnoreCase);
///
@@ -46,13 +50,20 @@ public sealed class AuditQueryModel
///
public HashSet SourceNodes { get; } = new(StringComparer.OrdinalIgnoreCase);
+ /// Selected time-range preset controlling which historical window is queried.
public AuditTimeRangePreset TimeRange { get; set; } = AuditTimeRangePreset.LastHour;
+ /// Custom start of the time window; used only when is .
public DateTime? CustomFromUtc { get; set; }
+ /// Custom end of the time window; used only when is .
public DateTime? CustomToUtc { get; set; }
+ /// Free-text filter applied to instance names (UI-only; dropped when converting to ).
public string InstanceSearch { get; set; } = string.Empty;
+ /// Free-text filter applied to script names (UI-only; dropped when converting to ).
public string ScriptSearch { get; set; } = string.Empty;
+ /// Free-text filter applied to the target field (external system / DB name / notification list).
public string TargetSearch { get; set; } = string.Empty;
+ /// Free-text filter applied to the actor field (instance or inbound API key name).
public string ActorSearch { get; set; } = string.Empty;
///
@@ -72,6 +83,7 @@ public sealed class AuditQueryModel
///
public string ParentExecutionId { get; set; } = string.Empty;
+ /// When true and no explicit status chips are selected, the filter targets the full non-success status set.
public bool ErrorsOnly { get; set; }
///
@@ -133,6 +145,8 @@ public sealed class AuditQueryModel
/// multi-select maps straight through to its filter list (an empty set yields
/// null — "do not constrain"). See class doc for the Errors-only rule.
///
+ /// The current UTC timestamp used to compute relative time-range windows.
+ /// A populated ready for the repository.
public AuditLogQueryFilter ToFilter(DateTime utcNow)
{
var statuses = ResolveStatuses();
diff --git a/src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor.cs b/src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor.cs
index 981b714b..cb965eba 100644
--- a/src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor.cs
@@ -183,6 +183,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
? $"--audit-col-width: {width}px;"
: string.Empty;
+ ///
protected override async Task OnParametersSetAsync()
{
// Reset & reload whenever the filter reference changes. AuditLogQueryFilter
@@ -255,6 +256,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
}
}
+ ///
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
@@ -366,6 +368,8 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// JS callback: the user finished resizing a column. Persists the new
/// per-column width and re-renders so the body cells track the header.
///
+ /// The stable key of the resized column.
+ /// The new column width in pixels.
[JSInvokable]
public async Task OnColumnResized(string columnKey, int widthPx)
{
@@ -384,6 +388,8 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// header of . Moves the dragged column into the
/// target's slot, persists the resulting order, and re-renders.
///
+ /// The stable key of the column being dragged.
+ /// The stable key of the target column drop slot.
[JSInvokable]
public async Task OnColumnReordered(string fromKey, string toKey)
{
@@ -422,6 +428,9 @@ public partial class AuditResultsGrid : IAsyncDisposable
}
}
+ ///
+ /// Releases the .NET object reference held for JS interop callbacks.
+ ///
public ValueTask DisposeAsync()
{
_selfRef?.Dispose();
diff --git a/src/ScadaLink.CentralUI/Components/Audit/ExecutionDetailModal.razor.cs b/src/ScadaLink.CentralUI/Components/Audit/ExecutionDetailModal.razor.cs
index 43d10946..1eebad72 100644
--- a/src/ScadaLink.CentralUI/Components/Audit/ExecutionDetailModal.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Audit/ExecutionDetailModal.razor.cs
@@ -80,6 +80,7 @@ public partial class ExecutionDetailModal
///
private const int RowPageSize = 100;
+ ///
protected override async Task OnParametersSetAsync()
{
// Load only on the closed → open transition. A re-render while already
diff --git a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs
index fc773f79..fe3a9c73 100644
--- a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs
@@ -100,6 +100,7 @@ public partial class ExecutionTree
// the whole chain is shown on arrival so the user sees the full picture.
private readonly HashSet _collapsed = new();
+ ///
protected override void OnParametersSet()
{
// Nested instance: the parent already assembled our subtrees.
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs
index b7c7a5c2..4a0363db 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs
@@ -54,6 +54,7 @@ public partial class AuditLogPage : IDisposable
private bool _drawerOpen;
private string? _initialInstanceSearch;
+ ///
protected override void OnInitialized()
{
ApplyQueryStringFilters();
@@ -81,6 +82,7 @@ public partial class AuditLogPage : IDisposable
});
}
+ /// Unsubscribes from navigation events to prevent memory leaks when the component is removed.
public void Dispose()
{
Navigation.LocationChanged -= HandleLocationChanged;
@@ -248,6 +250,10 @@ public partial class AuditLogPage : IDisposable
///
internal string ExportUrl => BuildExportUrl(_currentFilter);
+ ///
+ /// Builds the CSV export URL for the given filter, encoding all active filter dimensions as query parameters.
+ ///
+ /// Currently applied filter; null returns the bare export endpoint.
internal static string BuildExportUrl(AuditLogQueryFilter? filter)
{
const string basePath = "/api/centralui/audit/export";
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor.cs
index b760939a..2d3211a8 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Pages/Audit/ExecutionTreePage.razor.cs
@@ -49,6 +49,7 @@ public partial class ExecutionTreePage
private Guid? _modalExecutionId;
private bool _modalOpen;
+ ///
protected override async Task OnInitializedAsync()
{
_executionId = ParseExecutionId();
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TransportExport.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/Design/TransportExport.razor.cs
index c968e474..fbefbbf3 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Design/TransportExport.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TransportExport.razor.cs
@@ -105,6 +105,7 @@ public partial class TransportExport : ComponentBase
private bool _downloadInProgress;
private string? _downloadError;
+ ///
protected override async Task OnInitializedAsync()
{
await LoadAllAsync();
@@ -181,6 +182,7 @@ public partial class TransportExport : ComponentBase
/// to colour an inline strength meter; never used to gate the export — the
/// importer enforces its own strength + lockout policies.
///
+ /// The passphrase string to score.
internal static int PassphraseStrength(string s)
{
if (string.IsNullOrEmpty(s)) return 0;
@@ -259,6 +261,7 @@ public partial class TransportExport : ComponentBase
/// envelope-encrypt. Surfaces in the Step 3 warning banner so the user
/// knows exactly what an unencrypted export would leak.
///
+ /// The resolved export closure whose secret fields are counted.
internal static int CountSecrets(ResolvedExport resolved)
{
var count = 0;
@@ -363,6 +366,8 @@ public partial class TransportExport : ComponentBase
/// odd chars in TransportOptions.SourceEnvironment don't produce
/// browser-rejected filenames.
///
+ /// The environment label to embed in the filename (sanitised to filename-safe characters).
+ /// Timestamp to use for the datetime segment; defaults to when null.
internal static string BuildFilename(string sourceEnvironment, DateTimeOffset? nowUtc = null)
{
var safe = SanitizeForFilename(sourceEnvironment);
@@ -420,6 +425,10 @@ public partial class TransportExport : ComponentBase
/// Items that are in but NOT in —
/// the auto-included dependencies the resolver pulled in for the user.
///
+ /// The element type of the artifact list.
+ /// The full resolved list including both seed and auto-included items.
+ /// The set of explicitly selected item ids.
+ /// Function that extracts the integer id from an item.
internal static IReadOnlyList AutoIncluded(IReadOnlyList all, IReadOnlyCollection seed, Func idOf)
{
return all.Where(x => !seed.Contains(idOf(x))).ToList();
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs
index b6078b37..f18394c6 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs
@@ -353,6 +353,8 @@ public partial class TransportImport : ComponentBase
///
/// Visible to tests via internal so the default-mapping contract is unit-pinned.
///
+ /// The import preview containing all conflict items to map.
+ /// A dictionary keyed by (EntityType, Name) with default resolution actions populated.
internal static Dictionary<(string EntityType, string Name), ImportResolution> BuildDefaultResolutions(
ImportPreview preview)
{
diff --git a/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs
index 726a0252..9d0cf91d 100644
--- a/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs
@@ -89,6 +89,7 @@ public partial class SiteCallsReport
private bool HasNextPage => _nextCursor is not null;
+ ///
protected override async Task OnInitializedAsync()
{
try
diff --git a/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs b/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs
index 457d0bb2..bf514b88 100644
--- a/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs
+++ b/src/ScadaLink.CentralUI/Components/Shared/AlarmTriggerConfigCodec.cs
@@ -31,6 +31,9 @@ internal static class AlarmTriggerConfigCodec
/// type. Returns a model with default values on null/empty/malformed input
/// or for missing keys — never throws.
///
+ /// The trigger configuration JSON string, or null/empty for defaults.
+ /// The alarm trigger type that determines which properties to extract.
+ /// A populated AlarmTriggerModel with default values for missing fields.
internal static AlarmTriggerModel Parse(string? json, AlarmTriggerType type)
{
var model = new AlarmTriggerModel();
@@ -115,6 +118,9 @@ internal static class AlarmTriggerConfigCodec
/// current trigger type. Expression is not bound to a single
/// attribute, so attributeName is omitted for it.
///
+ /// The AlarmTriggerModel to serialize.
+ /// The alarm trigger type determining which properties to serialize.
+ /// The serialized JSON representation of the model.
internal static string Serialize(AlarmTriggerModel model, AlarmTriggerType type)
{
using var stream = new MemoryStream();
@@ -174,6 +180,11 @@ internal static class AlarmTriggerConfigCodec
return Encoding.UTF8.GetString(stream.ToArray());
}
+ ///
+ /// Normalizes a direction string to one of: rising, falling, or either.
+ ///
+ /// The raw direction string to normalize.
+ /// Normalized direction: "rising", "falling", or "either".
internal static string NormalizeDirection(string? raw) => raw?.ToLowerInvariant() switch
{
"rising" or "up" or "positive" => "rising",
@@ -213,47 +224,122 @@ internal static class AlarmTriggerConfigCodec
internal sealed class AlarmTriggerModel
{
+ ///
+ /// The attribute name bound to this trigger.
+ ///
public string? AttributeName { get; set; }
// ValueMatch
+ ///
+ /// The value to match against the attribute for ValueMatch triggers.
+ ///
public string? MatchValue { get; set; }
+ ///
+ /// Indicates whether the match should be inverted (not equal) for ValueMatch triggers.
+ ///
public bool NotEquals { get; set; }
// RangeViolation
+ ///
+ /// The minimum threshold for RangeViolation triggers.
+ ///
public double? Min { get; set; }
+ ///
+ /// The maximum threshold for RangeViolation triggers.
+ ///
public double? Max { get; set; }
// RateOfChange
+ ///
+ /// The threshold per second for RateOfChange triggers.
+ ///
public double? ThresholdPerSecond { get; set; }
+ ///
+ /// The time window in seconds for RateOfChange rate calculation.
+ ///
public double? WindowSeconds { get; set; }
+ ///
+ /// The direction of change: "rising", "falling", or "either" for RateOfChange triggers.
+ ///
public string Direction { get; set; } = "either";
// HiLo — any subset of setpoints may be set; per-setpoint priorities
// override the alarm-level priority for that band.
+ ///
+ /// The low-low setpoint for HiLo triggers.
+ ///
public double? LoLo { get; set; }
+ ///
+ /// The low setpoint for HiLo triggers.
+ ///
public double? Lo { get; set; }
+ ///
+ /// The high setpoint for HiLo triggers.
+ ///
public double? Hi { get; set; }
+ ///
+ /// The high-high setpoint for HiLo triggers.
+ ///
public double? HiHi { get; set; }
+ ///
+ /// The priority for low-low alarm state.
+ ///
public int? LoLoPriority { get; set; }
+ ///
+ /// The priority for low alarm state.
+ ///
public int? LoPriority { get; set; }
+ ///
+ /// The priority for high alarm state.
+ ///
public int? HiPriority { get; set; }
+ ///
+ /// The priority for high-high alarm state.
+ ///
public int? HiHiPriority { get; set; }
// Hysteresis: optional deactivation deadband per setpoint. Once at the
// band, the setpoint threshold is relaxed by this amount before the alarm
// de-escalates. Prevents flapping when the value hovers at the boundary.
+ ///
+ /// The deadband for low-low alarm de-escalation.
+ ///
public double? LoLoDeadband { get; set; }
+ ///
+ /// The deadband for low alarm de-escalation.
+ ///
public double? LoDeadband { get; set; }
+ ///
+ /// The deadband for high alarm de-escalation.
+ ///
public double? HiDeadband { get; set; }
+ ///
+ /// The deadband for high-high alarm de-escalation.
+ ///
public double? HiHiDeadband { get; set; }
// Per-band operator message. Optional; surfaces on AlarmStateChanged.Message
// and may be used by notification routing or operator displays.
+ ///
+ /// The operator message for low-low alarm state.
+ ///
public string? LoLoMessage { get; set; }
+ ///
+ /// The operator message for low alarm state.
+ ///
public string? LoMessage { get; set; }
+ ///
+ /// The operator message for high alarm state.
+ ///
public string? HiMessage { get; set; }
+ ///
+ /// The operator message for high-high alarm state.
+ ///
public string? HiHiMessage { get; set; }
// Expression — boolean C# expression evaluated on attribute updates.
+ ///
+ /// The boolean C# expression to evaluate for Expression triggers.
+ ///
public string? Expression { get; set; }
}
diff --git a/src/ScadaLink.CentralUI/Components/Shared/DialogService.cs b/src/ScadaLink.CentralUI/Components/Shared/DialogService.cs
index ae38c94c..c2f28abe 100644
--- a/src/ScadaLink.CentralUI/Components/Shared/DialogService.cs
+++ b/src/ScadaLink.CentralUI/Components/Shared/DialogService.cs
@@ -32,6 +32,7 @@ public class DialogService : IDialogService
// (the Blazor renderer's, for an event-handler caller).
private TaskCompletionSource