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:
1 parent
cda26947f2
commit
62f3f5394e
5 files changed
+409
-97
No files matched your search
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Evomedien\Vitec\Widgets;
|
||||
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
|
||||
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
||||
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
|
||||
|
||||
@@ -18,7 +19,7 @@ use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
|
||||
* figures are sums of positive deltas between consecutive samples - a
|
||||
* reset simply contributes nothing.
|
||||
*/
|
||||
final class SsrErrorsWidget implements WidgetInterface
|
||||
final class SsrErrorsWidget implements WidgetInterface, AdditionalCssInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WidgetConfigurationInterface $configuration,
|
||||
@@ -27,44 +28,55 @@ final class SsrErrorsWidget implements WidgetInterface
|
||||
|
||||
public function renderWidgetContent(): string
|
||||
{
|
||||
$h = CockpitHtml::class;
|
||||
$latest = $this->latestStatsRow();
|
||||
if ($latest === null) {
|
||||
return '<p class="text-muted">No /__stats samples yet. They arrive with the next '
|
||||
return $h::wrap('<p class="vcw-dim">No /__stats samples yet. They arrive with the next '
|
||||
. '<code>vitec:ssr-health</code> cron run (every 5 minutes) once the node release '
|
||||
. 'provides <code>/__stats</code>.</p>';
|
||||
. 'provides <code>/__stats</code>.</p>');
|
||||
}
|
||||
|
||||
$served = (int)$latest['stat_hit'] + (int)$latest['stat_miss'] + (int)$latest['stat_stale'];
|
||||
$hitRate = $served > 0
|
||||
? number_format(((int)$latest['stat_hit'] + (int)$latest['stat_stale']) / $served * 100, 1) . ' %'
|
||||
: '-';
|
||||
$hit = (int)$latest['stat_hit'];
|
||||
$stale = (int)$latest['stat_stale'];
|
||||
$miss = (int)$latest['stat_miss'];
|
||||
$served = $hit + $miss + $stale;
|
||||
$rate = $served > 0 ? ($hit + $stale) / $served : null;
|
||||
|
||||
[$newRender24h, $newBackend24h] = $this->errorDeltas24h();
|
||||
$down24h = $this->downSamples24h();
|
||||
|
||||
$rows = [
|
||||
['Cache hit rate (since node start)', htmlspecialchars($hitRate)
|
||||
. ' <small>(hit ' . (int)$latest['stat_hit']
|
||||
. ' / stale ' . (int)$latest['stat_stale']
|
||||
. ' / miss ' . (int)$latest['stat_miss'] . ')</small>'],
|
||||
['Render errors (new in 24 h / total)', $this->errorCell($newRender24h, (int)$latest['stat_render_errors'])],
|
||||
['Backend errors (new in 24 h / total)', $this->errorCell($newBackend24h, (int)$latest['stat_backend_errors'])],
|
||||
['DOWN samples (24 h)', $down24h['count'] > 0
|
||||
? '<span class="badge badge-danger">' . $down24h['count'] . '</span> <small>last: '
|
||||
. htmlspecialchars($down24h['last']) . '</small>'
|
||||
: '<span class="badge badge-success">0</span>'],
|
||||
['Sample age', htmlspecialchars($this->age((int)$latest['crdate']))],
|
||||
];
|
||||
// Hit rate ring + split bar hit / stale / miss
|
||||
$pct = static fn(int $v): string => $served > 0 ? round($v / $served * 100, 1) . '%' : '0%';
|
||||
$html = '<div class="vcw-gauge vcw-gauge-wide">'
|
||||
. $h::ring($rate ?? 0.0, 'ok', $rate === null ? '–' : number_format($rate * 100, 1) . ' %', 84)
|
||||
. '<div class="vcw-grow"><div class="vcw-label">Cache hit rate</div>'
|
||||
. '<div class="vcw-sub">since the node started · ' . number_format($served, 0, '.', ',') . ' requests</div>'
|
||||
. '<div class="vcw-split-bar" role="img" aria-label="Share of requests by cache result">'
|
||||
. '<i class="is-navy" style="width:' . $pct($hit) . '" title="served from cache: ' . $hit . '"></i>'
|
||||
. '<i class="is-sky" style="width:' . $pct($stale) . '" title="stale, refreshed in background: ' . $stale . '"></i>'
|
||||
. '<i class="is-warn" style="width:' . $pct($miss) . '" title="rendered fresh: ' . $miss . '"></i></div>'
|
||||
. '<div class="vcw-legend"><span><i class="is-navy"></i>cache ' . $hit . '</span>'
|
||||
. '<span><i class="is-sky"></i>stale ' . $stale . '</span><span><i class="is-warn"></i>fresh ' . $miss . '</span></div>'
|
||||
. '</div></div>';
|
||||
|
||||
$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>';
|
||||
$html .= '<p class="text-muted" style="font-size:11px">Totals are cumulative since the node '
|
||||
. 'last (re)started; a deploy resets them. Error details: '
|
||||
. '<code>journalctl -u vitec-ssr</code> on the VPS.</p>';
|
||||
return $html;
|
||||
$tile = static fn(string $label, string $value, string $sub, string $tone): string =>
|
||||
'<div class="vcw-tile"><div class="vcw-label">' . $h::e($label) . '</div><div class="vcw-val ' . $tone . '">'
|
||||
. $value . '</div><div class="vcw-sub">' . $sub . '</div></div>';
|
||||
$newCell = static fn(?int $new): array => $new === null
|
||||
? ['n/a', '']
|
||||
: ['+' . $new, $new > 0 ? 'vcw-bad' : 'vcw-ok'];
|
||||
[$renderVal, $renderTone] = $newCell($newRender24h);
|
||||
[$backendVal, $backendTone] = $newCell($newBackend24h);
|
||||
|
||||
$html .= '<div class="vcw-tiles">'
|
||||
. $tile('Render errors', $renderVal, 'last 24 h · ' . (int)$latest['stat_render_errors'] . ' since node start', $renderTone)
|
||||
. $tile('Backend errors', $backendVal, 'last 24 h · ' . (int)$latest['stat_backend_errors'] . ' since node start', $backendTone)
|
||||
. $tile('Down samples', (string)$down24h['count'], $down24h['count'] > 0 ? 'last ' . $h::e($down24h['last']) : 'last 24 h', $down24h['count'] > 0 ? 'vcw-bad' : 'vcw-ok')
|
||||
. $tile('Sample age', $h::e($this->age((int)$latest['crdate'])), 'cron every 5 min', '')
|
||||
. '</div>';
|
||||
$html .= '<p class="vcw-note">Totals count since the node last started; a deploy resets them. '
|
||||
. 'Error details: <code>journalctl -u vitec-ssr</code> on the VPS.</p>';
|
||||
return $h::wrap($html);
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
@@ -72,14 +84,9 @@ final class SsrErrorsWidget implements WidgetInterface
|
||||
return [];
|
||||
}
|
||||
|
||||
private function errorCell(?int $new24h, int $total): string
|
||||
public function getCssFiles(): array
|
||||
{
|
||||
$newBadge = $new24h === null
|
||||
? '<span class="badge badge-secondary">n/a</span>'
|
||||
: ($new24h > 0
|
||||
? '<span class="badge badge-danger">+' . $new24h . '</span>'
|
||||
: '<span class="badge badge-success">+0</span>');
|
||||
return $newBadge . ' <small>/ ' . $total . ' total</small>';
|
||||
return [CockpitHtml::CSS];
|
||||
}
|
||||
|
||||
private function latestStatsRow(): ?array
|
||||
|
||||
Reference in new issue
Block a user