A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
300 lines
9.2 KiB
Python
300 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
BQAS Notifier - Benachrichtigungsmodul fuer BQAS Test-Ergebnisse
|
|
|
|
Unterstuetzt verschiedene Benachrichtigungsmethoden:
|
|
- macOS Desktop-Benachrichtigungen
|
|
- Log-Datei
|
|
- Slack Webhook (optional)
|
|
- E-Mail (optional)
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from dataclasses import dataclass, asdict
|
|
|
|
|
|
@dataclass
|
|
class NotificationConfig:
|
|
"""Konfiguration fuer Benachrichtigungen."""
|
|
|
|
# Allgemein
|
|
enabled: bool = True
|
|
log_file: str = "/var/log/bqas/notifications.log"
|
|
|
|
# macOS Desktop
|
|
desktop_enabled: bool = True
|
|
desktop_sound_success: str = "Glass"
|
|
desktop_sound_failure: str = "Basso"
|
|
|
|
# Slack (optional)
|
|
slack_enabled: bool = False
|
|
slack_webhook_url: Optional[str] = None
|
|
slack_channel: str = "#bqas-alerts"
|
|
|
|
# E-Mail (optional)
|
|
email_enabled: bool = False
|
|
email_recipient: Optional[str] = None
|
|
email_sender: str = "bqas@localhost"
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "NotificationConfig":
|
|
"""Erstellt Config aus Umgebungsvariablen."""
|
|
return cls(
|
|
enabled=os.getenv("BQAS_NOTIFY_ENABLED", "true").lower() == "true",
|
|
log_file=os.getenv("BQAS_LOG_FILE", "/var/log/bqas/notifications.log"),
|
|
desktop_enabled=os.getenv("BQAS_NOTIFY_DESKTOP", "true").lower() == "true",
|
|
slack_enabled=os.getenv("BQAS_NOTIFY_SLACK", "false").lower() == "true",
|
|
slack_webhook_url=os.getenv("BQAS_SLACK_WEBHOOK"),
|
|
slack_channel=os.getenv("BQAS_SLACK_CHANNEL", "#bqas-alerts"),
|
|
email_enabled=os.getenv("BQAS_NOTIFY_EMAIL", "false").lower() == "true",
|
|
email_recipient=os.getenv("BQAS_EMAIL_RECIPIENT"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Notification:
|
|
"""Eine Benachrichtigung."""
|
|
|
|
status: str # "success", "failure", "warning"
|
|
message: str
|
|
details: Optional[str] = None
|
|
timestamp: str = ""
|
|
source: str = "bqas"
|
|
|
|
def __post_init__(self):
|
|
if not self.timestamp:
|
|
self.timestamp = datetime.now().isoformat()
|
|
|
|
|
|
class BQASNotifier:
|
|
"""Haupt-Notifier-Klasse fuer BQAS."""
|
|
|
|
def __init__(self, config: Optional[NotificationConfig] = None):
|
|
self.config = config or NotificationConfig.from_env()
|
|
|
|
def notify(self, notification: Notification) -> bool:
|
|
"""Sendet eine Benachrichtigung ueber alle aktivierten Kanaele."""
|
|
if not self.config.enabled:
|
|
return False
|
|
|
|
success = True
|
|
|
|
# Log-Datei (immer)
|
|
self._log_notification(notification)
|
|
|
|
# Desktop (macOS)
|
|
if self.config.desktop_enabled:
|
|
if not self._send_desktop(notification):
|
|
success = False
|
|
|
|
# Slack
|
|
if self.config.slack_enabled and self.config.slack_webhook_url:
|
|
if not self._send_slack(notification):
|
|
success = False
|
|
|
|
# E-Mail
|
|
if self.config.email_enabled and self.config.email_recipient:
|
|
if not self._send_email(notification):
|
|
success = False
|
|
|
|
return success
|
|
|
|
def _log_notification(self, notification: Notification) -> None:
|
|
"""Schreibt Benachrichtigung in Log-Datei."""
|
|
try:
|
|
log_path = Path(self.config.log_file)
|
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
log_entry = {
|
|
**asdict(notification),
|
|
"logged_at": datetime.now().isoformat(),
|
|
}
|
|
|
|
with open(log_path, "a") as f:
|
|
f.write(json.dumps(log_entry) + "\n")
|
|
except Exception as e:
|
|
print(f"Fehler beim Logging: {e}", file=sys.stderr)
|
|
|
|
def _send_desktop(self, notification: Notification) -> bool:
|
|
"""Sendet macOS Desktop-Benachrichtigung."""
|
|
try:
|
|
title = self._get_title(notification.status)
|
|
sound = (
|
|
self.config.desktop_sound_failure
|
|
if notification.status == "failure"
|
|
else self.config.desktop_sound_success
|
|
)
|
|
|
|
script = f'display notification "{notification.message}" with title "{title}" sound name "{sound}"'
|
|
|
|
subprocess.run(
|
|
["osascript", "-e", script], capture_output=True, timeout=5
|
|
)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Desktop-Benachrichtigung fehlgeschlagen: {e}", file=sys.stderr)
|
|
return False
|
|
|
|
def _send_slack(self, notification: Notification) -> bool:
|
|
"""Sendet Slack-Benachrichtigung."""
|
|
try:
|
|
import urllib.request
|
|
|
|
emoji = self._get_emoji(notification.status)
|
|
color = self._get_color(notification.status)
|
|
|
|
payload = {
|
|
"channel": self.config.slack_channel,
|
|
"attachments": [
|
|
{
|
|
"color": color,
|
|
"title": f"{emoji} BQAS {notification.status.upper()}",
|
|
"text": notification.message,
|
|
"fields": [
|
|
{
|
|
"title": "Details",
|
|
"value": notification.details or "Keine Details",
|
|
"short": False,
|
|
},
|
|
{
|
|
"title": "Zeitpunkt",
|
|
"value": notification.timestamp,
|
|
"short": True,
|
|
},
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
req = urllib.request.Request(
|
|
self.config.slack_webhook_url,
|
|
data=json.dumps(payload).encode("utf-8"),
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
|
|
with urllib.request.urlopen(req, timeout=10) as response:
|
|
return response.status == 200
|
|
except Exception as e:
|
|
print(f"Slack-Benachrichtigung fehlgeschlagen: {e}", file=sys.stderr)
|
|
return False
|
|
|
|
def _send_email(self, notification: Notification) -> bool:
|
|
"""Sendet E-Mail-Benachrichtigung (via sendmail)."""
|
|
try:
|
|
subject = f"[BQAS] {notification.status.upper()}: {notification.message}"
|
|
body = f"""
|
|
BQAS Test-Ergebnis
|
|
==================
|
|
|
|
Status: {notification.status.upper()}
|
|
Nachricht: {notification.message}
|
|
Details: {notification.details or 'Keine'}
|
|
Zeitpunkt: {notification.timestamp}
|
|
|
|
---
|
|
BQAS - Breakpilot Quality Assurance System
|
|
"""
|
|
|
|
msg = f"Subject: {subject}\nFrom: {self.config.email_sender}\nTo: {self.config.email_recipient}\n\n{body}"
|
|
|
|
process = subprocess.Popen(
|
|
["/usr/sbin/sendmail", "-t"],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
process.communicate(msg.encode("utf-8"), timeout=30)
|
|
|
|
return process.returncode == 0
|
|
except Exception as e:
|
|
print(f"E-Mail-Benachrichtigung fehlgeschlagen: {e}", file=sys.stderr)
|
|
return False
|
|
|
|
@staticmethod
|
|
def _get_title(status: str) -> str:
|
|
"""Gibt Titel basierend auf Status zurueck."""
|
|
titles = {
|
|
"success": "BQAS Erfolgreich",
|
|
"failure": "BQAS Fehlgeschlagen",
|
|
"warning": "BQAS Warnung",
|
|
}
|
|
return titles.get(status, "BQAS")
|
|
|
|
@staticmethod
|
|
def _get_emoji(status: str) -> str:
|
|
"""Gibt Emoji basierend auf Status zurueck."""
|
|
emojis = {
|
|
"success": ":white_check_mark:",
|
|
"failure": ":x:",
|
|
"warning": ":warning:",
|
|
}
|
|
return emojis.get(status, ":information_source:")
|
|
|
|
@staticmethod
|
|
def _get_color(status: str) -> str:
|
|
"""Gibt Slack-Farbe basierend auf Status zurueck."""
|
|
colors = {
|
|
"success": "good",
|
|
"failure": "danger",
|
|
"warning": "warning",
|
|
}
|
|
return colors.get(status, "#808080")
|
|
|
|
|
|
def main():
|
|
"""CLI-Einstiegspunkt."""
|
|
parser = argparse.ArgumentParser(description="BQAS Notifier")
|
|
parser.add_argument(
|
|
"--status",
|
|
choices=["success", "failure", "warning"],
|
|
required=True,
|
|
help="Status der Benachrichtigung",
|
|
)
|
|
parser.add_argument(
|
|
"--message",
|
|
required=True,
|
|
help="Benachrichtigungstext",
|
|
)
|
|
parser.add_argument(
|
|
"--details",
|
|
default=None,
|
|
help="Zusaetzliche Details",
|
|
)
|
|
parser.add_argument(
|
|
"--desktop-only",
|
|
action="store_true",
|
|
help="Nur Desktop-Benachrichtigung senden",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Konfiguration laden
|
|
config = NotificationConfig.from_env()
|
|
|
|
# Bei --desktop-only andere Kanaele deaktivieren
|
|
if args.desktop_only:
|
|
config.slack_enabled = False
|
|
config.email_enabled = False
|
|
|
|
# Benachrichtigung erstellen und senden
|
|
notifier = BQASNotifier(config)
|
|
notification = Notification(
|
|
status=args.status,
|
|
message=args.message,
|
|
details=args.details,
|
|
)
|
|
|
|
success = notifier.notify(notification)
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|