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 '