#!/usr/bin/env python3 """ BAN YARO — Quarterly Report Generator Aufruf: python3 scripts/generate_reports.py
Sections: sicherheit | funktionsumfang | dateien | nutzer | partner | server | all """ import os import sys import sqlite3 import subprocess from datetime import datetime from pathlib import Path DB_PATH = os.getenv("DB_PATH", "/data/banyaro.db") MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") APP_DIR = "/app" NOW = datetime.now() DATE_STR = NOW.strftime("%d.%m.%Y %H:%M") ISO_DATE = NOW.strftime("%Y-%m-%d") # ────────────────────────────────────────────────────────────────────────────── # Hilfsfunktionen # ────────────────────────────────────────────────────────────────────────────── def db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def q(sql, params=()): try: with db() as conn: return conn.execute(sql, params).fetchall() except Exception as e: return [] def q1(sql, params=()): rows = q(sql, params) return rows[0] if rows else None def val(sql, params=(), default=0): row = q1(sql, params) if row is None: return default return row[0] def sh(cmd): try: r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10) return r.stdout.strip() except Exception: return "(nicht verfügbar)" def hr(): return "\n---\n" def h(level, text): return f"\n{'#' * level} {text}\n" def table(headers, rows): col_widths = [len(h) for h in headers] for row in rows: for i, cell in enumerate(row): if i < len(col_widths): col_widths[i] = max(col_widths[i], len(str(cell))) sep = "| " + " | ".join("-" * w for w in col_widths) + " |" hdr = "| " + " | ".join(str(h).ljust(col_widths[i]) for i, h in enumerate(headers)) + " |" lines = [hdr, sep] for row in rows: line = "| " + " | ".join(str(row[i] if i < len(row) else "").ljust(col_widths[i]) for i in range(len(headers))) + " |" lines.append(line) return "\n".join(lines) def bytes_human(b): for unit in ("B", "KB", "MB", "GB"): if b < 1024: return f"{b:.1f} {unit}" b /= 1024 return f"{b:.1f} TB" # ────────────────────────────────────────────────────────────────────────────── # 1 SICHERHEITSBERICHT # ────────────────────────────────────────────────────────────────────────────── def report_sicherheit(): # Aktive Bans aus DB banned = val("SELECT COUNT(*) FROM users WHERE is_banned=1") unverifiziert = val("SELECT COUNT(*) FROM users WHERE email_verified=0") outreach_rows = q("SELECT COUNT(*) as n, from_account FROM outreach_log GROUP BY from_account") lines = [ f"# Sicherheitsbericht — Ban Yaro", f"\n_Erstellt: {DATE_STR}_\n", hr(), h(2, "Übersicht implementierter Schutzmaßnahmen"), h(3, "1. Authentifizierung & Passwörter"), "- **JWT** (HS256) mit 30-Tage-Ablauf, HttpOnly + Secure + SameSite=lax Cookie", "- **Bcrypt**-Passwort-Hashing mit automatischem Salt", "- Mindestlänge 8 Zeichen, serverseitig erzwungen", "- Passwort-Reset: kryptographisches Token, 2 Stunden Ablauf", "", h(3, "2. Registrierung"), "- **E-Mail-Verifikation** zwingend vor dem ersten Login", "- Verifikationslink läuft nach 7 Tagen ab", "- Rate Limit: 5 Registrierungen / Stunde / IP", "- Username-Blocklist: >200 reservierte und unangemessene Begriffe", "- Keine Doppelanmeldung (E-Mail und Username unique)", "", h(3, "3. Login-Schutz"), "- **IP-Rate-Limit**: 10 Versuche / 5 Minuten", "- **Email-Rate-Limit**: 5 Versuche / 5 Minuten pro E-Mail-Adresse", "- **Account-Lockout**: 5 Fehlversuche → 15 Minuten gesperrt (in-memory)", "- Fehlerzähler wird bei erfolgreichem Login zurückgesetzt", "- Gleiche Fehlermeldung bei falschem Passwort UND unbekannter E-Mail (kein User-Enumeration)", "", h(3, "4. Forum-Schutz"), "- E-Mail-Verifikation Pflicht zum Posten", "- **Post-Cooldown**: 30 Sekunden zwischen beliebigen Beiträgen", "- **Stunden-Limit Threads**: max. 5 neue Threads / Stunde / User", "- **Stunden-Limit Antworten**: max. 20 Antworten / Stunde / User", "- **Duplikat-Erkennung**: gleicher Text in 5 Minuten → blockiert", "- **Content-Filter**: Spam-Keywords, URL-Sperre für Accounts < 7 Tage, Sonderzeichen-Ratio", "- Moderatoren können Threads sperren, Beiträge löschen (Soft-Delete)", "- Report-System: User können Beiträge melden", "", h(3, "5. HTTP-Security-Headers"), "| Header | Wert |", "|--------|------|", "| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |", "| `Content-Security-Policy` | default-src 'self'; frame-ancestors 'none'; … |", "| `X-Content-Type-Options` | `nosniff` |", "| `Referrer-Policy` | `strict-origin-when-cross-origin` |", "| `Permissions-Policy` | camera=(), microphone=(), geolocation=(self) |", "", h(3, "6. Rate Limiting (alle Endpunkte)"), table( ["Endpunkt", "Limit", "Fenster"], [ ["/auth/register", "5 Req", "60 Min"], ["/auth/login (IP)", "10 Req", "5 Min"], ["/auth/login (Email)", "5 Req", "5 Min"], ["/auth/forgot-password", "3 Req", "60 Min"], ["/auth/resend-verification", "3 Req", "60 Min / Email"], ["/auth/reset-password", "5 Req", "60 Min"], ["KI-Features", "10 Req", "60 Min"], ["Poison-Reports", "3 Req", "60 Min"], ["Wiki-Liste", "60 Req", "60 Sek"], ["Wiki-Detail", "30 Req", "60 Sek"], ] ), "", h(3, "7. Honeypot-Fallen"), "Folgende Pfade blockieren Scanner-IPs sofort für 24 Stunden:", "", "```", "/api/admin/users /api/v1/users /api/users /api/.env", "/api/config /api/setup /api/install /api/phpinfo", "/api/debug /api/actuator /api/swagger /api/graphql", "/api/wiki/trap", "```", "", h(3, "8. Datei-Upload-Sicherheit"), "- **Magic-Byte-Prüfung**: JPEG, PNG, GIF, WebP, MP4, WebM", "- **Path-Traversal-Schutz**: alle Pfade bleiben innerhalb `MEDIA_DIR`", "- **Größenbeschränkung**: 20 MB globales Limit (Middleware)", "- Automatische Konvertierung: HEIC→JPEG, MOV/AVI→MP4", "- Max. 5 Fotos pro Forum-Thread", "", h(3, "9. Admin & Moderation"), "- Admin-Endpoints per `require_admin` Dependency geschützt", "- Moderatoren-Rolle mit eingeschränkten Rechten", "- User-Banning mit Sperrgrund, geprüft bei jedem Request", "- Outreach-Mailing nur über Admin-Panel, vollständiges Log", "", h(2, "Aktuelle Kennzahlen"), table( ["Metrik", "Wert"], [ ["Gesperrte Accounts", str(banned)], ["Unverifizierte Accounts", str(unverifiziert)], ["Gesendete Outreach-Mails", str(sum(r[0] for r in outreach_rows))], ] ), "", h(2, "Bekannte Einschränkungen"), "- Rate-Limit-Daten und IP-Blocklist sind **in-memory** → Reset bei Container-Neustart", "- Kein CAPTCHA (bewusst: Nutzerfreundlichkeit vs. Bot-Schutz)", "- Keine Refresh-Token-Rotation (JWT ist 30 Tage gültig)", "- Analytics (Besucher) extern über Umami — kein Zugriff aus dem Container", "", h(2, "Empfehlungen für nächste Überprüfung"), "- [ ] Prüfen ob IP-Blocklist-Persistenz via DB sinnvoll wäre", "- [ ] CSP weiter verschärfen (nonce-basiert statt unsafe-inline)", "- [ ] Login-Logs in DB schreiben (für Audit-Trail)", "- [ ] Zwei-Faktor-Authentifizierung für Admin-Accounts evaluieren", ] return "\n".join(lines) # ────────────────────────────────────────────────────────────────────────────── # 2 FUNKTIONSUMFANG # ────────────────────────────────────────────────────────────────────────────── def report_funktionsumfang(): BEREICHE = [ ("Authentifizierung", [ "Registrierung mit E-Mail-Verifikation", "Login / Logout (JWT + HttpOnly-Cookie)", "Passwort vergessen / zurücksetzen", "Verifikations-Mail erneut senden", "Referral-System (3 Stufen: 10/20/50 Refs → 20/30/50 % Rabatt)", "Partner-Codes (Gründer-Slot, eigene Einladungen)", ]), ("Hunde-Profile", [ "Anlegen / Bearbeiten von Hunde-Profilen (Rasse, Geburtsdatum, Gewicht, …)", "Avatar-Upload (JPEG/WebP-Konvertierung, Vorschau)", "Öffentliches Profil mit QR-Code und Teilen-Link", "Hunde-Ausweis (druckbares HTML-Dokument)", "Mehrere Hunde pro Account", ]), ("Forum", [ "Thread erstellen mit Kategorien (allgemein, rasse, region, …)", "Antworten, Likes, Foto-Anhänge (max. 5 pro Thread)", "Moderatoren: Thread pinnen, sperren, löschen", "Report-System: Beiträge melden", "Push-Benachrichtigungen bei neuer Antwort", "Öffentlich lesbar, Schreiben nur für verifizierte User", ]), ("Tagebuch", [ "Tageseinträge mit Freitext, Fotos, GPS-Koordinaten", "EXIF-GPS-Extraktion aus Foto-Uploads", "Kartenansicht aller Tagebuch-Pins", "Kalenderansicht nach Datum", "Medienansicht (Galerie aller Fotos)", "Day-One-kompatibles Format", ]), ("Gesundheit & Training", [ "Gewichtsverlauf mit Diagramm", "Gesundheits-Erinnerungen (Push, täglich 08:00)", "104 Übungen (DB-basiert, KI-Trainingspläne)", "Training-Logging mit Fortschrittsverfolgung", "KI-Gesundheitsberichte (wöchentlich, cloud/lokal)", ]), ("Karte & POIs", [ "Leaflet-Karte mit Cluster-Markern", "Nearby-Alerts: Giftköder, Vermisste Hunde in der Nähe", "Overpass-API-Integration (Tierärzte, Hundewiesen, Parks, …)", "90-Tage-Cache für Overpass-Abfragen", "ORS-Routenvorschläge zu Hundeparks", ]), ("Wiki & Rassen", [ "Rassen-Datenbank (TheDogAPI + Wikidata-Enrichment)", "Züchter-Verzeichnis mit Verifikation", "Breed-Interest-Tracking ('So einen hab ich' / 'Interessiert mich')", "KI-gestützte Rassen-Anreicherung", "Wikipedia-basierte Beschreibungen", ]), ("Züchter-Features", [ "Züchter-Antrag mit Dokument-Upload", "Admin-Prüfung und Freischaltung", "Züchter-Profil (Zwingername, Rassen, VDH, Stadt)", "Wurfverwaltung mit Elterntieren, Welpen, Fotos", "Tierschutz-Check vor Wurf-Anlage", "Stammbaum-Ansicht", "Genetik-Tracking (Farbgene, Erbkrankheiten)", "Kaufvertrags-Generator", "Jahresbericht-Export", ]), ("Social Features", [ "Freundschaften (anfragen, annehmen, ablehnen)", "Social-Media-Posts (Luna — KI-Social-Manager)", "Lober: wöchentlicher KI-Lob-Push (Mo 09:00)", "Benachrichtigungen (in-app + Push-Notifications)", ]), ("Admin & Moderation", [ "Admin-Dashboard: User-Verwaltung, Ban/Unban", "Moderation-Queue: gemeldete Beiträge", "Outreach-Mailing: Templates, Versand, Log", "Statistiken: User-Wachstum, Aktivität", "Züchter-Anträge prüfen", "Partner-Codes verwalten", "KI-Konfiguration (cloud/lokal, Limits)", ]), ("Infrastruktur", [ "Service Worker (Offline-Stufen 1–3)", "Push-Notifications (VAPID)", "APScheduler: 9 Hintergrund-Jobs (Gesundheit, Wetter, Events, …)", "Brevo E-Mail-API + SMTP-Fallback", "Analytics: Umami v2 (extern)", "SEO: robots.txt, sitemap.xml, llms.txt", "Landing Page + Widget", ]), ] lines = [ "# Funktionsumfang — Ban Yaro", f"\n_Erstellt: {DATE_STR}_\n", hr(), ] for bereich, features in BEREICHE: lines.append(h(2, bereich)) for f in features: lines.append(f"- {f}") lines.append("") # Anzahl Routes aus DB-Query-Kontext (statisch) lines += [ hr(), h(2, "Backend-Routers"), table( ["Router", "Präfix"], [ ["auth", "/api/auth"], ["dogs", "/api/dogs"], ["diary", "/api/diary"], ["health", "/api/health"], ["forum", "/api/forum"], ["wiki", "/api/wiki"], ["map", "/api/map"], ["poison", "/api/poison"], ["lost", "/api/lost"], ["breeder", "/api/breeder"], ["litters", "/api/litters"], ["training", "/api/training"], ["outreach", "/api/outreach"], ["moderation", "/api/moderation"], ["notes", "/api/notes"], ["notifications", "/api/notifications"], ["push", "/api/push"], ["friends", "/api/friends"], ["profile", "/api/profile"], ["social", "/api/social"], ["sitting", "/api/sitting"], ["achievements", "/api/achievements"], ["stats", "/api/stats"], ["walks", "/api/walks"], ["events", "/api/events"], ["alerts", "/api/alerts"], ["ratings", "/api/ratings"], ] ), ] return "\n".join(lines) # ────────────────────────────────────────────────────────────────────────────── # 3 DATEILISTE # ────────────────────────────────────────────────────────────────────────────── def report_dateien(): lines = [ "# Dateiliste — Ban Yaro", f"\n_Erstellt: {DATE_STR}_\n", hr(), ] def scan_dir(title, path, ext): lines.append(h(2, title)) files = sorted(Path(path).rglob(f"*.{ext}")) if Path(path).exists() else [] rows = [] total = 0 for f in files: try: size = f.stat().st_size total += size rows.append([str(f.relative_to(path)), bytes_human(size)]) except Exception: pass if rows: lines.append(table(["Datei", "Größe"], rows)) lines.append(f"\n**Gesamt**: {len(rows)} Dateien, {bytes_human(total)}\n") scan_dir("Backend — Python-Dateien", APP_DIR, "py") scan_dir("Frontend — JavaScript", f"{APP_DIR}/static/js", "js") scan_dir("Frontend — CSS", f"{APP_DIR}/static/css", "css") # HTML-Templates html_files = list(Path(f"{APP_DIR}/static").glob("*.html")) if Path(f"{APP_DIR}/static").exists() else [] if html_files: lines.append(h(2, "Frontend — HTML")) rows = [[f.name, bytes_human(f.stat().st_size)] for f in sorted(html_files)] lines.append(table(["Datei", "Größe"], rows)) lines.append("") return "\n".join(lines) # ────────────────────────────────────────────────────────────────────────────── # 4 NUTZERÜBERSICHT # ────────────────────────────────────────────────────────────────────────────── def report_nutzer(): lines = [ "# Nutzerübersicht — Ban Yaro", f"\n_Erstellt: {DATE_STR}_\n", hr(), ] # Nutzer nach Rolle lines.append(h(2, "Nutzer nach Rolle")) total_users = val("SELECT COUNT(*) FROM users") admins = val("SELECT COUNT(*) FROM users WHERE rolle='admin'") mods = val("SELECT COUNT(*) FROM users WHERE rolle='moderator' OR is_moderator=1") breeders = val("SELECT COUNT(*) FROM users WHERE rolle='breeder'") founders = val("SELECT COUNT(*) FROM users WHERE is_founder=1") partners = val("SELECT COUNT(*) FROM users WHERE is_partner=1") banned = val("SELECT COUNT(*) FROM users WHERE is_banned=1") unverifiziert = val("SELECT COUNT(*) FROM users WHERE email_verified=0") premium = val("SELECT COUNT(*) FROM users WHERE is_premium=1") lines.append(table( ["Gruppe", "Anzahl"], [ ["Gesamt Nutzer", str(total_users)], ["Admin", str(admins)], ["Moderatoren", str(mods)], ["Züchter", str(breeders)], ["Gründer (aktiv)", str(founders)], ["Partner", str(partners)], ["Premium", str(premium)], ["Gesperrt (banned)", str(banned)], ["E-Mail unverifiziert", str(unverifiziert)], ] )) # Registrierungen pro Monat (letzte 6 Monate) lines.append(h(2, "Registrierungen (letzte 6 Monate)")) reg_rows = q(""" SELECT strftime('%Y-%m', created_at) as monat, COUNT(*) as n FROM users WHERE created_at >= date('now', '-6 months') GROUP BY monat ORDER BY monat """) if reg_rows: lines.append(table(["Monat", "Neue Nutzer"], [(r[0], r[1]) for r in reg_rows])) else: lines.append("_Keine Daten_") lines.append("") # Hunde lines.append(h(2, "Hunde")) dogs = val("SELECT COUNT(*) FROM dogs") dogs_with_diary = val("SELECT COUNT(DISTINCT dog_id) FROM diary") lines.append(table( ["Metrik", "Anzahl"], [ ["Hunde gesamt", str(dogs)], ["Hunde mit Tagebuch-Einträgen", str(dogs_with_diary)], ] )) lines.append("") # Forum lines.append(h(2, "Forum")) threads = val("SELECT COUNT(*) FROM forum_threads WHERE is_deleted=0") posts = val("SELECT COUNT(*) FROM forum_posts WHERE is_deleted=0") reports_open = val("SELECT COUNT(*) FROM forum_reports WHERE resolved=0", default=0) lines.append(table( ["Metrik", "Anzahl"], [ ["Threads", str(threads)], ["Antworten", str(posts)], ["Offene Meldungen", str(reports_open)], ] )) # Kategorie-Verteilung kat_rows = q(""" SELECT kategorie, COUNT(*) as n FROM forum_threads WHERE is_deleted=0 GROUP BY kategorie ORDER BY n DESC """) if kat_rows: lines.append("\n**Threads nach Kategorie:**\n") lines.append(table(["Kategorie", "Threads"], [(r[0], r[1]) for r in kat_rows])) lines.append("") # Tagebuch lines.append(h(2, "Tagebuch")) diary_total = val("SELECT COUNT(*) FROM diary") diary_mit_foto = val("SELECT COUNT(*) FROM diary WHERE foto_url IS NOT NULL AND foto_url != ''") diary_mit_gps = val("SELECT COUNT(*) FROM diary WHERE lat IS NOT NULL") lines.append(table( ["Metrik", "Anzahl"], [ ["Einträge gesamt", str(diary_total)], ["Mit Foto", str(diary_mit_foto)], ["Mit GPS-Koordinaten", str(diary_mit_gps)], ] )) lines.append("") # Medien (Dateisystem) lines.append(h(2, "Medien auf dem Server")) media_root = Path(MEDIA_DIR) if media_root.exists(): rows = [] total_size = 0 total_count = 0 for subdir in sorted(media_root.iterdir()): if subdir.is_dir(): files = list(subdir.rglob("*")) files = [f for f in files if f.is_file()] size = sum(f.stat().st_size for f in files if f.is_file()) total_size += size total_count += len(files) rows.append([subdir.name, str(len(files)), bytes_human(size)]) rows.append(["**GESAMT**", str(total_count), bytes_human(total_size)]) lines.append(table(["Verzeichnis", "Dateien", "Größe"], rows)) else: lines.append(f"_Media-Verzeichnis nicht gefunden: {MEDIA_DIR}_") lines.append("") # Outreach-Mails lines.append(h(2, "Gesendete E-Mails")) mail_rows = q(""" SELECT from_account, COUNT(*) as n, MIN(sent_at) as erste, MAX(sent_at) as letzte FROM outreach_log GROUP BY from_account ORDER BY n DESC """) if mail_rows: lines.append(table( ["Absender", "Anzahl", "Erste Mail", "Letzte Mail"], [(r[0], r[1], r[2][:10] if r[2] else "—", r[3][:10] if r[3] else "—") for r in mail_rows] )) total_mails = sum(r[1] for r in mail_rows) lines.append(f"\n**Gesamt**: {total_mails} Mails gesendet\n") else: lines.append("_Noch keine Mails versendet_\n") # Analytics-Hinweis lines += [ h(2, "Besuche (Analytics)"), "> **Hinweis:** Besucher-Statistiken (Besuche/Tag und Monat) werden extern " "über **Umami** erfasst und sind nicht im Container verfügbar. " "Bitte Umami-Dashboard direkt aufrufen.", "", ] return "\n".join(lines) # ────────────────────────────────────────────────────────────────────────────── # 5 PARTNERLISTE # ────────────────────────────────────────────────────────────────────────────── def report_partner(): lines = [ "# Partnerliste — Ban Yaro", f"\n_Erstellt: {DATE_STR}_\n", hr(), ] # Partner-User lines.append(h(2, "Partner-Accounts")) partner_users = q(""" SELECT name, email, created_at, founder_number FROM users WHERE is_partner=1 ORDER BY created_at """) if partner_users: lines.append(table( ["Name", "E-Mail", "Partner seit", "Gründer-Nr."], [(r[0], r[1], r[2][:10] if r[2] else "—", str(r[3]) if r[3] else "—") for r in partner_users] )) else: lines.append("_Keine Partner-Accounts_") lines.append("") # Partner-Codes lines.append(h(2, "Partner-Codes")) codes = q(""" SELECT code, grants_founder, max_uses, uses, created_at FROM partner_codes ORDER BY created_at """) if codes: lines.append(table( ["Code", "Gründer-Slot", "Max. Nutzungen", "Verwendet", "Erstellt"], [( r[0], "Ja" if r[1] else "Nein", str(r[2]) if r[2] else "∞", str(r[3]), r[4][:10] if r[4] else "—" ) for r in codes] )) else: lines.append("_Keine Partner-Codes_") lines.append("") # Gründer lines.append(h(2, "Gründer")) gruender = q(""" SELECT founder_number, name, email, created_at FROM users WHERE is_founder=1 ORDER BY founder_number """) if gruender: lines.append(table( ["Nr.", "Name", "E-Mail", "Registriert"], [(r[0], r[1], r[2], r[3][:10] if r[3] else "—") for r in gruender] )) lines.append(f"\n**{len(gruender)} von 100 Gründer-Plätzen belegt.**\n") else: lines.append("_Noch keine Gründer_") lines.append("") return "\n".join(lines) # ────────────────────────────────────────────────────────────────────────────── # 6 SERVER & SPEICHER # ────────────────────────────────────────────────────────────────────────────── def report_server(): lines = [ "# Server & Speicherbelegung — Ban Yaro", f"\n_Erstellt: {DATE_STR}_\n", hr(), ] # Disk Usage lines.append(h(2, "Festplattenbelegung")) df_out = sh("df -h /data 2>/dev/null || df -h /") lines.append(f"```\n{df_out}\n```\n") # Media-Verzeichnisse lines.append(h(2, "Media-Verzeichnisse")) du_media = sh(f"du -sh {MEDIA_DIR}/* 2>/dev/null | sort -rh") du_total = sh(f"du -sh {MEDIA_DIR} 2>/dev/null") if du_media: lines.append(f"```\n{du_media}\n\nGesamt: {du_total}\n```\n") else: lines.append("_Keine Media-Daten_\n") # DB-Größe lines.append(h(2, "Datenbank")) db_size = sh(f"du -sh {DB_PATH} 2>/dev/null") db_rows = {} try: with db() as conn: tables = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" ).fetchall() for t in tables: name = t[0] count = conn.execute(f"SELECT COUNT(*) FROM {name}").fetchone()[0] db_rows[name] = count except Exception: pass lines.append(f"**DB-Größe:** {db_size}\n") if db_rows: rows_sorted = sorted(db_rows.items(), key=lambda x: x[1], reverse=True) lines.append(table(["Tabelle", "Zeilen"], [(k, f"{v:,}") for k, v in rows_sorted])) lines.append("") # App-Code Größe lines.append(h(2, "App-Code")) du_app = sh(f"du -sh {APP_DIR} 2>/dev/null") lines.append(f"**App-Verzeichnis ({APP_DIR}):** {du_app}\n") # Speicher-Kapazität (Warnung wenn >80 %) lines.append(h(2, "Kapazitäts-Warnung")) df_pct = sh("df /data 2>/dev/null | awk 'NR==2{print $5}' | tr -d '%' || df / | awk 'NR==2{print $5}' | tr -d '%'") try: pct = int(df_pct.strip()) if pct >= 90: lines.append(f"> ⚠️ **KRITISCH: {pct} % Festplatte belegt!** Sofortige Maßnahmen nötig.") elif pct >= 80: lines.append(f"> ⚠️ **Warnung: {pct} % Festplatte belegt.** Bald aufrüsten.") elif pct >= 70: lines.append(f"> ℹ️ {pct} % Festplatte belegt — im Blick behalten.") else: lines.append(f"> ✅ {pct} % Festplatte belegt — ausreichend Kapazität.") except (ValueError, TypeError): lines.append(f"> Belegung: {df_pct}") lines.append("") # Python-Pakete lines.append(h(2, "Installierte Python-Pakete")) pip_list = sh("pip list --format=columns 2>/dev/null | head -40") lines.append(f"```\n{pip_list}\n```\n") return "\n".join(lines) # ────────────────────────────────────────────────────────────────────────────── # Main # ────────────────────────────────────────────────────────────────────────────── REPORTS = { "sicherheit": report_sicherheit, "funktionsumfang": report_funktionsumfang, "dateien": report_dateien, "nutzer": report_nutzer, "partner": report_partner, "server": report_server, } if __name__ == "__main__": section = sys.argv[1] if len(sys.argv) > 1 else "all" if section == "all": for name, fn in REPORTS.items(): print(f"=== REPORT:{name} ===") print(fn()) print() elif section in REPORTS: print(REPORTS[section]()) else: print(f"Unbekannte Section: {section}", file=sys.stderr) print(f"Verfügbar: {', '.join(REPORTS.keys())}", file=sys.stderr) sys.exit(1)