fix(ui): carry SourceNode on SiteCallDetail + NotificationDetail records

The Site Calls and Notifications detail modals were reading SourceNode from
the summary record (d.SourceNode) while every other field read from the
detail record (det.X). The pattern works today because the modal always
opens via a row click that pre-loads the summary, but a future drill-in
from a deep link or refresh path could leave the summary stale or null and
the field would render blank or wrong.

Add SourceNode to both detail records, project it through the actor's
ToDetail mapping, and switch the razor markup to read det.SourceNode. Now
the modal binds uniformly to the detail record across all fields.
This commit is contained in:
Joseph Doherty
2026-05-23 18:37:53 -04:00
parent 8bf84fb7f3
commit c754666a3d
8 changed files with 20 additions and 7 deletions

View File

@@ -266,7 +266,7 @@
<dd class="col-sm-9">@SiteName(d.SourceSiteId)</dd>
<dt class="col-sm-3">Source node</dt>
<dd class="col-sm-9">@(string.IsNullOrEmpty(d.SourceNode) ? "—" : d.SourceNode)</dd>
<dd class="col-sm-9">@(string.IsNullOrEmpty(_detail?.SourceNode) ? "—" : _detail.SourceNode)</dd>
<dt class="col-sm-3">Source instance</dt>
<dd class="col-sm-9">@(string.IsNullOrEmpty(d.SourceInstanceId) ? "—" : d.SourceInstanceId)</dd>

View File

@@ -267,7 +267,7 @@
<dd class="col-sm-9">@SiteName(det.SourceSite)</dd>
<dt class="col-sm-3">Source node</dt>
<dd class="col-sm-9">@(string.IsNullOrEmpty(d.SourceNode) ? "—" : d.SourceNode)</dd>
<dd class="col-sm-9">@(string.IsNullOrEmpty(det.SourceNode) ? "—" : det.SourceNode)</dd>
<dt class="col-sm-3">Channel</dt>
<dd class="col-sm-9">@det.Channel</dd>

View File

@@ -119,7 +119,8 @@ public sealed record SiteCallDetail(
DateTime CreatedAtUtc,
DateTime UpdatedAtUtc,
DateTime? TerminalAtUtc,
DateTime IngestedAtUtc);
DateTime IngestedAtUtc,
string? SourceNode = null);
/// <summary>
/// Site Calls UI -> Central: request for the global <c>SiteCalls</c> KPI summary.

View File

@@ -119,7 +119,8 @@ public record NotificationDetail(
DateTimeOffset CreatedAt,
DateTimeOffset? LastAttemptAt,
DateTimeOffset? NextAttemptAt,
DateTimeOffset? DeliveredAt);
DateTimeOffset? DeliveredAt,
string? SourceNode = null);
/// <summary>
/// Outbox UI -> Central: request for the notification outbox KPI summary.

View File

@@ -749,7 +749,8 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
notification.CreatedAt,
notification.LastAttemptAt,
notification.NextAttemptAt,
notification.DeliveredAt);
notification.DeliveredAt,
notification.SourceNode);
return new NotificationDetailResponse(
request.CorrelationId, Success: true, ErrorMessage: null, detail);

View File

@@ -652,7 +652,8 @@ public class SiteCallAuditActor : ReceiveActor
CreatedAtUtc: row.CreatedAtUtc,
UpdatedAtUtc: row.UpdatedAtUtc,
TerminalAtUtc: row.TerminalAtUtc,
IngestedAtUtc: row.IngestedAtUtc);
IngestedAtUtc: row.IngestedAtUtc,
SourceNode: row.SourceNode);
}
/// <summary>

View File

@@ -354,6 +354,7 @@ public class NotificationOutboxActorQueryTests : TestKit
row.ResolvedTargets = "[\"ops@example.com\",\"oncall@example.com\"]";
row.TypeData = "{\"priority\":\"high\"}";
row.SourceScript = "HighLevelAlarm.csx";
row.SourceNode = "node-a";
row.SiteEnqueuedAt = DateTimeOffset.UtcNow.AddMinutes(-5);
row.DeliveredAt = DateTimeOffset.UtcNow;
_repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
@@ -377,6 +378,10 @@ public class NotificationOutboxActorQueryTests : TestKit
Assert.Equal("instance-42", detail.SourceInstanceId);
Assert.Equal(2, detail.RetryCount);
Assert.Equal("transient blip", detail.LastError);
// SourceNode flows through the detail projection so the report detail
// modal binds uniformly to the detail record (was previously read off
// the summary).
Assert.Equal("node-a", detail.SourceNode);
}
[Fact]

View File

@@ -402,7 +402,8 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
var repo = new SiteCallAuditRepository(context);
var actor = CreateActor(repo);
await repo.UpsertAsync(NewRow(id, siteId, status: "Attempted", retryCount: 2, lastError: "503"));
await repo.UpsertAsync(NewRow(
id, siteId, status: "Attempted", retryCount: 2, lastError: "503", sourceNode: "node-a"));
actor.Tell(new SiteCallDetailRequest("corr-d1", id.Value), TestActor);
@@ -414,6 +415,9 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
Assert.Equal(2, response.Detail.RetryCount);
Assert.Equal("503", response.Detail.LastError);
Assert.Equal(siteId, response.Detail.SourceSite);
// SourceNode flows through ToDetail so the report detail modal binds
// uniformly to the detail record (was previously read off the summary).
Assert.Equal("node-a", response.Detail.SourceNode);
}
[SkippableFact]