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

273 lines
8.4 KiB
Python

"""
Timer Service fuer Phasen-Countdown.
Berechnet verbleibende Zeit, Warnungen und Overtime-Status.
"""
from datetime import datetime
from typing import Dict, Any
from .models import LessonPhase, LessonSession
class PhaseTimer:
"""
Timer Service fuer den Phasen-Countdown.
Features:
- Berechnet verbleibende Zeit pro Phase
- Warnung bei 2 Minuten vor Ende
- Overtime-Erkennung wenn Zeit abgelaufen
"""
# Warnung X Sekunden vor Ende (2 Minuten)
WARNING_THRESHOLD_SECONDS = 120
def get_remaining_seconds(self, session: LessonSession) -> int:
"""
Berechnet die verbleibende Zeit in der aktuellen Phase.
Beruecksichtigt Pause-Zeit: Wenn pausiert, wird die Zeit seit Pause-Start
nicht mitgezaehlt. Die kumulative Pause-Zeit (total_paused_seconds) wird
von der verstrichenen Zeit abgezogen.
Args:
session: Die aktuelle Session
Returns:
Verbleibende Sekunden (min 0)
"""
# Inaktive Phasen haben keinen Timer
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
return 0
if not session.phase_started_at:
return 0
# Phasendauer holen
phase_id = session.current_phase.value
duration_minutes = session.phase_durations.get(phase_id, 10)
duration_seconds = duration_minutes * 60
# Verstrichene Zeit berechnen (mit Pause-Beruecksichtigung)
elapsed = self._get_effective_elapsed(session)
remaining = duration_seconds - elapsed
return max(0, int(remaining))
def _get_effective_elapsed(self, session: LessonSession) -> float:
"""
Berechnet die effektive verstrichene Zeit abzueglich Pausenzeit.
Args:
session: Die aktuelle Session
Returns:
Effektive verstrichene Sekunden
"""
if not session.phase_started_at:
return 0
# Basis: Zeit seit Phasenstart
total_elapsed = (datetime.utcnow() - session.phase_started_at).total_seconds()
# Abzug: Kumulative Pause-Zeit
total_elapsed -= session.total_paused_seconds
# Wenn aktuell pausiert: Zeit seit Pause-Start auch abziehen
if session.is_paused and session.pause_started_at:
current_pause = (datetime.utcnow() - session.pause_started_at).total_seconds()
total_elapsed -= current_pause
return max(0, total_elapsed)
def get_elapsed_seconds(self, session: LessonSession) -> int:
"""
Berechnet die effektive verstrichene Zeit in der aktuellen Phase.
Beruecksichtigt Pause-Zeit analog zu get_remaining_seconds.
Args:
session: Die aktuelle Session
Returns:
Verstrichene Sekunden (abzueglich Pausen)
"""
if not session.phase_started_at:
return 0
if session.current_phase == LessonPhase.ENDED:
return 0
return int(self._get_effective_elapsed(session))
def get_total_seconds(self, session: LessonSession) -> int:
"""
Gibt die Gesamtdauer der aktuellen Phase in Sekunden zurueck.
Args:
session: Die aktuelle Session
Returns:
Gesamtdauer in Sekunden
"""
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
return 0
phase_id = session.current_phase.value
duration_minutes = session.phase_durations.get(phase_id, 10)
return duration_minutes * 60
def get_percentage_remaining(self, session: LessonSession) -> int:
"""
Berechnet den Prozentsatz der verbleibenden Zeit.
Args:
session: Die aktuelle Session
Returns:
Prozent (0-100)
"""
total = self.get_total_seconds(session)
if total == 0:
return 0
remaining = self.get_remaining_seconds(session)
return round((remaining / total) * 100)
def get_percentage_elapsed(self, session: LessonSession) -> int:
"""
Berechnet den Prozentsatz der verstrichenen Zeit.
Args:
session: Die aktuelle Session
Returns:
Prozent (0-100)
"""
return 100 - self.get_percentage_remaining(session)
def is_warning(self, session: LessonSession) -> bool:
"""
Prueft ob die Warnungszeit erreicht ist (2 Min vor Ende).
Args:
session: Die aktuelle Session
Returns:
True wenn Warnung aktiv
"""
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
return False
remaining = self.get_remaining_seconds(session)
return 0 < remaining <= self.WARNING_THRESHOLD_SECONDS
def is_overtime(self, session: LessonSession) -> bool:
"""
Prueft ob die Phase ueberzogen wurde.
Args:
session: Die aktuelle Session
Returns:
True wenn Overtime
"""
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
return False
remaining = self.get_remaining_seconds(session)
return remaining == 0
def get_overtime_seconds(self, session: LessonSession) -> int:
"""
Berechnet wie viele Sekunden die Phase ueberzogen wurde.
Args:
session: Die aktuelle Session
Returns:
Overtime-Sekunden (0 wenn nicht ueberzogen)
"""
if not self.is_overtime(session):
return 0
elapsed = self.get_elapsed_seconds(session)
total = self.get_total_seconds(session)
return max(0, elapsed - total)
def format_time(self, seconds: int) -> str:
"""
Formatiert Sekunden als MM:SS String.
Args:
seconds: Sekunden
Returns:
Formatierte Zeit (z.B. "12:34")
"""
minutes = seconds // 60
secs = seconds % 60
return f"{minutes:02d}:{secs:02d}"
def get_phase_status(self, session: LessonSession) -> Dict[str, Any]:
"""
Gibt den vollstaendigen Timer-Status zurueck.
Args:
session: Die aktuelle Session
Returns:
Dictionary mit Timer-Informationen
"""
remaining = self.get_remaining_seconds(session)
total = self.get_total_seconds(session)
elapsed = self.get_elapsed_seconds(session)
overtime = self.get_overtime_seconds(session)
return {
"remaining_seconds": remaining,
"remaining_formatted": self.format_time(remaining),
"total_seconds": total,
"total_formatted": self.format_time(total),
"elapsed_seconds": elapsed,
"elapsed_formatted": self.format_time(elapsed),
"percentage_remaining": self.get_percentage_remaining(session),
"percentage_elapsed": self.get_percentage_elapsed(session),
"percentage": self.get_percentage_remaining(session), # Alias for Visual Timer
"warning": self.is_warning(session),
"overtime": self.is_overtime(session),
"overtime_seconds": overtime,
"overtime_formatted": self.format_time(overtime) if overtime > 0 else None,
"is_paused": session.is_paused,
}
def get_lesson_timer(self, session: LessonSession) -> Dict[str, Any]:
"""
Gibt den Timer-Status fuer die gesamte Stunde zurueck.
Args:
session: Die aktuelle Session
Returns:
Dictionary mit Stunden-Timer-Informationen
"""
total_planned = session.get_total_duration_minutes() * 60
if session.lesson_started_at:
if session.lesson_ended_at:
actual_duration = (session.lesson_ended_at - session.lesson_started_at).total_seconds()
else:
actual_duration = (datetime.utcnow() - session.lesson_started_at).total_seconds()
else:
actual_duration = 0
return {
"total_planned_seconds": total_planned,
"total_planned_formatted": self.format_time(total_planned),
"actual_duration_seconds": int(actual_duration),
"actual_duration_formatted": self.format_time(int(actual_duration)),
"started_at": session.lesson_started_at.isoformat() if session.lesson_started_at else None,
"ended_at": session.lesson_ended_at.isoformat() if session.lesson_ended_at else None,
}