chore: сохранение текущих изменений (deploy local-bundle/local-jar, request-service, slots UI markedObjectIds)

Безопасный коммит незакоммиченной работы перед реализацией бронирования слотов.
Вне коммита оставлены: package-lock.json (пустой, мусор), requests.json (scratch-данные).
This commit is contained in:
Дмитрий Соловьев
2026-06-11 14:00:12 +03:00
parent 6689351ef0
commit 345529cde0
36 changed files with 1674 additions and 37 deletions
+2
View File
@@ -53,6 +53,7 @@ out/
/pcp_zip.zip
/pcp-chatgpt-context*.zip
/deploy/local-bundle/build/
/deploy/local-bundle/.env
/deploy/local-bundle/pcp-local-bundle-*.tar.gz
### Node / Vite ###
@@ -69,3 +70,4 @@ out/
/services/pcp-tgu-ops-ui/yarn-debug.log*
/services/pcp-tgu-ops-ui/yarn-error.log*
/services/pcp-tgu-ops-ui/pnpm-debug.log*
.gstack/
Regular → Executable
-2
View File
@@ -1,4 +1,3 @@
cat > create-service-dbs.sh <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
@@ -124,4 +123,3 @@ done
echo
echo "All requested databases processed."
BASH
+24 -1
View File
@@ -1,5 +1,18 @@
COMPOSE_PROJECT_NAME=pcp-local
USE_EXTERNAL_INFRA=false
PCP_DOCKER_NETWORK=pcp-local_default
PCP_DOCKER_NETWORK_EXTERNAL=false
# Existing allinone infra example:
# USE_EXTERNAL_INFRA=true
# PCP_DOCKER_NETWORK=allinone_camunda
# PCP_DOCKER_NETWORK_EXTERNAL=true
# EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1
# EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1
# CAMUNDA_CLIENT_ENABLED=true
# CAMUNDA_GRPC_ADDRESS=http://orchestration:26500
PCP_IMAGE_TAG=local
POSTGRES_IMAGE=postgis/postgis:16-3.4
@@ -8,14 +21,24 @@ KAFKA_IMAGE=apache/kafka:3.7.2
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_PORT=5432
PCP_POSTGRES_HOST=postgres
PCP_POSTGRES_PORT=5432
EXTERNAL_POSTGRES_CONTAINER=
KAFKA_PORT=29092
PCP_KAFKA_HOST=kafka
PCP_KAFKA_PORT=9092
EXTERNAL_KAFKA_CONTAINER=
CAMUNDA_CLIENT_ENABLED=false
CAMUNDA_GRPC_ADDRESS=http://host.docker.internal:26500
CONFIG_SERVER_PORT=38888
PCP_ADMIN_PORT=7000
PCP_TLE_MONITORING_PORT=7001
PCP_COMPLEX_MISSION_PORT=7002
PCP_BALLISTICS_PORT=7003
PCP_ROUTE_PROCESSING_PORT=7004
PCP_REQUEST_PORT=7005
PCP_SLOTS_PORT=7006
PCP_UI_PORT=7008
+127 -2
View File
@@ -2,6 +2,9 @@
Локальный bundle предназначен для запуска PCP на машине с Docker без GitLab Config Repo, Kubernetes и Helm.
В bundle входят backend-сервисы PCP, кроме `tle-monitoring-service`; отдельный frontend
`pcp-tgu-ops-ui` также не включается.
## Состав
- `docker-compose.yml` - локальная схема запуска.
@@ -15,6 +18,72 @@
- `scripts/stop.sh` - остановка compose stack.
- `scripts/status.sh` - статус контейнеров.
## Локальный запуск без архива
Для запуска на той же машине, где собираются images, архив и `docker save` не нужны.
Используйте один скрипт:
```bash
./deploy/local-bundle/run-local.sh start
```
Скрипт:
- создаёт `.env` из `.env.example`, если его ещё нет;
- собирает `bootJar` в Gradle offline mode;
- собирает локальные Docker images `pcp/<service>:<PCP_IMAGE_TAG>`;
- запускает `docker compose`.
Для вашего allinone Postgres/Kafka/Camunda перед первым запуском настройте
`deploy/local-bundle/.env`:
```env
USE_EXTERNAL_INFRA=true
PCP_DOCKER_NETWORK=allinone_camunda
PCP_DOCKER_NETWORK_EXTERNAL=true
PCP_POSTGRES_HOST=postgres
PCP_POSTGRES_PORT=5432
PCP_KAFKA_HOST=kafka
PCP_KAFKA_PORT=9092
EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1
EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1
CAMUNDA_CLIENT_ENABLED=true
CAMUNDA_GRPC_ADDRESS=http://orchestration:26500
```
Команды:
```bash
./deploy/local-bundle/run-local.sh start
./deploy/local-bundle/run-local.sh status
./deploy/local-bundle/run-local.sh logs
./deploy/local-bundle/run-local.sh stop
```
## Пересборка одного сервиса
После первого запуска всего стека можно пересобирать и перезапускать только один сервис:
```bash
./deploy/local-bundle/services/pcp-request-service.sh
```
Это выполнит:
- `bootJar` только для `pcp-request-service`;
- `docker build` только для `pcp/pcp-request-service:<PCP_IMAGE_TAG>`;
- `docker compose up -d --no-deps --force-recreate pcp-request-service`.
Доступны wrapper-скрипты в `deploy/local-bundle/services/` для каждого сервиса.
То же самое можно вызвать через общий скрипт:
```bash
./deploy/local-bundle/run-service.sh pcp-request-service
./deploy/local-bundle/run-service.sh pcp-request-service build
./deploy/local-bundle/run-service.sh pcp-request-service logs
./deploy/local-bundle/run-service.sh pcp-request-service stop
```
## Сборка архива
Из корня репозитория:
@@ -38,6 +107,7 @@ deploy/local-bundle/build/pcp-local-bundle-<version>.tar.gz
Скрипт сборки:
- собирает `bootJar` для разворачиваемых сервисов;
- запускает Gradle в offline mode по умолчанию;
- собирает Docker images сервисов;
- сохраняет PCP service images в один `images/pcp-services.tar`;
- по умолчанию не сохраняет публичные infra images, чтобы не раздувать архив;
@@ -61,6 +131,41 @@ KOTLIN_DAEMON_JVMARGS="-Xmx3g -XX:MaxMetaspaceSize=768m" \
./deploy/local-bundle/build-local-bundle.sh
```
Gradle offline mode включён по умолчанию:
```bash
GRADLE_OFFLINE=true ./deploy/local-bundle/build-local-bundle.sh
```
Если нужно один раз прогреть локальный Gradle cache, временно отключите offline mode:
```bash
GRADLE_OFFLINE=false ./deploy/local-bundle/build-local-bundle.sh
```
Важно: `GRADLE_OFFLINE=true` отключает сетевой резолв зависимостей Gradle, но Docker всё равно
должен иметь локально доступный base image. Для local bundle сервисные Dockerfile собираются с
локальным override:
```text
LOCAL_BUNDLE_RUNTIME_IMAGE=bellsoft/liberica-openjre-alpine:21.0.5
```
Поэтому перед полностью offline Docker-сборкой подтяните или импортируйте:
```bash
docker pull bellsoft/liberica-openjre-alpine:21.0.5
```
Для полностью переносимого архива с infra images заранее подтяните Postgres/Kafka и включите их
сохранение:
```bash
docker pull postgis/postgis:16-3.4
docker pull apache/kafka:3.7.2
INCLUDE_INFRA_IMAGES=true ./deploy/local-bundle/build-local-bundle.sh
```
## Запуск на локальной машине
Распаковать архив:
@@ -72,6 +177,26 @@ cd pcp-local-bundle-<version>
Проверить `.env` и при необходимости изменить порты.
Если Postgres/Kafka/Camunda уже подняты внешним allinone compose, например в сети
`allinone_camunda`, включите external infra mode в `.env`:
```env
USE_EXTERNAL_INFRA=true
PCP_DOCKER_NETWORK=allinone_camunda
PCP_DOCKER_NETWORK_EXTERNAL=true
PCP_POSTGRES_HOST=postgres
PCP_POSTGRES_PORT=5432
PCP_KAFKA_HOST=kafka
PCP_KAFKA_PORT=9092
EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1
EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1
CAMUNDA_CLIENT_ENABLED=true
CAMUNDA_GRPC_ADDRESS=http://orchestration:26500
```
В этом режиме `scripts/start.sh` не поднимает свои `postgres` и `kafka`, а сервисы PCP
подключаются к уже существующим контейнерам через общую Docker network.
Импортировать images:
```bash
@@ -109,9 +234,9 @@ cd pcp-local-bundle-<version>
- Config Server: `http://localhost:38888`
- Spring Boot Admin: `http://localhost:7000`
- TLE Monitoring: `http://localhost:7001`
- Complex Mission: `http://localhost:7002`
- Ballistics: `http://localhost:7003`
- Route Processing: `http://localhost:7004`
- Request Service: `http://localhost:7005`
- Slots Service: `http://localhost:7006`
- UI: `http://localhost:7008`
@@ -178,13 +303,13 @@ SPRING_CLOUD_CONFIG_LABEL=
- `pcp_ballistics`
- `pcp_requests`
- `pcp_route_processing`
- `pcp_missions`
- `pcp_slots`
- `pcp_complex_plan`
- `pcp_satellite_catalog`
- `pcp_stations`
- `pcp_tgu`
- `pcp_tle`
- `pcp_satellites`
Таблицы, схемы и seed data вручную не создаются. Схемы создаются существующими Flyway migrations внутри сервисов при старте приложений.
+15 -3
View File
@@ -26,6 +26,7 @@ service_names=(
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
@@ -33,13 +34,14 @@ service_names=(
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
tle-monitoring-service
pcp-tgu-service
)
gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
gradle_offline="${GRADLE_OFFLINE:-true}"
runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}"
include_infra_images="${INCLUDE_INFRA_IMAGES:-false}"
gradle_common_args=(
@@ -49,6 +51,10 @@ gradle_common_args=(
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
if [[ "$gradle_offline" == "true" ]]; then
gradle_common_args+=("--offline")
fi
infra_images=(
"${POSTGRES_IMAGE:-postgis/postgis:16-3.4}"
"${KAFKA_IMAGE:-apache/kafka:3.7.2}"
@@ -117,9 +123,11 @@ echo "Building bootJar artifacts..."
echo "Gradle max workers: $gradle_max_workers"
echo "Gradle JVM args: $gradle_jvmargs"
echo "Kotlin daemon JVM args: $kotlin_daemon_jvmargs"
echo "Gradle offline mode: $gradle_offline"
echo "Local bundle runtime image: $runtime_image"
for service_name in "${service_names[@]}"; do
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
"$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
done
rm -rf "$dist_dir" "$docker_contexts_dir" "$archive_path"
@@ -132,7 +140,11 @@ for service_name in "${service_names[@]}"; do
context_dir="$docker_contexts_dir/$service_name"
copy_single_boot_jar "$service_name" "$context_dir"
docker build --file "$context_dir/Dockerfile" --tag "$image_name" "$context_dir"
docker build \
--build-arg "RUNTIME_IMAGE=$runtime_image" \
--file "$context_dir/Dockerfile" \
--tag "$image_name" \
"$context_dir"
service_images+=("$image_name")
done
+23 -13
View File
@@ -8,9 +8,17 @@ x-pcp-service-env: &pcp-service-env
SPRING_CLOUD_CONFIG_LABEL: ""
SPRING_KAFKA_ADMIN_AUTO_CREATE: "true"
SPRING_KAFKA_LISTENER_AUTO_STARTUP: "true"
PCP_POSTGRES_HOST: ${PCP_POSTGRES_HOST:-postgres}
PCP_POSTGRES_PORT: ${PCP_POSTGRES_PORT:-5432}
PCP_KAFKA_HOST: ${PCP_KAFKA_HOST:-kafka}
PCP_KAFKA_PORT: ${PCP_KAFKA_PORT:-9092}
CAMUNDA_CLIENT_ENABLED: ${CAMUNDA_CLIENT_ENABLED:-false}
CAMUNDA_GRPC_ADDRESS: ${CAMUNDA_GRPC_ADDRESS:-http://host.docker.internal:26500}
x-pcp-service-common: &pcp-service-common
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
spring-cloud-config-server:
condition: service_started
@@ -64,11 +72,8 @@ services:
spring-cloud-config-server:
image: pcp/spring-cloud-config-server:${PCP_IMAGE_TAG:-local}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
kafka:
condition: service_started
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
SPRING_PROFILES_ACTIVE: local
CONFIG_NATIVE_SEARCH_LOCATIONS: file:/config-repo
@@ -86,14 +91,6 @@ services:
ports:
- "${PCP_ADMIN_PORT:-7000}:7000"
tle-monitoring-service:
<<: *pcp-service-common
image: pcp/tle-monitoring-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_TLE_MONITORING_PORT:-7001}:7001"
pcp-complex-mission-service:
<<: *pcp-service-common
image: pcp/pcp-complex-mission-service:${PCP_IMAGE_TAG:-local}
@@ -118,6 +115,14 @@ services:
ports:
- "${PCP_REQUEST_PORT:-7005}:7005"
pcp-route-processing-service:
<<: *pcp-service-common
image: pcp/pcp-route-processing-service:${PCP_IMAGE_TAG:-local}
environment:
<<: *pcp-service-env
ports:
- "${PCP_ROUTE_PROCESSING_PORT:-7004}:7004"
slots-service:
<<: *pcp-service-common
image: pcp/slots-service:${PCP_IMAGE_TAG:-local}
@@ -193,3 +198,8 @@ services:
volumes:
postgres-data:
kafka-data:
networks:
default:
name: ${PCP_DOCKER_NETWORK:-pcp-local_default}
external: ${PCP_DOCKER_NETWORK_EXTERNAL:-false}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
env_file="$script_dir/.env"
env_example_file="$script_dir/.env.example"
docker_contexts_dir="$script_dir/build/local-docker-contexts"
service_names=(
spring-cloud-config-server
pcp-srpring-boot-admin-server
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
pcp-tgu-service
)
ensure_env_file() {
if [[ ! -f "$env_file" && -f "$env_example_file" ]]; then
cp "$env_example_file" "$env_file"
echo "Created $env_file from .env.example"
fi
}
ensure_source_config_repo_dir() {
if grep -q '^CONFIG_REPO_DIR=' "$env_file"; then
sed -i "s#^CONFIG_REPO_DIR=.*#CONFIG_REPO_DIR=$repo_root/config-repo#" "$env_file"
else
printf '\nCONFIG_REPO_DIR=%s/config-repo\n' "$repo_root" >> "$env_file"
fi
}
load_env() {
set -a
# shellcheck disable=SC1090
source "$env_file"
set +a
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name is required." >&2
exit 1
fi
}
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
copy_single_boot_jar() {
local service_name="$1"
local context_dir="$2"
local jar_count
local jar_file
jar_count="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l
)"
if [[ "$jar_count" != "1" ]]; then
echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' -print >&2 || true
exit 1
fi
jar_file="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1
)"
mkdir -p "$context_dir/build/libs"
cp "$repo_root/services/$service_name/Dockerfile" "$context_dir/Dockerfile"
cp "$jar_file" "$context_dir/build/libs/app.jar"
}
build_images() {
local gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
local gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
local kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
local gradle_offline="${GRADLE_OFFLINE:-true}"
local runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}"
local image_tag="${PCP_IMAGE_TAG:-local}"
local service_name
local image_name
local context_dir
local gradle_common_args=(
"--no-parallel"
"--max-workers=$gradle_max_workers"
"-Dorg.gradle.jvmargs=$gradle_jvmargs"
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
if [[ "$gradle_offline" == "true" ]]; then
gradle_common_args+=("--offline")
fi
echo "Building local Docker images..."
echo "Gradle offline mode: $gradle_offline"
echo "Local image tag: $image_tag"
echo "Runtime image: $runtime_image"
for service_name in "${service_names[@]}"; do
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
done
rm -rf "$docker_contexts_dir"
mkdir -p "$docker_contexts_dir"
for service_name in "${service_names[@]}"; do
image_name="pcp/$service_name:$image_tag"
context_dir="$docker_contexts_dir/$service_name"
copy_single_boot_jar "$service_name" "$context_dir"
echo "Building Docker image $image_name..."
docker build \
--build-arg "RUNTIME_IMAGE=$runtime_image" \
--file "$context_dir/Dockerfile" \
--tag "$image_name" \
"$context_dir"
done
}
usage() {
cat <<'EOF'
Usage:
./deploy/local-bundle/run-local.sh [command]
Commands:
start Build service images locally and start docker compose.
restart Stop compose stack, then build and start.
build Build service images locally, without starting compose.
stop Stop compose stack.
status Show compose status.
logs Follow compose logs.
EOF
}
start_local() {
build_images
"$script_dir/scripts/start.sh"
}
command="${1:-start}"
case "$command" in
help|-h|--help)
usage
exit 0
;;
esac
ensure_env_file
ensure_source_config_repo_dir
load_env
require_command docker
case "$command" in
start)
start_local
;;
restart)
"$script_dir/scripts/stop.sh"
start_local
;;
build)
build_images
;;
stop)
"$script_dir/scripts/stop.sh"
;;
status)
"$script_dir/scripts/status.sh"
;;
logs)
compose logs -f
;;
*)
usage >&2
exit 1
;;
esac
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
env_file="$script_dir/.env"
env_example_file="$script_dir/.env.example"
docker_contexts_dir="$script_dir/build/local-docker-contexts"
service_names=(
spring-cloud-config-server
pcp-srpring-boot-admin-server
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
pcp-tgu-service
)
usage() {
cat <<'EOF'
Usage:
./deploy/local-bundle/run-service.sh SERVICE [command]
Commands:
restart Build SERVICE bootJar, rebuild its Docker image and recreate only this compose service.
build Build SERVICE bootJar and Docker image, without restarting container.
stop Stop SERVICE container.
logs Follow SERVICE logs.
Examples:
./deploy/local-bundle/run-service.sh pcp-request-service
./deploy/local-bundle/run-service.sh pcp-request-service build
EOF
}
ensure_env_file() {
if [[ ! -f "$env_file" && -f "$env_example_file" ]]; then
cp "$env_example_file" "$env_file"
echo "Created $env_file from .env.example"
fi
}
ensure_source_config_repo_dir() {
if grep -q '^CONFIG_REPO_DIR=' "$env_file"; then
sed -i "s#^CONFIG_REPO_DIR=.*#CONFIG_REPO_DIR=$repo_root/config-repo#" "$env_file"
else
printf '\nCONFIG_REPO_DIR=%s/config-repo\n' "$repo_root" >> "$env_file"
fi
}
load_env() {
set -a
# shellcheck disable=SC1090
source "$env_file"
set +a
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name is required." >&2
exit 1
fi
}
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
is_known_service() {
local service_name="$1"
local known_service_name
for known_service_name in "${service_names[@]}"; do
if [[ "$known_service_name" == "$service_name" ]]; then
return 0
fi
done
return 1
}
copy_single_boot_jar() {
local service_name="$1"
local context_dir="$2"
local jar_count
local jar_file
jar_count="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l
)"
if [[ "$jar_count" != "1" ]]; then
echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' -print >&2 || true
exit 1
fi
jar_file="$(
find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1
)"
mkdir -p "$context_dir/build/libs"
cp "$repo_root/services/$service_name/Dockerfile" "$context_dir/Dockerfile"
cp "$jar_file" "$context_dir/build/libs/app.jar"
}
build_service_image() {
local service_name="$1"
local gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
local gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
local kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
local gradle_offline="${GRADLE_OFFLINE:-true}"
local runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}"
local image_tag="${PCP_IMAGE_TAG:-local}"
local image_name="pcp/$service_name:$image_tag"
local context_dir="$docker_contexts_dir/$service_name"
local gradle_common_args=(
"--no-parallel"
"--max-workers=$gradle_max_workers"
"-Dorg.gradle.jvmargs=$gradle_jvmargs"
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
if [[ "$gradle_offline" == "true" ]]; then
gradle_common_args+=("--offline")
fi
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
rm -rf "$context_dir"
copy_single_boot_jar "$service_name" "$context_dir"
echo "Building Docker image $image_name..."
docker build \
--build-arg "RUNTIME_IMAGE=$runtime_image" \
--file "$context_dir/Dockerfile" \
--tag "$image_name" \
"$context_dir"
}
http_get() {
local url="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsS "$url" >/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -q -O /dev/null "$url"
else
return 2
fi
}
wait_for_config_server() {
local attempt
local url="http://localhost:${CONFIG_SERVER_PORT:-38888}/actuator/health"
for attempt in {1..90}; do
if http_get "$url"; then
echo "spring-cloud-config-server is ready: $url"
return 0
fi
sleep 2
done
echo "spring-cloud-config-server is not ready after 180 seconds: $url" >&2
exit 1
}
restart_service() {
local service_name="$1"
build_service_image "$service_name"
if [[ "$service_name" == "spring-cloud-config-server" ]]; then
compose up -d --no-deps --force-recreate "$service_name"
wait_for_config_server
return 0
fi
compose up -d spring-cloud-config-server
wait_for_config_server
compose up -d --no-deps --force-recreate "$service_name"
}
service_name="${1:-}"
command="${2:-restart}"
if [[ -z "$service_name" || "$service_name" == "help" || "$service_name" == "-h" || "$service_name" == "--help" ]]; then
usage
exit 0
fi
if ! is_known_service "$service_name"; then
echo "Unknown service: $service_name" >&2
echo "Known services:" >&2
printf ' %s\n' "${service_names[@]}" >&2
exit 1
fi
ensure_env_file
ensure_source_config_repo_dir
load_env
require_command docker
case "$command" in
restart)
restart_service "$service_name"
;;
build)
build_service_image "$service_name"
;;
stop)
compose stop "$service_name"
;;
logs)
compose logs -f "$service_name"
;;
help|-h|--help)
usage
;;
*)
usage >&2
exit 1
;;
esac
+11 -2
View File
@@ -25,6 +25,10 @@ ensure_env_file() {
}
ensure_env_file
set -a
# shellcheck disable=SC1091
source "$bundle_dir/.env"
set +a
if [[ ! -d "$images_dir" ]]; then
echo "Images directory not found: $images_dir" >&2
@@ -49,7 +53,12 @@ if [[ -f "$images_dir/public-images.txt" ]]; then
fi
cd "$bundle_dir"
echo "Starting postgres, kafka and spring-cloud-config-server..."
compose up -d postgres kafka spring-cloud-config-server
if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then
echo "Using external infrastructure. Starting spring-cloud-config-server only."
compose up -d spring-cloud-config-server
else
echo "Starting postgres, kafka and spring-cloud-config-server..."
compose up -d postgres kafka spring-cloud-config-server
fi
echo "Images imported and base services requested."
+52 -5
View File
@@ -43,6 +43,26 @@ wait_for_postgres() {
exit 1
}
wait_for_external_postgres() {
local container_name="${EXTERNAL_POSTGRES_CONTAINER:-}"
local attempt
if [[ -z "$container_name" ]]; then
echo "EXTERNAL_POSTGRES_CONTAINER is required when USE_EXTERNAL_INFRA=true and SKIP_DB_INIT is not true." >&2
exit 1
fi
for attempt in {1..60}; do
if docker exec "$container_name" pg_isready -U "${POSTGRES_USER:-postgres}" -d postgres >/dev/null 2>&1; then
return 0
fi
sleep 2
done
echo "External Postgres is not ready after 120 seconds: $container_name" >&2
exit 1
}
create_database_if_missing() {
local db_name="$1"
local exists
@@ -61,16 +81,35 @@ create_database_if_missing() {
compose exec -T postgres createdb -U "${POSTGRES_USER:-postgres}" "$db_name"
}
create_external_database_if_missing() {
local db_name="$1"
local container_name="${EXTERNAL_POSTGRES_CONTAINER:-}"
local exists
exists="$(
docker exec "$container_name" psql -U "${POSTGRES_USER:-postgres}" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = '$db_name'" | tr -d '[:space:]'
)"
if [[ "$exists" == "1" ]]; then
echo "Database already exists: $db_name"
return 0
fi
echo "Creating database: $db_name"
docker exec "$container_name" createdb -U "${POSTGRES_USER:-postgres}" "$db_name"
}
databases=(
pcp_ballistics
pcp_requests
pcp_route_processing
pcp_missions
pcp_slots
pcp_complex_plan
pcp_satellite_catalog
pcp_stations
pcp_tgu
pcp_tle
pcp_satellites
)
@@ -78,12 +117,20 @@ ensure_env_file
load_env
cd "$bundle_dir"
compose up -d postgres
wait_for_postgres
if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then
wait_for_external_postgres
for db_name in "${databases[@]}"; do
for db_name in "${databases[@]}"; do
create_external_database_if_missing "$db_name"
done
else
compose up -d postgres
wait_for_postgres
for db_name in "${databases[@]}"; do
create_database_if_missing "$db_name"
done
done
fi
echo "Database initialization completed. Tables are left to service Flyway migrations."
echo "No schema objects or seed data were created by this script."
+37 -5
View File
@@ -72,11 +72,34 @@ wait_for_kafka() {
exit 1
}
wait_for_external_kafka() {
local container_name="${EXTERNAL_KAFKA_CONTAINER:-}"
local attempt
if [[ -z "$container_name" ]]; then
echo "External Kafka container is not configured; skipping Kafka readiness check."
return 0
fi
for attempt in {1..60}; do
if docker exec "$container_name" kafka-topics --bootstrap-server "${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}" --list >/dev/null 2>&1 ||
docker exec "$container_name" kafka-topics.sh --bootstrap-server "${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}" --list >/dev/null 2>&1; then
echo "External Kafka is ready in container $container_name."
return 0
fi
sleep 2
done
echo "External Kafka is not ready after 120 seconds: $container_name" >&2
exit 1
}
app_services=(
pcp-srpring-boot-admin-server
pcp-satellite-catalog-service
pcp-ballistics-service
pcp-request-service
pcp-route-processing-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
@@ -84,7 +107,6 @@ app_services=(
pcp-dynamic-plan-service
pcp-stations-service
pcp-ui-service
tle-monitoring-service
pcp-tgu-service
)
@@ -93,9 +115,16 @@ load_env
cd "$bundle_dir"
echo "Starting infrastructure..."
compose up -d postgres kafka
wait_for_kafka
if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then
echo "Using external infrastructure. Skipping local postgres/kafka startup."
echo "Postgres endpoint for services: ${PCP_POSTGRES_HOST:-postgres}:${PCP_POSTGRES_PORT:-5432}"
echo "Kafka endpoint for services: ${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}"
wait_for_external_kafka
else
echo "Starting infrastructure..."
compose up -d postgres kafka
wait_for_kafka
fi
if [[ "${SKIP_DB_INIT:-false}" != "true" ]]; then
"$script_dir/init-db.sh"
@@ -106,7 +135,10 @@ compose up -d spring-cloud-config-server
wait_for_http "spring-cloud-config-server" "http://localhost:${CONFIG_SERVER_PORT:-38888}/actuator/health"
echo "Starting application services..."
compose up -d "${app_services[@]}"
for service_name in "${app_services[@]}"; do
echo "Starting $service_name..."
compose up -d "$service_name"
done
echo "Application startup requested."
compose ps
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-ballistics-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-complex-mission-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-coverage-scheme-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-dynamic-plan-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-mission-planing-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-request-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-route-processing-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-satellite-catalog-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-srpring-boot-admin-server "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-stations-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-tgu-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" pcp-ui-service "$@"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" slots-service "$@"
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec "$script_dir/run-service.sh" spring-cloud-config-server "$@"
+79
View File
@@ -0,0 +1,79 @@
# Local jar runner
Скрипт запускает backend-сервисы как Spring Boot jar'ы на локальной машине.
Spring Cloud Config Server стартует первым и читает конфигурацию из локального
`config-repo`.
## Запуск
Из корня репозитория:
```bash
./deploy/local-jar/run-services.sh start
```
Что делает `start`:
- собирает `bootJar` для `spring-cloud-config-server` и всех Spring Boot сервисов;
- поднимает локальные `postgres` и `kafka` через `deploy/local-bundle/docker-compose.yml`;
- создаёт сервисные базы данных, если их ещё нет;
- запускает `spring-cloud-config-server` на `http://localhost:38888`;
- проверяет lookup конфигурации из `config-repo`;
- запускает остальные jar'ы в заданной последовательности.
Runtime-файлы создаются в `build/local-jar-runtime`:
- `pids/` - PID процессов;
- `logs/` - stdout/stderr каждого сервиса.
## Команды
```bash
./deploy/local-jar/run-services.sh status
./deploy/local-jar/run-services.sh logs pcp-request-service
./deploy/local-jar/run-services.sh stop
./deploy/local-jar/run-services.sh restart
```
Только сборка:
```bash
./deploy/local-jar/run-services.sh build
```
Только инфраструктура:
```bash
./deploy/local-jar/run-services.sh infra-up
./deploy/local-jar/run-services.sh infra-down
```
## Настройки
По умолчанию config-server использует:
```text
SPRING_PROFILES_ACTIVE=local
CONFIG_NATIVE_SEARCH_LOCATIONS=file:<repo>/config-repo
SERVER_PORT=38888
```
Сервисы используют:
```text
SPRING_PROFILES_ACTIVE=local
SPRING_CLOUD_CONFIG_PROFILE=local
SPRING_CLOUD_CONFIG_LABEL=
CONFIG_SERVER_URI=http://localhost:38888
```
Полезные override'ы:
```bash
START_INFRA=false ./deploy/local-jar/run-services.sh start
INIT_DB=false ./deploy/local-jar/run-services.sh start
WAIT_FOR_APPS=false ./deploy/local-jar/run-services.sh start
JAVA_OPTS="-Xmx1g" ./deploy/local-jar/run-services.sh start
CONFIG_REPO_DIR=/path/to/config-repo ./deploy/local-jar/run-services.sh start
LOCAL_SERVICE_PROFILES=local,foo ./deploy/local-jar/run-services.sh start
```
+538
View File
@@ -0,0 +1,538 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
local_bundle_dir="$repo_root/deploy/local-bundle"
compose_file="$local_bundle_dir/docker-compose.yml"
local_bundle_env_file="$local_bundle_dir/.env"
if [[ -f "$local_bundle_env_file" ]]; then
set -a
# shellcheck disable=SC1090
source "$local_bundle_env_file"
set +a
fi
config_repo_dir="${CONFIG_REPO_DIR:-$repo_root/config-repo}"
runtime_dir="${LOCAL_JAR_RUNTIME_DIR:-$repo_root/build/local-jar-runtime}"
pid_dir="$runtime_dir/pids"
log_dir="$runtime_dir/logs"
if [[ "$config_repo_dir" != /* ]]; then
config_repo_dir="$repo_root/$config_repo_dir"
fi
if [[ "$runtime_dir" != /* ]]; then
runtime_dir="$repo_root/$runtime_dir"
pid_dir="$runtime_dir/pids"
log_dir="$runtime_dir/logs"
fi
config_service="spring-cloud-config-server"
app_services=(
pcp-srpring-boot-admin-server
pcp-stations-service
pcp-satellite-catalog-service
pcp-request-service
pcp-route-processing-service
pcp-ballistics-service
slots-service
pcp-complex-mission-service
pcp-mission-planing-service
pcp-coverage-scheme-service
pcp-dynamic-plan-service
tle-monitoring-service
pcp-tgu-service
pcp-ui-service
)
all_services=("$config_service" "${app_services[@]}")
databases=(
pcp_ballistics
pcp_requests
pcp_route_processing
pcp_missions
pcp_slots
pcp_complex_plan
pcp_satellite_catalog
pcp_stations
pcp_tgu
pcp_tle
pcp_satellites
)
declare -A service_ports=(
[spring-cloud-config-server]="${CONFIG_SERVER_PORT:-38888}"
[pcp-srpring-boot-admin-server]="${PCP_ADMIN_PORT:-7000}"
[tle-monitoring-service]="${PCP_TLE_MONITORING_PORT:-7001}"
[pcp-complex-mission-service]="${PCP_COMPLEX_MISSION_PORT:-7002}"
[pcp-ballistics-service]="${PCP_BALLISTICS_PORT:-7003}"
[pcp-route-processing-service]="${PCP_ROUTE_PROCESSING_PORT:-7004}"
[pcp-request-service]="${PCP_REQUEST_PORT:-7005}"
[slots-service]="${PCP_SLOTS_PORT:-7006}"
[pcp-ui-service]="${PCP_UI_PORT:-7008}"
[pcp-stations-service]="${PCP_STATIONS_PORT:-7009}"
[pcp-mission-planing-service]="${PCP_MISSION_PORT:-7010}"
[pcp-tgu-service]="${PCP_TGU_PORT:-7011}"
[pcp-coverage-scheme-service]="${PCP_COVERAGE_SCHEME_PORT:-7012}"
[pcp-satellite-catalog-service]="${PCP_SATELLITE_CATALOG_PORT:-7013}"
[pcp-dynamic-plan-service]="${PCP_DYNAMIC_PLAN_PORT:-7014}"
)
gradle_max_workers="${GRADLE_MAX_WORKERS:-1}"
gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}"
kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}"
config_server_port="${service_ports[$config_service]}"
config_server_uri="${LOCAL_CONFIG_SERVER_URI:-http://localhost:$config_server_port}"
service_profiles="${LOCAL_SERVICE_PROFILES:-local}"
config_profiles="${LOCAL_CONFIG_PROFILES:-$service_profiles}"
postgres_host="${PCP_POSTGRES_HOST:-localhost}"
postgres_port="${PCP_POSTGRES_PORT:-5432}"
kafka_host="${PCP_KAFKA_HOST:-localhost}"
kafka_port="${PCP_KAFKA_PORT:-29092}"
gradle_common_args=(
"--no-parallel"
"--max-workers=$gradle_max_workers"
"-Dorg.gradle.jvmargs=$gradle_jvmargs"
"-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs"
)
usage() {
cat <<'EOF'
Usage:
./deploy/local-jar/run-services.sh [command]
Commands:
start Build bootJar artifacts and start local infra, config-server and services.
restart Stop local jar processes, then run start.
stop Stop jar processes started by this script.
status Show pid and log status for local jar processes.
build Build bootJar artifacts only.
infra-up Start local Postgres/Kafka via deploy/local-bundle/docker-compose.yml.
infra-down Stop local Postgres/Kafka containers.
logs NAME Tail log for a service.
Useful environment:
START_INFRA=false Do not start Postgres/Kafka containers during start.
INIT_DB=false Do not create local Postgres databases during start.
WAIT_FOR_APPS=false Do not wait for each app /actuator/health.
CONFIG_REPO_DIR=... Config repository used by spring-cloud-config-server.
JAVA_OPTS="-Xmx1g" Extra JVM options for every jar process.
LOCAL_SERVICE_PROFILES=local,foo
EOF
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name is required." >&2
exit 1
fi
}
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose --project-directory "$local_bundle_dir" -f "$compose_file" "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose --project-directory "$local_bundle_dir" -f "$compose_file" "$@"
else
echo "docker compose or docker-compose is required." >&2
exit 1
fi
}
http_get() {
local url="$1"
if command -v curl >/dev/null 2>&1; then
curl -fsS "$url" >/dev/null
elif command -v wget >/dev/null 2>&1; then
wget -q -O /dev/null "$url"
else
return 2
fi
}
wait_for_http() {
local name="$1"
local url="$2"
local attempts="${3:-90}"
local attempt
for ((attempt = 1; attempt <= attempts; attempt++)); do
if http_get "$url"; then
echo "$name is ready: $url"
return 0
fi
sleep 2
done
echo "$name is not ready after $((attempts * 2)) seconds: $url" >&2
exit 1
}
wait_for_postgres() {
local attempt
local postgres_user="${POSTGRES_USER:-postgres}"
for attempt in {1..60}; do
if compose exec -T postgres pg_isready -U "$postgres_user" -d postgres >/dev/null 2>&1; then
echo "Postgres is ready."
return 0
fi
sleep 2
done
echo "Postgres is not ready after 120 seconds." >&2
exit 1
}
wait_for_kafka() {
local attempt
for attempt in {1..60}; do
if compose exec -T kafka kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1; then
echo "Kafka is ready."
return 0
fi
sleep 2
done
echo "Kafka is not ready after 120 seconds." >&2
exit 1
}
create_database_if_missing() {
local db_name="$1"
local postgres_user="${POSTGRES_USER:-postgres}"
local exists
exists="$(
compose exec -T postgres psql -U "$postgres_user" -d postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = '$db_name'" | tr -d '[:space:]'
)"
if [[ "$exists" == "1" ]]; then
echo "Database already exists: $db_name"
return 0
fi
echo "Creating database: $db_name"
compose exec -T postgres createdb -U "$postgres_user" "$db_name"
}
start_infra() {
require_command docker
echo "Starting local Postgres and Kafka..."
compose up -d postgres kafka
wait_for_postgres
wait_for_kafka
}
init_databases() {
echo "Initializing local service databases..."
for db_name in "${databases[@]}"; do
create_database_if_missing "$db_name"
done
}
build_services() {
echo "Building bootJar artifacts..."
echo "Gradle max workers: $gradle_max_workers"
echo "Gradle JVM args: $gradle_jvmargs"
echo "Kotlin daemon JVM args: $kotlin_daemon_jvmargs"
for service_name in "${all_services[@]}"; do
echo "Building bootJar for $service_name..."
"$repo_root/gradlew" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
done
}
service_pid_file() {
local service_name="$1"
printf '%s/%s.pid' "$pid_dir" "$service_name"
}
service_log_file() {
local service_name="$1"
printf '%s/%s.log' "$log_dir" "$service_name"
}
is_pid_running() {
local pid="$1"
[[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1
}
read_service_pid() {
local service_name="$1"
local pid_file
pid_file="$(service_pid_file "$service_name")"
if [[ -f "$pid_file" ]]; then
tr -d '[:space:]' < "$pid_file"
fi
}
find_boot_jar() {
local service_name="$1"
local jar_dir="$repo_root/services/$service_name/build/libs"
local jar_count
local jar_file
if [[ ! -d "$jar_dir" ]]; then
echo "Missing build/libs for $service_name. Run build first." >&2
exit 1
fi
jar_count="$(find "$jar_dir" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
if [[ "$jar_count" != "1" ]]; then
echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2
find "$jar_dir" -maxdepth 1 -type f -name '*.jar' -print >&2 || true
exit 1
fi
jar_file="$(find "$jar_dir" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
printf '%s' "$jar_file"
}
start_service() {
local service_name="$1"
local jar_file
local pid_file
local log_file
local existing_pid
local port="${service_ports[$service_name]}"
local java_cmd=(java)
local parsed_java_opts=()
pid_file="$(service_pid_file "$service_name")"
log_file="$(service_log_file "$service_name")"
existing_pid="$(read_service_pid "$service_name" || true)"
if is_pid_running "$existing_pid"; then
echo "$service_name is already running with pid $existing_pid"
return 0
fi
mkdir -p "$pid_dir" "$log_dir"
jar_file="$(find_boot_jar "$service_name")"
if [[ -n "${JAVA_OPTS:-}" ]]; then
read -r -a parsed_java_opts <<< "$JAVA_OPTS"
java_cmd+=("${parsed_java_opts[@]}")
fi
java_cmd+=(-jar "$jar_file")
echo "Starting $service_name on port $port..."
if [[ "$service_name" == "$config_service" ]]; then
(
cd "$repo_root"
env \
SPRING_PROFILES_ACTIVE=local \
CONFIG_NATIVE_SEARCH_LOCATIONS="file:$config_repo_dir" \
SERVER_PORT="$config_server_port" \
"${java_cmd[@]}" > "$log_file" 2>&1 &
echo "$!" > "$pid_file"
)
else
(
cd "$repo_root"
env \
CONFIG_SERVER_URI="$config_server_uri" \
CONFIG_SERVER_FAIL_FAST=true \
SPRING_PROFILES_ACTIVE="$service_profiles" \
SPRING_CLOUD_CONFIG_PROFILE="$config_profiles" \
SPRING_CLOUD_CONFIG_LABEL= \
SPRING_KAFKA_ADMIN_AUTO_CREATE=true \
SPRING_KAFKA_LISTENER_MISSING_TOPICS_FATAL=false \
CAMUNDA_CLIENT_ENABLED=false \
PLANNING_EXTERNAL_UPDATE_ENABLED=false \
TLE_POLLING_ENABLED=false \
PCP_POSTGRES_HOST="$postgres_host" \
PCP_POSTGRES_PORT="$postgres_port" \
PCP_KAFKA_HOST="$kafka_host" \
PCP_KAFKA_PORT="$kafka_port" \
PCP_PORTS_CONFIG="$config_server_port" \
PCP_PORTS_ADMIN="${service_ports[pcp-srpring-boot-admin-server]}" \
PCP_PORTS_TLE_MONITORING="${service_ports[tle-monitoring-service]}" \
PCP_PORTS_COMPLEX_MISSION_SERVICE="${service_ports[pcp-complex-mission-service]}" \
PCP_PORTS_BALLISTICS="${service_ports[pcp-ballistics-service]}" \
PCP_PORTS_ROUTE_PROCESSING="${service_ports[pcp-route-processing-service]}" \
PCP_PORTS_REQUEST="${service_ports[pcp-request-service]}" \
PCP_PORTS_SLOTS="${service_ports[slots-service]}" \
PCP_PORTS_UI="${service_ports[pcp-ui-service]}" \
PCP_PORTS_STATIONS="${service_ports[pcp-stations-service]}" \
PCP_PORTS_MISSION="${service_ports[pcp-mission-planing-service]}" \
PCP_PORTS_TGU="${service_ports[pcp-tgu-service]}" \
PCP_PORTS_COVERAGE_SCHEME="${service_ports[pcp-coverage-scheme-service]}" \
PCP_PORTS_SATELLITE_CATALOG="${service_ports[pcp-satellite-catalog-service]}" \
PCP_PORTS_DYNAMIC_PLAN="${service_ports[pcp-dynamic-plan-service]}" \
"${java_cmd[@]}" > "$log_file" 2>&1 &
echo "$!" > "$pid_file"
)
fi
echo "$service_name log: $log_file"
}
start_config_server() {
if [[ ! -d "$config_repo_dir" ]]; then
echo "Config repo directory does not exist: $config_repo_dir" >&2
exit 1
fi
start_service "$config_service"
wait_for_http "$config_service" "$config_server_uri/actuator/health"
wait_for_http "$config_service config-repo lookup" "$config_server_uri/pcp-request-service/local"
}
wait_for_service_health() {
local service_name="$1"
local port="${service_ports[$service_name]}"
wait_for_http "$service_name" "http://localhost:$port/actuator/health" "${APP_HEALTH_ATTEMPTS:-90}"
}
start_app_services() {
for service_name in "${app_services[@]}"; do
start_service "$service_name"
if [[ "${WAIT_FOR_APPS:-true}" == "true" ]]; then
wait_for_service_health "$service_name"
fi
done
}
stop_service() {
local service_name="$1"
local pid_file
local pid
local attempt
pid_file="$(service_pid_file "$service_name")"
pid="$(read_service_pid "$service_name" || true)"
if ! is_pid_running "$pid"; then
rm -f "$pid_file"
echo "$service_name is not running."
return 0
fi
echo "Stopping $service_name with pid $pid..."
kill "$pid" >/dev/null 2>&1 || true
for attempt in {1..30}; do
if ! is_pid_running "$pid"; then
rm -f "$pid_file"
return 0
fi
sleep 1
done
echo "Force stopping $service_name with pid $pid..."
kill -9 "$pid" >/dev/null 2>&1 || true
rm -f "$pid_file"
}
stop_services() {
local service_name
local idx
for ((idx = ${#app_services[@]} - 1; idx >= 0; idx--)); do
service_name="${app_services[$idx]}"
stop_service "$service_name"
done
stop_service "$config_service"
}
status_services() {
local service_name
local pid
local log_file
printf '%-36s %-10s %s\n' "service" "pid" "log"
for service_name in "${all_services[@]}"; do
pid="$(read_service_pid "$service_name" || true)"
log_file="$(service_log_file "$service_name")"
if is_pid_running "$pid"; then
printf '%-36s %-10s %s\n' "$service_name" "$pid" "$log_file"
else
printf '%-36s %-10s %s\n' "$service_name" "-" "$log_file"
fi
done
}
tail_logs() {
local service_name="${1:-}"
local log_file
if [[ -z "$service_name" ]]; then
echo "Service name is required for logs command." >&2
exit 1
fi
log_file="$(service_log_file "$service_name")"
if [[ ! -f "$log_file" ]]; then
echo "Log file does not exist: $log_file" >&2
exit 1
fi
tail -f "$log_file"
}
start_all() {
require_command java
build_services
if [[ "${START_INFRA:-true}" == "true" ]]; then
start_infra
if [[ "${INIT_DB:-true}" == "true" ]]; then
init_databases
fi
else
echo "Skipping local infra startup. Expecting Postgres at $postgres_host:$postgres_port and Kafka at $kafka_host:$kafka_port."
fi
start_config_server
start_app_services
echo "Local jar startup requested."
status_services
}
command="${1:-start}"
case "$command" in
start)
start_all
;;
restart)
stop_services
start_all
;;
stop)
stop_services
;;
status)
status_services
;;
build)
build_services
;;
infra-up)
start_infra
if [[ "${INIT_DB:-true}" == "true" ]]; then
init_databases
fi
;;
infra-down)
require_command docker
compose stop kafka postgres
;;
logs)
tail_logs "${2:-}"
;;
help|-h|--help)
usage
;;
*)
usage >&2
exit 1
;;
esac
@@ -0,0 +1,173 @@
# Аудит проекта observatio-terrae
Дата: 2026-06-06. Автор: Claude (Opus 4.8).
Охват: фронтенд `pcp-tgu-ops-ui` + бэкенд-сервисы. Сервис `tle-monitoring-service` по
указанию исключён из анализа.
Метод: статический разбор кода, прогон сборки/тестов фронта, выборочное чтение бэкенд-сервисов
(акцент на недавно дорабатываемых: tgu, mission-planing, complex-mission, request), проверка
конфигов, CORS, обработки ошибок и гигиены репозитория.
## Сводка по состоянию
Базовое здоровье — хорошее. Фронтенд проходит строгую типизацию (`tsc -b`, exit 0) и все
тесты (53 теста в 12 файлах, зелёные); в исходниках нет `console.*`, `any`, `@ts-ignore`.
Бэкенд использует общий `GlobalExceptionHandler` из `pcp-types-lib`, нет `printStackTrace`,
нет `@Disabled`/`@Ignore` тестов. Найденные проблемы — это в основном технический долг,
копипаст-артефакты и один реальный риск надёжности в Camunda-воркерах.
Приоритеты: **B1** (надёжность), **R1/C1** (гигиена/конфиги), затем рефакторинг-долг.
---
## Бэкенд
### B1 (высокий). Camunda job-воркеры молча проглатывают исключения
Файл: [FlowService.kt](../services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/FlowService.kt)
Все воркеры объявлены с `autoComplete = false` и обёрнуты в:
```kotlin
catch (ex : Exception) { logger.warn(ex.message) }
```
Если исключение возникает до `client.newCompleteCommand(...).send()`, задача **не завершается и
не падает**: воркер «съел» ошибку. Camunda не получает ни complete, ни fail/incident — BPMN-процесс
зависает молча до таймаута активации, без видимого инцидента и без явной диагностики. Плюс
`logger.warn(ex.message)` теряет stack trace, а `ex.message` может быть `null`.
Рекомендация: в catch завершать задачу через `client.newFailCommand(job.key)` (с retries) либо
бросать исключение наверх, чтобы Camunda создала incident; логировать как `logger.error(msg, ex)`
со стек-трейсом. Это касается `surveyPlan`, `surveyDrop`, `sendPlan`, `confirmPlan` и прочих
воркеров файла.
### B2 (средний). Симуляционная заглушка `Thread.sleep(5000)` в проде-пути
Файл: [FlowService.kt:97](../services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/FlowService.kt#L97)
В `sendPlan` стоит `Thread.sleep(5000)` с комментарием «Иммитация сообщения о получении
подтверждения» — синхронно блокирует поток job-воркера на 5 с и эмулирует приём подтверждения
плана. Для MVP-демо допустимо (см. практику осознанных заглушек), но это нужно снять при
интеграции с реальной шиной подтверждений; пока имитация маскирует отсутствие реального канала и
держит поток пула воркеров.
### B3 (средний). Несогласованные пакеты — копипаст-артефакты
Базовые пакеты ряда сервисов не соответствуют их имени (видимо, форки шаблона):
| Сервис | Пакет | Проблема |
|---|---|---|
| pcp-ballistics-service | `space.nstart.pcp.pcp_request_service` | пакет от request-сервиса |
| pcp-complex-mission-service | `space.nstart.pcp.pcp_satellites_service` | чужое имя |
| pcp-ui-service | `space.nstart.pcp.slots_service` | совпадает со slots-service |
| slots-service | `space.nstart.pcp.slots_service` | тот же пакет, что у ui-service |
| pcp-tgu-service | `space.nstart.pcp_tgu_service` | пропущен сегмент `.pcp.` |
| pcp-request-service | `org.nstart.dep265.requestservice` | другой корень (`org` вместо `space`) |
Последствия: grep/навигация по пакету выводит на «не тот» сервис, IDE-импорты обманчивы,
ui-service и slots-service неотличимы по пакету. Переименование пакетов затрагивает много файлов —
делать осознанно, отдельной задачей; новые сервисы заводить уже с корректным пакетом.
### B4 (низкий). Опечатка «srpring» растиражирована
`pcp-srpring-boot-admin-server` — опечатка в имени каталога и пакета, продублированная в
`settings.gradle.kts`, `build.gradle.kts`, `.gitlab-ci.yml`, `deploy/local-bundle/docker-compose.yml`,
`config-repo/application-docker-local.yaml`. Косметика, но фиксируется во всей инфраструктуре, потому
исправлять дорого — оставить как известный долг или чинить вместе с B3.
### B5 (низкий). Потеря stack trace при логировании
~9 мест вида `logger.warn(ex.message)` / `logger.error(ex.message)` логируют только текст без
исключения. Для диагностики предпочтительно `logger.error("...", ex)`.
### B6 (инфо). Опечатки в лог-сообщениях
Напр. «Утверждение поана» (план), «Иммитация» (имитация) в FlowService.
---
## Фронтенд (pcp-tgu-ops-ui)
### F1 (средний). Дублирование HTTP-/error-обвязки в API-клиентах
`extractErrorText` скопирован в 5 файлах: `tguApi.ts`, `ballisticsApi.ts`, `missionPlanningApi.ts`,
`complexPlanApi.ts`, `tguPlanApi.ts`. Шаблон `try { fetch } catch { TguApiError("network") }` +
разбор `!response.ok` повторяется ~22 раза (7 в missionPlanningApi, 5 в tguPlanApi и т.д.).
В `tguApi.ts` уже есть аккуратный `fetchJson<T>()` — остальные клиенты переписывают то же руками.
Рекомендация: вынести единый `src/api/http.ts` (обёртка `fetchJson` + `extractErrorText` +
маппинг ошибок) и переиспользовать. Сократит ~150 строк и устранит расхождения (см. F5).
### F2 (низкий). Островок нетипизированного JS во вкладке «Группировка»
`features/constellation/*` — это `.jsx`/`.js` (`ConstellationDashboard.jsx`, `sim.js`, `data.js`,
`MapView.jsx`, `Panels.jsx`, `Timeline.jsx`, `util.js`), включён через `allowJs`. Остальное
приложение — строгий TS. Прототипный код подключён в прод через `ConstellationTab.tsx` (обёртка с
`as ComponentType`). Типы в нём фактически `any` — провал строгой типизации в этой ветке UI.
Долг: постепенно перевести на `.tsx` или хотя бы добавить декларации типов на границе.
### F3 (низкий). Мёртвый barrel `constellation/index.js`
`features/constellation/index.js` (реэкспорт `SIM`, `SATELLITES`, `STATIONS`, ...) нигде не
импортируется — можно удалить.
### F4 (низкий). God-компонент `TguEditorTab.tsx` (1727 строк)
Самый крупный файл фронта; держит десятки `useState`/`useRef`, режимы правой панели, построение
маршрутов, валидацию, undo/redo. Хорошо закомментирован, но монолитен. Кандидат на декомпозицию
(вынос хуков `useDraftHistory`, `useRouteBuilder`, разнесение панелей) для снижения связности.
### F5 (инфо). Расхождение приоритета полей ошибки
В `tguApi.extractErrorText` порядок полей `reason, message, error, detail`, а в
`missionPlanningApi.extractErrorText``message, error, reason, detail`. При едином ответе бэка
пользователь увидит разный текст из разных клиентов. Устранится при выносе общей обвязки (F1).
---
## Гигиена репозитория
### R1 (средний). Крупные дампы данных в корне репозитория (в git)
Отслеживаются git и лежат в корне:
- `pcp_requests_converted_create_requests.json` (~228 КБ)
- `response_заданияКубернетс.json` (~157 КБ)
- `response_заданияЕмельянов.json` (~66 КБ)
Это тестовые выгрузки, засоряющие корень и историю. Перенести в `docs/`/`*/src/test/resources`
или убрать из git (оставив в `.gitignore`).
### R2 (низкий). Посторонние артефакты в корне (не в git)
- `node_modules/` в корне + пустой `package-lock.json` (`"packages": {}`, 0 зависимостей) —
корневого Node-проекта нет, фронт живёт в `services/pcp-tgu-ops-ui`. Мусор от случайного
`npm install` в корне; удалить, при желании добавить `/package-lock.json` в `.gitignore`.
- `obj_terra.zip` (~1.5 МБ) в корне — не в git, но захламляет дерево.
### R3 (низкий). 26 устаревших каталогов `bin/` на диске
Скомпилированные `bin/main/...` (gitignored) задваивают исходники: любой `grep` по `services`
выдаёт каждый файл дважды (src + bin) — шум при навигации. Прогнать `./gradlew clean` /
удалить `bin/` каталоги.
---
## Конфигурация и безопасность
### C1 (средний). Несогласованное задание паролей БД в config-repo
В большинстве сервисов пароль зашит как `password: password`, и только
[pcp-tgu-service.yaml](../config-repo/pcp-tgu-service.yaml) использует переопределяемый паттерн
`password: ${DB_PASSWORD:password}`. Для локалки риск низкий, но шаблон важен: привести все
сервисы к `${DB_PASSWORD:...}` (и прочие креды — к env-переменным), чтобы прод/стенды
переопределялись без правки config-repo.
### C2 (низкий). CORS: широкий в ui-service, отсутствует в остальных
[WebConfig.kt](../services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/WebConfig.kt)
задаёт `allowedOriginPatterns("*")` + `allowCredentials=true` — широко; для прода имеет смысл
сузить origin до конкретных хостов. Остальные сервисы, которые фронт зовёт напрямую (tgu,
mission, request, ballistics, complex, stations), CORS-конфига не имеют — сейчас работает только
потому, что обращение идёт через Vite-прокси/nginx (same-origin). Прямые кросс-доменные вызовы к
ним из браузера упадут — это ограничение стоит держать в курсе при изменении схемы доступа.
---
## Что в порядке (намеренно не правим)
- Заглушки/тест-контроллеры/пустой контракт валидации миссии — осознанные MVP-срезы, не дефекты.
- Прямые вызовы микросервисов из фронта через прокси (без BFF) — принятое решение этапа.
- Naive `LocalDateTime` как локальное настенное время — намеренная договорённость проекта.
## Рекомендованный порядок работ
1. **B1** — починить обработку ошибок в Camunda-воркерах (надёжность процессов).
2. **R1, C1** — убрать дампы из корня; унифицировать env-пароли.
3. **F1** — общий `http.ts`, заодно закрыть F5.
4. **B2** — снять `Thread.sleep`-имитацию при появлении реального канала подтверждений.
5. Фоном: **R2/R3** (чистка), **F2/F4** (типизация constellation, декомпозиция редактора),
**B3/B4** (пакеты/опечатки) — отдельными задачами из-за широкого охвата.
+5
View File
@@ -38,6 +38,11 @@ _Обновлено: 2026-06-11_
### Бэкенд / инфраструктура
- [ ] **#7** — Перевести сервис станций на ordinis (по примеру получения аппаратов в сервисе tgu).
- [ ] **#47** — Перевести сервисы с Jackson 2 на Jackson 3 и убрать двойной стек. Под Spring Boot 4.0.2 MVC уже десериализует входящий JSON через **Jackson 3** (`tools.jackson.*`), а весь код монорепы написан под **Jackson 2** (`com.fasterxml.jackson`, `versions.jackson_databind=2.18.2`); в `pcp-request-service` для внутреннего парсинга держится отдельный Jackson 2-бин (`JacksonConfig`). Из-за этой рассинхронизации словили баг: Jackson 3 по умолчанию включает `FAIL_ON_NULL_FOR_PRIMITIVES`, и явный `null` на примитивном `Double coverageRequiredPercent` ронял создание заявки (контракт-тесты не ловили — они на Jackson 2). Срочности/EOL-риска нет, это техдолг и консистентность.
- **Сейчас, дёшево (часы):** системно снять класс багов «null на примитиве» — выставить `spring.jackson.deserialization.fail-on-null-for-primitives=false` в config-repo request-service (проверить, что свойство применяется к Jackson 3-бину; если нет — через `Jackson3ObjectMapperBuilderCustomizer`). Не латать каждое поле по очереди.
- **Среднесрок, по сервису:** мигрировать по одному, пилот — `pcp-request-service` (~7 файлов main, ~0.5–1 день). Аннотации **не трогаем**`jackson-annotations` в Jackson 3 остались в пакете `com.fasterxml.jackson.annotation` (у нас это основная масса: `@JsonIgnoreProperties` ×36, `@JsonProperty` ×16, `@JsonInclude` ×15…). Менять только databind-поверхность: `ObjectMapper` ×23, `TypeReference` ×8, `JsonNode` ×5, `jacksonObjectMapper()` ×12 → пакеты `tools.jackson.databind/core/module.kotlin`; в Jackson 3 `JsonProcessingException` стала unchecked. Дропнуть Jackson 2-зависимости и Jackson 2-бин `JacksonConfig`.
- **Главный риск — Kafka:** `KafkaConsumerConfig`/сериализаторы завязаны на `com.fasterxml` ObjectMapper. Перед стартом — спайк (~полдня): умеет ли `spring-kafka` в нашем Boot 4 Jackson 3-сериализаторы. Если нет — Kafka-слой мигрируем отдельно, HTTP/внутренний JSON переводим независимо.
- **Масштаб (src/main):** 67 файлов в 10 сервисах — pcp-dynamic-plan-service 22, pcp-ui-service 16 (выводится из эксплуатации, ср. #40 — можно не мигрировать), pcp-request-service 7, slots-service/pcp-tgu-service по 5, остальные 1–4. **Грубая оценка всей репы — ~6–10 человеко-дней**, основной риск — Kafka. Big-bang не нужен: шаг 1 закрывает причину бага, полную миграцию вести фоново сервис за сервисом.
- [ ] **#45** — Собрать native-образ сервиса (Kotlin/Native, `kompile`) и контейнер на его основе. Цель — быстрый старт и меньший footprint в сравнении с JVM-образом. Уточнить: какой сервис(ы) переводим первым, совместимость с рефлексией/AOT-конфигом (reachability metadata), сборку в CI и базовый образ контейнера.
- [ ] **#37** — Рассинхрон номеров витков между баллистическими источниками. Для одного и того же КА/времени `flight-line` (трасса, `/api/tgu-planning/spacecraft/{norad}/flight-line`) и `survey-route/build` (контур, pcp-mission-planing) отдают **разный номер витка**: на Emissio 1 (norad 2) 2026-06-06 12:5012:55 трасса помечена витком 142, а контур/сохранённые режимы — витком 144 (по широте ~74° с.ш. согласованы, расходится только нумерация). В редакторе карта фильтрует трассу/полосу обзора по видимому витку (`buildEditorMapScene``visibleRevolutions`), поэтому при сужении селектора до «виток N» точки трассы с другим номером выпадают и пропадает орбитальный контекст. Свести нумерацию витков к единому источнику (или маппинг на границе), чтобы трасса и маршрут согласовывались по витку.
@@ -67,7 +67,10 @@ data class CreateRequestDto(
*/
@field:DecimalMin(value = "0", inclusive = false)
@field:DecimalMax("100")
val coverageRequiredPercent: Double = 100.0,
// Nullable с дефолтом: под Jackson 3 (MVC) FAIL_ON_NULL_FOR_PRIMITIVES включён по умолчанию,
// и явный null от клиента падал бы на примитиве Double. Опциональное поле с дефолтом трактуем
// null как «взять дефолт» — при маппинге в сущность коалесим к 100.0.
val coverageRequiredPercent: Double? = 100.0,
/** Параметры оптической съемки. */
@field:Valid
@@ -81,7 +81,7 @@ class RequestService(
remainingArea = geometryArea,
// Порог завершения по покрытию приходит из API (по умолчанию 100%); это прогресс съёмки,
// в отличие от importance — важности заявки для карты приоритетов ячеек.
coverageRequiredPercent = request.coverageRequiredPercent,
coverageRequiredPercent = request.coverageRequiredPercent ?: 100.0,
importance = importance,
beginDateTime = request.beginDateTime,
endDateTime = request.endDateTime,
@@ -112,6 +112,23 @@ class RequestServiceTest {
assertEquals(80.0, persistedRequests.single().coverageRequiredPercent)
}
@Test
fun `create request coalesces null coverage required percent to default`() {
val persistedRequests = mutableListOf<RequestEntity>()
doReturn(false)
.`when`(requestRepository)
.existsById(REQUEST_ID)
doAnswer { invocation ->
persistedRequests += invocation.getArgument<RequestEntity>(0)
null
}.`when`(entityManager).persist(any(RequestEntity::class.java))
// Явный null (клиент прислал coverageRequiredPercent: null) трактуем как «взять дефолт».
service.createRequest(createRequestDto(coverageRequiredPercent = null))
assertEquals(100.0, persistedRequests.single().coverageRequiredPercent)
}
@Test
fun `get request by id returns name and importance`() {
val request = existingRequest()
@@ -296,7 +313,7 @@ class RequestServiceTest {
verify(requestCellRepository).deleteByRequestId(REQUEST_ID)
}
private fun createRequestDto(coverageRequiredPercent: Double = 100.0): CreateRequestDto {
private fun createRequestDto(coverageRequiredPercent: Double? = 100.0): CreateRequestDto {
return CreateRequestDto(
id = REQUEST_ID,
name = REQUEST_NAME,
@@ -0,0 +1,9 @@
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
FROM ${RUNTIME_IMAGE}
ENV JAVA_OPTS=""
ADD ./build/libs/*.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
@@ -24,6 +24,8 @@ type Tgu2DMapViewProps = {
selectedRequestPolygonId?: string;
/** Внешняя подсветка объекта (контур съёмки или линия сброса) по его id — «как будто кликнули на карте». */
selectedObjectId?: string;
/** Отмеченные объекты (постоянная подсветка независимо от выбора) — напр. слоты, отобранные под бронь. */
markedObjectIds?: ReadonlySet<string>;
/** Точка, на которую центрировать карту; перецентрирование срабатывает при смене focusToken. */
focusPoint?: GeoPoint;
focusToken?: number;
@@ -84,6 +86,7 @@ export function Tgu2DMapView({
hoveredRequestPolygonId,
selectedRequestPolygonId,
selectedObjectId,
markedObjectIds,
focusPoint,
focusToken,
enableObjectChooser = false,
@@ -529,6 +532,14 @@ export function Tgu2DMapView({
[hoveredRequestPolygonId, activePolygons]
);
const overlayPolygons = uniquePolygons([externalSelectedPolygon, selectedPolygon, externalHoveredPolygon, hoverRef.current]);
// Отмеченные объекты (напр. отобранные под бронь слоты) — постоянная подсветка под обычным выбором.
const markedPolygons = useMemo(
() =>
markedObjectIds && markedObjectIds.size > 0
? activePolygons.filter((p) => markedObjectIds.has(p.id))
: [],
[markedObjectIds, activePolygons]
);
const overlayMarkers = uniqueMarkers([selectedMarker, hoverMarkerRef.current]);
// Накопленное расстояние (км) от первой вершины линейки до каждой — по дуге большого круга.
@@ -563,6 +574,24 @@ export function Tgu2DMapView({
>
<canvas className="tgu-2d-map__canvas" ref={canvasRef} />
<svg className="tgu-2d-map__overlay" width={size.width} height={size.height} aria-hidden="true">
{markedPolygons.flatMap((polygon) => {
const raw = polygon.points.map((point) => projection.project(point));
const pts = unwrapX(raw, projection.worldSize);
return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
const visible = pts.some((p) => {
const x = p.x + dx;
return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
});
if (!visible) return [];
return (
<polygon
key={`marked:${polygon.id}:${dx}`}
points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
className="is-marked"
/>
);
});
})}
{overlayPolygons.flatMap((polygon) => {
const raw = polygon.points.map((point) => projection.project(point));
const pts = unwrapX(raw, projection.worldSize);
@@ -113,6 +113,12 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
// Ключи забронированных слотов — для бейджа «бронь» в таблице и отдельного слоя на карте.
const bookedKeys = useMemo(() => new Set(bookedSlots.map(slotKey)), [bookedSlots]);
// id полигонов слотов, отобранных галочками под бронь — постоянная подсветка на карте.
const markedSlotIds = useMemo(
() => new Set(Array.from(selectedKeys, (key) => `slot:${key}`)),
[selectedKeys]
);
// Слот по ключу — чтобы из выбора на карте (id "slot:key"/"booked:key") восстановить полные данные.
const slotByKey = useMemo(() => {
const map = new Map<string, SlotDTO>();
@@ -758,6 +764,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
onObjectsAtPoint={handleObjectsAtPoint}
onObjectSelect={handleObjectSelect}
selectedObjectId={hasRequest ? activeSlotId : activeHitId}
markedObjectIds={hasRequest ? markedSlotIds : undefined}
focusPoint={focusPoint}
focusToken={focusToken}
/>
@@ -1175,6 +1175,14 @@
stroke-width: 2;
}
/* Отобранные под бронь слоты — постоянная зелёная подсветка (отличается от оранжевого выбора). */
.tgu-2d-map__overlay polygon.is-marked {
fill: rgba(58, 209, 122, 0.22);
stroke: #3ad17a;
stroke-width: 2;
stroke-dasharray: 6 4;
}
.tgu-2d-map__overlay polyline.is-selected {
stroke: #f0ad2e;
stroke-width: 7;