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
Benjamin Admin 21a844cb8a 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

252 lines
8.0 KiB
Python

"""
Email Action für Alerts Agent.
Sendet E-Mail-Benachrichtigungen für Alerts.
"""
import logging
from typing import Dict, Any, List
from datetime import datetime
from .base import ActionHandler, ActionResult, ActionType, AlertContext
logger = logging.getLogger(__name__)
# HTML-Template für Alert-E-Mails
EMAIL_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: #4A90E2; color: white; padding: 20px; border-radius: 8px 8px 0 0; }}
.content {{ background: #f9f9f9; padding: 20px; border: 1px solid #ddd; border-top: none; }}
.alert-card {{ background: white; padding: 15px; margin: 10px 0; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
.alert-title {{ font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }}
.alert-title a {{ color: #4A90E2; text-decoration: none; }}
.alert-snippet {{ font-size: 14px; color: #666; margin-bottom: 8px; }}
.alert-meta {{ font-size: 12px; color: #999; }}
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; }}
.badge-keep {{ background: #d4edda; color: #155724; }}
.badge-review {{ background: #fff3cd; color: #856404; }}
.footer {{ padding: 15px; text-align: center; font-size: 12px; color: #999; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2 style="margin: 0;">BreakPilot Alert</h2>
<p style="margin: 5px 0 0 0; opacity: 0.9;">{topic_name}</p>
</div>
<div class="content">
<div class="alert-card">
<div class="alert-title">
<a href="{url}">{title}</a>
</div>
<div class="alert-snippet">{snippet}</div>
<div class="alert-meta">
{decision_badge}
{score_display}
{rule_display}
</div>
</div>
</div>
<div class="footer">
Gesendet von BreakPilot Alerts Agent<br>
<a href="{dashboard_url}" style="color: #4A90E2;">Zur Inbox</a>
</div>
</div>
</body>
</html>
"""
class EmailAction(ActionHandler):
"""
E-Mail-Benachrichtigungen für Alerts.
Konfiguration:
- to: E-Mail-Adresse(n) des Empfängers
- subject_prefix: Optionaler Betreff-Prefix
- include_snippet: Snippet einbinden (default: true)
"""
@property
def action_type(self) -> ActionType:
return ActionType.EMAIL
def get_required_config_fields(self) -> List[str]:
return ["to"]
def validate_config(self, config: Dict[str, Any]) -> bool:
to = config.get("to")
if not to:
return False
if isinstance(to, str):
return "@" in to
if isinstance(to, list):
return all("@" in email for email in to)
return False
async def execute(
self,
context: AlertContext,
config: Dict[str, Any],
) -> ActionResult:
"""
Sendet eine E-Mail-Benachrichtigung.
Args:
context: Alert-Kontext
config: E-Mail-Konfiguration (to, subject_prefix, etc.)
Returns:
ActionResult
"""
try:
# Empfänger
to = config.get("to")
if isinstance(to, str):
recipients = [to]
else:
recipients = to
# Betreff
subject_prefix = config.get("subject_prefix", "[BreakPilot Alert]")
subject = f"{subject_prefix} {context.title[:50]}"
# HTML-Body generieren
html_body = self._render_email(context, config)
# E-Mail senden
sent = await self._send_email(
recipients=recipients,
subject=subject,
html_body=html_body,
)
if sent:
return ActionResult(
success=True,
action_type=self.action_type,
message=f"E-Mail an {len(recipients)} Empfänger gesendet",
details={"recipients": recipients, "subject": subject},
)
else:
return ActionResult(
success=False,
action_type=self.action_type,
message="E-Mail konnte nicht gesendet werden",
error="SMTP-Fehler",
)
except Exception as e:
logger.error(f"Email action error: {e}")
return ActionResult(
success=False,
action_type=self.action_type,
message="E-Mail-Fehler",
error=str(e),
)
def _render_email(
self,
context: AlertContext,
config: Dict[str, Any],
) -> str:
"""Rendert das E-Mail-Template."""
# Decision Badge
decision_badge = ""
if context.relevance_decision:
badge_class = "badge-keep" if context.relevance_decision == "KEEP" else "badge-review"
decision_badge = f'<span class="badge {badge_class}">{context.relevance_decision}</span>'
# Score
score_display = ""
if context.relevance_score is not None:
score_display = f' | Score: {context.relevance_score:.0%}'
# Matched Rule
rule_display = ""
if context.matched_rule:
rule_display = f' | Regel: {context.matched_rule}'
# Snippet
snippet = context.snippet[:200] if context.snippet else ""
if config.get("include_snippet", True) is False:
snippet = ""
# Dashboard URL
dashboard_url = config.get("dashboard_url", "http://localhost:8000/studio#alerts")
return EMAIL_TEMPLATE.format(
topic_name=context.topic_name,
title=context.title,
url=context.url,
snippet=snippet,
decision_badge=decision_badge,
score_display=score_display,
rule_display=rule_display,
dashboard_url=dashboard_url,
)
async def _send_email(
self,
recipients: List[str],
subject: str,
html_body: str,
) -> bool:
"""
Sendet die E-Mail über SMTP.
Verwendet aiosmtplib für async SMTP.
"""
import os
smtp_host = os.getenv("SMTP_HOST", "localhost")
smtp_port = int(os.getenv("SMTP_PORT", "587"))
smtp_user = os.getenv("SMTP_USER", "")
smtp_pass = os.getenv("SMTP_PASS", "")
smtp_from = os.getenv("SMTP_FROM", "alerts@breakpilot.de")
try:
import aiosmtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# E-Mail erstellen
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = smtp_from
msg["To"] = ", ".join(recipients)
# HTML-Teil
html_part = MIMEText(html_body, "html", "utf-8")
msg.attach(html_part)
# Senden
await aiosmtplib.send(
msg,
hostname=smtp_host,
port=smtp_port,
username=smtp_user if smtp_user else None,
password=smtp_pass if smtp_pass else None,
start_tls=True if smtp_port == 587 else False,
)
logger.info(f"Email sent to {recipients}")
return True
except ImportError:
logger.warning("aiosmtplib not installed. Email not sent.")
# Im Dev-Modus: Erfolg simulieren
logger.info(f"[DEV] Would send email to {recipients}: {subject}")
return True
except Exception as e:
logger.error(f"SMTP error: {e}")
return False