""" API Routes für den Guided Mode Wizard. Verwaltet den 3-Schritt Setup-Wizard: 1. Rolle wählen (Lehrkraft, Schulleitung, IT-Beauftragte) 2. Templates auswählen (max. 3) 3. Bestätigung und Aktivierung Zusätzlich: Migration-Wizard für bestehende Google Alerts. Endpoints: - GET /wizard/state - Aktuellen Wizard-Status abrufen - PUT /wizard/step/{step} - Schritt speichern - POST /wizard/complete - Wizard abschließen - POST /wizard/reset - Wizard zurücksetzen - POST /wizard/migrate/email - E-Mail-Migration starten - POST /wizard/migrate/rss - RSS-Import """ import uuid from typing import Optional, List, Dict, Any 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, AlertSourceDB, AlertModeEnum, UserRoleEnum, MigrationModeEnum, FeedTypeEnum ) router = APIRouter(prefix="/wizard", tags=["wizard"]) # ============================================================================ # Request/Response Models # ============================================================================ class WizardState(BaseModel): """Aktueller Wizard-Status.""" subscription_id: Optional[str] = None current_step: int = 0 # 0=nicht gestartet, 1-3=Schritte, 4=abgeschlossen is_completed: bool = False step_data: Dict[str, Any] = {} recommended_templates: List[Dict[str, Any]] = [] class Step1Data(BaseModel): """Daten für Schritt 1: Rollenwahl.""" role: str = Field(..., description="lehrkraft, schulleitung, it_beauftragte") class Step2Data(BaseModel): """Daten für Schritt 2: Template-Auswahl.""" template_ids: List[str] = Field(..., min_length=1, max_length=3) class Step3Data(BaseModel): """Daten für Schritt 3: Bestätigung.""" notification_email: Optional[str] = None digest_enabled: bool = True digest_frequency: str = "weekly" class StepResponse(BaseModel): """Response für Schritt-Update.""" status: str current_step: int next_step: int message: str recommended_templates: List[Dict[str, Any]] = [] class MigrateEmailRequest(BaseModel): """Request für E-Mail-Migration.""" original_label: Optional[str] = Field(default=None, description="Beschreibung des Alerts") class MigrateEmailResponse(BaseModel): """Response für E-Mail-Migration.""" status: str inbound_address: str instructions: List[str] source_id: str class MigrateRssRequest(BaseModel): """Request für RSS-Import.""" rss_urls: List[str] = Field(..., min_length=1, max_length=20) labels: Optional[List[str]] = None class MigrateRssResponse(BaseModel): """Response für RSS-Import.""" status: str sources_created: int topics_created: int message: str # ============================================================================ # Helper Functions # ============================================================================ def get_user_id_from_request() -> str: """Extrahiert User-ID aus Request.""" return "demo-user" def _get_or_create_subscription(db: DBSession, user_id: str) -> UserAlertSubscriptionDB: """Hole oder erstelle Subscription für Wizard.""" subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.user_id == user_id, UserAlertSubscriptionDB.wizard_completed == False ).first() if not subscription: subscription = UserAlertSubscriptionDB( id=str(uuid.uuid4()), user_id=user_id, mode=AlertModeEnum.GUIDED, wizard_step=0, wizard_completed=False, wizard_state={}, is_active=True, ) db.add(subscription) db.commit() db.refresh(subscription) return subscription def _get_recommended_templates(db: DBSession, role: str) -> List[Dict[str, Any]]: """Hole empfohlene Templates für eine Rolle.""" templates = db.query(AlertTemplateDB).filter( AlertTemplateDB.is_active == True, AlertTemplateDB.is_premium == False ).order_by(AlertTemplateDB.sort_order).all() result = [] for t in templates: if role in (t.target_roles or []): result.append({ "id": t.id, "slug": t.slug, "name": t.name, "description": t.description, "icon": t.icon, "category": t.category, "recommended": True, }) return result def _generate_inbound_address(user_id: str, source_id: str) -> str: """Generiere eindeutige Inbound-E-Mail-Adresse.""" short_id = source_id[:8] return f"alerts+{short_id}@breakpilot.app" # ============================================================================ # Wizard Endpoints # ============================================================================ @router.get("/state", response_model=WizardState) async def get_wizard_state( db: DBSession = Depends(get_db) ): """ Hole aktuellen Wizard-Status. Gibt Schritt, gespeicherte Daten und empfohlene Templates zurück. """ user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.user_id == user_id ).order_by(UserAlertSubscriptionDB.created_at.desc()).first() if not subscription: return WizardState( subscription_id=None, current_step=0, is_completed=False, step_data={}, recommended_templates=[], ) # Empfohlene Templates basierend auf Rolle role = subscription.user_role.value if subscription.user_role else None recommended = _get_recommended_templates(db, role) if role else [] return WizardState( subscription_id=subscription.id, current_step=subscription.wizard_step or 0, is_completed=subscription.wizard_completed or False, step_data=subscription.wizard_state or {}, recommended_templates=recommended, ) @router.put("/step/1", response_model=StepResponse) async def save_step_1( data: Step1Data, db: DBSession = Depends(get_db) ): """ Schritt 1: Rolle speichern. Wählt die Rolle des Nutzers und gibt passende Template-Empfehlungen. """ user_id = get_user_id_from_request() # Validiere Rolle try: role = UserRoleEnum(data.role) except ValueError: raise HTTPException( status_code=400, detail="Ungültige Rolle. Erlaubt: 'lehrkraft', 'schulleitung', 'it_beauftragte'" ) subscription = _get_or_create_subscription(db, user_id) # Update subscription.user_role = role subscription.wizard_step = 1 wizard_state = subscription.wizard_state or {} wizard_state["step1"] = {"role": data.role} subscription.wizard_state = wizard_state subscription.updated_at = datetime.utcnow() db.commit() db.refresh(subscription) # Empfohlene Templates recommended = _get_recommended_templates(db, data.role) return StepResponse( status="success", current_step=1, next_step=2, message=f"Rolle '{data.role}' gespeichert. Bitte wählen Sie jetzt Ihre Themen.", recommended_templates=recommended, ) @router.put("/step/2", response_model=StepResponse) async def save_step_2( data: Step2Data, db: DBSession = Depends(get_db) ): """ Schritt 2: Templates auswählen. Speichert die ausgewählten Templates (1-3). """ user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.user_id == user_id, UserAlertSubscriptionDB.wizard_completed == False ).first() if not subscription: raise HTTPException(status_code=400, detail="Bitte zuerst Schritt 1 abschließen") # Validiere Template-IDs templates = db.query(AlertTemplateDB).filter( AlertTemplateDB.id.in_(data.template_ids) ).all() if len(templates) != len(data.template_ids): raise HTTPException(status_code=400, detail="Eine oder mehrere Template-IDs sind ungültig") # Update subscription.selected_template_ids = data.template_ids subscription.wizard_step = 2 wizard_state = subscription.wizard_state or {} wizard_state["step2"] = { "template_ids": data.template_ids, "template_names": [t.name for t in templates], } subscription.wizard_state = wizard_state subscription.updated_at = datetime.utcnow() db.commit() return StepResponse( status="success", current_step=2, next_step=3, message=f"{len(templates)} Themen ausgewählt. Bitte bestätigen Sie Ihre Auswahl.", recommended_templates=[], ) @router.put("/step/3", response_model=StepResponse) async def save_step_3( data: Step3Data, db: DBSession = Depends(get_db) ): """ Schritt 3: Digest-Einstellungen und Bestätigung. Speichert E-Mail und Digest-Präferenzen. """ user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.user_id == user_id, UserAlertSubscriptionDB.wizard_completed == False ).first() if not subscription: raise HTTPException(status_code=400, detail="Bitte zuerst Schritte 1 und 2 abschließen") if not subscription.selected_template_ids: raise HTTPException(status_code=400, detail="Bitte zuerst Templates auswählen (Schritt 2)") # Update subscription.notification_email = data.notification_email subscription.digest_enabled = data.digest_enabled subscription.digest_frequency = data.digest_frequency subscription.wizard_step = 3 wizard_state = subscription.wizard_state or {} wizard_state["step3"] = { "notification_email": data.notification_email, "digest_enabled": data.digest_enabled, "digest_frequency": data.digest_frequency, } subscription.wizard_state = wizard_state subscription.updated_at = datetime.utcnow() db.commit() return StepResponse( status="success", current_step=3, next_step=4, message="Einstellungen gespeichert. Klicken Sie auf 'Jetzt starten' um den Wizard abzuschließen.", recommended_templates=[], ) @router.post("/complete") async def complete_wizard( db: DBSession = Depends(get_db) ): """ Wizard abschließen und Templates aktivieren. Erstellt Topics, Rules und Profile basierend auf den gewählten Templates. """ user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.user_id == user_id, UserAlertSubscriptionDB.wizard_completed == False ).first() if not subscription: raise HTTPException(status_code=400, detail="Kein aktiver Wizard gefunden") if not subscription.selected_template_ids: raise HTTPException(status_code=400, detail="Bitte zuerst Templates auswählen") # Aktiviere Templates (über Subscription-Endpoint) from .subscriptions import activate_template, ActivateTemplateRequest # Markiere als abgeschlossen subscription.wizard_completed = True subscription.wizard_step = 4 subscription.updated_at = datetime.utcnow() db.commit() return { "status": "success", "message": "Wizard abgeschlossen! Ihre Alerts werden ab jetzt gesammelt.", "subscription_id": subscription.id, "selected_templates": subscription.selected_template_ids, "next_action": "Besuchen Sie die Inbox, um Ihre ersten Alerts zu sehen.", } @router.post("/reset") async def reset_wizard( db: DBSession = Depends(get_db) ): """Wizard zurücksetzen (für Neustart).""" user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.user_id == user_id, UserAlertSubscriptionDB.wizard_completed == False ).first() if subscription: db.delete(subscription) db.commit() return { "status": "success", "message": "Wizard zurückgesetzt. Sie können neu beginnen.", } # ============================================================================ # Migration Endpoints (für bestehende Google Alerts) # ============================================================================ @router.post("/migrate/email", response_model=MigrateEmailResponse) async def start_email_migration( request: MigrateEmailRequest = None, db: DBSession = Depends(get_db) ): """ Starte E-Mail-Migration für bestehende Google Alerts. Generiert eine eindeutige Inbound-E-Mail-Adresse, an die der Nutzer seine Google Alerts weiterleiten kann. """ user_id = get_user_id_from_request() # Erstelle AlertSource source = AlertSourceDB( id=str(uuid.uuid4()), user_id=user_id, source_type=FeedTypeEnum.EMAIL, original_label=request.original_label if request else "Google Alert Migration", migration_mode=MigrationModeEnum.FORWARD, is_active=True, ) # Generiere Inbound-Adresse source.inbound_address = _generate_inbound_address(user_id, source.id) db.add(source) db.commit() db.refresh(source) return MigrateEmailResponse( status="success", inbound_address=source.inbound_address, source_id=source.id, instructions=[ "1. Öffnen Sie Google Alerts (google.com/alerts)", "2. Klicken Sie auf das Bearbeiten-Symbol bei Ihrem Alert", f"3. Ändern Sie die E-Mail-Adresse zu: {source.inbound_address}", "4. Speichern Sie die Änderung", "5. Ihre Alerts werden automatisch importiert und gefiltert", ], ) @router.post("/migrate/rss", response_model=MigrateRssResponse) async def import_rss_feeds( request: MigrateRssRequest, db: DBSession = Depends(get_db) ): """ Importiere bestehende Google Alert RSS-Feeds. Erstellt für jede RSS-URL einen AlertSource und Topic. """ user_id = get_user_id_from_request() from ..db.models import AlertTopicDB sources_created = 0 topics_created = 0 for i, url in enumerate(request.rss_urls): # Label aus Request oder generieren label = None if request.labels and i < len(request.labels): label = request.labels[i] if not label: label = f"RSS Feed {i + 1}" # Erstelle AlertSource source = AlertSourceDB( id=str(uuid.uuid4()), user_id=user_id, source_type=FeedTypeEnum.RSS, original_label=label, rss_url=url, migration_mode=MigrationModeEnum.IMPORT, is_active=True, ) db.add(source) sources_created += 1 # Erstelle Topic topic = AlertTopicDB( id=str(uuid.uuid4()), user_id=user_id, name=label, description=f"Importiert aus RSS: {url[:50]}...", feed_url=url, feed_type=FeedTypeEnum.RSS, is_active=True, fetch_interval_minutes=60, ) db.add(topic) # Verknüpfe Source mit Topic source.topic_id = topic.id topics_created += 1 db.commit() return MigrateRssResponse( status="success", sources_created=sources_created, topics_created=topics_created, message=f"{sources_created} RSS-Feeds importiert. Die Alerts werden automatisch abgerufen.", ) @router.get("/migrate/sources") async def list_migration_sources( db: DBSession = Depends(get_db) ): """Liste alle Migration-Quellen des Users.""" user_id = get_user_id_from_request() sources = db.query(AlertSourceDB).filter( AlertSourceDB.user_id == user_id ).order_by(AlertSourceDB.created_at.desc()).all() return { "sources": [ { "id": s.id, "type": s.source_type.value if s.source_type else "unknown", "label": s.original_label, "inbound_address": s.inbound_address, "rss_url": s.rss_url, "migration_mode": s.migration_mode.value if s.migration_mode else "unknown", "items_received": s.items_received, "is_active": s.is_active, "created_at": s.created_at.isoformat() if s.created_at else None, } for s in sources ], "total": len(sources), }