SSR alert mail recipients maintainable in the site settings

New site setting vitec.ssr.alertEmails (Site Management, Settings, VITEC; one address per entry). vitec:ssr-health reads the recipients from there, invalid entries are skipped and logged, SSR_ALERT_EMAIL in the env file stays as fallback. The cockpit shows the recipients in the live band. Pre-filled with the address that was in the env file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Opus 5.5 committed 2026-11-11 11:13:37 +01:00
1 parent 994f9f2a1b
commit 90cb2faaf9
6 files changed
+81 -6

No files matched your search

@@ -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('<comment>skipped invalid alert addresses: ' . implode(', ', $alert['invalid']) . '</comment>');
}
$recipients = $alert['recipients'];
if ($recipients === []) {
$output->writeln('<comment>transition detected but SSR_ALERT_EMAIL is not configured - no mail sent</comment>');
$output->writeln('<comment>transition detected but no alert recipients are configured (Site settings › VITEC or SSR_ALERT_EMAIL) - no mail sent</comment>');
return;
}
try {
@@ -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'),
@@ -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<string>, source: string, invalid: list<string>}
*/
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