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

Архитектура geoscan: компоненты, связи, потоки данных

О чём этот раздел и кому читать. Здесь описано, из каких процессов собран сервис планирования БВС (вне каталога simulate/), как они связаны по сети, куда уходят данные одного расчёта и почему выбраны именно такие границы. Заказчику и жюри достаточно разделов «Контекст», «Ключевое решение — валидатор» и одной диаграммы потока. Разработчику нужны таблицы портов, ссылки на код и отличия docker-compose от k3s/aerozveno. Оператору — блок «Развёртывание» и «Что проверить, если сломалось». Детали расчёта галсов и энергии — в доменном разделе, REST-контракты — в API (черновик), пошаговый запуск — в эксплуатации.

Нормативные принципы бэкенда A-01…A-15 зафиксированы в docs/task5/50-stack/05-service-architecture.md; ниже — как они проявляются в текущем коде этой ветки.


Контекст системы (C4)

Сервис принимает сценарий (область съёмки, парк, ЛЗП, ПАФС/ЛАФС, ветер, критерий оптимизации) и возвращает полётные планы по бортам с метриками и выгрузкой в KML/GeoJSON. Пользователь работает через браузер; внешних публичных API для третьих сторон нет — контур .ff доступен только из tailnet.

C4Context
  title geoscan — контекст (продукт, без simulate/)
  Person(operator, "Оператор / инженер", "Редактирует сценарий, запускает расчёт, скачивает KML")
  Person(jury, "Жюри / заказчик", "Смотрит демо, POST /api/validate")
  System_Boundary(geoscan, "geoscan") {
    System(app, "Веб-приложение + API", "Планирование и проверка планов БВС")
  }
  System_Ext(prometheus, "Prometheus", "Сбор /metrics")
  System_Ext(creds, "creds-store", "Секреты (tailnet)")
  Rel(operator, app, "HTTPS", "aerozveno.ff / geoscan.ff")
  Rel(jury, app, "HTTPS + curl", "OpenAPI, /api/validate")
  Rel(app, prometheus, "scrape")
  Rel(app, creds, "ключи при деплое", "не в рантайме запросов")

Вывод: единственная «система» для человека — HTTPS на домене стенда; мониторинг и секреты — инфраструктура вокруг, не часть доменной логики.


Контейнеры и протоколы (C4)

Целевой контур — четыре бэкенд-процесса (A-15): gateway, planner, validator, Postgres. Отдельный сервис airspace в манифестах не развёрнут — зоны приходят в JSON сценария. Фронтенд на полном стенде aerozveno — отдельный pod с nginx; MinIO хранит только артефакт сборки SPA, не планы.

C4Container
  title geoscan — контейнеры (логические процессы)
  Person(user, "Браузер")
  Container_Boundary(k8s, "Kubernetes ns aerozveno / geoscan") {
    Container(ingress, "Ingress nginx", "TLS, whitelist tailnet", "HTTPS")
    Container(fe, "geoscan-frontend", "nginx + MF static", "HTTP 8080")
    Container(gw, "gateway", "NestJS + Fastify", "HTTP 3000")
    Container(pl, "planner", "Python + OR-Tools", "gRPC 5001, ops 3001")
    Container(vl, "validator", "Python", "gRPC 5002, ops 3002")
    ContainerDb(pg, "Postgres", "PostGIS image", "5432, schema gateway")
    Container(minio, "MinIO", "S3 API", "9000 — только aerozveno")
  }
  Rel(user, ingress, "HTTPS")
  Rel(ingress, fe, "/")
  Rel(ingress, gw, "/api, /metrics, …")
  Rel(gw, pl, "gRPC + x-service-api-key", "PlannerService.Plan (stream)")
  Rel(gw, vl, "gRPC", "ValidatorService.Validate")
  Rel(gw, pg, "SQL", "очередь планов, сценарии")
  Rel(fe, minio, "mc mirror", "init/sidecar")
  Rel(gw, pl, "gRPC", "ExportPlan → KML")

Таблица портов и вызовов

Компонент Публичный HTTP Ops HTTP gRPC Кто вызывает
gateway :3000 (в k8s Service geoscan:80→3000) /healthz, /readyz, /status, /metrics на том же порту Браузер, Ingress, Prometheus
planner нет (A-02) :3001 — тот же ops-контракт :5001 PlannerService gateway
validator нет :3002 (образ есть; в k8s readiness часто по TCP :5002) :5002 ValidatorService gateway (POST /api/validate), readiness
Postgres gateway (миграции + gateway.*)
frontend nginx :8080 (Service :80) /healthz, /readyz Ingress для /
MinIO ClusterIP :9000 init/sidecar frontend

