feat(central-ui): Site Calls page accepts ?correlationId= and opens the operation's detail

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 11:04:39 -04:00
parent 8abc0eca13
commit 33f9c97030
4 changed files with 209 additions and 3 deletions
@@ -90,6 +90,20 @@
<label class="form-check-label small" for="sc-stuck-only">Stuck only</label>
</div>
</div>
@if (_operationIdFilter is { } operationId)
{
@* Read-only pill for the ?correlationId= deep link (Flow Debugger /
Audit Log). Its × drops just this filter and re-queries; the
Clear button drops it along with everything else. *@
<div class="col-auto">
<span class="badge text-bg-secondary" data-test="site-calls-filter-operation"
title="@operationId">Operation @ShortId(operationId)</span>
<button type="button" class="btn btn-link btn-sm p-0 ms-1 align-baseline text-decoration-none"
aria-label="Clear operation filter" title="Clear operation filter"
data-test="site-calls-filter-operation-clear"
@onclick="ClearOperationFilter" disabled="@_loading">&times;</button>
</div>
}
<div class="col-auto">
<button class="btn btn-outline-secondary btn-sm" @onclick="ClearFilters"
disabled="@(!HasActiveFilters)">Clear</button>
@@ -102,6 +102,11 @@ public partial class SiteCallsReport
private DateTime? _fromFilter;
private DateTime? _toFilter;
// Exact-match on one operation, seeded only from the ?correlationId= deep
// link (Flow Debugger / Audit Log). Rendered as a read-only pill rather than
// an editable control — there is no sensible way to type one in.
private Guid? _operationIdFilter;
// ── Trends ──────────────────────────────────────────────────────────────
// Collapsible KPI-history charts for the SiteCallAudit / Global series. The
// section is collapsed on init and the series load lazily on first expand
@@ -162,14 +167,26 @@ public partial class SiteCallsReport
ApplyQueryStringFilters();
await RefreshAll();
// The ?correlationId= deep link lands on the operation itself: when the
// filtered first page is exactly that one row, open its detail so the
// operator is not left to click the row they navigated for.
if (_operationIdFilter is { } op
&& _siteCalls is { Count: 1 }
&& _siteCalls[0].TrackedOperationId == op)
{
await ShowDetail(_siteCalls[0]);
}
}
/// <summary>
/// Pre-apply the Health-dashboard KPI-tile drill-in filters from the URL query
/// string. <c>?status=&lt;status&gt;</c> seeds <see cref="_statusFilter"/> when it
/// matches a known status (case-insensitive); <c>?stuck=true</c> seeds
/// <see cref="_stuckOnly"/>. Lax parsing — an absent, blank, or unrecognised value
/// is silently dropped, leaving the filter empty (the no-param behaviour).
/// <see cref="_stuckOnly"/>; <c>?correlationId=&lt;guid&gt;</c> seeds
/// <see cref="_operationIdFilter"/> (the Flow Debugger / Audit Log deep link onto
/// one operation). Lax parsing — an absent, blank, or unrecognised value is
/// silently dropped, leaving the filter empty (the no-param behaviour).
/// </summary>
private void ApplyQueryStringFilters()
{
@@ -200,6 +217,12 @@ public partial class SiteCallsReport
{
_stuckOnly = stuck;
}
if (query.TryGetValue("correlationId", out var correlationValues)
&& Guid.TryParse(correlationValues.ToString(), out var operationId))
{
_operationIdFilter = operationId;
}
}
/// <summary>Re-fetch the current page (Refresh button, and after a relay action).</summary>
@@ -262,7 +285,8 @@ public partial class SiteCallsReport
AfterCreatedAtUtc: cursor.AfterCreatedAtUtc,
AfterId: cursor.AfterId,
PageSize: PageSize,
SourceNodeFilter: NullIfEmpty(_nodeFilter));
SourceNodeFilter: NullIfEmpty(_nodeFilter),
TrackedOperationIdFilter: _operationIdFilter);
var response = await CommunicationService.QuerySiteCallsAsync(request);
if (response.Success)
@@ -476,9 +500,18 @@ public partial class SiteCallsReport
_stuckOnly = false;
_fromFilter = null;
_toFilter = null;
_operationIdFilter = null;
}
/// <summary>The Operation pill's own × — drop the deep-link filter and re-query.</summary>
private async Task ClearOperationFilter()
{
_operationIdFilter = null;
await Search();
}
private bool HasActiveFilters =>
_operationIdFilter != null ||
!string.IsNullOrEmpty(_statusFilter) ||
!string.IsNullOrEmpty(_channelFilter) ||
!string.IsNullOrEmpty(_siteFilter) ||