- Dashboard.jsx: счётчики (активных / всего / завершено за 24ч из
/operations/summary), фильтры по статусу, сетка карточек операций
(title, статус-бейдж, подразделение, время обновления), клик →
рабочее пространство (анализ по карточке операции).
- Роут /dashboard, «/» → дашборд (было /desktop).
- CaseForm: после создания карточки автоматически создаётся поисковая
операция (title «Поиск: имя, N лет») — операция не блокирует анализ.
- VCL-density, палитра ЦОУ (#ff4d00 акценты, статусы цветом).
Требование Виктора: несколько одновременных поисков, много пользователей
(каждое ЦОУ = аккаунты), КОНТУР подключается к каждому поиску. За основу —
админка РВС (RBAC + сессии + аудит, паттерн проверен в проде).
План: search_operations (надстройка над cases, не дубль SEARCH_OPERATION
КОНТУРа) + cou_units (справочник ЦОУ с иерархией) + RBAC 4 роли +
гео-скоуп (отличие от РВС) + дашборд активных поисков + contour_operation_id
для B20. Этапы E1 RBAC-ядро → E2 скоуп/аудит → E3 операции → E4 КОНТУР →
E5 B17-ингестия. Границы и критерии приёмки внутри.
СТАТУС: план на ревью, не код.
1. Рекомендации не предписывают перекрывать водоёмы/ж/д — оператор ПРОВЕРЯЕТ
их (правки в rules_analysis, scoring_service ×2 копии, water.py + доки).
2. Геокодирование: район нужен (дубли улиц между районами) — подсказка формата
«пункт, район, адрес» и текст ошибки.
198 passed.
При анализе по case_id координаты карточки (tnp_lat/tnp_lon) не попадали
в SearchInput (читались только payload-lat/lon) → rules_analyze не видел
lat/lon и отдавал базовые fallback-зоны «Ближняя N / Средняя E» —
постоянные направления вверх-вправо на карте независимо от местности.
Фикс: case_id-режим подтягивает lat/lon из tnp_* (gps_* как алиас).
198 passed.
Дизайн Виктора (классика ПСО): возможная зона поиска — круг радиусом
max_distance от ТНП (ядро 0.5R / средняя 0.75R / внешняя R с градацией
плотности вероятности), а приоритетные направления ПОДСВЕЧИВАЮТСЯ
45°-секторами полного радиуса поверх кругов (топ-1 #ff4d00 op0.30,
топ-2 #ff9f1a 0.22, топ-3 #ffd166 0.18, прочие серые 0.10).
Раньше сектора были «пирогами» разного радиуса по score — зона выглядела
лоскутной. Теперь круги показывают охват, подсветка — приоритет.
Решение Виктора: СП(лес) 0.5→1.0. Прежняя таблица учитывала лес дважды
(в НормС и в СП) — подросток 14 лет за 3 часа получал 2.55 км вместо
реалистичных 5.1. Согласуется с априором ПСО (94% находок в 3 км):
радиусы стали верхне-перцентилями, не медианой.
- terrain_map пересчитан относительно среднего леса: лес/простой лес/поле/
луг/город = 1.0; сложный/густой лес 0.5; болото 0.4; горы/овраг 0.6;
дорога/тропа 0.8; дефолт 1.0.
- Контрольные точки тестов пересчитаны (6 шт: 1.92→3.84, 0.33→0.665,
2.43→2.7, 5.4→10.8, 13.5→15.0, stats 0.5→1.0) + операционный тест
подростка 5.1 км.
- settlement_score пока НЕ тронут — на обдумывание (Виктор).
193 passed.
Ближайшая вода бралась по bbox сектора: объект в углу bbox, но вне
45°-пирога, попадал в выборку — вода почти не различала направления
(1.393 км у N/NE/E с одной воды в центре). Теперь строго внутри сектора:
water 1.393–2.337 км по направлениям — честная география.
Два бага в одной строке: водные дистанции делились на cos (вместо
умножения, как дороги) и отдавались как км без /1000. Результат —
850.671 «км» до озера в Браславе (реально 271 м).
Верифицировано против geography: merc×cos = 271.0 м ≈ geo 271.5 м
(дельта 0.1%). Теперь Браслав SE: water 0.271 км, settle 0.119 км.
197 passed.
Закономерность «приоритетная зона вверх-вправо»: score_zone умножает
weight × forest_pct, ожидая ДОЛЮ (тесты скоринга 0.3–0.8), а гео-сервисы
(osm_local и legacy Overpass calculate_forest_coverage) отдавали
ПРОЦЕНТЫ 0..100. Лес получил фактический вес в 100 раз больше задумки —
топ зон систематически занимали чуть более зелёные сектора.
Фикс: osm_local.calculate_forest_coverage возвращают долю 0..1.
Разброс скорингов по Минску: 1.244 → 0.032 (честно однородный город).
Остаточные различия — вода/дороги; дифференциация плотностей город/село —
ревью скоринга (B1), коэффициенты не тронуты.
+регресс-тест 0<=forest_pct<=1. 197 passed.
Мобильная версия выпилена — браузерная геолокация оператора больше не
нужна: ТНП задаётся адресом (кнопка «Найти координаты» → geocode) или
вручную. handleGetLocation и его состояния удалены; .gps-button остался
только у геокод-кнопки.
.gps-button { width:100% } в flex-строке съедал всю ширину, input сжимался
до огрызка. Inline width:auto/flexShrink:0 у кнопки и width:auto/minWidth:0
у инпута.
- Step3Circumstances: удалено дублирующее «Последнее известное место»
(last_known_place); остаётся только tnp_address на шаге координат.
- Step4Environment: кнопка «Найти координаты» теперь РАБОЧАЯ — вызывает
новый GET /api/v1/geocode (прокси Nominatim, countrycodes=by, короткий
запрос «пункт, адрес» по граблям geocoding), заполняет lat/lon,
показывает найденный display_name или ошибку. Без debounce-автоплейса
(Pitfall 2) — только явный клик.
- Step3Circumstances: «Прошло времени» считается автоматически из
lost_time (пересчёт каждые 30 с, округление до 0.1 ч); поле остаётся
редактируемым для корректировки.
- БАГФИКС: форма шлёт lost_time, модель хранит loss_time — алиас в
_normalize_case_data. Раньше время пропажи молча терялось
(Pydantic игнорирует неизвестные поля) — elapsed_hours приходилось
вводить руками.
- last_known_place убран из валидации шага 4.
- backend/routers/geocode.py: Nominatim-прокси с auth (operator/field/admin).
192 passed, JSX валиден.
- Удалён /mobile-роут и MobileFormPage; " /" всегда ведёт на /desktop
(user-agent/width-детекция выпилена — ЦОУ работает только на десктопе).
- Тема mobile в index.css оставлена (мёртвый CSS, не мешает).
- CaseForm.isMobile не тронут: DesktopSARPage передаёт false явно.
- prod-stamp-backfill.sql: бэкфилл search_models v1 из cases.analysis_log
(для БД, где таблицы 006 уже созданы create_all — стемп alembic_version).
- e2e-analyze.sh: живая проверка /analyze на CT108 (bike 8yo 2h лес день
-> max_distance 5.4; login — form-urlencoded, не JSON).
- Мелочь: test-mig-seed.sql ON CONFLICT DO NOTHING.
Второй баг той же семьи: polygons.append(poly['coordinates'][0]) клал ring
на уровень полигона — у relation с ОДНИМ outer member'ом coordinates
получались depth-2 ([[pair],...]) при type=Polygon. Это 5 крупнейших
водных объектов (Камсамольскае возера и др.) — фронтовый toLatLngs
строил [[undefined,undefined]...] → Leaflet 'latlngs[0] undefined' →
белый экран страницы анализа.
Теперь relation собирает список Polygon'ов (coordinates=[ring]) и:
1 кольцо → отдаётся как Polygon; 2+ → MultiPolygon из polygon.coordinates.
Баг: для relation с 2+ внешними кольцами coordinates отдавался [ring0, ring1]
(глубина 3) при type=MultiPolygon — невалидный GeoJSON, Leaflet падал
('latlngs[0] undefined') → белый экран страницы анализа на кейсах с
мультиполигонами (РАС-кейс Минск: Камсамольскае возера из 2 колец).
Фронт: toLatLngs различает Polygon/MultiPolygon по глубине coords[0][0][0].
Проблема: 10 инфо-карточек в один столбец, карта с зонами — в самом
низу, немедленные действия терялись в середине, формула занимала
пол-экрана раньше вывода.
Новая структура (сверху вниз = порядок чтения штабом):
1. Шапка-панель решения: срочность | радиус (+множитель профилей
отдельным чипом) | критические действия списком — решение видно
без прокрутки; summary свёрнут в одну строку.
2. Главный ряд: КАРТА (2/3) + список ЗОН (1/3) рядом — зоны и карта
теперь одно визуальное целое; geo-чипы (💧/🚂) и предупреждения
геослоя компактными плашками в шапке карты.
3. Ряд поведение+действия: психотип (тактика 2×2 сеткой), теги
профилей, unmodeled-пометки; немедленные действия нумерованным
списком, КРИТИЧНО-пункты акцентной рамкой.
4. Ряд обоснование радиуса: формула моно-шрифтом + коэффициенты
чипами (НормС/СП/СКД/СУ/СУТ/ВВС/ВП с подсказками) + психотип-
поправки полос чипами.
5. Низ: справка (обстоятельства/здоровье/среда) сеткой 3 колонки.
Цветовая схема не тронута: var(--accent) #ff4d00, зоны #ff4d00/
#ff9f1a/#ffd166, тёмная тема ЦОУ. Направления зон — кириллицей
(С/СВ/В/...). Все поля бэкенда рендерятся (проверено живым analyze).
Определение специфических рекомендаций (матрица профилей §8):
1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
- /api/v1/water/{case_id} отдаёт railway=rail как LineString
(без service/industrial/military веток), кэш общий v2;
- railway_warning «перекрыть/проверить немедленно» по профилям;
- SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.
2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
- раньше чекбокс влиял только на текст, в скоринге был пробел;
- derive в analyze._derive_profiles — работает и для closed_cases.
3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
«вне поведенческой модели» с пояснением (vector_tasks B12:
профили без аналога не выдавать за учтённые); блок на фронте
в карточке здоровья.
Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
- GET /api/v1/water/{case_id}: полигоны водоёмов/болот OSM вокруг ТНП
(Overpass, natural=water+wetland, кэш 72ч, circuit breaker от geo_service);
бейдж-предупреждение по профилям случая (РАС/эпилепсия/не умеет плавать)
- geo_service: цепочка зеркал Overpass (maps.mail.ru → overpass-api.de):
основной сервер имеет AAAA, на хостах без IPv6-маршрута httpx падал
ConnectError'ом до IPv4-фолбэка; на CT108 фапало стабильно
- SearchMap: слой водоёмов под зонами (синие полигоны, болота пунктиром),
попапы с площадью/дистанцией от ТНП; AnalysisResult грузит слой,
показывает предупреждение профиля и счётчик водоёмов
- проверено на CT108: РАС-кейс (Минск) — 43 объекта, витя-кейс
(Каменец-борисовская обл.) — 7 объектов, включая водохранилище Загацце
Сверка с методикой ПСР ред. 12.17 (Евдокимов, Лейтес):
- ночь: 0.5 → 0.0 — методичка §12.3.5 прямо даёт «ночь = 0 км»:
без фонаря в лесу ночью пострадавший не перемещается.
Ранее 0.5 ЗАВЫШАЛО радиус ночного поиска.
- сумерки/вечер: 0.7 → 0.5 — «за час до заката скорость 50% от обычной».
B5 переносил 0.7 из старого фронта вопреки методичке.
- get_base_speed: документирована семантика «скорость смещения»
(не асфальтовая НормС из §12.3.2) — оператору не нужно вводить
асфальтовую скорость, она уже свёрнута; СП = поправка конкретной
местности поверх базового среднего леса.
Тесты обновлены под методичку, добавлен тест ночи = 0.
201 passed, 0 failed.
Тесты проверяли старые бэкендовские скорости (1.0–5.5 км/ч), тогда как
B5 перенёс в живую копию services/distance_service.py каноничные
displacement-скорости фронта (0.3–2.5 км/ч, априоры ПСО «Экстремум»,
94% в 3 км) и сумерки 0.7.
Обновлены ожидания (пересчитаны все кейсы):
- TestBaseSpeed: 0.3/0.7/1.2/1.5/2.0/2.5/2.5/1.5 по возрастам
- 5 кейсов calculate_max_distance пересчитаны (5.12→1.92 и т.д.)
- test_twilight: 0.5 → 0.7
- test_statistics_values: base_speed 4.0 → 1.5
- test_fatigue_minimum: 27.0 → 13.5
- Добавлена контрольная точка B5: bike 8yo 2h лес = 5.4 км
Коэффициенты в services/ НЕ тронуты (B1-safe) — только тесты.
201 passed, 0 failed.
Бэкенд:
- schemas.py: ClosedCaseCreate/Response — форма ~25 полей (субъект, среда,
ресурсы, исход) для внесения завершённых поисков вручную
- routers/closed_cases.py: CRUD + POST {id}/reanalyze. _run_model() —
тот же скоринг, что /analyze (WeightedScorer + distance_service +
психотип), НО БЕЗ claude_service: urgency/behavior/actions по правилам
- _accuracy_note(): прогнозный радиус vs факт (в радиусе / мимо)
- main.py: роутер подключён
Фронтенд:
- pages/ClosedCases.jsx + .css: форма внесения (VCL-density, чипы диагнозов/
рельефа), блок 'Прогноз vs факт', таблица архива (в радиусе/мимо)
- AdminDashboard.jsx: вкладки Кейсы | Завершённые поиски
Тесты: test_closed_cases.py — 6 passed. Существующие 15 failed
(test_distance_service) не связаны с изменениями — падали до них.
Версионированный протокольно-агностичный формат на границе: outbound
(SearchZones/RecommendedRoutes/ResourceTasks/SearchModelMeta) и inbound
(Positions/Tracks/AreasChecked/Observations/FoundEvent). Движок работает
только с форматом, транспорт — адаптеры Контура. От B20 зависят B17 и B18.
Рекоменд.порядок: B14→B15→B20→(B16,B17)→B18.
Co-Authored-By: Claude <noreply@anthropic.com>
По концепции пользователя: Вектор = «где искать», Контур = «как выполнять
поиск в поле» — два независимых проекта. Контур = ATAK + Meshtastic + GPS.
Замкнутый цикл: Вектор→зоны→Контур→поле→наблюдения→Вектор→перерасчёт.
Вектор-сторона взаимодействия = B17 (приём данных) + B18 (перерасчёт/выдача)
+ отдельная задача «Контракт обмена» (развязка: движок не знает ATAK/Mesh).
Co-Authored-By: Claude <noreply@anthropic.com>
Недостижимые от index.js -> App компоненты (23 файла):
- MobileForm.jsx/.tsx/.css (живая мобильная форма = CaseForm)
- BehavioralProfile.tsx/.css
- AnalysisLayout/, MapView/, ResultPanel/ (MapView/ResultPanel
импортировались только из неиспользуемого AnalysisLayout; живая
карта = SearchMap.jsx)
- admin/ (CaseDetail, CasesList, HeatmapView, Statistics) - AdminDashboard
использует inline-UI, не эти компоненты
- CaseForm/Step5Resources.jsx - не подключён в CaseForm.jsx
Сборка проходит. Мёртвый код с его багами (isPsycotypeSelected опечатка,
хардкод URL, NaN-гварды) уходит вместе с файлами.
Co-Authored-By: Claude <noreply@anthropic.com>