Адреса gRPC в gateway задаются env PLANNER_GRPC_ADDR / VALIDATOR_GRPC_ADDR (дефолты planner:5001, validator:5002) — см. src/backend/gateway/src/config/config.schema.ts:25-26. Метаданные gRPC: x-request-id, x-service-api-keysrc/backend/shared/src/grpc/metadata.ts:3-13.

Dockerfile'ы бэкенда (три процесса, не больше):

src/backend/gateway/Dockerfile
src/backend/planner/Dockerfile
src/backend/validator/Dockerfile

Ключевое архитектурное решение: независимый валидатор

Зачем отдельный процесс и отдельная кодовая база. Метод проекта — «валидатор раньше продукта» (docs/task5/70-plan/01-validator-first.md): любое утверждение о качестве плана должно сводиться к числам, которые пересчитывает не решатель. Валидатор не импортирует пакет planner (барьер validator/tests/test_independence.py + статический скан). Геометрия покрытия, энергия, ветер, НФЗ и назначение галсов считаются своими модулями в src/backend/validator/validator/checks/.

Зачем gRPC, если есть CLI. Тот же код доступен как python -m validator {run,check,serve} и как ValidatorService.Validate (src/backend/proto/geoscan/validator.proto:7-9). Публичная ручка POST /api/validate (src/backend/gateway/src/validate/validate.controller.ts:10-21) проксирует в gRPC — «проверьте наш план сами» для жюри без доступа к репозиторию.

Что это дало на практике. Расхождение энергомоделей между планировщиком и контрактом было поймано проверкой G3-04 (energy_used_frac): планировщик записывал долю от бюджета с вычетом резерва, валидатор — от полного endurance. Отношение ~1,25 при резерве 20 % задокументировано в docs/task5/98-fixes/energy-model.md; исправление — src/backend/planner/planner/pipeline.py (см. тот же документ). Без независимого пересчёта ошибка выглядела бы как «план валиден».

Ограничение текущей реализации. Очередь расчёта в gateway не вызывает validator после PlannerService.Plan — см. plan-dispatcher.service.ts:100-149 (только planner). Проверка на стенде после расчёта инициирует фронтенд: POST /api/validate в src/frontend/packages/state/src/api-compute.ts:173-186. Колонка gateway.plans.validation в миграции есть (migrations/1758153600000_gateway-init.sql:33), но markCompleted её не заполняет (postgres-plans.repository.ts:238-261). Итог: серверный отчёт валидатора доступен по API, но не является gate перед сохранением плана в БД.


Поток одного расчёта: кнопка → KML

Оператор в UI нажимает «Рассчитать». Дальше — цепочка ниже. Время ответа зависит от solver.time_limit_s в сценарии (типично десятки секунд); в интерфейсе идут SSE-события с stage и progress.

sequenceDiagram
  autonumber
  actor U as Браузер (shell + planner remote)
  participant GW as gateway :3000
  participant PG as Postgres gateway.*
  participant PL as planner gRPC :5001
  participant VL as validator gRPC :5002

  U->>GW: POST /api/scenarios (при необходимости черновика)
  U->>GW: POST /api/plans + Idempotency-Key
  GW->>PG: INSERT plan_jobs (queued), idempotency
  GW-->>U: 202 {planId, status: queued}

  U->>GW: GET /api/plans/{id}/events (SSE)
  GW->>PG: claim job → running
  GW->>PL: Plan(stream): scenario_json, criterion, time_limit_s
  loop прогресс решателя
    PL-->>GW: PlanProgress
    GW->>PG: plan_events + SSE data
    GW-->>U: event: progress / stage
  end
  PL-->>GW: PlanResult (plan_json, metrics)
  GW->>PG: INSERT gateway.plans, job done
  GW-->>U: SSE stage completed

  U->>GW: GET /api/plans/{id}
  GW-->>U: status completed, plan, metrics

  U->>GW: POST /api/validate {scenario, plan}
  GW->>VL: Validate
  VL-->>GW: admitted, violations, metrics
  GW-->>U: report (вкладка валидатора)

  U->>GW: GET /api/plans/{id}/export/kml
  GW->>PL: ExportPlan
  PL-->>GW: bytes KML
  GW-->>U: attachment

