Dashboard: admin-only Close session / Kill worker

Add IDashboardSessionAdminService (Admin-role gate, friendly errors,
audit logging) wrapping a new ISessionManager.KillWorkerAsync that
skips graceful shutdown and cleans up registry/metrics. Sessions,
Workers, and SessionDetails pages render Close / Kill buttons only
when CanManage; the service re-checks the role on every call so
forged clicks return Unauthenticated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-24 07:10:32 -04:00
parent 8a0c59d7e8
commit c5e7479ee4
15 changed files with 750 additions and 1 deletions
@@ -1,5 +1,7 @@
@page "/workers"
@inherits DashboardPageBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IDashboardSessionAdminService SessionAdminService
<PageTitle>Dashboard Workers</PageTitle>
@@ -16,6 +18,13 @@ else
</div>
</div>
@if (CanManage && !string.IsNullOrWhiteSpace(ResultMessage))
{
<div class="alert @(LastOperationSucceeded ? "alert-success" : "alert-danger")" role="alert">
@ResultMessage
</div>
}
<section class="dashboard-section">
@if (Snapshot.Workers.Count == 0)
{
@@ -32,6 +41,10 @@ else
<th scope="col">Session</th>
<th scope="col">Heartbeat</th>
<th scope="col">Fault</th>
@if (CanManage)
{
<th scope="col">Actions</th>
}
</tr>
</thead>
<tbody>
@@ -43,6 +56,16 @@ else
<td><NavLink href="@($"sessions/{Uri.EscapeDataString(worker.SessionId)}")"><code>@worker.SessionId</code></NavLink></td>
<td>@DashboardDisplay.DateTime(worker.LastHeartbeatAt)</td>
<td>@DashboardDisplay.Text(worker.LastFault)</td>
@if (CanManage)
{
<td>
<button type="button" class="btn btn-sm btn-outline-danger"
disabled="@IsBusy"
@onclick="() => KillWorkerAsync(worker.SessionId)">
Kill
</button>
</td>
}
</tr>
}
</tbody>
@@ -51,3 +74,47 @@ else
}
</section>
}
@code {
private bool CanManage { get; set; }
private bool IsBusy { get; set; }
private string? ResultMessage { get; set; }
private bool LastOperationSucceeded { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync().ConfigureAwait(false);
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
.ConfigureAwait(false);
CanManage = SessionAdminService.CanManage(authenticationState.User);
}
private async Task KillWorkerAsync(string sessionId)
{
if (IsBusy)
{
return;
}
IsBusy = true;
try
{
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
.ConfigureAwait(false);
CanManage = SessionAdminService.CanManage(authenticationState.User);
DashboardSessionAdminResult result = await SessionAdminService
.KillWorkerAsync(authenticationState.User, sessionId, CancellationToken.None)
.ConfigureAwait(false);
ResultMessage = result.Message;
LastOperationSucceeded = result.Succeeded;
}
finally
{
IsBusy = false;
}
}
}