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
breakpilot-pwa/backend/alerts_agent/api/subscriptions.py
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

422 lines
14 KiB
Python

"""
API Routes für User Alert Subscriptions.
Verwaltet Nutzer-Abonnements für Templates und Digest-Einstellungen.
Endpoints:
- POST /subscriptions - Neue Subscription erstellen
- GET /subscriptions - User-Subscriptions auflisten
- GET /subscriptions/{id} - Subscription-Details
- PUT /subscriptions/{id} - Subscription aktualisieren
- DELETE /subscriptions/{id} - Subscription deaktivieren
- POST /subscriptions/{id}/activate-template - Template aktivieren
"""
import uuid
from typing import Optional, List
from datetime import datetime
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 (
UserAlertSubscriptionDB, AlertTemplateDB, AlertProfileDB,
AlertTopicDB, AlertRuleDB, AlertModeEnum, UserRoleEnum
)
router = APIRouter(prefix="/subscriptions", tags=["subscriptions"])
# ============================================================================
# Request/Response Models
# ============================================================================
class SubscriptionCreate(BaseModel):
"""Request für neue Subscription."""
mode: str = Field(default="guided", description="'guided' oder 'expert'")
user_role: Optional[str] = Field(default=None, description="lehrkraft, schulleitung, it_beauftragte")
template_ids: List[str] = Field(default=[], description="Ausgewählte Template-IDs (max. 3)")
notification_email: Optional[str] = Field(default=None)
digest_enabled: bool = Field(default=True)
digest_frequency: str = Field(default="weekly")
class SubscriptionUpdate(BaseModel):
"""Request für Subscription-Update."""
template_ids: Optional[List[str]] = None
notification_email: Optional[str] = None
digest_enabled: Optional[bool] = None
digest_frequency: Optional[str] = None
is_active: Optional[bool] = None
class SubscriptionResponse(BaseModel):
"""Response für eine Subscription."""
id: str
user_id: str
mode: str
user_role: Optional[str]
selected_template_ids: List[str]
template_names: List[str]
notification_email: Optional[str]
digest_enabled: bool
digest_frequency: str
wizard_completed: bool
is_active: bool
created_at: datetime
updated_at: datetime
class SubscriptionListResponse(BaseModel):
"""Response für Subscription-Liste."""
subscriptions: List[SubscriptionResponse]
total: int
class ActivateTemplateRequest(BaseModel):
"""Request für Template-Aktivierung."""
create_topics: bool = Field(default=True, description="Topics aus Template-Config erstellen")
create_rules: bool = Field(default=True, description="Rules aus Template-Config erstellen")
class ActivateTemplateResponse(BaseModel):
"""Response für Template-Aktivierung."""
status: str
topics_created: int
rules_created: int
profile_updated: bool
message: str
# ============================================================================
# Helper Functions
# ============================================================================
def get_user_id_from_request() -> str:
"""
Extrahiert User-ID aus Request.
TODO: JWT-Token auswerten, aktuell Dummy.
"""
return "demo-user"
def _get_template_names(db: DBSession, template_ids: List[str]) -> List[str]:
"""Hole Template-Namen für IDs."""
if not template_ids:
return []
templates = db.query(AlertTemplateDB).filter(
AlertTemplateDB.id.in_(template_ids)
).all()
return [t.name for t in templates]
def _subscription_to_response(sub: UserAlertSubscriptionDB, db: DBSession) -> SubscriptionResponse:
"""Konvertiere DB-Model zu Response."""
template_ids = sub.selected_template_ids or []
return SubscriptionResponse(
id=sub.id,
user_id=sub.user_id,
mode=sub.mode.value if sub.mode else "guided",
user_role=sub.user_role.value if sub.user_role else None,
selected_template_ids=template_ids,
template_names=_get_template_names(db, template_ids),
notification_email=sub.notification_email,
digest_enabled=sub.digest_enabled if sub.digest_enabled is not None else True,
digest_frequency=sub.digest_frequency or "weekly",
wizard_completed=sub.wizard_completed if sub.wizard_completed is not None else False,
is_active=sub.is_active if sub.is_active is not None else True,
created_at=sub.created_at,
updated_at=sub.updated_at,
)
# ============================================================================
# Endpoints
# ============================================================================
@router.post("", response_model=SubscriptionResponse)
async def create_subscription(
request: SubscriptionCreate,
db: DBSession = Depends(get_db)
):
"""
Erstelle eine neue Alert-Subscription.
Im Guided Mode werden 1-3 Templates ausgewählt.
Im Expert Mode wird ein eigenes Profil konfiguriert.
"""
user_id = get_user_id_from_request()
# Validiere Modus
try:
mode = AlertModeEnum(request.mode)
except ValueError:
raise HTTPException(status_code=400, detail="Ungültiger Modus. Erlaubt: 'guided', 'expert'")
# Validiere Rolle
user_role = None
if request.user_role:
try:
user_role = UserRoleEnum(request.user_role)
except ValueError:
raise HTTPException(
status_code=400,
detail="Ungültige Rolle. Erlaubt: 'lehrkraft', 'schulleitung', 'it_beauftragte'"
)
# Validiere Template-IDs
if request.template_ids:
if len(request.template_ids) > 3:
raise HTTPException(status_code=400, detail="Maximal 3 Templates erlaubt")
# Prüfe ob Templates existieren
existing = db.query(AlertTemplateDB).filter(
AlertTemplateDB.id.in_(request.template_ids)
).count()
if existing != len(request.template_ids):
raise HTTPException(status_code=400, detail="Eine oder mehrere Template-IDs sind ungültig")
# Erstelle Subscription
subscription = UserAlertSubscriptionDB(
id=str(uuid.uuid4()),
user_id=user_id,
mode=mode,
user_role=user_role,
selected_template_ids=request.template_ids,
notification_email=request.notification_email,
digest_enabled=request.digest_enabled,
digest_frequency=request.digest_frequency,
wizard_completed=len(request.template_ids) > 0, # Abgeschlossen wenn Templates gewählt
is_active=True,
)
db.add(subscription)
db.commit()
db.refresh(subscription)
return _subscription_to_response(subscription, db)
@router.get("", response_model=SubscriptionListResponse)
async def list_subscriptions(
active_only: bool = Query(True, description="Nur aktive Subscriptions"),
db: DBSession = Depends(get_db)
):
"""Liste alle Subscriptions des aktuellen Users."""
user_id = get_user_id_from_request()
query = db.query(UserAlertSubscriptionDB).filter(
UserAlertSubscriptionDB.user_id == user_id
)
if active_only:
query = query.filter(UserAlertSubscriptionDB.is_active == True)
subscriptions = query.order_by(UserAlertSubscriptionDB.created_at.desc()).all()
return SubscriptionListResponse(
subscriptions=[_subscription_to_response(s, db) for s in subscriptions],
total=len(subscriptions)
)
@router.get("/{subscription_id}", response_model=SubscriptionResponse)
async def get_subscription(
subscription_id: str,
db: DBSession = Depends(get_db)
):
"""Hole Details einer Subscription."""
user_id = get_user_id_from_request()
subscription = db.query(UserAlertSubscriptionDB).filter(
UserAlertSubscriptionDB.id == subscription_id,
UserAlertSubscriptionDB.user_id == user_id
).first()
if not subscription:
raise HTTPException(status_code=404, detail="Subscription nicht gefunden")
return _subscription_to_response(subscription, db)
@router.put("/{subscription_id}", response_model=SubscriptionResponse)
async def update_subscription(
subscription_id: str,
request: SubscriptionUpdate,
db: DBSession = Depends(get_db)
):
"""Aktualisiere eine Subscription."""
user_id = get_user_id_from_request()
subscription = db.query(UserAlertSubscriptionDB).filter(
UserAlertSubscriptionDB.id == subscription_id,
UserAlertSubscriptionDB.user_id == user_id
).first()
if not subscription:
raise HTTPException(status_code=404, detail="Subscription nicht gefunden")
# Update Felder
if request.template_ids is not None:
if len(request.template_ids) > 3:
raise HTTPException(status_code=400, detail="Maximal 3 Templates erlaubt")
subscription.selected_template_ids = request.template_ids
if request.notification_email is not None:
subscription.notification_email = request.notification_email
if request.digest_enabled is not None:
subscription.digest_enabled = request.digest_enabled
if request.digest_frequency is not None:
subscription.digest_frequency = request.digest_frequency
if request.is_active is not None:
subscription.is_active = request.is_active
subscription.updated_at = datetime.utcnow()
db.commit()
db.refresh(subscription)
return _subscription_to_response(subscription, db)
@router.delete("/{subscription_id}")
async def deactivate_subscription(
subscription_id: str,
db: DBSession = Depends(get_db)
):
"""Deaktiviere eine Subscription (Soft-Delete)."""
user_id = get_user_id_from_request()
subscription = db.query(UserAlertSubscriptionDB).filter(
UserAlertSubscriptionDB.id == subscription_id,
UserAlertSubscriptionDB.user_id == user_id
).first()
if not subscription:
raise HTTPException(status_code=404, detail="Subscription nicht gefunden")
subscription.is_active = False
subscription.updated_at = datetime.utcnow()
db.commit()
return {"status": "success", "message": "Subscription deaktiviert"}
@router.post("/{subscription_id}/activate-template", response_model=ActivateTemplateResponse)
async def activate_template(
subscription_id: str,
request: ActivateTemplateRequest = None,
db: DBSession = Depends(get_db)
):
"""
Aktiviere die gewählten Templates für eine Subscription.
Erstellt:
- Topics aus Template.topics_config (RSS-Feeds)
- Rules aus Template.rules_config (Filter-Regeln)
- Aktualisiert das User-Profil mit Template.profile_config
"""
user_id = get_user_id_from_request()
subscription = db.query(UserAlertSubscriptionDB).filter(
UserAlertSubscriptionDB.id == subscription_id,
UserAlertSubscriptionDB.user_id == user_id
).first()
if not subscription:
raise HTTPException(status_code=404, detail="Subscription nicht gefunden")
if not subscription.selected_template_ids:
raise HTTPException(status_code=400, detail="Keine Templates ausgewählt")
# Lade Templates
templates = db.query(AlertTemplateDB).filter(
AlertTemplateDB.id.in_(subscription.selected_template_ids)
).all()
topics_created = 0
rules_created = 0
profile_updated = False
for template in templates:
# Topics erstellen
if request is None or request.create_topics:
for topic_config in (template.topics_config or []):
topic = AlertTopicDB(
id=str(uuid.uuid4()),
user_id=user_id,
name=topic_config.get("name", f"Topic from {template.name}"),
description=f"Automatisch erstellt aus Template: {template.name}",
is_active=True,
fetch_interval_minutes=60,
)
db.add(topic)
topics_created += 1
# Rules erstellen
if request is None or request.create_rules:
for rule_config in (template.rules_config or []):
rule = AlertRuleDB(
id=str(uuid.uuid4()),
user_id=user_id,
name=rule_config.get("name", f"Rule from {template.name}"),
description=f"Automatisch erstellt aus Template: {template.name}",
conditions=rule_config.get("conditions", []),
action_type=rule_config.get("action_type", "keep"),
action_config=rule_config.get("action_config", {}),
priority=rule_config.get("priority", 50),
is_active=True,
)
db.add(rule)
rules_created += 1
# Profil aktualisieren
if template.profile_config:
profile = db.query(AlertProfileDB).filter(
AlertProfileDB.user_id == user_id
).first()
if not profile:
profile = AlertProfileDB(
id=str(uuid.uuid4()),
user_id=user_id,
name=f"Profil für {user_id}",
)
db.add(profile)
# Merge priorities
existing_priorities = profile.priorities or []
new_priorities = template.profile_config.get("priorities", [])
for p in new_priorities:
if p not in existing_priorities:
existing_priorities.append(p)
profile.priorities = existing_priorities
# Merge exclusions
existing_exclusions = profile.exclusions or []
new_exclusions = template.profile_config.get("exclusions", [])
for e in new_exclusions:
if e not in existing_exclusions:
existing_exclusions.append(e)
profile.exclusions = existing_exclusions
profile_updated = True
# Markiere Wizard als abgeschlossen
subscription.wizard_completed = True
subscription.updated_at = datetime.utcnow()
db.commit()
return ActivateTemplateResponse(
status="success",
topics_created=topics_created,
rules_created=rules_created,
profile_updated=profile_updated,
message=f"Templates aktiviert: {topics_created} Topics, {rules_created} Rules erstellt."
)