Где в коде. Постановка в очередь: plans.controller.ts:40-60 (202 ACCEPTED). Диспетчер и gRPC planner: plan-dispatcher.service.ts:55-149. SSE: plans.controller.ts:87-162. Экспорт KML: plans-export.service.ts:31-66PlannerGrpcClient.exportPlan. Локальный fallback валидатора в браузере — validate() из @geoscan/domain, если POST /api/validate недоступен (api-compute.ts:171-188).

Известные разрывы (диаграмма выше — целевой E2E, не гарантия содержания плана)

Sequence показывает рабочую цепочку вызовов на стенде с gateway и planner. Отдельно от «сломанного API» остаются продуктовые ограничения, которые видны только в теле плана или на конкретном домене:

Разрыв Что видит человек Где в коде
Посадка не в точке дома (R-OUT-7) KML/3D: фаза LANDING стоит в последней точке галса, хотя перед ней уже есть TRANSIT домой После обхода галсов pos — конец последнего transect; фаза посадки задаёт path=[pos, pos], а не координаты ЛЗП: src/backend/planner/planner/pipeline.py:309-349
https://geoscan.ff — не полноценный UI В браузере одна строка «geoscan gateway», API и Swagger работают Заглушка src/backend/gateway/public/index.html:8; Ingress k3s/geoscan/60-ingress.yaml отдаёт весь / на gateway. Module Federation и карта — только aerozveno.ff (k3s/aerozveno/60-ingress.yaml)
Сценарий UI vs JSON-схема gateway 422 с SCHEMA_VALIDATION_FAILED при сохранении черновика Тело POST /api/scenarios проходит scenario.schema.json (scenario-schema.pipe.ts). UI шлёт только через scenarioToApi (api-compute.ts:109-112); регрессии имён полей блокирует mappers.spec.ts:24-40. Встроенные s01–s10 в ConfigMap scenarios (k3s/aerozveno/20-cm-scenarios.yaml) — отдельный источник для демо/CLI, не проходит фронтовый mapper
Валидатор не gate на сервере План в БД со статусом completed даже при нарушениях, если клиент не вызвал /api/validate См. § «Ключевое архитектурное решение» и plan-dispatcher.service.ts:100-149

Вердикт приёмки на эталонной ветке accept/geoscan-final2 перечислял те же классы проблем; на docs2/gs-architecture часть API уже доработана (SSE, turnaround_time_min в mapper), но геометрия посадки в planner по коду выше всё ещё расходится с ожиданием «посадка на дом».


Фронтенд: Module Federation

Хост shell подключает три remote-приложения (карта/редактор, отчёт валидатора, 3D) — src/frontend/apps/shell/vite.config.ts:18-39. Общее состояние не шарится через MF: zustand-store создаётся в shell и передаётся пропом (vite.config.ts:16-17). API только через gateway (A-01) — клиент @geoscan/api-client бьёт в /api/....

На стенде aerozveno статика не в образе gateway: Ingress отдаёт / на geoscan-frontend, /api на gateway (k3s/aerozveno/60-ingress.yaml:26-50). Бандл зеркалируется из MinIO (k3s/aerozveno/45-deployment-frontend.yaml:103-114, bucket aerozveno-frontend, endpoint minio.aerozveno.svc.cluster.local:9000).


Хранение данных (Postgres)

Используется образ PostGIS (postgis/postgis:16-3.4 в docker-compose.yml:9 и k3s/aerozveno/30-postgres.yaml:37), но прикладной код gateway не выполняет пространственных SQL-запросов PostGIS — геометрия в jsonb сценария/плана, расчёт в Python/Shapely и в валидаторе. Схема gateway одна на сервис (отдельные схемы planner/validator/airspace из A-10 пока не заведены).

