diff --git a/config/sites/vitec/settings.yaml b/config/sites/vitec/settings.yaml index b17c035..51b6c61 100644 --- a/config/sites/vitec/settings.yaml +++ b/config/sites/vitec/settings.yaml @@ -23,3 +23,5 @@ vitec.ssr.enabled: true # Shell-Seiten (Produkt-/News-/Story-Detail-Container) nicht in die XML-Sitemap # aufnehmen; 106 = alter /en/news-Ast (noindex-Duplikate). SEO-Audit 2026-11-09. seo.sitemap.pages.excludePagesRecursive: '10,21,26,106' +vitec.ssr.alertEmails: + - info@evomedien.de diff --git a/packages/vitec/Classes/Command/SsrHealthSampleCommand.php b/packages/vitec/Classes/Command/SsrHealthSampleCommand.php index 84e6b75..2b8a59b 100644 --- a/packages/vitec/Classes/Command/SsrHealthSampleCommand.php +++ b/packages/vitec/Classes/Command/SsrHealthSampleCommand.php @@ -206,15 +206,20 @@ final class SsrHealthSampleCommand extends Command /** * Mails are best effort: a mail problem must never break the sampler - * (the cron would stop logging history). Recipients come from - * SSR_ALERT_EMAIL in var/vitec-ssr-purge.env - unset means "alerts - * off", which the output states so the cron log shows why. + * (the cron would stop logging history). Recipients come from the site + * setting vitec.ssr.alertEmails (Site Management › Settings › VITEC), + * fallback SSR_ALERT_EMAIL in var/vitec-ssr-purge.env - none at all + * means "alerts off", which the output states so the cron log shows why. */ private function sendAlert(string $subject, string $body, OutputInterface $output): void { - $recipients = GeneralUtility::trimExplode(',', $this->purgeService->getAlertEmail(), true); + $alert = $this->purgeService->getAlertRecipients(); + if ($alert['invalid'] !== []) { + $output->writeln('skipped invalid alert addresses: ' . implode(', ', $alert['invalid']) . ''); + } + $recipients = $alert['recipients']; if ($recipients === []) { - $output->writeln('transition detected but SSR_ALERT_EMAIL is not configured - no mail sent'); + $output->writeln('transition detected but no alert recipients are configured (Site settings › VITEC or SSR_ALERT_EMAIL) - no mail sent'); return; } try { diff --git a/packages/vitec/Classes/Controller/Backend/SsrController.php b/packages/vitec/Classes/Controller/Backend/SsrController.php index 78378fd..5e11f46 100644 --- a/packages/vitec/Classes/Controller/Backend/SsrController.php +++ b/packages/vitec/Classes/Controller/Backend/SsrController.php @@ -70,6 +70,7 @@ final class SsrController $lastSample = $healthLog[0] ?? null; $clientErrors = $this->clientErrors(50); $cockpit = $this->cockpitData($health, $lastSample); + $alert = $this->purgeService->getAlertRecipients(); $settingsUri = ''; if ($GLOBALS['BE_USER']->isAdmin()) { @@ -92,6 +93,10 @@ final class SsrController 'cockpitJson' => json_encode($cockpit, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), 'settingsUri' => $settingsUri, 'failoverAfter' => $this->purgeService->getFailoverAfter(), + 'alert' => $alert, + 'alertList' => implode(', ', $alert['recipients']), + 'alertCount' => count($alert['recipients']), + 'alertInvalid' => implode(', ', $alert['invalid']), 'chunkCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => $r['is_chunk_reload'])), 'jsErrorCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => !$r['is_chunk_reload'])), 'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'), diff --git a/packages/vitec/Classes/Service/SsrPurgeService.php b/packages/vitec/Classes/Service/SsrPurgeService.php index 9e30f46..3e72292 100644 --- a/packages/vitec/Classes/Service/SsrPurgeService.php +++ b/packages/vitec/Classes/Service/SsrPurgeService.php @@ -7,6 +7,8 @@ namespace Evomedien\Vitec\Service; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Http\RequestFactory; +use TYPO3\CMS\Core\Site\SiteFinder; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Talks to the SSR node's POST /__purge endpoint (contract 0054-FE) and @@ -24,6 +26,7 @@ final class SsrPurgeService public function __construct( private readonly RequestFactory $requestFactory, private readonly ConnectionPool $connectionPool, + private readonly SiteFinder $siteFinder, ) {} public static function getConfigPath(): string @@ -66,6 +69,50 @@ final class SsrPurgeService return trim($this->getConfig()['SSR_ALERT_EMAIL'] ?? ''); } + /** + * Who gets the vitec:ssr-health alert mails. Maintained by editors in + * Site Management › Settings › VITEC (vitec.ssr.alertEmails, one + * address per entry); the first site with at least one valid address + * wins. Without such a list, SSR_ALERT_EMAIL from the env file is the + * fallback. Invalid entries are skipped and reported back. + * + * @return array{recipients: list, source: string, invalid: list} + */ + public function getAlertRecipients(): array + { + $invalid = []; + try { + foreach ($this->siteFinder->getAllSites() as $site) { + $list = $site->getSettings()->get('vitec.ssr.alertEmails', []); + if (is_string($list)) { + $list = GeneralUtility::trimExplode(',', $list, true); + } + $valid = []; + foreach ((array)$list as $address) { + $address = trim((string)$address); + if ($address === '') { + continue; + } + if (GeneralUtility::validEmail($address)) { + $valid[] = $address; + } else { + $invalid[] = $address; + } + } + if ($valid !== []) { + return ['recipients' => array_values(array_unique($valid)), 'source' => 'site settings', 'invalid' => $invalid]; + } + } + } catch (\Throwable) { + // No site configuration readable (e.g. broken YAML): use the env fallback. + } + $env = array_values(array_filter( + GeneralUtility::trimExplode(',', $this->getAlertEmail(), true), + static fn(string $a): bool => GeneralUtility::validEmail($a) + )); + return ['recipients' => $env, 'source' => $env !== [] ? 'var/vitec-ssr-purge.env' : '', 'invalid' => $invalid]; + } + /** * Consecutive DOWN samples after which vitec:ssr-health fails over to * the SPA (SSR_FAILOVER_AFTER, default 2 = ~10 minutes at the 5-minute diff --git a/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml b/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml index d818f98..ee0f04d 100644 --- a/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml +++ b/packages/vitec/Configuration/Sets/Vitecset/settings.definitions.yaml @@ -134,3 +134,10 @@ settings: category: VITEC type: bool default: true + + vitec.ssr.alertEmails: + label: 'SSR alert mail recipients' + description: 'E-mail addresses that get the alert mails of the SSR health check: node down, node back, automatic fallback to the SPA. One address per entry; invalid addresses are skipped. Empty: the addresses from SSR_ALERT_EMAIL in var/vitec-ssr-purge.env are used.' + category: VITEC + type: stringlist + default: [] diff --git a/packages/vitec/Resources/Private/Templates/Ssr/Index.html b/packages/vitec/Resources/Private/Templates/Ssr/Index.html index eebc096..da51150 100644 --- a/packages/vitec/Resources/Private/Templates/Ssr/Index.html +++ b/packages/vitec/Resources/Private/Templates/Ssr/Index.html @@ -14,7 +14,7 @@
- Live state of the SSR node on the VPS. The dot pulses green while the node answers its health check. Delivery shows whether visitors get server-rendered pages; the switch is in the Site settings. The strip shows the last 24 hours in 15-minute windows: green ok, yellow slower than 400 ms, red down, grey no sample. After {failoverAfter} failed checks in a row the cron switches back to the SPA and sends an alert mail. + Live state of the SSR node on the VPS. The dot pulses green while the node answers its health check. Delivery shows whether visitors get server-rendered pages; the switch is in the Site settings. The strip shows the last 24 hours in 15-minute windows: green ok, yellow slower than 400 ms, red down, grey no sample. After {failoverAfter} failed checks in a row the cron switches back to the SPA. Alert mails go to the addresses under Site settings › VITEC › SSR alert mail recipients; hover “Alert mail” to see them.
@@ -67,6 +67,15 @@
+
+
Alert mail
+
+ + {alertCount} recipients + none + +
+
Auto failover
after {failoverAfter} failed checks