diff --git a/backend/app/api/http/ekp.py b/backend/app/api/http/ekp.py index 116f18d..49d12e4 100644 --- a/backend/app/api/http/ekp.py +++ b/backend/app/api/http/ekp.py @@ -48,3 +48,14 @@ async def signs(prefix: list[str] = Query(default=[]), group: int | None = None) else None ), } + + +@router.get("/services") +async def services() -> dict: + """Каталог служб для ручного добавления в карточку — как окно «Добавьте службы». + + Каталог — справочник боевого АРМ, а не содержимое вызова: отдавать его + курсанту так же безопасно, как группы происшествий. + """ + data = ekp.catalog() + return {"source": data.source, "services": [item.model_dump() for item in data.services]} diff --git a/backend/app/api/ws/call.py b/backend/app/api/ws/call.py index 34e4b8d..4925c41 100644 --- a/backend/app/api/ws/call.py +++ b/backend/app/api/ws/call.py @@ -39,6 +39,7 @@ from app.domain.events import ( TraineeToServer, ) from app.dialog.slots import TurnResult +from app.domain.kio import PatchRejected, get_field from app.domain.roles import Role from app.scenarios import store from app.session.dds import prepare_handoff_queue @@ -270,16 +271,37 @@ async def _handle(session_id: UUID, state, event) -> None: case "kio.patch": old_code, old_notify = state.kio.incident_code, list(state.kio.notify) - state.patch_kio(event.fields) + old_extra = list(state.kio.notify_extra) + try: + state.patch_kio(event.fields) + except (ValidationError, PatchRejected) as exc: + reason = exc.errors()[0].get("msg", "") if isinstance(exc, ValidationError) else str(exc) + # Недопустимое значение — отказ курсанту, а не сбой операции: + # сбой закрыл бы занятие на этом узле. + hub.to_trainee(session_id, ErrorEvent( + code=ErrorKind.UNSUPPORTED_EVENT, + message=f"Поле карточки не принято: {reason}"[:200], + )) + # Пакет отклонён целиком, а фронт держит его в pending до эха. + # Серверное значение с `auto` снимает pending — иначе на экране + # останутся правки, которых на сервере нет. + hub.to_trainee(session_id, KioPatchOut( + fields={path: get_field(state.kio, path) for path in event.fields}, + source=PatchSource.AUTO, + )) + return hub.to_trainee(session_id, KioPatchOut( fields=event.fields, source=PatchSource.OPERATOR, )) + auto: dict = {} if (state.kio.incident_code, state.kio.notify) != (old_code, old_notify): - hub.to_trainee(session_id, KioPatchOut( - fields={"incident_code": state.kio.incident_code, - "notify": list(state.kio.notify)}, - source=PatchSource.AUTO, - )) + auto |= {"incident_code": state.kio.incident_code, "notify": list(state.kio.notify)} + # Добавки сервер чистит сам (повторы, службы из notify) — фронту + # нужно итоговое значение, а не эхо присланного. + if state.kio.notify_extra != event.fields.get("notify_extra", old_extra): + auto["notify_extra"] = list(state.kio.notify_extra) + if auto: + hub.to_trainee(session_id, KioPatchOut(fields=auto, source=PatchSource.AUTO)) # Наблюдателю уходит карточка целиком: рассинхрон на внешнем мониторе # посреди занятия дороже лишних килобайт. hub.to_observers(session_id, KioState(kio=state.kio)) diff --git a/backend/app/domain/ekp.py b/backend/app/domain/ekp.py index 4e52aa4..5551691 100644 --- a/backend/app/domain/ekp.py +++ b/backend/app/domain/ekp.py @@ -15,6 +15,9 @@ from pathlib import Path from pydantic import BaseModel, Field DATA = Path(__file__).parent / "ekp.json" +#: Каталог окна «Добавьте службы» из «СЛУЖБЫ 112.docx»: 209 служб, в том числе +#: районные ДДС. Источник и лицензия — в шапке файла и в docs/delivery/THIRD-PARTY.md. +CATALOG = Path(__file__).parent / "services_catalog.json" #: Часть строк книги существует ради интеграции с чужими системами, а оператору #: не показывается — книга помечает их прямо в первом признаке. @@ -89,6 +92,37 @@ def reference() -> Reference: return Reference.model_validate_json(DATA.read_text(encoding="utf-8")) +class CatalogService(BaseModel): + """Служба каталога боевого АРМ — то, что оператор добавляет в карточку вручную.""" + + id: str + name: str + organization: str | None = None + #: «ДДС», если это явно указано в названии в каталоге. + service_level: str | None = None + #: Скриншот и строка списка в «СЛУЖБЫ 112.docx». + source_reference: str + + +class Catalog(BaseModel): + source: str + transcription: str + license: str + services: list[CatalogService] + + +@lru_cache(maxsize=1) +def catalog() -> Catalog: + """Каталог только расширяет выбор служб. Список оповещения по-прежнему + считается из признаков по классификатору, эталоны сценариев от него не зависят.""" + return Catalog.model_validate_json(CATALOG.read_text(encoding="utf-8")) + + +@lru_cache(maxsize=1) +def catalog_names() -> frozenset[str]: + return frozenset(service.name for service in catalog().services) + + @lru_cache(maxsize=1) def _by_code() -> dict[str, Incident]: return {incident.code: incident for incident in reference().incidents} diff --git a/backend/app/domain/kio.py b/backend/app/domain/kio.py index 3193027..a8158a8 100644 --- a/backend/app/domain/kio.py +++ b/backend/app/domain/kio.py @@ -118,6 +118,13 @@ class KIO(BaseModel): # ДДС. `notify` считается из признаков и правится только системой: # в боевом АРМ оператор может добавить службу вручную, но не удалить. notify: list[str] = Field(default_factory=list, description="Список оповещения") + #: Службы, добавленные оператором вручную из каталога. Автоматический список + #: не заменяют и в оценку по эталону не входят. Каталог проверяется на входе + #: (apply_patch), а не здесь: иначе правка названия в каталоге сломала бы + #: загрузку сохранённых снимков занятий и сданных карточек. + notify_extra: list[str] = Field( + default_factory=list, max_length=20, description="Дополнительно оповестить" + ) dds: DDSCode | None = Field( default=None, description="Устаревшее: одна служба. Заменено списком оповещения" ) @@ -147,7 +154,7 @@ EDITABLE_KIO_FIELDS: frozenset[str] = frozenset({ "okato", "address", "street", "building", "entrance", "floor", "intercom_code", "description", "incident_group", "signs", "incident_type", "victims_count", "is_emergency", "life_threat", - "evacuation_needed", "dds", + "evacuation_needed", "dds", "notify_extra", "fire.fire_nature", "fire.object_kind", "fire.floors", "fire.gasified", "fire.people_inside", "fire.smoke_spread", "police.offence_kind", "police.suspects", "police.suspect_fled", @@ -223,11 +230,40 @@ def derive_incident(card: KIO) -> KIO: return KIO.model_validate(data) +class PatchRejected(ValueError): + """Дельта `kio.patch` не принята целиком: карточка не изменилась.""" + + +def _check_catalog(names: Any) -> None: + """Только службы каталога: свободный текст в адресатах карточки + на АРМ ДДС выглядел бы как настоящая служба.""" + from app.domain import ekp + + if not isinstance(names, list): + return # тип и лимит проверит модель + known = ekp.catalog_names() + unknown = [name for name in names if name not in known] + if unknown: + raise PatchRejected(f"нет в каталоге служб: {', '.join(map(str, unknown[:3]))}") + + +def _without_auto_services(card: KIO) -> KIO: + """Добавки без повторов и без служб, которые уже есть в списке оповещения: + иначе на АРМ ДДС одна служба окажется в двух строках.""" + extra = [name for name in dict.fromkeys(card.notify_extra) if name not in card.notify] + if extra == card.notify_extra: + return card + return card.model_copy(update={"notify_extra": extra}) + + def apply_patch(card: KIO, fields: dict[str, Any]) -> KIO: """Применить дельту `kio.patch`. Служебные поля игнорируются. Вложенные поля приходят плоским путём: {"fire.floors": 5}. + Недопустимое значение — `PatchRejected` или `ValidationError`. """ + if "notify_extra" in fields: + _check_catalog(fields["notify_extra"]) data = card.model_dump() for path, value in fields.items(): if path in READ_ONLY_FIELDS: @@ -243,4 +279,4 @@ def apply_patch(card: KIO, fields: dict[str, Any]) -> KIO: # Пересчитываем не только на смену признаков: пострадавшие поднимают скорую, # газификация — МОСГАЗ, и список оповещения обязан это отразить сразу. touched = MODIFIER_FIELDS & set(fields) - return derive_incident(updated) if touched else updated + return _without_auto_services(derive_incident(updated) if touched else updated) diff --git a/backend/app/domain/services_catalog.json b/backend/app/domain/services_catalog.json new file mode 100644 index 0000000..407bf97 --- /dev/null +++ b/backend/app/domain/services_catalog.json @@ -0,0 +1,1470 @@ +{ + "source": "СЛУЖБЫ 112.docx — раздаточный материал задачи № 9 (сообщение 686), 55 скриншотов окна «Добавьте службы»", + "transcription": "alex-indi/training-simulator-112, коммит ded466988f505082922aacac9fff32b777d1279a, backend/seed/services/services.json", + "license": "MIT; использование разрешено авторами, разрешение получено командой 27.09.2026", + "services": [ + { + "id": "catalog-001", + "name": "Служба 101 (ГУ МЧС России по г.Москве, ГКУ \"Пожарно спасательный центр\" ОДС)", + "organization": "ГУ МЧС России по г.Москве, ГКУ \"Пожарно спасательный центр\" ОДС", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image1.png/row-01" + }, + { + "id": "catalog-002", + "name": "ФСБ", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image1.png/row-02" + }, + { + "id": "catalog-003", + "name": "ЦЭМП", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image1.png/row-03" + }, + { + "id": "catalog-004", + "name": "Служба 103 (ГБУ города Москвы Станция скорой и неотложной медицинской помощи им.А.С. Пучкова)", + "organization": "ГБУ города Москвы Станция скорой и неотложной медицинской помощи им.А.С. Пучкова", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image1.png/row-04" + }, + { + "id": "catalog-005", + "name": "Служба 104 (АО \"МОСГАЗ\" Диспетчерское управление)", + "organization": "АО \"МОСГАЗ\" Диспетчерское управление", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image1.png/row-05" + }, + { + "id": "catalog-006", + "name": "Поселение Теплый Стан (ДДС района Теплый Стан города Москвы)", + "organization": "ДДС района Теплый Стан города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image10.png/row-01" + }, + { + "id": "catalog-007", + "name": "Поселение Рязанский (ДДС Рязанского района города Москвы)", + "organization": "ДДС Рязанского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image10.png/row-02" + }, + { + "id": "catalog-008", + "name": "Поселение ЗелАО (ДДС префектуры Зеленоградского административного округа города Москвы)", + "organization": "ДДС префектуры Зеленоградского административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image10.png/row-03" + }, + { + "id": "catalog-009", + "name": "Поселение Печатники (ДДС района Печатники города Москвы)", + "organization": "ДДС района Печатники города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image10.png/row-04" + }, + { + "id": "catalog-010", + "name": "Поселение САО (ДДС префектуры Северного административного округа города Москвы)", + "organization": "ДДС префектуры Северного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image11.png/row-01" + }, + { + "id": "catalog-011", + "name": "Поселение Новокосино (ДДС района Новокосино города Москвы)", + "organization": "ДДС района Новокосино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image11.png/row-02" + }, + { + "id": "catalog-012", + "name": "Поселение Царицыно (ДДС района Царицыно города Москвы)", + "organization": "ДДС района Царицыно города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image11.png/row-03" + }, + { + "id": "catalog-013", + "name": "Поселение Марьино (ДДС района Марьино города Москвы)", + "organization": "ДДС района Марьино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image12.png/row-01" + }, + { + "id": "catalog-014", + "name": "Поселение Кузьминки (ДДС района Кузьминки города Москвы)", + "organization": "ДДС района Кузьминки города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image12.png/row-02" + }, + { + "id": "catalog-015", + "name": "Поселение Митино (ДДС района Митино города Москвы)", + "organization": "ДДС района Митино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image12.png/row-03" + }, + { + "id": "catalog-016", + "name": "Поселение Савеловский (ДДС Савеловского района города Москвы)", + "organization": "ДДС Савеловского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image12.png/row-04" + }, + { + "id": "catalog-017", + "name": "Поселение Щукино (ДДС района Щукино города Москвы)", + "organization": "ДДС района Щукино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image12.png/row-05" + }, + { + "id": "catalog-018", + "name": "Поселение Покровское-Стрешнево (ДДС района Покровское-Стрешнево города Москвы)", + "organization": "ДДС района Покровское-Стрешнево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image13.png/row-02" + }, + { + "id": "catalog-019", + "name": "Поселение Первомайское (ДДС поселения Первомайское в городе Москве)", + "organization": "ДДС поселения Первомайское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image13.png/row-03" + }, + { + "id": "catalog-020", + "name": "МЖД (Московская железная дорога)", + "organization": "Московская железная дорога", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image13.png/row-04" + }, + { + "id": "catalog-021", + "name": "112 Кал. обл. (112 Калужская область)", + "organization": "112 Калужская область", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image13.png/row-05" + }, + { + "id": "catalog-022", + "name": "Поселение Северное Тушино (ДДС района Северное Тушино города Москвы)", + "organization": "ДДС района Северное Тушино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image14.png/row-02" + }, + { + "id": "catalog-023", + "name": "Поселение Нагатинский Затон (ДДС района Нагатинский Затон города Москвы)", + "organization": "ДДС района Нагатинский Затон города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image14.png/row-03" + }, + { + "id": "catalog-024", + "name": "Поселение Дорогомилово (ДДС района Дорогомилово города Москвы)", + "organization": "ДДС района Дорогомилово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image14.png/row-04" + }, + { + "id": "catalog-025", + "name": "Поселение Коптево (ДДС района Коптево города Москвы)", + "organization": "ДДС района Коптево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image15.png/row-01" + }, + { + "id": "catalog-026", + "name": "Поселение Крылатское (ДДС района Крылатское города Москвы)", + "organization": "ДДС района Крылатское города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image15.png/row-02" + }, + { + "id": "catalog-027", + "name": "Поселение Тверской (ДДС Тверского района города Москвы)", + "organization": "ДДС Тверского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image15.png/row-03" + }, + { + "id": "catalog-028", + "name": "Поселение Внуково (ДДС района Внуково города Москвы)", + "organization": "ДДС района Внуково города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image15.png/row-04" + }, + { + "id": "catalog-029", + "name": "Поселение Ярославский (ДДС Ярославского района города Москвы)", + "organization": "ДДС Ярославского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image16.png/row-01" + }, + { + "id": "catalog-030", + "name": "Поселение ЮАО (ДДС префектуры Южного административного округа города Москвы)", + "organization": "ДДС префектуры Южного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image16.png/row-02" + }, + { + "id": "catalog-031", + "name": "Поселение Михайлово-Ярцевское (ДДС поселения Михайлово-Ярцевское города Москвы)", + "organization": "ДДС поселения Михайлово-Ярцевское города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image16.png/row-03" + }, + { + "id": "catalog-032", + "name": "Поселение Некрасовка (ДДС района Некрасовка города Москвы)", + "organization": "ДДС района Некрасовка города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image16.png/row-04" + }, + { + "id": "catalog-033", + "name": "Поселение Выхино-Жулебино (ДДС района Выхино-Жулебино города Москвы)", + "organization": "ДДС района Выхино-Жулебино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image17.png/row-01" + }, + { + "id": "catalog-034", + "name": "Поселение Москворечье-Сабурово (ДДС района Москворечье-Сабурово города Москвы)", + "organization": "ДДС района Москворечье-Сабурово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image17.png/row-02" + }, + { + "id": "catalog-035", + "name": "Поселение ЮЗАО (ДДС префектура Юго-Западного административного округа города Москвы)", + "organization": "ДДС префектура Юго-Западного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image17.png/row-03" + }, + { + "id": "catalog-036", + "name": "Поселение Донской (ДДС Донского района города Москвы)", + "organization": "ДДС Донского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image18.png/row-01" + }, + { + "id": "catalog-037", + "name": "Поселение Куркино (ДДС района Куркино города Москвы)", + "organization": "ДДС района Куркино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image18.png/row-02" + }, + { + "id": "catalog-038", + "name": "Поселение Южное Бутово (ДДС района Южное Бутово города Москвы)", + "organization": "ДДС района Южное Бутово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image18.png/row-03" + }, + { + "id": "catalog-039", + "name": "Поселение Лианозово (ДДС района Лианозово города Москвы)", + "organization": "ДДС района Лианозово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image18.png/row-04" + }, + { + "id": "catalog-040", + "name": "Деп. Обр. (Московский Департамент Образования)", + "organization": "Московский Департамент Образования", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image19.png/row-01" + }, + { + "id": "catalog-041", + "name": "Поселение Лефортово (ДДС района Лефортово города Москвы)", + "organization": "ДДС района Лефортово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image19.png/row-02" + }, + { + "id": "catalog-042", + "name": "Поселение Замоскворечье (ДДС района Замоскворечье города Москвы)", + "organization": "ДДС района Замоскворечье города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image19.png/row-03" + }, + { + "id": "catalog-043", + "name": "Поселение СВАО (ДДС префектуры Северо-Восточного административного округа города Москвы)", + "organization": "ДДС префектуры Северо-Восточного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image19.png/row-04" + }, + { + "id": "catalog-044", + "name": "ОГДЦ", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image2.png/row-01" + }, + { + "id": "catalog-045", + "name": "ЦОДД (ГКУ \"Центр организации дорожного движения\")", + "organization": "ГКУ \"Центр организации дорожного движения\"", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image2.png/row-02" + }, + { + "id": "catalog-046", + "name": "Гормост (Гормост)", + "organization": "Гормост", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image2.png/row-03" + }, + { + "id": "catalog-047", + "name": "Мосгортранс", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image2.png/row-04" + }, + { + "id": "catalog-048", + "name": "Автодороги", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image2.png/row-05" + }, + { + "id": "catalog-049", + "name": "Поселение Сокольники (ДДС района Сокольники города Москвы)", + "organization": "ДДС района Сокольники города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image20.png/row-01" + }, + { + "id": "catalog-050", + "name": "Поселение Измайлово (ДДС района Измайлово города Москвы)", + "organization": "ДДС района Измайлово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image20.png/row-02" + }, + { + "id": "catalog-051", + "name": "Поселение Богородское (ДДС района Богородское города Москвы)", + "organization": "ДДС района Богородское города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image20.png/row-03" + }, + { + "id": "catalog-052", + "name": "Поселение Преображенский (ДДС района Преображенское города Москвы)", + "organization": "ДДС района Преображенское города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image21.png/row-01" + }, + { + "id": "catalog-053", + "name": "Поселение Восточное Измайлово (ДДС района Восточное Измайлово города Москвы)", + "organization": "ДДС района Восточное Измайлово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image21.png/row-02" + }, + { + "id": "catalog-054", + "name": "Поселение Ростокино (ДДС района Ростокино города Москвы)", + "organization": "ДДС района Ростокино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image21.png/row-03" + }, + { + "id": "catalog-055", + "name": "Поселение Соколиная гора (ДДС района Соколиная гора города Москвы)", + "organization": "ДДС района Соколиная гора города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image21.png/row-04" + }, + { + "id": "catalog-056", + "name": "Поселение Хорошево-Мневники (ДДС района Хорошево-Мневники города Москвы)", + "organization": "ДДС района Хорошево-Мневники города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image22.png/row-01" + }, + { + "id": "catalog-057", + "name": "Поселение Гольяново (ДДС района Гольяново города Москвы)", + "organization": "ДДС района Гольяново города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image22.png/row-02" + }, + { + "id": "catalog-058", + "name": "Поселение Северное Измайлово (ДДС района Северное Измайлово города Москвы)", + "organization": "ДДС района Северное Измайлово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image22.png/row-03" + }, + { + "id": "catalog-059", + "name": "Поселение Марфино (ДДС района Марфино города Москвы)", + "organization": "ДДС района Марфино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image22.png/row-04" + }, + { + "id": "catalog-060", + "name": "Поселение Метрогородок (ДДС района Метрогородок города Москвы)", + "organization": "ДДС района Метрогородок города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image23.png/row-01" + }, + { + "id": "catalog-061", + "name": "Поселение Фили-Давыдково (ДДС района Фили-Давыдково города Москвы)", + "organization": "ДДС района Фили-Давыдково города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image23.png/row-02" + }, + { + "id": "catalog-062", + "name": "Поселение Южное Медведково (ДДС района Южное Медведково города Москвы)", + "organization": "ДДС района Южное Медведково города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image23.png/row-03" + }, + { + "id": "catalog-063", + "name": "Поселение Рязановское (ДДС поселения Рязановское в городе Москве)", + "organization": "ДДС поселения Рязановское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image24.png/row-01" + }, + { + "id": "catalog-064", + "name": "Деп. природопользования (Департамент природопользования и охраны окружающей среды города Москвы)", + "organization": "Департамент природопользования и охраны окружающей среды города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image24.png/row-02" + }, + { + "id": "catalog-065", + "name": "Поселение Тимирязевский (ДДС Тимирязевского района города Москвы)", + "organization": "ДДС Тимирязевского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image24.png/row-03" + }, + { + "id": "catalog-066", + "name": "Поселение Бирюлево Западное (ДДС района Бирюлево Западное города Москвы)", + "organization": "ДДС района Бирюлево Западное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image24.png/row-04" + }, + { + "id": "catalog-067", + "name": "Поселение Ивановский (ДДС Ивановского района города Москвы)", + "organization": "ДДС Ивановского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image25.png/row-01" + }, + { + "id": "catalog-068", + "name": "Поселение Ново-Переделкино (ДДС района Ново-Переделкино города Москвы)", + "organization": "ДДС района Ново-Переделкино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image25.png/row-02" + }, + { + "id": "catalog-069", + "name": "Поселение Восточное Дегунино (ДДС района Восточное Дегунино города Москвы)", + "organization": "ДДС района Восточное Дегунино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image25.png/row-03" + }, + { + "id": "catalog-070", + "name": "Поселение Ховрино (ДДС района Ховрино города Москвы)", + "organization": "ДДС района Ховрино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image26.png/row-01" + }, + { + "id": "catalog-071", + "name": "Поселение Головинский (ДДС Головинского района города Москвы)", + "organization": "ДДС Головинского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image26.png/row-02" + }, + { + "id": "catalog-072", + "name": "Поселение Бескудниково (ДДС Бескудниковского района города Москвы)", + "organization": "ДДС Бескудниковского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image26.png/row-03" + }, + { + "id": "catalog-073", + "name": "Поселение Левобережный (ДДС района Левобережный города Москвы)", + "organization": "ДДС района Левобережный города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image26.png/row-04" + }, + { + "id": "catalog-074", + "name": "Поселение Дмитровский (ДДС Дмитровского района города Москвы)", + "organization": "ДДС Дмитровского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image27.png/row-01" + }, + { + "id": "catalog-075", + "name": "Поселение Сокол (ДДС района Сокол города Москвы)", + "organization": "ДДС района Сокол города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image27.png/row-02" + }, + { + "id": "catalog-076", + "name": "Поселение Молжаниновский (ДДС Молжаниновского района города Москвы)", + "organization": "ДДС Молжаниновского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image27.png/row-03" + }, + { + "id": "catalog-077", + "name": "Поселение Хамовники (ДДС района Хамовники города Москвы)", + "organization": "ДДС района Хамовники города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image27.png/row-04" + }, + { + "id": "catalog-078", + "name": "Поселение Басманный (ДДС Басманного района города Москвы)", + "organization": "ДДС Басманного района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image28.png/row-01" + }, + { + "id": "catalog-079", + "name": "Поселение Якиманка (ДДС района Якиманка города Москвы)", + "organization": "ДДС района Якиманка города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image28.png/row-02" + }, + { + "id": "catalog-080", + "name": "Поселение Мещанский (ДДС Мещанского района города Москвы)", + "organization": "ДДС Мещанского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image28.png/row-03" + }, + { + "id": "catalog-081", + "name": "Поселение Пресненский (ДДС Пресненского района города Москвы)", + "organization": "ДДС Пресненского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image28.png/row-04" + }, + { + "id": "catalog-082", + "name": "Служба 102 (Дежурная часть ГУ МВД России по г.Москве)", + "organization": "Дежурная часть ГУ МВД России по г.Москве", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image29.png/row-02" + }, + { + "id": "catalog-083", + "name": "Поселение Мосрентген (ДДС поселения «Мосрентген» в городе Москве)", + "organization": "ДДС поселения «Мосрентген» в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image29.png/row-03" + }, + { + "id": "catalog-084", + "name": "Поселение Ясенево (ДДС района Ясенево города Москвы)", + "organization": "ДДС района Ясенево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image29.png/row-04" + }, + { + "id": "catalog-085", + "name": "Мосводоканал (АО \"Мосводоканал\")", + "organization": "АО \"Мосводоканал\"", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image3.png/row-01" + }, + { + "id": "catalog-086", + "name": "Россети МР", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image3.png/row-02" + }, + { + "id": "catalog-087", + "name": "МОЭК", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image3.png/row-03" + }, + { + "id": "catalog-088", + "name": "Деп. ЖКХ (Департамент ЖКХ)", + "organization": "Департамент ЖКХ", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image3.png/row-04" + }, + { + "id": "catalog-089", + "name": "Метро", + "organization": null, + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image3.png/row-05" + }, + { + "id": "catalog-090", + "name": "МГТС (Московская городская телефонная сеть)", + "organization": "Московская городская телефонная сеть", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image30.png/row-01" + }, + { + "id": "catalog-091", + "name": "Поселение Щербинка (ДДС городского округа Щербинка города Москвы)", + "organization": "ДДС городского округа Щербинка города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image30.png/row-02" + }, + { + "id": "catalog-092", + "name": "Поселение Капотня (ДДС района Капотня города Москвы)", + "organization": "ДДС района Капотня города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image30.png/row-03" + }, + { + "id": "catalog-093", + "name": "Поселение Троицк (ДДС городского округа Троицк города Москвы)", + "organization": "ДДС городского округа Троицк города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image30.png/row-04" + }, + { + "id": "catalog-094", + "name": "Поселение Нагатино-Садовники (ДДС района Нагатино Садовники)", + "organization": "ДДС района Нагатино Садовники", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image31.png/row-01" + }, + { + "id": "catalog-095", + "name": "Поселение Клёновское (ДДС поселения Клёновское в городе Москве)", + "organization": "ДДС поселения Клёновское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image31.png/row-02" + }, + { + "id": "catalog-096", + "name": "Поселение Нагорный (ДДС района Нагорный города Москвы)", + "organization": "ДДС района Нагорный города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image31.png/row-03" + }, + { + "id": "catalog-097", + "name": "Поселение Бабушкинский (ДДС Бабушкинского района города Москвы)", + "organization": "ДДС Бабушкинского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image31.png/row-04" + }, + { + "id": "catalog-098", + "name": "Поселение Бутырский (ДДС Бутырского района города Москвы)", + "organization": "ДДС Бутырского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image32.png/row-01" + }, + { + "id": "catalog-099", + "name": "Поселение Лосиноостровский (ДДС Лосиноостровского района города Москвы)", + "organization": "ДДС Лосиноостровского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image32.png/row-02" + }, + { + "id": "catalog-100", + "name": "Поселение Останкинский (ДДС Останкинского района города Москвы)", + "organization": "ДДС Останкинского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image32.png/row-03" + }, + { + "id": "catalog-101", + "name": "Поселение Отрадное (ДДС района Отрадное города Москвы)", + "organization": "ДДС района Отрадное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image33.png/row-01" + }, + { + "id": "catalog-102", + "name": "Поселение Свиблово (ДДС района Свиблово города Москвы)", + "organization": "ДДС района Свиблово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image33.png/row-02" + }, + { + "id": "catalog-103", + "name": "Поселение Северное Медведково (ДДС района Северное Медведково города Москвы)", + "organization": "ДДС района Северное Медведково города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image33.png/row-03" + }, + { + "id": "catalog-104", + "name": "Поселение Северный (ДДС района Северный города Москвы)", + "organization": "ДДС района Северный города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image33.png/row-04" + }, + { + "id": "catalog-105", + "name": "Поселение Орехово-Борисово Северное (ДДС Орехово-Борисово Северное города Москвы)", + "organization": "ДДС Орехово-Борисово Северное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image34.png/row-01" + }, + { + "id": "catalog-106", + "name": "Поселение Перово (ДДС района Перово города Москвы)", + "organization": "ДДС района Перово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image34.png/row-02" + }, + { + "id": "catalog-107", + "name": "Поселение Алтуфьевский (ДДС Алтуфьевского района города Москвы)", + "organization": "ДДС Алтуфьевского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image34.png/row-03" + }, + { + "id": "catalog-108", + "name": "Поселение Можайский (ДДС Можайского района города Москвы)", + "organization": "ДДС Можайского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image34.png/row-04" + }, + { + "id": "catalog-109", + "name": "Поселение Раменки (ДДС района Раменки города Москвы)", + "organization": "ДДС района Раменки города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image35.png/row-03" + }, + { + "id": "catalog-110", + "name": "Поселение Алексеевский (ДДС Алексеевского района города Москвы)", + "organization": "ДДС Алексеевского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image35.png/row-04" + }, + { + "id": "catalog-111", + "name": "Поселение Кунцево (ДДС района Кунцево города Москвы)", + "organization": "ДДС района Кунцево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image36.png/row-01" + }, + { + "id": "catalog-112", + "name": "Поселение Тропарево-Никулино (ДДС района Тропарево-Никулино города Москвы)", + "organization": "ДДС района Тропарево-Никулино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image36.png/row-02" + }, + { + "id": "catalog-113", + "name": "Поселение Восточный (ДДС района Восточный города Москвы)", + "organization": "ДДС района Восточный города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image36.png/row-03" + }, + { + "id": "catalog-114", + "name": "Поселение Аэропорт (ДДС района Аэропорт города Москвы)", + "organization": "ДДС района Аэропорт города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image37.png/row-01" + }, + { + "id": "catalog-115", + "name": "Поселение Западное Дегунино (ДДС района Западное Дегунино города Москвы)", + "organization": "ДДС района Западное Дегунино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image37.png/row-02" + }, + { + "id": "catalog-116", + "name": "Поселение Войковский (ДДС Войковского района города Москвы)", + "organization": "ДДС Войковского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image37.png/row-03" + }, + { + "id": "catalog-117", + "name": "Поселение Хорошевский (ДДС Хорошевского района города Москвы)", + "organization": "ДДС Хорошевского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image37.png/row-04" + }, + { + "id": "catalog-118", + "name": "Поселение Беговой (ДДС района Беговой города Москвы)", + "organization": "ДДС района Беговой города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image38.png/row-01" + }, + { + "id": "catalog-119", + "name": "Поселение Текстильщики (ДДС района Текстильщики города Москвы)", + "organization": "ДДС района Текстильщики города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image38.png/row-02" + }, + { + "id": "catalog-120", + "name": "Поселение Московское (ДДС поселения Московский в городе Москве)", + "organization": "ДДС поселения Московский в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image38.png/row-03" + }, + { + "id": "catalog-121", + "name": "Поселение Братеево (ДДС района Братеево города Москвы)", + "organization": "ДДС района Братеево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image38.png/row-04" + }, + { + "id": "catalog-122", + "name": "Поселение Филимонковское (ДДС поселения Филимонковское в городе Москве)", + "organization": "ДДС поселения Филимонковское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image39.png/row-01" + }, + { + "id": "catalog-123", + "name": "Канал им. Москвы (ФГБУ \"Канал имени Москвы\")", + "organization": "ФГБУ \"Канал имени Москвы\"", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image39.png/row-02" + }, + { + "id": "catalog-124", + "name": "Поселение Люблино (ДДС района Люблино города Москвы)", + "organization": "ДДС района Люблино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image39.png/row-03" + }, + { + "id": "catalog-125", + "name": "Поселение Марушкинское (ДДС поселения Марушкинское в городе Москве)", + "organization": "ДДС поселения Марушкинское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image39.png/row-04" + }, + { + "id": "catalog-126", + "name": "112 Мос. обл. (112 Московской области)", + "organization": "112 Московской области", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image4.png/row-01" + }, + { + "id": "catalog-127", + "name": "ФГУП РСВО (Российские сети вещания и оповещения)", + "organization": "Российские сети вещания и оповещения", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image4.png/row-02" + }, + { + "id": "catalog-128", + "name": "ОЭК (Объединенная энергетическая компания)", + "organization": "Объединенная энергетическая компания", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image4.png/row-03" + }, + { + "id": "catalog-129", + "name": "Мослифт (Лифт МСК)", + "organization": "Лифт МСК", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image4.png/row-04" + }, + { + "id": "catalog-130", + "name": "Мосводосток (Мосводосток)", + "organization": "Мосводосток", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image4.png/row-05" + }, + { + "id": "catalog-131", + "name": "Деп. труда и соц.защиты (Департамент труда и социальной защиты населения города Москвы)", + "organization": "Департамент труда и социальной защиты населения города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image40.png/row-02" + }, + { + "id": "catalog-132", + "name": "Поселение Орехово-Борисово Южное (ДДС района Орехово-Борисово Южное города Москвы)", + "organization": "ДДС района Орехово-Борисово Южное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image40.png/row-03" + }, + { + "id": "catalog-133", + "name": "Поселение Даниловский (ДДС Даниловского района города Москвы)", + "organization": "ДДС Даниловского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image40.png/row-04" + }, + { + "id": "catalog-134", + "name": "Поселение Роговское (ДДС поселения Роговское в городе Москве)", + "organization": "ДДС поселения Роговское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image41.png/row-01" + }, + { + "id": "catalog-135", + "name": "Поселение ЗАО (ДДС префектуры Западного административного округа города Москвы)", + "organization": "ДДС префектуры Западного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image41.png/row-02" + }, + { + "id": "catalog-136", + "name": "Поселение Нижегородский (ДДС Нижегородского района города Москвы)", + "organization": "ДДС Нижегородского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image41.png/row-03" + }, + { + "id": "catalog-137", + "name": "Поселение Щаповское (ДДС поселения Щаповское в городе Москве)", + "organization": "ДДС поселения Щаповское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image41.png/row-04" + }, + { + "id": "catalog-138", + "name": "Поселение Южнопортовый (ДДС Южнопортового района города Москвы)", + "organization": "ДДС Южнопортового района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image42.png/row-01" + }, + { + "id": "catalog-139", + "name": "ЭВАЖД (ГБУ \"Учреждение по эксплуатации высотных административных и жилых домов\")", + "organization": "ГБУ \"Учреждение по эксплуатации высотных административных и жилых домов\"", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image42.png/row-02" + }, + { + "id": "catalog-140", + "name": "Поселение ЦАО (ДДС префектуры Центрального административного округа)", + "organization": "ДДС префектуры Центрального административного округа", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image42.png/row-03" + }, + { + "id": "catalog-141", + "name": "Поселение Черемушки (ДДС района Черемушки города Москвы)", + "organization": "ДДС района Черемушки города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image43.png/row-01" + }, + { + "id": "catalog-142", + "name": "Поселение Марьина роща (ДДС района Марьина роща города Москвы)", + "organization": "ДДС района Марьина роща города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image43.png/row-02" + }, + { + "id": "catalog-143", + "name": "Поселение Красносельский (ДДС Красносельского района города Москвы)", + "organization": "ДДС Красносельского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image43.png/row-03" + }, + { + "id": "catalog-144", + "name": "Поселение Строгино (ДДС района Строгино города Москвы)", + "organization": "ДДС района Строгино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image43.png/row-04" + }, + { + "id": "catalog-145", + "name": "Поселение СЗАО (ДДС префектуры Северо-Западного административного округа города Москвы)", + "organization": "ДДС префектуры Северо-Западного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image44.png/row-01" + }, + { + "id": "catalog-146", + "name": "Поселение Бирюлево Восточное (ДДС района Бирюлево Восточное города Москвы)", + "organization": "ДДС района Бирюлево Восточное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image44.png/row-02" + }, + { + "id": "catalog-147", + "name": "Поселение Чертаново Центральное (ДДС района Чертаново Центральное города Москвы)", + "organization": "ДДС района Чертаново Центральное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image44.png/row-03" + }, + { + "id": "catalog-148", + "name": "Поселение Гагаринский (ДДС Гагаринского района города Москвы)", + "organization": "ДДС Гагаринского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image44.png/row-04" + }, + { + "id": "catalog-149", + "name": "Поселение Обручевский (ДДС Обручевского района города Москвы)", + "organization": "ДДС Обручевского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image45.png/row-01" + }, + { + "id": "catalog-150", + "name": "Поселение Зюзино (ДДС района Зюзино города Москвы)", + "organization": "ДДС района Зюзино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image45.png/row-02" + }, + { + "id": "catalog-151", + "name": "Поселение Коньково (ДДС района Коньково города Москвы)", + "organization": "ДДС района Коньково города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image45.png/row-03" + }, + { + "id": "catalog-152", + "name": "Поселение Котловка (ДДС района Котловка города Москвы)", + "organization": "ДДС района Котловка города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image45.png/row-04" + }, + { + "id": "catalog-153", + "name": "Поселение Северное Бутово (ДДС района Северное Бутово города Москвы)", + "organization": "ДДС района Северное Бутово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image46.png/row-01" + }, + { + "id": "catalog-154", + "name": "Поселение Косино-Ухтомский (ДДС района Косино-Ухтомский города Москвы)", + "organization": "ДДС района Косино-Ухтомский города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image46.png/row-02" + }, + { + "id": "catalog-155", + "name": "Поселение Солнцево (ДДС района Солнцево города Москвы)", + "organization": "ДДС района Солнцево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image46.png/row-03" + }, + { + "id": "catalog-156", + "name": "Поселение Таганский (ДДС района Таганский города Москвы)", + "organization": "ДДС района Таганский города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image47.png/row-01" + }, + { + "id": "catalog-157", + "name": "Поселение Очаково-Матвеевское (ДДС района Очаково-Матвеевское города Москвы)", + "organization": "ДДС района Очаково-Матвеевское города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image47.png/row-02" + }, + { + "id": "catalog-158", + "name": "Поселение Южное Тушино (ДДС района Южное Тушино города Москвы)", + "organization": "ДДС района Южное Тушино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image47.png/row-03" + }, + { + "id": "catalog-159", + "name": "Поселение Филевский парк (ДДС района Филевский парк города Москвы)", + "organization": "ДДС района Филевский парк города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image47.png/row-04" + }, + { + "id": "catalog-160", + "name": "Поселение ТИНАО (ДДС префектуры Троицкого и Новомосковского округов города Москвы)", + "organization": "ДДС префектуры Троицкого и Новомосковского округов города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image48.png/row-01" + }, + { + "id": "catalog-161", + "name": "Поселение Проспект Вернадского (ДДС района Проспект Вернадского города Москвы)", + "organization": "ДДС района Проспект Вернадского города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image48.png/row-02" + }, + { + "id": "catalog-162", + "name": "Поселение Десеновское (ДДС поселения Десеновское в городе Москве)", + "organization": "ДДС поселения Десеновское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image48.png/row-03" + }, + { + "id": "catalog-163", + "name": "Поселение Арбат (ДДС района Арбат города Москвы)", + "organization": "ДДС района Арбат города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image48.png/row-04" + }, + { + "id": "catalog-164", + "name": "Дежурно-диспетчерская служба (Дежурно-диспетчерская служба)", + "organization": "Дежурно-диспетчерская служба", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image49.png/row-02" + }, + { + "id": "catalog-165", + "name": "Поселение Сосенское (ДДС поселения Сосенское в городе Москве)", + "organization": "ДДС поселения Сосенское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image49.png/row-03" + }, + { + "id": "catalog-166", + "name": "Поселение Кокошкино (ДДС поселения Кокошкино в городе Москве)", + "organization": "ДДС поселения Кокошкино в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image49.png/row-04" + }, + { + "id": "catalog-167", + "name": "Центррегионводхоз (Центррегионводхоз)", + "organization": "Центррегионводхоз", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image49.png/row-05" + }, + { + "id": "catalog-168", + "name": "Москоллектор (Москоллектор)", + "organization": "Москоллектор", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image5.png/row-01" + }, + { + "id": "catalog-169", + "name": "Воен. комендатура (Воен. комендатура)", + "organization": "Воен. комендатура", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image5.png/row-02" + }, + { + "id": "catalog-170", + "name": "Поселение Чертаново Северное (ДДС района Чертаново Северное города Москвы)", + "organization": "ДДС района Чертаново Северное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image5.png/row-03" + }, + { + "id": "catalog-171", + "name": "Поселение Академический (ДДС Академического района города Москвы)", + "organization": "ДДС Академического района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image5.png/row-04" + }, + { + "id": "catalog-172", + "name": "Поселение Воскресенское (ДДС поселения Воскресенское в городе Москве)", + "organization": "ДДС поселения Воскресенское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image50.png/row-02" + }, + { + "id": "catalog-173", + "name": "Поселение Краснопахорское (ДДС поселения Краснопахорское в городе Москве)", + "organization": "ДДС поселения Краснопахорское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image50.png/row-03" + }, + { + "id": "catalog-174", + "name": "Поселение Вороновское (ДДС поселения Вороновское в городе Москве)", + "organization": "ДДС поселения Вороновское в городе Москве", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image50.png/row-04" + }, + { + "id": "catalog-175", + "name": "Мособлгаз (Мособлгаз)", + "organization": "Мособлгаз", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image50.png/row-05" + }, + { + "id": "catalog-176", + "name": "Поселение Внуковское (ДДС поселения Внуковское города Москвы)", + "organization": "ДДС поселения Внуковское города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image51.png/row-02" + }, + { + "id": "catalog-177", + "name": "Поселение Новофедоровское (ДДС поселения Новофедоровское города Москвы)", + "organization": "ДДС поселения Новофедоровское города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image51.png/row-03" + }, + { + "id": "catalog-178", + "name": "Поселение Киевский (ДДС поселения Киевский города Москвы)", + "organization": "ДДС поселения Киевский города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image51.png/row-04" + }, + { + "id": "catalog-179", + "name": "ГБУ АД ВАО (ГБУ Автодороги ВАО)", + "organization": "ГБУ Автодороги ВАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image51.png/row-05" + }, + { + "id": "catalog-180", + "name": "ГБУ АД СВАО (ГБУ Автодороги СВАО)", + "organization": "ГБУ Автодороги СВАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image52.png/row-02" + }, + { + "id": "catalog-181", + "name": "ГБУ АД ЮВАО (ГБУ Автодороги ЮВАО)", + "organization": "ГБУ Автодороги ЮВАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image52.png/row-03" + }, + { + "id": "catalog-182", + "name": "ГБУ АД ЗелАО (ГБУ Автодороги ЗелАО)", + "organization": "ГБУ Автодороги ЗелАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image52.png/row-04" + }, + { + "id": "catalog-183", + "name": "ГБУ АД ЮЗАО (ГБУ Автодороги ЮЗАО)", + "organization": "ГБУ Автодороги ЮЗАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image52.png/row-05" + }, + { + "id": "catalog-184", + "name": "ГБУ АД ЮАО (ГБУ Автодороги ЮАО)", + "organization": "ГБУ Автодороги ЮАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image52.png/row-06" + }, + { + "id": "catalog-185", + "name": "ГБУ АД САО (ГБУ Автодороги САО)", + "organization": "ГБУ Автодороги САО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image53.png/row-02" + }, + { + "id": "catalog-186", + "name": "ГБУ АД СЗАО (ГБУ Автодороги СЗАО)", + "organization": "ГБУ Автодороги СЗАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image53.png/row-03" + }, + { + "id": "catalog-187", + "name": "ГБУ АД ЦАО (ГБУ Автодороги ЦАО)", + "organization": "ГБУ Автодороги ЦАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image53.png/row-04" + }, + { + "id": "catalog-188", + "name": "ГБУ АД ЗАО (ГБУ Автодороги ЗАО)", + "organization": "ГБУ Автодороги ЗАО", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image53.png/row-05" + }, + { + "id": "catalog-189", + "name": "Департамент культуры города Москвы (Департамент культуры города Москвы)", + "organization": "Департамент культуры города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image54.png/row-01" + }, + { + "id": "catalog-190", + "name": "ГКУ ЦСА (Центр социальной помощи)", + "organization": "Центр социальной помощи", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image54.png/row-02" + }, + { + "id": "catalog-191", + "name": "Мостуризм (Комитет по туризму города Москвы)", + "organization": "Комитет по туризму города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image54.png/row-03" + }, + { + "id": "catalog-192", + "name": "Департамент строительства (Департамент строительства)", + "organization": "Департамент строительства", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image54.png/row-04" + }, + { + "id": "catalog-193", + "name": "Комитет ветеринарии (Комитет ветеринарии города Москвы)", + "organization": "Комитет ветеринарии города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image55.png/row-04" + }, + { + "id": "catalog-194", + "name": "Мосжилинспекция (Государственная жилищная инспекция города Москвы)", + "organization": "Государственная жилищная инспекция города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image55.png/row-05" + }, + { + "id": "catalog-195", + "name": "Поселение Ломоносовский (ДДС Ломоносовского района города Москвы)", + "organization": "ДДС Ломоносовского района города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image6.png/row-01" + }, + { + "id": "catalog-196", + "name": "Поселение Бибирево (ДДС района Бибирево города Москвы)", + "organization": "ДДС района Бибирево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image6.png/row-02" + }, + { + "id": "catalog-197", + "name": "Поселение ЮВАО (ДДС префектуры Юго-Восточного административного округа города Москвы)", + "organization": "ДДС префектуры Юго-Восточного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image6.png/row-03" + }, + { + "id": "catalog-198", + "name": "Поселение ВАО (ДДС префектуры Восточного административного округа города Москвы)", + "organization": "ДДС префектуры Восточного административного округа города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image6.png/row-04" + }, + { + "id": "catalog-199", + "name": "Поселение Зябликово (ДДС района Зябликово города Москвы)", + "organization": "ДДС района Зябликово города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image7.png/row-01" + }, + { + "id": "catalog-200", + "name": "Поселение Силино (ДДС района Силино города Москвы)", + "organization": "ДДС района Силино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image7.png/row-02" + }, + { + "id": "catalog-201", + "name": "ОАТИ (Объединение Административно-Технических Инспекций города Москвы)", + "organization": "Объединение Административно-Технических Инспекций города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image7.png/row-03" + }, + { + "id": "catalog-202", + "name": "ГБУ МСППН (Московская служба психологической помощи населению ГБУ города Москвы)", + "organization": "Московская служба психологической помощи населению ГБУ города Москвы", + "service_level": null, + "source_reference": "СЛУЖБЫ 112.docx#word/media/image7.png/row-04" + }, + { + "id": "catalog-203", + "name": "Упр. района Вешняки (ДДС района Вешняки города Москвы)", + "organization": "ДДС района Вешняки города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image8.png/row-01" + }, + { + "id": "catalog-204", + "name": "Поселение Крюково (ДДС района Крюково города Москвы)", + "organization": "ДДС района Крюково города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image8.png/row-02" + }, + { + "id": "catalog-205", + "name": "Поселение Матушкино (ДДС района Матушкино города Москвы)", + "organization": "ДДС района Матушкино города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image8.png/row-03" + }, + { + "id": "catalog-206", + "name": "Поселение Новогиреево (ДДС района Новогиреево города Москвы)", + "organization": "ДДС района Новогиреево города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image9.png/row-01" + }, + { + "id": "catalog-207", + "name": "Поселение Савелки (ДДС района Савелки города Москвы)", + "organization": "ДДС района Савелки города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image9.png/row-02" + }, + { + "id": "catalog-208", + "name": "Поселение Старое Крюково (ДДС района Старое Крюково города Москвы)", + "organization": "ДДС района Старое Крюково города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image9.png/row-03" + }, + { + "id": "catalog-209", + "name": "Поселение Чертаново Южное (ДДС района Чертаново Южное города Москвы)", + "organization": "ДДС района Чертаново Южное города Москвы", + "service_level": "ДДС", + "source_reference": "СЛУЖБЫ 112.docx#word/media/image9.png/row-04" + } + ] +} diff --git a/backend/tests/test_route_auth_contract.py b/backend/tests/test_route_auth_contract.py index 5c9e5d0..e4c7d9b 100644 --- a/backend/tests/test_route_auth_contract.py +++ b/backend/tests/test_route_auth_contract.py @@ -12,13 +12,14 @@ from pathlib import Path API_ROOT = Path(__file__).parents[1] / "app" / "api" # Public by design: credential entry/session bootstrap and the non-sensitive -# incident classification dictionary. dev-token has its own fail-closed flag +# incident classification dictionary and service catalog. dev-token has its own fail-closed flag # and remains hidden in production. PUBLIC_HTTP_ROUTES = { ("auth.py", "post", "/login"), ("auth.py", "post", "/dev-token"), ("http/ekp.py", "get", "/groups"), ("http/ekp.py", "get", "/signs"), + ("http/ekp.py", "get", "/services"), } # Routes that centralize ownership + authentication checks in a shared helper. diff --git a/backend/tests/test_service_catalog.py b/backend/tests/test_service_catalog.py new file mode 100644 index 0000000..f6c7a0d --- /dev/null +++ b/backend/tests/test_service_catalog.py @@ -0,0 +1,167 @@ +"""Каталог служб из «СЛУЖБЫ 112» (lct-38). + +Каталог только расширяет ручной выбор служб в карточке. Список оповещения +по-прежнему считается по классификатору, эталоны сценариев от каталога не зависят. +""" + +import asyncio +import json +from pathlib import Path +from uuid import uuid4 + +import pytest +from fastapi.testclient import TestClient +from pydantic import ValidationError + +from app.api.ws import call as call_ws +from app.domain import ekp +from app.domain.events import ErrorEvent, KioPatchIn, KioPatchOut, PatchSource, SessionMode +from app.domain.kio import EDITABLE_KIO_FIELDS, KIO, PatchRejected, apply_patch +from app.main import app +from app.scenarios.loader import load_file +from app.scoring.card import evaluate_card +from app.session.hub import hub +from app.session.state import SessionState +from app.session.store import MemorySessionStore + + +def test_catalog_keeps_all_209_services_with_attribution_and_source(): + data = ekp.catalog() + + assert len(data.services) == 209 + assert len({item.name for item in data.services}) == 209 + assert sum(item.service_level == "ДДС" for item in data.services) == 157 + assert "ded466988f505082922aacac9fff32b777d1279a" in data.transcription + assert data.license.startswith("MIT") + assert all(item.source_reference.startswith("СЛУЖБЫ 112.docx#word/media/image") + for item in data.services) + + +@pytest.mark.parametrize("name", [ + 'Служба 101 (ГУ МЧС России по г.Москве, ГКУ "Пожарно спасательный центр" ОДС)', + 'Служба 104 (АО "МОСГАЗ" Диспетчерское управление)', + "ЦЭМП", + 'ЦОДД (ГКУ "Центр организации дорожного движения")', + 'ЭВАЖД (ГБУ "Учреждение по эксплуатации высотных административных и жилых домов")', +]) +def test_rows_checked_against_our_screenshots_are_verbatim(name): + assert name in ekp.catalog_names() + + +def test_classifier_services_and_notify_lists_are_untouched(): + """Индексы служб классификатора — основа списков оповещения 96 сценариев.""" + raw = json.loads(ekp.DATA.read_text(encoding="utf-8")) + assert [item.name for item in ekp.services()] == [item["name"] for item in raw["services"]] + assert len(ekp.services()) == 61 + + +def test_operator_adds_catalog_services_without_changing_the_notify_list(): + assert "notify_extra" in EDITABLE_KIO_FIELDS + card = KIO(notify=["Служба 101"]) + + updated = apply_patch(card, {"notify_extra": ["ФСБ", "ЦЭМП", "ФСБ"], "notify": []}) + + assert updated.notify_extra == ["ФСБ", "ЦЭМП"] + assert updated.notify == ["Служба 101"], "автоматический список оператор не правит" + with pytest.raises(PatchRejected): + apply_patch(card, {"notify_extra": ["Служба, которой нет"]}) + with pytest.raises(ValidationError): + apply_patch(card, {"notify_extra": [item.name for item in ekp.catalog().services[:21]]}) + + +def test_service_already_in_notify_is_not_added_twice(): + """ЦЭМП, ФСБ, МОЭК, Метро, Мосгортранс есть и в каталоге, и в классификаторе.""" + card = KIO(notify=["ЦЭМП"]) + + updated = apply_patch(card, {"notify_extra": ["ЦЭМП", "ФСБ"]}) + + assert updated.notify_extra == ["ФСБ"] + + +def test_saved_card_with_renamed_service_still_loads(): + """Каталог проверяется на входе kio.patch: правка названия в каталоге + не должна ломать восстановление снимков и сданных карточек.""" + card = KIO.model_validate({"notify_extra": ["Прежнее название службы"]}) + + assert card.notify_extra == ["Прежнее название службы"] + + +def test_added_services_do_not_change_the_score_against_the_reference(): + library = Path(__file__).resolve().parents[2] / "scenarios" + scenario = load_file(library / "fire-apartment-l2.yaml", library) + card = KIO(address="улица Ленина, 14", description="горит балкон") + + plain = evaluate_card(scenario, card) + extended = evaluate_card(scenario, card.model_copy(update={"notify_extra": ["ФСБ"]})) + + assert [(m.key, m.passed) for m in plain.metrics] == [(m.key, m.passed) for m in extended.metrics] + assert plain.score == extended.score + + +def test_catalog_is_served_like_the_classifier(): + with TestClient(app) as client: + response = client.get("/api/ekp/services") + + assert response.status_code == 200 + body = response.json() + assert len(body["services"]) == 209 + assert {"id", "name", "organization", "service_level", "source_reference"} <= body["services"][0].keys() + + +@pytest.fixture +def lesson(monkeypatch): + sent = [] + monkeypatch.setattr(hub, "store", MemorySessionStore()) + monkeypatch.setattr(hub, "to_trainee", lambda _session_id, event: sent.append(event)) + monkeypatch.setattr(hub, "to_observers", lambda *_args: None) + state = SessionState(session_id=uuid4(), scenario_id="test", scenario_title="Тест", + level="L1", mode=SessionMode.TRAINING) + hub.register(state) + + def patch(fields): + sent.clear() + + async def run(): + async with hub.operation(state.session_id): + await call_ws._handle(state.session_id, state, KioPatchIn(fields=fields)) + + asyncio.run(run()) + return list(sent) + + return state, patch + + +def test_unknown_service_in_kio_patch_is_refused_without_failing_the_lesson(lesson): + state, patch = lesson + + sent = patch({"notify_extra": ["Служба, которой нет"]}) + assert isinstance(sent[0], ErrorEvent) and "не принято" in sent[0].message + assert state.kio.notify_extra == [] + + sent = patch({"notify_extra": ["ФСБ"]}) + assert state.kio.notify_extra == ["ФСБ"] + + +def test_refused_kio_patch_returns_server_values_to_drop_pending(lesson): + """Пакет отклоняется целиком — фронт должен получить серверные значения + всех его полей с `auto`, иначе pending висит до следующего kio.state.""" + state, patch = lesson + patch({"address": "улица Ленина, 14", "notify_extra": ["ФСБ"]}) + + sent = patch({"address": "Тверская, 1", "fire.floors": 5, "notify_extra": ["ФСБ", "Служба, которой нет"]}) + + assert isinstance(sent[0], ErrorEvent) + assert isinstance(sent[1], KioPatchOut) and sent[1].source is PatchSource.AUTO + assert sent[1].fields == {"address": "улица Ленина, 14", "fire.floors": None, "notify_extra": ["ФСБ"]} + assert state.kio.address == "улица Ленина, 14" + + +def test_cleaned_notify_extra_is_sent_back_as_auto(lesson): + state, patch = lesson + state.kio = state.kio.model_copy(update={"notify": ["ЦЭМП"]}) + + sent = patch({"notify_extra": ["ЦЭМП", "ФСБ", "ФСБ"]}) + + auto = [event for event in sent if isinstance(event, KioPatchOut) and event.source is PatchSource.AUTO] + assert auto and auto[-1].fields["notify_extra"] == ["ФСБ"] + assert state.kio.notify_extra == ["ФСБ"] diff --git a/frontend/scripts/test-ws-outbox.mjs b/frontend/scripts/test-ws-outbox.mjs index 6e11fd9..516396e 100644 --- a/frontend/scripts/test-ws-outbox.mjs +++ b/frontend/scripts/test-ws-outbox.mjs @@ -74,6 +74,18 @@ socket.receive({ type: "kio.patch", fields: { }, source: "operator" }); socket = reconnect(socket); assert.deepEqual(socket.sent, [], "matching checkpoint echo clears the KIO patch"); +const refused = { type: "kio.patch", fields: { address: "ул. Новая, 3", notify_extra: ["Нет в каталоге"] } }; +call.send(refused); +socket.receive({ type: "kio.patch", fields: { address: patch.fields.address, notify_extra: [] }, source: "auto" }); +socket.sent = []; +call.send({ type: "kio.patch", fields: { floor: "2" } }); +assert.deepEqual(socket.sent.at(-1).fields, { floor: "2" }, + "server values after a refused patch acknowledge it: refused fields are not merged into the next edit"); +socket.receive({ type: "kio.patch", fields: { floor: "2" }, source: "operator" }); +socket = reconnect(socket); +assert.deepEqual(socket.sent, [], "nothing left to replay after refusal and the next echo"); +const refusedCard = applyPatch(edit(empty, "notify_extra", ["Нет в каталоге"]), { notify_extra: [] }, "auto"); +assert.deepEqual(refusedCard.pending, {}, "server value after refusal drops the pending edit"); const edited = edit(empty, "coords", { latitude: 55.7, longitude: 37.6 }); assert.deepEqual(applyPatch(edited, { coords: { longitude: 37.6, latitude: 55.7 } }, "operator").pending, {}); assert.deepEqual(applyState(edited, { coords: { longitude: 37.6, latitude: 55.7 } }).pending, {}); diff --git a/frontend/src/features/kio-card/KioCard.tsx b/frontend/src/features/kio-card/KioCard.tsx index 4f70658..6ca9c4b 100644 --- a/frontend/src/features/kio-card/KioCard.tsx +++ b/frontend/src/features/kio-card/KioCard.tsx @@ -1,7 +1,9 @@ // Карточка КИО. Один и тот же компонент рисуется у курсанта, на пульте, // на внешнем мониторе и на АРМ ДДС — там в режиме `readOnly`. -import { useIncidentGroups, useSigns } from "@/shared/api/http"; +import { useState } from "react"; + +import { useIncidentGroups, useServiceCatalog, useSigns } from "@/shared/api/http"; import { type CardState, display } from "./merge"; import { type Field, type FieldGroup, visibleGroups } from "./fields"; @@ -17,6 +19,7 @@ interface Props { export function KioCard({ state, required = [], readOnly = false, referenceArm = false, onChange }: Props) { const dds = display(state, "dds") as string | null; const incidentGroup = display(state, "incident_group") as number | null; + const notify = (display(state, "notify") as string[] | null) ?? []; const groups = visibleGroups(dds); const section = (group: FieldGroup) => (
@@ -36,6 +39,7 @@ export function KioCard({ state, required = [], readOnly = false, referenceArm = readOnly={readOnly || field.readOnly} referenceArm={referenceArm} incidentGroup={incidentGroup} + notify={notify} onChange={onChange} /> ))} @@ -58,7 +62,7 @@ export function KioCard({ state, required = [], readOnly = false, referenceArm = } function Row({ - field, value, pending, required, readOnly, referenceArm, incidentGroup, onChange, + field, value, pending, required, readOnly, referenceArm, incidentGroup, notify, onChange, }: { field: Field; value: unknown; @@ -67,6 +71,7 @@ function Row({ readOnly?: boolean; referenceArm: boolean; incidentGroup: number | null; + notify: string[]; onChange?: (path: string, value: unknown) => void; }) { const empty = value === null || value === undefined || value === "" || (Array.isArray(value) && value.length === 0); @@ -85,7 +90,7 @@ function Row({ {readOnly ? ( {formatValue(value, field)} ) : ( - + )} @@ -93,12 +98,13 @@ function Row({ } function Input({ - field, value, referenceArm, incidentGroup, onChange, + field, value, referenceArm, incidentGroup, notify, onChange, }: { field: Field; value: unknown; referenceArm: boolean; incidentGroup: number | null; + notify: string[]; onChange?: (path: string, value: unknown) => void; }) { const emit = (next: unknown) => onChange?.(field.path, next); @@ -141,6 +147,9 @@ function Input({ ); } + if (field.kind === "services") { + return emit(next)} />; + } if (field.kind === "signs") { return emit(next)} />; } @@ -170,6 +179,46 @@ function Input({ ); } +/** Как `max_length` у `KIO.notify_extra`. */ +const SERVICES_LIMIT = 20; + +/** Ручное добавление служб из каталога — окно «Добавьте службы» боевого АРМ. + * Список оповещения по признакам остаётся как есть: здесь только добавки. */ +function ServicesPicker({ value, notify, onChange }: { + value: string[]; notify: string[]; onChange: (next: string[]) => void; +}) { + const catalog = useServiceCatalog(); + const [query, setQuery] = useState(""); + const needle = query.trim().toLowerCase(); + // Лимит и службы из списка оповещения — те же правила, что на сервере: + // лишняя служба отклонила бы весь пакет правок. + const full = value.length >= SERVICES_LIMIT; + const found = needle && !full + ? (catalog.data?.services ?? []) + .filter((item) => !value.includes(item.name) && !notify.includes(item.name) + && item.name.toLowerCase().includes(needle)) + .slice(0, 8) + : []; + return ( +
+ {value.length > 0 &&
+ {value.map((name) => + {name} + )} +
} + setQuery(event.target.value)} /> + {found.length > 0 &&
    + {found.map((item) =>
  • + +
  • )} +
} +
+ ); +} + function IncidentGroupPicker({ value, onChange }: { value: number | null; onChange: (next: number | null) => void }) { const page = useIncidentGroups(); return