fetchHealth(); $lastPurge = $this->lastPurgeRow(); $limit = $this->purgeService->getCacheLimitMb(); // Head: pulse + state if ($health === null) { $head = '
' . '
SSR not reachable
' . '
The cron falls back to the SPA after ' . $this->purgeService->getFailoverAfter() . ' failed checks in a row
'; } else { $ok = (bool)($health['ok'] ?? false); $head = '
' . '
SSR online' : 'vcw-warn">reports errors') . '
' . '
' . ($flagOn ? 'Pages are rendered on the VPS' : 'Delivery is off: TYPO3 serves the SPA') . ' · ' . (int)$health['responseMs'] . ' ms
'; } $html = '
' . $head . '
'; // Cache ring + facts $html .= '
'; if ($health !== null) { $mb = (int)($health['cacheMB'] ?? 0); $pages = (int)($health['cachedPages'] ?? 0); $frac = $limit > 0 ? $mb / $limit : 0.0; $html .= '
' . $h::ring($frac, 'sky', $limit > 0 ? round($frac * 100) . ' %' : $mb . ' MB') . '
Cache
' . number_format($pages, 0, '.', ',') . 'pages
' . $mb . ($limit > 0 ? ' / ' . $limit : '') . ' MB
'; } $html .= '
' . '
Delivery
' . ($flagOn ? $h::pill('ok', 'ON') : $h::pill('muted', 'OFF')) . '
'; if ($health !== null) { $html .= '
Release
' . $h::e((string)($health['release']['release'] ?? '?')) . '
' . '
Commit
' . $h::e((string)($health['release']['commit'] ?? '?')) . '
'; } $html .= '
Webhook
' . ($this->purgeService->isConfigured() ? $h::pill('ok', 'configured') : $h::pill('warn', 'not configured', 'var/vitec-ssr-purge.env is missing or incomplete')) . '
' . '
'; // Last purge if ($lastPurge !== null) { [$label, $tone, $who] = $h::trigger((string)$lastPurge['trigger_source']); $paths = trim((string)$lastPurge['paths']); $html .= '
Last purge' . '' . date('d.m. H:i', (int)$lastPurge['crdate']) . '' . $h::pill($tone, $label) . ($who !== '' ? ' ' . $h::e($who) . '' : '') . ' ' . ((int)$lastPurge['success'] ? $h::pill('ok', 'ok') : $h::pill('bad', 'failed')) . '' . ($paths === '*' ? 'all pages (*)' : $h::e(mb_strimwidth($paths, 0, 70, '…'))) . '
'; } $html .= '
' . $h::moduleLink() . '
'; return $h::wrap($html, 'Live state of the SSR node on the VPS: whether it answers and how fast, ' . 'how full its page cache is against the limit, whether SSR delivery is switched on, which frontend ' . 'release runs, and the most recent cache purge. The link opens the cockpit with charts, protocol ' . 'and the purge and warm buttons.', 'status'); } 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) : '?'; } }