Фронтенд на https://aerozveno.ff — раскатка и связка с бэкендом¶
Дата: 2026-09-16. Ветка feat/frontend-deploy. Кластер — n1 (kubectl --context n1), ns aerozveno.
Задача была из четырёх частей: собрать четыре модуля под один домен, выложить бандл в S3, поднять nginx со статикой и — главное — перевести интерфейс на реальный API.
1. Что развёрнуто и по каким адресам¶
| Что | Адрес | Объект в кластере |
|---|---|---|
| Интерфейс (шелл + три ремоута) | https://aerozveno.ff/ |
deploy/geoscan-frontend (nginx) |
| REST/SSE бэкенда | https://aerozveno.ff/api/* |
deploy/geoscan (gateway, NestJS) |
| Ops-ручки | /healthz, /readyz, /status, /metrics, /api/docs |
тот же gateway |
| Объектное хранилище бандла | minio.aerozveno.svc.cluster.local:9000 (ClusterIP) |
sts/geoscan-minio |
| Метрики статики | geoscan-frontend.aerozveno.svc:9113 |
сайдкар nginx-exporter |
Всё tailnet-only, TLS от внутреннего CA (ff-ca/ff-ca.crt), ingress с
whitelist-source-range: 100.64.0.0/10,10.42.0.0/16.
Образ гейтвея не пересобирался — в этом и был смысл вынесения статики наружу.
Заглушка src/backend/gateway/public/index.html осталась в образе, но больше не видна:
/ уходит в nginx.
Манифесты — k3s/aerozveno/, разворачиваются одним kubectl --context n1 apply -k k3s/aerozveno/ --server-side.
Каталог в репозитории заведён в этой задаче: раньше он существовал только на n1
(/root/deploy-apps/aerozveno/) и разошёлся с k3s/geoscan/ (тот остался нацелен на ns geoscan
и домен geoscan.ff). Живое состояние втянуто в репозиторий, к нему добавлены 35-minio.yaml,
45-deployment-frontend.yaml и переписанный 60-ingress.yaml.
Маршрутизация¶
k3s/aerozveno/60-ingress.yaml: префиксы /api, /healthz, /readyz, /status, /metrics →
сервис geoscan (gateway); / → сервис geoscan-frontend (nginx). ingress-nginx сопоставляет
более длинный префикс первым, поэтому порядок в списке роли не играет.
Заодно на ingress добавлены proxy-buffering: "off" и proxy-read-timeout: 600 — без этого
SSE /api/plans/{id}/events доезжает до браузера одним куском в конце, и полоса прогресса расчёта
в топбаре не двигается.
2. Раскладка бандла и настройка ремоутов¶
/ index.html + assets шелла + remoteEntry.js + config.json
/remotes/planner/ remoteEntry.js + assets
/remotes/validator/ remoteEntry.js + assets
/remotes/viewer3d/ remoteEntry.js + assets
base в vite-конфигах. Каждому ремоуту задан свой префикс, шеллу — корень:
| Модуль | base |
|---|---|
apps/shell |
/ |
apps/planner |
/remotes/planner/ |
apps/validator |
/remotes/validator/ |
apps/viewer3d |
/remotes/viewer3d/ |
Конфиги переведены в функциональную форму defineConfig(({ command }) => ({ … })), и base
подставляется только при command === 'build'. В dev-сервере остаётся /, иначе ремоут на
localhost:5174 перестал бы открываться по корню и npm run dev сломался бы.
Адреса ремоутов. В apps/shell/vite.config.ts они уже были вынесены в
process.env.REMOTE_PLANNER / REMOTE_VALIDATOR / REMOTE_VIEWER3D, и значения по умолчанию
на момент задачи были уже относительными (/remotes/<name>/remoteEntry.js), а не
http://localhost:517x/..., как сказано в постановке. Проверено: @module-federation/vite
относительный путь принимает — в собранном шелле лежит ровно
/remotes/planner/remoteEntry.js, абсолютный адрес на https://aerozveno.ff не понадобился.
Сам remoteEntry.js ремоута подтягивает свои чанки относительным ./assets/… через
new URL(path, import.meta.url), а index.html и CSS ремоута — абсолютным
/remotes/<name>/assets/… из base. Обе формы разрешаются правильно.
Сборка одной командой:
cd src/frontend
npm run build:site # = npm run build && node tools/assemble-site.mjs
tools/assemble-site.mjs раскладывает apps/*/dist в src/frontend/dist-site/ по схеме выше
и выбрасывает index.html ремоутов (они нужны только в standalone-режиме и в раскладке
только путают). dist-site/ добавлен в .gitignore.
3. S3: что получилось и почему не Yandex Object Storage¶
Yandex Object Storage оказался недоступен — это ограничение, а не выбор¶
Ключи yc/YC_CR_SA_STATIC_KEY_ID + yc/YC_CR_SA_STATIC_SECRET принадлежат сервис-аккаунту
реестра образов (ajeane4reqpr65vjbkfn, папка b1g31q6gtmkd96mf7eb5). Прав на Object Storage
у них нет. Дословно:
LIST FAIL: {'Code': 'AccessDenied', 'Message': 'Access Denied', 'Resource': '/'}
create aerozveno-frontend: FAIL {'Code': 'AccessDenied', 'Message': 'Access Denied', 'Resource': '/aerozveno-frontend'}
Постановка предлагала в этом случае «завести отдельный SA и ключи». Это тоже упирается в права:
IAM-токен по authorized_key.json того же SA выдаётся, но дальше всё закрыто —
clouds: {} # ListClouds пусто: нет resource-manager
buckets(storage api): HTTP 403 "Access Denied"
create bucket: HTTP 403 "Access Denied"
list SAs: {} # нет iam.serviceAccounts.list
Учётки с правами заводить сервис-аккаунты или назначать роли в папке в creds-store нет
(namespace'ы: gitea, monitoring, project/agent-board, project/geoscan, s3, yc, yc/cr-sa).
Что сделано вместо¶
Поднят собственный приватный S3 — MinIO в том же ns aerozveno (k3s/aerozveno/35-minio.yaml).
Архитектура из постановки сохранена один в один: бандл живёт как артефакт в объектном хранилище,
nginx получает его оттуда. Менялся только провайдер S3.
| Параметр | Значение |
|---|---|
| Бакет | aerozveno-frontend |
| Эндпоинт | http://minio.aerozveno.svc.cluster.local:9000 |
| Доступ | ClusterIP, наружу не публикуется; MINIO_BROWSER=off |
| Политика | приватный, анонимная политика не выставлена |
| Диск | PVC 2 GiB, local-path |
Записано в creds-store (namespace project/geoscan): S3_FRONTEND_BUCKET,
S3_FRONTEND_ENDPOINT, S3_FRONTEND_ACCESS_KEY, S3_FRONTEND_SECRET_KEY.
В кластере — secret geoscan-s3.
Приватность проверена изнутри кластера:
anon GET /aerozveno-frontend/index.html -> HTTP 403
Выгрузка¶
aws/mc/yc на станции нет, а ClusterIP n1 со станции не маршрутизируется (в отличие от n2,
который анонсирует subnet-route). Поэтому scripts/publish-frontend.py на boto3 сам поднимает
kubectl port-forward svc/minio на время выгрузки и гасит его в конце:
uv venv /tmp/s3venv && uv pip install --python /tmp/s3venv/bin/python boto3
/tmp/s3venv/bin/python scripts/publish-frontend.py # выгрузить dist-site
/tmp/s3venv/bin/python scripts/publish-frontend.py --list # что лежит в бакете
Ключи скрипт берёт из creds-store по HTTPS с внутренним CA; переменные S3_ACCESS_KEY /
S3_SECRET_KEY — запасной путь. Объекты, которых больше нет в dist-site, из бакета удаляются.
Как nginx получает файлы¶
k3s/aerozveno/45-deployment-frontend.yaml, три контейнера + init:
- init
fetch-bundle(quay.io/minio/mc) —mc mirror --overwrite --removeбакета вemptyDir, затем проверкаtest -f /srv/www/index.html. Пока бандла нет, под не стартует, то есть пустой корень наружу не попадает. nginx(nginx:1.27-alpine) — отдаёт/srv/wwwтолько на чтение.resync-bundle— раз в минуту повторяетmc mirror --overwrite(сверх постановки): новая выгрузка в бакет подхватывается без пересоздания пода. Без--remove: имена чанков содержат хеш содержимого, поэтому дописывание новых файлов поверх старых безопасно, а удаление на лету может увести файл из-под уже открытой вкладки. Гарантированно чистая раскатка —kubectl -n aerozveno rollout restart deploy/geoscan-frontend.nginx-exporter—stub_status→ Prometheus.
SPA-fallback (шелл на BrowserRouter): location / { try_files $uri $uri/ /index.html; }.
Перехватить API он не может: location /api/ { return 404; } стоит отдельным правилом выше —
если запрос к API почему-то дойдёт до nginx, он получит честный 404, а не HTML вместо JSON.
assets/ кешируются на год как immutable, remoteEntry.js и config.json — no-store.
4. Связка фронтенда с бэкендом¶
Что было на самом деле¶
Постановка говорит «в src/frontend нет ни одного обращения к API». На момент начала работы это
уже не так: в дереве есть пакеты packages/api-client (клиент, эндпоинты fleet/scenarios/
plans/validate) и packages/state (маппинг API↔домен, опрос плана, SSE, бутстрап каталога).
Экраны на них уже опирались.
Чего не было — ни одной проверки против живого бэкенда. Первый же браузерный прогон показал, что связка местами не работает. Ниже — что именно было найдено и исправлено.
Таблица расхождений форматов фронт ↔ API¶
| # | Где | В чём расхождение | Что сделал |
|---|---|---|---|
| 1 | POST /api/scenarios, ВПП |
Фронт слал turnaround_time_s (секунды). scenario.schema.json требует turnaround_time_min и имеет additionalProperties: false → 422 SCHEMA_VALIDATION_FAILED |
mappers.ts: шлём turnaround_time_min в минутах. Обратное чтение принимает оба имени |
| 2 | POST /api/scenarios, критерий |
Фронт слал objective.energy_reserve_frac; схема требует energy_reserve → 422 |
Шлём energy_reserve; при чтении принимаем оба |
| 3 | POST /api/scenarios, ЛЗП |
Фронт генерировал по синтетической площадке <vpp>-lzp на каждую ВПП. У сценариев сервера landing_sites: [] — выдуманная площадка меняет условие задачи |
Отдаём только площадки с kind === 'reserve'; для сценариев с сервера — пустой список |
| 4 | POST /api/validate |
Фронт слал planToApi(plan) — обратный маппинг доменного плана, в котором missions оставались camelCase → 400, отчёт молча падал в браузерный |
Храним исходные документы сервера (scenarioDoc, planDoc) и валидируем ровно их. Ответ 200 |
| 5 | Ответ POST /api/validate, структура |
Сервер отдаёт плоский checks[] с полем group; домен ждёт groups[].checks[]. Из-за этого отчёт показывал «Проверок выполнено 0 / 0» |
Группируем по group, названия групп по-русски в CHECK_GROUP_TITLES. Теперь 24 / 24 |
| 6 | Ответ /api/validate, поля проверки |
Сервер: value/limit/units числами + message. Домен: строковые value, margin, detail |
Форматируем: «153.19 m», «предел 150 m»; message → detail |
| 7 | Ответ /api/validate, индекс безопасности |
Полей safety_index, crc32, core, generated_at в ответе нет |
safetyIndex считаем как долю пройденных проверок (87.5 при 21/24) и помечаем: core = «валидатор 0.1.0» из validator_version, crc32 = «—». Числа из solve_time_ms берём из validated_in_s |
| 8 | plan.missions[], сводка на борт |
Сервер даёт только total_flight_time_s. Полей total_distance_m, transect_count, photos на борт нет — панель «Борта в плане» показывала 00:00:00, 0,0 км, 0 / 1, 0 при непустом плане |
Досчитываем по фазам: длительность из total_flight_time_s, дистанция — сумма длин геометрий фаз по дуге большого круга, галсы — число различных transect_id в survey-фазах, снимки — сумма photos. Сошлось: 16,1 км при metrics.total_distance_m = 16096 |
| 9 | plan.missions[].sorties[].start_s |
Поле в ответе есть, маппер жёстко ставил startS: 0 → диаграмма Ганта для нескольких вылетов ложилась в одну точку |
Читаем start_s |
| 10 | plan.geometry[].transects |
Сервер отдаёт [{id, task_id, line: {LineString}, length_m}], маппер выбрасывал их (transects: []) → «t1 · 1.0 км² · 0 галсов» |
Маппим; line.coordinates → [LngLat, LngLat], строковый id ("1") → числовой. Стало «15 галсов» |
| 11 | metrics.violations |
Сервер считает 7 видов, домен знал 5 (altitude и turnaround терялись, в том числе из суммы «Нарушений») |
Расширен тип PlanMetrics['violations'] до 7 ключей; офлайн-счётчик отдаёт их нулями (он эти два вида не проверяет) |
| 12 | Сценарий: перекрытия | В документах сервера у задания нет overlap_forward_frac / overlap_side_frac |
Оставлены значения по умолчанию 70 % / 60 % при чтении. Расхождение не устранено — см. «Что осталось на моках» |
| 13 | Сценарий: борт | В документах сервера у борта нет enabled и battery_left_frac, есть available_from, которого нет в домене |
Читаем enabled как «включён по умолчанию», заряд 100 %. available_from фронт игнорирует. Расхождение не устранено |
| 14 | Сценарий: ВП | В документах сервера у airspace нет floor_m / ceiling_m |
Умолчания 0 / 500 м. Расхождение не устранено |
| 15 | Список сценариев | GET /api/scenarios отдаёт только scenario_id, name, created_at, source — без числа заданий, площади и критерия |
Таблица на старте показывает — и 0,0 км² в этих колонках с подписью «с сервера» |
| 16 | Гейтвей не валидирует objective |
POST /api/plans с objective: "min_makespan" принимается с 202, планировщик отбивается позже (известный дефект постановки) |
Фронт шлёт только makespan / total_flight_time / pareto — из типа Criterion. Дефект гейтвея не чинил, задача этого не требовала |
Найденный и исправленный дефект: правка сценария затирала демо¶
Не про формат, но нашлось по дороге и важнее половины таблицы.
POST /api/scenarios на гейтвее — upsert по scenario_id (ON CONFLICT (id) DO UPDATE),
а GET /api/scenarios/{id} отдаёт пользовательскую запись раньше встроенной. Фронт при
сохранении правленого сценария слал исходный scenario_id — то есть отредактированный
s01-simple затирал эталонный демо-сценарий для всех, навсегда.
Я наступил на это сам при отладке (перезаписал s01-simple) и восстановил документ из
встроенного файла образа гейтвея; проверено, что expected, start_time и spare_batteries
на месте. Исправление в коде: правленый сценарий всегда уезжает под собственным идентификатором
<base>-draft-<8 hex> (draftScenarioId в api-compute.ts).
Смежная проблема: ensureScenarioOnServer переиспользовал serverScenarioId, не глядя на то,
правился ли сценарий после загрузки. Правки парка, GSD, ветра и геометрии молча терялись —
сервер считал по старому документу. Добавлена подписка в create-mission-store.ts: если
scenario изменился, а scenarioDoc в том же обновлении — нет (то есть это локальная правка,
а не загрузка с API), привязка к серверной копии сбрасывается и сценарий пересохраняется.
Проверено в браузере: после изменения GSD на 5 см прогон даёт 201 POST /api/scenarios → новый
расчёт. Черновики, созданные прогонами, из БД удалены — в /api/scenarios снова ровно 10 сценариев.
Что теперь берётся с сервера¶
| Данные | Ручка |
|---|---|
| Каталог БВС и нагрузок | GET /api/fleet, GET /api/payloads (вместо packages/domain/src/fleet.ts) |
| Список сценариев | GET /api/scenarios |
| Документ сценария | GET /api/scenarios/{id} |
| Сохранение правленого сценария | POST /api/scenarios |
| Расчёт | POST /api/plans + SSE /events, с откатом на опрос GET /api/plans/{id} |
| Пересчёт при отказе борта | POST /api/plans/{id}/replan |
| Валидация | POST /api/validate с телом {scenario, plan} |
| Выгрузка KML / GeoJSON / QGC .plan / CSV | GET /api/plans/{id}/export/{fmt} |
Что осталось на моках и почему¶
| Что | Почему | Как помечено |
|---|---|---|
| Подсказки при рисовании полигона, предварительные высота и шаг галсов в панели задания | У API эквивалента нет — это интерактив, счёт на каждое движение мыши. Постановка прямо просит оставить | Не помечается: это расчёт черновика, а не результат |
3D-сцена (viewer3d) |
Синтетический DEM; рельефа и тайлов у бэкенда нет | — |
| Мультикритериальный анализ, экран «Парето» | Ручки для фронта Парето нет; pareto после расчёта по API остаётся пустым |
Экран пустой, ложных чисел не показывает |
Отчёт валидатора, если POST /api/validate недоступен |
Откат на браузерный счётчик @geoscan/domain |
Помечено в интерфейсе: тег рядом с заголовком — «Источник: POST /api/validate» (синий) либо «Источник: браузерный расчёт (сервер недоступен)» (оранжевый) |
| Переключатель «Офлайн» в топбаре | Осознанный демо-режим целиком на @geoscan/domain |
Тем же тегом: «Источник: браузерный расчёт (офлайн-режим)» |
| Перекрытия, заряд АКБ, floor/ceiling ВП (строки 12–14 таблицы) | Полей нет в документах сервера; подставлять «разумные» числа на стороне фронта — значит расходиться с сервером молча | Не помечено в интерфейсе — честный долг, см. «Что не доделано» |
| Подложка карты | OFFLINE_STYLE — один background-слой, внешних тайлов нет по проекту (контур tailnet-only). Геометрия рисуется через deck.gl |
Не дефект раскатки |
5. Мониторинг¶
Цель добавлена в существующий job aerozveno (правится в fairflow-crm/k3s/monitoring/prometheus.yml,
зеркало — k3s/monitoring/prometheus-aerozveno-scrape.yaml):
geoscan-frontend.aerozveno.svc.cluster.local:9113 component: frontend
Состояние целей после раскатки:
gateway geoscan.aerozveno.svc.cluster.local:80 up
planner planner.aerozveno.svc.cluster.local:3001 up
frontend geoscan-frontend.aerozveno.svc.cluster.local:9113 up
Алерт AerozvenoFrontendDown (up{component="frontend"} == 0 3 минуты) — в
k3s/monitoring/aerozveno.rules.yml.
Дашборд https://grafana.ff/d/aerozveno, четыре новые панели:
- «Статика фронтенда жива» —
upпо цели; - «Статика: запросов в минуту» —
nginx_http_requests_total; - «Статика: соединения nginx» — active/reading/writing/waiting;
- «Браузер → API: запросы шелла к гейтвею» —
geoscan_http_requests_totalпо маршрутам/api/(scenarios|plans|validate): панель отвечает ровно на вопрос «связка живая или только поды Running».
6. Проверки — дословный вывод¶
1. / отдаёт index шелла, а не заглушку гейтвея¶
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Геоскан · Планировщик беспилотных авиационных работ</title>
<script type="module" crossorigin src="/assets/mf-entry-bootstrap-0-00e057ea.js"></script>
<link rel="stylesheet" crossorigin href="/assets/style-CCGB7cq9.css">
(заглушка гейтвея была <p>geoscan gateway</p>, 162 байта)
2. remoteEntry.js трёх ремоутов¶
/remotes/planner/remoteEntry.js -> HTTP 200, Content-Type: application/javascript, 980 байт
/remotes/validator/remoteEntry.js -> HTTP 200, Content-Type: application/javascript, 988 байт
/remotes/viewer3d/remoteEntry.js -> HTTP 200, Content-Type: application/javascript, 984 байт
3. Прямые ссылки (SPA-fallback, F5)¶
/plan -> HTTP 200, text/html
/scenario -> HTTP 200, text/html
/validator -> HTTP 200, text/html
/export -> HTTP 200, text/html
/pareto -> HTTP 200, text/html
/viewer3d -> HTTP 200, text/html
/airspace -> HTTP 200, text/html
4. Маршрутизация API не сломана¶
/api/scenarios -> HTTP 200, application/json; charset=utf-8
/api/fleet -> HTTP 200, application/json; charset=utf-8
/api/payloads -> HTTP 200, application/json; charset=utf-8
/api/docs -> HTTP 200, text/html
/healthz -> HTTP 200, application/json; charset=utf-8
/readyz -> HTTP 200, application/json; charset=utf-8
/status -> HTTP 200, application/json; charset=utf-8
/metrics -> HTTP 200, text/plain; version=0.0.4; charset=utf-8
# SPA-fallback не перехватывает несуществующий путь API:
/api/nope -> HTTP 404, application/json; charset=utf-8
5. Браузерный путь целиком, headless¶
Playwright + Chromium, против живого https://aerozveno.ff.
Скрипт прогона — src/frontend/tools/e2e-aerozveno.mjs (запуск — см. раздел 8), скриншоты — /tmp/e2e/final/.
--- ШАГ: 1. Открыть шелл ---
[api] 200 GET /api/fleet
[api] 200 GET /api/payloads
[api] 200 GET /api/scenarios
title: Геоскан · Планировщик беспилотных авиационных работ
строк в таблице сценариев: 10
источник списка: 10 сценариев · GET /api/scenarios
--- OK: 1. Открыть шелл ---
--- ШАГ: 2. Открыть сценарий «Один борт, прямоугольник 1×1 км» (s01-simple) ---
[api] 200 GET /api/scenarios/s01-simple
парк БВС на экране: борт gemini-01 виден (каталог с GET /api/fleet)
--- OK: 2. Открыть сценарий «Один борт, прямоугольник 1×1 км» (s01-simple) ---
--- ШАГ: 3. «Рассчитать» → дождаться плана ---
[api] 200 GET /api/scenarios/s01-simple
[api] 202 POST /api/plans
[api] 200 GET /api/plans/2aac8067-4176-4b2c-8912-40f51ef4ef10/events
[api] 200 GET /api/plans/2aac8067-4176-4b2c-8912-40f51ef4ef10
[api] 200 GET /api/plans/2aac8067-4176-4b2c-8912-40f51ef4ef10
[api] 200 POST /api/validate
статус: ПЛАН РАССЧИТАН за ~2 с
--- экран «План» ---
МЕТРИКИ ПЛАНА · Время работ · 00:28:20 · Суммарный налёт · 00:28:20 · Покрытие · 100,0 % ·
Нарушений · 0 · … · Галсов: · 15 · Разворотов: · 14 · Кадров: · 384 · Решатель: · feasible ·
0.71 с · План полётов · НАРУШЕНИЙ НЕТ · СВОДКА · Время работ (makespan) · 00:28:20 ·
Суммарный налёт · 00:28:20 · Суммарная дистанция · 16,1 км · Вылетов · 1 · Галсов · 15 ·
Разворотов · 14 · Доля времени на развороты · 5,9 % · Снимков · 384 ·
РАСЧЁТНАЯ ГЕОМЕТРИЯ ЗАДАНИЙ · t1 · 1.0 км² · 15 галсов · Высота · 153 м · Шаг галсов · 72 м ·
Захват кадра · 180 × 119 м · Угол галсов · 3° · Интервал съёмки · 3.6 с ·
БОРТА В ПЛАНЕ · 1 борта · Геоскан Gemini · gemini-01 · Задание · Налёт · 00:28:20 ·
Дистанция · 16,1 км · Галсов / вылетов · 15 / 1 · Снимков · 384 ·
ИНДЕКС БЕЗОПАСНОСТИ · 87.5 · Открыть отчёт валидатора
--- OK: 3. «Рассчитать» → дождаться плана ---
--- ШАГ: 4. Отчёт валидатора (переход внутри SPA) ---
метка источника отчёта: Источник: POST /api/validate
--- отчёт ---
Отчёт валидатора и аудит безопасности полётов · ЕСТЬ БЛОКИРУЮЩИЕ НАРУШЕНИЯ ·
Источник: POST /api/validate · CRC32: — · ядро валидатор 0.1.0 · 21 мс ·
ПРОВЕРОК ВЫПОЛНЕНО · 24 / 24 · КРИТИЧЕСКИЕ ОШИБКИ · 1 ошибок · ПРЕДУПРЕЖДЕНИЯ · 2 требуют внимания ·
ИНДЕКС БЕЗОПАСНОСТИ · 87.5 / 100 · Все проверки 24 · Замечания 3 · Успешно 21 ·
Воздушное пространство и бесполётные зоны · … · Порог 150 м (разрешение на ИВП) ВНИМАНИЕ ·
максимальная высота 153 м выше 150 м — потребуется разрешение на ИВП (ALT_ABOVE_150M) ·
153.19 m · предел 150 m · …
--- OK: 4. Отчёт валидатора (переход внутри SPA) ---
--- ШАГ: 5. Экспорт KML через GET /api/plans/{id}/export/kml ---
[api] 200 GET /api/plans/2aac8067-4176-4b2c-8912-40f51ef4ef10/export/kml
[api] 200 GET /api/plans/2aac8067-4176-4b2c-8912-40f51ef4ef10/export/geojson
файл: s01-simple.kml (10496 байт)
первые 500 символов:
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>gemini-01</name>
<Folder>
<name>Вылет 1</name>
<Placemark>
<name>takeoff 0</name>
<LineString>
<tessellate>1</tessellate>
<altitudeMode>relativeToGround</altitudeMode>
<coordinates>37.6200000,55.7600000,0.0 37.6200000,55.7600000,0.0</coordinates>
</LineString>
</Placemark>
--- OK: 5. Экспорт KML через GET /api/plans/{id}/export/kml ---
--- ШАГ: 6. Правка сценария должна уходить на сервер (POST /api/scenarios) ---
GSD изменён на 5 см
[api] 201 POST /api/scenarios
[api] 202 POST /api/plans
[api] 200 GET /api/plans/941fc941-b02e-430a-bf87-403ce8a7dcbf/events
[api] 200 GET /api/plans/941fc941-b02e-430a-bf87-403ce8a7dcbf
[api] 200 POST /api/validate
статус после правки: ПЛАН РАССЧИТАН за ~4 с
POST /api/scenarios был: да
--- OK: 6. Правка сценария должна уходить на сервер (POST /api/scenarios) ---
--- ШАГ: 7. Прямая ссылка /plan (F5) ---
[api] 200 GET /api/fleet
[api] 200 GET /api/payloads
url: https://aerozveno.ff/plan | шелл отрисован: true
содержимое: Сценарий не загружен
--- OK: 7. Прямая ссылка /plan (F5) ---
=== ОШИБКИ КОНСОЛИ ===
нет
Ни одного ответа 4xx/5xx, ни одной ошибки в консоли за весь прогон.
6. Метрики гейтвея растут от кликов в браузере¶
Срез geoscan_http_requests_total до и после того же прогона:
маршрут метод код до после дельта
----------------------------------------------------------------------------------------
/api/fleet GET 200 13 14 +1
/api/payloads GET 200 11 12 +1
/api/plans/{id} GET 200 26 29 +3
/api/plans/{id}/events GET 200 9 11 +2
/api/plans/{id}/export/geojson GET 200 4 5 +1
/api/plans/{id}/export/kml GET 200 4 5 +1
/api/scenarios GET 200 20 21 +1
/api/scenarios/s01-simple GET 200 19 21 +2
/api/plans POST 202 13 15 +2
/api/scenarios POST 201 3 4 +1
/api/validate POST 200 11 13 +2
Дельты сходятся с сетевым логом прогона один в один.
Дополнительно: в кластере лежит ровно то, что собрано из репозитория¶
index.html в кластере: 00e24f54aa76806846f9318453e72f6b
index.html в дереве: 00e24f54aa76806846f9318453e72f6b
СОВПАДАЕТ — развёрнуто ровно то, что собрано из репозитория
7. Что не доделано¶
- Yandex Object Storage не задействован. Прав у имеющихся ключей нет, завести SA нечем.
Бандл лежит в MinIO внутри кластера. Чтобы переехать в YC, нужен сервис-аккаунт с ролью
storage.editorв папкеb1g31q6gtmkd96mf7eb5и его статические ключи в creds-store; код выгрузки менять не придётся — только эндпоинт и ключи. - Расхождения 12–14 таблицы не устранены и не помечены в интерфейсе. Перекрытия (70/60 %),
заряд АКБ (100 %) и floor/ceiling разрешённого ВП (0/500 м) подставляются фронтом по умолчанию,
потому что в документах сценария с сервера этих полей нет. Пользователь видит числа и не знает,
что они не с сервера. Правильное решение — добавить поля в
scenario.schema.jsonи в демо-сценарии, а не подкрашивать интерфейс. Отдельная задача. - Состояние живёт только в памяти вкладки. F5 на
/planотдаёт 200 и шелл, но план теряется («Сценарий не загружен»).activePlanIdесть — достаточно класть его в URL или sessionStorage и восстанавливать черезGET /api/plans/{id}. Не делал: выходит за рамки задачи. - Экран «Парето» пуст при работе по API. Ручки нет,
pareto: []. Ложных чисел не показывает, но и пользы не приносит. - Гейтвей не валидирует
objective.min_makespanпринимается с 202 и падает в планировщике. Фронт такого значения не шлёт, но дефект гейтвея остался — чинить его задача не просила. validatorне отдаёт ops-http (известный дефект A-02) — цели в Prometheus для него по-прежнему нет.- Стухшие
.d.tsвpackages/domain/src. Рядом сstore.tsлежитstore.d.tsот прошлых сборок, и он уже разошёлся с исходником (в нём нетdataSource,catalogLoaded, добавленных мной полей). Сейчас TypeScript берёт.tsи всё собирается, но это мина. Удалять не стал — не мой объём. - Пересинхронизация из бакета не атомарна. Сайдкар подкладывает новые файлы в работающий
emptyDir. Для контент-хешированных чанков это безопасно, но теоретически между обновлениемindex.htmlи появлением всех чанков есть окно. Гарантию даётrollout restart. - Прогон покрывает один сценарий (s01-simple). Остальные девять через браузер не гонялись.
8. Как повторить раскатку¶
# сборка и раскладка под один домен
cd src/frontend && npm ci && npm run build:site
# выгрузка в приватный бакет
cd ../.. && /tmp/s3venv/bin/python scripts/publish-frontend.py
# кластер
kubectl --context n1 apply -k k3s/aerozveno/ --server-side
kubectl --context n1 -n aerozveno rollout restart deploy/geoscan-frontend
kubectl --context n1 -n aerozveno rollout status deploy/geoscan-frontend
# проверка
curl --cacert ff-ca/ff-ca.crt https://aerozveno.ff/
# сквозной браузерный прогон. Playwright в зависимостях монорепы нет, и запускать
# скрипт надо оттуда, где он установлен: резолвинг пакетов идёт от каталога файла.
mkdir -p /tmp/e2e && cd /tmp/e2e && npm i playwright
cp <репозиторий>/src/frontend/tools/e2e-aerozveno.mjs .
node e2e-aerozveno.mjs https://aerozveno.ff /tmp/e2e/out