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

411 lines
14 KiB
Python

"""
API Routes für Alert-Templates (Playbooks).
Endpoints für Guided Mode:
- GET /templates - Liste aller verfügbaren Templates
- GET /templates/{template_id} - Template-Details
- POST /templates/{template_id}/preview - Vorschau generieren
- GET /templates/by-role/{role} - Templates für eine Rolle
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session as DBSession
from ..db.database import get_db
from ..db.models import AlertTemplateDB, UserRoleEnum
router = APIRouter(prefix="/templates", tags=["templates"])
# ============================================================================
# Request/Response Models
# ============================================================================
class TemplateListItem(BaseModel):
"""Kurzinfo für Template-Liste."""
id: str
slug: str
name: str
description: str
icon: str
category: str
target_roles: List[str]
is_premium: bool
max_cards_per_day: int
sort_order: int
class Config:
from_attributes = True
class TemplateDetail(BaseModel):
"""Vollständige Template-Details."""
id: str
slug: str
name: str
description: str
icon: str
category: str
target_roles: List[str]
topics_config: List[dict]
rules_config: List[dict]
profile_config: dict
importance_config: dict
max_cards_per_day: int
digest_enabled: bool
digest_day: str
is_premium: bool
is_active: bool
class Config:
from_attributes = True
class TemplateListResponse(BaseModel):
"""Response für Template-Liste."""
templates: List[TemplateListItem]
total: int
class PreviewRequest(BaseModel):
"""Request für Template-Vorschau."""
sample_count: int = Field(default=3, ge=1, le=5)
class PreviewItem(BaseModel):
"""Ein Vorschau-Item."""
title: str
snippet: str
importance_level: str
why_relevant: str
source_name: str
class PreviewResponse(BaseModel):
"""Response für Template-Vorschau."""
template_name: str
sample_items: List[PreviewItem]
estimated_daily_count: str
message: str
# ============================================================================
# Endpoints
# ============================================================================
@router.get("", response_model=TemplateListResponse)
async def list_templates(
category: Optional[str] = Query(None, description="Filter nach Kategorie"),
role: Optional[str] = Query(None, description="Filter nach Zielrolle"),
include_premium: bool = Query(True, description="Premium-Templates einschließen"),
db: DBSession = Depends(get_db)
):
"""
Liste alle verfügbaren Alert-Templates.
Templates sind vorkonfigurierte Playbooks für bestimmte Themen
(Förderprogramme, Datenschutz, IT-Security, etc.).
"""
query = db.query(AlertTemplateDB).filter(AlertTemplateDB.is_active == True)
if category:
query = query.filter(AlertTemplateDB.category == category)
if not include_premium:
query = query.filter(AlertTemplateDB.is_premium == False)
templates = query.order_by(AlertTemplateDB.sort_order).all()
# Filter nach Rolle (JSON-Feld)
if role:
templates = [t for t in templates if role in (t.target_roles or [])]
return TemplateListResponse(
templates=[
TemplateListItem(
id=t.id,
slug=t.slug,
name=t.name,
description=t.description,
icon=t.icon or "",
category=t.category or "",
target_roles=t.target_roles or [],
is_premium=t.is_premium or False,
max_cards_per_day=t.max_cards_per_day or 10,
sort_order=t.sort_order or 0,
)
for t in templates
],
total=len(templates)
)
@router.get("/by-role/{role}", response_model=TemplateListResponse)
async def get_templates_by_role(
role: str,
db: DBSession = Depends(get_db)
):
"""
Empfohlene Templates für eine bestimmte Rolle.
Rollen:
- lehrkraft: Fokus auf Unterricht, Fortbildungen, Wettbewerbe
- schulleitung: Fokus auf Administration, Fördermittel, Recht
- it_beauftragte: Fokus auf IT-Security, Datenschutz
"""
# Validiere Rolle
valid_roles = ["lehrkraft", "schulleitung", "it_beauftragte"]
if role not in valid_roles:
raise HTTPException(
status_code=400,
detail=f"Ungültige Rolle. Erlaubt: {', '.join(valid_roles)}"
)
templates = db.query(AlertTemplateDB).filter(
AlertTemplateDB.is_active == True,
AlertTemplateDB.is_premium == False # Nur kostenlose für Empfehlungen
).order_by(AlertTemplateDB.sort_order).all()
# Filter nach Rolle
filtered = [t for t in templates if role in (t.target_roles or [])]
return TemplateListResponse(
templates=[
TemplateListItem(
id=t.id,
slug=t.slug,
name=t.name,
description=t.description,
icon=t.icon or "",
category=t.category or "",
target_roles=t.target_roles or [],
is_premium=t.is_premium or False,
max_cards_per_day=t.max_cards_per_day or 10,
sort_order=t.sort_order or 0,
)
for t in filtered
],
total=len(filtered)
)
@router.get("/{template_id}", response_model=TemplateDetail)
async def get_template(
template_id: str,
db: DBSession = Depends(get_db)
):
"""
Vollständige Details eines Templates abrufen.
Enthält alle Konfigurationen (Topics, Rules, Profile).
"""
template = db.query(AlertTemplateDB).filter(
AlertTemplateDB.id == template_id
).first()
if not template:
# Versuche nach Slug zu suchen
template = db.query(AlertTemplateDB).filter(
AlertTemplateDB.slug == template_id
).first()
if not template:
raise HTTPException(status_code=404, detail="Template nicht gefunden")
return TemplateDetail(
id=template.id,
slug=template.slug,
name=template.name,
description=template.description,
icon=template.icon or "",
category=template.category or "",
target_roles=template.target_roles or [],
topics_config=template.topics_config or [],
rules_config=template.rules_config or [],
profile_config=template.profile_config or {},
importance_config=template.importance_config or {},
max_cards_per_day=template.max_cards_per_day or 10,
digest_enabled=template.digest_enabled if template.digest_enabled is not None else True,
digest_day=template.digest_day or "monday",
is_premium=template.is_premium or False,
is_active=template.is_active if template.is_active is not None else True,
)
@router.post("/{template_id}/preview", response_model=PreviewResponse)
async def preview_template(
template_id: str,
request: PreviewRequest = None,
db: DBSession = Depends(get_db)
):
"""
Generiere eine Vorschau, wie Alerts für dieses Template aussehen würden.
Zeigt Beispiel-Alerts mit Wichtigkeitsstufen und "Warum relevant?"-Erklärungen.
"""
template = db.query(AlertTemplateDB).filter(
AlertTemplateDB.id == template_id
).first()
if not template:
template = db.query(AlertTemplateDB).filter(
AlertTemplateDB.slug == template_id
).first()
if not template:
raise HTTPException(status_code=404, detail="Template nicht gefunden")
# Generiere Beispiel-Alerts basierend auf Template-Konfiguration
sample_items = _generate_preview_items(template)
return PreviewResponse(
template_name=template.name,
sample_items=sample_items[:request.sample_count if request else 3],
estimated_daily_count=f"Ca. {template.max_cards_per_day} Meldungen pro Tag",
message=f"Diese Vorschau zeigt, wie Alerts für '{template.name}' aussehen würden."
)
@router.post("/seed")
async def seed_templates(
force_update: bool = Query(False, description="Bestehende Templates aktualisieren"),
db: DBSession = Depends(get_db)
):
"""
Fügt die vordefinierten Templates in die Datenbank ein.
Nur für Entwicklung/Setup.
"""
from ..data.templates import seed_templates as do_seed
count = do_seed(db, force_update=force_update)
return {
"status": "success",
"templates_created": count,
"message": f"{count} Templates wurden eingefügt/aktualisiert."
}
# ============================================================================
# Helper Functions
# ============================================================================
def _generate_preview_items(template: AlertTemplateDB) -> List[PreviewItem]:
"""
Generiere Beispiel-Alerts für Template-Vorschau.
Diese sind statisch/exemplarisch, nicht aus echten Daten.
"""
# Template-spezifische Beispiele
examples = {
"foerderprogramme": [
PreviewItem(
title="DigitalPakt 2.0: Neue Antragsphase startet am 1. April",
snippet="Das BMBF hat die zweite Phase des DigitalPakt Schule angekündigt...",
importance_level="DRINGEND",
why_relevant="Frist endet in 45 Tagen. Betrifft alle Schulen mit Förderbedarf.",
source_name="Bundesministerium für Bildung"
),
PreviewItem(
title="Landesförderung: 50.000€ für innovative Schulprojekte",
snippet="Das Kultusministerium fördert Schulen, die digitale Lernkonzepte...",
importance_level="WICHTIG",
why_relevant="Passende Förderung für Ihr Bundesland. Keine Eigenbeteiligung erforderlich.",
source_name="Kultusministerium"
),
PreviewItem(
title="Erasmus+ Schulpartnerschaften: Jetzt bewerben",
snippet="Für das Schuljahr 2026/27 können Schulen EU-Förderung beantragen...",
importance_level="PRUEFEN",
why_relevant="EU-Programm mit hoher Fördersumme. Bewerbungsfrist in 3 Monaten.",
source_name="EU-Kommission"
),
],
"abitur-updates": [
PreviewItem(
title="Neue Operatoren für Abitur Deutsch ab 2027",
snippet="Die KMK hat überarbeitete Operatoren für das Fach Deutsch beschlossen...",
importance_level="WICHTIG",
why_relevant="Betrifft die Oberstufenplanung. Anpassung der Klausuren erforderlich.",
source_name="KMK"
),
PreviewItem(
title="Abiturtermine 2026: Prüfungsplan veröffentlicht",
snippet="Das Kultusministerium hat die Termine für das Abitur 2026 bekannt gegeben...",
importance_level="INFO",
why_relevant="Planungsgrundlage für Schuljahreskalender.",
source_name="Kultusministerium"
),
],
"datenschutz-recht": [
PreviewItem(
title="LfDI: Neue Handreichung zu Schülerfotos",
snippet="Der Landesbeauftragte für Datenschutz hat eine aktualisierte...",
importance_level="DRINGEND",
why_relevant="Handlungsbedarf: Bestehende Einwilligungen müssen geprüft werden.",
source_name="Datenschutzbeauftragter"
),
PreviewItem(
title="Microsoft 365 an Schulen: Neue Bewertung",
snippet="Die Datenschutzkonferenz hat ihre Position zu Microsoft 365 aktualisiert...",
importance_level="WICHTIG",
why_relevant="Betrifft Schulen mit Microsoft-Lizenzen. Dokumentationspflicht.",
source_name="DSK"
),
],
"it-security": [
PreviewItem(
title="CVE-2026-1234: Kritische Lücke in Moodle",
snippet="Eine schwerwiegende Sicherheitslücke wurde in Moodle 4.x gefunden...",
importance_level="KRITISCH",
why_relevant="Sofortiges Update erforderlich. Exploit bereits aktiv.",
source_name="BSI CERT-Bund"
),
PreviewItem(
title="Phishing-Welle: Gefälschte Schulportal-Mails",
snippet="Aktuell werden vermehrt Phishing-Mails an Lehrkräfte versendet...",
importance_level="DRINGEND",
why_relevant="Warnung an Kollegium empfohlen. Erkennungsmerkmale beachten.",
source_name="BSI"
),
],
"fortbildungen": [
PreviewItem(
title="Kostenlose Fortbildung: KI im Unterricht",
snippet="Das Landesinstitut bietet eine Online-Fortbildung zu KI-Tools...",
importance_level="PRUEFEN",
why_relevant="Passt zu Ihrem Interessenprofil. Online-Format, 4 Stunden.",
source_name="Landesinstitut"
),
],
"wettbewerbe-projekte": [
PreviewItem(
title="Jugend forscht: Anmeldung bis 30. November",
snippet="Der größte deutsche MINT-Wettbewerb sucht wieder junge Forscher...",
importance_level="WICHTIG",
why_relevant="Frist in 60 Tagen. Für Schüler ab Klasse 4.",
source_name="Jugend forscht e.V."
),
],
}
# Hole Beispiele für dieses Template oder generische
slug = template.slug
if slug in examples:
return examples[slug]
# Generische Beispiele
return [
PreviewItem(
title=f"Beispiel-Meldung für {template.name}",
snippet=f"Dies ist eine Vorschau, wie Alerts für das Thema '{template.name}' aussehen würden.",
importance_level="INFO",
why_relevant="Passend zu Ihren ausgewählten Themen.",
source_name="Beispielquelle"
)
]