103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
"""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,
|
|
)
|