count('tx_vitec_ssrpurgelog', 'crdate > ' . $d1); $purges7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7); $failed7d = $this->count('tx_vitec_ssrpurgelog', 'crdate > ' . $d7 . ' AND success = 0'); $full7d = $this->count('tx_vitec_ssrpurgelog', "crdate > " . $d7 . " AND paths = '*'"); $avgMs = $this->scalar('SELECT ROUND(AVG(response_ms)) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d1 . ' AND ok = 1'); $maxCache = $this->scalar('SELECT MAX(cache_mb) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d7); // Browser-side errors reported via POST /api/vitec/client-error. $clientErrors24h = $this->count('tx_vitec_clienterrorlog', 'crdate > ' . $d1); $clientErrors7d = $this->count('tx_vitec_clienterrorlog', 'crdate > ' . $d7); $strip = $h::strip($this->connectionPool); $uptime = $strip['uptime']; $uptimeTone = $uptime === null ? '' : ($uptime >= 99.5 ? 'vcw-ok' : ($uptime >= 95 ? 'vcw-warn' : 'vcw-bad')); $html = '
Availability · 24 h
' . '
' . ($uptime === null ? '–' : number_format($uptime, 1)) . '%
' . '
' . $strip['samples'] . ' samples
' . $strip['slow'] . ' slow · ' . $strip['down'] . ' down
' . $strip['html']; $tile = static fn(string $label, string $value, string $sub = '', string $tone = ''): string => '
' . $h::e($label) . '
' . $value . '
' . ($sub !== '' ? '
' . $sub . '
' : '') . '
'; $html .= '
' . $tile('Purges', $purges24h . '/ ' . $purges7d . '', '24 h / 7 days') . $tile('Full purges', (string)$full7d, 'last 7 days', $full7d > 0 ? 'vcw-warn' : '') . $tile('Failed purges', (string)$failed7d, 'last 7 days', $failed7d > 0 ? 'vcw-bad' : 'vcw-ok') . $tile('Avg. response', $avgMs !== null ? $avgMs . 'ms' : '–', 'last 24 h') . $tile('Max. cache', $maxCache !== null ? $maxCache . 'MB' : '–', 'last 7 days') . $tile('Client errors', $clientErrors24h . '/ ' . $clientErrors7d . '', '24 h / 7 days', $clientErrors24h > 0 ? 'vcw-warn' : 'vcw-ok') . '
'; if ($strip['samples'] === 0) { $html .= '

No samples yet: the cron job vitec:ssr-health fills this every 5 minutes.

'; } $html .= '
' . $h::moduleLink('Charts and protocol in the cockpit') . '
'; return $h::wrap($html); } public function getOptions(): array { return []; } public function getCssFiles(): array { return [CockpitHtml::CSS]; } private function count(string $table, string $where): int { try { return (int)$this->connectionPool->getConnectionForTable($table) ->executeQuery('SELECT COUNT(*) FROM ' . $table . ' WHERE ' . $where)->fetchOne(); } catch (\Throwable) { return 0; } } private function scalar(string $sql): ?string { try { $v = $this->connectionPool->getConnectionForTable('tx_vitec_ssrhealthlog') ->executeQuery($sql)->fetchOne(); return $v === null || $v === false ? null : (string)$v; } catch (\Throwable) { return null; } } }