Путь пользователя по интерфейсу geoscan¶
О чём этот раздел и кому читать: здесь описан веб-интерфейс планировщика (всё в src/frontend, без симулятора simulate/): какие экраны есть, как между ними переходить, что оператор видит и вводит от пустого старта до выгрузки ПЗ на борт, и в каких состояниях бывает UI при расчёте. Жюри и заказчик найдут сквозной сценарий и диаграммы; разработчик — привязку к роутам, store и API; оператор — пошаговые действия и честный список ограничений.
Связанные разделы: бизнес-контекст, сценарии использования, архитектура, API, эксплуатация.
Развёрнутая сборка: https://aerozveno.ff (tailnet, TLS внутреннего CA). Код оболочки: src/frontend/apps/shell, экраны — в remote-приложениях planner, validator, viewer3d (Module Federation).
Каркас приложения¶
| Зона | Назначение | Код |
|---|---|---|
Верхняя панель (TopBar) |
Имя сценария, вкладки «Сценарий / План / 3D-пролёт», переключатель API / Офлайн, статус плана, кнопки «Сброс», «Экспорт», «Рассчитать» | src/frontend/apps/shell/src/TopBar.tsx |
Левый рельс (LeftRail) |
Постоянная навигация по всем режимам (иконки + tooltip) | src/frontend/apps/shell/src/LeftRail.tsx |
| Основная область | React Router: стартовый экран или lazy-remote экран | src/frontend/apps/shell/src/App.tsx |
| Общее состояние | Один MissionStore (zustand), создаётся в shell и передаётся во все remote |
src/frontend/apps/shell/src/App.tsx:20-21, src/frontend/packages/state/src/create-mission-store.ts |
По умолчанию источник данных — API (dataSource: 'api'), не офлайн-мок:
```13:15:src/frontend/packages/state/src/runtime-config.ts export const defaultAppRuntimeConfig: AppRuntimeConfig = { dataSource: 'api', };
Все запросы к бэкенду идут на префикс **`/api`** (тот же origin, что и SPA на `aerozveno.ff`).
---
## Карта экранов и маршрутов
Маршруты заданы в shell; неизвестный путь перенаправляется на `/`.
| Путь | Пункт рельса / вход | Remote-модуль | Что делает пользователь |
|------|---------------------|---------------|-------------------------|
| `/` | «Сценарии» | shell `StartScreen` | Выбор демо, импорт GeoJSON/KML, таблица сценариев |
| `/scenario` | «Область съёмки и парк» | `planner/Scenario` | Карта, задания ПАФС, парк БВС, ветер, критерий, рисование полигонов |
| `/airspace` | «Воздушное пространство и БПЗ» | `planner/Airspace` | Контур разрешённого ВП, эшелоны, таблица вершин, БПЗ |
| `/plan` | «План полётов» | `planner/Plan` | Галсы на карте, гантт, метрики, replan при «выбытии» борта |
| `/pareto` | «Мультикритериальный анализ» | `validator/Pareto` | Фронт Парето, слайдер λ, сравнение планов A/B |
| `/validator` | «Отчёт валидатора» | `validator/Report` | Независимые проверки, фильтры, автоисправления |
| `/export` | «Экспорт и импорт» | `planner/Export` | KML, GeoJSON, QGC `.plan`, CSV |
| `/viewer3d` | «3D-пролёт» | `viewer3d/Viewer` | Рельеф, траектории, анимация, CFIT-клиренс |
Роутинг:
```30:39:src/frontend/apps/shell/src/App.tsx
<Routes>
<Route path="/" element={<StartScreen store={store} />} />
<Route path="/scenario" element={<RemoteView store={store} name="planner · сценарий" component={remotes.scenario} />} />
<Route path="/airspace" element={<RemoteView store={store} name="planner · воздушное пространство" component={remotes.airspace} />} />
<Route path="/plan" element={<RemoteView store={store} name="planner · план" component={remotes.plan} />} />
<Route path="/export" element={<RemoteView store={store} name="planner · экспорт" component={remotes.exportScreen} />} />
<Route path="/validator" element={<RemoteView store={store} name="validator · отчёт" component={remotes.report} />} />
<Route path="/pareto" element={<RemoteView store={store} name="validator · Парето" component={remotes.pareto} />} />
<Route path="/viewer3d" element={<RemoteView store={store} name="viewer3d" component={remotes.viewer3d} />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
Верхние вкладки в TopBar дублируют только три маршрута: /scenario, /plan, /viewer3d (TopBar.tsx:37-41). Остальные экраны — только через левый рельс или кнопки (например «Экспорт» → /export).
Диаграмма: навигация между экранами¶
На схеме — доступные переходы (клик по рельсу, вкладке, кнопке или программный navigate). Состояние «сценарий не загружен» блокирует содержимое большинства экранов, но маршрут сменить можно.
stateDiagram-v2
[*] --> Start: открытие /
Start --> Scenario: Открыть s04 / импорт / Мастер
Start --> Start: неизвестный URL → redirect
state "Рабочий контур" as Work {
Scenario --> Airspace: рельс
Airspace --> Scenario: рельс
Scenario --> Plan: Рассчитать (TopBar или экран)
Plan --> Scenario: рельс / вкладка
Plan --> Validator: кнопка «Отчёт валидатора»
Plan --> Export: TopBar «Экспорт»
Plan --> Viewer3D: вкладка / рельс
Scenario --> Viewer3D: вкладка
Validator --> Pareto: рельс
Pareto --> Plan: рельс
Export --> Plan: рельс
}
Start --> Work: сценарий в store
Work --> Start: рельс «Сценарии» (/)
note right of Start
Без scenario в store
/scenario и /plan показывают Empty
end note
Вывод: критический путь оператора — Start → Scenario → (опционально Airspace) → Plan → Validator → Export; анализ Парето и 3D — ветки после успешного расчёта.
Сквозной путь оператора: от входа до ПЗ на борт¶
Ниже — типовой сценарий в режиме API (как на aerozveno.ff). В режиме Офлайн расчёт идёт в браузере (@geoscan/domain), без POST /api/plans; шаги на экранах те же, но список сценариев и экспорт KML ограничены (см. ограничения).
Диаграмма: journey оператора¶
journey
title Оператор: сценарий s04 → план → выгрузка
section Старт
Открыть aerozveno.ff: 5: Оператор
Убедиться что переключатель API: 4: Оператор
Открыть s04 в таблице: 5: Оператор
section Сценарий
Проверить области ПАФС и парк на карте: 4: Оператор
При необходимости GSD ветер критерий: 4: Оператор
Рассчитать план: 3: Оператор
section План
Дождаться стадий в TopBar: 3: Оператор
Проверить галсы гантт метрики: 4: Оператор
section Качество
Отчёт валидатора: 4: Оператор
При нарушениях — кнопки исправления: 3: Оператор
section Выдача
Экспорт KML GeoJSON per-борт: 5: Оператор
Пошагово (что видеть и что вводить)¶
| Шаг | Экран | Действия | Что на экране |
|---|---|---|---|
| 1 | / |
Открыть https://aerozveno.ff |
Заголовок «Планирование беспилотных авиационных работ», три карточки (Мастер / Импорт / Демо), таблица сценариев |
| 2 | / |
В TopBar оставить API (не «Офлайн») |
В подвале таблицы: «Расчёт: бэкенд gateway» (StartScreen.tsx:269-270) |
| 3 | / |
Нажать Открыть у s04 (единственная строка с ready: true в офлайн-каталоге; в API список подгружается с GET /api/scenarios) |
Переход на /scenario, в TopBar — имя сценария |
| 4 | /scenario |
Осмотреть карту: области заданий, БПЗ, HUD ветра | Справа — задания ПАФС с расчётной высотой и шагом галсов после плана; до расчёта — параметры GSD и парк |
| 5 | /scenario |
(Опционально) Вкладки задания: тип съёмки, GSD, перекрытия; блок «Критерий оптимизации» — makespan / налёт / Парето + λ (ScenarioScreen.tsx:424-436) |
Переключатель критерия меняет scenario.objective и помечает сценарий «грязным» (dirty) |
| 6 | /scenario или TopBar |
Рассчитать | При клике с / или /scenario shell переводит на /plan (TopBar.tsx:129-131) |
| 7 | TopBar |
Дождаться окончания расчёта | Появляется ступенчатый индикатор: Очередь → Геометрия → … → Готово (TopBar.tsx:9-19, 142-155) |
| 8 | /plan |
Проверить HUD: время работ, налёт, покрытие, число нарушений | Гантт внизу; клик по полосе борта открывает drawer с фазами полёта |
| 9 | /plan |
Кнопка Открыть отчёт валидатора или рельс /validator |
Пустой отчёт до расчёта невозможен — нужны plan и report в store |
| 10 | /validator |
Прочитать сводку; при warnings — Применить (если есть) | Тег источника: POST /api/validate или браузерный расчёт (ReportScreen.tsx:95-108) |
| 11 | /export |
Выбрать форматы, Выгрузить | В API: KML/GeoJSON с сервера; .plan/CSV — по одному файлу на борт (ExportScreen.tsx:85-105) |
| 12 | — | Передать файлы на наземную станцию / QGC | Имена вида {scenarioId}-{uavId}.plan |
Импорт вместо демо: на / — «Выбрать файл» (.kml, .geojson, .json) → полигоны добавляются как задания → переход на /scenario (StartScreen.tsx:67-77).
«Новый сценарий» и карточка «Демо»: обе вызывают open('s04'), то есть загружают встроенный демо-сценарий s04, а не пустой проект (StartScreen.tsx:178, 216).
Расчёт плана: браузер, gateway и планировщик¶
Диаграмма: последовательность (режим API)¶
sequenceDiagram
participant UI as Браузер (shell + store)
participant GW as Gateway /api
participant PL as Planner (gRPC)
UI->>UI: compute() dataSource=api
UI->>GW: POST /api/scenarios (draft id при правках)
GW-->>UI: scenario_id
UI->>GW: POST /api/plans + Idempotency-Key
GW-->>UI: 202 plan_id
loop Прогресс
GW-->>UI: SSE plan events (stage, message)
Note over UI: TopBar Steps queued…completed
end
alt SSE недоступен
UI->>GW: GET /api/plans/{id} (poll)
end
GW->>PL: расчёт галсов маршрутов
PL-->>GW: plan document
UI->>GW: POST /api/validate (scenarioDoc + planDoc)
alt validate OK
GW-->>UI: отчёт
else validate недоступен
UI->>UI: validate() в @geoscan/domain
end
UI->>UI: plan, report, dirty=false
Логика в коде: create-mission-store.ts вызывает runApiCompute (api-compute.ts:124-207) — сохранение сценария, создание плана, ожидание SSE/опроса, валидация.
Черновик сценария на сервере получает новый scenario_id (…-draft-xxxxxxxx), чтобы не затирать встроенные s01–s10 (api-compute.ts:77-84).
Локальные правки сценария сбрасывают serverScenarioId и scenarioDoc, чтобы следующий расчёт не ушёл со старым телом (create-mission-store.ts:61-80).
Диаграмма: состояния UI расчёта и плана¶
Статус в TopBar (StatusPill) выводится из computing, computeError, plan, dirty (TopBar.tsx:43-51).
flowchart TD
A[Нет scenario] -->|loadScenario| B[Черновик: plan=null]
B -->|правка полей| C[Требует пересчёта dirty]
C -->|Рассчитать| D[computing=true]
B -->|Рассчитать| D
D -->|успех API/mock| E[План рассчитан dirty=false]
D -->|ошибка| F[computeError stage=failed]
E -->|правка сценария| C
E -->|plan.unassigned или warnings| G[Частичный результат на экране План]
E -->|violations в metrics| H[План с нарушениями в UI]
F -->|исправить ввод / повтор| D
Стадии расчёта в API (полоса Steps под TopBar): queued, geometry, coverage, routing, solving, export, completed; при ошибке — failed (TopBar.tsx:9-19, api-compute.ts:132-137).
Офлайн-режим: compute() в domain store синхронно вызывает solve, naiveBaseline, validate, paretoFront с задержкой 30 ms для отображения спиннера (src/frontend/packages/domain/src/store.ts:462-488). Стадии SSE не показываются (нет computeStage из бэкенда).
Экраны: возможности и переходы¶
Стартовый экран /¶
- Таблица: сценарий, задания АФС, парк, критерий, площадь, статус, Открыть (двойной клик по готовой строке).
- В режиме API — подгрузка
listScenarios; при пустом ответе или ошибке — fallback наSCENARIO_CARDS(StartScreen.tsx:32-64). - Импорт полигонов съёмки.
Сценарий /scenario¶
- Пусто:
Empty«Сценарий не загружен» (ScenarioScreen.tsx:74-79). - Карта: выбор задания, рисование области ПАФС или БПЗ (минимум 3 вершины); во время рисования расчёт заблокирован (
ScenarioScreen.tsx:149-152). - Панель: задания, включение/выключение бортов, ветер, критерий (makespan /
total_flight_time/ pareto + λ), слои карты (ScenarioScreen.tsx:424-436). - Инструменты на карте (
MapToolbar): выбор, рисование задания, рисование БПЗ, постановка/перемещение ВПП (place-site,ScenarioScreen.tsx:688-694), zoom (678-705). Типmeasureобъявлен вToolId, но кнопки измерения в тулбаре нет (src/frontend/packages/domain/src/store.ts:30).
Воздушное пространство /airspace¶
- Контур разрешённого ВП, редактирование нижней/верхней границы (AGL / AMSL), таблица вершин с азимутом и длиной ребра.
- Добавление БПЗ в режиме рисования на карте.
- Экспорт контура allowed в GeoJSON (кнопка на панели).
План /plan¶
- Нет плана:
Empty+ кнопка «Рассчитать план» (PlanScreen.tsx:60-69). - Карта: галсы, перелёты, слои (переключатели), сравнение с наивным бейзлайном, блок Replan после пересчёта без борта в API (
PlanScreen.tsx:104-112). - Выбытие борта: иконка молнии →
replanWithout(PlanScreen.tsx:246-252). - Секции «Нераспределённые галсы» и «Предупреждения» при непустых
plan.unassigned/plan.warnings(PlanScreen.tsx:278-306).
Отчёт валидатора /validator¶
- Группы проверок, фильтр all/warn/pass (
ReportScreen.tsx:33, UI181-186), кнопки автоисправленияapplyFixс повторнымcompute()(ReportScreen.tsx:51-76). - Экспорт JSON отчёта работает; кнопка PDF без обработчика (
ReportScreen.tsx:115-117).
Парето /pareto¶
- Требует
planи непустойparetoв store (ParetoScreen.tsx:38-47). - После API-расчёта
runApiComputeвызываетderiveParetoBaseline()и кладёт в store тот жеparetoFront(scenario), что и в офлайне (api-compute.ts:191-200). Пустой экран — еслиparetoFrontвернул[]или нетplan, а не из‑за «обнуления» API. Очисткаpareto: []при загрузке сценария с сервера —loadScenarioFromApi(api-compute.ts:226), не после расчёта.
Экспорт /export¶
- Без
scenarioиplan—Empty(ExportScreen.tsx:77-82). - KML в офлайне: предупреждение «нужен бэкенд» (
ExportScreen.tsx:113-115). - Кнопка «Архивом» disabled (
ExportScreen.tsx:192-194).
3D-пролёт /viewer3d¶
- Требует загруженный сценарий и рассчитанный план (иначе
Empty). - Без сценария/плана —
Empty(Viewer3DScreen.tsx:330). Orbit/chase —358-361; масштаб по Z, слои, анимация поtimeS/playing(хуки и deck выше по файлу).
Падение remote-модуля¶
Если не поднят dev-сервер federated-модуля, RemoteView показывает Result с кнопкой «Повторить» (RemoteView.tsx:26-44). В проде на aerozveno.ff remotes собраны в static site.
Локальный запуск интерфейса¶
Для разработки UI нужны Node по engines в src/frontend/package.json (>=20.19, на практике удобен LTS 20.x — перед командами проверьте node -v; на станции по умолчанию может стоять Node 24) и установленные зависимости в worktree (в git node_modules нет). Подробности сборки и деплоя — в эксплуатации, когда раздел заполнен.
Шаг 1 — зависимости (из корня репозитория):
cd src/frontend
npm ci
Без этого шага npm run dev сразу падает: sh: 1: concurrently: not found (скрипт dev вызывает concurrently из devDependencies, см. src/frontend/package.json:12-16).
Шаг 2 — dev-серверы Module Federation (shell + три remote):
npm run dev
Скрипт поднимает четыре процесса Vite: shell 5173, planner 5174, validator 5175, viewer3d 5176 (apps/*/package.json, порты в vite --port …).
Проверено в сессии доработки (заход 2): после npm ci (286 пакетов) при Node v20.20.2 все четыре процесса не стартуют — одинаковая ошибка загрузки vite.config.ts:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './internal' is not defined by "exports"
in .../node_modules/vite/package.json
imported from .../node_modules/@vitejs/plugin-react/dist/index.js
Причина в дереве зависимостей: в корневом node_modules оказывается vite 7.3.6 (через @module-federation/vite), а @vitejs/plugin-react подтягивает его вместо vite 8.x из workspace-приложений (npm ls vite помечает связку как invalid). Это не лечится одним только переключением с Node 24 на 20 — на той же ветке после чистого npm ci отказ воспроизводится. Пока lockfile не выровнен, для просмотра UI разумнее живой стенд https://aerozveno.ff (см. curl ниже) или сборка npm run build:site (отдельная проверка — в эксплуатации).
Тот же сценарий на Node v24.5.0 без смены версии: concurrently запускается, затем те же ERR_PACKAGE_PATH_NOT_EXPORTED на shell/planner/validator/viewer3d.
Проверка живого стенда (выполнено 2026-09-17, TLS: --cacert ff-ca/ff-ca.crt при строгой проверке CA; ниже — как на стенде с доверенным CA tailnet):
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' https://aerozveno.ff/
curl -sS https://aerozveno.ff/healthz
curl -sS 'https://aerozveno.ff/api/scenarios' | head -c 400
curl -sS -o /dev/null -w 'shell remoteEntry %{http_code}\n' https://aerozveno.ff/remoteEntry.js
curl -sS -o /dev/null -w 'planner remoteEntry %{http_code}\n' https://aerozveno.ff/planner/assets/remoteEntry.js
Фактический вывод:
HTTP 200
{"status":"ok"}
{"items":[{"scenario_id":"doc-api-1789645346","name":"Док: короткий черновик для POST 201",...
shell remoteEntry 200
planner remoteEntry 200
Порядок items в GET /api/scenarios на стенде не фиксирован — первым может быть пользовательский черновик, не s01-simple. Встроенный каталог UI (open('s04')) не обязан совпадать со списком API (на стенде есть s04-mixed-fleet и черновики s04-draft-…, отдельной строки s04 в API может не быть).
Без графического браузера в этой сессии не проверяли: клики по кнопкам, отрисовку карты, анимацию Steps и содержимое remote-экранов после загрузки JS. Эти состояния описаны по коду (ScenarioScreen, PlanScreen, TopBar) и считаются непроверенными визуально до ручного прогона в браузере.
Замечание: GET /api/health и GET /api/healthz на стенде отвечают ошибкой (Cannot GET …); liveness gateway — GET /healthz без префикса /api (src/backend/gateway/src/ops/ops.controller.ts:24-28, прокси в src/frontend/tools/serve-site.mjs:22).
Ограничения и расхождения с ожиданиями¶
Проверка по коду ветки и сверка с приёмкой: 96-status/00-final-status.md, 93-review/99-defects.md; архивный вердикт — git show accept/geoscan-final:docs/task5/96-acceptance/00-verdict.md. Бэкенд с 17.09.2026 частично чинился пакетами Б-1…Б-9 (merge-log); ниже — то, что оператор видит в UI независимо от волны фиксов.
| Тема | Ожидание пользователя | Факт в интерфейсе | Где в коде |
|---|---|---|---|
| Набор s01–s10 на старте | Все демо «готовы» | В офлайн-таблице только s04 с ready: true; остальные «Не загружен» |
scenarios.ts:220-236, 251, … |
| Загрузка s01… | Открыть любой тест | scenarioById возвращает данные только для s04; в API — по scenario_id с сервера, иначе fallback на офлайн |
scenarios.ts:351-352, create-mission-store.ts:170-177 |
| «Новый сценарий» | Пустой проект | Открывается тот же s04 | StartScreen.tsx:178 |
| Парето после расчёта на API | График компромиссов | После runApiCompute pareto из deriveParetoBaseline() / paretoFront — как в офлайне; пусто, если фронт не построил точек |
api-compute.ts:191-200, ParetoScreen.tsx:38-47 |
| Линейка / measure на карте | Измерение расстояния | measure в типе ToolId, кнопки в MapToolbar нет |
store.ts:30, ScenarioScreen.tsx:678-705 |
| PDF-отчёт | Выгрузка для жюри | Кнопка без действия | src/frontend/apps/validator/src/screens/ReportScreen.tsx:115-117 |
| ZIP всех форматов | Одна кнопка | disabled, подпись «в проде — с бэкенда» | ExportScreen.tsx:192-194 |
| KML без сервера | Локальная выгрузка | Только предупреждение в офлайне | ExportScreen.tsx:113-115 |
| Нижние иконки рельса | Настройки CRS / масштаб | Декоративные <span>, не ссылки |
LeftRail.tsx:72-81 |
Из вердикта приёмки (состояние на accept/geoscan-final, перепроверять по бэкенду текущей ветки): сквозной REST работал, а UI имел дефекты трассировки (маппинг полей, «залипание» scenarioId) — часть закрыта сбросом serverScenarioId при локальных правках (create-mission-store.ts:61-80). Утверждения вердикта о пустом плане на смешанном парке, заходе в НФЗ и расхождении валидатора относятся к планировщику/валидатору, но оператор увидит это на /plan и /validator как нарушения, предупреждения или пустой гантт — имеет смысл всегда открывать отчёт перед экспортом. |
Частичный результат в UI (не обязательно ошибка расчёта):
plan.unassigned— список галсов с причиной на/plan.plan.warnings— жёлтые алерты (например неполное покрытие с бэкенда).plan.metrics.violations— счётчик в HUD и статус «План с нарушениями» на экспорте.- Статус «Требует пересчёта» в TopBar при
dirty: trueпосле правок.
Сводка для трёх аудиторий¶
| Аудитория | Главное |
|---|---|
| Жюри | Сервис в браузере ведёт от области съёмки и парка БВС к ПЗ в KML/GeoJSON/QGC; независимый отчёт валидатора — отдельный экран; демо на s04 и API-стенд aerozveno.ff. |
| Разработчик | Один store, 8 маршрутов, 4 MF-remotes; API по умолчанию; расчёт — runApiCompute + SSE; офлайн — domain/solve. |
| Оператор | API → открыть s04 → проверить сценарий → Рассчитать → план → валидатор → экспорт; при «выбытии» борта — молния на карточке борта; ВПП — инструмент «Поставить / переместить ВПП» на /scenario. |
Диаграммы (учёт требований раздела)¶
В документе четыре обязательных типа:
- stateDiagram-v2 — карта экранов (раздел «Навигация»).
- journey — эмоционально-шаговый путь оператора.
- sequenceDiagram — браузер → gateway → планировщик при расчёте.
- flowchart — состояния расчёта и плана в UI.
Каждая сопровождена текстом «что показано» и «какой вывод».