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:
@@ -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"/> >= 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><redacted></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 & Trends" (K8) — the
|
||||
/// "KPI History & 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><redacted: redactor error></c> marker. Bundle C bridges this into
|
||||
/// <c><redacted: redactor error></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);
|
||||
|
||||
+7
-8
@@ -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"/> >= <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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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><AuthorizeView></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 & 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)
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+4
-4
@@ -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">← 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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user