comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); /** * Betriebs-Alert bei fehlgeschlagenem Scheduler-Task (R5.3): meldet den Task * per Log + Best-Effort-Mail, damit ein stiller Ausfall (z. B. Rechnungslauf, * Kontingent-Reset) nicht unbemerkt bleibt. */ $alertOnFailure = fn (string $task) => function (Event $event) use ($task): void { OperationalAlert::send("Geplanter Task fehlgeschlagen: {$task}", [ 'exit_code' => $event->exitCode, ]); }; // Sperren von withoutOverlapping() (Audit K-09): Ohne Angabe hält Laravel // sie 24 h. Stürzt ein Lauf ab (OOM, Deploy, Container-Neustart), würde der // Task bis zu einen Tag still übersprungen. Jede Sperre endet deshalb kurz // nach der erwarteten Höchstlaufzeit: häufige Tasks nach ihrem Intervall, // nächtliche nach 30–180 Minuten je nach Datenmenge. // ======================================== // Tägliche Bereinigungen // ======================================== // Bounce-Postfach: Rückläufer lesen und unzustellbare Adressen sperren // (Roadmap 1b.6). Alle zehn Minuten auf UNSEEN; ohne konfiguriertes Konto // beendet sich der Befehl sofort. Schedule::command(FetchMailBounces::class) ->everyTenMinutes() ->withoutOverlapping(10) ->runInBackground() ->onFailure($alertOnFailure('mail:fetch-bounces')); // Kontakterreichbarkeit: abgelaufene DNS-Befunde der Mail-Domains erneuern. // Nur Auffrischung — neue Adressen prüft der Observer beim Anlegen. Ein // Befund darf nicht unbegrenzt als Domainsperre stehen bleiben. Schedule::command(CheckMailDomains::class, ['--stale', '--limit=500']) ->dailyAt('04:20') ->withoutOverlapping(60) ->runInBackground() ->onFailure($alertOnFailure('mail:check-domains')); // Bot-Registrierungen löschen (Spamschutz B4, täglich 03:45): unbestätigt, // nie genutzt, älter als 14 Tage. Aus, bis der Probelauf auf Live geprüft // ist (USERS_PRUNE_BOT_REGISTRATIONS=true). Schedule::command(PruneBotRegistrations::class) ->dailyAt('03:45') ->when(fn (): bool => (bool) config('auth.prune_bot_registrations.enabled')) ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('users:prune-bot-registrations')); // Magic-Links: Abgelaufene / verbrauchte Tokens entfernen (täglich 03:00) Schedule::command(PurgeMagicLinks::class, ['--days=30']) ->dailyAt('03:00') ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('magic-links:purge')); // Meldeverfahren: Schwärzungs-Snapshots nach Löschfrist entfernen (täglich 03:10) Schedule::command(PurgeRedactionSnapshots::class) ->dailyAt('03:10') ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('snapshots:purge')); // Beweisarchiv (Bildkennzeichnung Block F.3): fällige Einträge löschen (03:20), // Tombstone-Stand exportieren (03:30), damit er einen Restore überlebt. Schedule::command(ArchivePurge::class) ->dailyAt('03:20') ->withoutOverlapping(120) ->runInBackground() ->onFailure($alertOnFailure('archive:purge')); // Bewusst eigener Termin statt ->then() an archive:purge (OPT-6, K-09): // Als Anhängsel liefe der Export nach einem gescheiterten Purge gar nicht // (onSuccess) bzw. ohne eigenen Failure-Alarm (then). So bleibt der Export // auch dann aktuell und meldet sich selbst. Die Sperren enden nach 2 h bzw. // 1 h; ein Purge, der länger als 10 Minuten läuft, überschneidet sich mit dem // Export — der Export liest nur und nimmt den nächsten Stand am Folgetag mit. Schedule::command(ArchiveExportTombstones::class) ->dailyAt('03:30') ->withoutOverlapping(60) ->runInBackground() ->onFailure($alertOnFailure('archive:export-tombstones')); // Fehlgeschlagene Archiveinträge nachholen (03:35) und Payloads ohne Zeile // nach 24 h Karenz entfernen (03:40) — Umsetzungsaudit 25.09.2026, N11. Schedule::command(ArchiveRetryPending::class) ->dailyAt('03:35') ->withoutOverlapping(60) ->runInBackground() ->onFailure($alertOnFailure('archive:retry-pending')); Schedule::command(ArchiveSweepOrphans::class) ->dailyAt('03:40') ->withoutOverlapping(120) ->runInBackground() ->onFailure($alertOnFailure('archive:sweep-orphans')); // PM-Entwürfe: Zombie-Drafts nach 180 Tagen Inaktivität archivieren (wöchentlich) Schedule::command(PurgeExpiredPressReleaseDrafts::class, ['--days=180']) ->weekly() ->sundays() ->at('04:00') ->withoutOverlapping(180) ->runInBackground() ->onFailure($alertOnFailure('press-releases:purge-drafts')); // Beweisoriginale ohne Bildzeile (Umsetzungsaudit 29.09.2026, B-11): Reste // eines hart abgebrochenen Uploads. 48 h Karenz schützt laufende Uploads; // während eines Restore-Abgleichs (Archiv-Sweep pausiert) löscht er nichts. Schedule::command(SweepOrphanPressReleaseOriginals::class, ['--execute', '--hours=48']) ->weekly() ->sundays() ->at('04:45') ->withoutOverlapping(60) ->runInBackground() ->onFailure($alertOnFailure('press-release-images:sweep-orphan-originals')); // ======================================== // Geplante PM-Veröffentlichung // ======================================== // PM mit scheduled_at <= now & Status review automatisch veröffentlichen. // Läuft alle 5 Min — passt zum FormRule "scheduled_at min. 5 Min in Zukunft". Schedule::command(PublishScheduledPressReleases::class) ->everyFiveMinutes() ->withoutOverlapping(10) ->runInBackground() ->onFailure($alertOnFailure('press-releases:publish-scheduled')); // ======================================== // Aufruf-Zähler // ======================================== // Gezählte Aufrufe aus dem Ereignis-Log in press_releases.hits verbuchen. // Bewusst außerhalb des Requests: der Schreibzugriff auf die große Tabelle // darf keine Web-Worker blockieren (siehe AggregatePressReleaseViews). Schedule::command(AggregatePressReleaseViews::class) ->everyMinute() ->withoutOverlapping(15) ->runInBackground() ->onFailure($alertOnFailure('press-releases:aggregate-views')); // Ereignis-Log auf den Aufbewahrungszeitraum begrenzen (täglich 03:50 — // nicht mehr zeitgleich mit archive:purge um 03:20, Audit K-09). // Die Tabelle nimmt jeden Seitenaufruf auf und wuchs als einzige Log-Tabelle // des Systems unbegrenzt. Gelöscht wird nur unterhalb der Wasserstandsmarke — // noch nicht in `hits` verbuchte Aufrufe bleiben unangetastet. Schedule::command(PurgePressReleaseEvents::class) ->dailyAt('03:50') ->withoutOverlapping(120) ->runInBackground() ->onFailure($alertOnFailure('press-releases:purge-events')); // Indexierungsstatus aus der Search Console (Roadmap 1c.4), täglich 05:10. // Ohne GSC_ENABLED/Schlüsseldatei beendet sich der Befehl sofort. Schedule::command(CheckIndexStatus::class) ->dailyAt('05:10') ->withoutOverlapping(60) ->runInBackground() ->onFailure($alertOnFailure('press-releases:check-index-status')); // Konversions-Ereignisse auf den Aufbewahrungszeitraum begrenzen (täglich 03:25). // Frist zusammen mit der Tabelle festgelegt (395 Tage), siehe config/conversion.php. Schedule::command(PurgeConversionEvents::class) ->dailyAt('03:25') ->withoutOverlapping(60) ->runInBackground() ->onFailure($alertOnFailure('conversion:purge-events')); // ======================================== // Manueller Rechnungskreis (MAN-) — Legacy-Zahlungen // ======================================== // Bestandsschutz mit überschrittenem Legacy-Vertragsende beenden (Audit // F06) — vor dem Rechnungslauf, damit kein beendeter Vertrag mehr // abgerechnet wird. Schedule::command(ExpireGrandfathering::class) ->dailyAt('04:15') ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('billing:expire-grandfathering')); // Fällige Rechnungen für laufende Legacy-Zahlungsvereinbarungen ausstellen // (Periodenende erreicht), wie im Altsystem. Neue Abschlüsse laufen über // Stripe (STR-Kreis) und werden hier nicht angefasst. Schedule::command(GenerateManualInvoices::class) ->dailyAt('04:30') ->withoutOverlapping(60) ->runInBackground() ->onFailure($alertOnFailure('billing:generate-manual-invoices')); // ======================================== // PM-Kontingent (Kalendermonats-Reset des Tarif-Modells) // ======================================== // Monatlicher Reset des verbrauchten PM-Kontingents (am 1. um 00:05). Schedule::command(ResetMonthlyPressReleaseQuota::class) ->monthlyOn(1, '00:05') ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('press-releases:reset-monthly-quota')); // ======================================== // Börsendaten für den Web-Ticker // ======================================== // Kurse (DAX, EUR/USD, Brent) alle 15 Minuten aktualisieren; die Views lesen // nur aus dem Cache — ein API-Ausfall leert die Anzeige nicht sofort. Schedule::command(RefreshMarketData::class) ->everyFifteenMinutes() ->withoutOverlapping(15) ->runInBackground() ->onFailure($alertOnFailure('market:refresh')); // ======================================== // Betriebsüberwachung (Monitoring-Plan Phase 1) // ======================================== // Herzschlag: Fällt der Scheduler komplett aus (Cron weg, Server hängt), // greift kein onFailure mehr — dann meldet /health den fehlenden Herzschlag. Schedule::command(MonitoringHeartbeat::class) ->everyMinute() ->withoutOverlapping(2) ->onFailure($alertOnFailure('monitoring:heartbeat')); // Tageszähler fehlgeschlagener Anmeldungen nach der Löschfrist entfernen // (täglich 04:05, config monitoring.failed_logins.retention_days) und // Spam-Quarantäne des Kontaktformulars nach 30 Tagen (Spamschutz B3). Schedule::command('model:prune', ['--model' => [FailedLoginTally::class, ContactMessage::class]]) ->dailyAt('04:05') ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('model:prune failed_login_tallies, contact_messages')); // Fehlgeschlagene Jobs nach 30 Tagen entfernen (täglich 03:55). Bis dahin // sind sie im Systemstatus sichtbar und per `queue:retry` wiederholbar. Schedule::command('queue:prune-failed', ['--hours=720']) ->dailyAt('03:55') ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('queue:prune-failed')); // Abgelaufene API-Tokens (optionales Ablaufdatum, 08.10.2026) 30 Tage nach // Ablauf entfernen (täglich 04:10). Bis dahin zeigt die Token-Seite sie als // „abgelaufen“ an; Sanctum lehnt sie ab dem Ablauf ohnehin ab. Schedule::command('sanctum:prune-expired', ['--hours=720']) ->dailyAt('04:10') ->withoutOverlapping(30) ->runInBackground() ->onFailure($alertOnFailure('sanctum:prune-expired'));