Files
VITEC-website/packages/vitec/Classes/Widgets/CockpitHtml.php
T
orascheandClaude Opus 5.5 994f9f2a1b SSR cockpit and dashboard tiles: info button with tooltip
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>
2026-11-11 11:05:33 +01:00

155 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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) . ' &rarr;</a>';
}
}