SSR dashboard tiles in the cockpit look

The three SSR widgets render dark VITEC tiles like the cockpit module: status with pulse, cache ring and last purge, statistics with the 24 h availability strip and number tiles, errors with hit rate ring and split bar. Shared markup in CockpitHtml, styles in dashboard-ssr.css loaded per widget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Opus 5.5 committed 2026-11-11 10:59:49 +01:00
1 parent cda26947f2
commit 62f3f5394e
5 files changed
+409 -97

No files matched your search

@@ -8,14 +8,16 @@ 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\AdditionalCssInterface;
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.
* delivery switch - green/red at a glance after backend login. Same look
* as the "VITEC SSR" cockpit module (CockpitHtml).
*/
final class SsrStatusWidget implements WidgetInterface
final class SsrStatusWidget implements WidgetInterface, AdditionalCssInterface
{
public function __construct(
private readonly WidgetConfigurationInterface $configuration,
@@ -26,44 +28,63 @@ final class SsrStatusWidget implements WidgetInterface
public function renderWidgetContent(): string
{
$h = CockpitHtml::class;
$flagOn = file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag');
$health = $this->fetchHealth();
$lastPurge = $this->lastPurgeRow();
$limit = $this->purgeService->getCacheLimitMb();
$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 delivery', $badge($flagOn, 'ON - pages served from the VPS', 'OFF - SPA as before')];
// Head: pulse + state
if ($health === null) {
$rows[] = ['SSR server', '<span class="badge badge-danger">NOT REACHABLE</span>'];
$head = '<span class="vcw-pulse is-down" aria-hidden="true"></span><div>'
. '<div class="vcw-title">SSR <span class="vcw-bad">not reachable</span></div>'
. '<div class="vcw-sub">The cron falls back to the SPA after '
. $this->purgeService->getFailoverAfter() . ' failed checks in a row</div></div>';
} else {
$rows[] = ['SSR server', $badge((bool)($health['ok'] ?? false), 'running (' . (int)$health['responseMs'] . ' ms)', 'reports errors')];
$rows[] = ['Release', htmlspecialchars((string)($health['release']['release'] ?? '?'))
. ' <small>(commit ' . htmlspecialchars((string)($health['release']['commit'] ?? '?'))
. ', built ' . htmlspecialchars($this->fmtDate((string)($health['release']['builtAt'] ?? ''))) . ')</small>'];
$rows[] = ['Page cache', (int)($health['cachedPages'] ?? 0) . ' pages / ' . (int)($health['cacheMB'] ?? 0) . ' MB'];
$ok = (bool)($health['ok'] ?? false);
$head = '<span class="vcw-pulse' . ($ok ? '' : ' is-warn') . '" aria-hidden="true"></span><div>'
. '<div class="vcw-title">SSR <span class="' . ($ok ? 'vcw-ok">online' : 'vcw-warn">reports errors') . '</span></div>'
. '<div class="vcw-sub">' . ($flagOn ? 'Pages are rendered on the VPS' : 'Delivery is off: TYPO3 serves the SPA')
. ' · ' . (int)$health['responseMs'] . ' ms</div></div>';
}
$html = '<div class="vcw-head">' . $head . '</div>';
// Cache ring + facts
$html .= '<div class="vcw-split">';
if ($health !== null) {
$mb = (int)($health['cacheMB'] ?? 0);
$pages = (int)($health['cachedPages'] ?? 0);
$frac = $limit > 0 ? $mb / $limit : 0.0;
$html .= '<div class="vcw-gauge">' . $h::ring($frac, 'sky', $limit > 0 ? round($frac * 100) . ' %' : $mb . ' MB')
. '<div><div class="vcw-label">Cache</div><div class="vcw-num">' . number_format($pages, 0, '.', ',')
. '<small>pages</small></div><div class="vcw-sub">' . $mb . ($limit > 0 ? ' / ' . $limit : '') . ' MB</div></div></div>';
}
$html .= '<dl class="vcw-facts">'
. '<div><dt>Delivery</dt><dd>' . ($flagOn ? $h::pill('ok', 'ON') : $h::pill('muted', 'OFF')) . '</dd></div>';
if ($health !== null) {
$html .= '<div><dt>Release</dt><dd><code title="built ' . $h::e($this->fmtDate((string)($health['release']['builtAt'] ?? '')))
. '">' . $h::e((string)($health['release']['release'] ?? '?')) . '</code></dd></div>'
. '<div><dt>Commit</dt><dd><code>' . $h::e((string)($health['release']['commit'] ?? '?')) . '</code></dd></div>';
}
$html .= '<div><dt>Webhook</dt><dd>' . ($this->purgeService->isConfigured()
? $h::pill('ok', 'configured')
: $h::pill('warn', 'not configured', 'var/vitec-ssr-purge.env is missing or incomplete')) . '</dd></div>'
. '</dl></div>';
// Last purge
if ($lastPurge !== null) {
$rows[] = ['Last purge', htmlspecialchars(date('m-d H:i', (int)$lastPurge['crdate']))
. ' <small>(' . htmlspecialchars((string)$lastPurge['trigger_source']) . ', '
. ($lastPurge['success'] ? 'ok' : 'FAILED') . ', '
. htmlspecialchars(mb_strimwidth((string)$lastPurge['paths'], 0, 60, '…')) . ')</small>'];
}
if (!$this->purgeService->isConfigured()) {
$rows[] = ['Auto purge', '<span class="badge badge-warning">not configured (var/vitec-ssr-purge.env missing)</span>'];
[$label, $tone, $who] = $h::trigger((string)$lastPurge['trigger_source']);
$paths = trim((string)$lastPurge['paths']);
$html .= '<div class="vcw-last"><span class="vcw-label">Last purge</span>'
. '<span class="vcw-time">' . date('d.m. H:i', (int)$lastPurge['crdate']) . '</span>'
. $h::pill($tone, $label) . ($who !== '' ? ' <span class="vcw-dim">' . $h::e($who) . '</span>' : '')
. ' ' . ((int)$lastPurge['success'] ? $h::pill('ok', 'ok') : $h::pill('bad', 'failed'))
. '<span class="vcw-path" title="' . $h::e($paths) . '">'
. ($paths === '*' ? 'all pages (*)' : $h::e(mb_strimwidth($paths, 0, 70, '…'))) . '</span></div>';
}
$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">Switch: Site Management &rsaquo; Settings &rsaquo; VITEC. Module "VITEC SSR" for purge &amp; protocol.</p>';
return $html;
$html .= '<div class="vcw-foot">' . $h::moduleLink() . '</div>';
return $h::wrap($html);
}
public function getOptions(): array
@@ -71,6 +92,11 @@ final class SsrStatusWidget implements WidgetInterface
return [];
}
public function getCssFiles(): array
{
return [CockpitHtml::CSS];
}
private function fetchHealth(): ?array
{
$endpoint = $this->purgeService->getHealthEndpoint();