diff --git a/backend/app/api/http/sessions.py b/backend/app/api/http/sessions.py index 1996e95..bebcc6c 100644 --- a/backend/app/api/http/sessions.py +++ b/backend/app/api/http/sessions.py @@ -111,10 +111,10 @@ class ActiveSessionOut(BaseModel): dds_overdue_cards: int dds_work_overdue_cards: int dds_statuses: dict[str, str] + paused: bool = False dds_snapshot: StationSnapshot | None = None signals: list[Signal] = [] presence_known: bool = True - paused: bool = False def _out(session) -> SessionOut: @@ -203,11 +203,10 @@ def _signals( ) -> list[Signal]: """Сигналы реестра: очередь, повторные отказы, курсант не на связи. - Пауза занятия (`session.pause`) их не трогает и ложных срабатываний не - даёт: очередь на паузе не выдаёт карточки, команды станции отклоняются, - поэтому счётчики стоят; присутствие сокета — факт и на паузе. - Присутствие — по наличию подписчика в хабе этого узла; окно нужно, чтобы - короткий разрыв соединения не сразу считался потерей связи курсанта. + Не завязаны на паузу занятия (`session.pause`, если появится) — очередь, + отказы и присутствие сокета не читают таймеры. Присутствие — по наличию + подписчика в хабе этого узла; окно нужно, чтобы короткий разрыв + соединения не сразу считался потерей связи курсанта. `live=False` — снимок чужого узла: его сокеты здесь не видны, поэтому присутствие неизвестно. Чтение реестра не меняет состояние занятия. @@ -331,10 +330,10 @@ async def active( for card in state.desk.cards.values() ), dds_statuses=latest_statuses, + paused=state.paused, dds_snapshot=station, signals=_signals(state, queue, now, live=state.session_id not in checkpoint_only), presence_known=state.session_id not in checkpoint_only, - paused=state.paused, )) return result diff --git a/backend/app/api/ws/call.py b/backend/app/api/ws/call.py index 37ac20b..78387ff 100644 --- a/backend/app/api/ws/call.py +++ b/backend/app/api/ws/call.py @@ -432,9 +432,6 @@ async def call(ws: WebSocket, session_id: UUID) -> None: if hub.is_lease_fenced(session_id): await close_fenced(ws) return - # Каждый новый канал получает серверное состояние, включая снятую паузу: - # клиент мог потерять событие resume во время переподключения. - queue.put_nowait(SessionPaused(paused=state.paused)) if state.exercise is Exercise.CARD: from app.api.ws.control import card_briefing @@ -468,6 +465,9 @@ async def call(ws: WebSocket, session_id: UUID) -> None: if state.recorder is None: state.recorder = start_recording(session_id) _start_voice(session_id, state, initial_statement=False) + # Каждый новый канал получает серверное состояние, включая снятую паузу: + # клиент мог потерять событие resume во время переподключения. + queue.put_nowait(SessionPaused(paused=state.paused)) writer = asyncio.create_task(pump(ws, queue)) try: while True: diff --git a/backend/tests/test_checkpoint_model.py b/backend/tests/test_checkpoint_model.py index 5cc764e..90cead6 100644 --- a/backend/tests/test_checkpoint_model.py +++ b/backend/tests/test_checkpoint_model.py @@ -160,14 +160,14 @@ def full_state() -> SessionState: desk=desk, resolved_outcome="справка", resolve_comment="передано в другой регион", + paused=True, + paused_at=AT, + total_paused_ms=15_000, processed_station_commands=[str(uuid4())], text_revealed_facts={"f_address": "улица Ленина, 14"}, consecutive_refusals=2, socket_last_seen_at=AT, socket_connected_at_checkpoint=True, - paused=True, - paused_at=AT, - total_paused_ms=15_000, )