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>
This commit is contained in:
410
backend/alerts_agent/api/templates.py
Normal file
410
backend/alerts_agent/api/templates.py
Normal file
@@ -0,0 +1,410 @@
|
||||
"""
|
||||
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"
|
||||
)
|
||||
]
|
||||
Reference in New Issue
Block a user