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:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit 21a844cb8a
1986 changed files with 744143 additions and 1731 deletions

View File

@@ -0,0 +1,43 @@
"""
State Engine - Herzstück des BreakPilot Begleiter-Modus.
Komponenten:
1. Schuljahres-State-Machine (Phasen)
2. Antizipations-Engine (Regeln + Vorschläge)
3. TeacherContext (Aggregierter Kontext)
"""
from .models import (
SchoolYearPhase,
PhaseInfo,
TeacherContext,
ClassSummary,
Event,
TeacherStats,
Milestone,
PHASE_INFO,
get_phase_info
)
from .rules import Rule, Suggestion, SuggestionPriority, RULES
from .engine import AnticipationEngine, PhaseService
__all__ = [
# Models
"SchoolYearPhase",
"PhaseInfo",
"TeacherContext",
"ClassSummary",
"Event",
"TeacherStats",
"Milestone",
"PHASE_INFO",
"get_phase_info",
# Rules
"Rule",
"Suggestion",
"SuggestionPriority",
"RULES",
# Engine
"AnticipationEngine",
"PhaseService",
]

View File

@@ -0,0 +1,367 @@
"""
State Engine - Antizipations-Engine und Phasen-Service.
Komponenten:
- AnticipationEngine: Evaluiert Regeln und generiert Vorschläge
- PhaseService: Verwaltet Phasen-Übergänge
"""
import logging
from datetime import datetime
from typing import List, Dict, Any, Optional, Callable
from dataclasses import dataclass
from .models import (
SchoolYearPhase,
TeacherContext,
PhaseInfo,
get_phase_info,
PHASE_INFO
)
from .rules import Rule, Suggestion, SuggestionPriority, RULES
logger = logging.getLogger(__name__)
# ============================================================================
# Phasen-Übergänge
# ============================================================================
@dataclass
class PhaseTransition:
"""Definition eines erlaubten Phasen-Übergangs."""
from_phase: SchoolYearPhase
to_phase: SchoolYearPhase
condition: Callable[[TeacherContext], bool]
auto_trigger: bool = False # Automatisch wenn Condition erfüllt
# Vordefinierte Übergänge
VALID_TRANSITIONS: List[PhaseTransition] = [
# Onboarding → SchoolYearStart
PhaseTransition(
from_phase=SchoolYearPhase.ONBOARDING,
to_phase=SchoolYearPhase.SCHOOL_YEAR_START,
condition=lambda ctx: (
ctx.has_completed_milestone("school_select") and
ctx.has_completed_milestone("consent_accept") and
ctx.has_completed_milestone("profile_complete")
),
auto_trigger=True,
),
# SchoolYearStart → TeachingSetup
PhaseTransition(
from_phase=SchoolYearPhase.SCHOOL_YEAR_START,
to_phase=SchoolYearPhase.TEACHING_SETUP,
condition=lambda ctx: (
len(ctx.classes) > 0 and
ctx.has_completed_milestone("add_students")
),
auto_trigger=True,
),
# TeachingSetup → Performance1
PhaseTransition(
from_phase=SchoolYearPhase.TEACHING_SETUP,
to_phase=SchoolYearPhase.PERFORMANCE_1,
condition=lambda ctx: (
ctx.weeks_since_start >= 6 and
ctx.has_learning_units()
),
auto_trigger=False, # Manueller Übergang
),
# Performance1 → SemesterEnd
PhaseTransition(
from_phase=SchoolYearPhase.PERFORMANCE_1,
to_phase=SchoolYearPhase.SEMESTER_END,
condition=lambda ctx: (
(ctx.is_in_month(1) or ctx.is_in_month(2)) and
ctx.has_completed_milestone("enter_grades")
),
auto_trigger=True,
),
# SemesterEnd → Teaching2
PhaseTransition(
from_phase=SchoolYearPhase.SEMESTER_END,
to_phase=SchoolYearPhase.TEACHING_2,
condition=lambda ctx: (
ctx.has_completed_milestone("generate_certificates")
),
auto_trigger=True,
),
# Teaching2 → Performance2
PhaseTransition(
from_phase=SchoolYearPhase.TEACHING_2,
to_phase=SchoolYearPhase.PERFORMANCE_2,
condition=lambda ctx: (
ctx.weeks_since_start >= 30 or
ctx.is_in_month(4) or ctx.is_in_month(5)
),
auto_trigger=False,
),
# Performance2 → YearEnd
PhaseTransition(
from_phase=SchoolYearPhase.PERFORMANCE_2,
to_phase=SchoolYearPhase.YEAR_END,
condition=lambda ctx: (
(ctx.is_in_month(6) or ctx.is_in_month(7)) and
ctx.has_completed_milestone("enter_final_grades")
),
auto_trigger=True,
),
# YearEnd → Archived
PhaseTransition(
from_phase=SchoolYearPhase.YEAR_END,
to_phase=SchoolYearPhase.ARCHIVED,
condition=lambda ctx: (
ctx.has_completed_milestone("generate_final_certificates") and
ctx.has_completed_milestone("archive_year")
),
auto_trigger=True,
),
]
class PhaseService:
"""
Verwaltet Schuljahres-Phasen und deren Übergänge.
Funktionen:
- Phasen-Status abrufen
- Automatische Übergänge prüfen
- Manuelle Übergänge durchführen
"""
def __init__(self, transitions: List[PhaseTransition] = None):
self.transitions = transitions or VALID_TRANSITIONS
def get_current_phase_info(self, phase: SchoolYearPhase) -> PhaseInfo:
"""Gibt Metadaten zur aktuellen Phase zurück."""
return get_phase_info(phase)
def get_all_phases(self) -> List[Dict[str, Any]]:
"""Gibt alle Phasen mit Metadaten zurück."""
return [
{
"phase": info.phase.value,
"display_name": info.display_name,
"description": info.description,
"typical_months": info.typical_months,
}
for info in PHASE_INFO.values()
]
def check_and_transition(self, ctx: TeacherContext) -> Optional[SchoolYearPhase]:
"""
Prüft ob ein automatischer Phasen-Übergang möglich ist.
Args:
ctx: Aktueller TeacherContext
Returns:
Neue Phase wenn Übergang stattfand, sonst None
"""
current_phase = ctx.current_phase
for transition in self.transitions:
if (transition.from_phase == current_phase and
transition.auto_trigger and
transition.condition(ctx)):
logger.info(
f"Auto-transition from {current_phase} to {transition.to_phase} "
f"for teacher {ctx.teacher_id}"
)
return transition.to_phase
return None
def can_transition_to(self, ctx: TeacherContext, target_phase: SchoolYearPhase) -> bool:
"""
Prüft ob ein Übergang zu einer bestimmten Phase möglich ist.
Args:
ctx: Aktueller TeacherContext
target_phase: Zielphase
Returns:
True wenn Übergang erlaubt
"""
for transition in self.transitions:
if (transition.from_phase == ctx.current_phase and
transition.to_phase == target_phase):
return transition.condition(ctx)
return False
def get_next_phase(self, current: SchoolYearPhase) -> Optional[SchoolYearPhase]:
"""Gibt die nächste Phase in der Sequenz zurück."""
phase_order = [
SchoolYearPhase.ONBOARDING,
SchoolYearPhase.SCHOOL_YEAR_START,
SchoolYearPhase.TEACHING_SETUP,
SchoolYearPhase.PERFORMANCE_1,
SchoolYearPhase.SEMESTER_END,
SchoolYearPhase.TEACHING_2,
SchoolYearPhase.PERFORMANCE_2,
SchoolYearPhase.YEAR_END,
SchoolYearPhase.ARCHIVED,
]
try:
idx = phase_order.index(current)
if idx < len(phase_order) - 1:
return phase_order[idx + 1]
except ValueError:
pass
return None
def get_progress_percentage(self, ctx: TeacherContext) -> float:
"""
Berechnet Fortschritt in der aktuellen Phase.
Returns:
Prozent (0-100)
"""
phase_info = get_phase_info(ctx.current_phase)
required = set(phase_info.required_actions)
completed = set(ctx.completed_milestones)
if not required:
return 100.0
done = len(required.intersection(completed))
return (done / len(required)) * 100
class AnticipationEngine:
"""
Evaluiert Antizipations-Regeln und generiert priorisierte Vorschläge.
Die Engine:
- Wendet alle aktiven Regeln auf den TeacherContext an
- Priorisiert Vorschläge nach Dringlichkeit
- Limitiert auf max. 5 Vorschläge
"""
def __init__(self, rules: List[Rule] = None, max_suggestions: int = 5):
self.rules = rules or RULES
self.max_suggestions = max_suggestions
def get_suggestions(self, ctx: TeacherContext) -> List[Suggestion]:
"""
Evaluiert alle Regeln und gibt priorisierte Vorschläge zurück.
Args:
ctx: TeacherContext mit allen relevanten Informationen
Returns:
Liste von max. 5 Vorschlägen, sortiert nach Priorität
"""
suggestions = []
for rule in self.rules:
try:
suggestion = rule.evaluate(ctx)
if suggestion:
suggestions.append(suggestion)
except Exception as e:
logger.warning(f"Rule {rule.id} evaluation failed: {e}")
return self._prioritize(suggestions)
def _prioritize(self, suggestions: List[Suggestion]) -> List[Suggestion]:
"""
Sortiert Vorschläge nach Priorität und limitiert.
Reihenfolge:
1. URGENT (1)
2. HIGH (2)
3. MEDIUM (3)
4. LOW (4)
"""
sorted_suggestions = sorted(
suggestions,
key=lambda s: s.priority.value
)
return sorted_suggestions[:self.max_suggestions]
def get_top_suggestion(self, ctx: TeacherContext) -> Optional[Suggestion]:
"""
Gibt den wichtigsten Vorschlag zurück.
Args:
ctx: TeacherContext
Returns:
Wichtigster Vorschlag oder None
"""
suggestions = self.get_suggestions(ctx)
return suggestions[0] if suggestions else None
def get_suggestions_by_category(
self,
ctx: TeacherContext
) -> Dict[str, List[Suggestion]]:
"""
Gruppiert Vorschläge nach Kategorie.
Returns:
Dict mit Kategorien als Keys und Listen von Vorschlägen
"""
suggestions = self.get_suggestions(ctx)
by_category: Dict[str, List[Suggestion]] = {}
for suggestion in suggestions:
if suggestion.category not in by_category:
by_category[suggestion.category] = []
by_category[suggestion.category].append(suggestion)
return by_category
def count_by_priority(self, ctx: TeacherContext) -> Dict[str, int]:
"""
Zählt Vorschläge nach Priorität.
Returns:
Dict mit Prioritäten und Anzahlen
"""
suggestions = self.get_suggestions(ctx)
counts = {
"urgent": 0,
"high": 0,
"medium": 0,
"low": 0,
}
for s in suggestions:
if s.priority == SuggestionPriority.URGENT:
counts["urgent"] += 1
elif s.priority == SuggestionPriority.HIGH:
counts["high"] += 1
elif s.priority == SuggestionPriority.MEDIUM:
counts["medium"] += 1
else:
counts["low"] += 1
return counts
# ============================================================================
# Factory Functions
# ============================================================================
def create_anticipation_engine(custom_rules: List[Rule] = None) -> AnticipationEngine:
"""Erstellt eine neue AnticipationEngine."""
return AnticipationEngine(rules=custom_rules)
def create_phase_service(custom_transitions: List[PhaseTransition] = None) -> PhaseService:
"""Erstellt einen neuen PhaseService."""
return PhaseService(transitions=custom_transitions)

