option('dry-run'); if (! $dryRun && ! $client->isConfigured()) { $this->warn('Search Console nicht konfiguriert (GSC_ENABLED, GSC_SERVICE_ACCOUNT_FILE) — nichts zu tun.'); return self::SUCCESS; } $budget = max(1, (int) ($this->option('budget') ?: config('search_console.daily_budget', 300))); $portals = $checker->portals(); if ($this->option('portal')) { $wanted = Portal::tryFrom((string) $this->option('portal')); $portals = array_values(array_filter($portals, fn (Portal $portal): bool => $portal === $wanted)); } if ($portals === []) { $this->warn('Kein Portal mit eingetragener Property.'); return self::SUCCESS; } $checked = 0; $failed = 0; $aborted = false; $deadline = now()->addMinutes((int) config('search_console.max_runtime_minutes', 40)); $maxConsecutive = max(1, (int) config('search_console.max_consecutive_failures', 5)); foreach ($portals as $portal) { $candidates = $checker->candidates($portal, $budget); $this->info(sprintf('%s: %d fällige Meldungen (Kontingent %d).', $portal->label(), $candidates->count(), $budget)); $consecutiveFailures = 0; foreach ($candidates as $release) { if ($dryRun) { $this->line(sprintf(' [%d] %s (veröffentlicht %s)', $release->id, $release->slug, $release->published_at?->format('d.m.Y') ?? '-')); continue; } // Zeitbudget unter der Overlap-Sperre: was übrig bleibt, ist // morgen weiter fällig. if (now()->greaterThanOrEqualTo($deadline)) { $this->warn('Zeitbudget erreicht — restliche Meldungen beim nächsten Lauf.'); Log::warning('Search Console: Zeitbudget des Laufs erreicht.', ['checked' => $checked, 'failed' => $failed]); break 2; } try { $check = $checker->check($release, $portal); $checked++; $consecutiveFailures = 0; $this->line(sprintf(' [%d] %s → %s', $release->id, $release->slug, $check->state->label())); } catch (SearchConsoleUnavailable $e) { // Google hängt oder hat einen eigenen Fehler: kein // Anwendungsfehler, nur Warnung — die Meldung bleibt fällig. $failed++; $consecutiveFailures++; Log::warning('Search Console vorübergehend nicht verfügbar.', ['release_id' => $release->id, 'error' => $e->getMessage()]); $this->warn(sprintf(' [%d] %s: %s', $release->id, $release->slug, $e->getMessage())); } catch (\Throwable $e) { // Auth, Rechte, Quote, kaputte Antwort: echter Fehler. $failed++; $consecutiveFailures++; report($e); $this->error(sprintf(' [%d] %s: %s', $release->id, $release->slug, $e->getMessage())); } if ($consecutiveFailures >= $maxConsecutive) { $aborted = true; $this->error(sprintf('%s: Abbruch nach %d Fehlern in Folge.', $portal->label(), $consecutiveFailures)); continue 2; } } } if ($dryRun) { return self::SUCCESS; } $attempted = $checked + $failed; $this->info(sprintf('%d Prüfungen gespeichert, %d fehlgeschlagen.', $checked, $failed)); // Fehlschlag (→ einmaliger Betriebs-Alert über den Scheduler) nur, // wenn ein Portal abbrechen musste oder mindestens die Hälfte // scheiterte; einzelne Aussetzer erledigen sich am nächsten Tag. if ($aborted || ($attempted >= 10 && $failed / $attempted >= 0.5)) { return self::FAILURE; } return self::SUCCESS; } }