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:
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>'],
|
||||
['∅ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Widgets;
|
||||
|
||||
use Evomedien\Vitec\Service\SsrPurgeService;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
||||
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
|
||||
|
||||
/**
|
||||
* Dashboard tile: live state of the SSR node on the Solr VPS plus the
|
||||
* delivery switch - green/red at a glance after backend login.
|
||||
*/
|
||||
final class SsrStatusWidget implements WidgetInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WidgetConfigurationInterface $configuration,
|
||||
private readonly SsrPurgeService $purgeService,
|
||||
private readonly RequestFactory $requestFactory,
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
public function renderWidgetContent(): string
|
||||
{
|
||||
$flagOn = file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag');
|
||||
$health = $this->fetchHealth();
|
||||
$lastPurge = $this->lastPurgeRow();
|
||||
|
||||
$badge = static function (bool $ok, string $yes, string $no): string {
|
||||
return $ok
|
||||
? '<span class="badge badge-success">' . htmlspecialchars($yes) . '</span>'
|
||||
: '<span class="badge badge-danger">' . htmlspecialchars($no) . '</span>';
|
||||
};
|
||||
|
||||
$rows = [];
|
||||
$rows[] = ['SSR-Auslieferung', $badge($flagOn, 'AN - Seiten kommen vom VPS', 'AUS - SPA wie bisher')];
|
||||
if ($health === null) {
|
||||
$rows[] = ['SSR-Server', '<span class="badge badge-danger">NICHT ERREICHBAR</span>'];
|
||||
} else {
|
||||
$rows[] = ['SSR-Server', $badge((bool)($health['ok'] ?? false), 'läuft (' . (int)$health['responseMs'] . ' ms)', 'meldet Fehler')];
|
||||
$rows[] = ['Release', htmlspecialchars((string)($health['release']['release'] ?? '?'))
|
||||
. ' <small>(Commit ' . htmlspecialchars((string)($health['release']['commit'] ?? '?'))
|
||||
. ', gebaut ' . htmlspecialchars($this->fmtDate((string)($health['release']['builtAt'] ?? ''))) . ')</small>'];
|
||||
$rows[] = ['Seiten-Cache', (int)($health['cachedPages'] ?? 0) . ' Seiten / ' . (int)($health['cacheMB'] ?? 0) . ' MB'];
|
||||
}
|
||||
if ($lastPurge !== null) {
|
||||
$rows[] = ['Letzter Purge', htmlspecialchars(date('d.m. H:i', (int)$lastPurge['crdate']))
|
||||
. ' <small>(' . htmlspecialchars((string)$lastPurge['trigger_source']) . ', '
|
||||
. ($lastPurge['success'] ? 'ok' : 'FEHLER') . ', '
|
||||
. htmlspecialchars(mb_strimwidth((string)$lastPurge['paths'], 0, 60, '…')) . ')</small>'];
|
||||
}
|
||||
if (!$this->purgeService->isConfigured()) {
|
||||
$rows[] = ['Auto-Purge', '<span class="badge badge-warning">nicht konfiguriert (var/vitec-ssr-purge.env fehlt)</span>'];
|
||||
}
|
||||
|
||||
$html = '<table class="table table-striped table-sm"><tbody>';
|
||||
foreach ($rows as [$label, $value]) {
|
||||
$html .= '<tr><th style="width:38%">' . htmlspecialchars($label) . '</th><td>' . $value . '</td></tr>';
|
||||
}
|
||||
$html .= '</tbody></table>';
|
||||
$html .= '<p class="text-muted" style="font-size:11px">Schalter: Site Management › Settings › VITEC. Modul "VITEC SSR" für Purge & Protokoll.</p>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function fetchHealth(): ?array
|
||||
{
|
||||
$endpoint = $this->purgeService->getHealthEndpoint();
|
||||
if ($endpoint === '') {
|
||||
return null;
|
||||
}
|
||||
$start = microtime(true);
|
||||
try {
|
||||
$response = $this->requestFactory->request($endpoint, 'GET', [
|
||||
'timeout' => 3, 'connect_timeout' => 2, 'http_errors' => false,
|
||||
]);
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return null;
|
||||
}
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
$data['responseMs'] = (int)round((microtime(true) - $start) * 1000);
|
||||
return $data;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function lastPurgeRow(): ?array
|
||||
{
|
||||
try {
|
||||
$qb = $this->connectionPool->getQueryBuilderForTable('tx_vitec_ssrpurgelog');
|
||||
$row = $qb->select('*')->from('tx_vitec_ssrpurgelog')
|
||||
->orderBy('uid', 'DESC')->setMaxResults(1)
|
||||
->executeQuery()->fetchAssociative();
|
||||
return $row ?: null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function fmtDate(string $iso): string
|
||||
{
|
||||
$ts = strtotime($iso);
|
||||
return $ts ? date('d.m.Y H:i', $ts) : '?';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user