scanJobRepository = $scanJobRepository; $this->networkScanner = $networkScanner; } /** * Run a single pending scan job. */ public function runNext(): ?ScanJob { $pending = $this->scanJobRepository->findPending(); if (empty($pending)) { return null; } $job = $pending[0]; $this->execute($job); return $job; } /** * Run all pending scan jobs in a loop. * * @param callable|null $onProgress Called after each job: fn(ScanJob $job) */ public function runAll(?callable $onProgress = null): int { $count = 0; while (true) { $job = $this->runNext(); if ($job === null) { break; } $count++; if ($onProgress !== null) { $onProgress($job); } } return $count; } public function execute(ScanJob $job): void { $job->status = 'running'; $job->startedAt = new \DateTimeImmutable(); $this->scanJobRepository->save($job); try { switch ($job->type) { case 'network_discovery': $this->runNetworkDiscovery($job); break; default: throw new \RuntimeException("Unknown scan job type: {$job->type}"); } $job->status = 'done'; $job->finishedAt = new \DateTimeImmutable(); } catch (\Throwable $e) { $job->status = 'failed'; $job->errorMessage = $e->getMessage(); $job->finishedAt = new \DateTimeImmutable(); } $this->scanJobRepository->save($job); } private function runNetworkDiscovery(ScanJob $job): void { // For MVP, we scan all enabled network ranges if no specific range set // TODO: filter by specific range when network_range_id is set $settings = require dirname(__DIR__, 3) . '/../phinx.php'; // Use the ranges from database via NetworkRangeRepository // For now, this is handled by the controller that creates the job with a specific range } }