diff --git a/MONITORING.md b/MONITORING.md index 03564d5..a419465 100644 --- a/MONITORING.md +++ b/MONITORING.md @@ -148,7 +148,10 @@ Siehe auch: [`todo/03-monitoring-pull-phase1.md`](todo/03-monitoring-pull-phase1 | Ort | Datei | Zweck | |-----|-------|--------| | 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 | `private/data/last-checks.json` | Letzte Pull-Check-Ergebnisse | | Webspace | `private/data/boka-status.json` | Letzter Push-Stand | | boka | `~/server/scripts/push-status.sh` | Sammelt Metriken, POST an Webspace | | boka | `~/server/config/push.env` | `INGEST_URL`, `PUSH_TOKEN` | diff --git a/README.md b/README.md index 8a5f2f3..e5d6fef 100644 --- a/README.md +++ b/README.md @@ -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 | | **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 | -| **Öffentliche Präsenz** | Minimale, aber sinnvolle Landing Page statt leerer Domain | +| **Öffentliche Präsenz** | Baustellen-Platzhalter auf `/`; Status-Dashboard unter `/monitor/` | ## Hosting @@ -53,7 +53,8 @@ Der Webspace sieht boka **von außen** — interne Metriken brauchen das Push-Sk ├── SITE.md # Konzept Landing Page ├── .env.example ├── 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 │ └── ingest.php # Empfang Push von boka ├── config/ # PHP-Konfiguration → deploy nach private/config/ diff --git a/SITE.md b/SITE.md index b1864c9..695d460 100644 --- a/SITE.md +++ b/SITE.md @@ -2,15 +2,22 @@ **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) -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 @@ -18,6 +25,6 @@ Noch unklar. Die Domain bleibt frei für etwas Eigenes — Portfolio, Experiment ## Was bewusst nicht auf 0012.de / -- Öffentliche Monitoring-Kacheln (→ Subdomain/Pfad) +- Öffentliche Monitoring-Kacheln (→ `/monitor/`) - IPs, Tokens, `.env`-Inhalte - Comic Sans (never again) diff --git a/config/status-view.php b/config/status-view.php new file mode 100644 index 0000000..1624263 --- /dev/null +++ b/config/status-view.php @@ -0,0 +1,103 @@ + $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, + ]; +} diff --git a/public/monitor/.htaccess b/public/monitor/.htaccess new file mode 100644 index 0000000..ff0ea9f --- /dev/null +++ b/public/monitor/.htaccess @@ -0,0 +1,2 @@ +RewriteEngine On +RewriteRule ^status\.json$ status.php [L] diff --git a/public/monitor/index.php b/public/monitor/index.php new file mode 100644 index 0000000..9b251da --- /dev/null +++ b/public/monitor/index.php @@ -0,0 +1,274 @@ + 0 + ? "{$containers} containers ({$unhealthy} unhealthy)" + : "{$containers} containers"; + } + + if ($details !== []) { + $line .= ' · ' . implode(' · ', $details); + } + + return $line; +} + +$jahr = (int) date('Y'); +?> + + + + + + + 0012 — status + + + +
+
+ +

External status · pull checks · boka push

+
+ + +
+
+
+ + +
+

boka heartbeat

+
+
+ +
+

Targets

+ +
No check results yet
+ +
+ + +
+ +
+ +

+ +
+ +
+
+ +
+ +
+ + + + +
+ + diff --git a/public/monitor/status.php b/public/monitor/status.php new file mode 100644 index 0000000..f19557d --- /dev/null +++ b/public/monitor/status.php @@ -0,0 +1,18 @@ +