From d044afec2f72bb741c74d178aa76c5bb2a5bb4f7 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 31 Mar 2026 18:07:07 +0200 Subject: [PATCH] feat(status-page): add public status page at status.mana.how MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/generate-status-page.sh: Shell-Script das VictoriaMetrics abfragt und eine statische HTML-Statusseite generiert (probe_success + response times) - docker-compose.macmini.yml: mana-status-gen Container (Alpine, jq, curl) schreibt alle 60s nach /Volumes/ManaData/landings/status/ - docker/nginx/landings.conf: status.mana.how vHost mit Cache-Control: no-store - cloudflared-config.yml: status.mana.how → localhost:4400 Co-Authored-By: Claude Sonnet 4.6 --- cloudflared-config.yml | 4 + docker-compose.macmini.yml | 25 +++ docker/nginx/landings.conf | 22 ++ scripts/generate-status-page.sh | 370 ++++++++++++++++++++++++++++++++ 4 files changed, 421 insertions(+) create mode 100755 scripts/generate-status-page.sh diff --git a/cloudflared-config.yml b/cloudflared-config.yml index 4393cd900..88d84ed51 100644 --- a/cloudflared-config.yml +++ b/cloudflared-config.yml @@ -132,6 +132,10 @@ ingress: - hostname: whopxl.mana.how service: http://localhost:5100 + # Public Status Page (generated every 60s by mana-status-gen container) + - hostname: status.mana.how + service: http://localhost:4400 + # Monitoring & Tools - hostname: grafana.mana.how service: http://localhost:8000 diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index aae48ff3b..1edb98b56 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -1710,6 +1710,31 @@ services: retries: 3 start_period: 20s + status-page-gen: + image: alpine:3.20 + container_name: mana-status-gen + restart: always + mem_limit: 32m + depends_on: + victoriametrics: + condition: service_healthy + environment: + VICTORIAMETRICS_URL: http://victoriametrics:9090 + OUTPUT_FILE: /output/index.html + volumes: + - ./scripts/generate-status-page.sh:/generate.sh:ro + - /Volumes/ManaData/landings/status:/output + command: + - sh + - -c + - | + apk add --no-cache curl jq > /dev/null 2>&1 + mkdir -p /output + while true; do + sh /generate.sh || echo "$(date '+%H:%M:%S') Fehler beim Generieren" + sleep 60 + done + blackbox-exporter: image: prom/blackbox-exporter:v0.25.0 container_name: mana-mon-blackbox diff --git a/docker/nginx/landings.conf b/docker/nginx/landings.conf index 563109109..c43a72a31 100644 --- a/docker/nginx/landings.conf +++ b/docker/nginx/landings.conf @@ -106,3 +106,25 @@ server { index index.html; include /etc/nginx/snippets/landing-common.conf; } + +# status.mana.how — Public Status Page (generated by mana-status-gen every 60s) +server { + listen 80; + server_name status.mana.how; + root /srv/landings/status; + index index.html; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Cache-Control "no-store" always; + + location / { + try_files $uri /index.html; + } + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} diff --git a/scripts/generate-status-page.sh b/scripts/generate-status-page.sh new file mode 100755 index 000000000..9976cef70 --- /dev/null +++ b/scripts/generate-status-page.sh @@ -0,0 +1,370 @@ +#!/usr/bin/env sh +# generate-status-page.sh +# Fragt VictoriaMetrics ab und erzeugt eine statische HTML-Statusseite. +# Läuft in einem Alpine-Container im Docker-Netz (http://victoriametrics:9090) +# oder auf dem Host (http://localhost:9090). +# +# Ausgabe: /output/index.html (gemountet als /Volumes/ManaData/landings/status/) + +set -eu + +VM_URL="${VICTORIAMETRICS_URL:-http://victoriametrics:9090}" +OUTPUT="${OUTPUT_FILE:-/output/index.html}" +TMPDIR_LOCAL="$(mktemp -d)" +trap 'rm -rf "$TMPDIR_LOCAL"' EXIT + +# ── Daten aus VictoriaMetrics holen ──────────────────────────────────────── + +fetch_metric() { + curl -sf --max-time 10 \ + "${VM_URL}/api/v1/query?query=$(printf '%s' "$1" | sed 's/ /%20/g;s/{/%7B/g;s/}/%7D/g;s/=~/%3D~/g;s/|/%7C/g;s/"/%22/g')" \ + 2>/dev/null || echo '{"status":"error","data":{"result":[]}}' +} + +SUCCESS_JSON="$(fetch_metric 'probe_success{job=~"blackbox-web|blackbox-api|blackbox-infra|blackbox-gpu"}')" +DURATION_JSON="$(fetch_metric 'probe_duration_seconds{job=~"blackbox-web|blackbox-api|blackbox-infra|blackbox-gpu"}')" + +# ── Hilfsfunktionen ───────────────────────────────────────────────────────── + +# Gibt den probe_success-Wert für eine Instanz zurück (0 oder 1) +get_success() { + instance="$1" + echo "$SUCCESS_JSON" | jq -r --arg inst "$instance" \ + '.data.result[] | select(.metric.instance == $inst) | .value[1]' 2>/dev/null || echo "0" +} + +# Gibt die Antwortzeit in ms zurück +get_duration_ms() { + instance="$1" + val=$(echo "$DURATION_JSON" | jq -r --arg inst "$instance" \ + '.data.result[] | select(.metric.instance == $inst) | .value[1]' 2>/dev/null || echo "") + if [ -n "$val" ] && [ "$val" != "null" ]; then + printf "%.0f" "$(echo "$val * 1000" | awk '{printf "%.1f", $1}')" + else + echo "" + fi +} + +# Alle Instanzen einer Job-Gruppe, sortiert +get_instances() { + job="$1" + echo "$SUCCESS_JSON" | jq -r --arg job "$job" \ + '.data.result[] | select(.metric.job == $job) | .metric.instance' 2>/dev/null | sort +} + +# Freundlicher Name aus URL +friendly_name() { + url="$1" + # Entferne https:// und .mana.how + name="${url#https://}" + name="${name%.mana.how}" + # Entferne /health suffix + name="${name%/health}" + # Erster Buchstabe groß (POSIX-kompatibel) + printf '%s' "$name" | awk '{print toupper(substr($0,1,1)) substr($0,2)}' +} + +# Zählt UP-Dienste einer Job-Gruppe +count_up() { + job="$1" + echo "$SUCCESS_JSON" | jq -r --arg job "$job" \ + '[.data.result[] | select(.metric.job == $job) | .value[1]] | map(tonumber) | add // 0' \ + 2>/dev/null || echo "0" +} + +count_total() { + job="$1" + echo "$SUCCESS_JSON" | jq -r --arg job "$job" \ + '[.data.result[] | select(.metric.job == $job)] | length' \ + 2>/dev/null || echo "0" +} + +# ── Service-Rows HTML ──────────────────────────────────────────────────────── + +render_rows() { + job="$1" + instances="$(get_instances "$job")" + if [ -z "$instances" ]; then + printf 'Noch keine Daten — Blackbox Exporter lädt…\n' + return + fi + echo "$instances" | while IFS= read -r inst; do + [ -z "$inst" ] && continue + success="$(get_success "$inst")" + ms="$(get_duration_ms "$inst")" + name="$(friendly_name "$inst")" + if [ "$success" = "1" ]; then + status_class="up" + status_text="UP" + ms_html="${ms:+${ms}ms}" + else + status_class="down" + status_text="DOWN" + ms_html="" + fi + printf '%s%s%s %s\n' \ + "$status_class" "$status_class" "$name" "$inst" \ + "$status_text" \ + "${ms_html:-}" + done +} + +# ── Gesamtstatus ───────────────────────────────────────────────────────────── + +web_up="$(count_up blackbox-web)"; web_total="$(count_total blackbox-web)" +api_up="$(count_up blackbox-api)"; api_total="$(count_total blackbox-api)" +infra_up="$(count_up blackbox-infra)"; infra_total="$(count_total blackbox-infra)" +gpu_up="$(count_up blackbox-gpu)"; gpu_total="$(count_total blackbox-gpu)" + +total_up=$(( web_up + api_up + infra_up + gpu_up )) +total_all=$(( web_total + api_total + infra_total + gpu_total )) +total_down=$(( total_all - total_up )) + +if [ "$total_down" -eq 0 ] && [ "$total_all" -gt 0 ]; then + overall_class="all-good" + overall_icon="✓" + overall_text="Alle Systeme operational" +elif [ "$total_up" -gt $(( total_all / 2 )) ]; then + overall_class="partial" + overall_icon="⚠" + overall_text="Teilweise Beeinträchtigungen (${total_down} Dienste down)" +else + overall_class="outage" + overall_icon="✕" + overall_text="Größerer Ausfall (${total_down} von ${total_all} Diensten down)" +fi + +TIMESTAMP="$(date -u '+%d. %B %Y, %H:%M Uhr UTC')" + +# ── HTML generieren ────────────────────────────────────────────────────────── + +cat > "${OUTPUT}.tmp" << HTMLEOF + + + + + + + ManaCore Status + + + + + + +
+ +
+
+
${web_up}/${web_total}
+
Web Apps
+
+
+
${api_up}/${api_total}
+
API Backends
+
+
+
${infra_up}/${infra_total}
+
Infrastruktur
+
+
+
${gpu_up}/${gpu_total}
+
GPU Dienste
+
+
+ +
+
+

Web Apps

+ ${web_up} von ${web_total} online +
+ +$(render_rows blackbox-web) +
+
+ +
+
+

API Backends

+ ${api_up} von ${api_total} online +
+ +$(render_rows blackbox-api) +
+
+ +
+
+

Infrastruktur

+ ${infra_up} von ${infra_total} online +
+ +$(render_rows blackbox-infra) +
+
+ +
+
+

GPU Dienste

+ ${gpu_up} von ${gpu_total} online +
+ +$(render_rows blackbox-gpu) +
+
+ + + +
+ + +HTMLEOF + +mv "${OUTPUT}.tmp" "$OUTPUT" +echo "$(date '+%H:%M:%S') Status-Seite generiert → $OUTPUT (${total_up}/${total_all} online)"