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>
144 lines
6.2 KiB
PHP
144 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Evomedien\Vitec\Widgets;
|
|
|
|
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. Same look
|
|
* as the "VITEC SSR" cockpit module (CockpitHtml).
|
|
*/
|
|
final class SsrStatusWidget implements WidgetInterface, AdditionalCssInterface
|
|
{
|
|
public function __construct(
|
|
private readonly WidgetConfigurationInterface $configuration,
|
|
private readonly SsrPurgeService $purgeService,
|
|
private readonly RequestFactory $requestFactory,
|
|
private readonly ConnectionPool $connectionPool,
|
|
) {}
|
|
|
|
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();
|
|
|
|
// Head: pulse + state
|
|
if ($health === null) {
|
|
$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 {
|
|
$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) {
|
|
[$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 .= '<div class="vcw-foot">' . $h::moduleLink() . '</div>';
|
|
return $h::wrap($html);
|
|
}
|
|
|
|
public function getOptions(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function getCssFiles(): array
|
|
{
|
|
return [CockpitHtml::CSS];
|
|
}
|
|
|
|
private function fetchHealth(): ?array
|
|
{
|
|
$endpoint = $this->purgeService->getHealthEndpoint();
|
|
if ($endpoint === '') {
|
|
return null;
|
|
}
|
|
$start = microtime(true);
|
|
try {
|
|
$response = $this->requestFactory->request($endpoint, 'GET', [
|
|
'timeout' => 3, 'connect_timeout' => 2, 'http_errors' => false,
|
|
]);
|
|
if ($response->getStatusCode() !== 200) {
|
|
return null;
|
|
}
|
|
$data = json_decode((string)$response->getBody(), true);
|
|
if (!is_array($data)) {
|
|
return null;
|
|
}
|
|
$data['responseMs'] = (int)round((microtime(true) - $start) * 1000);
|
|
return $data;
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function lastPurgeRow(): ?array
|
|
{
|
|
try {
|
|
$qb = $this->connectionPool->getQueryBuilderForTable('tx_vitec_ssrpurgelog');
|
|
$row = $qb->select('*')->from('tx_vitec_ssrpurgelog')
|
|
->orderBy('uid', 'DESC')->setMaxResults(1)
|
|
->executeQuery()->fetchAssociative();
|
|
return $row ?: null;
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function fmtDate(string $iso): string
|
|
{
|
|
$ts = strtotime($iso);
|
|
return $ts ? date('Y-m-d H:i', $ts) : '?';
|
|
}
|
|
}
|