klausur-service (11 files): - cv_gutter_repair, ocr_pipeline_regression, upload_api - ocr_pipeline_sessions, smart_spell, nru_worksheet_generator - ocr_pipeline_overlays, mail/aggregator, zeugnis_api - cv_syllable_detect, self_rag backend-lehrer (17 files): - classroom_engine/suggestions, generators/quiz_generator - worksheets_api, llm_gateway/comparison, state_engine_api - classroom/models (→ 4 submodules), services/file_processor - alerts_agent/api/wizard+digests+routes, content_generators/pdf - classroom/routes/sessions, llm_gateway/inference - classroom_engine/analytics, auth/keycloak_auth - alerts_agent/processing/rule_engine, ai_processor/print_versions agent-core (5 files): - brain/memory_store, brain/knowledge_graph, brain/context_manager - orchestrator/supervisor, sessions/session_manager admin-lehrer (5 components): - GridOverlay, StepGridReview, DevOpsPipelineSidebar - DataFlowDiagram, sbom/wizard/page website (2 files): - DependencyMap, lehrer/abitur-archiv Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
147 lines
4.3 KiB
Python
147 lines
4.3 KiB
Python
"""
|
|
Alert Digests - PDF-Generierung und E-Mail-Versand.
|
|
"""
|
|
|
|
import io
|
|
import logging
|
|
|
|
from ..db.models import AlertDigestDB
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def generate_pdf_from_html(html_content: str) -> bytes:
|
|
"""
|
|
Generiere PDF aus HTML.
|
|
|
|
Verwendet WeasyPrint oder wkhtmltopdf als Fallback.
|
|
"""
|
|
try:
|
|
from weasyprint import HTML
|
|
pdf_bytes = HTML(string=html_content).write_pdf()
|
|
return pdf_bytes
|
|
except ImportError:
|
|
pass
|
|
|
|
try:
|
|
import pdfkit
|
|
pdf_bytes = pdfkit.from_string(html_content, False)
|
|
return pdf_bytes
|
|
except ImportError:
|
|
pass
|
|
|
|
try:
|
|
from xhtml2pdf import pisa
|
|
result = io.BytesIO()
|
|
pisa.CreatePDF(io.StringIO(html_content), dest=result)
|
|
return result.getvalue()
|
|
except ImportError:
|
|
pass
|
|
|
|
raise ImportError(
|
|
"Keine PDF-Bibliothek verfuegbar. "
|
|
"Installieren Sie: pip install weasyprint oder pip install pdfkit oder pip install xhtml2pdf"
|
|
)
|
|
|
|
|
|
async def send_digest_by_email(digest: AlertDigestDB, recipient_email: str):
|
|
"""
|
|
Versende Digest per E-Mail.
|
|
|
|
Verwendet:
|
|
- Lokalen SMTP-Server (Postfix/Sendmail)
|
|
- SMTP-Relay (z.B. SES, Mailgun)
|
|
- SendGrid API
|
|
"""
|
|
import os
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.application import MIMEApplication
|
|
|
|
msg = MIMEMultipart('alternative')
|
|
msg['Subject'] = f"Wochenbericht: {digest.period_start.strftime('%d.%m.%Y')} - {digest.period_end.strftime('%d.%m.%Y')}"
|
|
msg['From'] = os.getenv('SMTP_FROM', 'alerts@breakpilot.app')
|
|
msg['To'] = recipient_email
|
|
|
|
text_content = f"""
|
|
BreakPilot Alerts - Wochenbericht
|
|
|
|
Zeitraum: {digest.period_start.strftime('%d.%m.%Y')} - {digest.period_end.strftime('%d.%m.%Y')}
|
|
Gesamt: {digest.total_alerts} Meldungen
|
|
Kritisch: {digest.critical_count}
|
|
Dringend: {digest.urgent_count}
|
|
|
|
Oeffnen Sie die HTML-Version fuer die vollstaendige Uebersicht.
|
|
|
|
---
|
|
Diese E-Mail wurde automatisch von BreakPilot Alerts generiert.
|
|
"""
|
|
msg.attach(MIMEText(text_content, 'plain', 'utf-8'))
|
|
|
|
if digest.summary_html:
|
|
msg.attach(MIMEText(digest.summary_html, 'html', 'utf-8'))
|
|
|
|
try:
|
|
pdf_bytes = await generate_pdf_from_html(digest.summary_html)
|
|
pdf_attachment = MIMEApplication(pdf_bytes, _subtype='pdf')
|
|
pdf_attachment.add_header(
|
|
'Content-Disposition', 'attachment',
|
|
filename=f"wochenbericht_{digest.period_start.strftime('%Y%m%d')}.pdf"
|
|
)
|
|
msg.attach(pdf_attachment)
|
|
except Exception:
|
|
pass # PDF-Anhang ist optional
|
|
|
|
smtp_host = os.getenv('SMTP_HOST', 'localhost')
|
|
smtp_port = int(os.getenv('SMTP_PORT', '25'))
|
|
smtp_user = os.getenv('SMTP_USER', '')
|
|
smtp_pass = os.getenv('SMTP_PASS', '')
|
|
|
|
try:
|
|
if smtp_port == 465:
|
|
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
|
else:
|
|
server = smtplib.SMTP(smtp_host, smtp_port)
|
|
if smtp_port == 587:
|
|
server.starttls()
|
|
|
|
if smtp_user and smtp_pass:
|
|
server.login(smtp_user, smtp_pass)
|
|
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
except Exception as e:
|
|
sendgrid_key = os.getenv('SENDGRID_API_KEY')
|
|
if sendgrid_key:
|
|
await send_via_sendgrid(msg, sendgrid_key)
|
|
else:
|
|
raise e
|
|
|
|
|
|
async def send_via_sendgrid(msg, api_key: str):
|
|
"""Fallback: SendGrid API."""
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
"https://api.sendgrid.com/v3/mail/send",
|
|
headers={
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json"
|
|
},
|
|
json={
|
|
"personalizations": [{"to": [{"email": msg['To']}]}],
|
|
"from": {"email": msg['From']},
|
|
"subject": msg['Subject'],
|
|
"content": [
|
|
{"type": "text/plain", "value": msg.get_payload(0).get_payload()},
|
|
{"type": "text/html", "value": msg.get_payload(1).get_payload() if len(msg.get_payload()) > 1 else ""}
|
|
]
|
|
}
|
|
)
|
|
|
|
if response.status_code >= 400:
|
|
raise Exception(f"SendGrid error: {response.status_code}")
|