Every tile of the module and the three SSR dashboard widgets has a round question mark button top right. Hovering it or focusing it with the keyboard shows a tooltip that explains what the tile shows. Pure CSS, anchored to the card so it never leaves the tile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
6.2 KiB
PHP
155 lines
6.2 KiB
PHP
<?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);
|
||
}
|
||
|
||
/**
|
||
* Tile wrapper; $info becomes the round "?" button top right whose
|
||
* tooltip explains the tile (CSS only: hover or keyboard focus).
|
||
*/
|
||
public static function wrap(string $inner, string $info = '', string $id = ''): string
|
||
{
|
||
$button = '';
|
||
if ($info !== '') {
|
||
$tipId = 'vcw-tip-' . ($id !== '' ? $id : substr(md5($info), 0, 8));
|
||
$button = '<span class="vcw-info"><button type="button" class="vcw-info-btn" aria-label="About this tile"'
|
||
. ' aria-describedby="' . $tipId . '">?</button><span class="vcw-tip" role="tooltip" id="' . $tipId . '">'
|
||
. self::e($info) . '</span></span>';
|
||
}
|
||
return '<div class="vc-widget">' . $button . $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) . ' →</a>';
|
||
}
|
||
}
|