Архитектура 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-key — src/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-66 → PlannerGrpcClient.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 на n1 — kubectl --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-методов и ссылки на барьерные тесты.