85ab829805
Status page and JSON API read from private/data; root index.php stays a placeholder. Mark todo 06 done. Co-authored-by: Cursor <cursoragent@cursor.com>
275 lines
8.0 KiB
PHP
275 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
$root = dirname(dirname(__DIR__));
|
|
$configDir = is_dir($root . '/private/config')
|
|
? $root . '/private/config'
|
|
: $root . '/config';
|
|
|
|
require_once $configDir . '/status-view.php';
|
|
|
|
$status = formatPublicStatus(loadStatusSnapshot());
|
|
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
header('X-Robots-Tag: noindex');
|
|
|
|
function e(string $value): string
|
|
{
|
|
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
function formatCheckedAt(?string $iso): string
|
|
{
|
|
if ($iso === null || $iso === '') {
|
|
return '—';
|
|
}
|
|
|
|
$ts = strtotime($iso);
|
|
if ($ts === false) {
|
|
return '—';
|
|
}
|
|
|
|
return gmdate('Y-m-d H:i', $ts) . ' UTC';
|
|
}
|
|
|
|
function bokaHeartbeatLine(?array $boka): string
|
|
{
|
|
if ($boka === null) {
|
|
return 'No push data yet';
|
|
}
|
|
|
|
$host = (string) ($boka['host'] ?? 'boka');
|
|
$age = $boka['age_minutes'];
|
|
|
|
if ($age === null) {
|
|
return "{$host}: no push timestamp";
|
|
}
|
|
|
|
$line = "{$host}: last push {$age} min ago";
|
|
|
|
$details = [];
|
|
if ($boka['disk_percent'] !== null) {
|
|
$details[] = "disk {$boka['disk_percent']}%";
|
|
}
|
|
if ($boka['mem_available_mb'] !== null) {
|
|
$details[] = "mem {$boka['mem_available_mb']} MB free";
|
|
}
|
|
if ($boka['containers_running'] !== null) {
|
|
$containers = (int) $boka['containers_running'];
|
|
$unhealthy = (int) ($boka['containers_unhealthy'] ?? 0);
|
|
$details[] = $unhealthy > 0
|
|
? "{$containers} containers ({$unhealthy} unhealthy)"
|
|
: "{$containers} containers";
|
|
}
|
|
|
|
if ($details !== []) {
|
|
$line .= ' · ' . implode(' · ', $details);
|
|
}
|
|
|
|
return $line;
|
|
}
|
|
|
|
$jahr = (int) date('Y');
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex">
|
|
<title>0012 — status</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
:root {
|
|
--bg: #0a0a0a;
|
|
--fg: #e8e8e8;
|
|
--dim: #666;
|
|
--line: #333;
|
|
--acid: #c8ff00;
|
|
--blood: #ff2a2a;
|
|
--amber: #ffb020;
|
|
}
|
|
html { font-size: 16px; }
|
|
body {
|
|
min-height: 100vh;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-family: "IBM Plex Mono", "Courier New", monospace;
|
|
line-height: 1.45;
|
|
padding: 0 clamp(1rem, 4vw, 3rem);
|
|
}
|
|
.wrap {
|
|
width: 100%;
|
|
max-width: 52rem;
|
|
margin: 0 auto;
|
|
min-height: 100vh;
|
|
border-left: 4px solid var(--acid);
|
|
border-right: 1px solid var(--line);
|
|
}
|
|
header {
|
|
border-bottom: 1px solid var(--line);
|
|
padding: 2rem 1.5rem 1.5rem;
|
|
}
|
|
.logo {
|
|
font-size: clamp(2.5rem, 10vw, 4rem);
|
|
font-weight: 700;
|
|
letter-spacing: -0.04em;
|
|
line-height: 1;
|
|
color: var(--fg);
|
|
}
|
|
.logo span { color: var(--acid); }
|
|
.claim {
|
|
margin-top: 0.75rem;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.2em;
|
|
color: var(--dim);
|
|
}
|
|
section {
|
|
border-bottom: 1px solid var(--line);
|
|
padding: 1.5rem;
|
|
}
|
|
h2 {
|
|
font-size: 0.7rem;
|
|
font-weight: 400;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.25em;
|
|
color: var(--dim);
|
|
margin-bottom: 1rem;
|
|
}
|
|
p { margin-bottom: 0.75rem; }
|
|
p:last-child { margin-bottom: 0; }
|
|
.empty {
|
|
border: 1px solid var(--line);
|
|
padding: 1.25rem;
|
|
color: var(--dim);
|
|
font-size: 0.85rem;
|
|
}
|
|
.heartbeat {
|
|
font-size: 0.85rem;
|
|
padding: 0.75rem 1rem;
|
|
border: 1px solid var(--line);
|
|
border-left: 3px solid var(--acid);
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 0;
|
|
border: 1px solid var(--line);
|
|
}
|
|
@media (max-width: 520px) {
|
|
.grid { grid-template-columns: 1fr; }
|
|
.tile { border-right: none !important; }
|
|
}
|
|
.tile {
|
|
padding: 1rem 1.25rem;
|
|
border-right: 1px solid var(--line);
|
|
border-bottom: 1px solid var(--line);
|
|
font-size: 0.8rem;
|
|
border-left: 3px solid var(--line);
|
|
}
|
|
.tile:nth-child(2n) { border-right: none; }
|
|
.tile--ok { border-left-color: var(--acid); }
|
|
.tile--warn { border-left-color: var(--amber); }
|
|
.tile--down { border-left-color: var(--blood); }
|
|
.tile strong {
|
|
display: block;
|
|
font-size: 0.65rem;
|
|
letter-spacing: 0.15em;
|
|
text-transform: uppercase;
|
|
color: var(--dim);
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
.tile .status {
|
|
font-size: 0.65rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.tile--ok .status { color: var(--acid); }
|
|
.tile--warn .status { color: var(--amber); }
|
|
.tile--down .status { color: var(--blood); }
|
|
.tile .meta {
|
|
font-size: 0.7rem;
|
|
color: var(--dim);
|
|
margin-top: 0.35rem;
|
|
}
|
|
footer {
|
|
padding: 1.5rem;
|
|
font-size: 0.7rem;
|
|
color: var(--dim);
|
|
letter-spacing: 0.05em;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header>
|
|
<div class="logo">00<span>12</span> / monitor</div>
|
|
<p class="claim">External status · pull checks · boka push</p>
|
|
</header>
|
|
|
|
<?php if (!$status['initialized']): ?>
|
|
<section>
|
|
<div class="empty"><?= e((string) $status['message']) ?></div>
|
|
</section>
|
|
<?php else: ?>
|
|
|
|
<section>
|
|
<h2>boka heartbeat</h2>
|
|
<div class="heartbeat"><?= e(bokaHeartbeatLine($status['boka'])) ?></div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Targets</h2>
|
|
<?php if ($status['checks'] === []): ?>
|
|
<div class="empty">No check results yet</div>
|
|
<?php else: ?>
|
|
<div class="grid">
|
|
<?php foreach ($status['checks'] as $check): ?>
|
|
<?php
|
|
$tileStatus = (string) ($check['status'] ?? 'down');
|
|
$tileClass = in_array($tileStatus, ['ok', 'warn', 'down'], true) ? $tileStatus : 'down';
|
|
?>
|
|
<div class="tile tile--<?= e($tileClass) ?>">
|
|
<strong><?= e((string) $check['name']) ?></strong>
|
|
<div class="status"><?= e($tileClass) ?></div>
|
|
<?php if (!empty($check['message'])): ?>
|
|
<p><?= e((string) $check['message']) ?></p>
|
|
<?php endif; ?>
|
|
<div class="meta">
|
|
<?php
|
|
$meta = [];
|
|
if ($check['response_ms'] !== null) {
|
|
$meta[] = (int) $check['response_ms'] . ' ms';
|
|
}
|
|
if ($check['ssl_days_remaining'] !== null) {
|
|
$meta[] = 'SSL ' . (int) $check['ssl_days_remaining'] . 'd';
|
|
}
|
|
echo e($meta !== [] ? implode(' · ', $meta) : '—');
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</section>
|
|
|
|
<?php endif; ?>
|
|
|
|
<footer>
|
|
<?php if ($status['updated_at'] !== null): ?>
|
|
Last pull: <?= e(formatCheckedAt($status['updated_at'])) ?>
|
|
<?php if ($status['duration_ms'] !== null): ?>
|
|
· <?= e((string) $status['duration_ms']) ?> ms run
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
No pull timestamp
|
|
<?php endif; ?>
|
|
<br>© <?= $jahr ?> 0012.de · ops page · noindex
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>
|