feat(usage): open the TokenTracker dashboard from the usage monitor

The modal deliberately shows only a slice of the analytics; this hands off
to TokenTracker's own local dashboard for the rest. The worker starts
`tokentracker serve` on demand and returns the URL, the UI opens the browser.

Three things the spawn has to get right: port 7680 is not free on Windows
(Delivery Optimization holds [::]:7680) and serve does not fall back, so we
scan 7680-7689 with a dual-stack bind probe; --no-open because the CLI would
open the browser before the server answers; and the child is a cmd.exe shim,
so shutdown kills the process tree. --no-sync keeps our no-cloud-sync rule.
This commit is contained in:
mika kuns
2026-08-24 16:51:00 +02:00
parent 2dec590dfc
commit 3003cfa561
19 changed files with 413 additions and 4 deletions
+22
View File
@@ -184,6 +184,8 @@ public record TaskUsageRowDto(
bool? Productive = null,
bool? OneShot = null);
public record TokenTrackerDashboardDto(bool Ok, string? Url, string? Error);
public record TokenTrackerStatusDto(
bool Installed,
string? Version,
@@ -1233,6 +1235,26 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
return BuildTokenTrackerStatus(await _tokenTracker.ProbeAsync(ct: Context.ConnectionAborted));
});
/// <summary>
/// Brings up TokenTracker's own local dashboard (<c>tokentracker serve</c>) and hands the URL
/// back — the UI opens the browser, since that is a user-session concern. The server is a
/// child of this worker and dies with it. Never throws: a failure is a message the caller
/// flashes in the footer.
/// </summary>
public Task<TokenTrackerDashboardDto> OpenTokenTrackerDashboard() => HubGuard(async () =>
{
if (_tokenTracker is null)
return new TokenTrackerDashboardDto(false, null, "TokenTracker is not configured on this worker.");
var dashboard = await _tokenTracker.OpenDashboardAsync(Context.ConnectionAborted);
if (!dashboard.Ok)
await _broadcaster.WorkerLog(
dashboard.Error ?? "TokenTracker dashboard failed to start.",
WorkerLogLevel.Error, DateTime.UtcNow);
return new TokenTrackerDashboardDto(dashboard.Ok, dashboard.Url, dashboard.Error);
});
private TokenTrackerStatusDto BuildTokenTrackerStatus(TokenTrackerProbe probe)
{
var state = _tokenTracker!.State;