docs(comments): strip internal task/milestone/bundle bookkeeping from code comments

Remove project bookkeeping citations from shipped code comments across the
solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/
issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels,
and C/D/K/S/T phase labels.

Comment text only — no code logic, string/log literals, or XML-doc structure
changed. Genuine descriptions are preserved (only the citation is stripped),
and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365,
UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker
TaskReferenceInComment / TrackingReferenceInComment checks plus targeted
grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
Joseph Doherty
2026-07-07 11:03:26 -04:00
parent 67005ca4c0
commit 9cff87fe85
435 changed files with 2338 additions and 2547 deletions
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T8, T9) — central singleton implementation of
/// Central singleton implementation of
/// <see cref="IAuditCentralHealthSnapshot"/>. Owns thread-safe
/// <see cref="System.Threading.Interlocked"/> counters for
/// <c>CentralAuditWriteFailures</c> + <c>AuditRedactionFailure</c> and a
@@ -71,7 +71,7 @@ public class AuditLogIngestActor : ReceiveActor
_logger = logger;
ReceiveAsync<IngestAuditEventsCommand>(OnIngestAsync);
// The single-repository test ctor cannot service the M3 dual-write —
// The single-repository test ctor cannot service the dual-write —
// it has no SiteCalls repo and no DbContext. The handler still
// registers (so callers don't dead-letter) but replies empty so the
// test surface stays explicit about what this ctor supports.
@@ -125,7 +125,7 @@ public class AuditLogIngestActor : ReceiveActor
// InsertIfNotExistsAsync. The single-repository test ctor has no
// service provider — it falls through with no redactor, which preserves
// the small-payload assumptions baked into the existing fixtures.
// AuditLog-003: use CreateAsyncScope + await using so scoped EF Core
// Use CreateAsyncScope + await using so scoped EF Core
// services (IAsyncDisposable DbContexts) dispose asynchronously
// without blocking on sync Dispose() of pending connection cleanup.
if (_injectedRepository is not null)
@@ -135,7 +135,7 @@ public class AuditLogIngestActor : ReceiveActor
}
else
{
// AuditLog-014: guard scope-creation + repository resolution in a
// Guard scope-creation + repository resolution in a
// try/catch, mirroring OnCachedTelemetryAsync. A transient DI /
// DbContext-factory fault (pooled-context init, SQL-connection
// exhaustion, a resolution race during host churn) would otherwise
@@ -151,7 +151,7 @@ public class AuditLogIngestActor : ReceiveActor
await using var scope = _serviceProvider!.CreateAsyncScope();
var repository = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
var redactor = scope.ServiceProvider.GetService<IAuditRedactor>();
// M6 Bundle E (T8): central health counter is best-effort —
// Central health counter is best-effort —
// unregistered (test composition roots) means the per-row catch
// simply logs without surfacing on the health dashboard.
var failureCounter = scope.ServiceProvider.GetService<ICentralAuditWriteFailureCounter>();
@@ -194,11 +194,11 @@ public class AuditLogIngestActor : ReceiveActor
// retry, reconciliation) is a silent no-op.
// Redact BEFORE the IngestedAtUtc stamp so the redacted
// copy carries the central-side ingest timestamp. The redactor
// is contract-bound to never throw. AuditLog-008: a null
// is contract-bound to never throw. A null
// redactor (test composition root, no IAuditRedactor
// registered) now falls back to the SafeDefault rather than
// pass-through, so HTTP header redaction always runs.
// C3 transitional shim: IngestedAtUtc is a DetailsJson field on
// IngestedAtUtc is a DetailsJson field on
// the canonical record, so stamp it via the projection helper.
var safeRedactor = redactor ?? SafeDefaultAuditRedactor.Instance;
var filtered = safeRedactor.Apply(evt);
@@ -210,7 +210,7 @@ public class AuditLogIngestActor : ReceiveActor
{
// Per-row catch — one bad row never sinks the whole batch.
// The row stays Pending at the site; the next drain retries.
// M6 Bundle E (T8): bump the central health counter so a
// Bump the central health counter so a
// sustained insert-throw failure surfaces on the dashboard.
try { failureCounter?.Increment(); }
catch { /* counter must never throw — defence in depth */ }
@@ -222,7 +222,7 @@ public class AuditLogIngestActor : ReceiveActor
}
/// <summary>
/// M3 dual-write handler. For every <see cref="CachedTelemetryEntry"/> the
/// Dual-write handler. For every <see cref="CachedTelemetryEntry"/> the
/// actor opens a fresh MS SQL transaction, inserts the AuditLog row
/// idempotently AND upserts the SiteCalls row monotonically. Both succeed
/// or both roll back, so the audit and operational mirrors never drift
@@ -248,13 +248,13 @@ public class AuditLogIngestActor : ReceiveActor
var auditRepo = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
var siteCallRepo = scope.ServiceProvider.GetRequiredService<ISiteCallAuditRepository>();
var dbContext = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
// Bundle C (M5-T6): resolve the redactor for the whole batch from
// Resolve the redactor for the whole batch from
// the scope; null = SafeDefault for test composition roots that
// skip the redactor registration. The redactor is contract-bound to
// never throw, so we can apply it inside the per-entry try
// without risking an unbounded blast radius.
var redactor = scope.ServiceProvider.GetService<IAuditRedactor>();
// M6 Bundle E (T8): same best-effort central health counter as
// Same best-effort central health counter as
// the OnIngestAsync path — null on test composition roots that
// skip the registration.
var failureCounter = scope.ServiceProvider.GetService<ICentralAuditWriteFailureCounter>();
@@ -275,9 +275,9 @@ public class AuditLogIngestActor : ReceiveActor
// Redact the audit half BEFORE the dual-write — only the
// AuditLog row's payload columns are redactable; SiteCalls
// carries operational state only (status, retry count) and
// is left untouched. AuditLog-008: null redactor falls back
// is left untouched. Null redactor falls back
// to SafeDefault so header redaction always runs.
// C3 transitional shim: IngestedAtUtc is a DetailsJson field
// IngestedAtUtc is a DetailsJson field
// on the canonical record, so stamp it via the projection helper.
var safeRedactor = redactor ?? SafeDefaultAuditRedactor.Instance;
var filteredAudit = safeRedactor.Apply(entry.Audit);
@@ -298,7 +298,7 @@ public class AuditLogIngestActor : ReceiveActor
// EventId is NOT added to `accepted` so the site keeps its
// row Pending and retries on the next drain. Other entries
// in the batch continue with their own transactions.
// M6 Bundle E (T8): bump the central health counter so a
// Bump the central health counter so a
// sustained dual-write failure surfaces on the dashboard.
try { failureCounter?.Increment(); }
catch { /* counter must never throw — defence in depth */ }
@@ -2,7 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Tuning knobs for the central
/// <see cref="AuditLogPartitionMaintenanceService"/> hosted service (M6-T5).
/// <see cref="AuditLogPartitionMaintenanceService"/> hosted service.
/// Defaults: once every 24 hours, keep at least one future monthly
/// boundary ahead of <see cref="DateTime.UtcNow"/>.
/// </summary>
@@ -7,7 +7,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Central <see cref="IHostedService"/> (M6-T5, Bundle D) that rolls
/// Central <see cref="IHostedService"/> that rolls
/// <c>pf_AuditLog_Month</c> forward once a day. Each tick opens a fresh DI
/// scope, resolves <see cref="IPartitionMaintenance"/>, and calls
/// <see cref="IPartitionMaintenance.EnsureLookaheadAsync"/> to SPLIT any
@@ -19,7 +19,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// </summary>
/// <remarks>
/// <para>
/// <b>Why a hosted service, not an actor.</b> Bundle C's
/// <b>Why a hosted service, not an actor.</b>
/// <see cref="AuditLogPurgeActor"/> sits inside the central singleton
/// because it needs supervised lifecycle alongside the rest of the
/// reconciliation / ingest pipeline. Roll-forward is genuinely a once-a-day
@@ -33,8 +33,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <b>Failure containment.</b> The tick body wraps the maintenance call in
/// a try/catch so a transient SQL Server error never tears down the hosted
/// service — the next tick simply retries. The exception is logged with
/// the original stack trace at <c>Error</c> level; ops surfaces (M6 Bundle
/// E's central health collector) can subscribe to the logger to alert on
/// the original stack trace at <c>Error</c> level; ops surfaces can subscribe to the logger to alert on
/// repeated failures.
/// </para>
/// <para>
@@ -9,7 +9,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Central singleton (M6 Bundle C) that drives the daily AuditLog partition
/// Central singleton that drives the daily AuditLog partition
/// purge. On a configurable timer (default 24 hours) the actor:
/// <list type="number">
/// <item>Queries <see cref="IAuditLogRepository.GetPartitionBoundariesOlderThanAsync"/>
@@ -19,7 +19,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <see cref="IAuditLogRepository.SwitchOutPartitionAsync"/> which runs
/// the drop-and-rebuild dance around <c>UX_AuditLog_EventId</c>.</item>
/// <item>Publishes <see cref="AuditLogPurgedEvent"/> on the actor-system
/// EventStream so the Bundle E central health collector + ops surfaces
/// EventStream so the central health collector + ops surfaces
/// can subscribe without coupling to this actor.</item>
/// </list>
/// </summary>
@@ -51,8 +51,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <para>
/// <b>EventStream.</b> Publishing <see cref="AuditLogPurgedEvent"/> through
/// the EventStream rather than direct messaging avoids coupling this actor
/// to its consumers; M6 Bundle E will subscribe a central health-counter
/// bridge that surfaces purge progress on the central health report.
/// to its consumers; a central health-counter bridge will subscribe to
/// surface purge progress on the central health report.
/// </para>
/// </remarks>
public class AuditLogPurgeActor : ReceiveActor
@@ -134,7 +134,7 @@ public class AuditLogPurgeActor : ReceiveActor
// restart.
var threshold = DateTime.UtcNow - TimeSpan.FromDays(_auditOptions.RetentionDays);
// AuditLog-003: use CreateAsyncScope + await using so scoped EF Core
// Use CreateAsyncScope + await using so scoped EF Core
// services (IAsyncDisposable DbContexts) dispose asynchronously
// without blocking on sync Dispose() of pending connection cleanup.
await using var scope = _services.CreateAsyncScope();
@@ -206,7 +206,7 @@ public class AuditLogPurgeActor : ReceiveActor
}
}
// M5.5 (T3): after the channel-blind global partition switch-out, apply any
// After the channel-blind global partition switch-out, apply any
// per-channel retention overrides that are SHORTER than the global window via
// a bounded, batched row DELETE on the same maintenance path. The global
// switch-out has already dropped whole months older than RetentionDays; these
@@ -216,7 +216,7 @@ public class AuditLogPurgeActor : ReceiveActor
}
/// <summary>
/// M5.5 (T3): runs each per-channel retention override whose window is strictly
/// Runs each per-channel retention override whose window is strictly
/// shorter than the global <see cref="AuditLogOptions.RetentionDays"/>, deleting
/// rows of that channel older than the channel-specific threshold via a bounded,
/// batched maintenance-path DELETE. Each channel runs inside its own try/catch so
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Tuning knobs for the central <see cref="AuditLogPurgeActor"/> singleton.
/// Default cadence is 24 hours per the M6 plan; the retention window itself
/// Default cadence is 24 hours; the retention window itself
/// is sourced from <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Configuration.AuditLogOptions.RetentionDays"/>
/// (default 365) so operators tune retention from a single section.
/// </summary>
@@ -31,7 +31,7 @@ public sealed class AuditLogPurgeOptions
public int IntervalHours { get; set; } = 24;
/// <summary>
/// M5.5 (T3): batch size for the per-channel retention-override row DELETE
/// Batch size for the per-channel retention-override row DELETE
/// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.IAuditLogRepository.PurgeChannelOlderThanAsync"/>).
/// Each <c>DELETE TOP (@batch)</c> caps the transaction-log and lock footprint
/// per statement; the repository loops batches until no rows remain. Default
@@ -40,7 +40,7 @@ public sealed class AuditLogPurgeOptions
/// <see cref="ChannelPurgeBatchSize"/>.
/// </summary>
/// <remarks>
/// AuditLog-013: the operator-facing config key is <c>ChannelPurgeBatchSize</c>
/// The operator-facing config key is <c>ChannelPurgeBatchSize</c>
/// (per Component-AuditLog.md), so the binder maps that documented key onto this
/// backing property via <see cref="ConfigurationKeyNameAttribute"/>. The unattributed
/// property name (<c>ChannelPurgeBatchSizeConfigured</c>) would otherwise have been
@@ -2,8 +2,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Published on the actor-system EventStream by <see cref="AuditLogPurgeActor"/>
/// after each successful partition switch-out. Downstream consumers (Bundle E
/// central health collector, ops dashboards, audit trails) subscribe so a
/// after each successful partition switch-out. Downstream consumers (central
/// health collector, ops dashboards, audit trails) subscribe so a
/// purge action is observable without the actor needing to know about any
/// specific subscriber.
/// </summary>
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T9) — bridges
/// Bridges
/// <see cref="IAuditRedactionFailureCounter"/> (incremented by
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/> every time
/// a header / body / SQL parameter redactor stage throws and the redactor has
@@ -13,11 +13,11 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// </summary>
/// <remarks>
/// <para>
/// <b>Site vs central.</b> M5 Bundle C wired the SITE-side bridge
/// <b>Site vs central.</b> Wired the SITE-side bridge
/// (<see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.HealthMetricsAuditRedactionFailureCounter"/>),
/// which routes increments into the site health report payload's
/// <c>AuditRedactionFailure</c> field. That handles redactor failures on the
/// site SQLite hot-path (FallbackAuditWriter). M6 Bundle E (T9) adds the
/// site SQLite hot-path (FallbackAuditWriter). Adds the
/// MIRROR bridge here so the same payload filter — when it runs on the
/// central <see cref="CentralAuditWriter"/> /
/// <see cref="AuditLogIngestActor"/> paths — surfaces its failures on the
@@ -47,22 +47,22 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
private readonly INodeIdentityProvider? _nodeIdentity;
/// <summary>
/// Bundle C (M5-T6) — the central direct-write path used by the
/// The central direct-write path used by the
/// NotificationOutboxActor dispatch and the Inbound API middleware also
/// needs to truncate + redact before the row hits MS SQL. The filter is
/// optional so the M4 test composition roots that don't pass one keep
/// optional so test composition roots that don't pass one keep
/// working (they only ever write small payloads); production DI registers
/// the real filter via <see cref="ServiceCollectionExtensions.AddAuditLog"/>.
/// M6 Bundle E (T8) — adds the optional
/// Adds the optional
/// <see cref="ICentralAuditWriteFailureCounter"/> so a swallowed repository
/// throw bumps the central health surface's
/// <c>CentralAuditWriteFailures</c> counter. Defaults to a NoOp so test
/// composition roots that don't wire the counter keep their current
/// behaviour. SourceNode-stamping (Task 12) — adds the optional
/// behaviour. SourceNode-stamping — adds the optional
/// <see cref="INodeIdentityProvider"/> so central-origin rows (Notification
/// Outbox dispatch, Inbound API) carry the writing central node's
/// identifier when the caller hasn't already supplied one. Optional /
/// defaulting-to-null so M4 test composition roots that don't pass a
/// defaulting-to-null so test composition roots that don't pass a
/// provider keep working — the caller-wins discipline means an absent
/// provider simply leaves SourceNode at whatever the caller set (often
/// null, which is the legacy behaviour).
@@ -81,8 +81,8 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// AuditLog-008: never default to null — over-redact instead.
// C3 (Task 2.5): wired via the canonical IAuditRedactor seam.
// Never default to null — over-redact instead.
// Wired via the canonical IAuditRedactor seam.
// SafeDefaultAuditRedactor applies HTTP header redaction with
// hard-coded sensitive defaults so a composition root that omits the
// real redactor still scrubs Authorization / X-Api-Key / Cookie /
@@ -106,13 +106,13 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
try
{
// Redact BEFORE stamping IngestedAtUtc + handing to the repo. The
// redactor contract is "never throws". AuditLog-008: _redactor is
// redactor contract is "never throws". _redactor is
// now non-null (SafeDefaultAuditRedactor fallback) so header
// redaction always runs even in composition roots that omit the
// real redactor.
var filtered = _redactor.Apply(evt);
// SourceNode-stamping (Task 12): caller-provided value wins
// SourceNode-stamping: caller-provided value wins
// (supports any future direct-write callsite that already has its
// own node id); otherwise stamp from the local
// INodeIdentityProvider, when one is wired. Production DI on
@@ -126,7 +126,7 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
await using var scope = _services.CreateAsyncScope();
var repo = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
// C3 transitional shim: IngestedAtUtc is a DetailsJson field on the
// IngestedAtUtc is a DetailsJson field on the
// canonical record, so stamp it via the projection helper.
var stamped = AuditRowProjection.WithIngestedAtUtc(filtered, DateTime.UtcNow);
await repo.InsertIfNotExistsAsync(stamped, ct).ConfigureAwait(false);
@@ -134,7 +134,7 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
catch (Exception ex)
{
// Audit failure NEVER aborts the user-facing action — swallow and log.
// M6 Bundle E (T8): also surface the failure on the central health
// Also surface the failure on the central health
// counter so a sustained audit-write outage is visible on the
// health dashboard rather than disappearing into the log file.
try
@@ -12,7 +12,7 @@ using PullAuditEventsResponse = ZB.MOM.WW.ScadaBridge.Commons.Messages.Integrati
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Production <see cref="IPullAuditEventsClient"/> (Audit Log #23, M6) that the
/// Production <see cref="IPullAuditEventsClient"/> that the
/// central <see cref="SiteAuditReconciliationActor"/> uses to pull the next
/// reconciliation batch from a site over the <c>PullAuditEvents</c> unary gRPC
/// RPC served by <c>SiteStreamGrpcServer</c>.
@@ -13,7 +13,7 @@ using PullSiteCallsResponse = ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Production <see cref="IPullSiteCallsClient"/> (Site Call Audit #22) that the
/// Production <see cref="IPullSiteCallsClient"/> (Site Call Audit) that the
/// central reconciliation tick (a separate follow-up component) uses to pull the
/// next batch of cached-call operational rows from a site over the
/// <c>PullSiteCalls</c> unary gRPC RPC served by <c>SiteStreamGrpcServer</c>.
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E read-side surface exposing the central-side
/// Read-side surface exposing the central-side
/// audit-health counters: <see cref="CentralAuditWriteFailures"/> (every
/// repository insert throw from <see cref="CentralAuditWriter"/> /
/// <see cref="AuditLogIngestActor"/>), <see cref="AuditRedactionFailure"/>
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <para>
/// <b>Read-only contract.</b> Implementations expose a point-in-time snapshot
/// — increments and tracker updates happen through the dedicated counter /
/// tracker interfaces, not through this surface. Consumers (M7+ central
/// tracker interfaces, not through this surface. Consumers (central
/// health pages) read these properties; they never mutate.
/// </para>
/// <para>
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// failure / redaction-failure counters originate ON central (no site report
/// carries them), so they live on a dedicated snapshot rather than being
/// retro-fitted into a per-site state. The two surfaces will be composed at
/// the M7 dashboard layer.
/// the dashboard layer.
/// </para>
/// </remarks>
public interface IAuditCentralHealthSnapshot
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M5.3 (T7) counter sink incremented by
/// Audit Log counter sink incremented by
/// <see cref="ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware.AuditWriteMiddleware"/>
/// whenever an inbound request or response body is truncated at the
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Configuration.AuditLogOptions.InboundMaxBytes"/>
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T8) counter sink invoked by central-side audit
/// Counter sink invoked by central-side audit
/// writers (<see cref="CentralAuditWriter"/>, <see cref="AuditLogIngestActor"/>)
/// every time a repository <c>InsertIfNotExistsAsync</c> throws. Mirrors the
/// site-side <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.IAuditWriteFailureCounter"/>
@@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// instead. A NoOp default is the correct safe fallback while the central
/// health surface is being wired in; <see cref="AuditCentralHealthSnapshot"/>
/// is the production binding that routes increments into the aggregated
/// central health snapshot consumed by future M7+ pages.
/// central health snapshot, to be consumed by future central health pages.
/// </remarks>
public interface ICentralAuditWriteFailureCounter
{
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Mockable abstraction over the central-side <c>PullSiteCalls</c> gRPC client
/// surface used by the Site Call Audit (#22) reconciliation tick to fetch the
/// surface used by the Site Call Audit reconciliation tick to fetch the
/// next batch of cached-call operational rows from a specific site — the
/// documented periodic self-heal pull that backfills the eventually-consistent
/// central <c>SiteCalls</c> mirror when best-effort push telemetry is lost.
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Central singleton (M6 Bundle B) that drives the audit-log reconciliation
/// Central singleton that drives the audit-log reconciliation
/// pull loop. On a configurable timer (default 5 minutes) the actor walks every
/// known site, asks the site for any <see cref="AuditEvent"/> rows with
/// <see cref="AuditEvent.OccurredAtUtc"/> &gt;= the site's last reconciled
@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// missed push (gRPC blip, central restart, site offline) is eventually
/// repaired by central re-pulling whatever the site still has in
/// <c>Pending</c>/<c>Forwarded</c> state. Idempotency on
/// <see cref="AuditEvent.EventId"/> (M2 Bundle A's race-fix) makes duplicate
/// <see cref="AuditEvent.EventId"/> makes duplicate
/// arrivals from both paths a silent no-op.
/// </para>
/// <para>
@@ -33,7 +33,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// the cursors reset to <see cref="DateTime.MinValue"/>. That is conservative
/// but correct — the next tick simply asks for everything the site still has,
/// and idempotent ingest swallows the dupes. Persisting cursors to MS SQL was
/// considered and rejected for M6: the cost of a write per tick outweighs the
/// considered and rejected: the cost of a write per tick outweighs the
/// rare benefit of avoiding one over-broad pull after a restart.
/// </para>
/// <para>
@@ -41,7 +41,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// consecutive pull cycles BOTH return non-empty AND <c>MoreAvailable=true</c>
/// — i.e. the backlog isn't draining. The actor publishes
/// <see cref="SiteAuditTelemetryStalledChanged"/> on the actor system's
/// EventStream so a future <c>ICentralHealthCollector</c> bridge (M6 Bundle E)
/// EventStream so a future <c>ICentralHealthCollector</c> bridge
/// can flip the health metric without coupling this actor to the health
/// collection surface today.
/// </para>
@@ -97,7 +97,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
private readonly Dictionary<string, bool> _stalled = new();
/// <summary>
/// AuditLog-004: per-EventId retry counter for rows whose central insert
/// Per-EventId retry counter for rows whose central insert
/// threw. While a row keeps failing AND is below
/// <see cref="MaxPermanentInsertAttempts"/>, the cursor is held back so the
/// next reconciliation tick re-pulls and retries the row. Crossing the
@@ -110,7 +110,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
private readonly Dictionary<Guid, int> _failedInsertAttempts = new();
/// <summary>
/// AuditLog-004: number of consecutive central-insert failures before a row
/// Number of consecutive central-insert failures before a row
/// is permanently abandoned with a Critical log entry and the cursor is
/// allowed to advance past it. Five attempts at the 5-minute default tick
/// is ~25 min of retry budget before a stuck row stops blocking progress.
@@ -199,7 +199,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
return;
}
// AuditLog-003: use CreateAsyncScope + await using so scoped EF Core
// Use CreateAsyncScope + await using so scoped EF Core
// services (IAsyncDisposable DbContexts) dispose asynchronously
// without blocking on sync Dispose() of pending connection cleanup.
await using var scope = _services.CreateAsyncScope();
@@ -260,9 +260,8 @@ public class SiteAuditReconciliationActor : ReceiveActor
{
// Idempotent repository write: duplicate EventIds (from a
// concurrent push, or a retry of this very pull) collapse to
// a no-op courtesy of M2 Bundle A's race-fix on
// InsertIfNotExistsAsync.
// C3: IngestedAtUtc is a DetailsJson field on the canonical record —
// a no-op on InsertIfNotExistsAsync.
// IngestedAtUtc is a DetailsJson field on the canonical record —
// stamp it via the projection helper.
var ingested = AuditRowProjection.WithIngestedAtUtc(evt, nowUtc);
await repository.InsertIfNotExistsAsync(ingested).ConfigureAwait(false);
@@ -271,7 +270,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
}
catch (Exception ex)
{
// AuditLog-004: per-row catch so one bad event does not abandon
// Per-row catch so one bad event does not abandon
// the rest of the batch. Track the failure count per EventId —
// below MaxPermanentInsertAttempts the cursor is HELD BACK so
// the next tick re-pulls and retries; at the threshold the row
@@ -305,7 +304,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
}
}
// C3: canonical OccurredAtUtc is a DateTimeOffset; the cursor is a UTC DateTime.
// Canonical OccurredAtUtc is a DateTimeOffset; the cursor is a UTC DateTime.
var occurredUtc = evt.OccurredAtUtc.UtcDateTime;
if (advanceForThisRow && occurredUtc > maxOccurred)
{
@@ -313,7 +312,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
}
}
// AuditLog-004: only advance the persisted cursor if no event in this
// Only advance the persisted cursor if no event in this
// batch is still being retried. Leaving the cursor at `since` re-pulls
// the whole batch next tick — successful rows are no-ops thanks to
// InsertIfNotExistsAsync's idempotency, and the failing row gets
@@ -388,7 +387,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
/// <summary>
/// Published on the actor system EventStream when a site's reconciliation
/// puller transitions into or out of the "stalled" state (backlog not
/// draining across multiple cycles). The M6 Bundle E central health collector
/// draining across multiple cycles). The central health collector
/// will subscribe to this and surface
/// <c>SiteAuditTelemetryStalled</c> on the health-report payload.
/// </summary>
@@ -2,13 +2,13 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Tuning knobs for the central <see cref="SiteAuditReconciliationActor"/> singleton.
/// Defaults mirror the M6 Bundle B brief: pull every 5 minutes per site, 256 rows per
/// Defaults: pull every 5 minutes per site, 256 rows per
/// batch, declare a site "stalled" after two consecutive pull cycles return non-empty
/// AND <c>MoreAvailable=true</c> (the backlog is not draining).
/// </summary>
/// <remarks>
/// <para>
/// Per the M6 plan the reconciliation actor is the fallback when push telemetry is
/// The reconciliation actor is the fallback when push telemetry is
/// lost; it is intentionally low-frequency. Lowering
/// <see cref="ReconciliationIntervalSeconds"/> in production trades MS SQL load for
/// fresher self-healing — keep the default unless a deployment can prove the extra
@@ -5,10 +5,10 @@ using Akka.Event;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T7) — central singleton that subscribes to the
/// Central singleton that subscribes to the
/// actor system's EventStream for <see cref="SiteAuditTelemetryStalledChanged"/>
/// publications and maintains a per-site latched stalled-state map readable
/// via <see cref="Snapshot"/>. Consumed by the M6 Bundle E
/// via <see cref="Snapshot"/>. Consumed by the
/// <see cref="AuditCentralHealthSnapshot"/> aggregator so the central health
/// surface can surface per-site "reconciliation isn't draining" without
/// coupling the publisher (<see cref="SiteAuditReconciliationActor"/>) to the
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
/// <summary>
/// Configuration for Audit Log (#23). Bound from the <c>AuditLog</c> section of
/// Configuration for Audit Log. Bound from the <c>AuditLog</c> section of
/// <c>appsettings.json</c>. Defaults reflect the design (alog.md §6, §10): an
/// 8 KiB payload-summary cap, a 64 KiB cap on error rows, and a 365-day central
/// retention window with monthly partition-switch purge. The default
@@ -38,7 +38,7 @@ public sealed class AuditLogOptions
public int RetentionDays { get; set; } = 365;
/// <summary>
/// M5.5 (T3) per-channel retention overrides, keyed by the canonical channel name
/// Per-channel retention overrides, keyed by the canonical channel name
/// (the <see cref="AuditChannel"/> enum name — e.g. <c>ApiOutbound</c>,
/// <c>DbOutbound</c>, <c>Notification</c>, <c>ApiInbound</c>). The value is a
/// retention window in days that MUST be SHORTER than or equal to the global
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
/// <summary>
/// Validates <see cref="AuditLogOptions"/> on startup. The caps drive payload
/// truncation in the M2+ writers, so an unset/zero cap would let arbitrarily
/// truncation in the writers, so an unset/zero cap would let arbitrarily
/// large blobs into the central <c>AuditLog</c> table. <see cref="AuditLogOptions.ErrorCapBytes"/>
/// must be at least as large as <see cref="AuditLogOptions.DefaultCapBytes"/>
/// because the error cap is meant to capture <em>more</em> detail than the
@@ -54,7 +54,7 @@ public sealed class AuditLogOptionsValidator : OptionsValidatorBase<AuditLogOpti
$"AuditLog:{nameof(AuditLogOptions.InboundMaxBytes)} ({options.InboundMaxBytes}) " +
$"must be in [{MinInboundMaxBytes}, {MaxInboundMaxBytes}] bytes.");
// M5.5 (T3): per-channel retention overrides. Each entry must be keyed by a
// Per-channel retention overrides. Each entry must be keyed by a
// recognized AuditChannel name and carry a window in [MinRetentionDays,
// RetentionDays] — i.e. SHORTER than or equal to the global window. A longer
// per-channel window is meaningless under month-partition switch-out (governed
@@ -17,7 +17,7 @@ public sealed class PerTargetRedactionOverride
/// <summary>
/// Opt-in SQL parameter redaction: case-insensitive regex matched against
/// each SQL parameter NAME in the M4 <c>AuditingDbCommand</c> RequestSummary
/// each SQL parameter NAME in the <c>AuditingDbCommand</c> RequestSummary
/// JSON (<c>{"sql":"...","parameters":{"@name":"value", ...}}</c>); values
/// whose name matches are replaced with <c>&lt;redacted&gt;</c>. Null (the
/// default) means parameter values are captured verbatim. Only applied to
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Kpi;
/// <summary>
/// Audit Log (#23) M6 "KPI History &amp; Trends" (K8) — the
/// "KPI History &amp; Trends" — the
/// <see cref="IKpiSampleSource"/> for the <see cref="KpiSources.AuditLog"/> source.
/// The central recorder singleton enumerates this provider once per sampling pass
/// and persists its samples into the central <c>KpiSample</c> history table.
@@ -38,7 +38,7 @@ public sealed class AuditLogKpiSampleSource : IKpiSampleSource
// Metric catalog — the exact strings persisted in KpiSample.Metric. All three are
// charted, so they share the public Commons catalog: source + UI trend page key off
// one symbol (#178). Stable identifiers; do not rename (values are persisted).
// one symbol. Stable identifiers; do not rename (values are persisted).
private const string TotalEventsLastHourMetric = KpiMetrics.AuditLog.TotalEventsLastHour;
private const string ErrorEventsLastHourMetric = KpiMetrics.AuditLog.ErrorEventsLastHour;
private const string BacklogTotalMetric = KpiMetrics.AuditLog.BacklogTotal;
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
/// Pure, stateless redaction + truncation primitives used by
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/>
/// (which operates on <c>ZB.MOM.WW.Audit.AuditEvent</c> + its <c>DetailsJson</c>).
/// Extracted in ScadaBridge audit re-architecture stage C2 (Task 2.5) so the
/// Extracted in a ScadaBridge audit re-architecture so the
/// byte-exact redaction logic lives in ONE place.
/// </summary>
/// <remarks>
@@ -193,7 +193,7 @@ internal static class AuditRedactionPrimitives
}
/// <summary>
/// Walk the M4 <c>{"sql":"...","parameters":{...}}</c> RequestSummary
/// Walk the <c>{"sql":"...","parameters":{...}}</c> RequestSummary
/// shape; for each parameter whose NAME matches
/// <paramref name="paramNameRegex"/>, replace its value with
/// <see cref="RedactedMarker"/>. Re-serialise. No-op pass-through when the
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
/// <summary>
/// Per-instance compiled-regex cache for audit body / SQL-parameter redactors
/// used by <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/>.
/// Extracted in ScadaBridge audit re-architecture stage C2 (Task 2.5) to
/// Extracted in ScadaBridge audit re-architecture to
/// centralize compile rules (50 ms per-match timeout, 100 ms compile budget,
/// invalid-pattern sentinel).
/// </summary>
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
/// Counter sink invoked by <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/>
/// every time a redactor (header / body regex / SQL parameter) throws and the
/// redactor has to over-redact the offending field with the
/// <c>&lt;redacted: redactor error&gt;</c> marker. Bundle C bridges this into
/// <c>&lt;redacted: redactor error&gt;</c> marker. This is bridged into
/// the Site Health Monitoring report payload as <c>AuditRedactionFailure</c>.
/// </summary>
/// <remarks>
@@ -2,7 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
/// <summary>
/// Default <see cref="IAuditRedactionFailureCounter"/> binding used when the
/// Site Health Monitoring bridge has not been wired yet. Bundle C replaces
/// Site Health Monitoring bridge has not been wired yet. Replaces
/// this registration with the real counter that surfaces in the site health
/// report payload as <c>AuditRedactionFailure</c>.
/// </summary>
@@ -11,7 +11,7 @@ public sealed class NoOpAuditRedactionFailureCounter : IAuditRedactionFailureCou
/// <inheritdoc/>
public void Increment()
{
// Intentionally empty — Bundle C overrides this binding with the real
// Intentionally empty — overrides this binding with the real
// health-metric counter.
}
}
@@ -19,12 +19,12 @@ using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWri
namespace ZB.MOM.WW.ScadaBridge.AuditLog;
/// <summary>
/// Composition root for the Audit Log (#23) component.
/// Composition root for the Audit Log component.
/// </summary>
/// <remarks>
/// <para>
/// M1 registered <see cref="AuditLogOptions"/> + the validator. M2 Bundle E
/// extends the surface with the site-side writer chain
/// Registered <see cref="AuditLogOptions"/> + the validator. Extends the
/// surface with the site-side writer chain
/// (<see cref="SqliteAuditWriter"/> + <see cref="RingBufferFallback"/> +
/// <see cref="FallbackAuditWriter"/>) and the telemetry collaborators
/// (<see cref="ISiteAuditQueue"/>, <see cref="ISiteStreamAuditClient"/>,
@@ -32,8 +32,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog;
/// <see cref="SqliteAuditWriterOptions"/>).
/// </para>
/// <para>
/// Audit Log (#23) sits alongside Notification Outbox (#21) and Site Call
/// Audit (#22). <c>IAuditLogRepository</c> is registered by
/// Audit Log sits alongside Notification Outbox and Site Call
/// Audit. <c>IAuditLogRepository</c> is registered by
/// <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.ServiceCollectionExtensions.AddConfigurationDatabase</c>,
/// so the caller (the Host on the central node) must also call that.
/// </para>
@@ -59,7 +59,7 @@ public static class ServiceCollectionExtensions
public const string ReconciliationSectionName = "AuditLog:Reconciliation";
/// <summary>
/// Registers the Audit Log (#23) component services: options, the site
/// Registers the Audit Log component services: options, the site
/// SQLite writer chain (primary + ring fallback + failure-counter sink),
/// and the site-→central telemetry collaborators. Idempotent re-registration
/// is not supported; call this exactly once per <see cref="IServiceCollection"/>.
@@ -72,7 +72,7 @@ public static class ServiceCollectionExtensions
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(config);
// M1: top-level AuditLogOptions + validator (redaction policy, payload caps, etc.).
// Top-level AuditLogOptions + validator (redaction policy, payload caps, etc.).
// Collapsed onto the shared ZB.MOM.WW.Configuration helper: it binds the
// "AuditLog" section, registers the validator, and enables ValidateOnStart in
// one call. Same section path as before; AddAuditLog is call-once per
@@ -80,20 +80,19 @@ public static class ServiceCollectionExtensions
// validator (a strict improvement over the previous AddSingleton).
services.AddValidatedOptions<AuditLogOptions, AuditLogOptionsValidator>(config, ConfigSectionName);
// C3 (Task 2.5): the canonical IAuditRedactor is wired as
// The canonical IAuditRedactor is wired as
// ScadaBridgeAuditRedactor — same truncation + header / body /
// SQL-parameter redaction as the original pipeline, applied between
// event construction and persistence. Singleton — stateless; the
// IOptionsMonitor dependency picks up hot reloads on its own.
services.AddSingleton<IAuditRedactor, ScadaBridgeAuditRedactor>();
// M5 Bundle B: per-stage redactor-failure counter. NoOp default;
// Bundle C replaces this binding with the Site Health Monitoring
// bridge that surfaces failures as AuditRedactionFailure on the site
// health report.
// Per-stage redactor-failure counter. NoOp default; this binding is
// later replaced with the Site Health Monitoring bridge that surfaces
// failures as AuditRedactionFailure on the site health report.
services.TryAddSingleton<IAuditRedactionFailureCounter, NoOpAuditRedactionFailureCounter>();
// M2 Bundle E: site writer + telemetry options bindings.
// Site writer + telemetry options bindings.
// BindConfiguration is not used because the configuration root supplied
// by the caller may not be the application root — we go through the
// section explicitly so a partial IConfiguration (e.g. a test stub
@@ -113,18 +112,18 @@ public static class ServiceCollectionExtensions
// RingBufferFallback: drop-oldest in-memory ring used by
// FallbackAuditWriter when the primary SQLite writer throws. Default
// capacity is fine for M2 (1024).
// capacity is fine (1024).
services.AddSingleton<RingBufferFallback>();
// IAuditWriteFailureCounter: NoOp default. Bundle G overrides this
// binding with the real Site Health Monitoring counter. Registered
// IAuditWriteFailureCounter: NoOp default. This binding is later
// overridden with the real Site Health Monitoring counter. Registered
// before FallbackAuditWriter so the factory can resolve it.
services.AddSingleton<IAuditWriteFailureCounter, NoOpAuditWriteFailureCounter>();
// The script-thread surface is FallbackAuditWriter (primary + ring +
// counter), not the raw SqliteAuditWriter — primary failures must NEVER
// abort the user-facing action.
// C3 (Task 2.5): the canonical IAuditRedactor singleton above is wired
// The canonical IAuditRedactor singleton above is wired
// through the factory so every event written through this surface is
// truncated + redacted before it hits SQLite (and the ring on
// failure).
@@ -145,72 +144,71 @@ public static class ServiceCollectionExtensions
// created during Akka bootstrap, not at DI-composition time).
services.AddSingleton<ISiteStreamAuditClient, NoOpSiteStreamAuditClient>();
// M3 Bundle F: site-side dual emitter for cached-call lifecycle
// Site-side dual emitter for cached-call lifecycle
// telemetry. ScriptRuntimeContext.ExternalSystem.CachedCall /
// Database.CachedWrite resolves this through DI and pushes one combined
// packet per lifecycle event; the forwarder writes the audit half
// through IAuditWriter and the operational half through the
// IOperationTrackingStore. The audit writer is always wired (the M2
// IOperationTrackingStore. The audit writer is always wired (the
// chain above); the operational tracking store is SITE-ONLY (registered
// by ZB.MOM.WW.ScadaBridge.SiteRuntime). On a Central composition root the tracking
// store has no registration, so the factory resolves it with GetService
// (returning null) — the forwarder degrades to "audit-only" emission,
// mirroring the lazy IAuditWriter chain established in M2.
// mirroring the lazy IAuditWriter chain established above.
services.AddSingleton<ICachedCallTelemetryForwarder>(sp =>
new CachedCallTelemetryForwarder(
sp.GetRequiredService<IAuditWriter>(),
sp.GetService<ZB.MOM.WW.ScadaBridge.Commons.Interfaces.IOperationTrackingStore>(),
sp.GetRequiredService<ILogger<CachedCallTelemetryForwarder>>(),
// AuditLog-007: INodeIdentityProvider is now required across
// INodeIdentityProvider is now required across
// every consumer in AddAuditLog. The Host's
// SiteServiceRegistration registers it as a singleton on both
// site and central paths (InboundAPI-022 / Host registration
// sweep), and the AddAuditLogTests fixture provides a
// site and central paths, and the AddAuditLogTests fixture provides a
// FakeNodeIdentityProvider; a silent GetService() that
// returned null would mask a future composition root that
// forgot to register the provider.
sp.GetRequiredService<INodeIdentityProvider>()));
// M3 Bundle F: bridge the store-and-forward retry-loop observer hook
// Bridge the store-and-forward retry-loop observer hook
// to the cached-call forwarder so per-attempt + terminal telemetry
// emitted from the S&F retry sweep lands on the same SQLite hot-path
// as the script-thread CachedSubmit row. Registered as a singleton
// and also bound to ICachedCallLifecycleObserver so AddStoreAndForward
// can resolve it through DI (Bundle F StoreAndForward wiring change).
// SourceNode-stamping (Task 14): factory-resolved so the
// can resolve it through DI.
// SourceNode-stamping: factory-resolved so the
// INodeIdentityProvider singleton can be threaded through — the
// bridge stamps SiteCallOperational.SourceNode from
// INodeIdentityProvider.NodeName on every cached-call lifecycle row.
// AuditLog-007: the provider is resolved with GetRequiredService —
// The provider is resolved with GetRequiredService —
// SiteServiceRegistration.BindSharedOptions registers it on both
// site and central paths, so a missing registration is a
// composition-root bug, not a silent null-SourceNode degradation.
services.AddSingleton<CachedCallLifecycleBridge>(sp => new CachedCallLifecycleBridge(
sp.GetRequiredService<ICachedCallTelemetryForwarder>(),
sp.GetRequiredService<ILogger<CachedCallLifecycleBridge>>(),
// AuditLog-007: required, matches the other consumers in this
// Required, matches the other consumers in this
// composition root — the provider is always registered by
// SiteServiceRegistration.
sp.GetRequiredService<INodeIdentityProvider>()));
services.AddSingleton<ICachedCallLifecycleObserver>(
sp => sp.GetRequiredService<CachedCallLifecycleBridge>());
// M6 Bundle E (T8): central audit-write failure counter — NoOp default
// Central audit-write failure counter — NoOp default
// for site/test composition roots that don't wire the central health
// snapshot. AddAuditLogCentralMaintenance below replaces this binding
// with the AuditCentralHealthSnapshot implementation so increments
// surface on the central dashboard.
services.TryAddSingleton<ICentralAuditWriteFailureCounter, NoOpCentralAuditWriteFailureCounter>();
// M5.3 (T7): inbound body-ceiling hit counter — NoOp default for
// Inbound body-ceiling hit counter — NoOp default for
// site/test roots. AddAuditLogCentralMaintenance replaces this binding
// with the AuditCentralHealthSnapshot implementation so ceiling-hit
// counts surface on the central dashboard alongside write-failure and
// redaction-failure counters.
services.TryAddSingleton<IAuditInboundCeilingHitsCounter, NoOpAuditInboundCeilingHitsCounter>();
// M4 Bundle B: central direct-write audit writer used by
// NotificationOutboxActor (Bundle B) and Inbound API (Bundle C/D) to
// Central direct-write audit writer used by
// NotificationOutboxActor and Inbound API to
// emit AuditLog rows that originate ON central, not via site telemetry.
// Singleton — the writer is stateless; its per-call scope opens a fresh
// IAuditLogRepository (a SCOPED EF Core service registered by
@@ -218,17 +216,17 @@ public static class ServiceCollectionExtensions
// is intentionally distinct from IAuditWriter so site composition roots
// do not accidentally bind it; central composition roots that include
// AddConfigurationDatabase get a working implementation transparently.
// C3 (Task 2.5): wire the canonical IAuditRedactor into the factory so
// Wire the canonical IAuditRedactor into the factory so
// NotificationOutboxActor + Inbound API rows are truncated + redacted
// before they hit MS SQL.
// M6 Bundle E (T8): also wire the ICentralAuditWriteFailureCounter
// Also wire the ICentralAuditWriteFailureCounter
// so swallowed repo throws bump the central health counter.
services.AddSingleton<ICentralAuditWriter>(sp => new CentralAuditWriter(
sp,
sp.GetRequiredService<ILogger<CentralAuditWriter>>(),
sp.GetRequiredService<IAuditRedactor>(),
sp.GetRequiredService<ICentralAuditWriteFailureCounter>(),
// SourceNode-stamping (Task 12): wire the local node identity so
// SourceNode-stamping: wire the local node identity so
// central-origin rows (Notification Outbox dispatch, Inbound API)
// carry the writing node's identifier when the caller hasn't
// already supplied one. GetRequiredService — the production
@@ -236,7 +234,7 @@ public static class ServiceCollectionExtensions
// provider as a singleton on both site and central paths.
sp.GetRequiredService<INodeIdentityProvider>()));
// M6 (K8) KPI History & Trends: the AuditLog source the central KPI
// KPI History & Trends: the AuditLog source the central KPI
// recorder enumerates each sampling pass to snapshot Global-scope Audit
// Log KPIs (volume / error rate / backlog) into the KpiSample history
// table. Scoped to match its IAuditLogRepository dependency — a SCOPED
@@ -252,7 +250,7 @@ public static class ServiceCollectionExtensions
}
/// <summary>
/// Audit Log (#23) M2 Bundle G + M5 Bundle C — swap the default
/// Swap the default
/// <see cref="NoOpAuditWriteFailureCounter"/> and
/// <see cref="NoOpAuditRedactionFailureCounter"/> registrations for the
/// real <see cref="HealthMetricsAuditWriteFailureCounter"/> /
@@ -282,10 +280,10 @@ public static class ServiceCollectionExtensions
/// double-registered (AddHostedService has no TryAdd variant).
/// </para>
/// <para>
/// Site-side only for M5: the central composition root keeps the NoOp
/// Site-side only: the central composition root keeps the NoOp
/// defaults; the central health-metric surface that would expose
/// <c>AuditRedactionFailure</c> next to the existing central counters
/// ships in M6.
/// ships later.
/// </para>
/// </remarks>
/// <param name="services">The service collection to register into.</param>
@@ -294,7 +292,7 @@ public static class ServiceCollectionExtensions
{
ArgumentNullException.ThrowIfNull(services);
// AuditLog-011: guard against double-registration. AddHostedService is
// Guard against double-registration. AddHostedService is
// additive (no TryAdd variant) so a second call without this sentinel
// would spin up a second SiteAuditBacklogReporter, doubling the 30 s
// SQL probe rate and racing on the ISiteHealthCollector snapshot. The
@@ -310,7 +308,7 @@ public static class ServiceCollectionExtensions
ServiceDescriptor.Singleton<IAuditWriteFailureCounter, HealthMetricsAuditWriteFailureCounter>());
services.Replace(
ServiceDescriptor.Singleton<IAuditRedactionFailureCounter, HealthMetricsAuditRedactionFailureCounter>());
// M6 Bundle E (T6): the site-side backlog reporter polls the
// The site-side backlog reporter polls the
// SqliteAuditWriter every 30 s and pushes the snapshot into the
// collector so the next SiteHealthReport carries a fresh
// SiteAuditBacklog field. Registered alongside the other site-only
@@ -322,7 +320,7 @@ public static class ServiceCollectionExtensions
}
/// <summary>
/// Audit Log (#23) M6-T5 Bundle D — central-only registration for the
/// Central-only registration for the
/// <see cref="AuditLogPartitionMaintenanceService"/> hosted service plus
/// its <see cref="AuditLogPartitionMaintenanceOptions"/> binding. Must be
/// called from the Central role's composition root (not from a site
@@ -372,7 +370,7 @@ public static class ServiceCollectionExtensions
services.AddOptions<SiteAuditReconciliationOptions>()
.Bind(config.GetSection(ReconciliationSectionName));
// M6 Bundle E (T8 + T9): central health snapshot — a single object
// Central health snapshot — a single object
// that owns the CentralAuditWriteFailures + AuditRedactionFailure
// Interlocked counters AND surfaces them on
// IAuditCentralHealthSnapshot. The same instance is bound to BOTH
@@ -391,7 +389,7 @@ public static class ServiceCollectionExtensions
sp => sp.GetRequiredService<AuditCentralHealthSnapshot>());
services.Replace(ServiceDescriptor.Singleton<ICentralAuditWriteFailureCounter>(
sp => sp.GetRequiredService<AuditCentralHealthSnapshot>()));
// M6 Bundle E (T9): override the NoOp IAuditRedactionFailureCounter
// Override the NoOp IAuditRedactionFailureCounter
// (registered by AddAuditLog) with the CentralAuditRedactionFailureCounter
// bridge so payload-filter throws on CentralAuditWriter /
// AuditLogIngestActor paths surface on the central dashboard. The
@@ -400,11 +398,11 @@ public static class ServiceCollectionExtensions
// counter as CentralAuditWriteFailures. The site composition root
// overrides this binding AGAIN via AddAuditLogHealthMetricsBridge —
// central nodes do not call that bridge, so this is the final
// binding on a central host. Mirrors the M5 Bundle C
// binding on a central host. Mirrors the
// HealthMetricsAuditRedactionFailureCounter shape one-for-one.
services.Replace(ServiceDescriptor.Singleton<IAuditRedactionFailureCounter,
CentralAuditRedactionFailureCounter>());
// M5.3 (T7): replace the NoOp IAuditInboundCeilingHitsCounter with the
// Replace the NoOp IAuditInboundCeilingHitsCounter with the
// AuditCentralHealthSnapshot so ceiling-hit counts surface on the
// central dashboard. Same singleton-forward pattern as
// ICentralAuditWriteFailureCounter above.
@@ -415,7 +413,7 @@ public static class ServiceCollectionExtensions
}
/// <summary>
/// Audit Log (#23) M6 — central-only registration of the production
/// Audit Log — central-only registration of the production
/// <see cref="IPullAuditEventsClient"/> (<see cref="GrpcPullAuditEventsClient"/>)
/// and its unary-call invoker (<see cref="GrpcPullAuditEventsInvoker"/>) used
/// by <see cref="SiteAuditReconciliationActor"/> to pull reconciliation
@@ -500,7 +498,7 @@ public static class ServiceCollectionExtensions
sp.GetRequiredService<GrpcPullAuditEventsClient.IPullAuditEventsInvoker>(),
sp.GetRequiredService<ILogger<GrpcPullAuditEventsClient>>()));
// Site Call Audit (#22) reconciliation pull client — central-only, the
// Site Call Audit reconciliation pull client — central-only, the
// sibling of the audit pull client above. Lives here (not in the
// SiteCallAudit project) so it can reuse the central-only
// ISiteEnumerator registered just above; SiteCallAudit does not
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <para>
/// Each primary failure increments <see cref="IAuditWriteFailureCounter"/> so
/// Site Health Monitoring can surface a sustained outage as
/// <c>SiteAuditWriteFailures</c> (Bundle G).
/// <c>SiteAuditWriteFailures</c>.
/// </para>
/// <para>
/// Errors raised by the ring drain on recovery are logged and silently dropped
@@ -36,7 +36,7 @@ public sealed class FallbackAuditWriter : IAuditWriter
private readonly SemaphoreSlim _drainGate = new(1, 1);
/// <summary>
/// Bundle C (M5-T6) wires the singleton <see cref="IAuditRedactor"/>
/// Wires the singleton <see cref="IAuditRedactor"/>
/// here so every event written via the site hot path is truncated +
/// header/body/SQL-param redacted before it hits both the primary SQLite
/// writer AND the ring fallback. The parameter is optional (defaults to
@@ -62,8 +62,8 @@ public sealed class FallbackAuditWriter : IAuditWriter
_ring = ring ?? throw new ArgumentNullException(nameof(ring));
_failureCounter = failureCounter ?? throw new ArgumentNullException(nameof(failureCounter));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// AuditLog-008: never default to a null redactor — over-redact instead.
// C3 (Task 2.5): wired via the canonical IAuditRedactor seam.
// Never default to a null redactor — over-redact instead.
// Wired via the canonical IAuditRedactor seam.
// SafeDefaultAuditRedactor performs HTTP header redaction with the
// hard-coded sensitive defaults (Authorization, X-Api-Key, Cookie,
// Set-Cookie) on the DetailsJson summaries so a test composition root
@@ -82,7 +82,7 @@ public sealed class FallbackAuditWriter : IAuditWriter
// and (on failure) to the ring buffer — so a primary outage that
// drains later still hands the SqliteAuditWriter a row that has
// already been truncated and redacted. The redactor contract is
// "MUST NOT throw". AuditLog-008: _redactor is now non-null (defaults
// "MUST NOT throw". _redactor is now non-null (defaults
// to SafeDefaultAuditRedactor so header redaction is always applied
// even in composition roots that don't wire the real redactor).
var filtered = _redactor.Apply(evt);
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <summary>
/// Audit Log (#23) M5 Bundle C — bridges
/// Bridges
/// <see cref="IAuditRedactionFailureCounter"/> (incremented by
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/> every time
/// a header / body / SQL parameter redactor stage throws and the redactor has
@@ -22,16 +22,15 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// redaction failures must NEVER abort the user-facing action, alog.md §7).
/// </para>
/// <para>
/// Mirrors the M2 Bundle G <see cref="HealthMetricsAuditWriteFailureCounter"/>
/// Mirrors the <see cref="HealthMetricsAuditWriteFailureCounter"/>
/// shape one-for-one so the two health-metric bridges age together.
/// </para>
/// <para>
/// Site-side only for M5: the redaction filter also runs on the central
/// writers (CentralAuditWriter + AuditLogIngestActor), but the central
/// health-metric surface that would expose <c>AuditRedactionFailure</c>
/// alongside the existing central counters ships in M6. Until then, the
/// central composition root keeps the NoOp default — the redactions still
/// happen, they just don't get counted into a health report.
/// Site-side only: the redaction filter also runs on the central
/// writers (CentralAuditWriter + AuditLogIngestActor); the central
/// health-metric surface exposing <c>AuditRedactionFailure</c> is bridged
/// separately by <c>CentralAuditRedactionFailureCounter</c>, which the
/// central composition root registers in place of the NoOp default.
/// </para>
/// </remarks>
public sealed class HealthMetricsAuditRedactionFailureCounter : IAuditRedactionFailureCounter
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <summary>
/// Audit Log (#23) M2 Bundle G — bridges <see cref="IAuditWriteFailureCounter"/>
/// Bridges <see cref="IAuditWriteFailureCounter"/>
/// (incremented by <see cref="FallbackAuditWriter"/> every time the primary
/// SQLite writer throws) into <see cref="ISiteHealthCollector"/> so the count
/// surfaces in the site health report payload as
@@ -3,7 +3,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <summary>
/// Lightweight counter sink invoked by <see cref="FallbackAuditWriter"/> every
/// time the primary <see cref="SqliteAuditWriter"/> throws on an audit write.
/// Bundle G (M2-T11) implements this as a thread-safe Interlocked counter
/// Implements this as a thread-safe Interlocked counter
/// bridged into the Site Health Monitoring report payload as
/// <c>SiteAuditWriteFailures</c>.
/// </summary>
@@ -3,7 +3,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <summary>
/// Default <see cref="IAuditWriteFailureCounter"/> registered by
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.ServiceCollectionExtensions.AddAuditLog"/> on
/// every node. Bundle G replaces this binding with a real counter that bridges
/// every node. This binding will be replaced with a real counter that bridges
/// into the Site Health Monitoring report payload as
/// <c>SiteAuditWriteFailures</c> — until then,
/// <see cref="FallbackAuditWriter"/> emits to a silent sink rather than NRE-ing
@@ -19,7 +19,7 @@ public sealed class NoOpAuditWriteFailureCounter : IAuditWriteFailureCounter
/// <inheritdoc/>
public void Increment()
{
// Intentionally empty. Bundle G overrides this binding with the real
// Intentionally empty. This binding is replaced with the real
// counter once Site Health Monitoring is wired.
}
}
@@ -21,7 +21,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// count at capacity, exactly one event has been dropped.
/// </para>
/// <para>
/// Per the M2 plan: the ring is the absolute-last-resort buffer for the
/// The ring is the absolute-last-resort buffer for the
/// hot-path; it is NOT a substitute for the bounded
/// <see cref="SqliteAuditWriter"/> write queue.
/// </para>
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T6) — site-side hosted service that
/// Site-side hosted service that
/// periodically pulls a backlog snapshot from <see cref="ISiteAuditQueue"/>
/// and pushes it into <see cref="ISiteHealthCollector"/> so the next
/// <see cref="ISiteHealthCollector.CollectReport"/> emits a fresh
@@ -26,7 +26,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// across many reports, fine enough that the central dashboard never lags by
/// more than one health-report interval. Tunable via
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.SqliteAuditWriterOptions"/> in a follow-up
/// if ops needs a different cadence; for M6 we hard-code the value because the
/// if ops needs a different cadence; for now we hard-code the value because the
/// brief calls it out explicitly.
/// </para>
/// <para>
@@ -12,7 +12,7 @@ using AuditOutcome = ZB.MOM.WW.Audit.AuditOutcome;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// <summary>
/// Site-side SQLite hot-path writer for Audit Log (#23) events. Mirrors the
/// Site-side SQLite hot-path writer for Audit Log events. Mirrors the
/// <see cref="ZB.MOM.WW.ScadaBridge.SiteEventLogging.SiteEventLogger"/> design — a single
/// owned <see cref="SqliteConnection"/> serialised behind a write lock, fed by a
/// bounded <see cref="Channel{T}"/> drained on a dedicated background writer
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// </summary>
/// <remarks>
/// <para>
/// <b>C4 (Task 2.5) — two-table schema.</b> The site store is now two tables:
/// <b>Two-table schema.</b> The site store is now two tables:
/// the append-only canonical <c>audit_event</c> (the 10 canonical
/// <see cref="AuditEvent"/> fields stored directly — NO 24-column decompose) and
/// the mutable operational <c>audit_forward_state</c> sidecar that carries the
@@ -31,10 +31,10 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
/// </para>
/// <para>
/// <b>Ephemeral reset.</b> The site SQLite store is ephemeral (≈7-day retention,
/// recreated per deployment), so C4's schema change is an in-place RESET: the new
/// recreated per deployment), so this schema change is an in-place RESET: the new
/// tables are created and the old single 24-column <c>AuditLog</c> table is
/// DROP-ped if present. No SQLite data migration is performed (and none is
/// needed) — any rows in a pre-C4 <c>AuditLog</c> table are within the retention
/// needed) — any rows in the old <c>AuditLog</c> table are within the retention
/// window and are discarded by the drop.
/// </para>
/// <para>
@@ -56,7 +56,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
private const int SqliteErrorConstraint = 19;
private readonly SqliteConnection _connection;
// AuditLog-005: dedicated read-only connection used by GetBacklogStatsAsync,
// Dedicated read-only connection used by GetBacklogStatsAsync,
// ReadPendingAsync, ReadPendingSinceAsync, and ReadForwardedAsync so a slow
// backlog scan (COUNT(*) over hundreds of thousands of Pending rows under a
// central outage) never parks the hot-path writer behind _writeLock.
@@ -102,7 +102,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
InitializeSchema();
// AuditLog-005: open a second connection for read-only callers
// Open a second connection for read-only callers
// (GetBacklogStatsAsync, ReadPendingAsync, ReadPendingSinceAsync,
// ReadForwardedAsync). InitializeSchema set journal_mode=WAL on the
// writer connection, which is a database-level setting that persists
@@ -128,7 +128,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
new BoundedChannelOptions(_options.ChannelCapacity)
{
// The hot-path enqueue must back-pressure if the background
// writer falls behind; a higher-level fallback (Bundle B-T4)
// writer falls behind; a higher-level fallback
// handles truly catastrophic primary failure with a drop-oldest
// ring buffer.
FullMode = BoundedChannelFullMode.Wait,
@@ -150,7 +150,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
pragmaCmd.ExecuteNonQuery();
}
// AuditLog-005: enable WAL so a second connection on the same file can
// Enable WAL so a second connection on the same file can
// serve read-only callers (GetBacklogStatsAsync, ReadPendingAsync,
// ReadPendingSinceAsync, ReadForwardedAsync) concurrently with the
// batched writer, decoupling those reads from _writeLock. WAL is a
@@ -185,14 +185,14 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
pragmaCmd.ExecuteNonQuery();
}
// C4 (Task 2.5) — in-place reset. The site store is EPHEMERAL (≈7-day
// In-place reset. The site store is EPHEMERAL (≈7-day
// retention, recreated per deployment), so we do NOT migrate the old
// single 24-column AuditLog table to the new two-table shape: any rows
// it holds are within the retention window and discarded. DROP it if a
// pre-C4 deployment left it behind, then CREATE the two new tables. This
// it holds are within the retention window and discarded. DROP it if an
// earlier deployment left it behind, then CREATE the two new tables. This
// is safe precisely BECAUSE the site store is ephemeral — never do this
// on a durable store (the central SQL Server side keeps its shim until
// C5 and is migrated, not reset).
// it is migrated, not reset).
using (var dropCmd = _connection.CreateCommand())
{
dropCmd.CommandText = "DROP TABLE IF EXISTS AuditLog;";
@@ -442,10 +442,10 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
}
}
// AuditLog-001: cached-lifecycle audit kinds that ride the combined-telemetry
// Cached-lifecycle audit kinds that ride the combined-telemetry
// drain (joined with the operational tracking row + pushed via
// IngestCachedTelemetryAsync into the central dual-write transaction).
// C4: this is the SAME set the pre-C4 ReadPendingCachedTelemetryAsync query
// This is the SAME set the earlier ReadPendingCachedTelemetryAsync query
// filtered on (Kind IN (...)); it is now precomputed into the sidecar's
// IsCachedKind flag at INSERT (see IsCachedKind) so the read split is a cheap
// integer predicate, not a JSON parse. ReadPendingAsync drains everything
@@ -459,7 +459,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
};
/// <summary>
/// C4: precomputes the sidecar's <c>IsCachedKind</c> flag from a canonical
/// Precomputes the sidecar's <c>IsCachedKind</c> flag from a canonical
/// row's <c>DetailsJson</c>. Parses the <see cref="AuditDetails.Kind"/>
/// discriminator via <see cref="AuditDetailsCodec"/> and returns <c>true</c>
/// iff it is one of the cached-lifecycle kinds
@@ -489,14 +489,14 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
throw new ArgumentOutOfRangeException(nameof(limit), "limit must be > 0.");
}
// AuditLog-005: read via the dedicated _readConnection so this scan
// Read via the dedicated _readConnection so this scan
// (which can be expensive when the backlog grows under a central
// outage) does not block the batched writer on _writeLock. WAL mode
// gives us a stable snapshot of the tables while writes proceed on the
// writer connection. _readLock serialises this connection across
// multiple concurrent read callers since SqliteConnection itself is
// not thread-safe.
// C4: JOIN the sidecar and filter on IsCachedKind=0 — the cached-
// JOIN the sidecar and filter on IsCachedKind=0 — the cached-
// lifecycle kinds (IsCachedKind=1) flow through
// ReadPendingCachedTelemetryAsync + the combined-telemetry drain. The
// split is a precomputed integer predicate on the indexed sidecar, not
@@ -539,7 +539,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
throw new ArgumentOutOfRangeException(nameof(limit), "limit must be > 0.");
}
// AuditLog-001 / C4: dedicated read surface for the cached-call lifecycle
// Dedicated read surface for the cached-call lifecycle
// drain — symmetric to ReadPendingAsync but filtered to IsCachedKind=1.
// Same _readConnection + _readLock pattern so the hot-path writer is not
// contended.
@@ -586,9 +586,9 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
throw new ArgumentOutOfRangeException(nameof(limit), "limit must be > 0.");
}
// AuditLog-005: mirror ReadPendingAsync — read via _readConnection /
// Mirror ReadPendingAsync — read via _readConnection /
// _readLock so this query never contends with the batched writer on
// _writeLock. C4: JOIN the sidecar and filter on ForwardState='Forwarded'
// _writeLock. JOIN the sidecar and filter on ForwardState='Forwarded'
// (no IsCachedKind split — both cached and non-cached Forwarded rows are
// returned, as before).
lock (_readLock)
@@ -626,7 +626,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
using var cmd = _connection.CreateCommand();
// C4: flip the sidecar — UPDATE audit_forward_state, not the canonical
// Flip the sidecar — UPDATE audit_forward_state, not the canonical
// audit_event (which is append-only / write-once). Bump AttemptCount +
// stamp LastAttemptUtc so operators can see how many drain passes a row
// took to forward. Build a single IN (...) parameter list so we issue
@@ -667,7 +667,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
/// <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.
/// Used by the 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>
@@ -681,8 +681,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
throw new ArgumentOutOfRangeException(nameof(batchSize), "batchSize must be > 0.");
}
// AuditLog-005: read via _readConnection / _readLock — same lock-
// decoupling as ReadPendingAsync. C4: JOIN the sidecar; the range scan
// Read via _readConnection / _readLock — same lock-
// decoupling as ReadPendingAsync. JOIN the sidecar; the range scan
// is on the sidecar's duplicated OccurredAtUtc so it stays on IX_fwd.
// Both Pending and Forwarded rows are returned (the central reconciliation
// puller dedups on EventId; re-shipping a Forwarded-but-not-yet-ingested
@@ -729,7 +729,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
using var cmd = _connection.CreateCommand();
// C4: flip the sidecar from Pending/Forwarded → Reconciled. Rows
// Flip the sidecar from Pending/Forwarded → Reconciled. Rows
// already Reconciled are left untouched (idempotent re-call), and the
// canonical audit_event row is never modified.
var sb = new System.Text.StringBuilder();
@@ -759,13 +759,13 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
int pendingCount;
DateTime? oldestPending;
// AuditLog-005: read via the dedicated _readConnection (under
// Read via the dedicated _readConnection (under
// _readLock) so this probe — polled every 30 s by SiteAuditBacklogReporter
// — never blocks the batched hot-path writer on _writeLock. Under a
// central outage the Pending backlog can grow to hundreds of thousands
// of rows and the COUNT(*) scan correspondingly stretches; that no
// longer adds tail latency to user-facing audit writes.
// C4: count over the sidecar (audit_forward_state) — the canonical
// Count over the sidecar (audit_forward_state) — the canonical
// audit_event table carries no ForwardState. The IX_fwd index makes both
// aggregates cheap (count is a covering scan, min is the first key).
lock (_readLock)
@@ -843,7 +843,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
}
/// <summary>
/// C4: builds the canonical <see cref="AuditEvent"/> DIRECTLY from the 10
/// Builds the canonical <see cref="AuditEvent"/> DIRECTLY from the 10
/// stored <c>audit_event</c> columns — no 24-column <c>Recompose</c>, because
/// <c>audit_event</c> already holds the canonical fields + <c>DetailsJson</c>.
/// <c>Outcome</c> is stored as the enum's name; the safe
@@ -875,7 +875,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
/// Disposes the audit writer and releases resources.
/// </summary>
/// <remarks>
/// AuditLog-006: prefer <see cref="DisposeAsync"/> when possible (DI honours
/// Prefer <see cref="DisposeAsync"/> when possible (DI honours
/// <see cref="IAsyncDisposable"/> on singletons). The sync path remains for
/// callers that only know about <see cref="IDisposable"/> (e.g. legacy
/// composition roots, <c>using</c> statements without <c>await</c>). To
@@ -938,7 +938,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable
_connection.Dispose();
}
// AuditLog-005: dispose the dedicated read connection after the writer
// Dispose the dedicated read connection after the writer
// is fully drained and closed. _readLock is taken to fence out any
// in-flight read caller that grabbed the lock before _disposed flipped
// — they observe ObjectDisposedException on the next attempt.
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
/// <summary>
/// Audit Log #23 (M3 Bundle E — Tasks E4/E5): translates per-attempt
/// Translates per-attempt
/// notifications from the store-and-forward retry loop into one (or two)
/// <see cref="CachedCallTelemetry"/> packets and pushes them through
/// <see cref="ICachedCallTelemetryForwarder"/>.
@@ -40,7 +40,7 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
private readonly ILogger<CachedCallLifecycleBridge> _logger;
/// <summary>
/// SourceNode-stamping (Task 14): the local node identity provider used to
/// SourceNode-stamping: the local node identity provider used to
/// stamp <c>SiteCallOperational.SourceNode</c> on every cached-call
/// lifecycle row this bridge emits. Optional — when null (legacy hosts /
/// tests that don't register the provider) SourceNode stays null and
@@ -93,8 +93,8 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
// Per-attempt row: kind discriminates channel; status is always
// Attempted regardless of outcome (success vs. failure is captured
// by the companion HttpStatus / ErrorMessage fields, NOT by flipping
// the status — CachedResolve carries the terminal Status). Per the
// M3 brief and alog.md §4.
// the status — CachedResolve carries the terminal Status). Per
// alog.md §4.
var kind = ChannelToAttemptKind(context.Channel);
var status = AuditStatus.Attempted;
@@ -148,22 +148,22 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
occurredAtUtc: DateTime.SpecifyKind(context.OccurredAtUtc, DateTimeKind.Utc),
target: context.Target,
correlationId: context.TrackedOperationId.Value,
// Audit Log #23 (ExecutionId Task 4): the originating script
// The originating script
// execution's per-run correlation id, threaded through the S&F
// buffer; null on rows buffered before Task 4 (back-compat).
// buffer; null on rows buffered before ExecutionId support was added (back-compat).
executionId: context.ExecutionId,
// Audit Log #23 (ParentExecutionId Task 6): the spawning
// The spawning
// inbound-API request's ExecutionId, threaded through the S&F
// buffer alongside ExecutionId so the retry-loop cached rows
// correlate back to the cross-execution chain. Null for a
// non-routed run and on rows buffered before Task 6.
// non-routed run and on rows buffered before ParentExecutionId support was added.
parentExecutionId: context.ParentExecutionId,
sourceSiteId: string.IsNullOrEmpty(context.SourceSite) ? null : context.SourceSite,
sourceInstanceId: context.SourceInstanceId,
// Audit Log #23 (ExecutionId Task 4): SourceScript is now
// SourceScript is now
// threaded through the S&F buffer alongside ExecutionId — the
// retry-loop cached rows carry the same provenance the
// script-side cached rows do. Null on pre-Task-4 buffered rows.
// script-side cached rows do. Null on rows buffered before ExecutionId support was added.
sourceScript: context.SourceScript,
httpStatus: httpStatus,
durationMs: context.DurationMs,
@@ -173,7 +173,7 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
Channel: context.Channel,
Target: context.Target,
SourceSite: context.SourceSite,
// SourceNode-stamping (Task 14): the local cluster node name
// SourceNode-stamping: the local cluster node name
// (node-a/node-b on a site). Stamped from the injected
// INodeIdentityProvider; null when no provider was wired so
// central persists SiteCalls.SourceNode as NULL.
@@ -9,15 +9,15 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
/// <summary>
/// Site-side dual emitter for cached-call lifecycle telemetry (Audit Log #23 /
/// M3). Sister to <see cref="SiteAuditTelemetryActor"/>: where the M2 actor
/// Site-side dual emitter for cached-call lifecycle telemetry. Sister to
/// <see cref="SiteAuditTelemetryActor"/>: where that actor
/// drains audit-only events, this forwarder takes a combined
/// <see cref="CachedCallTelemetry"/> packet and fans it out to the two
/// site-local stores in a single call:
/// <list type="bullet">
/// <item><description>The <see cref="AuditEvent"/> row is written via
/// <see cref="IAuditWriter"/> (the site <c>FallbackAuditWriter</c> +
/// <c>SqliteAuditWriter</c> chain established in M2).</description></item>
/// <c>SqliteAuditWriter</c> chain).</description></item>
/// <item><description>The operational <see cref="SiteCallOperational"/> half
/// updates the site-local <c>OperationTracking</c> SQLite store via
/// <see cref="IOperationTrackingStore"/>, with the per-lifecycle method
@@ -54,7 +54,7 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
private readonly ILogger<CachedCallTelemetryForwarder> _logger;
/// <summary>
/// SourceNode-stamping (Task 14): local node identity provider used to
/// SourceNode-stamping: local node identity provider used to
/// stamp the tracking-store row's <c>SourceNode</c> column on
/// <c>RecordEnqueueAsync</c>. Optional — when null (legacy / test hosts)
/// the column stays NULL on the tracking row.
@@ -64,11 +64,11 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
/// <summary>
/// Construct the forwarder. <paramref name="trackingStore"/> is optional —
/// when null only the audit half of the packet is emitted, which matches
/// the M3 Bundle F composition-root contract on Central nodes: the
/// the composition-root contract on Central nodes: the
/// AuditLog DI surface registers the forwarder unconditionally (mirroring
/// the IAuditWriter chain) but the site-only tracking store has no central
/// registration. Production site nodes wire both — the central lazy
/// resolution is a no-op path kept symmetric with the M2 writer chain.
/// resolution is a no-op path kept symmetric with the writer chain.
/// </summary>
/// <param name="auditWriter">Writer used to persist audit events from the telemetry packet.</param>
/// <param name="trackingStore">Optional store for updating operation tracking state; null on central nodes.</param>
@@ -111,7 +111,7 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
// FallbackAuditWriter) handles transient writer failures upstream;
// a throw bubbling up here means the writer's own swallow contract
// failed, which is itself best-effort-handled.
// C3: Kind/Status are domain fields carried in DetailsJson — decompose to log them.
// Kind/Status are domain fields carried in DetailsJson — decompose to log them.
var d = AuditRowProjection.Decompose(telemetry.Audit);
_logger.LogWarning(ex,
"CachedCallTelemetryForwarder: audit emission threw for EventId {EventId} (Kind {Kind}, Status {Status})",
@@ -130,7 +130,7 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
return;
}
// C3: the audit half's domain fields (Kind/SourceInstanceId/SourceScript)
// The audit half's domain fields (Kind/SourceInstanceId/SourceScript)
// ride inside DetailsJson — decompose once for this packet.
var audit = AuditRowProjection.Decompose(telemetry.Audit);
try
@@ -141,7 +141,7 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
// Enqueue — insert-if-not-exists with the operational
// channel as the kind discriminator. RetryCount is fixed
// at 0 by the tracking store's INSERT contract.
// SourceNode-stamping (Task 14): stamp the local node
// SourceNode-stamping: stamp the local node
// name (node-a/node-b) from the injected
// INodeIdentityProvider; null when no provider was wired
// so the tracking row's SourceNode column stays NULL.
@@ -24,8 +24,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
/// The <see cref="SiteAuditTelemetryActor"/> drain loop treats a thrown
/// exception as transient and leaves the rows <c>Pending</c> for the next tick.
/// Swallowing the fault into an empty ack would be indistinguishable from "zero
/// rows accepted" and would silently lose the retry signal. Task 1 confirmed
/// the central receiving end does not collapse an ingest fault into an empty
/// rows accepted" and would silently lose the retry signal. It has been confirmed
/// that the central receiving end does not collapse an ingest fault into an empty
/// ack either, so a site-side Ask through the whole path faults cleanly on a
/// central-side timeout.
/// </para>
@@ -26,7 +26,7 @@ public interface ISiteStreamAuditClient
Task<IngestAck> IngestAuditEventsAsync(AuditEventBatch batch, CancellationToken ct);
/// <summary>
/// Forwards the combined <see cref="CachedTelemetryBatch"/> (Audit Log #23)
/// Forwards the combined <see cref="CachedTelemetryBatch"/>
/// to the central cached-telemetry ingest path. Each packet carries both the
/// audit row and the operational <c>SiteCalls</c> upsert; central writes both
/// in a single MS SQL transaction. Returns the same <see cref="IngestAck"/>
@@ -45,7 +45,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
/// which case the cached-drain scheduler is never armed.
/// </para>
/// <para>
/// Per Bundle D's brief, audit-write paths must be fail-safe — a thrown
/// Audit-write paths must be fail-safe — a thrown
/// exception inside the actor MUST NOT crash it. Both Drain handlers wrap
/// their pipelines in a top-level try/catch that logs and re-schedules; the
/// actor's <see cref="SupervisorStrategy"/> defaults to
@@ -54,7 +54,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
/// matters.
/// </para>
/// <para>
/// AuditLog-001: wires the previously-unreachable combined-telemetry transport.
/// Wires the previously-unreachable combined-telemetry transport.
/// Prior to this the cached audit rows flowed through the audit-only drain via
/// <c>IngestAuditEventsAsync</c> and the central <c>OnCachedTelemetryAsync</c>
/// dual-write handler was dead production code; the operational <c>SiteCalls</c>
@@ -70,7 +70,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
private readonly ILogger<SiteAuditTelemetryActor> _logger;
private ICancelable? _pendingTick;
private ICancelable? _pendingCachedTick;
// AuditLog-010: per-actor lifecycle CTS so an in-flight drain (queue read,
// Per-actor lifecycle CTS so an in-flight drain (queue read,
// gRPC push, mark-forwarded write) is actually cancelled when the actor is
// stopped — without it, a stuck IngestAuditEventsAsync would hold the
// continuation through CoordinatedShutdown's actor-system terminate window.
@@ -132,7 +132,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
{
_pendingTick?.Cancel();
_pendingCachedTick?.Cancel();
// AuditLog-010: cancel any in-flight drain so a stuck queue read or
// Cancel any in-flight drain so a stuck queue read or
// gRPC push does not hold the continuation past actor stop.
try
{
@@ -149,7 +149,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
private async Task OnDrainAsync()
{
var nextDelay = TimeSpan.FromSeconds(_options.BusyIntervalSeconds);
// AuditLog-010: route every async dependency call through the
// Route every async dependency call through the
// per-actor lifecycle token so PostStop cancellation actually
// propagates into the queue read, the gRPC push, and the
// mark-forwarded write. OperationCanceledException is swallowed by
@@ -178,7 +178,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
catch (Exception ex)
{
// gRPC fault — leave the rows in Pending so the next drain
// retries. Bundle D's brief: "On gRPC exception (any), log
// retries "On gRPC exception (any), log
// Warning, schedule next Drain in BusyIntervalSeconds."
_logger.LogWarning(ex,
"IngestAuditEvents push failed for {Count} pending events; will retry next drain.",
@@ -201,7 +201,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
}
finally
{
// AuditLog-010: if the actor is already shutting down, do not
// If the actor is already shutting down, do not
// arm another tick — the scheduler would fire after PostStop and
// the message would land in dead letters.
if (!_lifecycleCts.IsCancellationRequested)
@@ -212,7 +212,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
}
/// <summary>
/// AuditLog-001: combined-telemetry drain. Reads cached-lifecycle audit
/// Combined-telemetry drain. Reads cached-lifecycle audit
/// rows, joins each with the matching <see cref="IOperationTrackingStore"/>
/// snapshot, builds a <see cref="CachedTelemetryBatch"/>, and pushes via
/// <see cref="ISiteStreamAuditClient.IngestCachedTelemetryAsync"/>. Rows
@@ -354,7 +354,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
}
/// <summary>
/// AuditLog-001: build the combined wire packet from one cached audit row
/// Build the combined wire packet from one cached audit row
/// + its matching operational tracking snapshot. The operational state
/// reflects the latest tracking row at emission time (not the per-event
/// status the audit row implies) because central's <c>SiteCalls</c>
@@ -364,7 +364,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
private static CachedTelemetryPacket BuildCachedPacket(
AuditEvent auditRow, TrackingStatusSnapshot snapshot)
{
// C3: SourceSiteId + Channel ride inside the canonical record's
// SourceSiteId + Channel ride inside the canonical record's
// DetailsJson — decompose to read them.
var audit = AuditRowProjection.Decompose(auditRow);
var sourceSite = audit.SourceSiteId ?? string.Empty;
@@ -457,7 +457,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
/// <summary>
/// Self-tick message that triggers a combined-telemetry drain cycle.
/// AuditLog-001: introduced alongside the cached-drain to keep the two
/// Introduced alongside the cached-drain to keep the two
/// paths' cadences independent — a stall on one does not block the other.
/// </summary>
private sealed class CachedDrain
@@ -2,7 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
/// <summary>
/// Tuning knobs for the site-side <see cref="SiteAuditTelemetryActor"/> drain
/// loop. Defaults mirror Bundle D's plan: drain every 5 s while rows are
/// loop. Defaults: drain every 5 s while rows are
/// flowing (busy), every 30 s when the queue is empty (idle).
/// </summary>
public sealed class SiteAuditTelemetryOptions
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Bundle D D1: SiteAuditTelemetryActor + (D2) AuditLogIngestActor live
<!-- SiteAuditTelemetryActor + AuditLogIngestActor live
in this project, so Akka is an explicit dependency. -->
<PackageReference Include="Akka" />
<PackageReference Include="Microsoft.Data.Sqlite" />
@@ -32,7 +32,7 @@ internal static class AlarmTriggerConfigJson
/// <param name="hiHi">HiLo: high-high setpoint.</param>
/// <param name="expression">Expression: boolean trigger expression.</param>
/// <param name="analysisKind">
/// M9-T28b: optional analysis kind for Expression triggers ("strict" → emits
/// Optional analysis kind for Expression triggers ("strict" → emits
/// <c>"analysisKind":"Strict"</c>; null/"advisory"/anything else → Advisory default,
/// key omitted). Ignored for non-Expression trigger types.
/// </param>
@@ -86,7 +86,7 @@ internal static class AlarmTriggerConfigJson
break;
case "expression":
w.WriteString("expression", expression ?? "");
// M9-T28b: emit "analysisKind":"Strict" only when the caller passes
// Emit "analysisKind":"Strict" only when the caller passes
// --trigger-kind strict (case-insensitive); Advisory (the default) is
// expressed by omitting the key, matching ValidationService.IsStrictAnalysis.
if (string.Equals(analysisKind?.Trim(), "Strict", StringComparison.OrdinalIgnoreCase))
+1 -1
View File
@@ -40,7 +40,7 @@ public class CliConfig
".scadabridge", "config.json");
if (File.Exists(configPath))
{
// CLI-021: a malformed (`JsonException`), unreadable
// A malformed (`JsonException`), unreadable
// (`UnauthorizedAccessException`), or otherwise faulted
// (`IOException`) config file must not crash the CLI before any
// command runs — even a command that supplies everything via
@@ -27,8 +27,8 @@ public sealed class AuditBackfillSourceNodeArgs
}
/// <summary>
/// Pure helpers for the <c>audit backfill-source-node</c> subcommand (Audit Log
/// #23 M5.6 T5). Builds the request body, POSTs to
/// Pure helpers for the <c>audit backfill-source-node</c> subcommand. Builds
/// the request body, POSTs to
/// <c>/api/audit/backfill-source-node</c>, and renders the result. Kept separate
/// from the command wiring so each piece is unit-testable without standing up the
/// command tree.
@@ -4,8 +4,8 @@ using System.CommandLine.Parsing;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// The <c>scadabridge audit</c> command group (Audit Log #23 M8). Provides read access to
/// the centralized append-only Audit Log via the Bundle B REST endpoints
/// The <c>scadabridge audit</c> command group. Provides read access to
/// the centralized append-only Audit Log via the REST endpoints
/// (<c>GET /api/audit/query</c>, <c>GET /api/audit/export</c>,
/// <c>GET /api/audit/tree</c>), plus a v1 no-op <c>verify-chain</c> placeholder
/// for the deferred hash-chain tamper-evidence feature.
@@ -291,7 +291,7 @@ public static class AuditCommands
}
/// <summary>
/// Builds the <c>audit backfill-source-node</c> sub-command (Audit Log #23 M5.6 T5).
/// Builds the <c>audit backfill-source-node</c> sub-command.
/// Sets <c>SourceNode</c> on historical pre-feature rows whose <c>SourceNode IS NULL</c>
/// and <c>OccurredAtUtc</c> is older than <c>--before</c>, in batches. Admin-only.
/// </summary>
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Filter + destination arguments for an <c>audit export</c> invocation. Mirrors the
/// Bundle B <c>GET /api/audit/export</c> parameters.
/// <c>GET /api/audit/export</c> parameters.
/// <see cref="Channel"/>/<see cref="Kind"/>/<see cref="Status"/>/<see cref="Site"/>
/// are multi-valued — each supplied value becomes a repeated query-string param so
/// the server's multi-value <c>IN (…)</c> filter sees the full set, exactly like
@@ -150,7 +150,7 @@ public static class AuditExportHelpers
if (!response.IsSuccessStatusCode)
{
var message = await response.Content.ReadAsStringAsync();
// CLI-018: honour the documented "authorization failure → exit 2"
// Honour the documented "authorization failure → exit 2"
// contract on the REST audit surface as well. HTTP 403 is the
// primary signal; the server may also surface UNAUTHORIZED /
// FORBIDDEN via the JSON error envelope on a non-403 status.
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Renders a page of audit-log events to a writer. The <c>audit query</c> command picks
/// a formatter from the <c>--format</c> option. The default JSONL formatter is defined
/// here; the human-readable table formatter is supplied by Bundle C.
/// here; the human-readable table formatter is supplied by <see cref="TableAuditFormatter"/>.
/// </summary>
public interface IAuditFormatter
{
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// centralized append-only Audit Log served by <see cref="AuditCommands"/>).
/// </summary>
/// <remarks>
/// Renamed from <c>audit-log</c> in #23 M8-T7 to avoid confusion with the new
/// Renamed from <c>audit-log</c> to avoid confusion with the new
/// <c>scadabridge audit</c> group. The old <c>audit-log</c> name is retained as a
/// deprecated alias; <see cref="DeprecatedAlias"/> still resolves the full subcommand
/// tree, and <c>Program.cs</c> prints a deprecation warning when it is used.
@@ -54,7 +54,7 @@ public static class AuditLogCommands
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" };
// Backward-compatible alias for the pre-M8 `audit-log` name. The alias keeps
// Backward-compatible alias for the old `audit-log` name. The alias keeps
// full subcommand parity automatically; the deprecation warning is emitted by
// the args[0] check in Program.cs.
command.Aliases.Add(DeprecatedAlias);
@@ -6,7 +6,7 @@ using System.Text.RegularExpressions;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Filter arguments for an <c>audit query</c> invocation. Mirrors the Bundle B
/// Filter arguments for an <c>audit query</c> invocation. Mirrors the
/// <c>GET /api/audit/query</c> filter parameters; <see cref="Since"/>/<see cref="Until"/>
/// are time-specs (relative like <c>1h</c>/<c>7d</c>, or absolute ISO-8601).
/// <see cref="Channel"/>/<see cref="Kind"/>/<see cref="Status"/>/<see cref="Site"/>
@@ -192,7 +192,7 @@ public static class AuditQueryHelpers
{
OutputFormatter.WriteError(
response.Error ?? "Audit query failed.", response.ErrorCode ?? "ERROR");
// CLI-018: surface the documented "authorization failure → exit 2"
// Surface the documented "authorization failure → exit 2"
// contract for the audit REST surface too, not just /management.
return CommandHelpers.IsAuthorizationFailure(response) ? 2 : 1;
}
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Transport (#24) bundle export / preview / import. The bundle bytes travel
/// Transport bundle export / preview / import. The bundle bytes travel
/// through the management endpoint as base64 inside the standard JSON envelope
/// so no transport plumbing diverges from the other commands; the CLI handles
/// file I/O at the edges.
@@ -61,14 +61,14 @@ public static class BundleCommands
var dbConnectionsOption = NameListOption("--db-connections", "Comma-separated database-connection names");
var notificationListsOption = NameListOption("--notification-lists", "Comma-separated notification-list names");
var smtpConfigsOption = NameListOption("--smtp-configs", "Comma-separated SMTP host names");
// SMS (S10b): SmsConfiguration is keyed by AccountSid (no Name column), so
// SMS: SmsConfiguration is keyed by AccountSid (no Name column), so
// tokens are AccountSid values — mirrors --smtp-configs using Host.
var smsConfigsOption = NameListOption("--sms-configs", "Comma-separated SMS provider account SIDs");
// Inbound API keys are not transported between environments (re-arch C4) — no
// Inbound API keys are not transported between environments — no
// --api-keys option. Re-create keys and re-grant their method scopes on the
// destination via the admin UI/CLI.
var apiMethodsOption = NameListOption("--api-methods", "Comma-separated API-method names");
// M8 (B4): site/instance-scoped export. Sites accept a SiteIdentifier
// Site/instance-scoped export. Sites accept a SiteIdentifier
// (preferred) or friendly Name per token; instances accept a UniqueName.
var sitesOption = NameListOption("--sites", "Comma-separated site identifiers or names");
var instancesOption = NameListOption("--instances", "Comma-separated instance unique-names");
@@ -131,7 +131,7 @@ public static class BundleCommands
timeout: BundleCommandTimeout,
onSuccess: jsonOk =>
{
// CLI-020: previously the JSON envelope parse + property extraction +
// Previously the JSON envelope parse + property extraction +
// base64 decode all ran unguarded — a server-side bug that omits one of
// the two expected properties, returns a null base64 value, sends invalid
// base64, or returns a malformed JSON envelope would surface as one of
@@ -139,7 +139,7 @@ public static class BundleCommands
// JsonException, i.e. an unhandled stack trace rather than the
// documented "exit 1 with a clean INVALID_RESPONSE error". Wrap the
// envelope parse and the streamed write in a single try/catch matching
// the graceful-degradation theme established by CLI-002 / CLI-003 / CLI-005.
// the graceful-degradation theme established elsewhere in this command group.
string base64;
int byteCount;
try
@@ -158,7 +158,7 @@ public static class BundleCommands
return 1;
}
// CLI-019: stream the base64 → file write so a 100 MB bundle
// Stream the base64 → file write so a 100 MB bundle
// doesn't double-buffer through Convert.FromBase64String's
// ~100 MB byte[] on the LOH plus a synchronous File.WriteAllBytes.
// The management envelope's body is still buffered into the
@@ -314,7 +314,7 @@ public static class BundleCommands
Description = "Resolution policy applied to every Modified row: skip, overwrite, or rename. Default: overwrite.",
DefaultValueFactory = _ => "overwrite",
};
// M8 (D3): site/connection name mapping. Repeatable. A token with no
// Site/connection name mapping. Repeatable. A token with no
// "=dst" part, or "=" with an empty right-hand side, means CreateNew (the
// destination is created from the bundle payload); otherwise the source
// is mapped to the named existing destination.
@@ -400,7 +400,7 @@ public static class BundleCommands
// Shared HTTP plumbing
// ====================================================================
//
// CLI-017: bundle commands previously routed through a private
// Bundle commands previously routed through a private
// RunBundleCommandAsync that re-implemented URL/credential resolution and
// skipped the IsAuthorizationFailure(...) check that ExecuteCommandAsync
// enforces — a server that signalled FORBIDDEN/UNAUTHORIZED via the error
@@ -409,7 +409,7 @@ public static class BundleCommands
// the longer BundleCommandTimeout and a per-command success handler, so the
// exit-code contract is unified across every command group.
// CLI-019: chunked base64 → file streaming. The management envelope's
// Chunked base64 → file streaming. The management envelope's
// success body is a single buffered JSON string (the wire format does not
// currently support response-body streaming), so we cannot remove the
// ~base64-string + ~envelope-string allocation. What we CAN — and do —
@@ -27,13 +27,12 @@ internal static class CommandHelpers
/// body instead of running the default <see cref="HandleResponse"/> rendering path —
/// useful when the caller needs to capture the response (e.g. write a file) rather
/// than print it. The authorization-failure exit-code contract
/// (<see cref="IsAuthorizationFailure"/>) is preserved on the error path either way,
/// closing CLI-017's regression.
/// (<see cref="IsAuthorizationFailure"/>) is preserved on the error path either way.
/// </param>
/// <param name="tableProjector">
/// Optional transform applied to the success JSON body <em>only</em> when the resolved
/// format is <c>table</c>. Lets a command render a compact table projection (e.g.
/// <c>template list</c> dropping per-template attribute dumps, followup #6) while
/// <c>template list</c> dropping per-template attribute dumps) while
/// leaving JSON output untouched for machine consumers. Ignored when
/// <paramref name="onSuccess"/> is supplied.
/// </param>
@@ -95,8 +94,7 @@ internal static class CommandHelpers
// Caller-supplied success handler short-circuits the default rendering — but
// the error path still routes through IsAuthorizationFailure so the documented
// exit-2 contract holds whether or not a custom handler is provided
// (CLI-017 unification of the bundle path).
// exit-2 contract holds whether or not a custom handler is provided.
if (onSuccess is not null)
{
if (response.JsonData is not null)
@@ -255,7 +253,7 @@ internal static class CommandHelpers
// Derive the header set as the union of property names across *every*
// element, in first-seen order. Using only items[0] would silently drop
// columns for any later element with a different shape (CLI-016).
// columns for any later element with a different shape.
var objectItems = items.Where(i => i.ValueKind == JsonValueKind.Object).ToList();
string[] headers;
if (objectItems.Count > 0)
@@ -100,7 +100,7 @@ public static class DebugCommands
.WithAutomaticReconnect()
.Build();
// CLI-011: CancellationTokenSource owns a WaitHandle and must be disposed.
// CancellationTokenSource owns a WaitHandle and must be disposed.
using var cts = new CancellationTokenSource();
var exitTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -199,7 +199,7 @@ public static class DebugCommands
}
catch (Exception ex)
{
// CLI-010: Ctrl+C during connect throws OperationCanceledException — that is
// Ctrl+C during connect throws OperationCanceledException — that is
// a graceful user cancellation, not a connection failure.
var failure = DebugStreamHelpers.ClassifyConnectFailure(ex, cts.IsCancellationRequested);
if (failure.IsCancellation)
@@ -250,7 +250,7 @@ public static class DebugCommands
await connection.DisposeAsync();
// CLI-012: resolve the exit code from a single authoritative source. A result
// Resolve the exit code from a single authoritative source. A result
// set by OnStreamTerminated/Closed always wins; a brief grace period covers a
// termination racing with Ctrl+C. Pure Ctrl+C (no result) is a graceful exit 0.
return await DebugStreamHelpers.ResolveStreamExitCodeAsync(exitTcs.Task);
@@ -3,8 +3,8 @@ namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Pure, testable helpers for the <c>debug stream</c> command. The SignalR-driven
/// <see cref="DebugCommands"/> body itself cannot be unit-tested without a live hub, so
/// the decision logic — connect-failure classification (CLI-010) and exit-code
/// resolution after stream termination (CLI-012) — is extracted here.
/// the decision logic — connect-failure classification and exit-code
/// resolution after stream termination — is extracted here.
/// </summary>
internal static class DebugStreamHelpers
{
@@ -3,8 +3,8 @@ using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Human-readable table formatter for <c>audit query --format table</c> (Audit Log
/// #23 M8-T6). Renders each fetched page as a column-aligned text table with a fixed
/// Human-readable table formatter for <c>audit query --format table</c>.
/// Renders each fetched page as a column-aligned text table with a fixed
/// column set (<see cref="Columns"/>). Long free-text fields (Target, Actor) are
/// truncated with an ellipsis so columns stay aligned regardless of payload size.
/// </summary>
@@ -446,7 +446,7 @@ public static class TemplateCommands
/// <summary>
/// Determines whether <c>--trigger-kind</c> was supplied without a <c>--trigger-config</c>.
/// In that case <see cref="TriggerConfigJson.InjectAnalysisKind"/> has no JSON object to inject
/// into and silently drops the kind (#257) — the caller should warn the user.
/// into and silently drops the kind — the caller should warn the user.
/// </summary>
/// <param name="triggerConfig">The raw <c>--trigger-config</c> JSON value, or null when absent.</param>
/// <param name="triggerKind">The raw <c>--trigger-kind</c> value, or null when absent.</param>
@@ -502,7 +502,7 @@ public static class TemplateCommands
var hiOption = new Option<double?>("--hi") { Description = "HiLo: high setpoint" };
var hiHiOption = new Option<double?>("--hihi") { Description = "HiLo: high-high setpoint" };
var expressionOption = new Option<string?>("--expression") { Description = "Expression: boolean trigger expression" };
// M9-T28b: analysis kind for Expression triggers (advisory|strict; default advisory).
// Analysis kind for Expression triggers (advisory|strict; default advisory).
// Writes "analysisKind":"Strict" into the trigger config when set to strict.
var triggerKindOption = new Option<string?>("--trigger-kind")
{
@@ -567,7 +567,7 @@ public static class TemplateCommands
var updateTriggerConfigOption = new Option<string?>("--trigger-config") { Description = "Trigger configuration JSON" };
var updateLockedOption = new Option<bool>("--locked") { Description = "Lock status" };
updateLockedOption.DefaultValueFactory = _ => false;
// M9-T28b: --trigger-kind for update (same semantics as add)
// --trigger-kind for update (same semantics as add)
var updateTriggerKindOption = new Option<string?>("--trigger-kind")
{
Description = AlarmTriggerKindOptionDescription
@@ -699,7 +699,7 @@ public static class TemplateCommands
var returnOption = new Option<string?>("--return-def") { Description = "Return definition JSON" };
var minTimeOption = new Option<string?>("--min-time-between-runs") { Description = MinTimeBetweenRunsOptionDescription };
var execTimeoutOption = new Option<int?>("--execution-timeout-seconds") { Description = ExecutionTimeoutOptionDescription };
// M9-T28b: analysis kind for Expression triggers (advisory|strict; default advisory).
// Analysis kind for Expression triggers (advisory|strict; default advisory).
var scriptTriggerKindOption = new Option<string?>("--trigger-kind")
{
Description = ScriptTriggerKindOptionDescription
@@ -758,7 +758,7 @@ public static class TemplateCommands
var updateReturnOption = new Option<string?>("--return-def") { Description = "Return definition JSON" };
var updateMinTimeOption = new Option<string?>("--min-time-between-runs") { Description = MinTimeBetweenRunsOptionDescription };
var updateExecTimeoutOption = new Option<int?>("--execution-timeout-seconds") { Description = ExecutionTimeoutOptionDescription };
// M9-T28b: --trigger-kind for update (same semantics as add)
// --trigger-kind for update (same semantics as add)
var updateScriptTriggerKindOption = new Option<string?>("--trigger-kind")
{
Description = ScriptTriggerKindOptionDescription
@@ -4,7 +4,7 @@ using System.Text.Json.Nodes;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// Compact table projection for <c>template list</c> / <c>template get</c> (followup #6).
/// Compact table projection for <c>template list</c> / <c>template get</c>.
/// The management API returns full <c>Template</c> entities — every attribute, alarm,
/// script, and composition inline — which the generic table renderer dumps as one giant
/// cell per template (~171 KB for a real catalogue, unusable in a terminal). This
@@ -93,7 +93,7 @@ public class ManagementHttpClient : IDisposable
/// <summary>
/// Issues a plain HTTP <c>GET</c> against a REST endpoint (e.g. the audit
/// <c>/api/audit/query</c> endpoint introduced by Audit Log #23 M8) and returns the
/// <c>/api/audit/query</c> endpoint) and returns the
/// response body. Unlike <see cref="SendCommandAsync"/>, this does not wrap the call
/// in the <c>POST /management</c> command envelope — the audit endpoints are plain
/// REST resources. Authentication (HTTP Basic) and the base address are shared.
+1 -1
View File
@@ -41,7 +41,7 @@ rootCommand.SetAction(_ =>
Console.WriteLine("Use --help to see available commands.");
});
// Deprecation notice for the pre-M8 `audit-log` command name. The command itself
// Deprecation notice for the earlier `audit-log` command name. The command itself
// still works (it is an alias of `audit-config`), but using the old name emits a
// warning to stderr so scripts can be migrated.
AuditLogCommands.WriteDeprecationWarningIfNeeded(args, Console.Error);
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Audit;
/// <summary>
/// Minimal-API endpoint hosting the Audit Log CSV export (#23 M7-T14 / Bundle F).
/// Minimal-API endpoint hosting the Audit Log CSV export.
///
/// <para>
/// CentralUI ships no MVC controllers (see <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.Auth.AuthEndpoints"/>
@@ -24,7 +24,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Audit;
///
/// <para>
/// The route is gated on the <see cref="AuthorizationPolicies.AuditExport"/>
/// policy (#23 M7-T15 / Bundle G) so only roles with the bulk-export
/// policy so only roles with the bulk-export
/// permission can pull a CSV — the page-level
/// <see cref="AuthorizationPolicies.OperationalAudit"/> gate is read-only
/// and intentionally narrower. The query-string parser silently drops
@@ -45,7 +45,7 @@ public static class AuthEndpoints
}
// Map LDAP groups to roles via the shared IGroupRoleMapper<string> seam
// (Task 1.1 ScadaBridgeGroupRoleMapper, wrapping the DB-backed RoleMapper).
// (ScadaBridgeGroupRoleMapper, wrapping the DB-backed RoleMapper).
// The full RoleMappingResult — including PermittedSiteIds and the
// system-wide flag — is carried in the mapping's opaque Scope so the
// site-scope→SiteId claims below are built exactly as before.
@@ -63,7 +63,7 @@ public static class AuthEndpoints
: new RoleMappingResult(roleMapping.Roles, [], IsSystemWideDeployment: false);
// Build claims from LDAP auth + role mapping.
// CentralUI-005: no fixed "expires_at" absolute-cap claim is stamped
// No fixed "expires_at" absolute-cap claim is stamped
// — session expiry is owned by the cookie middleware's sliding window
// (ZB.MOM.WW.ScadaBridge.Security AddCookie: ExpireTimeSpan = idle timeout,
// SlidingExpiration = true). A frozen absolute claim would contradict
@@ -71,11 +71,11 @@ public static class AuthEndpoints
var displayName = string.IsNullOrEmpty(authResult.DisplayName) ? username : authResult.DisplayName;
var resolvedUsername = string.IsNullOrEmpty(authResult.Username) ? username : authResult.Username;
// M2.19 (#15): build the cookie principal through the shared
// Build the cookie principal through the shared
// SessionClaimBuilder — the SINGLE source of truth that the mid-session
// OnValidatePrincipal role-refresh path ALSO uses, so login and refresh can
// never drift. It stamps the canonical identity/role/scope claims (with
// roleType/nameType pinned for IsInRole), PLUS the M2.19 additions: one
// roleType/nameType pinned for IsInRole), PLUS these additions: one
// zb:group claim per raw LDAP group (the durable input the mid-session
// RoleMapper re-run consumes) and a zb:lastrolerefresh anchor (login time,
// UTC) that also seeds the LastActivity idle anchor. The refresh timestamp is
@@ -147,7 +147,7 @@ public static class AuthEndpoints
});
}).DisableAntiforgery();
// Logout is a state-changing authenticated action (CentralUI-017): it
// Logout is a state-changing authenticated action: it
// keeps antiforgery validation enabled so it cannot be triggered
// cross-site. The NavMenu sign-out form includes the antiforgery token
// (rendered by the <AntiforgeryToken /> component). There is deliberately
@@ -159,9 +159,9 @@ public static class AuthEndpoints
context.Response.Redirect("/login");
});
// CentralUI-020: liveness probe for the client-side idle-logout check.
// Liveness probe for the client-side idle-logout check.
// The Blazor circuit's CookieAuthenticationStateProvider serves a frozen
// constructor-time principal (CentralUI-004), so a circuit can never
// constructor-time principal, so a circuit can never
// observe a server-side cookie expiry by polling the auth state.
// SessionExpiry instead polls this endpoint via fetch(): being a normal
// HTTP request, the cookie middleware re-validates (and slides) the
@@ -178,7 +178,7 @@ public static class AuthEndpoints
/// <summary>
/// Handler for <c>GET /auth/ping</c>. Returns <c>200</c> while the caller's
/// cookie session is still valid and <c>401</c> once it has lapsed
/// server-side. See CentralUI-020.
/// server-side.
/// </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>
@@ -192,7 +192,7 @@ public static class AuthEndpoints
/// <summary>
/// Builds the <see cref="AuthenticationProperties"/> for the login sign-in.
/// CentralUI-005: deliberately does <b>not</b> set <see cref="AuthenticationProperties.ExpiresUtc"/>.
/// Deliberately does <b>not</b> set <see cref="AuthenticationProperties.ExpiresUtc"/>.
/// Session expiry is owned by the cookie authentication middleware's sliding
/// window (configured in <c>ZB.MOM.WW.ScadaBridge.Security</c>'s <c>AddCookie</c>:
/// <c>ExpireTimeSpan</c> = the idle timeout, <c>SlidingExpiration = true</c>).
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
/// <summary>
/// Claim-lookup helpers for the Central UI. CentralUI-024: claim types are owned
/// Claim-lookup helpers for the Central UI. Claim types are owned
/// by <see cref="JwtTokenService"/> (the single source of truth). These helpers
/// resolve them through the <c>JwtTokenService</c> constants so a rename there
/// propagates here instead of silently breaking ten copy-pasted call sites.
@@ -36,7 +36,7 @@ public static class ClaimsPrincipalExtensions
/// <summary>
/// Resolves the current user's audit username from the auth state provider.
/// Replaces the <c>GetCurrentUserAsync</c> helper that was copy-pasted into
/// ten components (CentralUI-024).
/// ten components.
/// </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>
@@ -17,7 +17,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
/// </para>
/// <para>
/// We must NOT read <see cref="IHttpContextAccessor"/> on every
/// <see cref="GetAuthenticationStateAsync"/> call (CentralUI-004): for the
/// <see cref="GetAuthenticationStateAsync"/> call: for the
/// lifetime of a long-lived SignalR circuit <c>HttpContext</c> is <c>null</c>
/// (or, worse, a stale/foreign context), so a later re-evaluation —
/// e.g. <c>&lt;AuthorizeView&gt;</c> re-rendering — would otherwise see an
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
/// <summary>
/// Resolves the set of sites the current user is permitted to operate on, from
/// the <c>SiteId</c> claims attached at login (CentralUI-002).
/// the <c>SiteId</c> claims attached at login.
/// <para>
/// The design (Component-CentralUI, CLAUDE.md "Security &amp; Auth") makes the
/// Deployment role site-scoped: a Deployment user mapped through an LDAP group
@@ -93,7 +93,7 @@ public sealed class SiteScopeService
}
// No SiteId claims => system-wide. Absence of scope rules means an
// unrestricted deployer (Security-017 made this service the sole
// unrestricted deployer (this service is the sole
// site-scoping mechanism — there is no separate handler to mirror).
var result = (IsSystemWide: ids.Count == 0, Sites: (IReadOnlySet<int>)ids);
_cached = result;
@@ -1,7 +1,7 @@
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
@* Audit Log drilldown drawer (#23 M7 Bundle C / M7-T4..T8).
@* Audit Log drilldown drawer.
Right-side Bootstrap offcanvas-style drawer hosted by the Audit Log page.
The drawer owns only the offcanvas chrome (backdrop, header, Close buttons);
the single-AuditEvent detail body is delegated to <AuditEventDetail>, which
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <summary>
/// Child component for the central Audit Log page (#23 M7 Bundle C / M7-T4..T8).
/// Child component for the central Audit Log page.
/// Renders one <see cref="AuditEventView"/> in a right-side off-canvas drawer.
/// The drawer owns only the offcanvas chrome — backdrop, header, and the two
/// Close buttons; the single-row detail body (read-only fields, conditional
@@ -1,7 +1,7 @@
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
@* Reusable single-AuditEvent detail body (#23 M7 Bundle C / M7-T4..T8).
@* Reusable single-AuditEvent detail body.
Extracted from AuditDrilldownDrawer so the drawer and the execution-tree
node-detail modal share one rendering of a row's detail.
All form/field rendering follows the form-layout memory:
@@ -9,8 +9,8 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <summary>
/// Reusable single-<see cref="AuditEvent"/> detail body (#23 M7 Bundle C /
/// M7-T4..T8). Extracted verbatim from <see cref="AuditDrilldownDrawer"/> so
/// Reusable single-<see cref="AuditEvent"/> detail body. Extracted verbatim
/// from <see cref="AuditDrilldownDrawer"/> so
/// the drawer and the execution-tree node-detail modal render a row's detail
/// identically. Renders the read-only field list, the conditional
/// Error/Request/Response/Extra subsections, and the action buttons (Copy as
@@ -54,7 +54,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <see cref="AuditEvent.ParentExecutionId"/> is set the "View parent
/// execution" button navigates to <c>/audit/log?executionId={parentId}</c>
/// — the spawner's id used as the per-run drill-in target. All are deep
/// links the Audit Log page deserializes on init (Bundle D) and auto-loads.
/// links the Audit Log page deserializes on init and auto-loads.
/// </para>
/// </summary>
public partial class AuditEventDetail
@@ -281,7 +281,7 @@ public partial class AuditEventDetail
/// <summary>
/// Drill-in to the execution-chain TREE view (Audit Log ParentExecutionId
/// feature, Task 10). Navigates to
/// feature). Navigates to
/// <c>/audit/execution-tree?executionId={ExecutionId}</c> — the tree page
/// resolves the whole chain rooted at the topmost ancestor and renders it
/// expandably, with this row's execution highlighted. The button is only
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <summary>
/// Filter bar for the central Audit Log page (#23 M7-T2). Owns the
/// Filter bar for the central Audit Log page. Owns the
/// <see cref="AuditQueryModel"/> binding state and renders the filter controls
/// — Channel as a single-select (one channel at a time, so the Kind options
/// narrow to it cleanly); Kind / Status / Site as compact
@@ -55,7 +55,7 @@ public partial class AuditFilterBar
[Parameter] public Func<DateTime>? NowUtcProvider { get; set; }
/// <summary>
/// Bundle D drill-in seam (#23 M7-T10..T12). When set on first render,
/// When set on first render,
/// pre-populates the Instance free-text input. Instance is UI-only — the
/// repository filter contract has no instance column — so this flows in
/// through a separate parameter rather than the <see cref="AuditLogQueryFilter"/>
@@ -167,7 +167,7 @@ public partial class AuditFilterBar
private async Task Apply()
{
// CentralUI-026: <input type="datetime-local"> binds with DateTimeKind.Unspecified
// <input type="datetime-local"> binds with DateTimeKind.Unspecified
// — the value is the user's browser-local wall-clock. Tag it as Local then convert
// to UTC before the model emits the filter, otherwise a non-UTC operator's window
// is silently shifted by their UTC offset. Done on a swap-and-restore basis so the
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <summary>
/// UI-side binding model for <see cref="AuditFilterBar"/> (#23 M7-T2).
/// UI-side binding model for <see cref="AuditFilterBar"/>.
///
/// <para>
/// The model mirrors <see cref="AuditLogQueryFilter"/> but allows multi-select chip
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <summary>
/// Keyset-paged results grid for the central Audit Log page (#23 M7-T3).
/// Keyset-paged results grid for the central Audit Log page.
/// Renders the columns named in Component-AuditLog.md §10 — OccurredAtUtc,
/// Site, Channel, Kind, Status, Target, Actor, DurationMs, HttpStatus,
/// ErrorMessage — plus the ExecutionId per-run correlation column and the
@@ -66,7 +66,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
private bool _loading;
private string? _error;
// CentralUI-032: small in-component stack of prior-page cursors so the user
// Small in-component stack of prior-page cursors so the user
// can step backwards through results. Each Next push captures the cursor
// that produced the current page (null for page 1) before advancing; each
// Previous pop reloads the page at the recovered cursor. Mirrors the
@@ -108,7 +108,7 @@ public partial class AuditResultsGrid : IAsyncDisposable
[Parameter] public IReadOnlyList<string>? ColumnOrder { get; set; }
/// <summary>
/// Raised when the user clicks a row. Bundle C wires this to the drilldown
/// Raised when the user clicks a row. Consumers wire this to the drilldown
/// drawer. The event payload is the full <see cref="AuditEventView"/>.
/// </summary>
[Parameter] public EventCallback<AuditEventView> OnRowSelected { get; set; }
@@ -228,15 +228,15 @@ public partial class AuditResultsGrid : IAsyncDisposable
AfterOccurredAtUtc: last.OccurredAtUtc,
AfterEventId: last.EventId);
// CentralUI-032: remember the cursor that produced the current page so
// a later Previous can navigate back to it. The page-1 entry is pushed
// Remember the cursor that produced the current page so
// a later Previous can navigate back to it. The first-page entry is pushed
// as null — LoadAsync treats null as "first page" (PageSize-only).
_cursorStack.Push(_currentPaging);
await LoadAsync(cursor);
_pageNumber++;
}
// CentralUI-032: pops the previous-page cursor off the stack and reloads
// Pops the previous-page cursor off the stack and reloads
// at that position. The pop only happens AFTER a successful reload — a
// failed page-fetch leaves the user on the current page with the error
// banner instead of stranding them between pages.
@@ -8,8 +8,8 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <summary>
/// Execution-Tree Node Detail Modal (Execution-Tree Node Detail Modal feature,
/// Task 3). Opened from an execution-tree node double-click: given an
/// Execution-Tree Node Detail Modal. Opened from an execution-tree node
/// double-click: given an
/// <see cref="ExecutionId"/> it loads that execution's audit rows via
/// <see cref="IAuditLogQueryService"/> and shows a list → per-row detail.
///
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;
/// <summary>
/// Recursive Blazor tree component for the execution-chain view (Audit Log
/// ParentExecutionId feature, Task 10).
/// ParentExecutionId feature).
///
/// <para>
/// <b>Flat list → tree.</b> The repository / query service returns the chain as
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components;
/// <para>
/// CLAUDE.md mandates UTC throughout the system, but a <c>datetime-local</c>
/// value carries no offset, so it must be <i>converted</i> to UTC, not relabelled
/// as UTC. Relabelling (the CentralUI-008 bug) shifts every query window by the
/// as UTC. Relabelling shifts every query window by the
/// user's offset for any non-UTC browser.
/// </para>
/// </summary>
@@ -134,7 +134,7 @@
private string _failureMessage = "";
private List<TreeNode> _rootNodes = new();
// Search state (T16): _searchActive distinguishes "ran a search that found
// Search state: _searchActive distinguishes "ran a search that found
// nothing" from "never searched"; a blank query clears the panel entirely.
private string _searchQuery = "";
private bool _searchActive;
@@ -157,7 +157,7 @@
public bool HasChildren { get; }
/// <summary>
/// Friendly DataType name for Variable nodes (T15 type column); null when
/// Friendly DataType name for Variable nodes; null when
/// not a Variable or the type read failed. Rendered as a muted badge.
/// </summary>
public string? DataType { get; init; }
@@ -261,7 +261,7 @@
node.Expanded = true;
}
// Load-more / BrowseNext (T15): fetch the next page under a truncated node,
// Load-more / BrowseNext: fetch the next page under a truncated node,
// APPEND the children (preserve what's already shown + expanded), and refresh
// the stored token (null → exhausted → the button disappears).
private async Task LoadMoreAsync(TreeNode node)
@@ -297,7 +297,7 @@
private Task SearchOnEnter(KeyboardEventArgs e) =>
e.Key == "Enter" ? SearchAsync() : Task.CompletedTask;
// Address-space search (T16): a blank query clears the panel; otherwise run
// Address-space search: a blank query clears the panel; otherwise run
// the bounded recursive search and render matches as a flat selectable list.
private async Task SearchAsync()
{
@@ -349,7 +349,7 @@
[Parameter] public bool IsLegacy { get; set; }
[Parameter] public ValidationResult? Errors { get; set; }
// Verify-endpoint context (M7 T17): the site + connection identity the verify
// Verify-endpoint context (M7): the site + connection identity the verify
// probe targets. Supplied by DataConnectionForm (_formSiteId → SiteIdentifier,
// _formName, _protocol). When SiteIdentifier is blank the connection has not been
// assigned a site yet, so verification is unavailable.
@@ -394,7 +394,7 @@
}
}
// M7 T17: trust the captured untrusted server certificate at every node of the
// M7: trust the captured untrusted server certificate at every node of the
// owning site, then re-run Verify (which should now succeed and clear the cert
// panel). Administrator-gated by the AuthorizeView wrapping the button; the
// CertManagementService enforces the same role server-side-of-the-trust-boundary.
@@ -1,5 +1,5 @@
@*
Audit Log (#23) M7 Bundle E (T13) — three Health-dashboard KPI tiles for the
Three Health-dashboard KPI tiles for the
Audit channel: Volume / Error rate / Backlog. Renders Bootstrap card tiles in
a single row, each acting as a navigation link to a pre-filtered Audit Log
view. The component is purely presentational — the parent page owns the
@@ -4,11 +4,11 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Health;
/// <summary>
/// Audit Log (#23) M7 Bundle E (T13) code-behind for <see cref="AuditKpiTiles"/>.
/// Code-behind for <see cref="AuditKpiTiles"/>.
/// Renders three KPI tiles — volume, error rate, backlog — from a
/// <see cref="AuditLogKpiSnapshot"/> the parent page supplies. Tiles act as
/// drill-in links: clicking navigates to <c>/audit/log</c> with the relevant
/// query-string filter pre-applied (Bundle D already parses these params).
/// query-string filter pre-applied (the page already parses these params).
/// </summary>
/// <remarks>
/// <para>
@@ -94,7 +94,7 @@ public partial class AuditKpiTiles
{
return "—";
}
// Format to one decimal so a 1-error-in-2000 rate doesn't round to 0%.
// Format to one decimal so a rate of 1 error in 2000 doesn't round to 0%.
return $"{ErrorRatePercent:0.0}%";
}
}
@@ -58,7 +58,7 @@
{
<div class="text-muted small mb-3">Site Call KPIs unavailable: @ErrorMessage</div>
}
@* ── Per-node stuck/parked sub-table (T6: M5.2 per-node stuck-count KPIs) ── *@
@* ── Per-node stuck/parked sub-table ── *@
@if (HasNodeBreakdown)
{
<div class="mb-3">
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Health;
/// <summary>
/// Site Call Audit (#22) Task 7 code-behind for <see cref="SiteCallKpiTiles"/>.
/// Site Call Audit code-behind for <see cref="SiteCallKpiTiles"/>.
/// Renders three KPI tiles — Buffered, Stuck, Parked — from a
/// <see cref="SiteCallKpiResponse"/> the parent Health dashboard supplies.
/// Tiles act as drill-in links: clicking navigates to <c>/site-calls/report</c>
@@ -61,7 +61,7 @@ public partial class SiteCallKpiTiles
[Parameter] public string? ErrorMessage { get; set; }
/// <summary>
/// Optional per-node KPI breakdown (T6: M5.2 per-node stuck-count KPIs).
/// Optional per-node KPI breakdown.
/// When non-null and non-empty, a compact node-level stuck/parked sub-table
/// is rendered below the main tiles. <c>null</c> means the parent has not
/// loaded it yet or has opted out — the sub-table is suppressed entirely.
@@ -8,7 +8,7 @@
<NavMenu />
</Nav>
<RailFooter>
@* T34b: dark-mode toggle sits in the rail footer alongside the session
@* Dark-mode toggle sits in the rail footer alongside the session
block. It is auth-agnostic (pure client-side theme) so it renders even
on the login page. *@
<DarkModeToggle />
@@ -85,7 +85,7 @@
</Authorized>
</AuthorizeView>
@* Operations — Secured Writes (M7 T14b). Two-person MxGateway write workflow:
@* Operations — Secured Writes. Two-person MxGateway write workflow:
Operator submits, a different Verifier approves/rejects. The section must show
for Operator OR Verifier. There is no combined policy, so the OR is expressed
as: Operator → render; otherwise (NotAuthorized) fall through to a Verifier
@@ -124,8 +124,7 @@
</AuthorizeView>
</NavRailSection>
@* Audit — gated on the OperationalAudit policy (#23 M7-T15
/ Bundle G). Hosts the Audit Log page (#23 M7) and the
@* Audit — gated on the OperationalAudit policy. Hosts the Audit Log page (#23 M7) and the
Configuration Audit Log (IAuditService config-change
viewer). The whole section sits inside the policy block:
a non-audit user does not even see the heading.
@@ -29,7 +29,7 @@
@:Add API Key
}
</h4>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit Log
@* Drill-in: deep-link into the central Audit Log
pre-filtered to this API key's inbound calls. Inbound audit rows record
the key Name as Actor and live on the ApiInbound channel. *@
@if (IsEditMode && !string.IsNullOrWhiteSpace(_formName))
@@ -112,7 +112,7 @@
</div>
@code {
// Inbound-API key re-arch (C3): this form drives the IInboundApiKeyAdmin seam.
// Inbound-API key re-arch: this form drives the IInboundApiKeyAdmin seam.
// Keys are identified by an opaque string KeyId; method access is a set of method
// NAMES (scopes) carried on the key, replacing the old ApiMethod.ApprovedApiKeyIds CSV.
// The list of all methods still comes from IInboundApiRepository (methods stay in SQL).
@@ -97,7 +97,7 @@
</div>
@code {
// Inbound-API key re-arch (C3): this page reads keys from the IInboundApiKeyAdmin seam
// Inbound-API key re-arch: this page reads keys from the IInboundApiKeyAdmin seam
// (string KeyId, method-scopes) rather than the SQL Server ApiKey entity. The seam has no
// retrievable hash, so the old masked Key-Hash column is gone; KeyId identifies each row.
private List<InboundApiKeyInfo> _keys = new();
@@ -22,7 +22,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<h6 class="card-title">@(IsEditMode ? "Edit Site" : "Add Site")</h6>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit
@* Drill-in: deep-link into the central Audit
Log pre-filtered to this site's events. AuditEvent.SourceSiteId
stores the SiteIdentifier (string), so we pass that through. *@
@if (IsEditMode && !string.IsNullOrWhiteSpace(_formIdentifier))
@@ -12,7 +12,7 @@
<div class="container-fluid mt-3">
<h1 class="h4 mb-3">Audit Log</h1>
@* Trends panel (M6 / K15). A best-effort collapsible Bootstrap card sitting
@* Trends panel. A best-effort collapsible Bootstrap card sitting
above the audit query UI: one KpiTrendChart per AuditLog global metric over
a 24h (default) / 7d window. Series are fetched independently in the
code-behind — a failed fetch degrades only that chart to the unavailable
@@ -62,22 +62,22 @@
}
</div>
@* Filter bar (Bundle B / M7-T2). Apply hands the collapsed filter to the grid.
Bundle D (M7-T10..T12) threads a query-string instance prefill through
@* Filter bar. Apply hands the collapsed filter to the grid.
Threads a query-string instance prefill through
InitialInstanceSearch — UI-only because the filter contract has no instance column. *@
<div class="mb-3">
<AuditFilterBar OnFilterChanged="HandleFilterChanged"
InitialInstanceSearch="@_initialInstanceSearch" />
</div>
@* Export button (Bundle F / M7-T14). A plain <a download> link triggers the
@* Export button. A plain <a download> link triggers the
streaming CSV endpoint at /api/centralui/audit/export — chosen over a
SignalR-driven download because the request can stream 100k rows directly
to the response body without buffering through the Blazor circuit. The
href reflects the most recently applied filter; before Apply is clicked,
an unconstrained export is exposed.
Bundle G (#23 M7-T15) gates the button on the AuditExport policy so an
Gates the button on the AuditExport policy so an
OperationalAudit-only operator (read access without bulk export) sees the
page + filters but cannot trigger the CSV pull. The endpoint itself is
gated separately, so a hand-crafted URL still 403s — the AuthorizeView
@@ -96,7 +96,7 @@
</Authorized>
</AuthorizeView>
@* Results grid (Bundle B / M7-T3). Row clicks emit OnRowSelected for Bundle C's
@* Results grid. Row clicks emit OnRowSelected for the
drilldown drawer; the grid stays in "no events" mode until the user applies a
filter so the page does not auto-load the full audit table on first render. *@
<div>
@@ -104,7 +104,7 @@
</div>
</div>
@* Drilldown drawer (Bundle C / M7-T4..T8). Hosted at the page level so the
@* Drilldown drawer. Hosted at the page level so the
off-canvas overlay sits above the grid / filter bar irrespective of scroll. *@
<AuditDrilldownDrawer Event="@_selectedEvent"
IsOpen="@_drawerOpen"
@@ -11,19 +11,19 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit;
/// <summary>
/// Code-behind for the central Audit Log page (#23 M7). Bundle B (M7-T2 + M7-T3)
/// wires up <c>AuditFilterBar</c> and <c>AuditResultsGrid</c>: the page owns the
/// Code-behind for the central Audit Log page. Wires up <c>AuditFilterBar</c>
/// and <c>AuditResultsGrid</c>: the page owns the
/// active <see cref="AuditLogQueryFilter"/> and re-pushes a fresh instance to the
/// grid on every Apply (the grid uses reference identity as its "reload"
/// trigger). Row clicks land in <see cref="HandleRowSelected"/>, which pins
/// the selected row and opens the drilldown drawer.
///
/// <para>
/// Bundle D (M7-T10..T12) adds query-string drill-in parsing so other pages can
/// Adds query-string drill-in parsing so other pages can
/// deep-link to a pre-filtered Audit Log: <c>?correlationId=</c>, <c>?target=</c>,
/// <c>?actor=</c>, <c>?site=</c>, <c>?channel=</c>, <c>?kind=</c>, and the UI-only
/// <c>?instance=</c> are read on initialization. Bundle E (M7-T13) extends
/// this with <c>?status=</c> so the Health-dashboard Audit error-rate tile can
/// <c>?instance=</c> are read on initialization. This extends
/// with <c>?status=</c> so the Health-dashboard Audit error-rate tile can
/// drill in to <c>?status=Failed</c>. The ExecutionId follow-up adds
/// <c>?executionId=</c> for the "View this execution" drill-in, and the
/// ParentExecutionId follow-up adds <c>?parentExecutionId=</c> for the
@@ -51,7 +51,7 @@ public partial class AuditLogPage : IDisposable
[Inject] private NavigationManager Navigation { get; set; } = null!;
/// <summary>
/// KPI-history facade for the M6 (K15) Trends panel — fetched per metric,
/// KPI-history facade for the Trends panel — fetched per metric,
/// per window. Best-effort: a failed series fetch degrades that one chart to
/// the unavailable placeholder and never breaks the audit query UI.
/// </summary>
@@ -187,7 +187,7 @@ public partial class AuditLogPage : IDisposable
IReadOnlyList<AuditKind>? kinds =
AuditQueryParamParsers.ParseEnumList<AuditKind>(Raw(query, "kind"));
// Bundle E (M7-T13): the Health-dashboard Audit error-rate tile drills in
// The Health-dashboard Audit error-rate tile drills in
// with ?status=Failed (and operators may craft URLs with Parked/Discarded).
// Unknown values are silently dropped — the page still renders without
// the constraint.
@@ -248,7 +248,7 @@ public partial class AuditLogPage : IDisposable
private void HandleRowSelected(AuditEventView row)
{
// Bundle C: a grid row click hands us the full AuditEvent. We pin it as
// A grid row click hands us the full AuditEvent. We pin it as
// the selected row and open the drilldown drawer — the drawer is fully
// presentational so we do not need to refetch the row.
_selectedEvent = row;
@@ -263,7 +263,7 @@ public partial class AuditLogPage : IDisposable
}
// ─────────────────────────────────────────────────────────────────────────
// M6 (K15) — Audit Log trend charts.
// Audit Log trend charts.
//
// A best-effort Trends panel that sits above the audit query UI: one
// KpiTrendChart per AuditLog global metric, over a 24h (default) or 7d
@@ -278,8 +278,7 @@ public partial class AuditLogPage : IDisposable
/// <summary>
/// True while a window's series are being (re)fetched — disables the 24h/7d
/// window toggle buttons so a mid-flight click cannot stack overlapping loads
/// (mirrors the K13/K14 trend pages).
/// window toggle buttons so a mid-flight click cannot stack overlapping loads.
/// </summary>
private bool _trendsLoading;
@@ -367,7 +366,7 @@ public partial class AuditLogPage : IDisposable
}
/// <summary>
/// Bundle F (M7-T14): URL the Export-CSV link points at. Renders the most
/// URL the Export-CSV link points at. Renders the most
/// recently applied filter as query-string params so the server-side
/// streaming endpoint reproduces the user's current view. With no filter
/// applied yet, returns the bare endpoint — i.e. an unconstrained export.
@@ -394,7 +393,7 @@ public partial class AuditLogPage : IDisposable
// No capacity hint: the dimensions are multi-value, so the part count is
// unbounded by the number of filter fields.
var parts = new List<KeyValuePair<string, string?>>();
// Task 9: the filter dimensions are multi-value end-to-end. Emit ONE
// The filter dimensions are multi-value end-to-end. Emit ONE
// repeated query-string key per selected value (channel=A&channel=B); the
// export endpoint's ParseFilter reads the full repeated set.
if (filter.Channels is { Count: > 0 } channels)
@@ -13,7 +13,7 @@
<ToastNotification @ref="_toast" />
@* Bundle Import filter chip (T24). Set via ?bundleImportId={guid} query
@* Bundle Import filter chip. Set via ?bundleImportId={guid} query
string so drill-ins from the Import wizard / other pages can scope this
page to a single import run. Cleared via the × button, which navigates
back to the page without the query param so the user sees all rows. *@
@@ -192,7 +192,7 @@
@code {
/// <summary>
/// T24 (Transport). When non-null, scopes the page to a single bundle
/// When non-null, scopes the page to a single bundle
/// import run. Set via the <c>?bundleImportId=</c> query string from
/// drill-ins (Import wizard summary, future BundleImported row links).
/// </summary>
@@ -256,7 +256,7 @@
protected override async Task OnParametersSetAsync()
{
// T24: when the BundleImportId query param is set (or cleared), refetch
// When the BundleImportId query param is set (or cleared), refetch
// automatically so the user lands on a pre-filtered page from a drill-in
// link without having to click Search.
if (BundleImportId != _lastFetchedBundleImportId)
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit;
/// <summary>
/// Code-behind for the execution-chain tree page (Audit Log ParentExecutionId
/// feature, Task 10). Route <c>/audit/execution-tree</c>, reached via the Audit
/// feature). Route <c>/audit/execution-tree</c>, reached via the Audit
/// Log drilldown drawer's "View execution chain" action with
/// <c>?executionId={guid}</c>.
///
@@ -40,7 +40,7 @@ public partial class ExecutionTreePage
private bool _loading;
private string? _error;
// Execution-Tree Node Detail Modal feature (Task 4) — state backing the
// Execution-Tree Node Detail Modal feature — state backing the
// <ExecutionDetailModal>. A double-click on a tree node sets
// _modalExecutionId + flips _modalOpen true; the modal loads that
// execution's audit rows on the closed → open transition. _modalOpen is the
@@ -4,8 +4,8 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
/// <summary>
/// Pure (no Blazor/DI) builder that turns a flat list of streamed attribute (and,
/// in DV-4, alarm) events into a collapsible composition forest of
/// Pure (no Blazor/DI) builder that turns a flat list of streamed attribute (and
/// alarm) events into a collapsible composition forest of
/// <see cref="DebugTreeNode"/>. Path-qualified canonical names are split on
/// <c>'.'</c> to derive branch nodes; the terminal segment carries the payload.
/// </summary>
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; // AlarmState
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
/// <summary>
/// A node in a Debug View composition tree. Attribute (and, in DV-4, alarm)
/// A node in a Debug View composition tree. Attribute (and alarm)
/// names are path-qualified canonical names (e.g. <c>Motor1.Compressor.Pump</c>);
/// the tree is derived by splitting those names on <c>'.'</c>. A node is either a
/// branch (composition member, no payload, has children) or a leaf (one attribute
@@ -22,9 +22,9 @@ public sealed class DebugTreeNode
// Leaf payloads — exactly one is set on a leaf; both null on a pure branch.
public AttributeValueChanged? Attribute { get; init; }
public AlarmStateChanged? Alarm { get; init; } // computed leaf, native condition, or placeholder (DV-4)
public AlarmStateChanged? Alarm { get; init; } // computed leaf, native condition, or placeholder
public bool IsNativeBinding { get; init; } // branch grouping native conditions (DV-4)
public bool IsNativeBinding { get; init; } // branch grouping native conditions
// Roll-up (set by the builder for branch nodes).
public AlarmState WorstState { get; set; } = AlarmState.Normal;
@@ -25,7 +25,7 @@
<div class="d-flex align-items-center mb-3">
<button class="btn btn-outline-secondary btn-sm me-3" @onclick="GoBack">&larr; Back to Topology</button>
<h4 class="mb-0">Configure Instance</h4>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit Log
@* Drill-in: deep-link into the central Audit Log
pre-filtered to this instance. Instance is UI-only on the filter bar
(AuditEvent has no Instance column), so we use the ?instance= UI-text
seam — the filter bar's Instance free-text input is pre-populated. *@
@@ -178,7 +178,7 @@
<div class="card mb-3">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<strong>Attribute Overrides</strong>
@* M7-T16: bulk import of attribute overrides from a CSV
@* Bulk import of attribute overrides from a CSV
(AttributeName,Value[,ElementType]). Selecting a file parses +
validates it against this instance's overridable attributes and —
all-or-nothing — applies every row through the SAME
@@ -598,7 +598,7 @@
// un-parseable List element caught on the pre-submit round-trip).
private Dictionary<string, string> _overrideErrors = new();
// M7-T16: CSV bulk-import result summary. _csvImportResult is the headline
// CSV bulk-import result summary. _csvImportResult is the headline
// ("Imported N overrides." or "Import rejected — N error(s)."); on failure the
// per-line messages are listed from _csvImportErrors. Null until an import runs.
private string? _csvImportResult;
@@ -1047,7 +1047,7 @@
_saving = false;
}
// ── M7-T16: CSV bulk override import ────────────────────
// ── CSV bulk override import ────────────────────
/// <summary>
/// Handles a selected override CSV. Reads the file text (size-capped), parses it
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
/// <summary>
/// Code-behind for <c>InstanceConfigure.razor</c> — hosts the pure, side-effect-free
/// core of the CSV bulk-override import (M7-T16) so it can be unit-pinned without
/// core of the CSV bulk-override import so it can be unit-pinned without
/// standing up the page's ≈7 injected services. The Razor side reads the uploaded
/// file's text, calls <see cref="OverrideCsvParser.Parse"/>, then feeds the result
/// here; on success it applies the returned dict through the SAME
@@ -198,7 +198,7 @@
private IReadOnlyList<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker> _markers
= Array.Empty<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker>();
// Inbound-API key re-arch (C3): the approved-keys list is driven by the IInboundApiKeyAdmin
// Inbound-API key re-arch: the approved-keys list is driven by the IInboundApiKeyAdmin
// seam, not ApiMethod.ApprovedApiKeyIds. The ApiMethod entity itself (name/script/params/etc.)
// still lives on IInboundApiRepository — only the key↔method approval relationship moved to
// per-key method-scopes. Keys are identified by an opaque string KeyId.
@@ -213,7 +213,7 @@
private ToastNotification _toast = default!;
// M9-T25: live per-connection health, keyed by DataConnection.Id. Sourced from
// Live per-connection health, keyed by DataConnection.Id. Sourced from
// the existing site→central health transport via IConnectionHealthQueryService
// and refreshed on a ~10s poll, mirroring the Health dashboard's timer pattern.
// A connection absent from the map renders an "Unknown" badge (tolerates a site
@@ -419,7 +419,7 @@
}
}
// ── M9-T24b / T33b: Move connection to another site ──
// ── Move connection to another site ──
// Opened via IDialogService.ShowAsync. The body dispatches MoveDataConnectionCommand
// through the guard-running ManagementActor path (IDataConnectionMoveService) — NOT
// a direct repository write — so the server enforces the Designer gate and every
@@ -451,7 +451,7 @@
.Select(r => (r.SiteId!.Value, r.Label))
.ToList();
// M9-T25: enum → Bootstrap badge class. Mirrors the Health dashboard's
// Enum → Bootstrap badge class. Mirrors the Health dashboard's
// GetConnectionHealthBadge (Components/Pages/Monitoring/Health.razor) so the
// design page surfaces the same colour coding for the same status. Kept as a
// small local mirror — the Health helper is a private page-local method, not a
@@ -12,7 +12,7 @@
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">@(Id.HasValue ? "Edit External System" : "Add External System")</h4>
@* Bundle D (#23 M7-T12) drill-in: deep-link into the central Audit Log
@* Drill-in: deep-link into the central Audit Log
pre-filtered to this external system's outbound API events. Audit rows
record the target by external-system name, so we filter on Target. *@
@if (Id.HasValue && !string.IsNullOrWhiteSpace(_name))
@@ -6,9 +6,9 @@
@inject ISchemaLibraryService SchemaLibraryService
@inject IDialogService Dialog
@* Schema Library (M9-T32c): list + create/edit (via SchemaBuilder) + delete the
@* Schema Library: list + create/edit (via SchemaBuilder) + delete the
reusable named JSON-Schema library entries that the {"$ref":"lib:Name"} resolver
(T32b) resolves against. Every mutation is dispatched through ISchemaLibraryService
resolves against. Every mutation is dispatched through ISchemaLibraryService
— the guard-running ManagementActor path — never a direct repo write. *@
<div class="container-fluid mt-3">
@@ -64,7 +64,7 @@
private Template? _ownerTemplate;
private TemplateComposition? _ownerComposition;
// M9-T26b: the FULL transitively-resolved inherited member set (multi-level
// M9: the FULL transitively-resolved inherited member set (multi-level
// chain + post-creation base additions) + staleness, fetched read-only via
// GetResolvedTemplateMembersCommand. Populated only for derived templates;
// null when the read failed (editor falls back to the stored-row view).
@@ -228,7 +228,7 @@
}
}
// M9-T26b: resolve the FULL inherited member set (whole chain +
// M9: resolve the FULL inherited member set (whole chain +
// post-creation base additions) read-only. The stored-row tables
// above only carry the IMMEDIATE base; this surfaces the rest with
// origin annotation + a base-changed staleness banner. Read-only:
@@ -314,7 +314,7 @@
</div>
}
@* M9-T26b: the FULL transitively-resolved inherited member set — the whole
@* M9: the FULL transitively-resolved inherited member set — the whole
chain (grandparent + further ancestors) plus base members added after
this template was created, which the immediate-base tables below cannot
show. Read-only preview; each row carries its origin template + lock
@@ -473,7 +473,7 @@
}
};
// ---- M9-T26b: full resolved inherited set (read-only preview) ----
// ---- M9: full resolved inherited set (read-only preview) ----
/// <summary>
/// Renders the FULL transitively-resolved effective member set (attributes,
@@ -425,7 +425,7 @@
}
}
// ---- Sibling reorder (T23b) ----
// ---- Sibling reorder ----
// Dispatches the reorder the same way Move/Rename do — directly through
// TemplateFolderService (the backend swaps SortOrder with the adjacent
// same-parent sibling, no-op at the ends) — then reloads so the new order
@@ -471,7 +471,7 @@
.ToList();
}
// ---- Root-level context menu (T23b) ----
// ---- Root-level context menu ----
// Right-click on the tree zone (empty space or below the tree) offers
// New Folder / New Template at root. Node right-clicks are handled by the
// TreeView's own context menu and never reach here.
@@ -9,7 +9,7 @@
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@*
TransportExport wizard (Component #24, Task T21).
TransportExport wizard (Component #24).
A 4-step linear wizard:
Step 1 — Select : templates (tree, checkbox-mode) + flat artifact lists.
@@ -138,7 +138,7 @@
@RenderCheckboxList(_smtpConfigs, s => s.Id, s => s.Host, _selectedSmtpConfigs)
</fieldset>
@* S10c: SMS provider configs, mirroring the SMTP section above. Labelled by
@* SMS provider configs, mirroring the SMTP section above. Labelled by
AccountSid (the bundle key); the secret AuthToken is never rendered. *@
<fieldset class="mb-4" data-testid="group-sms-configs">
<legend class="h6">SMS Configurations</legend>
@@ -361,12 +361,12 @@
{
<li>SmtpConfig: @s.Host</li>
}
@* S10c: SMS configs in the closure, mirroring SmtpConfig above; AccountSid only, never AuthToken. *@
@* SMS configs in the closure, mirroring SmtpConfig above; AccountSid only, never AuthToken. *@
@foreach (var s in _resolved.SmsConfigs.OrderBy(s => s.AccountSid, StringComparer.OrdinalIgnoreCase))
{
<li>SmsConfig: @s.AccountSid</li>
}
@* Inbound API keys are not transported (re-arch C4) — methods only. *@
@* Inbound API keys are not transported — methods only. *@
@foreach (var m in _resolved.ApiMethods.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase))
{
<li>ApiMethod: @m.Name</li>

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