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

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Widgets;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Shared markup for the three SSR dashboard tiles, in the look of the
* "VITEC SSR" cockpit module (dark VITEC navy, orange accents).
* The dashboard inserts widget HTML without running scripts, so rings
* and bars are drawn here as plain SVG/HTML. Styles:
* Resources/Public/Css/dashboard-ssr.css (scoped to .vc-widget).
*/
final class CockpitHtml
{
public const CSS = 'EXT:vitec/Resources/Public/Css/dashboard-ssr.css';
public static function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES);
}
public static function wrap(string $inner): string
{
return '<div class="vc-widget">' . $inner . '</div>';
}
public static function pill(string $tone, string $text, string $title = ''): string
{
return '<span class="vcw-pill is-' . self::e($tone) . '"'
. ($title !== '' ? ' title="' . self::e($title) . '"' : '') . '>' . self::e($text) . '</span>';
}
/**
* Ring gauge, 0..1, with a short text in the middle.
*/
public static function ring(float $fraction, string $tone, string $center, int $size = 76): string
{
$c = 2 * M_PI * 40;
$offset = $c * (1 - max(0.0, min(1.0, $fraction)));
return '<div class="vcw-ring" style="width:' . $size . 'px;height:' . $size . 'px">'
. '<svg viewBox="0 0 96 96" width="' . $size . '" height="' . $size . '" aria-hidden="true">'
. '<circle class="vcw-track" cx="48" cy="48" r="40"/>'
. '<circle class="vcw-val is-' . self::e($tone) . '" cx="48" cy="48" r="40" stroke-dasharray="'
. round($c, 2) . '" stroke-dashoffset="' . round($offset, 2) . '"/></svg>'
. '<span class="vcw-ring-c">' . self::e($center) . '</span></div>';
}
/**
* Same labels and tones as the purge timeline in the module.
*
* @return array{0: string, 1: string, 2: string} [label, tone, user]
*/
public static function trigger(string $source): array
{
$who = '';
if (str_contains($source, ':')) {
[$source, $who] = explode(':', $source, 2);
}
return match ($source) {
'module' => ['manual', 'orange', $who],
'contextmenu' => ['context menu', 'navy', $who],
'editor-save', 'editor-command' => ['auto-purge', 'ok', $who],
'switch-on' => ['switch-on', 'warn', $who],
default => [$source, 'sky', $who],
};
}
/**
* Availability strip of the last 24 hours, 96 windows of 15 minutes,
* from the health log (cron vitec:ssr-health every 5 minutes).
*
* @return array{html: string, uptime: ?float, slow: int, down: int, samples: int}
*/
public static function strip(ConnectionPool $pool): array
{
$slot = 900;
$end = intdiv(time(), $slot) * $slot + $slot;
$windows = array_fill(0, 96, null);
$ok = 0;
$n = 0;
try {
$rows = $pool->getConnectionForTable('tx_vitec_ssrhealthlog')->executeQuery(
'SELECT crdate, ok, response_ms FROM tx_vitec_ssrhealthlog WHERE crdate > ?',
[$end - 96 * $slot]
)->fetchAllAssociative();
} catch (\Throwable) {
$rows = [];
}
foreach ($rows as $r) {
$i = 95 - intdiv($end - 1 - (int)$r['crdate'], $slot);
if ($i < 0 || $i > 95) {
continue;
}
$n++;
$isOk = (int)$r['ok'] === 1;
$ok += $isOk ? 1 : 0;
$state = !$isOk ? 2 : ((int)$r['response_ms'] > 400 ? 1 : 0);
if ($windows[$i] === null || $state > $windows[$i]['s']) {
$windows[$i] = ['s' => $state, 'ms' => (int)$r['response_ms']];
}
}
$slow = 0;
$down = 0;
$html = '<div class="vcw-bars" role="img" aria-label="Availability of the SSR node over the last 24 hours">';
foreach ($windows as $i => $w) {
$from = $end - (96 - $i) * $slot;
$label = date('H:i', $from) . '–' . date('H:i', $from + $slot) . ' · ';
if ($w === null) {
$html .= '<i class="is-none" title="' . self::e($label . 'no sample') . '"></i>';
continue;
}
$cls = ['', 'is-slow', 'is-down'][$w['s']];
$slow += $w['s'] === 1 ? 1 : 0;
$down += $w['s'] === 2 ? 1 : 0;
$text = $w['s'] === 2 ? 'down' : ($w['s'] === 1 ? 'slow, ' : 'ok, ') . ($w['s'] === 2 ? '' : $w['ms'] . ' ms');
$html .= '<i class="' . $cls . '" title="' . self::e($label . $text) . '"></i>';
}
$html .= '</div><div class="vcw-axis"><span>' . date('H:i', $end - 96 * $slot) . '</span><span>'
. date('H:i', $end - 48 * $slot) . '</span><span>now</span></div>';
return [
'html' => $html,
'uptime' => $n > 0 ? round($ok / $n * 100, 1) : null,
'slow' => $slow,
'down' => $down,
'samples' => $n,
];
}
public static function moduleLink(string $label = 'Open the SSR cockpit'): string
{
try {
$uri = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('web_vitecssr');
} catch (\Throwable) {
return '';
}
return '<a class="vcw-link" href="' . self::e($uri) . '">' . self::e($label) . ' &rarr;</a>';
}
}