subMinutes($thresholdMinutes); $machines = Machine::query() ->whereNotIn('status', ['offline', 'maintenance']) ->where(function ($query) use ($cutoff) { $query->whereNull('last_heartbeat_at') ->orWhere('last_heartbeat_at', '<', $cutoff); }) ->get(); foreach ($machines as $machine) { DB::transaction(function () use ($machine) { $locked = Machine::query()->whereKey($machine->id)->lockForUpdate()->firstOrFail(); if (in_array($locked->status, ['offline', 'maintenance', 'running'], true)) { return; } $thresholdMinutes = (int) config('laverie.machine.offline_threshold_minutes', 10); $cutoff = now()->subMinutes($thresholdMinutes); if ($locked->last_heartbeat_at !== null && $locked->last_heartbeat_at->gte($cutoff)) { return; } $previousStatus = $locked->status; $provider = config('laverie.simulation.provider_name', 'simulated'); $locked->update(['status' => 'offline']); MachineStatusHistory::query()->create([ 'machine_id' => $locked->id, 'previous_status' => $previousStatus, 'new_status' => 'offline', 'source' => 'RefreshOfflineMachines', 'reason' => 'heartbeat_timeout', 'created_at' => now(), ]); MachineEvent::query()->create([ 'machine_id' => $locked->id, 'provider' => $provider, 'event_type' => 'machine_offline', 'payload' => ['reason' => 'heartbeat_timeout'], 'occurred_at' => now(), 'received_at' => now(), 'processed_at' => now(), 'processing_status' => 'processed', ]); }); } } }