From 50d05790623da0e0c0beb397f8c33d0127253e60 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 23:28:12 +0000 Subject: [PATCH] fix: guard against no-phase programs in timer screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: when a program exists but has no phases yet, `Program::totalDuration()` returns 0 (programTotalDuration = 0) and `TimerRunner::start()` throws RuntimeException("Program has no phases."). Livewire silently rolls back the response, so the UI never transitions out of idle — the Start button appears to do nothing. Changes: - `TimerScreen::start()`: return early when phases are empty; avoids the unhandled exception and leaves the idle state intact so the user can still navigate to the editor. - `timer-screen.blade.php`: replace the Start button with an amber warning notice ("No phases — edit this program…") when `$phases` is empty and state is idle, so the user immediately understands why the timer cannot start. - `ProgramEditor::totalDuration()`: add an empty-phases guard (returns 0) and subtract the last phase's cooldown, matching `Program::totalDuration()` which already skips it (the timer goes straight to COMPLETED after the final rep, never entering the last cooldown). https://claude.ai/code/session_01GWCpmWmmUb8R1EE23kC29k --- app/Livewire/ProgramEditor.php | 9 +++++++- app/Livewire/TimerScreen.php | 4 ++++ .../views/livewire/timer-screen.blade.php | 21 ++++++++++++------- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/Livewire/ProgramEditor.php b/app/Livewire/ProgramEditor.php index 76559d8..0690d91 100644 --- a/app/Livewire/ProgramEditor.php +++ b/app/Livewire/ProgramEditor.php @@ -217,7 +217,11 @@ public function formattedDuration(): string public function totalDuration(): int { - return array_reduce( + if (empty($this->phases)) { + return 0; + } + + $total = array_reduce( $this->phases, static function (int $carry, array $p): int { $repTime = $p['duration'] * $p['repetitions']; @@ -226,6 +230,9 @@ static function (int $carry, array $p): int { }, 0, ); + + // The last phase's cooldown is never executed (timer goes straight to COMPLETED). + return $total - (int) ($this->phases[array_key_last($this->phases)]['cooldown'] ?? 0); } /** diff --git a/app/Livewire/TimerScreen.php b/app/Livewire/TimerScreen.php index abb7958..3ee5e5f 100644 --- a/app/Livewire/TimerScreen.php +++ b/app/Livewire/TimerScreen.php @@ -126,6 +126,10 @@ public function start(): void $runner = app(TimerRunner::class); $program = Program::with('phases')->findOrFail($this->programId); + if ($program->phases->isEmpty()) { + return; + } + $this->programTotalDuration = $program->totalDuration(); $runner->load($program); $runner->start(); diff --git a/resources/views/livewire/timer-screen.blade.php b/resources/views/livewire/timer-screen.blade.php index b405b1f..87cb252 100644 --- a/resources/views/livewire/timer-screen.blade.php +++ b/resources/views/livewire/timer-screen.blade.php @@ -259,13 +259,20 @@ class="text-gray-800 text-xs mt-2 select-none" {{-- Primary action --}} @if($state === StateMachine::idle) - + @if(count($phases) === 0) +
+ No phases — edit this program to add at least one phase before starting. +
+ @else + + @endif @elseif($state === StateMachine::prepare) {{-- No primary action during prepare — user just waits --}}