"""Teacher dashboard routes backed by real server session state.""" from __future__ import annotations from datetime import datetime from typing import Annotated from fastapi import APIRouter, Depends from pydantic import BaseModel, Field from .. import session_persistence from ..deps import Principal, Role, require_role from ..runtime_policy import require_runtime_fallback_allowed from ..store import InProcSession, store router = APIRouter(prefix="/teacher", tags=["teacher"]) TeacherPrincipal = Annotated[Principal, Depends(require_role(Role.TEACHER, Role.ADMIN))] class TeacherSessionSummary(BaseModel): session_id: str learner_id: str learner_label: str persona_code: str persona_name: str session_no: int status: str stage: str turn_count: int learner_turn_count: int client_turn_count: int started_at: str ended_at: str | None = None class TeacherDashboardResponse(BaseModel): source: str = "in_memory" cohort_label: str = "현재 학습 기록" total_learners: int active_sessions: int ended_sessions: int pending_reviews: list[TeacherSessionSummary] = Field(default_factory=list) recent_sessions: list[TeacherSessionSummary] = Field(default_factory=list) message: str def _iso(ts: float | None) -> str | None: if ts is None: return None return datetime.fromtimestamp(ts).isoformat(timespec="seconds") def _learner_label(learner_id: str) -> str: suffix = learner_id[-6:] if len(learner_id) > 6 else learner_id return f"학습자 {suffix}" def _summary(sess: InProcSession) -> TeacherSessionSummary: learner_turns = sum(1 for turn in sess.turns if turn.speaker == "counselor") client_turns = sum(1 for turn in sess.turns if turn.speaker == "client") return TeacherSessionSummary( session_id=sess.session_id, learner_id=sess.learner_id, learner_label=_learner_label(sess.learner_id), persona_code=sess.persona_code, persona_name=sess.persona.display_name, session_no=sess.session_no, status="ended" if sess.ended else "active", stage=sess.state.stage.value, turn_count=len(sess.turns), learner_turn_count=learner_turns, client_turn_count=client_turns, started_at=_iso(sess.created_at) or "", ended_at=_iso(sess.ended_at), ) @router.get("/dashboard", response_model=TeacherDashboardResponse) async def teacher_dashboard(principal: TeacherPrincipal) -> TeacherDashboardResponse: """Return teacher-visible dashboard data from real sessions only.""" sessions, durable = await session_persistence.list_sessions(principal) if not durable: require_runtime_fallback_allowed("teacher dashboard") sessions = sorted(store.list(), key=lambda sess: sess.created_at, reverse=True) summaries = [_summary(sess) for sess in sessions] pending_reviews = [item for item in summaries if item.status == "ended"] learners = {sess.learner_id for sess in sessions} if sessions: message = "현재 기록된 실제 학습 세션만 표시합니다." else: message = "아직 표시할 실제 학습자 세션이 없습니다." return TeacherDashboardResponse( source="database" if durable else "runtime", total_learners=len(learners), active_sessions=sum(1 for sess in sessions if not sess.ended), ended_sessions=sum(1 for sess in sessions if sess.ended), pending_reviews=pending_reviews[:20], recent_sessions=summaries[:20], message=message, )