From be99f413f701102529c3f3646b6ed2d96f33d92d Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 18:11:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20provisioning=20liveness=20?= =?UTF-8?q?=E2=80=94=20per-run=20progress=20bar,=20last-activity,=20stale?= =?UTF-8?q?=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- app/Livewire/Admin/Provisioning.php | 39 ++++++++++++++----- lang/de/admin.php | 4 ++ lang/en/admin.php | 4 ++ .../livewire/admin/provisioning.blade.php | 32 +++++++++++++-- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/app/Livewire/Admin/Provisioning.php b/app/Livewire/Admin/Provisioning.php index 72725b4..20abbeb 100644 --- a/app/Livewire/Admin/Provisioning.php +++ b/app/Livewire/Admin/Provisioning.php @@ -60,6 +60,14 @@ class Provisioning extends Component $this->dispatch('notify', message: __('admin.run_retried')); } + /** A run that claims to be in progress but hasn't advanced in a while looks stuck. */ + private function isStale(ProvisioningRun $run): bool + { + return in_array($run->status, ['running', 'waiting'], true) + && $run->updated_at !== null + && $run->updated_at->lt(now()->subMinutes(2)); + } + private function subjectLabel(ProvisioningRun $run): string { $subject = $run->subject; @@ -84,16 +92,24 @@ class Provisioning extends Component 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(), + 'rows' => $runs->map(function (ProvisioningRun $r) { + $total = max(count(config('provisioning.pipelines.'.$r->pipeline, [])), 1); + $done = $r->status === ProvisioningRun::STATUS_COMPLETED ? $total : $r->current_step; + + return [ + 'uuid' => $r->uuid, + 'customer' => $this->subjectLabel($r), + 'pipeline' => $r->pipeline, + 'step' => $r->status === 'completed' ? '—' : $this->currentStepLabel($r), + 'n' => ($r->current_step + 1).'/'.$total, + 'percent' => (int) round($done / $total * 100), + 'attempt' => $r->attempt, + 'state' => $this->runState($r), + 'failed' => $r->status === ProvisioningRun::STATUS_FAILED, + 'activity' => $r->updated_at?->diffForHumans() ?? '—', + 'stale' => $this->isStale($r), + ]; + })->all(), 'panel' => $active ? $this->panelFor($active) : null, ]); } @@ -120,6 +136,9 @@ class Provisioning extends Component 'done' => $done, 'total' => $total, 'percent' => (int) round($done / $total * 100), + 'activity' => $run->updated_at?->diffForHumans() ?? '—', + 'started' => $run->started_at?->diffForHumans() ?? null, + 'stale' => $this->isStale($run), ]; } } diff --git a/lang/de/admin.php b/lang/de/admin.php index 2dbc668..b709732 100644 --- a/lang/de/admin.php +++ b/lang/de/admin.php @@ -95,6 +95,10 @@ return [ 'retry' => 'Erneut versuchen', 'run_retried' => 'Lauf wird erneut gestartet.', + 'run_started' => 'Gestartet', + 'run_activity' => 'Letzte Aktivität', + 'run_stale_hint' => 'Seit über 2 Minuten kein Fortschritt — der Lauf hängt möglicherweise (in dieser Umgebung fehlt echte Infrastruktur).', + 'state_stale' => 'Keine Aktivität', 'state_running' => 'Läuft', 'state_done' => 'Fertig', 'state_failed' => 'Fehlgeschlagen', diff --git a/lang/en/admin.php b/lang/en/admin.php index acdf864..e0d5cca 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -95,6 +95,10 @@ return [ 'retry' => 'Retry', 'run_retried' => 'Run is being retried.', + 'run_started' => 'Started', + 'run_activity' => 'Last activity', + 'run_stale_hint' => 'No progress for over 2 minutes — the run may be stuck (this environment has no real infrastructure).', + 'state_stale' => 'No activity', 'state_running' => 'Running', 'state_done' => 'Done', 'state_failed' => 'Failed', diff --git a/resources/views/livewire/admin/provisioning.blade.php b/resources/views/livewire/admin/provisioning.blade.php index 135a6c8..ef2ba1e 100644 --- a/resources/views/livewire/admin/provisioning.blade.php +++ b/resources/views/livewire/admin/provisioning.blade.php @@ -26,10 +26,22 @@ {{ $r['customer'] }} {{ $r['pipeline'] }} - {{ $r['step'] }} - {{ $r['n'] }} + +
+ {{ $r['step'] }} + {{ $r['n'] }} +
+
+
+
+
+ {{ $r['activity'] }} +
+ {{ $r['attempt'] }} - {{ __('admin.state_'.$r['state']) }} + + {{ $r['stale'] ? __('admin.state_stale') : __('admin.state_'.$r['state']) }} + @if ($r['failed'])