Rename the user-facing product name from ScadaLink to ScadaBridge across
the six display strings (browser title, sidebar brand, login + not-authorized
headings, dashboard welcome/subtitle). Namespaces, assemblies, config keys,
and _content/ScadaLink.CentralUI asset routes are unchanged.
Apply the technical-light design system: vendor theme.css + IBM Plex fonts
into the CentralUI RCL, include theme.css globally (after Bootstrap so its
--bs-* token overrides win), and restyle the layout chrome to a light
sidebar — white surface, hairline rules, ink text, accent-blue active item,
the brand accent mark. Page markup stays Bootstrap and inherits the warm
paper background, Plex type, accent, and hairline borders via the tokens.
Tests: build 0 warnings; bUnit 542 passed; Playwright 64 passed.
The drilldown drawer's 'View this/parent execution' actions call
NavigationManager.NavigateTo('/audit/log?executionId=...') while the
user is already on the routed AuditLogPage. Blazor treats this as a
same-component navigation, so OnInitialized does not re-run and
ApplyQueryStringFilters() (which was wired only to OnInitialized) never
re-parsed the new query string: _currentFilter stayed stale and the
results grid never reloaded to the drill-in target.
AuditLogPage now subscribes to NavigationManager.LocationChanged,
re-applies the query-string filters on every location change (closing
the drawer and calling StateHasChanged), and unsubscribes via
IDisposable. The 'View parent execution' drill-in now genuinely lands
on /audit/log?executionId={parentId} with the grid reloaded.
Also corrects the Playwright test wait: a same-page query-string Blazor
navigation pushes history.pushState over the SignalR circuit rather
than triggering a document load, so WaitForLoadState(NetworkIdle)
returned before the URL settled. Switched to WaitForURLAsync, the
correct primitive for SPA/pushState navigations.
The headline ParentExecutionIdCorrelationTests intermittently failed under
full-suite parallel load, seeing 6 of 7 routed-run rows (NotifySend missing).
Root cause: WaitForSiteRowsPersistedAsync checked only a row *count*, which a
partial snapshot could satisfy before the last-emitted NotifySend row settled,
letting the SiteAuditTelemetryActor drain a partial batch. Fix is test-only:
wait on the specific audit Kinds (guaranteeing NotifySend is durably in SQLite
before the assertion) and widen the assertion ceiling 30s -> 90s for drain
headroom under load. Also drops leftover // DIAG sampler debug scaffolding.
The store-and-forward retry loop emits the per-attempt and terminal cached
audit rows (ApiCallCached/DbWriteCached Attempted, CachedResolve) via
CachedCallLifecycleBridge from a CachedCallAttemptContext, not from the
script context. The ExecutionId rollout (Task 4) already threaded ExecutionId
and SourceScript through this path; ParentExecutionId — the spawning
inbound-API request's ExecutionId — was not, so those retry-loop rows had
ParentExecutionId = null even for an inbound-API-routed run.
Thread it additively as a sibling at every carry point ExecutionId passes
through:
- StoreAndForwardMessage gains ParentExecutionId (Guid?).
- StoreAndForwardStorage adds a nullable parent_execution_id column via the
same idempotent PRAGMA-probed ALTER TABLE migration; rows persisted by an
older build read back null (back-compat). The defensive Guid.TryParse read
helper (ParseExecutionId) is renamed ParseGuidColumn and reused for both
columns so a corrupt value cannot abort the retry sweep.
- StoreAndForwardService.EnqueueAsync gains an optional parentExecutionId
param, stamped onto the buffered message and surfaced on the
CachedCallAttemptContext built in the retry loop.
- CachedCallAttemptContext gains ParentExecutionId.
- CachedCallLifecycleBridge.BuildPacket sets AuditEvent.ParentExecutionId
from the context, beside the existing ExecutionId.
- IExternalSystemClient.CachedCallAsync / IDatabaseGateway.CachedWriteAsync
gain an optional parentExecutionId param; ScriptRuntimeContext's CachedCall
/ CachedWrite helpers pass _parentExecutionId.
All threading is additive — ParentExecutionId is Guid? everywhere, null for
non-routed runs, and old buffered S&F rows still deserialize with the new
field null.
The store-and-forward retry loop emits the per-attempt and terminal cached
audit rows (ApiCallCached/DbWriteCached Attempted, CachedResolve) via
CachedCallLifecycleBridge from a CachedCallAttemptContext, not from the
script context. ExecutionId (and SourceScript) were not threaded through the
S&F buffer, so those rows had ExecutionId = null and SourceScript = null.
Thread both, additively, from the cached-call enqueue path:
- StoreAndForwardMessage gains ExecutionId (Guid?) / SourceScript (string?).
- StoreAndForwardStorage adds nullable execution_id / source_script columns
via an idempotent PRAGMA-probed ALTER TABLE migration; rows persisted by
an older build read back null (back-compat).
- StoreAndForwardService.EnqueueAsync gains optional executionId /
sourceScript params, stamped onto the buffered message and surfaced on the
CachedCallAttemptContext built in the retry loop.
- CachedCallAttemptContext gains ExecutionId / SourceScript.
- CachedCallLifecycleBridge.BuildPacket sets AuditEvent.ExecutionId and
AuditEvent.SourceScript from the context (replacing the hard-coded
SourceScript = null and its now-stale comment).
- IExternalSystemClient.CachedCallAsync / IDatabaseGateway.CachedWriteAsync
gain optional executionId / sourceScript params; ScriptRuntimeContext's
CachedCall / CachedWrite helpers pass _executionId / _sourceScript.
Script-side cached rows (CachedSubmit, immediate Attempted+Resolve) are
unchanged. All threading is additive — old buffered S&F rows still
deserialize and process with the new fields null.
Move the per-script-execution Guid on ScriptRuntimeContext from
_auditCorrelationId to _executionId, and stamp it into the dedicated
AuditEvent.ExecutionId column on every script-side audit row:
- Sync ApiCall / DbWrite: ExecutionId set; CorrelationId reverts to
null (a sync one-shot call has no operation lifecycle).
- Cached-call script-side rows (CachedSubmit, immediate-completion
ApiCallCached + CachedResolve) and NotifySend: ExecutionId set;
CorrelationId unchanged (per-operation TrackedOperationId /
NotificationId).
Renames the threaded ctor param/field across ExternalSystemHelper,
DatabaseHelper, AuditingDbConnection and AuditingDbCommand, and threads
the id through NotifyHelper/NotifyTarget. The S&F retry-loop cached rows
(CachedCallLifecycleBridge) are out of scope here.
The outbound ApiCall emitter hard-coded RequestSummary/ResponseSummary to null,
so audited API calls carried no inputs/outputs — contrary to the Audit Log
payload-capture spec. Thread the call arguments into the sync ApiCall emitter
and the cached immediate-completion path (CachedSubmit / ApiCallCached /
CachedResolve), and stamp the response body from ExternalCallResult.ResponseJson.
The writer's payload filter still applies the size cap + redaction downstream.
The S&F retry-loop cached rows are unchanged — request data is not threaded
through the store-and-forward buffer (same boundary as SourceScript).
Channel narrows the Kind options to the chosen channel, so filtering by more
than one channel at a time is incoherent. Replace the Channel multi-select
dropdown with a native single-select (matching the Time range control); Kind,
Status and Site stay multi-select. The query filter contract is unchanged —
Channels just carries 0 or 1 value.
Stamp the audit Actor column on outbound rows (calling script identity) and
central-dispatch rows (system identity); the original emission code left it
null on every channel except Inbound API.
Per the Audit Log Actor-column spec, Actor should carry the calling script
identity on outbound rows (ApiCall, DbWrite, NotifySend) and a system identity
on central-dispatch rows (NotifyDeliver). The original emission code hard-coded
Actor=null at all four sites, so only Inbound API rows (API key name) ever
filled it. Stamp the script identity and 'system' respectively.
Replace the four stacked chip-button groups (Channel, Kind, Status, Site) on
the Audit Log filter bar with a reusable MultiSelectDropdown component, so the
bar collapses from four full-width chip blocks to four inline dropdowns sharing
one wrapped filter row. Bootstrap dropdown + checkbox menu (data-bs-auto-close
=outside); no third-party UI libraries.
Three Playwright E2E failures, all test-side timing/data bugs (no
feature defects found):
- AuditGridColumnTests.ColumnOrderAndWidths_PersistAcrossReload: read
sessionStorage synchronously right after Mouse.UpAsync, racing the
async OnColumnResized/OnColumnReordered JS->.NET->JS save round-trip.
Now polls (WaitForFunctionAsync) for the storage keys and for the
reorder re-render to settle; also hardens the flaky ReorderDrag test.
- SiteCallsPageTests.FilterNarrowing_ChannelFilterShrinksGrid: the
Target-keyword #sc-search @bind committed via the Query click's own
blur, racing change vs click on the circuit so Search() sometimes
ran with a stale empty filter. Commit the value with an explicit,
fully-awaited DispatchEventAsync('change') and use the retrying
ToHaveCount assertion for the negative row checks.
- SiteCallsPageTests.RetryClickThrough_OnParkedRow: seeded SourceSite
'plant-a' is not a real cluster site (site-a/b/c), so the relay had
no ClusterClient route and only resolved on the 10s inner Ask
timeout - past the 5s toast wait. Seed a live site (site-a) for a
fast NotParked round-trip and give the toast a 15s wait.
Playwright E2E suite: 60 passed, 0 failed, 0 skipped.
Adds drag-to-resize and drag-to-reorder column UX to AuditResultsGrid,
with chosen widths + column order persisted in browser sessionStorage.
- wwwroot/js/audit-grid.js: dependency-free helper — pointer-driven
resize handles, native HTML5 drag-and-drop reorder, and a
sessionStorage save/load wrapper (mirrors treeview-storage.js).
- AuditResultsGrid: renders a resize handle per <th>, makes headers
draggable, applies persisted widths via a --audit-col-width custom
property, and wires reorder into the existing ColumnOrder /
OrderedColumns() mechanism. JS-invokable OnColumnResized /
OnColumnReordered persist + re-render. A stored order naming an
unknown column degrades gracefully (drops unknown keys, appends
missing columns in default order); widths clamp to a 64px minimum.
- AuditResultsGrid.razor.css: subtle scoped styling for the resize
handle affordance and the reorder drop-target highlight.
- App.razor references audit-grid.js alongside the other scripts.
- Tests: 6 new bUnit tests for the load/apply/persist logic and
graceful degradation; a new AuditGridColumnTests Playwright suite
for the drag UX + reload persistence. Audit page bUnit tests set
loose JSInterop mode since the grid now calls into audit-grid.js.