$pass ? 'pass' : 'fail', 'critical' => $critical, 'value' => $value]; }; $configured = (string) config('serp.dataforseo.login') !== '' && (string) config('serp.dataforseo.password') !== ''; $gate('environment', app()->environment('production'), app()->environment()); $gate('debug', ! config('app.debug'), config('app.debug') ? 'enabled' : 'disabled'); $gate('provider', config('serp.provider') === 'dataforseo', config('serp.provider')); $state = $activation->state(); $consistent = (! config('serp.enabled') && in_array($state->value, ['disabled', 'validation_only', 'canary'], true)) || (config('serp.enabled') && in_array($state->value, ['canary', 'enabled', 'emergency_stopped'], true)); $gate('activation_consistency', $consistent, ['enabled' => (bool) config('serp.enabled'), 'mode' => $state->value]); $gate('credentials', $configured, $configured ? 'configured' : 'not_configured'); try { $local = $health->validateConfig(); $gate('provider_security', true, ['host' => $local['host'], 'tls' => $local['tls']]); } catch (Throwable) { $gate('provider_security', false, 'invalid'); }$pending = $this->pendingMigrations(); $gate('migrations', $pending === 0, ['pending' => $pending]); try { $lock = Cache::lock('serp:preflight-lock', 5); $acquired = $lock->get(); if ($acquired) { $lock->release(); }$gate('cache_lock', $acquired, config('cache.default')); } catch (Throwable) { $gate('cache_lock', false, 'unavailable'); }$gate('queue', in_array(config('queue.default'), ['redis', 'database'], true), ['connection' => config('queue.default'), 'serp_queue' => config('serp.queue')]); $console = file_get_contents(base_path('routes/console.php')); $gate('scheduler', str_contains($console, 'serp:dispatch-due') && str_contains($console, 'serp:reconcile'), ['defined' => true]); $stateRow = SerpOperationState::find(1); $gate('kill_switch', ! $stateRow?->emergency_stopped_at, $stateRow?->emergency_stopped_at ? 'active' : 'clear'); $gate('circuit', ! $circuit->open(), $circuit->open() ? 'open' : 'closed'); foreach (['daily_budget_usd', 'monthly_budget_usd', 'low_balance_usd', 'critical_balance_usd'] as $key) { try { $gate($key, Money::micros((string) config('serp.'.$key)) > 0, (string) config('serp.'.$key)); } catch (Throwable) { $gate($key, false, 'invalid'); } }$gate('depth', config('serp.dataforseo.result_depth') === 10, config('serp.dataforseo.result_depth')); $gate('provider_modes', config('serp.manual_mode') === 'live' && config('serp.automatic_mode') === 'standard', ['manual' => config('serp.manual_mode'), 'automatic' => config('serp.automatic_mode')]); $summary = ['status' => collect($gates)->contains(fn ($g) => $g['critical'] && $g['status'] === 'fail') ? 'failed' : 'passed', 'remote' => 'not_requested', 'gates' => $gates, 'operations' => ['last_health_at' => $stateRow?->last_health_at, 'last_provider_success_at' => $stateRow?->last_provider_success_at, 'stuck' => SerpCheck::whereIn('status', ['pending', 'running'])->where('updated_at', '<', now()->subMinutes((int) config('serp.stuck_after_minutes')))->count(), 'pending' => SerpCheck::where('status', 'pending')->count(), 'running' => SerpCheck::where('status', 'running')->count()]]; $this->option('json') ? $this->line(json_encode($summary, JSON_THROW_ON_ERROR)) : $this->line('Preflight: '.$summary['status']); return $summary['status'] === 'passed' ? self::SUCCESS : self::FAILURE; } private function pendingMigrations(): int { if (! Schema::hasTable('migrations')) { return count(glob(database_path('migrations/*.php'))); }$ran = DB::table('migrations')->pluck('migration')->all(); return count(array_filter(glob(database_path('migrations/*.php')), fn ($f) => ! in_array(pathinfo($f, PATHINFO_FILENAME), $ran, true))); } }