Initial commit – TimeMaster Zeiterfassung & HR-Tool

Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-05-23 20:03:27 +02:00
commit 1fedd683e0
178 changed files with 29896 additions and 0 deletions
+628
View File
@@ -0,0 +1,628 @@
#!/bin/bash
# =============================================================================
# TimeMaster Umfassendes Update- und Health-Check-Script
# Server: root@192.168.1.137
# =============================================================================
# Phasen:
# 0 Pre-Flight Check
# 1 Backup (DB-Dump, pip freeze, 30-Tage-Retention)
# 2 System-Update (apt) + Erkennung PG/Python-Versionssprung
# 3 Python-Abhängigkeiten prüfen und aktualisieren
# 4 Post-Update Funktionstest (API-Endpunkte + nginx)
# 5 Performance-Test (curl, psql, redis-cli)
# 6 Zusammenfassung
# =============================================================================
# --- Globale Einstellungen ---------------------------------------------------
APP_DIR="/opt/timemaster"
BACKEND_DIR="$APP_DIR/backend"
VENV="$BACKEND_DIR/venv"
REQ="$BACKEND_DIR/requirements.txt"
BACKUP_DIR="/tank/backup"
DB_NAME="timemaster_db"
API_BASE="http://localhost:8000"
FRONTEND_BASE="http://localhost"
MIN_FREE_MB=500
PERF_RUNS=5 # Anzahl Messungen für Durchschnitt
EXIT_CODE=0 # wird auf 1 gesetzt bei Problemen
# --- Farben ------------------------------------------------------------------
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
# --- Hilfsfunktionen ---------------------------------------------------------
ok() { echo -e "${GREEN}$*${RESET}"; }
warn() { echo -e "${YELLOW}⚠️ $*${RESET}"; EXIT_CODE=1; }
err() { echo -e "${RED}$*${RESET}"; EXIT_CODE=1; }
info() { echo -e "${CYAN} $*${RESET}"; }
hdr() { echo -e "\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"; \
echo -e "${BOLD} $*${RESET}"; \
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"; }
# Misst Response-Zeit eines curl-Aufrufs in ms, gibt HTTP-Status und Zeit aus
# Aufruf: measure_curl <url>
# Ausgabe (stdout, TAB-getrennt): HTTP_STATUS<TAB>MS
measure_curl() {
local url="$1"
local result
result=$(curl -s -o /dev/null -w "%{http_code}\t%{time_total}" \
--connect-timeout 5 --max-time 10 "$url" 2>/dev/null || echo "000\t0")
local http_code ms_float
http_code=$(echo "$result" | cut -f1)
ms_float=$(echo "$result" | cut -f2)
# Sekunden → Millisekunden (ganzzahlig)
local ms
ms=$(awk "BEGIN {printf \"%d\", $ms_float * 1000}")
echo "${http_code}"$'\t'"${ms}"
}
# Durchschnitt aus N curl-Messungen (nur ms)
avg_curl_ms() {
local url="$1"
local n="${2:-$PERF_RUNS}"
local total=0
local i
for (( i=0; i<n; i++ )); do
local res
res=$(measure_curl "$url")
local ms
ms=$(echo "$res" | cut -f2)
total=$(( total + ms ))
done
echo $(( total / n ))
}
# Farbe für Performance-Ergebnis
perf_color() {
local ms="$1" threshold="$2"
if [[ "$ms" -lt "$threshold" ]]; then echo "${GREEN}";
elif [[ "$ms" -lt $(( threshold * 3 )) ]]; then echo "${YELLOW}";
else echo "${RED}"; fi
}
perf_label() {
local ms="$1" threshold="$2"
if [[ "$ms" -lt "$threshold" ]]; then echo "OK";
elif [[ "$ms" -lt $(( threshold * 3 )) ]]; then echo "LANGSAM";
else echo "FEHLER"; fi
}
# Sammle Zusammenfassungs-Zeilen
SUMMARY_LINES=()
summary_add() { SUMMARY_LINES+=("$1"); }
# =============================================================================
# PHASE 0 PRE-FLIGHT CHECK
# =============================================================================
hdr "Phase 0 · Pre-Flight Check"
PREFLIGHT_OK=true
# --- Services prüfen ---------------------------------------------------------
for SVC in timemaster nginx postgresql redis; do
if systemctl is-active --quiet "$SVC" 2>/dev/null; then
ok "Service $SVC läuft"
else
err "Service $SVC ist NICHT aktiv"
PREFLIGHT_OK=false
fi
done
# --- Festplattenplatz --------------------------------------------------------
FREE_MB=$(df -m / | awk 'NR==2 {print $4}')
if [[ "$FREE_MB" -ge "$MIN_FREE_MB" ]]; then
ok "Festplatte: ${FREE_MB} MB frei (Minimum: ${MIN_FREE_MB} MB)"
else
err "Zu wenig Festplattenplatz: ${FREE_MB} MB frei (Minimum: ${MIN_FREE_MB} MB)"
PREFLIGHT_OK=false
fi
# --- PostgreSQL erreichbar ---------------------------------------------------
if su -c "psql -d $DB_NAME -c 'SELECT 1' -q --tuples-only" postgres &>/dev/null; then
ok "PostgreSQL erreichbar (DB: $DB_NAME)"
else
err "PostgreSQL NICHT erreichbar (DB: $DB_NAME)"
PREFLIGHT_OK=false
fi
# --- Redis erreichbar --------------------------------------------------------
if redis-cli ping 2>/dev/null | grep -q "PONG"; then
ok "Redis erreichbar"
else
err "Redis NICHT erreichbar"
PREFLIGHT_OK=false
fi
# --- API Health-Endpoint -----------------------------------------------------
API_PRE=$(measure_curl "$API_BASE/health")
API_PRE_STATUS=$(echo "$API_PRE" | cut -f1)
if [[ "$API_PRE_STATUS" == "200" ]]; then
ok "API /health antwortet mit HTTP 200"
else
err "API /health antwortet NICHT (HTTP $API_PRE_STATUS)"
PREFLIGHT_OK=false
fi
# --- Abbruch bei Pre-Flight-Fehler ------------------------------------------
if ! $PREFLIGHT_OK; then
echo ""
err "Pre-Flight Check FEHLGESCHLAGEN. Script wird abgebrochen."
err "Diagnose-Befehle:"
err " journalctl -u timemaster -n 50"
err " journalctl -u nginx -n 20"
err " systemctl status postgresql"
err " redis-cli ping"
exit 1
fi
summary_add "${GREEN}Phase 0: Pre-Flight Check OK${RESET}"
# =============================================================================
# PHASE 1 BACKUP
# =============================================================================
hdr "Phase 1 · Backup"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# PostgreSQL Dump
DB_DUMP="$BACKUP_DIR/db_${TIMESTAMP}.dump"
info "Erstelle PostgreSQL-Dump → $DB_DUMP"
if su -c "pg_dump -Fc $DB_NAME" postgres > "$DB_DUMP" 2>/tmp/pg_dump_err; then
DUMP_SIZE=$(du -sh "$DB_DUMP" | cut -f1)
ok "DB-Dump erstellt: $DB_DUMP ($DUMP_SIZE)"
summary_add "${GREEN}Phase 1: DB-Backup OK ($DUMP_SIZE)${RESET}"
else
warn "DB-Dump FEHLGESCHLAGEN: $(cat /tmp/pg_dump_err)"
summary_add "${YELLOW}Phase 1: DB-Backup FEHLGESCHLAGEN${RESET}"
fi
# pip freeze sichern
PIP_FREEZE="$BACKUP_DIR/pip_freeze_${TIMESTAMP}.txt"
info "Sichere pip freeze → $PIP_FREEZE"
if "$VENV/bin/pip" freeze > "$PIP_FREEZE" 2>/dev/null; then
ok "pip freeze gesichert: $PIP_FREEZE"
else
warn "pip freeze konnte nicht gesichert werden"
fi
# 30-Tage-Retention (DB-Dumps)
info "Lösche DB-Dumps älter als 30 Tage..."
find "$BACKUP_DIR" -name "db_*.dump" -mtime +30 -delete 2>/dev/null && ok "Alte Backups bereinigt" || true
# pip-freeze-Dateien ebenfalls bereinigen
find "$BACKUP_DIR" -name "pip_freeze_*.txt" -mtime +30 -delete 2>/dev/null || true
# =============================================================================
# PHASE 2 SYSTEM-UPDATE (apt)
# =============================================================================
hdr "Phase 2 · System-Update (apt)"
# Versionen VOR Update sichern
PG_BEFORE=$(pg_lsclusters -h 2>/dev/null | awk 'NR==1 {print $1}' || echo "unbekannt")
PYTHON_BEFORE_FULL=$(grep "^version = " "$VENV/pyvenv.cfg" 2>/dev/null | cut -d' ' -f3 || echo "unbekannt")
PYTHON_BEFORE_MAJOR=$(echo "$PYTHON_BEFORE_FULL" | cut -d. -f1-2)
info "PostgreSQL vor Update : $PG_BEFORE"
info "Python (venv) vor Update: $PYTHON_BEFORE_FULL (Major: $PYTHON_BEFORE_MAJOR)"
# apt-Operationen (kein set -e hier, damit Fehler nicht alles abbricht)
info "Führe apt update aus..."
APT_UPDATE_OUT=$(apt update 2>&1)
APT_UPDATE_RC=$?
if [[ $APT_UPDATE_RC -eq 0 ]]; then
ok "apt update erfolgreich"
else
warn "apt update mit Warnungen: $(echo "$APT_UPDATE_OUT" | tail -3)"
fi
info "Führe apt upgrade -y aus (kann etwas dauern)..."
APT_UPGRADE_OUT=$(DEBIAN_FRONTEND=noninteractive apt upgrade -y 2>&1)
APT_UPGRADE_RC=$?
# Zähle aktualisierte Pakete
APT_UPGRADED=$(echo "$APT_UPGRADE_OUT" | grep -oP '^\d+ upgraded' | grep -oP '^\d+' || echo "0")
APT_INSTALLED=$(echo "$APT_UPGRADE_OUT" | grep -oP '\d+ newly installed' | grep -oP '^\d+' || echo "0")
if [[ $APT_UPGRADE_RC -eq 0 ]]; then
ok "apt upgrade erfolgreich (${APT_UPGRADED:-0} Pakete aktualisiert, ${APT_INSTALLED:-0} neu installiert)"
summary_add "${GREEN}Phase 2: apt upgrade OK (${APT_UPGRADED:-0} Pakete)${RESET}"
else
warn "apt upgrade mit Fehler (RC=$APT_UPGRADE_RC)"
summary_add "${YELLOW}Phase 2: apt upgrade mit Warnungen${RESET}"
fi
info "Führe apt autoremove -y aus..."
DEBIAN_FRONTEND=noninteractive apt autoremove -y &>/dev/null && ok "apt autoremove OK" || warn "apt autoremove mit Fehler"
# --- PostgreSQL Major-Versionssprung erkennen --------------------------------
PG_AFTER=$(pg_lsclusters -h 2>/dev/null | awk 'NR==1 {print $1}' || echo "unbekannt")
info "PostgreSQL nach Update: $PG_AFTER"
if [[ "$PG_BEFORE" != "$PG_AFTER" && "$PG_BEFORE" != "unbekannt" ]]; then
warn "PostgreSQL Major-Versionssprung erkannt: $PG_BEFORE$PG_AFTER"
echo ""
echo -e "${YELLOW} Das Backup liegt in: $BACKUP_DIR${RESET}"
echo -e "${YELLOW} Verfügbare Cluster:${RESET}"
pg_lsclusters
echo ""
read -r -p " pg_upgradecluster $PG_BEFORE main → $PG_AFTER jetzt ausführen? [j/N] " PG_ANSWER
if [[ "$PG_ANSWER" =~ ^[jJ]$ ]]; then
info "Stoppe timemaster Service..."
systemctl stop timemaster
info "Führe pg_upgradecluster $PG_BEFORE main aus..."
if pg_upgradecluster "$PG_BEFORE" main; then
ok "pg_upgradecluster erfolgreich"
summary_add "${GREEN}Phase 2: PostgreSQL-Cluster-Upgrade $PG_BEFORE$PG_AFTER OK${RESET}"
else
err "pg_upgradecluster FEHLGESCHLAGEN manuell eingreifen!"
summary_add "${RED}Phase 2: PostgreSQL-Upgrade FEHLGESCHLAGEN${RESET}"
fi
info "Starte timemaster Service..."
systemctl start timemaster
sleep 3
else
warn "pg_upgradecluster übersprungen. Alter Cluster $PG_BEFORE ist noch aktiv!"
warn "Bitte manuell migrieren: pg_upgradecluster $PG_BEFORE main"
summary_add "${YELLOW}Phase 2: PostgreSQL-Upgrade übersprungen${RESET}"
fi
else
ok "PostgreSQL: kein Major-Versionssprung ($PG_AFTER)"
summary_add "${GREEN}Phase 2: PostgreSQL stabil (Version $PG_AFTER)${RESET}"
fi
# --- Python Major-Versionssprung erkennen ------------------------------------
PYTHON_AFTER_FULL=$(python3 --version 2>/dev/null | cut -d' ' -f2 || echo "unbekannt")
PYTHON_AFTER_MAJOR=$(echo "$PYTHON_AFTER_FULL" | cut -d. -f1-2)
info "Python nach Update: $PYTHON_AFTER_FULL (Major: $PYTHON_AFTER_MAJOR)"
if [[ "$PYTHON_BEFORE_MAJOR" != "$PYTHON_AFTER_MAJOR" && "$PYTHON_BEFORE_MAJOR" != "unbekannt" ]]; then
warn "Python Major-Versionssprung: $PYTHON_BEFORE_MAJOR$PYTHON_AFTER_MAJOR"
info "Baue venv automatisch neu..."
systemctl stop timemaster || true
cd "$BACKEND_DIR"
mv venv "venv.old.${PYTHON_BEFORE_MAJOR}" 2>/dev/null || true
if python3 -m venv venv; then
"$VENV/bin/pip" install --upgrade pip -q
if "$VENV/bin/pip" install -r "$REQ" -q; then
ok "venv neu gebaut mit Python $PYTHON_AFTER_FULL"
summary_add "${GREEN}Phase 2: venv neu gebaut (Python $PYTHON_BEFORE_MAJOR$PYTHON_AFTER_MAJOR)${RESET}"
else
err "pip install -r requirements.txt FEHLGESCHLAGEN"
summary_add "${RED}Phase 2: venv-Neubau FEHLGESCHLAGEN${RESET}"
fi
else
err "python3 -m venv fehlgeschlagen"
summary_add "${RED}Phase 2: venv-Neubau FEHLGESCHLAGEN${RESET}"
fi
systemctl start timemaster || true
sleep 3
else
ok "Python: kein Major-Versionssprung ($PYTHON_AFTER_MAJOR)"
summary_add "${GREEN}Phase 2: Python stabil (Version $PYTHON_AFTER_FULL)${RESET}"
fi
# =============================================================================
# PHASE 3 PYTHON-ABHÄNGIGKEITEN PRÜFEN UND AKTUALISIEREN
# =============================================================================
hdr "Phase 3 · Python-Abhängigkeiten"
if [[ ! -f "$REQ" ]]; then
warn "requirements.txt nicht gefunden: $REQ"
summary_add "${YELLOW}Phase 3: requirements.txt fehlt${RESET}"
else
# Installierte Pakete als assoziatives Array: name → version (lowercase)
# pip list --format=freeze liefert "Paketname==Version" cut auf erstes == aufteilen
declare -A INSTALLED_PKGS
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^# ]] && continue
pkg=$(echo "$line" | cut -d= -f1)
ver=$(echo "$line" | cut -d= -f3) # f1=name, f2="" (wegen ==), f3=version
pname=$(echo "$pkg" | tr '[:upper:]' '[:lower:]' | tr '_' '-' | xargs)
pver=$(echo "$ver" | xargs)
[[ -n "$pname" && -n "$pver" ]] && INSTALLED_PKGS["$pname"]="$pver"
done < <("$VENV/bin/pip" list --format=freeze 2>/dev/null)
MISSING_PKGS=()
WRONG_VERSION_PKGS=()
OK_PKGS=0
# requirements.txt parsen (ignoriere Kommentare, leere Zeilen, git+-Zeilen)
while IFS= read -r line; do
# Leere Zeilen und Kommentare überspringen
[[ -z "$line" || "$line" =~ ^# ]] && continue
# git+https etc. überspringen
[[ "$line" =~ ^-e|^git\+ ]] && continue
# Paketname und Version extrahieren
req_name=$(echo "$line" | grep -oP '^[A-Za-z0-9_.-]+' | tr '[:upper:]' '[:lower:]' | tr '_' '-')
req_op=$(echo "$line" | grep -oP '(==|>=|<=|~=|!=)' | head -1 || true)
req_ver=$(echo "$line" | grep -oP '(==|>=|<=|~=|!=)\K[0-9A-Za-z._-]+' | head -1 || true)
[[ -z "$req_name" ]] && continue
inst_ver="${INSTALLED_PKGS[$req_name]:-}"
if [[ -z "$inst_ver" ]]; then
MISSING_PKGS+=("$line")
elif [[ -n "$req_op" && "$req_op" == "==" && "$inst_ver" != "$req_ver" ]]; then
WRONG_VERSION_PKGS+=("$req_name==$req_ver (installiert: $inst_ver)")
else
(( OK_PKGS++ )) || true
fi
done < "$REQ"
ok "$OK_PKGS Pakete bereits korrekt installiert"
CHANGED_PKGS=()
if [[ ${#MISSING_PKGS[@]} -gt 0 ]]; then
echo -e "${YELLOW}⚠️ ${#MISSING_PKGS[@]} fehlende Pakete gefunden werden jetzt installiert:${RESET}"
for p in "${MISSING_PKGS[@]}"; do info " + $p"; done
info "Stoppe timemaster Service für pip install..."
systemctl stop timemaster || true
INSTALL_FAILED=()
for p in "${MISSING_PKGS[@]}"; do
info " Installiere: $p"
if "$VENV/bin/pip" install "$p" -q; then
ok " Installiert: $p"
CHANGED_PKGS+=("+ $p")
else
err " FEHLER bei Installation: $p"
INSTALL_FAILED+=("$p")
fi
done
info "Starte timemaster Service..."
systemctl start timemaster || true
sleep 3
# Nur Exit-Code setzen wenn Installation tatsächlich fehlschlug
if [[ ${#INSTALL_FAILED[@]} -gt 0 ]]; then
warn "${#INSTALL_FAILED[@]} Pakete konnten NICHT installiert werden"
else
ok "Alle fehlenden Pakete erfolgreich nachinstalliert"
fi
else
ok "Keine fehlenden Pakete"
fi
if [[ ${#WRONG_VERSION_PKGS[@]} -gt 0 ]]; then
echo -e "${YELLOW}⚠️ ${#WRONG_VERSION_PKGS[@]} Pakete mit falscher Version werden aktualisiert:${RESET}"
for p in "${WRONG_VERSION_PKGS[@]}"; do info " ~ $p"; done
# Service nur stoppen wenn nicht schon gestoppt
if systemctl is-active --quiet timemaster 2>/dev/null; then
info "Stoppe timemaster Service für pip upgrade..."
systemctl stop timemaster || true
RESTART_AFTER_PIP=true
else
RESTART_AFTER_PIP=false
fi
UPGRADE_FAILED=()
for entry in "${WRONG_VERSION_PKGS[@]}"; do
pkg_spec=$(echo "$entry" | cut -d' ' -f1)
info " Aktualisiere: $pkg_spec"
if "$VENV/bin/pip" install "$pkg_spec" -q; then
ok " Aktualisiert: $pkg_spec"
CHANGED_PKGS+=("~ $pkg_spec")
else
err " FEHLER beim Update: $pkg_spec"
UPGRADE_FAILED+=("$pkg_spec")
fi
done
if $RESTART_AFTER_PIP; then
info "Starte timemaster Service..."
systemctl start timemaster || true
sleep 3
fi
if [[ ${#UPGRADE_FAILED[@]} -gt 0 ]]; then
warn "${#UPGRADE_FAILED[@]} Pakete konnten NICHT aktualisiert werden"
else
ok "Alle Versionskonflikte erfolgreich behoben"
fi
else
ok "Alle Paketversionen korrekt"
fi
if [[ ${#CHANGED_PKGS[@]} -gt 0 ]]; then
summary_add "${GREEN}Phase 3: ${#CHANGED_PKGS[@]} Pakete geändert:${RESET}"
for c in "${CHANGED_PKGS[@]}"; do
summary_add " ${CYAN}$c${RESET}"
done
else
summary_add "${GREEN}Phase 3: Keine Paketänderungen notwendig${RESET}"
fi
fi
# =============================================================================
# PHASE 4 POST-UPDATE FUNKTIONSTEST
# =============================================================================
hdr "Phase 4 · Post-Update Funktionstest"
# Service sicherheitshalber aktivieren
if ! systemctl is-active --quiet timemaster 2>/dev/null; then
info "Starte timemaster Service..."
systemctl start timemaster || true
sleep 5
fi
FUNC_OK=true
# GET /health → 200
RES=$(measure_curl "$API_BASE/health")
STATUS=$(echo "$RES" | cut -f1)
MS=$(echo "$RES" | cut -f2)
if [[ "$STATUS" == "200" ]]; then
ok "GET /health → HTTP $STATUS (${MS}ms)"
else
err "GET /health → HTTP $STATUS (erwartet: 200)"
FUNC_OK=false
fi
# GET /api/v1/auth/me → 401 (kein Token)
RES=$(measure_curl "$API_BASE/api/v1/auth/me")
STATUS=$(echo "$RES" | cut -f1)
MS=$(echo "$RES" | cut -f2)
if [[ "$STATUS" == "401" ]]; then
ok "GET /api/v1/auth/me → HTTP $STATUS (kein Token = korrekt, ${MS}ms)"
else
err "GET /api/v1/auth/me → HTTP $STATUS (erwartet: 401)"
FUNC_OK=false
fi
# GET / nginx Frontend → 200 mit HTML
NGINX_RES=$(curl -s -o /tmp/nginx_root.html -w "%{http_code}" \
--connect-timeout 5 --max-time 10 "$FRONTEND_BASE/" 2>/dev/null || echo "000")
if [[ "$NGINX_RES" == "200" ]]; then
# Prüfe ob HTML zurückkommt
if grep -qi "<html" /tmp/nginx_root.html 2>/dev/null; then
ok "GET / → HTTP 200 mit HTML (nginx liefert Frontend aus)"
else
warn "GET / → HTTP 200, aber kein HTML-Inhalt (prüfe nginx-Konfiguration)"
fi
else
err "GET / → HTTP $NGINX_RES (nginx Frontend nicht erreichbar)"
FUNC_OK=false
fi
rm -f /tmp/nginx_root.html
if $FUNC_OK; then
summary_add "${GREEN}Phase 4: Alle Funktionstests bestanden${RESET}"
else
summary_add "${RED}Phase 4: Funktionstests FEHLGESCHLAGEN${RESET}"
fi
# =============================================================================
# PHASE 5 PERFORMANCE-TEST
# =============================================================================
hdr "Phase 5 · Performance-Test (je $PERF_RUNS Messungen)"
# Tabellen-Header
printf "\n${BOLD}%-40s %14s %10s${RESET}\n" "Endpunkt / Ressource" "Ø Zeit (ms)" "Status"
printf "%-40s %14s %10s\n" "$(printf '%0.s─' {1..40})" "$(printf '%0.s─' {1..14})" "$(printf '%0.s─' {1..10})"
PERF_ISSUES=0
# Funktion: Zeile in Tabelle ausgeben
print_perf_row() {
local label="$1" ms="$2" threshold="$3"
local label_status
label_status=$(perf_label "$ms" "$threshold")
local col
col=$(perf_color "$ms" "$threshold")
if [[ "$label_status" != "OK" ]]; then (( PERF_ISSUES++ )) || true; fi
printf "${col}%-40s %14s %10s${RESET}\n" "$label" "${ms}ms" "$label_status"
}
# GET /health (Schwellwert 100ms)
info "Messe GET /health ($PERF_RUNS Runs)..."
HEALTH_MS=$(avg_curl_ms "$API_BASE/health")
print_perf_row "GET /health" "$HEALTH_MS" 100
# GET /api/v1/auth/me (Schwellwert 200ms)
info "Messe GET /api/v1/auth/me ($PERF_RUNS Runs)..."
AUTH_MS=$(avg_curl_ms "$API_BASE/api/v1/auth/me")
print_perf_row "GET /api/v1/auth/me" "$AUTH_MS" 200
# PostgreSQL SELECT 1 (Schwellwert 100ms)
info "Messe PostgreSQL SELECT 1 ($PERF_RUNS Runs)..."
PG_TOTAL=0
for (( i=0; i<PERF_RUNS; i++ )); do
PG_START=$( date +%s%N )
su -c "psql -d $DB_NAME -c 'SELECT 1' -q --tuples-only" postgres &>/dev/null || true
PG_END=$( date +%s%N )
PG_DIFF=$(( (PG_END - PG_START) / 1000000 ))
PG_TOTAL=$(( PG_TOTAL + PG_DIFF ))
done
PG_AVG=$(( PG_TOTAL / PERF_RUNS ))
print_perf_row "PostgreSQL SELECT 1" "$PG_AVG" 100
# Redis PING (Schwellwert 50ms)
info "Messe Redis PING ($PERF_RUNS Runs)..."
REDIS_TOTAL=0
for (( i=0; i<PERF_RUNS; i++ )); do
R_START=$( date +%s%N )
redis-cli ping &>/dev/null || true
R_END=$( date +%s%N )
R_DIFF=$(( (R_END - R_START) / 1000000 ))
REDIS_TOTAL=$(( REDIS_TOTAL + R_DIFF ))
done
REDIS_AVG=$(( REDIS_TOTAL / PERF_RUNS ))
# Redis PONG prüfen
REDIS_PING_RESULT=$(redis-cli ping 2>/dev/null || echo "FEHLER")
if [[ "$REDIS_PING_RESULT" != "PONG" ]]; then
printf "${RED}%-40s %14s %10s${RESET}\n" "Redis PING" "${REDIS_AVG}ms" "FEHLER"
(( PERF_ISSUES++ )) || true
else
print_perf_row "Redis PING" "$REDIS_AVG" 50
fi
echo ""
if [[ $PERF_ISSUES -eq 0 ]]; then
ok "Performance: Alle Werte im grünen Bereich"
summary_add "${GREEN}Phase 5: Performance OK (health ${HEALTH_MS}ms, auth/me ${AUTH_MS}ms, PG ${PG_AVG}ms, Redis ${REDIS_AVG}ms)${RESET}"
else
warn "Performance: $PERF_ISSUES Wert(e) außerhalb des Schwellwerts"
summary_add "${YELLOW}Phase 5: Performance-Warnung ($PERF_ISSUES langsame Werte)${RESET}"
fi
# =============================================================================
# PHASE 6 ZUSAMMENFASSUNG
# =============================================================================
hdr "Phase 6 · Zusammenfassung"
echo ""
for line in "${SUMMARY_LINES[@]}"; do
echo -e " $line"
done
echo ""
echo -e "${BOLD}Service-Status:${RESET}"
for SVC in timemaster nginx postgresql redis; do
if systemctl is-active --quiet "$SVC" 2>/dev/null; then
ok " $SVC"
else
err " $SVC (NICHT aktiv)"
fi
done
echo ""
echo -e "${BOLD}Log-Befehle (bei Problemen):${RESET}"
echo -e " ${CYAN}journalctl -u timemaster -n 100 --no-pager${RESET}"
echo -e " ${CYAN}journalctl -u nginx -n 50 --no-pager${RESET}"
echo -e " ${CYAN}journalctl -u postgresql -n 50 --no-pager${RESET}"
echo -e " ${CYAN}journalctl -u redis -n 20 --no-pager${RESET}"
echo -e " ${CYAN}pg_lsclusters${RESET}"
echo ""
echo -e "${BOLD}Backups:${RESET}"
echo -e " Verzeichnis: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"/*.dump 2>/dev/null | tail -5 || echo " (keine Dumps gefunden)"
echo ""
FINISH_TIME=$(date)
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
if [[ $EXIT_CODE -eq 0 ]]; then
echo -e "${GREEN}${BOLD}✅ Update & Health-Check ERFOLGREICH abgeschlossen.${RESET}"
else
echo -e "${YELLOW}${BOLD}⚠️ Update & Health-Check abgeschlossen PROBLEME GEFUNDEN.${RESET}"
echo -e "${YELLOW} Bitte obige Fehlermeldungen und Log-Befehle prüfen.${RESET}"
fi
echo -e " Fertig: $FINISH_TIME"
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
exit $EXIT_CODE