View File

@@ -0,0 +1,317 @@
"""
State Engine Models - Datenstrukturen für das Phasen-Management.
Definiert:
- SchoolYearPhase: Die 9 Phasen des Schuljahres
- TeacherContext: Aggregierter Kontext für Antizipation
- Event, Milestone, Stats: Unterstützende Modelle
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import List, Optional, Dict, Any
import uuid
class SchoolYearPhase(str, Enum):
"""Die 9 Phasen eines Schuljahres."""
# Phase 1: Schuljahresbeginn (Aug/Sep)
ONBOARDING = "onboarding"
# Neue Lehrer, Schulsuche, Grundkonfiguration
# Phase 2: Schuljahresstart (Sep/Okt)
SCHOOL_YEAR_START = "school_year_start"
# Klassen anlegen, Stundenplan, erste Einheiten
# Phase 3: Unterrichtsaufbau (Okt/Nov)
TEACHING_SETUP = "teaching_setup"
# Lerneinheiten, Materialien, Elternkommunikation
# Phase 4: Leistungsphase 1 (Nov/Dez)
PERFORMANCE_1 = "performance_1"
# Klausuren, Korrektur, erste Noten
# Phase 5: Halbjahresabschluss (Jan/Feb)
SEMESTER_END = "semester_end"
# Halbjahreszeugnisse, Konferenzen, Elterngespräche
# Phase 6: 2. Halbjahr Unterricht (Feb/Apr)
TEACHING_2 = "teaching_2"
# Wiederholung von Phase 3
# Phase 7: Leistungsphase 2 (Apr/Jun)
PERFORMANCE_2 = "performance_2"
# Klausuren, Korrektur, finale Noten
# Phase 8: Jahresabschluss (Jun/Jul)
YEAR_END = "year_end"
# Abschlusszeugnisse, Versetzung, Archivierung
# Phase 9: Archiviert
ARCHIVED = "archived"
# Schuljahr abgeschlossen
@dataclass
class PhaseInfo:
"""Metadaten zu einer Phase."""
phase: SchoolYearPhase
display_name: str
description: str
typical_months: List[int] # 1-12
expected_duration_weeks: int
required_actions: List[str]
optional_actions: List[str]
# Phasen-Definitionen mit Metadaten
PHASE_INFO: Dict[SchoolYearPhase, PhaseInfo] = {
SchoolYearPhase.ONBOARDING: PhaseInfo(
phase=SchoolYearPhase.ONBOARDING,
display_name="Onboarding",
description="Willkommen bei BreakPilot! Richte dein Schuljahr ein.",
typical_months=[8, 9],
expected_duration_weeks=2,
required_actions=["school_select", "consent_accept", "profile_complete"],
optional_actions=["import_previous_year"],
),
SchoolYearPhase.SCHOOL_YEAR_START: PhaseInfo(
phase=SchoolYearPhase.SCHOOL_YEAR_START,
display_name="Schuljahresstart",
description="Lege deine Klassen und den Stundenplan an.",
typical_months=[9, 10],
expected_duration_weeks=3,
required_actions=["create_classes", "add_students", "create_timetable"],
optional_actions=["import_students_csv", "invite_parents"],
),
SchoolYearPhase.TEACHING_SETUP: PhaseInfo(
phase=SchoolYearPhase.TEACHING_SETUP,
display_name="Unterrichtsaufbau",
description="Erstelle Lerneinheiten und Materialien.",
typical_months=[10, 11],
expected_duration_weeks=4,
required_actions=["create_learning_units"],
optional_actions=["generate_worksheets", "prepare_parent_meeting"],
),
SchoolYearPhase.PERFORMANCE_1: PhaseInfo(
phase=SchoolYearPhase.PERFORMANCE_1,
display_name="Leistungsphase 1",
description="Erste Klausuren und Bewertungen.",
typical_months=[11, 12],
expected_duration_weeks=6,
required_actions=["schedule_exams", "enter_grades"],
optional_actions=["use_correction_module", "generate_feedback"],
),
SchoolYearPhase.SEMESTER_END: PhaseInfo(
phase=SchoolYearPhase.SEMESTER_END,
display_name="Halbjahresabschluss",
description="Halbjahreszeugnisse und Konferenzen.",
typical_months=[1, 2],
expected_duration_weeks=3,
required_actions=["complete_grades", "generate_certificates"],
optional_actions=["parent_conferences", "archive_semester"],
),
SchoolYearPhase.TEACHING_2: PhaseInfo(
phase=SchoolYearPhase.TEACHING_2,
display_name="2. Halbjahr",
description="Weiterführender Unterricht im 2. Halbjahr.",
typical_months=[2, 3, 4],
expected_duration_weeks=8,
required_actions=["update_learning_units"],
optional_actions=["generate_worksheets"],
),
SchoolYearPhase.PERFORMANCE_2: PhaseInfo(
phase=SchoolYearPhase.PERFORMANCE_2,
display_name="Leistungsphase 2",
description="Finale Klausuren und Bewertungen.",
typical_months=[4, 5, 6],
expected_duration_weeks=8,
required_actions=["schedule_exams", "enter_final_grades"],
optional_actions=["use_correction_module"],
),
SchoolYearPhase.YEAR_END: PhaseInfo(
phase=SchoolYearPhase.YEAR_END,
display_name="Jahresabschluss",
description="Abschlusszeugnisse und Versetzung.",
typical_months=[6, 7],
expected_duration_weeks=3,
required_actions=["complete_all_grades", "generate_final_certificates"],
optional_actions=["archive_year", "export_data"],
),
SchoolYearPhase.ARCHIVED: PhaseInfo(
phase=SchoolYearPhase.ARCHIVED,
display_name="Archiviert",
description="Das Schuljahr ist abgeschlossen.",
typical_months=[7, 8],
expected_duration_weeks=0,
required_actions=[],
optional_actions=["view_archive"],
),
}
def get_phase_info(phase: SchoolYearPhase) -> PhaseInfo:
"""Gibt Metadaten für eine Phase zurück."""
return PHASE_INFO.get(phase, PHASE_INFO[SchoolYearPhase.ONBOARDING])
@dataclass
class ClassSummary:
"""Zusammenfassung einer Klasse."""
class_id: str
name: str
grade_level: int
student_count: int
subject: str
@dataclass
class Event:
"""Ein anstehendes Ereignis."""
type: str # "exam", "parent_meeting", "deadline"
title: str
date: datetime
in_days: int
class_id: Optional[str] = None
priority: str = "medium" # "high", "medium", "low"
@dataclass
class Milestone:
"""Ein erreichter Meilenstein."""
milestone: str
completed_at: datetime
@dataclass
class TeacherStats:
"""Statistiken eines Lehrers."""
learning_units_created: int = 0
exams_scheduled: int = 0
exams_graded: int = 0
grades_entered: int = 0
parent_messages_count: int = 0
avg_response_time_hours: float = 0.0
unanswered_messages: int = 0
@dataclass
class TeacherContext:
"""
Aggregierter Kontext für einen Lehrer.
Enthält alle relevanten Informationen für die Antizipations-Engine:
- Identifikation
- Schulkontext
- Zeitlicher Kontext
- Klassen und Schüler
- Termine und Events
- Fortschritt
- Statistiken
"""
# Identifikation
teacher_id: str
school_id: str
school_year_id: str
# Schulkontext
federal_state: str = "niedersachsen" # Bundesland
school_type: str = "gymnasium" # Schulform
# Zeitlicher Kontext
school_year_start: datetime = field(default_factory=datetime.now)
current_phase: SchoolYearPhase = SchoolYearPhase.ONBOARDING
phase_entered_at: datetime = field(default_factory=datetime.now)
weeks_since_start: int = 0
days_in_phase: int = 0
# Klassen und Schüler
classes: List[ClassSummary] = field(default_factory=list)
total_students: int = 0
# Termine und Events
upcoming_events: List[Event] = field(default_factory=list)
overdue_actions: List[Dict[str, Any]] = field(default_factory=list)
# Fortschritt
completed_milestones: List[str] = field(default_factory=list)
pending_milestones: List[str] = field(default_factory=list)
# Statistiken
stats: TeacherStats = field(default_factory=TeacherStats)
def has_completed_milestone(self, milestone: str) -> bool:
"""Prüft ob ein Meilenstein erreicht wurde."""
return milestone in self.completed_milestones
def has_learning_units(self) -> bool:
"""Prüft ob Lerneinheiten erstellt wurden."""
return self.stats.learning_units_created > 0
def is_in_month(self, month: int) -> bool:
"""Prüft ob aktueller Monat übereinstimmt."""
return datetime.now().month == month
def get_next_deadline(self) -> Optional[Event]:
"""Gibt die nächste Deadline zurück."""
for e in self.upcoming_events:
if e.type == "deadline":
return e
return None
def get_next_exam(self) -> Optional[Event]:
"""Gibt die nächste Klausur zurück."""
for e in self.upcoming_events:
if e.type == "exam" and e.in_days > 0:
return e
return None
def to_dict(self) -> Dict[str, Any]:
"""Konvertiert zu Dictionary."""
return {
"teacher_id": self.teacher_id,
"school_id": self.school_id,
"school_year_id": self.school_year_id,
"federal_state": self.federal_state,
"school_type": self.school_type,
"school_year_start": self.school_year_start.isoformat(),
"current_phase": self.current_phase.value,
"phase_entered_at": self.phase_entered_at.isoformat(),
"weeks_since_start": self.weeks_since_start,
"days_in_phase": self.days_in_phase,
"classes": [
{
"class_id": c.class_id,
"name": c.name,
"grade_level": c.grade_level,
"student_count": c.student_count,
"subject": c.subject,
}
for c in self.classes
],
"total_students": self.total_students,
"upcoming_events": [
{
"type": e.type,
"title": e.title,
"date": e.date.isoformat(),
"in_days": e.in_days,
"class_id": e.class_id,
"priority": e.priority,
}
for e in self.upcoming_events
],
"completed_milestones": self.completed_milestones,
"pending_milestones": self.pending_milestones,
"stats": {
"learning_units_created": self.stats.learning_units_created,
"exams_scheduled": self.stats.exams_scheduled,
"exams_graded": self.stats.exams_graded,
"grades_entered": self.stats.grades_entered,
"parent_messages_count": self.stats.parent_messages_count,
"avg_response_time_hours": self.stats.avg_response_time_hours,
"unanswered_messages": self.stats.unanswered_messages,
},
}

View File

@@ -0,0 +1,484 @@
"""
State Engine Rules - Antizipations-Regeln für proaktive Vorschläge.
Definiert:
- Suggestion: Ein Vorschlag mit Priorität und Aktion
- Rule: Eine Regel die auf TeacherContext angewendet wird
- RULES: Vordefinierte Regeln (15+)
"""
from dataclasses import dataclass
from enum import Enum
from typing import Callable, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .models import TeacherContext
class SuggestionPriority(Enum):
"""Priorität eines Vorschlags."""
URGENT = 1 # Sofort erforderlich
HIGH = 2 # Heute/Diese Woche
MEDIUM = 3 # Diese Woche/Bald
LOW = 4 # Irgendwann
@dataclass
class Suggestion:
"""Ein Vorschlag für den Lehrer."""
id: str
title: str
description: str
action_type: str # "navigate", "create", "remind"
action_target: str # URL oder Action-ID
priority: SuggestionPriority
category: str # "classes", "grades", "communication", etc.
icon: str # Material Icon Name
estimated_time: int # Minuten
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"action_type": self.action_type,
"action_target": self.action_target,
"priority": self.priority.name,
"priority_value": self.priority.value,
"category": self.category,
"icon": self.icon,
"estimated_time": self.estimated_time,
}
@dataclass
class Rule:
"""Eine Antizipations-Regel."""
id: str
name: str
condition: Callable[['TeacherContext'], bool]
suggestion_generator: Callable[['TeacherContext'], Suggestion]
applies_to_phases: List[str] # Leere Liste = alle Phasen
def evaluate(self, ctx: 'TeacherContext') -> Optional[Suggestion]:
"""
Evaluiert die Regel gegen den Kontext.
Returns:
Suggestion wenn Regel zutrifft, sonst None
"""
# Prüfe Phasen-Einschränkung
if self.applies_to_phases:
if ctx.current_phase.value not in self.applies_to_phases:
return None
# Prüfe Condition
try:
if not self.condition(ctx):
return None
except Exception:
return None
return self.suggestion_generator(ctx)
# ============================================================================
# Helper Functions für Regeln
# ============================================================================
def _get_parent_meeting_days(ctx: 'TeacherContext') -> int:
"""Gibt Tage bis zum nächsten Elternabend zurück."""
for e in ctx.upcoming_events:
if e.type == "parent_meeting":
return e.in_days
return 999
def _get_next_exam_days(ctx: 'TeacherContext') -> int:
"""Gibt Tage bis zur nächsten Klausur zurück."""
for e in ctx.upcoming_events:
if e.type == "exam" and e.in_days > 0:
return e.in_days
return 999
def _get_uncorrected_exams(ctx: 'TeacherContext') -> int:
"""Gibt Anzahl unkorrigierter Klausuren zurück."""
return ctx.stats.exams_scheduled - ctx.stats.exams_graded
# ============================================================================
# Vordefinierte Regeln (15+)
# ============================================================================
RULES: List[Rule] = [
# ═══════════════════════════════════════════════════════════════════════
# ONBOARDING / SCHULJAHRESSTART
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="no_classes",
name="Keine Klassen angelegt",
condition=lambda ctx: len(ctx.classes) == 0,
suggestion_generator=lambda ctx: Suggestion(
id="create_first_class",
title="Erste Klasse anlegen",
description="Lege deine erste Klasse an, um loszulegen.",
action_type="navigate",
action_target="/studio/school",
priority=SuggestionPriority.URGENT,
category="classes",
icon="group_add",
estimated_time=5,
),
applies_to_phases=["onboarding", "school_year_start"],
),
Rule(
id="no_students",
name="Klassen ohne Schüler",
condition=lambda ctx: len(ctx.classes) > 0 and ctx.total_students == 0,
suggestion_generator=lambda ctx: Suggestion(
id="add_students",
title="Schüler hinzufügen",
description=f"Deine {len(ctx.classes)} Klasse(n) haben noch keine Schüler.",
action_type="navigate",
action_target="/studio/school",
priority=SuggestionPriority.HIGH,
category="classes",
icon="person_add",
estimated_time=10,
),
applies_to_phases=["school_year_start"],
),
Rule(
id="consent_missing",
name="Einwilligung ausstehend",
condition=lambda ctx: not ctx.has_completed_milestone("consent_accept"),
suggestion_generator=lambda ctx: Suggestion(
id="accept_consent",
title="Datenschutz-Einwilligung",
description="Bitte akzeptiere die Datenschutzbestimmungen.",
action_type="navigate",
action_target="/studio/consent",
priority=SuggestionPriority.URGENT,
category="settings",
icon="security",
estimated_time=2,
),
applies_to_phases=["onboarding"],
),
Rule(
id="profile_incomplete",
name="Profil unvollständig",
condition=lambda ctx: not ctx.has_completed_milestone("profile_complete"),
suggestion_generator=lambda ctx: Suggestion(
id="complete_profile",
title="Profil vervollständigen",
description="Vervollständige dein Profil für bessere Personalisierung.",
action_type="navigate",
action_target="/studio/profile",
priority=SuggestionPriority.HIGH,
category="settings",
icon="account_circle",
estimated_time=5,
),
applies_to_phases=["onboarding", "school_year_start"],
),
# ═══════════════════════════════════════════════════════════════════════
# UNTERRICHT
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="no_learning_units",
name="Keine Lerneinheiten",
condition=lambda ctx: (
ctx.current_phase.value in ["teaching_setup", "teaching_2"] and
ctx.stats.learning_units_created == 0
),
suggestion_generator=lambda ctx: Suggestion(
id="create_learning_unit",
title="Erste Lerneinheit erstellen",
description="Erstelle Lerneinheiten für deine Klassen.",
action_type="navigate",
action_target="/studio/worksheets",
priority=SuggestionPriority.HIGH,
category="teaching",
icon="auto_stories",
estimated_time=15,
),
applies_to_phases=["teaching_setup", "teaching_2"],
),
Rule(
id="few_learning_units",
name="Wenige Lerneinheiten",
condition=lambda ctx: (
ctx.current_phase.value in ["teaching_setup", "teaching_2"] and
0 < ctx.stats.learning_units_created < 3
),
suggestion_generator=lambda ctx: Suggestion(
id="create_more_units",
title="Weitere Lerneinheiten erstellen",
description=f"Du hast {ctx.stats.learning_units_created} Lerneinheit(en). Erstelle mehr für abwechslungsreichen Unterricht.",
action_type="navigate",
action_target="/studio/worksheets",
priority=SuggestionPriority.MEDIUM,
category="teaching",
icon="library_add",
estimated_time=15,
),
applies_to_phases=["teaching_setup", "teaching_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# ELTERNKOMMUNIKATION
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="parent_meeting_upcoming",
name="Elternabend steht bevor",
condition=lambda ctx: any(
e.type == "parent_meeting" and e.in_days <= 14
for e in ctx.upcoming_events
),
suggestion_generator=lambda ctx: Suggestion(
id="prepare_parent_meeting",
title="Elternabend vorbereiten",
description=f"In {_get_parent_meeting_days(ctx)} Tagen ist Elternabend.",
action_type="navigate",
action_target="/studio/letters",
priority=SuggestionPriority.HIGH,
category="communication",
icon="groups",
estimated_time=30,
),
applies_to_phases=[], # Alle Phasen
),
Rule(
id="unanswered_parent_messages",
name="Unbeantwortete Elternnachrichten",
condition=lambda ctx: ctx.stats.unanswered_messages > 0,
suggestion_generator=lambda ctx: Suggestion(
id="answer_messages",
title="Elternnachrichten beantworten",
description=f"{ctx.stats.unanswered_messages} Nachricht(en) warten auf Antwort.",
action_type="navigate",
action_target="/studio/messenger",
priority=SuggestionPriority.HIGH if ctx.stats.unanswered_messages > 3 else SuggestionPriority.MEDIUM,
category="communication",
icon="mail",
estimated_time=15,
),
applies_to_phases=[], # Alle Phasen
),
Rule(
id="no_parent_contact",
name="Keine Elternkontakte",
condition=lambda ctx: (
ctx.total_students > 0 and
ctx.stats.parent_messages_count == 0 and
ctx.weeks_since_start >= 4
),
suggestion_generator=lambda ctx: Suggestion(
id="start_parent_communication",
title="Elternkommunikation starten",
description="Nimm Kontakt mit den Eltern auf.",
action_type="navigate",
action_target="/studio/letters",
priority=SuggestionPriority.MEDIUM,
category="communication",
icon="family_restroom",
estimated_time=20,
),
applies_to_phases=["teaching_setup", "teaching_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# LEISTUNG / KLAUSUREN
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="exam_in_7_days",
name="Klausur in 7 Tagen",
condition=lambda ctx: any(
e.type == "exam" and 0 < e.in_days <= 7
for e in ctx.upcoming_events
),
suggestion_generator=lambda ctx: Suggestion(
id="prepare_exam",
title="Klausur vorbereiten",
description=f"Klausur in {_get_next_exam_days(ctx)} Tagen.",
action_type="navigate",
action_target="/studio/worksheets",
priority=SuggestionPriority.URGENT,
category="exams",
icon="quiz",
estimated_time=60,
),
applies_to_phases=["performance_1", "performance_2"],
),
Rule(
id="exam_needs_correction",
name="Klausur wartet auf Korrektur",
condition=lambda ctx: _get_uncorrected_exams(ctx) > 0,
suggestion_generator=lambda ctx: Suggestion(
id="correct_exam",
title="Klausur korrigieren",
description=f"{_get_uncorrected_exams(ctx)} Klausur(en) warten auf Korrektur.",
action_type="navigate",
action_target="/studio/correction",
priority=SuggestionPriority.URGENT,
category="exams",
icon="grading",
estimated_time=120,
),
applies_to_phases=["performance_1", "performance_2"],
),
Rule(
id="no_exams_scheduled",
name="Keine Klausuren geplant",
condition=lambda ctx: (
ctx.current_phase.value in ["performance_1", "performance_2"] and
ctx.stats.exams_scheduled == 0
),
suggestion_generator=lambda ctx: Suggestion(
id="schedule_exams",
title="Klausuren planen",
description="Plane die Klausuren für diese Leistungsphase.",
action_type="navigate",
action_target="/studio/school",
priority=SuggestionPriority.HIGH,
category="exams",
icon="event",
estimated_time=15,
),
applies_to_phases=["performance_1", "performance_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# NOTEN / ZEUGNISSE
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="grades_missing",
name="Noten fehlen",
condition=lambda ctx: (
ctx.current_phase.value in ["semester_end", "year_end"] and
not ctx.has_completed_milestone("complete_grades")
),
suggestion_generator=lambda ctx: Suggestion(
id="enter_grades",
title="Noten eintragen",
description="Trage alle Noten für die Zeugnisse ein.",
action_type="navigate",
action_target="/studio/gradebook",
priority=SuggestionPriority.URGENT,
category="grades",
icon="calculate",
estimated_time=30,
),
applies_to_phases=["semester_end", "year_end"],
),
Rule(
id="certificates_due",
name="Zeugnisse erstellen",
condition=lambda ctx: (
ctx.current_phase.value in ["semester_end", "year_end"] and
ctx.has_completed_milestone("complete_grades") and
not ctx.has_completed_milestone("generate_certificates")
),
suggestion_generator=lambda ctx: Suggestion(
id="generate_certificates",
title="Zeugnisse erstellen",
description="Alle Noten sind eingetragen. Erstelle jetzt die Zeugnisse.",
action_type="navigate",
action_target="/studio/certificates",
priority=SuggestionPriority.HIGH,
category="certificates",
icon="description",
estimated_time=45,
),
applies_to_phases=["semester_end", "year_end"],
),
Rule(
id="gradebook_empty",
name="Notenbuch leer",
condition=lambda ctx: (
ctx.stats.grades_entered == 0 and
ctx.weeks_since_start >= 6
),
suggestion_generator=lambda ctx: Suggestion(
id="start_gradebook",
title="Notenbuch nutzen",
description="Beginne mit der Notenverwaltung im Notenbuch.",
action_type="navigate",
action_target="/studio/gradebook",
priority=SuggestionPriority.MEDIUM,
category="grades",
icon="grade",
estimated_time=10,
),
applies_to_phases=["teaching_setup", "performance_1", "teaching_2", "performance_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# ALLGEMEINE ERINNERUNGEN
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="long_time_inactive",
name="Lange inaktiv",
condition=lambda ctx: ctx.days_in_phase > 14 and len(ctx.completed_milestones) == 0,
suggestion_generator=lambda ctx: Suggestion(
id="get_started",
title="Loslegen",
description="Du bist schon eine Weile in dieser Phase. Lass uns starten!",
action_type="navigate",
action_target="/studio",
priority=SuggestionPriority.HIGH,
category="general",
icon="rocket_launch",
estimated_time=5,
),
applies_to_phases=[], # Alle Phasen
),
Rule(
id="deadline_approaching",
name="Deadline naht",
condition=lambda ctx: any(
e.type == "deadline" and 0 < e.in_days <= 3
for e in ctx.upcoming_events
),
suggestion_generator=lambda ctx: Suggestion(
id="check_deadline",
title="Deadline beachten",
description=f"Eine Deadline in {ctx.get_next_deadline().in_days if ctx.get_next_deadline() else 0} Tagen.",
action_type="navigate",
action_target="/studio",
priority=SuggestionPriority.URGENT,
category="general",
icon="alarm",
estimated_time=5,
),
applies_to_phases=[],
),
]
def get_rules_for_phase(phase: str) -> List[Rule]:
"""Gibt alle Regeln für eine bestimmte Phase zurück."""
return [
rule for rule in RULES
if not rule.applies_to_phases or phase in rule.applies_to_phases
]