lct-15: самооценка курсанта до показа автооценки

Правило сервера, а не интерфейса: score.ready курсанту уходит только
после self_assessment.submit, обойти его через DevTools нельзя.
Преподаватель и монитор получают оценку сразу — им ждать нечего.

Чек-лист для самооценки отдаётся отдельной точкой и только после конца
звонка: во время разговора это содержимое подсказок, и открыть его
значило бы выдать в контрольном режиме то, чего там быть не должно.

Попутно подключён расчёт оценки при завершении звонка — детерминированный
слой был написан в lct-12, но его никто не вызывал.

Тест на отсутствие события смотрит очередь курсанта, а не ждёт из сокета:
ожидание того, чего не будет, вешает прогон навсегда.
This commit is contained in:
Ivan Gerasimov 2026-09-17 21:24:34 +03:00
commit 25143645d3
10 changed files with 260 additions and 2 deletions

View file

@ -11,6 +11,7 @@ import { type Capture, startCapture } from "@/shared/audio/capture";
import { EnergyGate } from "@/shared/audio/levels";
import { CALLER_RATE, Playback } from "@/shared/audio/playback";
import { type CardState, applyPatch, edit, empty as emptyCard } from "@/features/kio-card/merge";
import type { ChecklistItem } from "@/features/self-assessment/SelfAssessment";
import type { CallIncoming, DDSCode, ServerToTrainee, TimerSnapshot } from "@/shared/types/generated";
/** Правки копятся и уходят одной дельтой: 300 мс тишины — и отправка. */
@ -33,6 +34,9 @@ export function useCall(sessionId: string | null) {
const [callerSpeaking, setCallerSpeaking] = useState(false);
const [micOn, setMicOn] = useState(false);
const [card, setCard] = useState<CardState>(emptyCard);
const [checklist, setChecklist] = useState<ChecklistItem[]>([]);
const [selfAssessed, setSelfAssessed] = useState(false);
const [scoreReady, setScoreReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const channel = useRef<ReturnType<typeof callChannel> | null>(null);
@ -98,6 +102,15 @@ export function useCall(sessionId: string | null) {
case "call.ended":
setPhase("ended");
audio.current?.ambience.stop();
// Чек-лист открывается только после звонка: во время него это
// содержимое подсказок.
void fetch(`/api/sessions/${sessionId}/checklist`)
.then((response) => (response.ok ? response.json() : []))
.then(setChecklist)
.catch(() => setChecklist([]));
break;
case "score.ready":
setScoreReady(true);
break;
case "error":
setError(event.message);
@ -162,8 +175,14 @@ export function useCall(sessionId: string | null) {
channel.current?.send({ type: "dds.dispatch", service });
}, []);
const submitSelfAssessment = useCallback((missed: string[], comment: string) => {
channel.current?.send({ type: "self_assessment.submit", missed, comment });
setSelfAssessed(true);
}, []);
return {
status, phase, incoming, lines, timers, callerSpeaking, micOn, error, card,
answer, hangup, hint, patchKio, dispatch,
checklist, selfAssessed, scoreReady,
answer, hangup, hint, patchKio, dispatch, submitSelfAssessment,
};
}