feat(worker): surface startup recovery via OperationProgress channel
The six Lifecycle/*Recovery hosted services now broadcast one
OperationProgress("startup-recovery", <phase>, current, total) message each
after they finish, instead of leaving the UI on a bare "connecting" text
during worker startup. IslandsShellViewModel subscribes and swaps in
"Recovering... (i/n)" (existing ops.worker.startupRecovery key, no locale
changes) while Worker.IsReconnecting is true, and clears it once actually
connected so a later transient reconnect doesn't replay stale text.
OperationProgress broadcasts to Clients.All with no replay-on-connect, so a
UI that hasn't finished its SignalR handshake yet can miss some or all of
these messages and simply keep showing "connecting" as before -- accepted
rather than adding a cached-state + reconnect-replay path (mirroring
RefreshExternalMergeConflictsAsync) for what is a fast, best-effort,
local-only startup sweep with no UI-visible failure mode beyond that.
This commit is contained in:
@@ -20,14 +20,24 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
public ListsIslandViewModel? Lists { get; }
|
||||
public TasksIslandViewModel? Tasks { get; }
|
||||
public DetailsIslandViewModel? Details { get; }
|
||||
public IWorkerClient? Worker { get; }
|
||||
public IWorkerClient? Worker { get; internal set; }
|
||||
public MissionControlViewModel? MissionControl { get; }
|
||||
public UsagePillViewModel? UsagePill { get; }
|
||||
public UpdateCheckService UpdateCheck => _updateCheck;
|
||||
|
||||
// Stable opKey the six Lifecycle/*Recovery startup sweeps broadcast under (see
|
||||
// OperationProgressOpKeys.StartupRecovery on the worker side — not shared across the
|
||||
// SignalR wire, so the literal is duplicated here on purpose).
|
||||
private const string StartupRecoveryOpKey = "startup-recovery";
|
||||
|
||||
// Last startup-recovery text received this connection cycle, shown in place of the generic
|
||||
// "connecting" label. Cleared once actually connected so a later transient reconnect (not a
|
||||
// fresh worker boot) doesn't replay stale recovery text.
|
||||
private string? _startupRecoveryText;
|
||||
|
||||
public string ConnectionText =>
|
||||
Worker?.IsConnected == true ? Loc.T("vm.connection.online")
|
||||
: Worker?.IsReconnecting == true ? Loc.T("vm.connection.connecting")
|
||||
: Worker?.IsReconnecting == true ? (_startupRecoveryText ?? Loc.T("vm.connection.connecting"))
|
||||
: Loc.T("vm.connection.offline");
|
||||
|
||||
public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true;
|
||||
@@ -228,6 +238,18 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
public void OnPlanningMergeCompleted(string planningTaskId) => ClearExternalMergeConflict(planningTaskId);
|
||||
|
||||
// Wired to Worker.OperationProgressEvent; also called directly by tests. A foreign opKey
|
||||
// (merge phases, worktree cleanup, ...) is left untouched — this only tracks the
|
||||
// startup-recovery channel.
|
||||
public void OnOperationProgress(string opKey, string phase, int current, int total)
|
||||
{
|
||||
if (opKey != StartupRecoveryOpKey) return;
|
||||
_startupRecoveryText = total > 0
|
||||
? $"{Loc.T("ops.worker.startupRecovery")} ({current}/{total})"
|
||||
: Loc.T("ops.worker.startupRecovery");
|
||||
OnPropertyChanged(nameof(ConnectionText));
|
||||
}
|
||||
|
||||
private void ClearExternalMergeConflict(string planningTaskId)
|
||||
{
|
||||
_externalMergeConflicts.Remove(planningTaskId);
|
||||
@@ -358,11 +380,13 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
if (e.PropertyName is nameof(IWorkerClient.IsConnected) or nameof(IWorkerClient.IsReconnecting))
|
||||
{
|
||||
if (Worker.IsConnected) _startupRecoveryText = null;
|
||||
OnPropertyChanged(nameof(ConnectionText));
|
||||
OnPropertyChanged(nameof(IsOffline));
|
||||
OnPropertyChanged(nameof(CanOpenWorkerConnectionHelp));
|
||||
}
|
||||
};
|
||||
Worker.OperationProgressEvent += OnOperationProgress;
|
||||
Worker.WorkerLogReceivedEvent += OnWorkerLogReceived;
|
||||
Worker.ConnectionRestoredEvent += () => _ = RefreshStaleWorkerCheckAsync();
|
||||
Worker.PlanningMergeConflictEvent += OnPlanningMergeConflict;
|
||||
@@ -410,6 +434,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Worker is not null) Worker.OperationProgressEvent -= OnOperationProgress;
|
||||
_clearTimer.Stop();
|
||||
_clearTimer.Dispose();
|
||||
_connectTimer.Stop();
|
||||
|
||||
Reference in New Issue
Block a user