This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/voice-service/bqas/notifier.py
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

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()