erDiagram
  gateway_scenarios ||--o{ gateway_plans : "scenario_id"
  gateway_scenarios ||--o{ gateway_plan_jobs : "scenario_id"
  gateway_plan_jobs ||--|| gateway_plans : "plan_id"
  gateway_plan_jobs ||--o{ gateway_plan_events : "plan_id"
  gateway_idempotency_keys }o--o| gateway_plan_jobs : "plan_job_id"
  gateway_plans ||--o{ gateway_plans : "parent_plan_id"

  gateway_scenarios {
    uuid id PK
    uuid group_id
    int version
    jsonb body
    text source
  }
  gateway_plans {
    uuid id PK
    uuid scenario_id FK
    text objective
    jsonb body
    jsonb metrics
    jsonb validation
    text solver_status
  }
  gateway_plan_jobs {
    uuid plan_id PK
    text status
    jsonb job_input
    text request_id
    float progress
  }
  gateway_plan_events {
    uuid plan_id PK
    bigint event_id PK
    jsonb payload
  }
  gateway_idempotency_keys {
    text scope PK
    text key PK
    text response_body
  }

Таблицы и ограничения — src/backend/gateway/migrations/1758153600000_gateway-init.sql и 1758153800000_gateway-runtime.sql (очередь, SSE, idempotency).


Доменные типы (фронт и контракт)

На клиенте канонические типы сценария и плана — src/frontend/packages/domain/src/types.ts. На бэкенде зеркало входа — Pydantic geoscan_contracts.scenario (shared-py/geoscan_contracts/scenario.py). Имена в API — snake_case (A-03, keepCase в gRPC).

classDiagram
  class Scenario {
    +schemaVersion
    +scenarioId
    +launchSites
    +fleet
    +tasks
    +airspace
    +weather
    +objective
    +solver
  }
  class FleetUnit {
    +id
    +model
    +payload
    +home
    +enabled
    +batteryPct
  }
  class SurveyTask {
    +surveyType
    +gsdCm
    +area Polygon
    +angleDeg
  }
  class Plan {
    +planId
    +missions
    +metrics
    +unassigned
  }
  class Mission {
    +uavId
    +sorties
    +totalDurationS
  }
  class Sortie {
    +phases
    +energyUsedFrac
    +durationS
  }
  class ValidationReport {
    +admitted
    +violations
    +checks
  }
  Scenario "1" --> "*" SurveyTask : tasks
  Scenario "1" --> "*" FleetUnit : fleet
  Plan "1" --> "*" Mission : missions
  Mission "1" --> "*" Sortie : sorties
  Scenario ..> Plan : вход расчёта
  Plan ..> ValidationReport : POST /api/validate

Принципы A-01…A-15: смысл и статус в коде

Источник определений — 05-service-architecture.md. Сводка по фактическому состоянию репозитория (не по плану).

ID Суть на практике Статус в ветке
A-01 Один HTTP-процесс наружу: REST /api/*, OpenAPI, ops, SSE, export Частично. Три Dockerfile'а только у gateway/planner/validator. На aerozveno Ingress делит / (frontend) и /api (gateway) — два HTTP-сервиса снаружи, но бизнес-API один. Swagger UI: /api/docs (SwaggerModule.setup('api/docs', …)main.ts:90-91; строка лога docs: /docs на main.ts:98 — устаревшая подпись, не URL). Статика в gateway через fastify-static (main.ts:76-80) — на geoscan.ff это заглушка public/index.html, на aerozveno основной UI с nginx
A-02 У доменов только ops HTTP Да для planner/validator (planner/rpc/ops.py)
A-03 .proto + keepCase, pin-тесты Даproto/geoscan/*.proto, shared/src/grpc/loader-options.ts
A-04 JWT снаружи, сервисный ключ в gRPC Упрощённо: ключ в metadata, пользовательский JWT не разобран в доменах
A-05 x-request-id сквозь вызовы Даrequest-id.plugin.ts, buildMetadata
A-06 gRPC → HTTP в одном месте Даgrpc-to-http.ts, DomainExceptionFilter
A-07 Долгий расчёт: 202 + SSE + stream RPC ДаPOST 202, Planner.Plan stream, SSE
A-08 Outbox / шина Вне объёма — RabbitMQ не используется
A-09 Idempotency-Key на POST /api/plans Даplans.service.ts:47-120, таблица idempotency_keys
A-10 Схема на сервис Частично: только gateway в БД; airspace как процесс не поднят
A-11 Конфиг из env Даconfig.schema.ts, секреты в k8s Secret
A-12 /metrics с первого дня Да gateway; geoscan_validator_violations_total в validator (validator/rpc/prom_metrics.py)
A-13 Барьерные тесты Да — independence, loader options, integration в src/backend/tests/
A-14 Новый сервис по шаблону Процедура в A-14 документе; фактически три доменных образа
A-15 Минимум процессов (~4) Да — gateway + planner + validator + Postgres; MinIO/nginx только для доставки UI

Подробный чеклист ревью: docs/task5/93-review/00-checklist.md.


Развёртывание: локально и в кластере

Локальный контур (docker-compose.yml)

Сервис Назначение
postgres БД, volume postgres-data
planner, validator без published ports
gateway единственный published port 3000:3000 (docker-compose.yml:105-106)

Фронтенд в compose не входит — разработка через Vite на хосте с proxy на :3000. GATEWAY_PLANS_STORAGE=postgres в compose (docker-compose.yml:102).

Команда проверки CLI валидатора (выполнена в среде документирования):

cd src/backend && uv run python -m validator --help

Фрагмент вывода:

usage: python -m validator [-h] [--version] {run,check,serve} ...
Независимый пересчёт метрик и нарушений полётного плана
  run              сводная таблица по каталогу сценариев
  check            подробный отчёт по одному плану
  serve            gRPC ValidatorService (D-24)

Барьер независимости (50 тестов):

cd src/backend && uv run pytest validator/tests/test_independence.py -q
# 50 passed in 1.11s

Kubernetes: два namespace

k3s/geoscan/ k3s/aerozveno/
Домен geoscan.ff aerozveno.ff
UI в браузере заглушка gateway (public/index.html:8) shell + MF remotes через geoscan-frontend
Ingress всё / → gateway (60-ingress.yaml:19-24) /api, ops → gateway; / → frontend (60-ingress.yaml:26-50)
Frontend + MinIO нет в kustomization 45-deployment-frontend.yaml, 35-minio.yaml
Образы gitea.ff/gpb/geoscan-*:7173c9af cr.yandex/.../aerozveno-*:5e046484
ConfigMap нет сценариев в pod hardware-catalog, scenarios смонтированы в gateway
flowchart TB
  subgraph internet["Публичный интернет"]
    X[запросы]
  end
  subgraph tailnet["Tailscale 100.64.0.0/10"]
    U[Оператор]
  end
  subgraph n1["k3s n1 — ns aerozveno"]
    ING[Ingress nginx<br/>whitelist tailnet + 10.42.0.0/16]
    FE[deploy/geoscan-frontend<br/>nginx :8080]
    MINIO[(MinIO :9000)]
    GW[deploy/geoscan gateway :3000]
    PL[deploy/geoscan-planner<br/>5001 gRPC]
    VL[deploy/geoscan-validator<br/>5002 gRPC]
    PG[(StatefulSet postgres<br/>PostGIS)]
  end
  X -->|403| ING
  U -->|HTTPS aerozveno.ff| ING
  ING -->|/| FE
  ING -->|/api| GW
  FE -. init mc mirror .-> MINIO
  GW --> PL
  GW --> VL
  GW --> PG
  PL -. ops metrics .-> PROM[Prometheus n1]
  GW -. scrape :3000/metrics .-> PROM

Раскатка: geoscan на n2./scripts/deploy.sh geoscan n2 (scripts/deploy.sh:26-27); aerozveno на n1kubectl --context n1 apply -k k3s/aerozveno/ (скрипт deploy.sh не подходит: Deployment gateway называется geoscan, не aerozveno). Миграции gateway — initContainer node dist/db/migrate.js (k3s/aerozveno/40-deployment-gateway.yaml:25-34).


Связь с симулятором

Продукт simulate/ — отдельный репозиторийный контур (исполнение и проверка планов во времени). Архитектура симулятора: simulate/docs/91-doc/03-architecture/README.md. Geoscan отдаёт планы и KML; симулятор потребляет их по своим интерфейсам — без общего процесса в одном pod.


Диагностика (оператор)

Симптом Куда смотреть
403 снаружи tailnet Ingress whitelist k3s/aerozveno/60-ingress.yaml:9
503 на /readyz gateway gRPC до planner/validator — readiness.service.ts:40-47
Расчёт «завис», SSE молчит Ingress proxy-buffering: off на aerozveno (60-ingress.yaml:12); иначе прогресс приходит пачкой в конце
Пустой UI frontend init: «в бакете нет index.html» (45-deployment-frontend.yaml:112)
План есть, KML 409 PlanNotReadyError — статус не completed (plans-export.service.ts:40-41)
Метрики энергии не сходятся python -m validator check --scenario … --plan … и группа G3 в отчёте

См. также

Тема Раздел
Бизнес-цель и метрики приёмки 01-business
Сценарии и акторы 02-usecases
Экраны UI 03-userflow — на доработке
Галсы, GSD, ветер 05-domain
REST и форматы 06-api — на доработке
Установка и деплой 07-operate

Дополнительно в этом каталоге: принципы и контракты в деталях — расширенная таблица gRPC-методов и ссылки на барьерные тесты.