SSR control center: auto-purge webhook, dashboard tiles, backend module

Package 1: SsrPurgeService posts to the node /__purge (endpoint+secret in var/vitec-ssr-purge.env, outside git) and logs every attempt to tx_vitec_ssrpurgelog. DataHandler hooks queue per request and flush once: pages with nav-relevant field changes and all tx_news_/tx_vitec_ records purge everything, other page/content edits purge the hosting page path. Gated by the routing flag and the new Site Set switch vitec.ssr.autoPurge. Flipping the SSR switch ON purges everything once. CLI: vitec:ssr-purge, vitec:ssr-health (samples /__health into tx_vitec_ssrhealthlog, 14 days kept, scheduler-ready). Package 2: dashboard widgets vitecSsrStatus (live health, release, switch, last purge) and vitecSsrStats (purge counts, failures, uptime, response time). Package 3: backend module web_vitecssr with manual purge (paths or everything) and both protocols. Tested end to end against the running node (purge single/all 200, health sample logged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-11-09 20:46:16 +01:00
1 parent d52d49974f
commit 63f950f9db
14 files changed
+1003

No files matched your search

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Widgets;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
/**
* Dashboard tile: purge and health statistics from the two protocol
* tables. History depth = what vitec:ssr-health sampling (scheduler)
* and the purge log hold (14 days).
*/
final class SsrStatsWidget implements WidgetInterface
{
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
private readonly ConnectionPool $connectionPool,
) {}
public function renderWidgetContent(): string
{
$now = time();
$d1 = $now - 86400;
$d7 = $now - 7 * 86400;
$purges24h = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d1);
$purges7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7);
$failed7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7 . ' AND success = 0');
$full7d = $this->count('tx_vitec_ssrpurgelog', "crdate > " . $d7 . " AND paths = '*'");
$samples24h = $this->count('tx_vitec_ssrhealthlog', 'crdate > ' . $d1);
$ok24h = $this->count('tx_vitec_ssrhealthlog', 'crdate > ' . $d1 . ' AND ok = 1');
$uptime = $samples24h > 0 ? round($ok24h / $samples24h * 100, 1) . ' %' : '-';
$avgMs = $this->scalar('SELECT ROUND(AVG(response_ms)) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d1 . ' AND ok = 1');
$maxCache = $this->scalar('SELECT MAX(cache_mb) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d7);
$rows = [
['Purges (24 h / 7 Tage)', $purges24h . ' / ' . $purges7d],
['davon Komplett-Purges (7 T.)', (string)$full7d],
['Fehlgeschlagene Purges (7 T.)', $failed7d > 0
? '<span class="badge badge-danger">' . $failed7d . '</span>'
: '<span class="badge badge-success">0</span>'],
['Verfügbarkeit (24 h)', htmlspecialchars($uptime)
. ' <small>(' . $ok24h . '/' . $samples24h . ' Messungen)</small>'],
['&empty; Antwortzeit (24 h)', $avgMs !== null ? $avgMs . ' ms' : '-'],
['Max. Cache-Größe (7 T.)', $maxCache !== null ? $maxCache . ' MB' : '-'],
];
$html = '<table class="table table-striped table-sm"><tbody>';
foreach ($rows as [$label, $value]) {
$html .= '<tr><th style="width:55%">' . $label . '</th><td>' . $value . '</td></tr>';
}
$html .= '</tbody></table>';
if ($samples24h === 0) {
$html .= '<p class="text-muted" style="font-size:11px">Noch keine Messwerte: Scheduler-Task für <code>vitec:ssr-health</code> einrichten (z. B. alle 5 Minuten).</p>';
}
return $html;
}
public function getOptions(): array
{
return [];
}
private function count(string $table, string $where): int
{
try {
return (int)$this->connectionPool->getConnectionForTable($table)
->executeQuery('SELECT COUNT(*) FROM ' . $table . ' WHERE ' . $where)->fetchOne();
} catch (\Throwable) {
return 0;
}
}
private function scalar(string $sql): ?string
{
try {
$v = $this->connectionPool->getConnectionForTable('tx_vitec_ssrhealthlog')
->executeQuery($sql)->fetchOne();
return $v === null || $v === false ? null : (string)$v;
} catch (\Throwable) {
return null;
}
}
}