refactor: Jitsi/Matrix/Voice von Core übernommen, Camunda/BPMN gelöscht, Kommunikation-Nav
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 25s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m55s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 18s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 25s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m55s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 18s
- Voice-Service von Core nach Lehrer verschoben (bp-lehrer-voice-service) - 4 Jitsi-Services + 2 Synapse-Services in docker-compose.yml aufgenommen - Camunda komplett gelöscht: workflow pages, workflow-config.ts, bpmn-js deps - CAMUNDA_URL aus backend-lehrer environment entfernt - Sidebar: Kategorie "Compliance SDK" + "Katalogverwaltung" entfernt - Sidebar: Neue Kategorie "Kommunikation" mit Video & Chat, Voice Service, Alerts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
299
voice-service/bqas/notifier.py
Normal file
299
voice-service/bqas/notifier.py
Normal file
@@ -0,0 +1,299 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user