Stabilize runtime auth and E2E coverage
This commit is contained in:
parent
6a3e3b541c
commit
188e899394
133 changed files with 55987 additions and 6775 deletions
103
apps/api/app/routes/teacher.py
Normal file
103
apps/api/app/routes/teacher.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue