Перейти к содержанию

Путь пользователя по интерфейсу 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, UI 181-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 и planEmpty (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.

Диаграммы (учёт требований раздела)

В документе четыре обязательных типа:

  1. stateDiagram-v2 — карта экранов (раздел «Навигация»).
  2. journey — эмоционально-шаговый путь оператора.
  3. sequenceDiagram — браузер → gateway → планировщик при расчёте.
  4. flowchart — состояния расчёта и плана в UI.

Каждая сопровождена текстом «что показано» и «какой вывод».