diff --git a/app/Actions/Automation/Automation/GetAutomationMetrics.php b/app/Actions/Automation/Automation/GetAutomationMetrics.php index 229c5aa4..422c0ba9 100644 --- a/app/Actions/Automation/Automation/GetAutomationMetrics.php +++ b/app/Actions/Automation/Automation/GetAutomationMetrics.php @@ -43,8 +43,8 @@ public function __invoke(Automation $automation, CarbonInterface $start, CarbonI $successRate = $finished > 0 ? (int) round($completed->count() / $finished * 100) : null; $durations = $completed - ->filter(fn ($run) => $run->started_at !== null && $run->finished_at !== null) - ->map(fn ($run) => $run->started_at->diffInMilliseconds($run->finished_at)); + ->map(fn ($run) => $run->durationInMilliseconds()) + ->filter(fn ($ms) => $ms !== null); $avgDurationMs = $durations->isNotEmpty() ? (int) round($durations->avg()) : null; return [ diff --git a/app/Http/Controllers/App/AutomationController.php b/app/Http/Controllers/App/AutomationController.php index d4721fb0..cfd6b898 100644 --- a/app/Http/Controllers/App/AutomationController.php +++ b/app/Http/Controllers/App/AutomationController.php @@ -33,6 +33,7 @@ use App\Models\AutomationRun; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Response as HttpResponse; use Inertia\Inertia; use Inertia\Response; @@ -183,7 +184,7 @@ public function retryRun( RetryRunFromNode $retry, Automation $automation, AutomationRun $run, - ): \Illuminate\Http\Response { + ): HttpResponse { $this->authorize('update', $automation); abort_unless($run->automation_id === $automation->id, 404); diff --git a/app/Http/Resources/AutomationInvocationResource.php b/app/Http/Resources/AutomationInvocationResource.php index 0c70a6ed..503ae62e 100644 --- a/app/Http/Resources/AutomationInvocationResource.php +++ b/app/Http/Resources/AutomationInvocationResource.php @@ -18,16 +18,12 @@ class AutomationInvocationResource extends JsonResource */ public function toArray(Request $request): array { - $durationMs = $this->started_at !== null && $this->finished_at !== null - ? $this->started_at->diffInMilliseconds($this->finished_at) - : null; - return [ 'id' => $this->id, 'status' => $this->status->value, 'is_manual' => (bool) $this->is_manual, 'node_run_count' => (int) ($this->node_runs_count ?? 0), - 'duration_ms' => $durationMs, + 'duration_ms' => $this->durationInMilliseconds(), 'error_message' => is_array($this->error) ? ($this->error['message'] ?? null) : $this->error, 'created_at' => $this->created_at, 'started_at' => $this->started_at, diff --git a/app/Models/AutomationRun.php b/app/Models/AutomationRun.php index 876630a3..c8777254 100644 --- a/app/Models/AutomationRun.php +++ b/app/Models/AutomationRun.php @@ -48,6 +48,19 @@ public function rootId(): string return $this->root_run_id ?? $this->id; } + /** + * Wall-clock execution time, or null while the run hasn't both started and + * finished. Single source of truth for the Invocations list and metrics. + */ + public function durationInMilliseconds(): ?int + { + if ($this->started_at === null || $this->finished_at === null) { + return null; + } + + return (int) $this->started_at->diffInMilliseconds($this->finished_at); + } + /** * Context for template (`{{ ... }}`) resolution: the run context plus the * automation's workflow variables, merged in-memory. Variables are NEVER diff --git a/database/migrations/2026_05_22_211640_create_automations_table.php b/database/migrations/2026_05_22_211640_create_automations_table.php index c7de2ea6..a26f6bb8 100644 --- a/database/migrations/2026_05_22_211640_create_automations_table.php +++ b/database/migrations/2026_05_22_211640_create_automations_table.php @@ -22,6 +22,7 @@ public function up(): void $table->string('trigger_type')->nullable(); $table->json('nodes')->nullable(); $table->json('connections')->nullable(); + $table->json('variables')->nullable(); $table->timestamp('activated_at')->nullable(); $table->timestamp('paused_at')->nullable(); $table->timestamps(); diff --git a/database/migrations/2026_05_22_211642_create_automation_runs_table.php b/database/migrations/2026_05_22_211642_create_automation_runs_table.php index 70c65da7..df74603c 100644 --- a/database/migrations/2026_05_22_211642_create_automation_runs_table.php +++ b/database/migrations/2026_05_22_211642_create_automation_runs_table.php @@ -16,6 +16,7 @@ public function up(): void Schema::create('automation_runs', function (Blueprint $table) { $table->uuid('id')->primary(); $table->foreignUuid('automation_id')->constrained('automations')->cascadeOnDelete(); + $table->foreignUuid('root_run_id')->nullable(); $table->foreignUuid('trigger_item_id')->nullable()->constrained('automation_trigger_items')->nullOnDelete(); $table->string('current_node_id')->nullable(); $table->string('status')->default('pending'); @@ -32,6 +33,10 @@ public function up(): void $table->index(['automation_id', 'status']); $table->index(['status', 'next_action_at']); }); + + Schema::table('automation_runs', function (Blueprint $table) { + $table->foreign('root_run_id')->references('id')->on('automation_runs')->nullOnDelete(); + }); } /** diff --git a/database/migrations/2026_06_11_181832_add_variables_to_automations_table.php b/database/migrations/2026_06_11_181832_add_variables_to_automations_table.php deleted file mode 100644 index cd9f748b..00000000 --- a/database/migrations/2026_06_11_181832_add_variables_to_automations_table.php +++ /dev/null @@ -1,28 +0,0 @@ -json('variables')->nullable()->after('connections'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('automations', function (Blueprint $table) { - $table->dropColumn('variables'); - }); - } -}; diff --git a/database/migrations/2026_06_11_213339_add_root_run_id_to_automation_runs_table.php b/database/migrations/2026_06_11_213339_add_root_run_id_to_automation_runs_table.php deleted file mode 100644 index 4e6701d8..00000000 --- a/database/migrations/2026_06_11_213339_add_root_run_id_to_automation_runs_table.php +++ /dev/null @@ -1,28 +0,0 @@ -foreignUuid('root_run_id')->nullable()->after('automation_id') - ->constrained('automation_runs')->nullOnDelete(); - }); - } - - public function down(): void - { - Schema::table('automation_runs', function (Blueprint $table) { - $table->dropConstrainedForeignId('root_run_id'); - }); - } -}; diff --git a/tests/Feature/Automation/AutomationModelTest.php b/tests/Feature/Automation/AutomationModelTest.php index 4a83c620..494b24ed 100644 --- a/tests/Feature/Automation/AutomationModelTest.php +++ b/tests/Feature/Automation/AutomationModelTest.php @@ -38,6 +38,19 @@ expect($automation->trigger_type)->toBeNull(); }); +it('computes run duration only once both timestamps are set', function () { + $start = now()->startOfSecond(); + + $running = AutomationRun::factory()->make(['started_at' => $start, 'finished_at' => null]); + $finished = AutomationRun::factory()->make([ + 'started_at' => $start, + 'finished_at' => $start->copy()->addSeconds(2), + ]); + + expect($running->durationInMilliseconds())->toBeNull() + ->and($finished->durationInMilliseconds())->toBe(2000); +}); + it('relates trigger items, runs and node runs', function () { $automation = Automation::factory()->create(); $item = AutomationTriggerItem::factory()->for($automation)->create();