feat: статусы реагирования на АРМ ДДС и ошибки диспетчера D1–D6 (lct-33)
ТЗ называет обучаемого оператором ДДС, а его работа в системе-112 состоит из одного действия: получить карточку и правильно проставить статус реагирования с комментарием. Памятка заказчика посвящена этому целиком. Наш АРМ умел два статуса из девяти. - domain/statuses.py: девять статусов реагирования с автоматом переходов, семь статусов карточки, обязательные комментарии к отказам. Формулировки из памятки дословно — диспетчер должен узнать слова своего боевого АРМ. - Автомат на сервере: фронт рисует только пришедшее в available. Отвергнутая отметка не попадает в журнал и возвращается текстом для курсанта. - scoring/dispatcher.py: D1, D2, D3, D4, D6 детерминированно. D3 (отказ от профильного происшествия) проверяется по списку оповещения из ЕКП — без классификатора его было бы не на чем посчитать. D5 оставлен судье. - Отметки D идут в отчёт рядом с E: в живой цепочке 112 → ДДС участвуют обе роли. - Экран ДДС: таблица служб со статусами, поле комментария у отказов, журнал отметок, статус карточки красным в трёх случаях из семи. Сквозной тест повис на ожидании station.state и вскрыл продуктовый дефект: снимок уходил только в ответ на действие диспетчера, то есть список оповещения он видел лишь после того, как что-то нажмёт. Теперь station.state отправляется сразу за card.received. Упрощения записаны в карточке: статусы «Проверена» и «Не завершено» не считаются — первый требует главного специалиста, второй 48 часов. 148 тестов зелёных (23 новых), make typecheck чистый.
This commit is contained in:
parent
842a333d16
commit
d4f0d0a0f8
14 changed files with 953 additions and 9 deletions
|
|
@ -13,6 +13,7 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from app.domain.events import (
|
||||
StationState,
|
||||
CallEnded,
|
||||
CallEndReason,
|
||||
CallStarted,
|
||||
|
|
@ -123,6 +124,9 @@ async def _handle(session_id: UUID, state, event) -> None:
|
|||
# Карточка замораживается снимком и уходит диспетчеру: оператор
|
||||
# не должен иметь возможности дописать поле задним числом.
|
||||
hub.to_station(session_id, state.card_received_event())
|
||||
# Список оповещения диспетчер должен видеть сразу, а не после
|
||||
# первого своего действия: отмечаться ему по нему же (lct-33).
|
||||
hub.to_station(session_id, StationState(snapshot=state.station_snapshot()))
|
||||
hub.to_observers(session_id, KioState(kio=state.kio))
|
||||
hub.to_observers(session_id, TimerTick(timers=state.timers.snapshot()))
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ from uuid import UUID
|
|||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from app.domain.events import ErrorEvent, ErrorKind, StationToServer
|
||||
from app.domain.events import ErrorEvent, ErrorKind, StationState, StationToServer
|
||||
from app.domain.statuses import PRIMARY, ServiceStatus, StatusError
|
||||
from app.session.hub import hub
|
||||
from app.session.state import now_utc
|
||||
|
||||
|
|
@ -28,8 +29,31 @@ _adapter = TypeAdapter(StationToServer)
|
|||
async def _handle(session_id: UUID, state, event) -> None:
|
||||
match event.type:
|
||||
case "card.ack":
|
||||
# Подтверждение приёма — это статус «Принята» у главной службы.
|
||||
# Кнопка осталась ради живой цепочки 112 → ДДС (lct-20), где
|
||||
# диспетчер один и выбирать службу не из чего.
|
||||
state.on_event("card.ack")
|
||||
state.dds_log.append(("card.ack", now_utc(), None))
|
||||
services = state.notified_services()
|
||||
if services:
|
||||
try:
|
||||
state.set_service_status(services[0], ServiceStatus.ACCEPTED)
|
||||
except StatusError:
|
||||
pass # статус уже стоит: повторное нажатие ничего не меняет
|
||||
case "card.status":
|
||||
try:
|
||||
state.set_service_status(
|
||||
event.service, event.status, event.comment, author="диспетчер"
|
||||
)
|
||||
except StatusError as exc:
|
||||
hub.to_station(
|
||||
session_id,
|
||||
ErrorEvent(code=ErrorKind.UNSUPPORTED_EVENT, message=str(exc)),
|
||||
)
|
||||
return
|
||||
# Первичный статус останавливает норматив 30 секунд.
|
||||
if event.status in PRIMARY:
|
||||
state.on_event("card.ack")
|
||||
case "card.bounce":
|
||||
# Карточка вернулась: в разборе это E6 с конкретной причиной.
|
||||
state.bounced_fields = list(event.missing_fields)
|
||||
|
|
@ -44,6 +68,7 @@ async def _handle(session_id: UUID, state, event) -> None:
|
|||
state.on_event("crew.arrived")
|
||||
state.kio = state.kio.model_copy(update={"arrival_at": event.at})
|
||||
state.dds_log.append(("crew.arrived", now_utc(), None))
|
||||
hub.to_station(session_id, StationState(snapshot=state.station_snapshot()))
|
||||
hub.to_observers(session_id, state.snapshot())
|
||||
|
||||
|
||||
|
|
@ -72,6 +97,7 @@ async def station(ws: WebSocket, session_id: UUID, role: str = "dds") -> None:
|
|||
# диспетчер садится за АРМ, когда вызов уже идёт.
|
||||
if state.dispatched_card is not None:
|
||||
hub.to_station(session_id, state.card_received_event())
|
||||
hub.to_station(session_id, StationState(snapshot=state.station_snapshot()))
|
||||
while True:
|
||||
payload = await ws.receive_json()
|
||||
try:
|
||||
|
|
|
|||
Loading…
Reference in a new issue