feat: add monitoring dashboard under /monitor/
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>
This commit is contained in:
@@ -148,7 +148,10 @@ Siehe auch: [`todo/03-monitoring-pull-phase1.md`](todo/03-monitoring-pull-phase1
|
|||||||
| Ort | Datei | Zweck |
|
| Ort | Datei | Zweck |
|
||||||
|-----|-------|--------|
|
|-----|-------|--------|
|
||||||
| Webspace | `monitor.php` | Cron: Pull-Checks + boka-Stale/Schwellen |
|
| Webspace | `monitor.php` | Cron: Pull-Checks + boka-Stale/Schwellen |
|
||||||
|
| Webspace | `monitor/index.php` | Öffentliches Status-Dashboard (`/monitor/`, noindex) |
|
||||||
|
| Webspace | `monitor/status.php` | Read-only JSON-API (`/monitor/status.php`, Alias `status.json`) |
|
||||||
| Webspace | `ingest.php` | Empfang Push von boka (Token + IP) |
|
| Webspace | `ingest.php` | Empfang Push von boka (Token + IP) |
|
||||||
|
| Webspace | `private/data/last-checks.json` | Letzte Pull-Check-Ergebnisse |
|
||||||
| Webspace | `private/data/boka-status.json` | Letzter Push-Stand |
|
| Webspace | `private/data/boka-status.json` | Letzter Push-Stand |
|
||||||
| boka | `~/server/scripts/push-status.sh` | Sammelt Metriken, POST an Webspace |
|
| boka | `~/server/scripts/push-status.sh` | Sammelt Metriken, POST an Webspace |
|
||||||
| boka | `~/server/config/push.env` | `INGEST_URL`, `PUSH_TOKEN` |
|
| boka | `~/server/config/push.env` | `INGEST_URL`, `PUSH_TOKEN` |
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Dieser Webspace ist bewusst **unabhängig vom vServer `boka`** (Webtropia Cloud
|
|||||||
| **Alarmierung** | E-Mail (und optional Telegram) bei Ausfällen — vom Webspace aus, ohne Zugang zu boka |
|
| **Alarmierung** | E-Mail (und optional Telegram) bei Ausfällen — vom Webspace aus, ohne Zugang zu boka |
|
||||||
| **SMTP-Relay** | Prozesse auf boka und anderswo können über den Plesk-Mailserver von 0012.de senden |
|
| **SMTP-Relay** | Prozesse auf boka und anderswo können über den Plesk-Mailserver von 0012.de senden |
|
||||||
| **Mail-Hosting** | Postfächer für Domains, die noch keinen eigenen Mailserver haben |
|
| **Mail-Hosting** | Postfächer für Domains, die noch keinen eigenen Mailserver haben |
|
||||||
| **Öffentliche Präsenz** | Minimale, aber sinnvolle Landing Page statt leerer Domain |
|
| **Öffentliche Präsenz** | Baustellen-Platzhalter auf `/`; Status-Dashboard unter `/monitor/` |
|
||||||
|
|
||||||
## Hosting
|
## Hosting
|
||||||
|
|
||||||
@@ -53,7 +53,8 @@ Der Webspace sieht boka **von außen** — interne Metriken brauchen das Push-Sk
|
|||||||
├── SITE.md # Konzept Landing Page
|
├── SITE.md # Konzept Landing Page
|
||||||
├── .env.example
|
├── .env.example
|
||||||
├── public/ # Document Root (FTP → httpdocs)
|
├── public/ # Document Root (FTP → httpdocs)
|
||||||
│ ├── index.php # Status-Beacon Landing
|
│ ├── index.php # Baustellen-Platzhalter (spätere Website)
|
||||||
|
│ ├── monitor/ # Status-Dashboard + JSON-API
|
||||||
│ ├── monitor.php # Cron: Pull-Checks
|
│ ├── monitor.php # Cron: Pull-Checks
|
||||||
│ └── ingest.php # Empfang Push von boka
|
│ └── ingest.php # Empfang Push von boka
|
||||||
├── config/ # PHP-Konfiguration → deploy nach private/config/
|
├── config/ # PHP-Konfiguration → deploy nach private/config/
|
||||||
|
|||||||
@@ -2,15 +2,22 @@
|
|||||||
|
|
||||||
**0012.de ist eine eigene Website** — Inhalt noch offen. Kein Monitoring-Dashboard auf der Root-Domain.
|
**0012.de ist eine eigene Website** — Inhalt noch offen. Kein Monitoring-Dashboard auf der Root-Domain.
|
||||||
|
|
||||||
Monitoring, Status-Beacons, `ingest.php` usw. laufen später in **Unterverzeichnissen oder Subdomains** (z. B. `status.0012.de`, `/ops/` — noch festzulegen).
|
Monitoring und Status laufen unter **`/monitor/`** (nicht auf `/`).
|
||||||
|
|
||||||
## Aktuell (Platzhalter)
|
## Aktuell (Platzhalter)
|
||||||
|
|
||||||
Brutalistische Startseite: scharf, dunkel, sarkastisch. Keine Secrets, keine Ops-Daten.
|
[`public/index.php`](public/index.php) ist eine brutalistische Baustellenseite: scharf, dunkel, sarkastisch. Keine Secrets, keine Ops-Daten. Irgendwann kommt hier vermutlich eine eigenständige Website — Inhalt und Form noch offen.
|
||||||
|
|
||||||
## Später: Status / Monitoring (nicht Root)
|
## Status / Monitoring (`/monitor/`)
|
||||||
|
|
||||||
Siehe [`MONITORING.md`](MONITORING.md) und [`todo/06-site-0012-landing.md`](todo/06-site-0012-landing.md) — Zielpfad anpassen, wenn Subdomain/Pfad steht.
|
Öffentliches Ops-Dashboard (read-only, `noindex`):
|
||||||
|
|
||||||
|
- `https://0012.de/monitor/` — HTML-Statusraster aus Pull-Checks + boka-Heartbeat
|
||||||
|
- `https://0012.de/monitor/status.php` — JSON-API (Alias: `/monitor/status.json` via `.htaccess`)
|
||||||
|
|
||||||
|
Siehe [`MONITORING.md`](MONITORING.md) und [`todo/06-site-0012-landing.md`](todo/06-site-0012-landing.md).
|
||||||
|
|
||||||
|
Cron/Ingest bleiben auf Root: `monitor.php`, `ingest.php`.
|
||||||
|
|
||||||
## Was 0012.de werden könnte
|
## Was 0012.de werden könnte
|
||||||
|
|
||||||
@@ -18,6 +25,6 @@ Noch unklar. Die Domain bleibt frei für etwas Eigenes — Portfolio, Experiment
|
|||||||
|
|
||||||
## Was bewusst nicht auf 0012.de /
|
## Was bewusst nicht auf 0012.de /
|
||||||
|
|
||||||
- Öffentliche Monitoring-Kacheln (→ Subdomain/Pfad)
|
- Öffentliche Monitoring-Kacheln (→ `/monitor/`)
|
||||||
- IPs, Tokens, `.env`-Inhalte
|
- IPs, Tokens, `.env`-Inhalte
|
||||||
- Comic Sans (never again)
|
- Comic Sans (never again)
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/monitor-lib.php';
|
||||||
|
|
||||||
|
function loadStatusSnapshot(): array
|
||||||
|
{
|
||||||
|
$checksData = readJsonFile(privateDataDir() . '/last-checks.json');
|
||||||
|
$bokaData = readJsonFile(privateDataDir() . '/boka-status.json');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'checks_data' => $checksData,
|
||||||
|
'boka_data' => $bokaData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array{checks_data: ?array, boka_data: ?array} $snapshot */
|
||||||
|
function formatPublicStatus(array $snapshot): array
|
||||||
|
{
|
||||||
|
$checksData = $snapshot['checks_data'];
|
||||||
|
$bokaData = $snapshot['boka_data'];
|
||||||
|
|
||||||
|
if ($checksData === null && $bokaData === null) {
|
||||||
|
return [
|
||||||
|
'initialized' => false,
|
||||||
|
'message' => 'Monitoring not initialized',
|
||||||
|
'updated_at' => null,
|
||||||
|
'duration_ms' => null,
|
||||||
|
'checks' => [],
|
||||||
|
'boka' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$checks = [];
|
||||||
|
foreach ($checksData['checks'] ?? [] as $check) {
|
||||||
|
if (!is_array($check)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = (string) ($check['name'] ?? '');
|
||||||
|
if ($name === '' || str_starts_with($name, '_')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = (string) ($check['status'] ?? 'down');
|
||||||
|
if (!in_array($status, ['ok', 'warn', 'down'], true)) {
|
||||||
|
$status = 'down';
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $check['message'] ?? null;
|
||||||
|
if (is_string($message) && $message !== '') {
|
||||||
|
$message = sanitizeError($message);
|
||||||
|
} else {
|
||||||
|
$message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checks[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'status' => $status,
|
||||||
|
'message' => $message,
|
||||||
|
'response_ms' => isset($check['response_ms']) ? (int) $check['response_ms'] : null,
|
||||||
|
'ssl_days_remaining' => isset($check['ssl_days_remaining']) ? (int) $check['ssl_days_remaining'] : null,
|
||||||
|
'checked_at' => isset($check['checked_at']) && is_string($check['checked_at']) ? $check['checked_at'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$boka = null;
|
||||||
|
if ($bokaData !== null) {
|
||||||
|
$lastPush = $bokaData['last_push'] ?? null;
|
||||||
|
$ageMinutes = null;
|
||||||
|
if (is_string($lastPush) && $lastPush !== '') {
|
||||||
|
$lastPushAt = strtotime($lastPush) ?: 0;
|
||||||
|
if ($lastPushAt > 0) {
|
||||||
|
$ageMinutes = (int) floor((time() - $lastPushAt) / 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$boka = [
|
||||||
|
'host' => isset($bokaData['host']) && is_string($bokaData['host']) ? $bokaData['host'] : 'boka',
|
||||||
|
'last_push' => is_string($lastPush) ? $lastPush : null,
|
||||||
|
'age_minutes' => $ageMinutes,
|
||||||
|
'disk_percent' => isset($bokaData['disk_percent']) ? (int) $bokaData['disk_percent'] : null,
|
||||||
|
'mem_available_mb' => isset($bokaData['mem_available_mb']) ? (int) $bokaData['mem_available_mb'] : null,
|
||||||
|
'containers_running' => isset($bokaData['containers_running']) ? (int) $bokaData['containers_running'] : null,
|
||||||
|
'containers_unhealthy' => isset($bokaData['containers_unhealthy']) ? (int) $bokaData['containers_unhealthy'] : null,
|
||||||
|
'backup_last_success' => isset($bokaData['backup_last_success']) && is_string($bokaData['backup_last_success'])
|
||||||
|
? $bokaData['backup_last_success']
|
||||||
|
: null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'initialized' => $checksData !== null || $bokaData !== null,
|
||||||
|
'message' => null,
|
||||||
|
'updated_at' => isset($checksData['updated_at']) && is_string($checksData['updated_at'])
|
||||||
|
? $checksData['updated_at']
|
||||||
|
: null,
|
||||||
|
'duration_ms' => isset($checksData['duration_ms']) ? (int) $checksData['duration_ms'] : null,
|
||||||
|
'checks' => $checks,
|
||||||
|
'boka' => $boka,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
RewriteRule ^status\.json$ status.php [L]
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?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: application/json; charset=utf-8');
|
||||||
|
header('X-Robots-Tag: noindex');
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
|
||||||
|
echo json_encode($status, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
# 06 — Landing Page 0012.de (Status Beacon)
|
# 06 — Status-Dashboard unter `/monitor/`
|
||||||
|
|
||||||
**Priorität:** medium
|
**Priorität:** medium
|
||||||
|
**Status:** erledigt (2026-06-12)
|
||||||
**Abhängigkeiten:** 03 (liefert `last-checks.json`)
|
**Abhängigkeiten:** 03 (liefert `last-checks.json`)
|
||||||
**Konzept:** [`../SITE.md`](../SITE.md)
|
**Konzept:** [`../SITE.md`](../SITE.md)
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
|
|
||||||
Öffentliche, minimale Startseite — Live-Status aus Monitoring-Daten, kein Leer-Domain-Eindruck.
|
Öffentliche, minimale Statusseite unter `/monitor/` — Live-Status aus Monitoring-Daten. Root (`index.php`) bleibt Baustellen-Platzhalter für eine spätere eigenständige Website.
|
||||||
|
|
||||||
## Checkliste
|
## Checkliste
|
||||||
|
|
||||||
- [ ] `public/index.php` — liest `data/last-checks.json` + `data/boka-status.json`
|
- [x] `public/monitor/index.php` — liest `private/data/last-checks.json` + `boka-status.json`
|
||||||
- [ ] Raster: ~12 Kacheln, Farbe nach Status (ok / warn / down)
|
- [x] `config/status-view.php` — gemeinsame Aufbereitung für HTML + JSON
|
||||||
- [ ] boka-Heartbeat-Zeile („last push X min ago“)
|
- [x] Raster: Kacheln, Farbe nach Status (ok / warn / down)
|
||||||
- [ ] Dark-Theme CSS inline oder eine `assets/style.css`
|
- [x] boka-Heartbeat-Zeile („last push X min ago“)
|
||||||
- [ ] Graceful fallback wenn JSON fehlt („Monitoring not initialized“)
|
- [x] Dark-Theme CSS inline
|
||||||
- [ ] Keine sensiblen Daten (Tokens, interne Fehlerdetails)
|
- [x] Graceful fallback wenn JSON fehlt („Monitoring not initialized“)
|
||||||
- [ ] Optional: `public/status.json` read-only API
|
- [x] Keine sensiblen Daten (Tokens, IPs, interne Test-Targets `_…`)
|
||||||
- [ ] Meta: `noindex` erwägen (Ops-Seite, keine SEO)
|
- [x] `public/monitor/status.php` read-only JSON-API (+ `.htaccess` → `status.json`)
|
||||||
|
- [x] Meta: `noindex` (Ops-Seite, keine SEO)
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
|
|
||||||
- `https://0012.de/` zeigt aktuellen Status nach Cron-Lauf
|
- `https://0012.de/monitor/` zeigt aktuellen Status nach Cron-Lauf
|
||||||
|
- `https://0012.de/` bleibt unveränderte Baustellenseite
|
||||||
- Seite lädt ohne JavaScript
|
- Seite lädt ohne JavaScript
|
||||||
- Mobile lesbar
|
- Mobile lesbar
|
||||||
|
|
||||||
## Nicht im Scope
|
## Nicht im Scope
|
||||||
|
|
||||||
|
- Monitoring auf `public/index.php`
|
||||||
- Blog, CMS, WordPress
|
- Blog, CMS, WordPress
|
||||||
|
- Link von Root zur Ops-Seite
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ Nummerierte Todos für den 0012.de-Webspace. Abhängigkeiten beachten.
|
|||||||
| 03 | [03-monitoring-pull-phase1.md](03-monitoring-pull-phase1.md) | **high** | `monitor.php`, HTTPS/SSL-Checks, Alarm |
|
| 03 | [03-monitoring-pull-phase1.md](03-monitoring-pull-phase1.md) | **high** | `monitor.php`, HTTPS/SSL-Checks, Alarm |
|
||||||
| 04 | [04-monitoring-push-boka.md](04-monitoring-push-boka.md) | **medium** | Push von boka → `ingest.php` — erledigt |
|
| 04 | [04-monitoring-push-boka.md](04-monitoring-push-boka.md) | **medium** | Push von boka → `ingest.php` — erledigt |
|
||||||
| 05 | [05-plesk-cron.md](05-plesk-cron.md) | **high** | Geplante Aufgaben in Plesk — erledigt |
|
| 05 | [05-plesk-cron.md](05-plesk-cron.md) | **high** | Geplante Aufgaben in Plesk — erledigt |
|
||||||
| 06 | [06-site-0012-landing.md](06-site-0012-landing.md) | **medium** | Status-Beacon Landing Page |
|
| 06 | [06-site-0012-landing.md](06-site-0012-landing.md) | **medium** | Status-Dashboard unter `/monitor/` — erledigt |
|
||||||
| 07 | [07-mail-domains-external.md](07-mail-domains-external.md) | **medium** | MX für Domains ohne Mailserver |
|
| 07 | [07-mail-domains-external.md](07-mail-domains-external.md) | **medium** | MX für Domains ohne Mailserver |
|
||||||
| 08 | [08-monitoring-phase2.md](08-monitoring-phase2.md) | **medium** | API-Health, Latenz, Backup-Heartbeat |
|
| 08 | [08-monitoring-phase2.md](08-monitoring-phase2.md) | **medium** | API-Health, Latenz, Backup-Heartbeat |
|
||||||
| 09 | [09-monitoring-phase3.md](09-monitoring-phase3.md) | **low** | Rest-Domains, Dashboard, Header-Checks |
|
| 09 | [09-monitoring-phase3.md](09-monitoring-phase3.md) | **low** | Rest-Domains, Dashboard, Header-Checks |
|
||||||
@@ -29,7 +29,7 @@ Todo **11** — boka SMTP-Relay; abhängig von erledigtem **02**, kein Blocker f
|
|||||||
## Empfohlene Reihenfolge
|
## Empfohlene Reihenfolge
|
||||||
|
|
||||||
```
|
```
|
||||||
01 → 02 → 05 ✓ → 03 ✓ → 06
|
01 → 02 → 05 ✓ → 03 ✓ → 06 ✓
|
||||||
↘ 04 ✓ (Push ingest.php)
|
↘ 04 ✓ (Push ingest.php)
|
||||||
07 (wenn Domains feststehen)
|
07 (wenn Domains feststehen)
|
||||||
08 → 09
|
08 → 09
|
||||||
|
|||||||
Reference in New Issue
Block a user