authorize('provisioning.retry'); // Atomically claim the run: only the request that transitions it out of // FAILED proceeds, so two concurrent retries can't double-dispatch. $claimed = ProvisioningRun::query() ->where('uuid', $uuid) ->where('status', ProvisioningRun::STATUS_FAILED) ->update([ 'status' => ProvisioningRun::STATUS_RUNNING, 'attempt' => 0, 'next_attempt_at' => now(), 'started_at' => now(), // reset the step timer so it doesn't re-time-out instantly 'error' => null, ]); if ($claimed === 0) { return; // already retried by a concurrent request } $run = ProvisioningRun::query()->where('uuid', $uuid)->first(); if ($run === null) { return; } // Move the subject out of its error state so the console reflects the retry. $subject = $run->subject; if ($subject instanceof Host) { $subject->update(['status' => 'onboarding']); } elseif ($subject instanceof Order) { $subject->update(['status' => 'provisioning']); Instance::query()->where('order_id', $subject->id)->where('status', 'failed')->update(['status' => 'provisioning']); } AdvanceRunJob::dispatch($run->uuid); $this->dispatch('notify', message: __('admin.run_retried')); } private function subjectLabel(ProvisioningRun $run): string { $subject = $run->subject; if ($subject instanceof Order) { return $subject->customer?->name ?? 'Order'; } if ($subject instanceof Host) { return $subject->name; } return class_basename($run->subject_type); } public function render() { $runs = ProvisioningRun::query()->latest('id')->limit(30)->get(); $active = $runs->first(fn (ProvisioningRun $r) => in_array( $r->status, ['pending', 'running', 'waiting'], true )) ?? $runs->first(); return view('livewire.admin.provisioning', [ 'hasActive' => $active !== null && in_array($active->status, ['pending', 'running', 'waiting'], true), 'rows' => $runs->map(fn (ProvisioningRun $r) => [ 'uuid' => $r->uuid, 'customer' => $this->subjectLabel($r), 'pipeline' => $r->pipeline, 'step' => $r->status === 'completed' ? '—' : $this->currentStepLabel($r), 'n' => ($r->current_step + 1).'/'.count(config('provisioning.pipelines.'.$r->pipeline, [])), 'attempt' => $r->attempt, 'state' => $this->runState($r), 'failed' => $r->status === ProvisioningRun::STATUS_FAILED, ])->all(), 'panel' => $active ? $this->panelFor($active) : null, ]); } /** Compact "current run" readout for the right column. */ private function panelFor(ProvisioningRun $run): array { $pipeline = config('provisioning.pipelines.'.$run->pipeline, []); $total = max(count($pipeline), 1); $current = min($run->current_step, $total - 1); $completed = $run->status === ProvisioningRun::STATUS_COMPLETED; $done = $completed ? $total : $current; return [ 'uuid' => $run->uuid, 'subject' => $this->subjectLabel($run), 'pipeline' => $run->pipeline, 'attempt' => $run->attempt, 'status' => $this->runState($run), // running|done|failed 'failed' => $run->status === ProvisioningRun::STATUS_FAILED, 'current' => $completed ? null : (isset($pipeline[$current]) ? __(app($pipeline[$current])->label()) : null), 'next' => isset($pipeline[$current + 1]) && ! $completed ? __(app($pipeline[$current + 1])->label()) : null, 'error' => $run->error, 'done' => $done, 'total' => $total, 'percent' => (int) round($done / $total * 100), ]; } }