feat: сигналы реестра занятий: очередь, повторные отказы, курсант не на связи

This commit is contained in:
kaifarikman 2026-09-27 17:16:21 +03:00
commit 69b483f570
9 changed files with 244 additions and 3 deletions

View file

@ -446,6 +446,7 @@ class DdsDesk(BaseModel):
services[0], ServiceStatus.ACCEPTED, command.comment,
author="диспетчер",
)
session.consecutive_refusals = 0
except StatusError:
pass # статус уже стоит: повторное нажатие ничего не меняет
case "card.status":
@ -462,6 +463,13 @@ class DdsDesk(BaseModel):
# Первичный статус останавливает норматив 30 секунд.
if command.status in PRIMARY:
card.on_event("card.ack")
# Сигнал реестра "повторные отказы" считает подряд идущие
# отказы, а не сумму за занятие.
session.consecutive_refusals = (
session.consecutive_refusals + 1
if command.status is ServiceStatus.DECLINED
else 0
)
if command.status in {
ServiceStatus.COMPLETED, ServiceStatus.DECLINED, ServiceStatus.REFUSED,
}:

View file

@ -314,6 +314,12 @@ class SessionHub:
def observer_count(self, session_id: UUID) -> int:
return len(self._observers.get(session_id, set()))
def station_connected(self, session_id: UUID) -> bool:
return bool(self._stations.get(session_id))
def trainee_connected(self, session_id: UUID) -> bool:
return bool(self._trainees.get(session_id))
# ── такт таймеров ──
def start_ticker(self, session_id: UUID) -> None:

View file

@ -124,6 +124,9 @@ class PersistedSession(BaseModel):
#: a lost WebSocket acknowledgement cannot apply an operation twice.
processed_station_commands: list[str] = Field(default_factory=list)
text_revealed_facts: dict[str, str] = Field(default_factory=dict)
#: Подряд идущих первичных отказов без принятой карточки между ними —
#: сигнал реестра преподавателя, не влияет на балл.
consecutive_refusals: int = 0
@field_validator("processed_station_commands")
@classmethod
@ -170,6 +173,9 @@ class SessionState(PersistedSession):
voice: object | None = Field(default=None, exclude=True)
recorder: object | None = Field(default=None, exclude=True)
recording_path: str | None = Field(default=None, exclude=True)
#: Когда реестр преподавателя последний раз видел подключённый сокет
#: курсанта (станция ДДС или звонок 112) — для сигнала "не на связи".
socket_last_seen_at: datetime | None = Field(default=None, exclude=True)
def persisted(self) -> PersistedSession:
"""Сохраняемая часть без копирования — то, что уходит в снимок."""