diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1d67dc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,504 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - '*' + pull_request: + branches: + - '*' + +env: + IMAGE_TAG: ${{ github.sha }} + +jobs: + build-backend: + name: Build and Push Backend + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Verify secrets are set + run: | + if [ -z "${{ secrets.YC_IAM_TOKEN }}" ]; then + echo "❌ ERROR: YC_IAM_TOKEN secret is not set or empty" + exit 1 + fi + if [ -z "${{ secrets.REGISTRY_ID }}" ]; then + echo "❌ ERROR: REGISTRY_ID secret is not set or empty" + exit 1 + fi + echo "✅ Secrets are set" + echo "Registry: cr.yandex" + echo "Registry ID: ${{ secrets.REGISTRY_ID }}" + + - name: Login to Yandex Container Registry + run: | + echo "${{ secrets.YC_IAM_TOKEN }}" | \ + docker login --username iam --password-stdin cr.yandex + + - name: Build and push backend image + env: + REGISTRY_ID: ${{ secrets.REGISTRY_ID }} + IMAGE_TAG: ${{ env.IMAGE_TAG }} + YC_IAM_TOKEN: ${{ secrets.YC_IAM_TOKEN }} + run: | + cd CI + chmod +x build_backend.sh + ./build_backend.sh + + deploy-infra: + name: Deploy Infrastructure + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.6.6 + + - name: Check for existing API Gateway + working-directory: infrastructure/serverless + env: + YC_TOKEN: ${{ secrets.YC_IAM_TOKEN }} + run: | + # Устанавливаем yc CLI для проверки + curl -s https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash >/dev/null 2>&1 || true + export PATH="$PATH:$HOME/yandex-cloud/bin" + + # Настраиваем авторизацию + export YC_TOKEN="$YC_TOKEN" + export YC_FOLDER_ID="${{ secrets.YC_FOLDER_ID }}" + export YC_CLOUD_ID="${{ secrets.YC_CLOUD_ID }}" + yc config set token "$YC_TOKEN" >/dev/null 2>&1 || true + yc config set folder-id "${{ secrets.YC_FOLDER_ID }}" >/dev/null 2>&1 || true + yc config set cloud-id "${{ secrets.YC_CLOUD_ID }}" >/dev/null 2>&1 || true + + # Проверяем существование API Gateway + API_GATEWAY_NAME="devops-portfolio-serverless-api" + if yc serverless api-gateway get --name "$API_GATEWAY_NAME" --format json >/dev/null 2>&1; then + echo "API_GATEWAY_EXISTS=true" >> $GITHUB_ENV + echo "ℹ️ API Gateway '$API_GATEWAY_NAME' already exists - will not create (Terraform import not supported)" + echo "TODO: ТРЕБУЕТСЯ ПРАВКА - автоматическое обнаружение и управление существующим API Gateway" + else + # Если не найден или yc auth failed - по умолчанию создаем (если уже существует, Terraform выдаст ошибку) + echo "API_GATEWAY_EXISTS=false" >> $GITHUB_ENV + echo "✓ API Gateway '$API_GATEWAY_NAME' not found (or yc auth failed) - will attempt to create" + fi + + - name: Create terraform.tfvars + working-directory: infrastructure/serverless + env: + API_GATEWAY_EXISTS: ${{ env.API_GATEWAY_EXISTS }} + run: | + # Определяем значение create_api_gateway на основе проверки + if [ "$API_GATEWAY_EXISTS" = "true" ]; then + CREATE_API_GATEWAY="false" + else + CREATE_API_GATEWAY="true" + fi + + cat > terraform.tfvars </dev/null 2>&1; then + echo "✓ yc authentication successful" + else + echo "✗ yc authentication failed - trying alternative method..." + # Пробуем через переменную окружения напрямую + YC_TOKEN="$YC_TOKEN" yc iam service-account list --format json >/dev/null 2>&1 && echo "✓ yc authentication successful (via env var)" || echo "✗ yc authentication still failed" + fi + + # Получаем project_name из terraform.tfvars или используем дефолт + PROJECT_NAME=$(grep -E '^project_name\s*=' terraform.tfvars 2>/dev/null | sed 's/.*= *"\(.*\)".*/\1/' || echo "") + if [ -z "$PROJECT_NAME" ]; then + PROJECT_NAME="devops-portfolio-serverless" + fi + + echo "Using project_name: '$PROJECT_NAME'" + + # Имена сервисных аккаунтов + SERVERLESS_SA_NAME="${PROJECT_NAME}-serverless-sa" + STATIC_SITE_SA_NAME="${PROJECT_NAME}-static-site" + + echo "Checking for existing service accounts: '$SERVERLESS_SA_NAME' and '$STATIC_SITE_SA_NAME'" + + # Пробуем найти и удалить по имени + SERVERLESS_SA_ID=$(yc iam service-account get --name "$SERVERLESS_SA_NAME" --format json 2>/dev/null | jq -r '.id // empty' || echo "") + if [ -n "$SERVERLESS_SA_ID" ]; then + echo "Deleting existing service account: $SERVERLESS_SA_NAME ($SERVERLESS_SA_ID)" + yc iam service-account delete --id "$SERVERLESS_SA_ID" --format json 2>&1 || echo "Failed to delete or already deleted" + else + echo "Service account $SERVERLESS_SA_NAME not found by name" + fi + + STATIC_SITE_SA_ID=$(yc iam service-account get --name "$STATIC_SITE_SA_NAME" --format json 2>/dev/null | jq -r '.id // empty' || echo "") + if [ -n "$STATIC_SITE_SA_ID" ]; then + echo "Deleting existing service account: $STATIC_SITE_SA_NAME ($STATIC_SITE_SA_ID)" + yc iam service-account delete --id "$STATIC_SITE_SA_ID" --format json 2>&1 || echo "Failed to delete or already deleted" + else + echo "Service account $STATIC_SITE_SA_NAME not found by name" + fi + + # Агрессивно удаляем по известным ID (независимо от имени) + echo "Attempting to delete service accounts by known IDs..." + for known_id in "ajebh4qukejfcj3vurm1" "aje6eo39svoevpsmv61o"; do + echo "Checking if service account $known_id exists..." + if yc iam service-account get --id "$known_id" --format json >/dev/null 2>&1; then + echo "Found service account $known_id, deleting..." + yc iam service-account delete --id "$known_id" --format json 2>&1 && echo "Successfully deleted $known_id" || echo "Failed to delete $known_id (may have dependencies)" + else + echo "Service account $known_id not found (already deleted or doesn't exist)" + fi + done + + echo "Cleanup completed. Attempting to import known service accounts into Terraform state..." + echo "This prevents 'AlreadyExists' errors if the accounts still exist." + + # Пробуем импортировать известные сервисные аккаунты напрямую через Terraform + # Terraform сам проверит существование и либо импортирует, либо проигнорирует ошибку + SERVERLESS_SA_ID="ajebh4qukejfcj3vurm1" + STATIC_SITE_SA_ID="aje6eo39svoevpsmv61o" + + # Импортируем serverless SA (игнорируем ошибки - если уже в state или не существует, это нормально) + echo "📥 Attempting to import serverless service account ($SERVERLESS_SA_ID)..." + if terraform import yandex_iam_service_account.serverless "$SERVERLESS_SA_ID" 2>&1; then + echo " ✓ Successfully imported serverless service account" + else + echo " ℹ️ Import skipped (resource may not exist, already in state, or name mismatch - this is OK)" + fi + + # Импортируем static_site SA + echo "📥 Attempting to import static_site service account ($STATIC_SITE_SA_ID)..." + if terraform import yandex_iam_service_account.static_site "$STATIC_SITE_SA_ID" 2>&1; then + echo " ✓ Successfully imported static_site service account" + else + echo " ℹ️ Import skipped (resource may not exist, already in state, or name mismatch - this is OK)" + fi + + echo "✅ Service account import attempt completed" + + # Импортируем другие существующие ресурсы + echo "" + echo "Attempting to import other existing resources..." + + # Получаем имя бакета из terraform.tfvars + BUCKET_NAME=$(grep -E '^static_bucket_name\s*=' terraform.tfvars 2>/dev/null | sed 's/.*= *"\(.*\)".*/\1/' || echo "") + if [ -n "$BUCKET_NAME" ]; then + echo "📥 Attempting to import storage bucket ($BUCKET_NAME)..." + if terraform import yandex_storage_bucket.static_site "$BUCKET_NAME" 2>&1; then + echo " ✓ Successfully imported storage bucket" + else + echo " ℹ️ Import skipped (bucket may not exist, already in state, or name mismatch - this is OK)" + fi + fi + + # Проверяем, что уже в state + echo "Checking current Terraform state..." + terraform state list 2>/dev/null || echo " (No state file yet)" + + # Импортируем serverless container (нужен ID, получаем через data source) + CONTAINER_NAME="devops-portfolio-serverless-backend" + echo "📥 Attempting to import serverless container ($CONTAINER_NAME)..." + + # Создаем временный файл с data source для получения ID контейнера + cat > container_import.tf <<'EOF' + data "yandex_serverless_container" "existing_backend" { + name = "devops-portfolio-serverless-backend" + } + + output "container_id" { + value = try(data.yandex_serverless_container.existing_backend.id, "") + } + EOF + + # Инициализируем Terraform снова для загрузки data source + terraform init -upgrade >/dev/null 2>&1 || true + + # Пробуем получить ID контейнера через terraform apply для data source и output + # Игнорируем ошибки если контейнер не найден + terraform apply -auto-approve -target=data.yandex_serverless_container.existing_backend -target=output.container_id >/dev/null 2>&1 || true + CONTAINER_ID=$(terraform output -raw container_id 2>/dev/null || echo "") + + # Удаляем временный файл + rm -f container_import.tf + + if [ -n "$CONTAINER_ID" ] && [ "$CONTAINER_ID" != "null" ] && [ "$CONTAINER_ID" != "" ]; then + echo " Found container ID: $CONTAINER_ID" + if terraform import yandex_serverless_container.backend "$CONTAINER_ID" 2>&1; then + echo " ✓ Successfully imported serverless container" + else + echo " ℹ️ Import failed (container may already be in state or name mismatch)" + fi + else + echo " ℹ️ Container not found via data source (may not exist or need different approach)" + echo " Note: If you see 'Container already exists' error later, container exists but needs manual import with ID" + + # Если контейнер не импортировался, пробуем удалить его + echo "🗑️ Attempting to delete existing serverless container ($CONTAINER_NAME) if it exists..." + if yc serverless container get --name "$CONTAINER_NAME" --format json >/dev/null 2>&1; then + CONTAINER_ID_FOR_DELETE=$(yc serverless container get --name "$CONTAINER_NAME" --format json 2>/dev/null | jq -r '.id // empty' || echo "") + if [ -n "$CONTAINER_ID_FOR_DELETE" ]; then + echo " Found container ID: $CONTAINER_ID_FOR_DELETE, deleting..." + yc serverless container delete --id "$CONTAINER_ID_FOR_DELETE" 2>&1 && echo " ✓ Successfully deleted container" || echo " ✗ Failed to delete container (may have dependencies)" + fi + else + echo " ℹ️ Container not found (may not exist or yc auth failed)" + fi + fi + if [ -z "$CONTAINER_ID" ] || [ "$CONTAINER_ID" = "null" ] || [ "$CONTAINER_ID" = "" ]; then + echo "🗑️ Attempting to delete existing serverless container ($CONTAINER_NAME) if it exists..." + if yc serverless container get --name "$CONTAINER_NAME" --format json >/dev/null 2>&1; then + CONTAINER_ID_FOR_DELETE=$(yc serverless container get --name "$CONTAINER_NAME" --format json 2>/dev/null | jq -r '.id // empty' || echo "") + if [ -n "$CONTAINER_ID_FOR_DELETE" ]; then + echo " Found container ID: $CONTAINER_ID_FOR_DELETE, deleting..." + yc serverless container delete --id "$CONTAINER_ID_FOR_DELETE" 2>&1 && echo " ✓ Successfully deleted container" || echo " ✗ Failed to delete container (may have dependencies)" + fi + else + echo " ℹ️ Container not found (may not exist or yc auth failed)" + fi + fi + + - name: Terraform Apply + working-directory: infrastructure/serverless + env: + YC_TOKEN: ${{ secrets.YC_IAM_TOKEN }} + TF_INPUT: "false" + run: | + # Пробуем terraform apply + set +e # Не прерывать выполнение при ошибке + terraform apply -auto-approve 2>&1 | tee /tmp/terraform-apply.log + APPLY_EXIT_CODE=${PIPESTATUS[0]} + set -e + + if [ $APPLY_EXIT_CODE -eq 0 ]; then + echo "✓ Terraform apply completed successfully" + else + # Проверяем, была ли ошибка "Duplicate name" для API Gateway + if grep -q "Duplicate name.*devops-portfolio-serverless-api" /tmp/terraform-apply.log || \ + grep -q "yandex_api_gateway.backend.*Duplicate name" /tmp/terraform-apply.log; then + echo "" + echo "⚠️ API Gateway already exists (Duplicate name error)" + echo "ℹ️ This is expected - API Gateway exists but Terraform import is not supported" + echo "ℹ️ Continuing with other resources..." + echo "TODO: ТРЕБУЕТСЯ ПРАВКА - автоматическое обнаружение существующего API Gateway" + echo "" + + # Пробуем применить остальные ресурсы, исключая API Gateway + # Используем terraform apply с явным указанием ресурсов через -target + echo "Applying resources excluding API Gateway..." + terraform apply -auto-approve \ + -target=yandex_iam_service_account.serverless \ + -target=yandex_iam_service_account.static_site \ + -target=yandex_iam_service_account_static_access_key.static_site \ + -target=yandex_resourcemanager_folder_iam_member.serverless_admin \ + -target=yandex_resourcemanager_folder_iam_member.cr_puller \ + -target=yandex_resourcemanager_folder_iam_member.static_site_storage_editor \ + -target=yandex_container_registry_iam_binding.registry_puller \ + -target=yandex_serverless_container.backend \ + -target=yandex_serverless_container_iam_binding.backend_public \ + -target=yandex_storage_bucket.static_site 2>&1 || { + echo "⚠️ Some resources may have failed, but continuing..." + } + + echo "✓ Completed apply (API Gateway skipped as it already exists)" + else + echo "✗ Terraform apply failed with error (not related to API Gateway)" + echo "Error output:" + cat /tmp/terraform-apply.log + exit $APPLY_EXIT_CODE + fi + fi + + - name: Save Terraform Outputs + working-directory: infrastructure/serverless + run: terraform output -json > terraform-outputs.json + + - name: Upload Terraform artifacts + uses: actions/upload-artifact@v4 + with: + name: terraform-state + path: infrastructure/serverless/ + retention-days: 1 + build-frontend: + name: Build and Deploy Frontend + runs-on: ubuntu-latest + needs: deploy-infra + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download Terraform artifacts + uses: actions/download-artifact@v4 + with: + name: terraform-state + path: infrastructure/serverless/ + + - name: Install Yandex Cloud CLI + run: | + curl https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash + export PATH="$PATH:$HOME/yandex-cloud/bin" + echo "$HOME/yandex-cloud/bin" >> $GITHUB_PATH + yc --version + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Extract Terraform outputs + working-directory: infrastructure/serverless + env: + YC_TOKEN: ${{ secrets.YC_IAM_TOKEN }} + YC_FOLDER_ID: ${{ secrets.YC_FOLDER_ID }} + YC_CLOUD_ID: ${{ secrets.YC_CLOUD_ID }} + run: | + # Добавляем путь к yc CLI в PATH (если установлен в предыдущем шаге) + export PATH="$PATH:$HOME/yandex-cloud/bin" + + export STATIC_BUCKET_NAME=$(jq -r '.static_bucket_name.value' terraform-outputs.json) + export AWS_ACCESS_KEY_ID=$(jq -r '.static_site_access_key.value' terraform-outputs.json) + export AWS_SECRET_ACCESS_KEY=$(jq -r '.static_site_secret_key.value' terraform-outputs.json) + + # Получаем API Gateway endpoint (может быть null если не создан) + API_GATEWAY_ENDPOINT_RAW=$(jq -r '.api_gateway_endpoint.value' terraform-outputs.json) + + # Проверяем, что это не null и не пустая строка + if [ "$API_GATEWAY_ENDPOINT_RAW" != "null" ] && [ -n "$API_GATEWAY_ENDPOINT_RAW" ]; then + API_GATEWAY_ENDPOINT="$API_GATEWAY_ENDPOINT_RAW" + echo "✓ API Gateway endpoint from Terraform: $API_GATEWAY_ENDPOINT" + else + echo "ℹ️ API Gateway endpoint not in Terraform output (may already exist)" + echo "Attempting to get API Gateway endpoint via yc CLI..." + + # Устанавливаем yc CLI если еще не установлен + if ! command -v yc &> /dev/null; then + curl -s https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash >/dev/null 2>&1 + export PATH="$PATH:$HOME/yandex-cloud/bin" + fi + + # Настраиваем авторизацию + export YC_TOKEN="$YC_TOKEN" + yc config set token "$YC_TOKEN" || true + yc config set folder-id "$YC_FOLDER_ID" || true + yc config set cloud-id "$YC_CLOUD_ID" || true + + # Проверяем авторизацию + if ! yc iam service-account list --format json >/dev/null 2>&1; then + echo "⚠️ yc authentication failed, trying alternative method..." + # Пробуем через переменные окружения + export YC_TOKEN="$YC_TOKEN" + fi + + # Пробуем получить endpoint существующего API Gateway + API_GATEWAY_NAME="devops-portfolio-serverless-api" + echo "Looking for API Gateway: $API_GATEWAY_NAME" + + # Сначала пробуем через get по имени + API_GATEWAY_DOMAIN=$(yc serverless api-gateway get --name "$API_GATEWAY_NAME" --format json 2>&1 | jq -r '.domain // empty' || echo "") + + # Если не получилось, пробуем через list и фильтрацию + if [ -z "$API_GATEWAY_DOMAIN" ] || [ "$API_GATEWAY_DOMAIN" = "null" ]; then + echo "Trying to list all API Gateways..." + API_GATEWAY_DOMAIN=$(yc serverless api-gateway list --format json 2>&1 | jq -r ".[] | select(.name == \"$API_GATEWAY_NAME\") | .domain // empty" || echo "") + fi + + if [ -n "$API_GATEWAY_DOMAIN" ] && [ "$API_GATEWAY_DOMAIN" != "null" ] && [ "$API_GATEWAY_DOMAIN" != "" ]; then + API_GATEWAY_ENDPOINT="https://${API_GATEWAY_DOMAIN}" + echo "✓ Found existing API Gateway endpoint: $API_GATEWAY_ENDPOINT" + else + echo "✗ Could not get API Gateway endpoint" + echo " API Gateway may not exist or yc authentication failed" + echo " Attempting to use backend URL directly..." + # Пробуем использовать backend URL напрямую + BACKEND_URL=$(jq -r '.backend_url.value' terraform-outputs.json) + if [ -n "$BACKEND_URL" ] && [ "$BACKEND_URL" != "null" ]; then + API_GATEWAY_ENDPOINT="$BACKEND_URL" + echo "⚠️ Using backend URL as fallback: $API_GATEWAY_ENDPOINT" + else + echo "✗ No API Gateway endpoint available and no backend URL found" + exit 1 + fi + fi + fi + + echo "API_GATEWAY_ENDPOINT=${API_GATEWAY_ENDPOINT}" >> $GITHUB_ENV + echo "STATIC_BUCKET_NAME=${STATIC_BUCKET_NAME}" >> $GITHUB_ENV + echo "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" >> $GITHUB_ENV + echo "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" >> $GITHUB_ENV + echo "API_GATEWAY_ENDPOINT=${API_GATEWAY_ENDPOINT}" + echo "STATIC_BUCKET_NAME=${STATIC_BUCKET_NAME}" + + - name: Build and deploy frontend + working-directory: CI + env: + API_GATEWAY_ENDPOINT: ${{ env.API_GATEWAY_ENDPOINT }} + STATIC_BUCKET_NAME: ${{ env.STATIC_BUCKET_NAME }} + AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }} + YC_IAM_TOKEN: ${{ secrets.YC_IAM_TOKEN }} + YC_FOLDER_ID: ${{ secrets.YC_FOLDER_ID }} + YC_CLOUD_ID: ${{ secrets.YC_CLOUD_ID }} + run: | + chmod +x build_frontend.sh + ./build_frontend.sh + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ad0c08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +dist/ +dist-ssr/ +*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Environment +.env +.env.local +.env.*.local + +# Logs +*.log +logs/ + +# OS +Thumbs.db + +# Terraform artifacts (infrastructure) +infrastructure/**/.terraform/ +infrastructure/**/*.tfstate +infrastructure/**/*.tfstate.* +infrastructure/**/crash.log +infrastructure/**/.terraform.lock.hcl + +# Terraform secrets (облако) +infrastructure/**/terraform.tfvars +infrastructure/**/*.auto.tfvars + diff --git a/CI/CHECK_REGISTRY.md b/CI/CHECK_REGISTRY.md new file mode 100644 index 0000000..1877907 --- /dev/null +++ b/CI/CHECK_REGISTRY.md @@ -0,0 +1,124 @@ +# Проверка Container Registry + +## Быстрая проверка + +### 1. Список реестров + +```bash +yc container registry list +``` + +Вывод должен показать ваш реестр с ID (например, `crp50gpc30l3tbd4rtj0`). + +### 2. Список образов в реестре + +```bash +# Замените REGISTRY_ID на ваш ID реестра +yc container image list --registry-id crp50gpc30l3tbd4rtj0 +``` + +Или по имени реестра: + +```bash +yc container image list --registry-name devops-portfolio +``` + +### 3. Детальная информация об образе + +```bash +# По ID образа +yc container image get --id crpjf10o8ke35dcsh20k + +# Или по имени и тегу +yc container image get --name crp50gpc30l3tbd4rtj0/backend:latest +``` + +### 4. Проверка доступа через Docker + +**Важно**: Если у вас настроен credential helper для `yc`, нужно либо отключить его, либо использовать другой профиль. + +#### Вариант 1: Отключить credential helper для cr.yandex + +```bash +# Проверить текущую конфигурацию +cat ~/.docker/config.json + +# Если есть credential helper для cr.yandex, можно временно отключить его +# или использовать другой профиль +``` + +#### Вариант 2: Использовать IAM токен напрямую + +```bash +# Создать IAM токен +TOKEN=$(yc iam create-token) + +# Логин (может не работать, если включен credential helper) +echo "$TOKEN" | docker login --username iam --password-stdin cr.yandex +``` + +#### Вариант 3: Настроить credential helper правильно + +```bash +# Настроить Docker для работы с Yandex Container Registry +yc container registry configure-docker +``` + +### 5. Проверка pull образа + +```bash +# Попробовать скачать образ +docker pull cr.yandex/crp50gpc30l3tbd4rtj0/backend:latest +``` + +### 6. Проверка push образа + +```bash +# Создать тестовый образ +docker tag cr.yandex/crp50gpc30l3tbd4rtj0/test:latest + +# Попробовать загрузить +docker push cr.yandex/crp50gpc30l3tbd4rtj0/test:latest +``` + +## Текущее состояние вашего реестра + +- **Registry ID**: `crp50gpc30l3tbd4rtj0` +- **Registry Name**: `devops-portfolio` +- **Образы**: + - `backend:latest` (59.0 MB) + - `frontend:latest` (23.0 MB) + +## Проверка прав доступа + +```bash +# Проверить, какие роли назначены вашему аккаунту +yc container registry list-access-bindings --id crp50gpc30l3tbd4rtj0 +``` + +## Устранение проблем + +### Ошибка: "unauthorized" + +1. Проверьте, что IAM токен действителен (не истёк) +2. Проверьте права доступа к реестру +3. Убедитесь, что используете правильный Registry ID + +### Ошибка: "credential helper" + +Если Docker использует credential helper от `yc`, можно: +1. Временно отключить его в `~/.docker/config.json` +2. Использовать другой профиль: `yc container registry configure-docker --profile ` +3. Использовать переменные окружения вместо credential helper + +### Проверка токена + +```bash +# Создать новый токен +yc iam create-token + +# Проверить, что токен работает +yc config set token $(yc iam create-token) +yc container registry list +``` + diff --git a/CI/README.md b/CI/README.md new file mode 100644 index 0000000..b4c6b45 --- /dev/null +++ b/CI/README.md @@ -0,0 +1,58 @@ +# CI Scripts + +В каталоге собраны вспомогательные скрипты, которые используются для ручного запуска или в GitHub Actions workflow для подготовки артефактов перед Terraform. + +> **📋 Перед использованием CI/CD**: Настройте секреты в GitHub согласно инструкции в [SECRETS.md](SECRETS.md) + +> **📝 GitHub Actions workflow**: Файл `.github/workflows/ci.yml` содержит конфигурацию автоматического CI/CD пайплайна. + +> **🔧 Проблемы?**: См. [TROUBLESHOOTING.md](TROUBLESHOOTING.md) для решения частых проблем. + +## `build_backend.sh` + +Собирает backend-образ и публикует его в Yandex Container Registry. + +Переменные окружения (обязательные): + +| Переменная | Описание | +|-------------------|----------| +| `REGISTRY_ID` | ID Container Registry (например, `crp50gpc30l3tbd4rtj0`). | +| `IMAGE_TAG` | Тег образа (`latest`, `dev`, `$CI_COMMIT_SHORT_SHA`). | +| `YC_OAUTH_TOKEN` | OAuth/IAM токен, полученный через `yc iam create-token`. | + +Пример локального запуска: + +```bash +export REGISTRY_ID=crp50gpc30l3tbd4rtj0 +export IMAGE_TAG=dev +export YC_OAUTH_TOKEN=$(yc iam create-token) +./CI/build_backend.sh +``` + +В GitHub Actions workflow переменные настраиваются автоматически через секреты (`REGISTRY_ID`, `YC_OAUTH_TOKEN`). `IMAGE_TAG` по умолчанию использует `${{ github.sha }}` (полный хеш коммита). + +## `build_frontend.sh` + +Собирает React/Vite фронтенд и выгружает готовый `dist/` в Object Storage (Yandex S3). + +Переменные окружения (обязательные): + +| Переменная | Описание | +|-------------------------|----------| +| `STATIC_BUCKET_NAME` | Имя бакета (например, `kulibin-devops-portfolio`). | +| `API_GATEWAY_ENDPOINT` | URL API Gateway, который будет подставлен в `VITE_API_BASE_URL`. | +| `AWS_ACCESS_KEY_ID` | Access key для Object Storage (из Terraform output). | +| `AWS_SECRET_ACCESS_KEY` | Secret key для Object Storage (из Terraform output). | + +Пример запуска: + +```bash +export STATIC_BUCKET_NAME=kulibin-devops-portfolio +export API_GATEWAY_ENDPOINT=https://d5dm4d9170q82do7f5m8.lievo6ut.apigw.yandexcloud.net +export AWS_ACCESS_KEY_ID=$(terraform -chdir=infrastructure/serverless output -raw static_site_access_key) +export AWS_SECRET_ACCESS_KEY=$(terraform -chdir=infrastructure/serverless output -raw static_site_secret_key) +./CI/build_frontend.sh +``` + +В GitHub Actions workflow переменные (`STATIC_BUCKET_NAME`, `API_GATEWAY_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) автоматически извлекаются из Terraform outputs. Скрипт выполняет `npm ci`, `npm run build` и `yc storage s3 cp --recursive dist/ ...`. + diff --git a/CI/SECRETS.md b/CI/SECRETS.md new file mode 100644 index 0000000..9861af8 --- /dev/null +++ b/CI/SECRETS.md @@ -0,0 +1,184 @@ +# Настройка секретов для CI/CD + +Эта инструкция описывает, как настроить переменные окружения (секреты) в GitHub Actions для автоматического развёртывания. + +## Необходимые переменные + +Для работы CI/CD пайплайна нужно настроить следующие секреты в GitHub: + +| Переменная | Описание | Тип | Где получить | +|------------|----------|-----|-------------| +| `YC_IAM_TOKEN` | IAM-токен для Docker login и Terraform | **Secret** | `yc iam create-token` (действителен 12 часов) | +| `REGISTRY_ID` | ID Container Registry в Yandex Cloud | **Variable** или **Secret** | Из `terraform.tfvars` или `yc container registry list` | +| `YC_CLOUD_ID` | ID облака в Yandex Cloud | **Secret** | `yc config list` (поле `cloud-id`) | +| `YC_FOLDER_ID` | ID каталога в Yandex Cloud | **Secret** | `yc config list` (поле `folder-id`) | +| `YC_ZONE` | Зона размещения ресурсов | **Variable** или **Secret** | Обычно `ru-central1-a` | +| `STATIC_BUCKET_NAME` | Имя бакета для статического фронтенда | **Variable** или **Secret** | Например, `kulibin-devops-portfolio` | + +> **⚠️ Важно**: Для Docker login в Yandex Container Registry **необходим IAM-токен** (получается через `yc iam create-token`). IAM-токен действителен только 12 часов, поэтому его нужно периодически обновлять в секретах GitHub. + +## Пошаговая инструкция + +### 1. Получение IAM-токена для Docker login + +Для Docker login в Yandex Container Registry нужен IAM-токен: + +```bash +# Создайте IAM-токен +yc iam create-token +``` + +**Скопируйте весь токен** (он начинается с `t1.` и заканчивается точкой). + +> **⚠️ ВАЖНО**: IAM-токен действителен только **12 часов**! После истечения нужно создать новый токен и обновить секрет в GitHub. + +### 2. Получение остальных значений + +```bash +# Посмотреть конфигурацию (cloud_id, folder_id) +yc config list + +# Посмотреть список реестров (registry_id) +yc container registry list + +# Зона обычно: ru-central1-a +# Имя бакета: например, kulibin-devops-portfolio +``` + +#### REGISTRY_ID + +```bash +# Посмотреть список реестров +yc container registry list + +# Или взять из terraform.tfvars +# container_registry_id = "crp50gpc30l3tbd4rtj0" +``` + +### 3. Добавление секретов в GitHub + +1. Откройте ваш репозиторий в GitHub +2. Перейдите в **Settings** (в верхнем меню репозитория) +3. В левом меню выберите **Secrets and variables** → **Actions** +4. Нажмите **New repository secret** (для секретов) или **New repository variable** (для обычных переменных) + +#### Для YC_IAM_TOKEN (Secret): + +1. Нажмите **New repository secret** +2. **Name**: `YC_IAM_TOKEN` +3. **Secret**: вставьте IAM-токен, полученный через `yc iam create-token` +4. Нажмите **Add secret** + +> **Примечание**: +> - В GitHub секреты автоматически скрыты в логах и не могут быть просмотрены после сохранения +> - IAM-токен действителен только 12 часов, после истечения нужно обновить секрет +> - Для автоматизации можно использовать сервисный аккаунт (см. раздел "Автоматизация обновления токена" ниже) + +#### Для остальных секретов: + +Добавьте следующие секреты/переменные: + +1. **REGISTRY_ID** (Variable или Secret): + - Значение: `crp50gpc30l3tbd4rtj0` (ваш Registry ID) + +2. **YC_CLOUD_ID** (Secret): + - Значение: из `yc config list` (поле `cloud-id`) + +3. **YC_FOLDER_ID** (Secret): + - Значение: из `yc config list` (поле `folder-id`) + +4. **YC_ZONE** (Variable или Secret): + - Значение: `ru-central1-a` (или другая зона) + +5. **STATIC_BUCKET_NAME** (Variable или Secret): + - Значение: `kulibin-devops-portfolio` (или другое имя бакета) + +### 4. Проверка настроек + +После добавления секретов они будут доступны в GitHub Actions workflows. Проверьте, что секреты правильно используются: + +- `YC_IAM_TOKEN` используется в: + - `build-backend` job (для `docker login` в Yandex Container Registry) +- `REGISTRY_ID` используется в: + - `build-backend` job (для формирования пути к образу) + +## Автоматические переменные + +Следующие переменные настраивать не нужно — они автоматически доступны в GitHub Actions: + +- `GITHUB_SHA` — полный хеш коммита (можно использовать `${{ github.sha }}` или `${{ github.sha }}` с обрезкой) +- `GITHUB_REF_NAME` — имя ветки или тега +- `GITHUB_REPOSITORY` — имя репозитория + +Для получения короткого хеша коммита (аналог `CI_COMMIT_SHORT_SHA`) используйте: +```yaml +IMAGE_TAG: ${{ github.sha }} +# или для короткого хеша (первые 7 символов): +IMAGE_TAG: ${{ github.sha }} +``` + +## Безопасность + +### Рекомендации: + +1. **YC_OAUTH_TOKEN**: + - Используйте сервисный аккаунт для production + - Настройте автоматическое обновление токена (через cron или внешний сервис) + - Или используйте статический ключ сервисного аккаунта (но это менее безопасно) + +2. **Защита секретов**: + - В GitHub секреты автоматически скрыты в логах + - Можно использовать **Environment secrets** для разделения секретов по окружениям (dev, staging, production) + - Для ограничения доступа используйте **Environment protection rules** в настройках окружения + +3. **Ограничение доступа**: + - Используйте **Environments** (Settings → Environments) для разделения секретов по окружениям + - Настройте **Environment protection rules** для ограничения доступа к секретам по веткам + +## Альтернатива: использование IAM-токена (только для Terraform) + +> **⚠️ Внимание**: IAM-токен **не работает** для Docker login в Yandex Container Registry! Используйте только access key. + +Если вы хотите использовать IAM-токен только для Terraform (не для Docker), можно добавить дополнительный секрет: + +1. Создайте IAM-токен: +```bash +yc iam create-token +``` + +2. Добавьте секрет `YC_TOKEN` в GitHub (опционально, если хотите использовать токен для terraform) + +3. Обновите workflow, чтобы использовать `YC_TOKEN` для terraform вместо access key. + +Но для Docker login **обязательно** нужен access key! + +## Устранение проблем + +### Ошибка: "YC_IAM_TOKEN is not set" + +Проверьте: +1. Секрет добавлен в Settings → Secrets and variables → Actions → Secrets +2. Правильное имя секрета (чувствительно к регистру): `YC_IAM_TOKEN` +3. В workflow файле секрет правильно используется: `${{ secrets.YC_IAM_TOKEN }}` + +### Ошибка: "unauthorized: Password is invalid - must be IAM token" при docker push + +Эта ошибка означает, что: +1. Токен истёк (IAM-токен действителен только 12 часов) +2. Токен вставлен неправильно (возможно, есть лишние пробелы) + +Решение: +1. Создайте новый IAM-токен: `yc iam create-token` +2. Обновите секрет `YC_IAM_TOKEN` в GitHub +3. Перезапустите workflow + +### Ошибка: "REGISTRY_ID is not set" + +Проверьте: +1. Переменная или секрет `REGISTRY_ID` добавлен в Settings → Secrets and variables → Actions +2. В workflow файле правильно используется: `${{ vars.REGISTRY_ID }}` (если переменная) или `${{ secrets.REGISTRY_ID }}` (если секрет) + +### Как просмотреть секреты в GitHub? + +**Важно**: После сохранения секрета в GitHub его нельзя просмотреть. Можно только обновить или удалить. Это сделано для безопасности. + diff --git a/CI/TROUBLESHOOTING.md b/CI/TROUBLESHOOTING.md new file mode 100644 index 0000000..9171fcd --- /dev/null +++ b/CI/TROUBLESHOOTING.md @@ -0,0 +1,152 @@ +# Устранение проблем CI/CD + +## Проблема: "unauthorized: Password is invalid - must be OAuth token" + +Эта ошибка означает, что токен `YC_OAUTH_TOKEN` неверный или истёк. + +### Причины: + +1. **Токен истёк** — IAM-токены действительны только **12 часов** +2. **Токен вставлен неправильно** — возможно, есть лишние пробелы или символы +3. **Секрет не сохранён** — секрет не был правильно добавлен в GitHub + +### Решение: + +#### 1. Проверьте секрет в GitHub: + +1. Откройте репозиторий в GitHub +2. Перейдите в **Settings** → **Secrets and variables** → **Actions** +3. Найдите секрет `YC_OAUTH_TOKEN` +4. Проверьте, что он существует и правильно назван (чувствительно к регистру!) + +#### 2. Обновите токен: + +```bash +# Создайте новый IAM-токен +yc iam create-token +``` + +Скопируйте **весь** токен (он начинается с `t1.` и заканчивается точкой). + +#### 3. Обновите секрет в GitHub: + +1. В GitHub: **Settings** → **Secrets and variables** → **Actions** +2. Найдите `YC_OAUTH_TOKEN` +3. Нажмите **Update** (или удалите и создайте заново) +4. Вставьте **новый** токен (без пробелов в начале/конце) +5. Нажмите **Update secret** + +#### 4. Перезапустите workflow: + +1. Перейдите в **Actions** вкладку +2. Найдите упавший workflow +3. Нажмите **Re-run all jobs** + +### Альтернатива: Использование сервисного аккаунта + +Для production рекомендуется использовать сервисный аккаунт со статическим ключом (не истекает): + +```bash +# Создать сервисный аккаунт +yc iam service-account create --name github-actions + +# Создать статический ключ +yc iam access-key create --service-account-name github-actions + +# Выдаст: +# access_key_id: +# secret: # Сохраните это! +``` + +Затем в GitHub добавьте секреты: +- `YC_ACCESS_KEY_ID` = `` +- `YC_SECRET_ACCESS_KEY` = `` + +И обновите workflow для использования этих переменных (потребуется изменить скрипты). + +--- + +## Как поделиться логами для диагностики + +### Вариант 1: Скопировать полный лог + +1. В GitHub перейдите в **Actions** +2. Откройте упавший workflow run +3. Откройте упавший job (например, "Build and Push Backend") +4. Нажмите на любой step, чтобы развернуть лог +5. Нажмите кнопку **...** (три точки) в правом верхнем углу лога +6. Выберите **Download log** или **Copy log** +7. Вставьте в сообщение (можно сократить, оставив только ошибки) + +### Вариант 2: Скопировать только ошибки + +1. В логе найдите строки, начинающиеся с `##[error]` +2. Скопируйте их вместе с несколькими строками контекста до и после +3. Вставьте в сообщение + +### Вариант 3: Скриншот + +Сделайте скриншот секции с ошибкой в логе. + +--- + +## Другие частые проблемы + +### Ошибка: "REGISTRY_ID is not set" + +**Решение**: Проверьте, что секрет `REGISTRY_ID` добавлен в GitHub: +- Settings → Secrets and variables → Actions → Secrets (или Variables) + +### Ошибка: "terraform: command not found" + +**Решение**: Это не должно происходить, так как мы используем `hashicorp/setup-terraform@v3`. Если возникает, проверьте версию workflow файла. + +### Ошибка: "yc: command not found" + +**Решение**: Проверьте, что шаг "Install Yandex Cloud CLI" выполнился успешно. Если нет, проверьте логи этого шага. + +### Ошибка при terraform apply + +**Решение**: +1. Проверьте, что `YC_TOKEN` (секрет `YC_OAUTH_TOKEN`) действителен +2. Проверьте, что terraform state не повреждён +3. Попробуйте выполнить `terraform init` и `terraform apply` локально для диагностики + +--- + +## Отладка локально + +Вы можете запустить скрипты локально для проверки: + +```bash +# Проверка backend скрипта +export REGISTRY_ID=crp50gpc30l3tbd4rtj0 +export IMAGE_TAG=test +export YC_OAUTH_TOKEN=$(yc iam create-token) +./CI/build_backend.sh + +# Проверка frontend скрипта (после terraform apply) +cd infrastructure/serverless +export API_GATEWAY_ENDPOINT=$(terraform output -raw api_gateway_endpoint) +export STATIC_BUCKET_NAME=$(terraform output -raw static_bucket_name) +export AWS_ACCESS_KEY_ID=$(terraform output -raw static_site_access_key) +export AWS_SECRET_ACCESS_KEY=$(terraform output -raw static_site_secret_key) +cd ../.. +./CI/build_frontend.sh +``` + +--- + +## Проверка токена + +Чтобы проверить, действителен ли токен: + +```bash +# Используйте токен для запроса к Yandex Cloud API +export YC_TOKEN="ваш_токен" +yc config set token $YC_TOKEN +yc container registry list +``` + +Если команда выполняется успешно, токен действителен. Если нет — создайте новый. + diff --git a/CI/build_backend.sh b/CI/build_backend.sh new file mode 100755 index 0000000..79eff39 --- /dev/null +++ b/CI/build_backend.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Скрипт для сборки backend-образа и публикации его в Yandex Container Registry. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +main() { + : "${REGISTRY_ID:?Нужно указать REGISTRY_ID (например, crp50gpc30l3tbd4rtj0)}" + : "${IMAGE_TAG:?Нужно указать IMAGE_TAG (например, latest или \$CI_COMMIT_SHORT_SHA)}" + : "${YC_IAM_TOKEN:?Нужно указать YC_IAM_TOKEN (yc iam create-token)}" + + local image="cr.yandex/${REGISTRY_ID}/backend:${IMAGE_TAG}" + + echo "==> Авторизация в YCR через IAM токен" + echo "${YC_IAM_TOKEN}" | docker login --username iam --password-stdin cr.yandex + + echo "==> Сборка backend-образа: ${image}" + docker build -t "${image}" "${REPO_ROOT}/backend" + + echo "==> Публикация образа" + docker push "${image}" + + echo "Готово: ${image}" +} + +main "$@" + diff --git a/CI/build_frontend.sh b/CI/build_frontend.sh new file mode 100755 index 0000000..8c157be --- /dev/null +++ b/CI/build_frontend.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Собирает фронтенд (React/Vite) и выгружает dist/ в Object Storage. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +main() { + : "${STATIC_BUCKET_NAME:?Нужно указать STATIC_BUCKET_NAME (например, kulibin-devops-portfolio)}" + : "${API_GATEWAY_ENDPOINT:?Нужно указать API_GATEWAY_ENDPOINT (например, https://....apigw.yandexcloud.net)}" + : "${AWS_ACCESS_KEY_ID:?Нужно указать access key (из Terraform output)}" + : "${AWS_SECRET_ACCESS_KEY:?Нужно указать secret key (из Terraform output)}" + + cd "${REPO_ROOT}/frontend" + + echo "==> Устанавливаем зависимости" + npm ci + + echo "==> Собираем фронтенд (VITE_API_BASE_URL=${API_GATEWAY_ENDPOINT})" + VITE_API_BASE_URL="${API_GATEWAY_ENDPOINT}" npm run build + + echo "==> Настраиваем авторизацию yc для Object Storage" + if [ -n "${YC_IAM_TOKEN:-}" ]; then + export YC_TOKEN="${YC_IAM_TOKEN}" + yc config set token "${YC_IAM_TOKEN}" || true + yc config set folder-id "${YC_FOLDER_ID:-}" || true + yc config set cloud-id "${YC_CLOUD_ID:-}" || true + else + echo "⚠️ YC_IAM_TOKEN не установлен, пробуем использовать существующую конфигурацию yc" + fi + + echo "==> Загружаем dist/ в Object Storage (s3://${STATIC_BUCKET_NAME})" + + # Функция для определения Content-Type по расширению + get_content_type() { + local file="$1" + case "${file##*.}" in + html) echo "text/html; charset=utf-8" ;; + css) echo "text/css; charset=utf-8" ;; + js) echo "application/javascript; charset=utf-8" ;; + json) echo "application/json; charset=utf-8" ;; + png) echo "image/png" ;; + jpg|jpeg) echo "image/jpeg" ;; + svg) echo "image/svg+xml" ;; + ico) echo "image/x-icon" ;; + woff) echo "font/woff" ;; + woff2) echo "font/woff2" ;; + ttf) echo "font/ttf" ;; + eot) echo "application/vnd.ms-fontobject" ;; + map) echo "application/json" ;; + *) echo "text/plain" ;; + esac + } + + # Загружаем все файлы из dist/ с правильными Content-Type + cd dist + find . -type f | while read -r file; do + # Убираем ведущую точку и слэш + key="${file#./}" + content_type=$(get_content_type "$file") + + echo "Загружаем: $key (Content-Type: $content_type)" + yc storage s3api put-object \ + --bucket "${STATIC_BUCKET_NAME}" \ + --key "$key" \ + --body "$file" \ + --content-type "$content_type" \ + --acl public-read + done + + echo "Готово: фронтенд обновлён" +} + +main "$@" + diff --git a/DevOpsPortfolio.code-workspace b/DevOpsPortfolio.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/DevOpsPortfolio.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/FinOps.md b/FinOps.md new file mode 100644 index 0000000..5ca9aa0 --- /dev/null +++ b/FinOps.md @@ -0,0 +1,62 @@ +# FinOps: стратегии снижения стоимости в Yandex Cloud + +Цель — держать витринный проект доступным 24/7, но платить только за реально используемые ресурсы, сохранив возможность масштабировать решение до корпоративного портала. + +## 1. Базовые сервисы и текущие расходы + +| Компонент | Назначение | Стоимость (оценка) | +|-----------|------------|--------------------| +| Yandex Container Registry | Хранение образов frontend/backend | ₽ до ~120 ₽/мес при 5–10 образах | +| Serverless Containers | Backend/Frontend API | ₽ по микросекундам; при низкой нагрузке → копейки | +| Terraform state/storage | Управление IaC | Бесплатно (локально) | + +Узкое место — отсутствие постоянного домена/прокси. Прямой доступ к контейнерам даёт рандомный URL и усложняет деплой. + +## 2. Варианты «front door» + +### 2.1 Object Storage + Cloud CDN + API Gateway (текущий план) +* **Frontend**: React билд лежит в бакете (`kulibin-devops-portfolio.website.yandexcloud.net`), при необходимости навешиваем CDN/домен. +* **Backend**: Yandex API Gateway проксирует `https://api.example.ru` → serverless backend. +* **Плюсы**: минимальная стоимость, полностью serverless, легко скейлиться. +* **Минусы**: две сущности (CDN + Gateway), нужно продумать CORS/авторизацию. + +### 2.2 Application Load Balancer +* **Плюсы**: единый домен и маршрутизация (`/` и `/api/*`), строгий контроль доступа. +* **Минусы**: фиксированная абонплата ~1.5–2 ₽/час (~1100–1500 ₽/мес) даже без трафика. + +### 2.3 Network Load Balancer + собственный прокси +* **Плюсы**: дешевле ALB (₽1/час), можно держать прокси в serverless container/VM. +* **Минусы**: нет L7 маршрутизации, нужен отдельный прокси-слой, больше ручной поддержки. + +### 2.4 Постоянный «прокси-контейнер» +* **Плюсы**: почти бесплатен, гибок. +* **Минусы**: придётся самим управлять конфигами, сертификатами, мониторингом. Подходит только как временное решение. + +## 3. Стоимость ключевых блоков + +| Ресурс | Примерная цена | +|--------|----------------| +| Домен .ru | 200–400 ₽/год | +| Certificate Manager (публичные LE) | 0 ₽ | +| Object Storage (10 ГБ, 100 ГБ трафика) | <50 ₽/мес | +| Cloud CDN (100 ГБ трафика) | ~15–20 ₽/мес | +| API Gateway | ~0.4 ₽ за 1 М вызовов + милисекунды исполнения | +| Application Load Balancer | 1.5–2 ₽/час + трафик | +| Network Load Balancer | ~1 ₽/час + трафик | + +## 4. Миграция к enterprise-уровню + +1. Текущий шаг: Object Storage + CDN + API Gateway, frontend container выключен (можно вернуть, если понадобится). +2. При росте нагрузки: + - Перевести backend за Application Load Balancer (тот же домен). + - Включить WAF/логирование. + - Оставить Object Storage + CDN либо перенести фронт в контейнеры/VM. +3. Для приватных сервисов (например, YDB или внутренние API) — использовать ALB/Service Connect + VPC. + +## 5. Практические рекомендации + +- Вся IaC собрана в `infrastructure/serverless`: backend контейнер, статический бакет и API Gateway управляются из одного Terraform state. +- Frontend container оставлен опциональным (флаг `enable_frontend_container`), но по умолчанию используем Object Storage + API Gateway. +- Секреты/URL backend — через Terraform outputs → Terraform Cloud/Secret Manager → API Gateway. +- Мониторим реальные затраты через Yandex Cost Management и фиксируем значения в FinOps.md после каждого релиза. + diff --git a/README.md b/README.md index a3ca3fa..18dee6f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,295 @@ -# DevOpsPortfolio -Проект странички-портфолио для демонстрации навыков +# DevOps Portfolio + +Проект сайта-визитки для демонстрации DevOps навыков. Включает полный стек технологий: Kubernetes, CI/CD, мониторинг, GitOps и многое другое. + +## 🚀 Технологии + +- **Backend**: Python FastAPI +- **Frontend**: React + Vite +- **Архитектура**: Микросервисная (разделение фронтенда и бэкенда) + +## 📋 Структура проекта + +``` +DevOpsPortfolio/ +├── backend/ # FastAPI приложение +│ ├── app/ +│ │ ├── main.py # Главный файл приложения +│ │ ├── models.py # Pydantic модели +│ │ └── routers/ # API роутеры +│ ├── requirements.txt +│ └── run.py # Скрипт запуска +├── frontend/ # React приложение +│ ├── src/ +│ │ ├── components/ # React компоненты +│ │ ├── App.jsx +│ │ └── main.jsx +│ ├── package.json +│ └── vite.config.js +├── k8s/ # Kubernetes манифесты +│ └── README.md # Подробная документация по Kubernetes +├── devops-portfolio/ # Helm chart +│ └── README.md # Подробная документация по Helm +└── README.md # Этот файл +``` + +## 📚 Структура документации + +- **README.md** (этот файл) — общая информация и быстрый старт +- **[k8s/README.md](k8s/README.md)** — подробная документация по развертыванию через Kubernetes манифесты +- **[devops-portfolio/README.md](devops-portfolio/README.md)** — подробная документация по использованию Helm chart +- **[infrastructure/serverless/README.md](infrastructure/serverless/README.md)** — деплой backend + статического фронтенда (Object Storage + API Gateway) +- **[FinOps.md](FinOps.md)** — анализ вариантов инфраструктуры и стоимости владения + +## 🛠️ Установка и запуск + +### Предварительные требования + +- Python 3.9+ +- Node.js 18+ +- npm или yarn + +### Backend (FastAPI) + +1. Перейдите в директорию backend: +```bash +cd backend +``` + +2. Создайте виртуальное окружение: +```bash +python -m venv venv +source venv/bin/activate # Linux/Mac +# или +venv\Scripts\activate # Windows +``` + +3. Установите зависимости: +```bash +pip install -r requirements.txt +``` + +4. Запустите сервер: +```bash +python run.py +``` + +Backend будет доступен по адресу: http://localhost:8000 +API документация (Swagger): http://localhost:8000/docs + +### Frontend (React) + +1. Перейдите в директорию frontend: +```bash +cd frontend +``` + +2. Установите зависимости: +```bash +npm install +``` + +3. Запустите dev сервер: +```bash +npm run dev +``` + +Frontend будет доступен по адресу: http://localhost:3000 + +## 📡 API Endpoints + +- `GET /` - Корневой эндпоинт +- `GET /health` - Health check +- `GET /api/about` - Информация о себе +- `GET /api/skills` - Список навыков +- `GET /api/projects` - Список проектов +- `GET /api/contacts` - Контактная информация + +## 🐳 Контейнеризация приложения + +### Предварительные требования + +- Docker установлен и запущен +- Пользователь добавлен в группу `docker` (или используйте `sudo`) + +### Сборка Docker образов + +#### Предварительная загрузка базовых образов + +Перед сборкой рекомендуется предварительно загрузить базовые образы: + +```bash +# Загружаем базовые образы +docker pull node:20-alpine +docker pull python:3.13-slim +docker pull nginx:alpine +``` + +**Если загрузка образов из Docker Hub не работает:** + +Если у вас проблемы с загрузкой образов из основного репозитория Docker Hub (например, из-за блокировок), можно использовать удаленный сервер: + +1. **На удаленном сервере** (где загрузка работает): +```bash +# Загружаем образы +docker pull node:20-alpine +docker pull python:3.13-slim +docker pull nginx:alpine + +# Сохраняем образы в tar-файлы +docker save node:20-alpine -o node-20-alpine.tar +docker save python:3.13-slim -o python-3.13-slim.tar +docker save nginx:alpine -o nginx-alpine.tar +``` + +2. **Копируем файлы на локальную машину** (через scp, rsync или другой способ): +```bash +scp user@remote-server:/path/to/*.tar /home/user/ +``` + +3. **На локальной машине** загружаем образы: +```bash +docker load -i node-20-alpine.tar +docker load -i python-3.13-slim.tar +docker load -i nginx-alpine.tar + +# Проверяем загруженные образы +docker images +``` + +#### Сборка образов приложения + +После загрузки базовых образов можно собирать образы приложения: + +**Backend:** +```bash +cd backend +docker build -t devops-portfolio-backend:latest . +``` + +**Frontend:** +```bash +cd frontend +docker build -t devops-portfolio-frontend:latest . +``` + +**Проверка собранных образов:** +```bash +docker images | grep devops-portfolio +``` + +Ожидаемый результат: +``` +devops-portfolio-frontend latest ~53MB +devops-portfolio-backend latest ~173MB +``` + +#### Тестирование образов + +**Запуск backend:** +```bash +docker run -d -p 8000:8000 --name backend-test devops-portfolio-backend:latest +``` + +**Проверка работы:** +```bash +curl http://localhost:8000/health +curl http://localhost:8000/api/about +``` + +**Остановка и удаление тестового контейнера:** +```bash +docker stop backend-test +docker rm backend-test +``` +### Запуск через Docker Compose + +Для удобного запуска всего приложения используйте Docker Compose: + +**Запуск всех сервисов:** +```bash +cd /home/user/yandex/DevOpsPortfolio +docker-compose up -d +``` + +**Просмотр логов:** +```bash +# Все сервисы +docker-compose logs -f + +# Только backend +docker-compose logs -f backend + +# Только frontend +docker-compose logs -f frontend +``` + +**Проверка статуса:** +```bash +docker-compose ps +``` + +**Остановка сервисов:** +```bash +docker-compose down +``` + +**Пересборка образов:** +```bash +docker-compose build +docker-compose up -d +``` + +**Доступ к приложению:** +- Frontend: http://localhost +- Backend API: http://localhost:8000 +- API документация: http://localhost:8000/docs + +**Особенности конфигурации:** +- Frontend ждет готовности backend (health check) +- Сервисы общаются через внутреннюю сеть `portfolio-network` +- Backend доступен для frontend по универсальному имени `backend:8000` (работает во всех случаях развертывания) +- Автоматический перезапуск контейнеров при сбоях (`restart: unless-stopped`) + +## ☸️ Локальное развертывание в Kubernetes (minikube) + +Для развертывания приложения в локальном Kubernetes кластере используйте манифесты из каталога `k8s/`. + +**Быстрый старт:** +```bash +# Установка minikube и kubectl (Arch Linux) +sudo pacman -S kubectl minikube + +# Запуск minikube +minikube start --driver=docker + +# Развертывание приложения +kubectl apply -f k8s/ +``` + +**Подробные инструкции:** см. [k8s/README.md](k8s/README.md) + +## 📦 Управление развертыванием через Helm + +Helm — менеджер пакетов для Kubernetes, упрощающий развертывание и управление приложениями. + +**Быстрый старт:** +```bash +# Установка Helm chart +cd devops-portfolio +helm install devops-portfolio . +``` + +**Подробные инструкции:** см. [devops-portfolio/README.md](devops-portfolio/README.md) + +## 🎯 Roadmap + +См. [roadmap.md](roadmap.md) для полного списка задач проекта. + +## 📝 Лицензия + +MIT License - см. [LICENSE](LICENSE) + +## 👤 Автор + +UZver24 - [GitHub](https://github.com/UZver24) diff --git a/about.md b/about.md new file mode 100644 index 0000000..3a0e2a7 --- /dev/null +++ b/about.md @@ -0,0 +1,39 @@ +Есть вот-такой сайт-визитка: https://tosinonikute.com/ +Надо сделать его аналог и развернуть его в облаке, но при этом продемонстрировать навыки DevOps. То есть итог: сайт-визитка + его проект на GitHub, по которому можно оценить мой уровень. + +Cтек технологий, которые должен демонстрировать проект: + +## Критически важные (Must Have): +* Kubernetes - k8s (Pod, Deployment, Service, Ingress, ConfigMap, Secret, StatefulSet, DaemonSet) +* CI/CD пайплайны (GitHub Actions, GitLab CI, или Jenkins) +* образы, контейнеры, Dockerfile, Docker Compose +* Terraform (Infrastructure as Code) +* Мониторинг и метрики (Prometheus, Grafana) +* GitOps (ArgoCD, Flux) +* Helm (управление приложениями в k8s) +* Code style, принципы чистого кода (SOLID, DRY, KISS) + +## Очень важные (Should Have): +* Централизованное логирование (ELK Stack, Loki, Fluentd) +* DevSecOps ("Сдвиг влево", Управление секретами (HashiCorp Vault, AWS Secrets Manager), сканирование образов на уязвимости (Trivy, Grype), анализ кода (SAST)) +* стратегии деплоя (Blue-Green, Canary, Rolling Update) +* Multi-environment подход (dev, staging, production) +* Управление сертификатами (cert-manager, Let's Encrypt) +* OWASP Top 10 (безопасность приложений) +* Автоматизация тестирования в CI/CD (unit, integration, e2e тесты) +* minikube (для локального тестирования развёртывания) + +## Важные (Nice to Have): +* Ansible (конфигурационное управление) +* Облачные сервисы (AWS EKS/GKE/AKS, S3, CloudFront, Route53 или аналоги в GCP/Azure, Yandex Cloud) +* Контейнерные реестры (Harbor, AWS ECR, Google Container Registry, Yandex Container Registry) +* Базы данных и их управление (PostgreSQL, MongoDB, Redis) с backup стратегиями +* Disaster Recovery и backup стратегии +* Трейсинг (Jaeger или Zipkin) +* Service Mesh (Istio, Linkerd) - для продвинутого управления трафиком в k8s +* API Gateway (Kong, Ambassador, Traefik) + +## Дополнительные компетенции (Advanced): +* Platform Engineering и Internal Developer Platforms (IDP) +* FinOps (управление затратами на облачную инфраструктуру) +* Serverless и Container-as-a-Service (AWS Lambda, Yandex Cloud Functions) \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..a0cc37b --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,30 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +*.egg-info/ +.installed.cfg +*.egg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Environment +.env +.env.local + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db + diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ff3ecfb --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,35 @@ +# Dockerfile для FastAPI Backend +FROM python:3.13-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Устанавливаем системные зависимости (если нужны) +RUN apt-get update && apt-get install -y \ + --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +# Копируем файл зависимостей +COPY requirements.txt . + +# Устанавливаем Python зависимости +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Копируем код приложения +COPY . . + +# Открываем порт +EXPOSE 8000 + +# Переменные окружения +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 + +# Запускаем приложение +CMD ["python", "run.py"] + diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..2604a6a --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,2 @@ +# Backend app package + diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..b87ef02 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,40 @@ +""" +FastAPI Backend для сайта-визитки DevOps портфолио +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.routers import about, skills, projects, contacts + +app = FastAPI( + title="DevOps Portfolio API", + description="API для сайта-визитки DevOps инженера", + version="1.0.0" +) + +# Настройка CORS для работы с React фронтендом +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Подключение роутеров +app.include_router(about.router, prefix="/api", tags=["about"]) +app.include_router(skills.router, prefix="/api", tags=["skills"]) +app.include_router(projects.router, prefix="/api", tags=["projects"]) +app.include_router(contacts.router, prefix="/api", tags=["contacts"]) + + +@app.get("/") +async def root(): + """Корневой эндпоинт""" + return {"message": "DevOps Portfolio API", "version": "1.0.0"} + + +@app.get("/health") +async def health_check(): + """Health check эндпоинт для мониторинга""" + return {"status": "healthy"} + diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..1427815 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,64 @@ +""" +Pydantic модели для API +""" +from typing import List, Optional +from pydantic import BaseModel, EmailStr + + +class AboutResponse(BaseModel): + """Модель информации о себе""" + name: str + title: str + description: str + bio: str + location: str + email: EmailStr + github: str + + class Config: + json_schema_extra = { + "example": { + "name": "UZver24", + "title": "DevOps Engineer", + "description": "Опытный DevOps инженер", + "bio": "Специализируюсь на...", + "location": "Россия", + "email": "m-kar@inbox.ru", + "github": "https://github.com/UZver24" + } + } + + +class SkillCategory(BaseModel): + """Модель категории навыков""" + name: str + items: List[str] + + +class SkillsResponse(BaseModel): + """Модель списка навыков""" + categories: List[SkillCategory] + + +class Project(BaseModel): + """Модель проекта""" + id: int + name: str + description: str + technologies: List[str] + github_url: Optional[str] = None + status: str + + +class ProjectsResponse(BaseModel): + """Модель списка проектов""" + projects: List[Project] + + +class ContactResponse(BaseModel): + """Модель контактов""" + email: EmailStr + github: str + linkedin: Optional[str] = None + telegram: Optional[str] = None + diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..c15851f --- /dev/null +++ b/backend/app/routers/__init__.py @@ -0,0 +1,2 @@ +# Routers package + diff --git a/backend/app/routers/about.py b/backend/app/routers/about.py new file mode 100644 index 0000000..78e01ba --- /dev/null +++ b/backend/app/routers/about.py @@ -0,0 +1,22 @@ +""" +Роутер для информации о себе +""" +from fastapi import APIRouter +from app.models import AboutResponse + +router = APIRouter() + + +@router.get("/about", response_model=AboutResponse) +async def get_about(): + """Получить информацию о себе""" + return AboutResponse( + name="UZver24", + title="DevOps Engineer", + description="Опытный DevOps инженер с фокусом на автоматизацию, контейнеризацию и облачные технологии", + bio="Специализируюсь на настройке CI/CD пайплайнов, работе с Kubernetes, инфраструктуре как коде (Terraform), мониторинге и обеспечении безопасности приложений.", + location="Россия", + email="m-kar@inbox.ru", + github="https://github.com/UZver24" + ) + diff --git a/backend/app/routers/contacts.py b/backend/app/routers/contacts.py new file mode 100644 index 0000000..4ef9721 --- /dev/null +++ b/backend/app/routers/contacts.py @@ -0,0 +1,19 @@ +""" +Роутер для контактов +""" +from fastapi import APIRouter +from app.models import ContactResponse + +router = APIRouter() + + +@router.get("/contacts", response_model=ContactResponse) +async def get_contacts(): + """Получить контактную информацию""" + return ContactResponse( + email="m-kar@inbox.ru", + github="https://github.com/UZver24", + linkedin=None, + telegram=None + ) + diff --git a/backend/app/routers/projects.py b/backend/app/routers/projects.py new file mode 100644 index 0000000..f51cf3d --- /dev/null +++ b/backend/app/routers/projects.py @@ -0,0 +1,25 @@ +""" +Роутер для проектов +""" +from fastapi import APIRouter +from app.models import ProjectsResponse + +router = APIRouter() + + +@router.get("/projects", response_model=ProjectsResponse) +async def get_projects(): + """Получить список проектов""" + return ProjectsResponse( + projects=[ + { + "id": 1, + "name": "DevOps Portfolio", + "description": "Сайт-визитка с демонстрацией DevOps навыков. Включает полный стек: Kubernetes, CI/CD, мониторинг, GitOps и многое другое.", + "technologies": ["Python", "FastAPI", "React", "Kubernetes", "Terraform", "Helm", "ArgoCD"], + "github_url": "https://github.com/UZver24/DevOpsPortfolio", + "status": "В разработке" + } + ] + ) + diff --git a/backend/app/routers/skills.py b/backend/app/routers/skills.py new file mode 100644 index 0000000..13b127c --- /dev/null +++ b/backend/app/routers/skills.py @@ -0,0 +1,49 @@ +""" +Роутер для навыков +""" +from fastapi import APIRouter +from app.models import SkillsResponse + +router = APIRouter() + + +@router.get("/skills", response_model=SkillsResponse) +async def get_skills(): + """Получить список навыков""" + return SkillsResponse( + categories=[ + { + "name": "Контейнеризация", + "items": ["Docker", "Docker Compose", "Kubernetes", "Helm"] + }, + { + "name": "CI/CD", + "items": ["GitHub Actions", "GitLab CI", "Jenkins"] + }, + { + "name": "Infrastructure as Code", + "items": ["Terraform", "Ansible"] + }, + { + "name": "Мониторинг", + "items": ["Prometheus", "Grafana", "ELK Stack", "Loki"] + }, + { + "name": "GitOps", + "items": ["ArgoCD", "Flux"] + }, + { + "name": "Облачные платформы", + "items": ["Yandex Cloud", "AWS", "GCP"] + }, + { + "name": "Языки программирования", + "items": ["Python", "Bash", "Go"] + }, + { + "name": "Базы данных", + "items": ["PostgreSQL", "MongoDB", "Redis"] + } + ] + ) + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..6b5295e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.32.0 +pydantic[email]>=2.10.0 +python-multipart>=0.0.12 + diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000..7cbf3d9 --- /dev/null +++ b/backend/run.py @@ -0,0 +1,16 @@ +""" +Скрипт для запуска FastAPI приложения +""" +import os + +import uvicorn + + +if __name__ == "__main__": + port = int(os.getenv("PORT", "8080")) + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=port, + reload=False, # Отключаем reload в production (Docker) + ) diff --git a/devops-portfolio/.helmignore b/devops-portfolio/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/devops-portfolio/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/devops-portfolio/Chart.yaml b/devops-portfolio/Chart.yaml new file mode 100644 index 0000000..2efbcd8 --- /dev/null +++ b/devops-portfolio/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: devops-portfolio +description: Helm chart для DevOps Portfolio приложения +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - portfolio + - devops + - fastapi + - react +maintainers: + - name: UZver24 + email: m-kar@inbox.ru diff --git a/devops-portfolio/README.md b/devops-portfolio/README.md new file mode 100644 index 0000000..3e38eee --- /dev/null +++ b/devops-portfolio/README.md @@ -0,0 +1,110 @@ +# Helm Chart для DevOps Portfolio + +## Что такое Helm + +**Helm** — это менеджер пакетов для Kubernetes, который упрощает развертывание и управление приложениями. + +### Зачем использовать Helm: + +1. **Шаблонизация конфигураций** — позволяет создавать переиспользуемые шаблоны Kubernetes манифестов +2. **Управление версиями** — можно версионировать конфигурации и легко откатываться к предыдущим версиям +3. **Упрощение развертывания** — одна команда `helm install` вместо множества `kubectl apply` +4. **Гибкая конфигурация** — легко менять параметры через `values.yaml` без редактирования манифестов +5. **Управление зависимостями** — можно определять зависимости между чартами +6. **Переиспользование** — один чарт можно использовать для разных окружений (dev, staging, production) + +### Преимущества для нашего проекта: + +- Легко менять количество реплик, ресурсы, образы через `values.yaml` +- Можно создавать разные конфигурации для разных окружений +- Упрощает CI/CD — одна команда для развертывания +- Легко обновлять приложение через `helm upgrade` + +## Структура Chart + +``` +devops-portfolio/ +├── Chart.yaml # Метаданные чарта +├── values.yaml # Значения по умолчанию +└── templates/ # Шаблоны Kubernetes манифестов + ├── backend-deployment.yaml + ├── backend-service.yaml + ├── frontend-deployment.yaml + ├── frontend-service.yaml + └── _helpers.tpl # Вспомогательные шаблоны +``` + +## Использование + +### Установка + +```bash +# Загружаем образы в minikube (если еще не загружены) +minikube image load devops-portfolio-backend:latest +minikube image load devops-portfolio-frontend:latest + +# Устанавливаем chart +cd devops-portfolio +helm install devops-portfolio . + +# Или с кастомными значениями +helm install devops-portfolio . -f my-values.yaml +``` + +### Обновление + +```bash +# Обновляем развертывание +helm upgrade devops-portfolio . + +# С кастомными значениями +helm upgrade devops-portfolio . -f my-values.yaml +``` + +### Просмотр статуса + +```bash +# Список установленных релизов +helm list + +# Статус конкретного релиза +helm status devops-portfolio + +# Просмотр сгенерированных манифестов +helm template devops-portfolio . +``` + +### Удаление + +```bash +helm uninstall devops-portfolio +``` + +## Кастомизация через values.yaml + +Все параметры можно изменить в `values.yaml`: + +```yaml +backend: + replicaCount: 3 # Изменить количество реплик + resources: + limits: + memory: "512Mi" # Увеличить лимиты памяти +``` + +## Сравнение с обычными манифестами + +**Без Helm:** +```bash +kubectl apply -f k8s/backend-deployment.yaml +kubectl apply -f k8s/backend-service.yaml +kubectl apply -f k8s/frontend-deployment.yaml +kubectl apply -f k8s/frontend-service.yaml +``` + +**С Helm:** +```bash +helm install devops-portfolio . +``` + +Одна команда вместо четырех, плюс возможность легко менять конфигурацию! diff --git a/devops-portfolio/templates/NOTES.txt b/devops-portfolio/templates/NOTES.txt new file mode 100644 index 0000000..e772788 --- /dev/null +++ b/devops-portfolio/templates/NOTES.txt @@ -0,0 +1,28 @@ +1. Get the application URL by running these commands: +{{- if .Values.frontend.enabled }} +{{- if eq .Values.frontend.service.type "NodePort" }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services frontend) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "NodePort" .Values.frontend.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services frontend) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.frontend.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w frontend' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} frontend --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.frontend.service.port }} +{{- else if contains "ClusterIP" .Values.frontend.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app=frontend" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +{{- end }} + +2. Backend API is available at: +{{- if .Values.backend.enabled }} + kubectl port-forward service/backend 8000:8000 + Then visit http://localhost:8000/docs for API documentation +{{- end }} diff --git a/devops-portfolio/templates/_helpers.tpl b/devops-portfolio/templates/_helpers.tpl new file mode 100644 index 0000000..b7db546 --- /dev/null +++ b/devops-portfolio/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "devops-portfolio.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "devops-portfolio.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "devops-portfolio.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "devops-portfolio.labels" -}} +helm.sh/chart: {{ include "devops-portfolio.chart" . }} +{{ include "devops-portfolio.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "devops-portfolio.selectorLabels" -}} +app.kubernetes.io/name: {{ include "devops-portfolio.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "devops-portfolio.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "devops-portfolio.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/devops-portfolio/templates/backend-deployment.yaml b/devops-portfolio/templates/backend-deployment.yaml new file mode 100644 index 0000000..77eb50a --- /dev/null +++ b/devops-portfolio/templates/backend-deployment.yaml @@ -0,0 +1,42 @@ +{{- if .Values.backend.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + labels: + {{- include "devops-portfolio.labels" . | nindent 4 }} + component: api +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + app: backend + {{- include "devops-portfolio.selectorLabels" . | nindent 6 }} + component: api + template: + metadata: + labels: + app: backend + {{- include "devops-portfolio.selectorLabels" . | nindent 8 }} + component: api + spec: + containers: + - name: backend + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + ports: + - containerPort: 8000 + name: http + env: + - name: PYTHONUNBUFFERED + value: "1" + - name: PYTHONDONTWRITEBYTECODE + value: "1" + resources: + {{- toYaml .Values.backend.resources | nindent 10 }} + livenessProbe: + {{- toYaml .Values.backend.livenessProbe | nindent 10 }} + readinessProbe: + {{- toYaml .Values.backend.readinessProbe | nindent 10 }} +{{- end }} + diff --git a/devops-portfolio/templates/backend-service.yaml b/devops-portfolio/templates/backend-service.yaml new file mode 100644 index 0000000..23485a9 --- /dev/null +++ b/devops-portfolio/templates/backend-service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.backend.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: backend + labels: + {{- include "devops-portfolio.labels" . | nindent 4 }} + component: api +spec: + type: {{ .Values.backend.service.type }} + ports: + - port: {{ .Values.backend.service.port }} + targetPort: 8000 + protocol: TCP + name: http + selector: + app: backend + {{- include "devops-portfolio.selectorLabels" . | nindent 4 }} + component: api +{{- end }} + diff --git a/devops-portfolio/templates/frontend-deployment.yaml b/devops-portfolio/templates/frontend-deployment.yaml new file mode 100644 index 0000000..d50ef63 --- /dev/null +++ b/devops-portfolio/templates/frontend-deployment.yaml @@ -0,0 +1,37 @@ +{{- if .Values.frontend.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + {{- include "devops-portfolio.labels" . | nindent 4 }} + component: web +spec: + replicas: {{ .Values.frontend.replicaCount }} + selector: + matchLabels: + app: frontend + {{- include "devops-portfolio.selectorLabels" . | nindent 6 }} + component: web + template: + metadata: + labels: + app: frontend + {{- include "devops-portfolio.selectorLabels" . | nindent 8 }} + component: web + spec: + containers: + - name: frontend + image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + ports: + - containerPort: 80 + name: http + resources: + {{- toYaml .Values.frontend.resources | nindent 10 }} + livenessProbe: + {{- toYaml .Values.frontend.livenessProbe | nindent 10 }} + readinessProbe: + {{- toYaml .Values.frontend.readinessProbe | nindent 10 }} +{{- end }} + diff --git a/devops-portfolio/templates/frontend-service.yaml b/devops-portfolio/templates/frontend-service.yaml new file mode 100644 index 0000000..ea6744a --- /dev/null +++ b/devops-portfolio/templates/frontend-service.yaml @@ -0,0 +1,24 @@ +{{- if .Values.frontend.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + {{- include "devops-portfolio.labels" . | nindent 4 }} + component: web +spec: + type: {{ .Values.frontend.service.type }} + ports: + - port: {{ .Values.frontend.service.port }} + targetPort: 80 + protocol: TCP + name: http + {{- if eq .Values.frontend.service.type "NodePort" }} + nodePort: {{ .Values.frontend.service.nodePort }} + {{- end }} + selector: + app: frontend + {{- include "devops-portfolio.selectorLabels" . | nindent 4 }} + component: web +{{- end }} + diff --git a/devops-portfolio/values.yaml b/devops-portfolio/values.yaml new file mode 100644 index 0000000..b3ea3c3 --- /dev/null +++ b/devops-portfolio/values.yaml @@ -0,0 +1,83 @@ +# Default values for devops-portfolio +# Это файл значений по умолчанию для Helm chart + +# Backend конфигурация +backend: + enabled: true + image: + repository: devops-portfolio-backend + tag: latest + pullPolicy: Never # Для minikube используем локальные образы + + replicaCount: 2 + + service: + type: ClusterIP + port: 8000 + + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + +# Frontend конфигурация +frontend: + enabled: true + image: + repository: devops-portfolio-frontend + tag: latest + pullPolicy: Never # Для minikube используем локальные образы + + replicaCount: 2 + + service: + type: NodePort + port: 80 + nodePort: 30080 + + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "200m" + + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7552c6d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: devops-portfolio-backend + ports: + - "8000:8000" + environment: + - PORT=8000 + - PORT=80 + - API_BASE_URL=http://backend:8000 + - PYTHONUNBUFFERED=1 + - PYTHONDONTWRITEBYTECODE=1 + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + restart: unless-stopped + networks: + - portfolio-network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: devops-portfolio-frontend + ports: + - "80:80" + depends_on: + backend: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + restart: unless-stopped + networks: + - portfolio-network + +networks: + portfolio-network: + driver: bridge + diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..f2c7c7a --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Build output +dist/ +dist-ssr/ +build/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..a2ed08d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,42 @@ +# Multi-stage build для React Frontend + +# Stage 1: Сборка приложения +FROM node:20-alpine AS builder + +WORKDIR /app + +# Копируем файлы зависимостей +COPY package.json package-lock.json* ./ + +# Устанавливаем зависимости +RUN npm ci + +# Копируем исходный код +COPY . . + +# Собираем приложение для production +RUN npm run build + +# Stage 2: Production образ с nginx +FROM nginx:alpine + +RUN apk add --no-cache gettext + +# Копируем собранное приложение из builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Копируем конфигурацию nginx +COPY nginx.conf /etc/nginx/conf.d/default.conf.template +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +# Открываем порт +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD sh -c "wget --quiet --tries=1 --spider http://localhost:${PORT:-8080}/ || exit 1" + +# Запускаем nginx через entrypoint (шаблонизация конфига) +ENTRYPOINT ["/docker-entrypoint.sh"] + diff --git a/frontend/docker-entrypoint.sh b/frontend/docker-entrypoint.sh new file mode 100644 index 0000000..23a9847 --- /dev/null +++ b/frontend/docker-entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -euo pipefail + +PORT="${PORT:-8080}" +API_BASE_URL="${API_BASE_URL:-http://localhost:8000}" + +export PORT API_BASE_URL + +envsubst '${PORT} ${API_BASE_URL}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + +exec nginx -g 'daemon off;' + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..b09fc7a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + DevOps Portfolio - UZver24 + + +
+ + + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..feac192 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,46 @@ +server { + listen ${PORT}; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # SPA routing - все запросы на index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Кэширование статических файлов + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Прокси для API запросов к backend + # Универсальный URL работает во всех случаях развертывания: + # - Docker Compose: backend - имя сервиса в docker-compose.yml + # - Kubernetes манифесты: backend - имя сервиса, Kubernetes DNS разрешит автоматически + # - Helm chart: backend - имя сервиса (унифицировано для всех случаев) + location /api { + proxy_pass ${API_BASE_URL}$request_uri; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $proxy_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_ssl_server_name on; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..e9c8244 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4953 @@ +{ + "name": "devops-portfolio-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devops-portfolio-frontend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.55.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "vite": "^5.0.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.259", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", + "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f8fb5bc --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "devops-portfolio-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "axios": "^1.6.2" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.55.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "vite": "^5.0.8" + } +} + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..585de78 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,22 @@ +.App { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +section { + padding: 4rem 2rem; + max-width: 1200px; + margin: 0 auto; +} + +@media (max-width: 768px) { + section { + padding: 2rem 1rem; + } +} + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..1dd8a89 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,70 @@ +import { useState, useEffect } from 'react' +import Header from './components/Header' +import Hero from './components/Hero' +import About from './components/About' +import Skills from './components/Skills' +import Projects from './components/Projects' +import Contacts from './components/Contacts' +import Footer from './components/Footer' +import './App.css' + +function App() { + const [about, setAbout] = useState(null) + const [skills, setSkills] = useState(null) + const [projects, setProjects] = useState(null) + const [contacts, setContacts] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchData = async () => { + const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '') + const buildUrl = (path) => `${apiBaseUrl}${path}` + + try { + const [aboutRes, skillsRes, projectsRes, contactsRes] = await Promise.all([ + fetch(buildUrl('/api/about')), + fetch(buildUrl('/api/skills')), + fetch(buildUrl('/api/projects')), + fetch(buildUrl('/api/contacts')) + ]) + + setAbout(await aboutRes.json()) + setSkills(await skillsRes.json()) + setProjects(await projectsRes.json()) + setContacts(await contactsRes.json()) + } catch (error) { + console.error('Error fetching data:', error) + } finally { + setLoading(false) + } + } + + fetchData() + }, []) + + if (loading) { + return ( +
+
+

Загрузка...

+
+ ) + } + + return ( +
+
+
+ + + + + +
+
+
+ ) +} + +export default App + diff --git a/frontend/src/components/About.css b/frontend/src/components/About.css new file mode 100644 index 0000000..f5b37fb --- /dev/null +++ b/frontend/src/components/About.css @@ -0,0 +1,51 @@ +.about { + background: var(--bg-light); +} + +.section-title { + font-size: 2.5rem; + margin-bottom: 2rem; + text-align: center; + color: var(--text-color); +} + +.about-content { + display: grid; + gap: 2rem; +} + +.about-bio { + font-size: 1.125rem; + line-height: 1.8; + color: var(--text-color); + margin-bottom: 2rem; +} + +.about-info { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.info-item { + padding: 1rem; + background: var(--bg-color); + border-radius: 0.5rem; + box-shadow: var(--shadow); +} + +.info-item a { + color: var(--primary-color); + text-decoration: none; +} + +.info-item a:hover { + text-decoration: underline; +} + +@media (max-width: 768px) { + .section-title { + font-size: 2rem; + } +} + diff --git a/frontend/src/components/About.jsx b/frontend/src/components/About.jsx new file mode 100644 index 0000000..94141ac --- /dev/null +++ b/frontend/src/components/About.jsx @@ -0,0 +1,30 @@ +import './About.css' + +function About({ about }) { + if (!about) return null + + return ( +
+

О себе

+
+
+

{about.bio}

+
+
+ Местоположение: {about.location} +
+
+ Email: {about.email} +
+
+ GitHub: {about.github} +
+
+
+
+
+ ) +} + +export default About + diff --git a/frontend/src/components/Contacts.css b/frontend/src/components/Contacts.css new file mode 100644 index 0000000..2c77b86 --- /dev/null +++ b/frontend/src/components/Contacts.css @@ -0,0 +1,47 @@ +.contacts { + background: var(--bg-color); +} + +.contacts-content { + max-width: 600px; + margin: 0 auto; + text-align: center; +} + +.contacts-description { + font-size: 1.125rem; + color: var(--text-light); + margin-bottom: 2rem; +} + +.contacts-list { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.contact-item { + padding: 1.5rem; + background: var(--bg-light); + border-radius: 0.5rem; + box-shadow: var(--shadow); +} + +.contact-item strong { + display: block; + margin-bottom: 0.5rem; + color: var(--text-color); +} + +.contact-item a { + color: var(--primary-color); + text-decoration: none; + font-size: 1.125rem; + transition: color 0.2s; +} + +.contact-item a:hover { + color: var(--secondary-color); + text-decoration: underline; +} + diff --git a/frontend/src/components/Contacts.jsx b/frontend/src/components/Contacts.jsx new file mode 100644 index 0000000..73be001 --- /dev/null +++ b/frontend/src/components/Contacts.jsx @@ -0,0 +1,49 @@ +import './Contacts.css' + +function Contacts({ contacts }) { + if (!contacts) return null + + return ( +
+

Контакты

+
+

+ Свяжитесь со мной, если у вас есть вопросы или предложения по сотрудничеству. +

+
+
+ Email: + {contacts.email} +
+ {contacts.github && ( + + )} + {contacts.linkedin && ( + + )} + {contacts.telegram && ( + + )} +
+
+
+ ) +} + +export default Contacts + diff --git a/frontend/src/components/Footer.css b/frontend/src/components/Footer.css new file mode 100644 index 0000000..049eb8f --- /dev/null +++ b/frontend/src/components/Footer.css @@ -0,0 +1,18 @@ +.footer { + background: var(--text-color); + color: white; + padding: 2rem; + text-align: center; +} + +.footer-content { + max-width: 1200px; + margin: 0 auto; +} + +.footer-note { + margin-top: 0.5rem; + font-size: 0.875rem; + opacity: 0.8; +} + diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx new file mode 100644 index 0000000..bb90cfa --- /dev/null +++ b/frontend/src/components/Footer.jsx @@ -0,0 +1,15 @@ +import './Footer.css' + +function Footer() { + return ( +
+
+

© 2025 DevOps Portfolio. Все права защищены.

+

Создано с демонстрацией DevOps практик

+
+
+ ) +} + +export default Footer + diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css new file mode 100644 index 0000000..6160561 --- /dev/null +++ b/frontend/src/components/Header.css @@ -0,0 +1,52 @@ +.header { + background: var(--bg-color); + box-shadow: var(--shadow); + position: sticky; + top: 0; + z-index: 100; +} + +.header-container { + max-width: 1200px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo-text { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.nav { + display: flex; + gap: 2rem; +} + +.nav-link { + color: var(--text-color); + text-decoration: none; + font-weight: 500; + transition: color 0.3s; +} + +.nav-link:hover { + color: var(--primary-color); +} + +@media (max-width: 768px) { + .header-container { + flex-direction: column; + gap: 1rem; + } + + .nav { + gap: 1rem; + flex-wrap: wrap; + justify-content: center; + } +} + diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 0000000..7a727c6 --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,22 @@ +import './Header.css' + +function Header() { + return ( +
+
+
+ DevOps Portfolio +
+ +
+
+ ) +} + +export default Header + diff --git a/frontend/src/components/Hero.css b/frontend/src/components/Hero.css new file mode 100644 index 0000000..dc78761 --- /dev/null +++ b/frontend/src/components/Hero.css @@ -0,0 +1,81 @@ +.hero { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + color: white; + padding: 6rem 2rem; + text-align: center; +} + +.hero-content { + max-width: 800px; + margin: 0 auto; +} + +.hero-name { + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 1rem; +} + +.hero-title { + font-size: 2rem; + font-weight: 400; + margin-bottom: 1.5rem; + opacity: 0.9; +} + +.hero-description { + font-size: 1.25rem; + margin-bottom: 2rem; + opacity: 0.95; +} + +.hero-links { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.btn { + padding: 0.75rem 2rem; + border-radius: 0.5rem; + text-decoration: none; + font-weight: 600; + transition: transform 0.2s, box-shadow 0.2s; + display: inline-block; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.btn-primary { + background: white; + color: var(--primary-color); +} + +.btn-secondary { + background: transparent; + color: white; + border: 2px solid white; +} + +@media (max-width: 768px) { + .hero { + padding: 4rem 1rem; + } + + .hero-name { + font-size: 2.5rem; + } + + .hero-title { + font-size: 1.5rem; + } + + .hero-description { + font-size: 1rem; + } +} + diff --git a/frontend/src/components/Hero.jsx b/frontend/src/components/Hero.jsx new file mode 100644 index 0000000..5901106 --- /dev/null +++ b/frontend/src/components/Hero.jsx @@ -0,0 +1,26 @@ +import './Hero.css' + +function Hero({ about }) { + if (!about) return null + + return ( +
+
+

{about.name}

+

{about.title}

+

{about.description}

+ +
+
+ ) +} + +export default Hero + diff --git a/frontend/src/components/Projects.css b/frontend/src/components/Projects.css new file mode 100644 index 0000000..31a9962 --- /dev/null +++ b/frontend/src/components/Projects.css @@ -0,0 +1,78 @@ +.projects { + background: var(--bg-light); +} + +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 2rem; +} + +.project-card { + background: var(--bg-color); + padding: 2rem; + border-radius: 0.5rem; + box-shadow: var(--shadow); + transition: transform 0.2s, box-shadow 0.2s; + display: flex; + flex-direction: column; +} + +.project-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.project-name { + font-size: 1.5rem; + margin-bottom: 1rem; + color: var(--primary-color); +} + +.project-description { + color: var(--text-light); + margin-bottom: 1.5rem; + flex: 1; + line-height: 1.6; +} + +.project-technologies { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.tech-tag { + background: var(--bg-light); + color: var(--text-color); + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.875rem; + border: 1px solid var(--border-color); +} + +.project-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.project-status { + color: var(--text-light); + font-size: 0.875rem; +} + +.project-link { + color: var(--primary-color); + text-decoration: none; + font-weight: 600; + transition: color 0.2s; +} + +.project-link:hover { + color: var(--secondary-color); +} + diff --git a/frontend/src/components/Projects.jsx b/frontend/src/components/Projects.jsx new file mode 100644 index 0000000..1ac8676 --- /dev/null +++ b/frontend/src/components/Projects.jsx @@ -0,0 +1,40 @@ +import './Projects.css' + +function Projects({ projects }) { + if (!projects || !projects.projects) return null + + return ( +
+

Проекты

+
+ {projects.projects.map((project) => ( +
+

{project.name}

+

{project.description}

+
+ {project.technologies.map((tech, index) => ( + {tech} + ))} +
+
+ {project.status} + {project.github_url && ( + + GitHub → + + )} +
+
+ ))} +
+
+ ) +} + +export default Projects + diff --git a/frontend/src/components/Skills.css b/frontend/src/components/Skills.css new file mode 100644 index 0000000..2d8572e --- /dev/null +++ b/frontend/src/components/Skills.css @@ -0,0 +1,44 @@ +.skills { + background: var(--bg-color); +} + +.skills-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 2rem; +} + +.skill-category { + background: var(--bg-light); + padding: 1.5rem; + border-radius: 0.5rem; + box-shadow: var(--shadow); + transition: transform 0.2s, box-shadow 0.2s; +} + +.skill-category:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.category-name { + font-size: 1.5rem; + margin-bottom: 1rem; + color: var(--primary-color); +} + +.skill-items { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.skill-item { + background: var(--primary-color); + color: white; + padding: 0.5rem 1rem; + border-radius: 1.5rem; + font-size: 0.875rem; + font-weight: 500; +} + diff --git a/frontend/src/components/Skills.jsx b/frontend/src/components/Skills.jsx new file mode 100644 index 0000000..f84b485 --- /dev/null +++ b/frontend/src/components/Skills.jsx @@ -0,0 +1,26 @@ +import './Skills.css' + +function Skills({ skills }) { + if (!skills || !skills.categories) return null + + return ( +
+

Навыки

+
+ {skills.categories.map((category, index) => ( +
+

{category.name}

+
+ {category.items.map((item, itemIndex) => ( + {item} + ))} +
+
+ ))} +
+
+ ) +} + +export default Skills + diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..4d6a43e --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,57 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #2563eb; + --secondary-color: #1e40af; + --text-color: #1f2937; + --text-light: #6b7280; + --bg-color: #ffffff; + --bg-light: #f9fafb; + --border-color: #e5e7eb; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: var(--text-color); + line-height: 1.6; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + gap: 1rem; +} + +.spinner { + width: 50px; + height: 50px; + border: 4px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..299bc52 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..eeb455c --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + } + } + } +}) + diff --git a/infrastructure/cloudK8/README.md b/infrastructure/cloudK8/README.md new file mode 100644 index 0000000..fc99767 --- /dev/null +++ b/infrastructure/cloudK8/README.md @@ -0,0 +1,112 @@ +# Terraform (облачная инфраструктура, Yandex Cloud) + +Этот каталог предназначен для ресурсов, автоматизирующих развёртывание облачной инфраструктуры проекта в Яндекс.Облаке с помощью Terraform. + +## Важно о секретных переменных +- Все чувствительные данные (токены, cloud_id, folder_id, zone) хранятся только в отдельном файле `terraform.tfvars`. +- Рабочий файл `terraform.tfvars` ВСЕГДА должен быть в .gitignore (не попадёт в git, см. правила в проекте). +- В репозитории публикуется только шаблон: `terraform.tfvars.example`. Перед работой скопируйте его и заполните: + ```bash + cp terraform.tfvars.example terraform.tfvars + # Далее отредактируйте terraform.tfvars под свои значения + ``` + +## Описание сети (VPC, subnet, security group) + +- **VPC (виртуальная облачная сеть)** позволяет изолировать и структурировать ресурсы в облаке, нужна для работы всего остального. +- **Подсеть (subnet)** определяет, где именно (в какой зоне, с каким адресным пространством) появляются ресурсы, и позволяет задавать доступ в Интернет. +- **Security group** — облачный firewall, управляет доступом к портам сервисов (например, разрешаем HTTP/HTTPS/SSH из Интернета, остальное закрыто). +- В облачных сценариях нельзя создать ВМ/кластер без указания VPC/subnet, а грамотно организованная security group — основа будущей безопасности. +- В проекте используется структура: одна VPC, одна публичная подсеть (можно расширять под high availability), security group с открытыми 22, 80, 443 портами (минимальный набор для администрирования и Web). + +## Автоматическое создание сервисного аккаунта для Kubernetes + +- При запуске Terraform сервисный аккаунт для управления Kubernetes-кластером создаётся автоматически, вручную создавать его не требуется. +- Все необходимые IAM-права (`k8s.admin` для кластера, `container-registry.editor` для docker registry) назначаются автоматически. +- Дальнейшие ресурсы (кластер, node group) используют этот аккаунт для безопасного и корректного управления ресурсами. + +Пример кода — см. `main.tf`. + +## Общие цели +- Создание облачного окружения «инфраструктура как код» (IaC) +- Развёртывание сетей, кластеров, машин и сервисов в облаке повторяемым способом + +## Используемые источники +- [Разворачиваем без боли Terraform в Яндекс облаке (Habr)](https://habr.com/ru/companies/otus/articles/957982/) + +--- + +## Первый этап — подключение облачного провайдера Terraform (Yandex Cloud) + +1. Установите Terraform +2. Добавьте в файл `~/.terraformrc` mirror Яндекс (см. README локального варианта): + ``` + provider_installation { + network_mirror { + url = "https://terraform-mirror.yandexcloud.net/" + include = ["registry.terraform.io/*/*"] + } + direct { + exclude = ["registry.terraform.io/*/*"] + } + } + ``` +3. Пропишите провайдер в `main.tf` (пример ниже) + +--- + +## Пример provider блока для Yandex Cloud +```hcl +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = ">= 0.89" # актуальная версия на момент добавления + } + } +} + +provider "yandex" { + token = var.yc_token + cloud_id = var.yc_cloud_id + folder_id = var.yc_folder_id + zone = var.yc_zone +} +``` + +--- + +## Managed Kubernetes Cluster (Yandex Cloud) + +- После развёртывания VPC и подсети, с помощью Terraform создаётся облачный управляемый Kubernetes-кластер (Yandex Managed Service for Kubernetes). +- Вместе с кластером создаётся группа рабочих узлов (node group), которая может масштабироваться как по числу машин, так и по их параметрам (размер памяти, CPU, диск). +- Все параметры кластера (ID сервисного аккаунта, платформа, размер, количество узлов и др.) вынесены в переменные для гибкости. +- Для тестовых и MVP-задач рекомендовано использовать 1-2 маленьких ноды с preemptible-VM (экономно и подходит для разработки/обучения). +- Правильная структура позволяет позже безболезненно увеличить кластер или повысить отказоустойчивость. +- Сервисный аккаунт обязателен — он выдается при создании кластера и управляет всеми действиями узлов кластера и сервисов. + +**В результате получаем:** +- Готовый для деплоя кластер Kubernetes в выбранной зоне Яндекс.Облака, +- Рабочий пул узлов под приложеня (бэкенд, фронтенд, БД) — всё будет развернуто через Helm или kubectl, +- Структуру, которая готова к масштабированию и эксплуатации по всем best practices DevOps. + +**Важно!** Полные параметры и инструкции см. в примерах: `main.tf`, `variables.tf`, `terraform.tfvars.example`. + +--- + +## Получение kubeconfig для кластера + +- kubeconfig — файл с параметрами аутентификации и доступа к кластеру, необходим для kubectl, helm, CI/CD и работы с кластером из любого места. +- После применения terraform достаточно выполнить одну команду: + + ```bash + yc managed-kubernetes cluster get-credentials $(terraform output -raw k8s_cluster_id) --external + ``` + + Эта команда автоматически получает id только что созданного кластера через terraform output и подготавливает файл kubeconfig. + +- Без kubeconfig не получится деплоить приложения и управлять кластером! + +--- + +> **Важно:** Не забудьте задокументировать переменные (`variables.tf`) и сохранить `README.md` всегда в актуальном виде! diff --git a/infrastructure/cloudK8/main.tf b/infrastructure/cloudK8/main.tf new file mode 100644 index 0000000..d75f44a --- /dev/null +++ b/infrastructure/cloudK8/main.tf @@ -0,0 +1,151 @@ +# main.tf — настройка подключения к облаку Yandex Cloud через Terraform + +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = ">= 0.89" + } + } +} + +provider "yandex" { + token = var.yc_token # IAM-токен сервисного аккаунта + cloud_id = var.yc_cloud_id # b1g5h4qcn4qvf54hber1 + folder_id = var.yc_folder_id # b1g5h63h4jh5ju7f4ecs + zone = var.yc_zone # Зона размещения ресурсов (например, ru-central1-a) +} + +# ------------------------------------- +# Создаём VPC (виртуальную облачную сеть) +# ------------------------------------- +resource "yandex_vpc_network" "main" { + name = "${var.project_name}-vpc" +} + +# ----------------------------------------------------------- +# Создаём публичную подсеть в выбранной зоне (например, ru-central1-a) +# CIDR блок 10.10.0.0/24 — пример, можешь изменить под себя +# ----------------------------------------------------------- +resource "yandex_vpc_subnet" "public" { + name = "${var.project_name}-subnet-public" + zone = var.yc_zone + network_id = yandex_vpc_network.main.id + v4_cidr_blocks = ["10.10.0.0/24"] +} + +# ----------------------------------------------------------- +# Security group: разрешим SSH (22), HTTP (80), HTTPS (443) для всех +# ----------------------------------------------------------- +resource "yandex_vpc_security_group" "main" { + name = "${var.project_name}-sg" + network_id = yandex_vpc_network.main.id + + ingress { + protocol = "TCP" + description = "Allow SSH from anywhere" + port = 22 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + protocol = "TCP" + description = "Allow HTTP from anywhere" + port = 80 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + protocol = "TCP" + description = "Allow HTTPS from anywhere" + port = 443 + v4_cidr_blocks = ["0.0.0.0/0"] + } + + # Разрешить все исходящие соединения (иначе поды не выйдут в интернет) + egress { + protocol = "ANY" + description = "Allow all outbound traffic" + from_port = 0 + to_port = 65535 + v4_cidr_blocks = ["0.0.0.0/0"] + } +} + +# ----------------------------------------------------------- +# Автоматическое создание сервисного аккаунта для управления кластером +# ----------------------------------------------------------- +resource "yandex_iam_service_account" "k8s" { + name = "${var.project_name}-k8s-sa" + description = "Service Account для управления Managed Kubernetes Cluster и worker-нодами" +} + +# Назначение ролей сервисному аккаунту +resource "yandex_resourcemanager_folder_iam_member" "k8s_admin" { + folder_id = var.yc_folder_id + role = "k8s.admin" + member = "serviceAccount:${yandex_iam_service_account.k8s.id}" +} + +resource "yandex_resourcemanager_folder_iam_member" "container_registry_editor" { + folder_id = var.yc_folder_id + role = "container-registry.editor" + member = "serviceAccount:${yandex_iam_service_account.k8s.id}" +} + +# ----------------------------------------------------------- +# Создаём Managed Kubernetes Cluster используя новый сервисный аккаунт +# ----------------------------------------------------------- +resource "yandex_kubernetes_cluster" "main" { + name = "${var.project_name}-yk8s" + network_id = yandex_vpc_network.main.id + master { + public_ip = true + zonal { + zone = var.yc_zone + subnet_id = yandex_vpc_subnet.public.id + } + security_group_ids = [yandex_vpc_security_group.main.id] + } + service_account_id = yandex_iam_service_account.k8s.id + node_service_account_id = yandex_iam_service_account.k8s.id + release_channel = "STABLE" +} + +# ----------------------------------------------------------- +# Worker node group как раньше (использует тот же SA) +# ----------------------------------------------------------- +resource "yandex_kubernetes_node_group" "ng1" { + cluster_id = yandex_kubernetes_cluster.main.id + name = "${var.project_name}-nodes" + instance_template { + platform_id = var.worker_platform_id + resources { + memory = var.worker_memory_gb + cores = var.worker_cores + } + boot_disk { + size = var.worker_disk_gb + type = "network-ssd" + } + network_interface { + nat = true + subnet_ids = [yandex_vpc_subnet.public.id] + security_group_ids = [yandex_vpc_security_group.main.id] + } + scheduling_policy { + preemptible = true + } + } + scale_policy { + fixed_scale { + size = var.worker_node_count + } + } + allocation_policy { + location { + zone = var.yc_zone + } + } + version = "1.29" +} diff --git a/infrastructure/cloudK8/outputs.tf b/infrastructure/cloudK8/outputs.tf new file mode 100644 index 0000000..6394cec --- /dev/null +++ b/infrastructure/cloudK8/outputs.tf @@ -0,0 +1,11 @@ +# outputs.tf — выходные параметры инфраструктуры Yandex Cloud + +output "k8s_cluster_id" { + value = yandex_kubernetes_cluster.main.id + description = "ID созданного Kubernetes-кластера (используется в yc managed-kubernetes cluster get-credentials)" +} + +output "k8s_cluster_name" { + value = yandex_kubernetes_cluster.main.name + description = "Имя созданного Kubernetes-кластера (альтернатива для выгрузки kubeconfig)" +} diff --git a/infrastructure/cloudK8/terraform.tfvars.example b/infrastructure/cloudK8/terraform.tfvars.example new file mode 100644 index 0000000..4c572d0 --- /dev/null +++ b/infrastructure/cloudK8/terraform.tfvars.example @@ -0,0 +1,18 @@ +# Пример terraform.tfvars для подключения к Яндекс Облаку +# Скопируйте этот файл как terraform.tfvars и заполните своими данными! + +# IAM-токен сервисного аккаунта (запросить через yc iam create-token) +yc_token = "" +# Cloud ID вашего облака (yc config list) +yc_cloud_id = "" +# Folder ID (каталог) вашего проекта (yc config list) +yc_folder_id = "" +# Зона размещения ресурсов, например ru-central1-a +yc_zone = "ru-central1-a" + +# Конфигурация воркеров для минимального MVP/разработки (можно масштабировать) +worker_platform_id = "standard-v2" # тип n1-standard-v2 (или v3/v4) +worker_memory_gb = 2 # минимум для тестов +worker_cores = 2 # минимум для тестов +worker_disk_gb = 20 # лучше не меньше! +worker_node_count = 1 # одна нода для экономии diff --git a/infrastructure/cloudK8/variables.tf b/infrastructure/cloudK8/variables.tf new file mode 100644 index 0000000..4e4f582 --- /dev/null +++ b/infrastructure/cloudK8/variables.tf @@ -0,0 +1,58 @@ +# variables.tf — переменные для провайдера Yandex Cloud + +variable "project_name" { + description = "Имя проекта для ресурсов VPC и других тегов" + type = string + default = "devops-portfolio-cloud" +} + +variable "yc_token" { + description = "IAM-токен сервисного аккаунта Яндекс.Облака. Получить командой 'yc iam create-token'." + type = string + sensitive = true +} + +variable "yc_cloud_id" { + description = "ID облака (cloud_id). Получить командой 'yc config list'." + type = string +} + +variable "yc_folder_id" { + description = "ID каталога (folder_id) в Яндекс.Облаке. Получить командой 'yc config list'." + type = string +} + +variable "yc_zone" { + description = "Зона размещения ресурсов (например, ru-central1-a)." + type = string +} + +variable "worker_platform_id" { + description = "Тип VM для нод k8s (например, 'standard-v3')." + type = string + default = "standard-v2" +} + +variable "worker_memory_gb" { + description = "Оперативная память VM-воркера, в GB (минимум 2, лучше 4+)" + type = number + default = 2 +} + +variable "worker_cores" { + description = "Кол-во vCPU на VM-воркере (минимум 2 для нормального старта)" + type = number + default = 2 +} + +variable "worker_disk_gb" { + description = "Размер boot-диска воркера, GB (лучше минимум 20)" + type = number + default = 20 +} + +variable "worker_node_count" { + description = "Кол-во рабочих нод в пуле (для MVP достаточно 1-2; можно масштабировать позже)" + type = number + default = 1 +} diff --git a/infrastructure/local/README.md b/infrastructure/local/README.md new file mode 100644 index 0000000..09a5918 --- /dev/null +++ b/infrastructure/local/README.md @@ -0,0 +1,57 @@ +# Terraform (локальная инфраструктура) + +> **Важно!** +> Из-за санкций, официальный реестр плагинов Terraform ([registry.terraform.io](https://registry.terraform.io/)) недоступен. Рабочее решение — использовать зеркала, как описано в статье: [Разворачиваем без боли Terraform в Яндекс облаке (Habr)](https://habr.com/ru/companies/otus/articles/957982/) +> +> **Для этого настройте файл `~/.terraformrc` (в домашней папке):** +> +``` +provider_installation { + network_mirror { + url = "https://terraform-mirror.yandexcloud.net/" + include = ["registry.terraform.io/*/*"] + } + direct { + exclude = ["registry.terraform.io/*/*"] + } +} +``` +> +> Это позволит Terraform скачивать плагины через российское зеркало и обойти блокировки. + +Данный каталог содержит инфраструктурный код Terraform для автоматизации локального развёртывания учебного проекта. + +## Зачем это нужно +- Автоматизация создания необходимых ресурсов для локального окружения +- Возможность воспроизвести инфраструктуру проекта на новой локальной машине + +## Примеры задач для автоматизации: +- Поднятие виртуальной машины с minikube (например, через Vagrant, libvirt, Docker) +- Генерация настроек сети и проброса портов +- Создание ssh-ключей при необходимости + +## Основные файлы +- `main.tf` — основной инфраструктурный код +- `variables.tf` — описание переменных +- `outputs.tf` — выходные параметры (например, IP-адреса, пути к kubeconfig) + +## Как использовать +1. Установить Terraform ([официальная инструкция](https://learn.hashicorp.com/tutorials/terraform/install-cli)). +2. Настроить файл `~/.terraformrc` (см. выше), если вы видите ошибку про registry.terraform.io! +3. Зайти в каталог `infrastructure/local` +4. Инициализировать проект: + ```bash + terraform init + ``` +5. Просмотреть план изменений: + ```bash + terraform plan + ``` +6. Применить инфраструктуру: + ```bash + terraform apply + ``` + +--- + +> **Внимание!** Данный каталог и все его файлы относятся только к локальному варианту. Для облачных сценариев будет отдельная структура. diff --git a/infrastructure/local/info.txt b/infrastructure/local/info.txt new file mode 100755 index 0000000..f98bd9f --- /dev/null +++ b/infrastructure/local/info.txt @@ -0,0 +1 @@ +Учебный проект: стартовый пример работы Terraform локально. \ No newline at end of file diff --git a/infrastructure/local/main.tf b/infrastructure/local/main.tf new file mode 100644 index 0000000..2e3096b --- /dev/null +++ b/infrastructure/local/main.tf @@ -0,0 +1,12 @@ +# main.tf — Пример для локальной инфраструктуры + +# Используем локальный provider (не создает никаких облачных ресурсов) +provider "local" {} + +# Пример — создадим инфофайл при инициализации (можно заменить на реальные задачи) +resource "local_file" "info" { + filename = "${path.module}/info.txt" + content = "Учебный проект: стартовый пример работы Terraform локально." +} + +# (Здесь же позже могут быть ресурсы для запуска VM, настройки сети и т.д.) diff --git a/infrastructure/local/outputs.tf b/infrastructure/local/outputs.tf new file mode 100644 index 0000000..047356e --- /dev/null +++ b/infrastructure/local/outputs.tf @@ -0,0 +1,11 @@ +# outputs.tf — выходные значения + +output "info_file_path" { + description = "Путь к файлу info.txt, сгенерированному Terraform." + value = local_file.info.filename +} + +output "project_name" { + description = "Название локального проекта из переменной." + value = var.project_name +} diff --git a/infrastructure/local/variables.tf b/infrastructure/local/variables.tf new file mode 100644 index 0000000..3fa3796 --- /dev/null +++ b/infrastructure/local/variables.tf @@ -0,0 +1,7 @@ +# variables.tf — определение переменных + +variable "project_name" { + description = "Название проекта для теста локального Terraform." + type = string + default = "devops-portfolio-local" +} diff --git a/infrastructure/serverless/README.md b/infrastructure/serverless/README.md new file mode 100644 index 0000000..60b39a5 --- /dev/null +++ b/infrastructure/serverless/README.md @@ -0,0 +1,97 @@ +# Serverless Containers (Yandex Cloud) — деплой приложения через Terraform + +В этом разделе описан альтернативный, экономичный вариант деплоя приложения с помощью Yandex Serverless Containers (YSC). Такой подход отлично подходит для MVP, тестовых и учебных задач, когда запуск полноценного кластера Kubernetes не нужен (или слишком дорог). + +## Преимущества Serverless Containers: +- Платишь только за реально потраченные ресурсы (CPU, RAM, время работы), нет постоянных VM или node pool +- Нет необходимости управлять Kubernetes, ingress, VM и прочей инфраструктурой +- Всё работает “из коробки”, доступ по HTTP(S), логирование, переменные окружения, автошкалирование +- Простая интеграция с Yandex Container Registry и Object Storage + +## Минусы (по сравнению с K8s): +- Меньше гибкости и меньше “тонких” возможностей для сложных и Stateful-сервисов +- Нет постоянного pod’а (только обработка http-запросов/триггеров) + +--- + +## Сравнение с вариантом “Managed Service for Kubernetes” +- K8s-подход требует целого кластера, что стоит больше 1000 ₽/мес (даже минимум), и требует отдельной настройки ingress/CI/CD +- Вариант serverless не требует VM, оплачивается по-микросекундам, идеально для development, прототипирования, портфолио +- K8s-манифесты и инфраструктура тут НЕ реализованы (разрабатывается отдельно; подробнее см. папку `cloudK8/` и README там) + +--- + +## Предварительные требования +- Установлены `yc`, `terraform`, `docker` +- В облаке создан отдельный каталог/проект, в котором есть доступ на запись в Container Registry +- Есть IAM-токен сервисного аккаунта или пользователя для Terraform (`yc iam create-token`) + +## Пошаговая инструкция + +1. **Соберите Docker-образ backend** + ```bash + docker build -t backend:latest ../../backend + ``` +2. **Создайте (или возьмите существующий) реестр YCR и запушьте образ** + ```bash + yc container registry create --name devops-portfolio # при отсутствии + yc iam create-token | docker login --username oauth --password-stdin cr.yandex + docker tag backend:latest cr.yandex//backend:latest + docker push cr.yandex//backend:latest + ``` +3. **Заполните `infrastructure/serverless/terraform.tfvars`** + ```hcl + yc_token = "..." # yc iam create-token + yc_cloud_id = "..." # yc config list + yc_folder_id = "..." + yc_zone = "ru-central1-a" + environment = "dev" + + backend_image = "cr.yandex//backend:latest" + container_registry_id = "" + + static_bucket_name = "kulibin-devops-portfolio" + static_allowed_origins = ["https://kulibin-devops-portfolio.website.yandexcloud.net"] + api_allowed_origins = ["https://kulibin-devops-portfolio.website.yandexcloud.net"] + ``` + `enable_frontend_container = false` — фронтенд отдаём из Object Storage. В дальнейшем можно включить контейнер, если потребуется. +4. **Примените Terraform** + ```bash + cd infrastructure/serverless + terraform init + terraform apply + ``` + Конфигурация создаст: + - serverless container для backend; + - Object Storage + ключи для выкладки статики; + - API Gateway, который проксирует запросы на backend. +5. **Соберите и загрузите фронтенд** + ```bash + export API_URL=$(terraform output -raw api_gateway_endpoint) + cd ../../frontend + VITE_API_BASE_URL=$API_URL npm run build + + cd .. + export AWS_ACCESS_KEY_ID=$(terraform -chdir=infrastructure/serverless output -raw static_site_access_key) + export AWS_SECRET_ACCESS_KEY=$(terraform -chdir=infrastructure/serverless output -raw static_site_secret_key) + yc storage s3 cp --recursive frontend/dist/ s3://$(terraform -chdir=infrastructure/serverless output -raw static_bucket_name) + ``` +6. **Проверьте URLs** + - `terraform output backend_url` — прямой доступ к контейнеру (для отладки). + - `terraform output api_gateway_endpoint` — адрес API, на который ходит фронт. + - `terraform output static_site_endpoint` — адрес статического сайта (`https://.website.yandexcloud.net`). + +> Сборка и публикация сейчас выполняются вручную. Позже можно автоматизировать их в CI/CD, чтобы push образа/статического билда автоматически триггерил Terraform или `yc` скрипты. + +--- + +## Документация и полезные ссылки +- [Yandex Serverless Containers, документация](https://cloud.yandex.ru/docs/containers) +- [Тарифы Serverless Containers](https://cloud.yandex.ru/prices/serverless-containers) +- [Terraform Yandex Provider, ресурс yandex_serverless_container](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/serverless_container) + +--- + +> Это самый экономичный, простой и “cloud native” способ деплоя небольших сервисов с оплатой именно за полезную нагрузку. + + diff --git a/infrastructure/serverless/main.tf b/infrastructure/serverless/main.tf new file mode 100644 index 0000000..9a55646 --- /dev/null +++ b/infrastructure/serverless/main.tf @@ -0,0 +1,191 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + version = ">= 0.89" + } + } +} + +provider "yandex" { + token = var.yc_token + cloud_id = var.yc_cloud_id + folder_id = var.yc_folder_id + zone = var.yc_zone +} + +locals { + backend_cores = var.backend_cpu >= 1 ? floor(var.backend_cpu) : 1 + backend_core_fraction = var.backend_cpu >= 1 ? 100 : var.backend_cpu * 100 + frontend_cores = var.frontend_cpu >= 1 ? floor(var.frontend_cpu) : 1 + frontend_core_fraction = var.frontend_cpu >= 1 ? 100 : var.frontend_cpu * 100 + default_labels = merge( + { + project = var.project_name + environment = var.environment + managed_by = "terraform" + }, + var.extra_labels, + ) +} + +# Сервисный аккаунт для обоих контейнеров +resource "yandex_iam_service_account" "serverless" { + name = "${var.project_name}-serverless-sa" + description = "Используется контейнерами backend/frontend" +} + +# Необходимые роли для запуска контейнеров и скачивания образов +resource "yandex_resourcemanager_folder_iam_member" "serverless_admin" { + folder_id = var.yc_folder_id + role = "serverless.containers.admin" + member = "serviceAccount:${yandex_iam_service_account.serverless.id}" +} + +resource "yandex_resourcemanager_folder_iam_member" "cr_puller" { + folder_id = var.yc_folder_id + role = "container-registry.images.puller" + member = "serviceAccount:${yandex_iam_service_account.serverless.id}" +} + +resource "yandex_container_registry_iam_binding" "registry_puller" { + registry_id = var.container_registry_id + role = "container-registry.images.puller" + members = [ + "serviceAccount:${yandex_iam_service_account.serverless.id}" + ] +} + +# Сервисный аккаунт и бакет для статического фронтенда +resource "yandex_iam_service_account" "static_site" { + name = "${var.project_name}-static-site" + description = "Обслуживает Object Storage для статического фронтенда" +} + +resource "yandex_resourcemanager_folder_iam_member" "static_site_storage_editor" { + folder_id = var.yc_folder_id + role = "storage.editor" + member = "serviceAccount:${yandex_iam_service_account.static_site.id}" +} + +resource "yandex_iam_service_account_static_access_key" "static_site" { + service_account_id = yandex_iam_service_account.static_site.id + description = "Статический ключ для загрузки билда фронтенда" +} + +resource "yandex_storage_bucket" "static_site" { + bucket = var.static_bucket_name + + access_key = yandex_iam_service_account_static_access_key.static_site.access_key + secret_key = yandex_iam_service_account_static_access_key.static_site.secret_key + + default_storage_class = var.static_default_storage_class + max_size = var.static_max_size + + website { + index_document = var.static_index_document + error_document = var.static_error_document + } + + cors_rule { + allowed_methods = ["GET"] + allowed_origins = var.static_allowed_origins + allowed_headers = ["*"] + expose_headers = [] + max_age_seconds = 3600 + } + + anonymous_access_flags { + read = true + list = false + config_read = true + } + + force_destroy = true +} + +# --- Backend Serverless Container --- +resource "yandex_serverless_container" "backend" { + name = "${var.project_name}-backend" + description = "Backend FastAPI server (deployed as Serverless Container)" + memory = var.backend_memory_mb + cores = local.backend_cores + core_fraction = local.backend_core_fraction + concurrency = var.backend_concurrency + execution_timeout = "30s" + + image { + url = var.backend_image + environment = var.backend_env + } + + runtime { + type = "http" + } + + service_account_id = yandex_iam_service_account.serverless.id +} + +# --- Frontend Serverless Container --- +resource "yandex_serverless_container" "frontend" { + count = var.enable_frontend_container ? 1 : 0 + + name = "${var.project_name}-frontend" + description = "Frontend React-server (опционально, используется вместо Object Storage)" + memory = var.frontend_memory_mb + cores = local.frontend_cores + core_fraction = local.frontend_core_fraction + concurrency = var.frontend_concurrency + execution_timeout = "30s" + + image { + url = var.frontend_image + environment = merge( + var.frontend_env, + { + API_BASE_URL = trimsuffix(yandex_serverless_container.backend.url, "/") + } + ) + } + + runtime { + type = "http" + } + + service_account_id = yandex_iam_service_account.serverless.id +} + +# Публикуем контейнеры для публичного доступа +resource "yandex_serverless_container_iam_binding" "backend_public" { + container_id = yandex_serverless_container.backend.id + role = "serverless.containers.invoker" + members = ["system:allUsers"] +} + +resource "yandex_serverless_container_iam_binding" "frontend_public" { + count = var.enable_frontend_container ? 1 : 0 + + container_id = yandex_serverless_container.frontend[0].id + role = "serverless.containers.invoker" + members = ["system:allUsers"] +} + +# TODO: API Gateway не поддерживает импорт в Terraform, поэтому используем временное решение: +# проверяем существование через yc CLI в workflow и устанавливаем create_api_gateway = false если уже существует. +# ТРЕБУЕТСЯ ПРАВКА: нужно реализовать автоматическое обнаружение и управление существующим API Gateway. +# API Gateway для backend (используется фронтендом из Object Storage) +resource "yandex_api_gateway" "backend" { + count = var.create_api_gateway ? 1 : 0 + + name = "${var.project_name}-api" + description = "HTTP вызовы к backend serverless контейнеру" + folder_id = var.yc_folder_id + labels = local.default_labels + + spec = templatefile("${path.module}/templates/api-gateway.yaml.tmpl", { + backend_base_url = trimsuffix(yandex_serverless_container.backend.url, "/") + allowed_origins = var.api_allowed_origins + }) +} + + diff --git a/infrastructure/serverless/outputs.tf b/infrastructure/serverless/outputs.tf new file mode 100644 index 0000000..36eea45 --- /dev/null +++ b/infrastructure/serverless/outputs.tf @@ -0,0 +1,37 @@ +output "backend_url" { + value = yandex_serverless_container.backend.url + description = "Публичный endpoint backend serverless container" +} + +output "frontend_url" { + value = var.enable_frontend_container ? yandex_serverless_container.frontend[0].url : null + description = "Публичный endpoint frontend-контейнера (если включён)" +} + +output "static_bucket_name" { + value = yandex_storage_bucket.static_site.bucket + description = "Имя бакета со статическим фронтом" +} + +output "static_site_endpoint" { + value = "https://${yandex_storage_bucket.static_site.bucket}.website.yandexcloud.net" + description = "URL статического сайта" +} + +output "static_site_access_key" { + value = yandex_iam_service_account_static_access_key.static_site.access_key + description = "Access key для загрузки файлов в бакет" +} + +output "static_site_secret_key" { + value = yandex_iam_service_account_static_access_key.static_site.secret_key + description = "Secret key для загрузки файлов (храните в секрете)" + sensitive = true +} + +output "api_gateway_endpoint" { + value = var.create_api_gateway ? "https://${yandex_api_gateway.backend[0].domain}" : null + description = "Публичный endpoint API Gateway (null если не создан, так как уже существует)" +} + + diff --git a/infrastructure/serverless/templates/api-gateway.yaml.tmpl b/infrastructure/serverless/templates/api-gateway.yaml.tmpl new file mode 100644 index 0000000..2886b5c --- /dev/null +++ b/infrastructure/serverless/templates/api-gateway.yaml.tmpl @@ -0,0 +1,48 @@ +openapi: 3.0.0 +info: + title: DevOps Portfolio API Gateway + version: 1.0.0 +x-yc-apigateway-cors: + allowOrigins: +%{ for origin in allowed_origins ~} + - "${origin}" +%{ endfor ~} + allowMethods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + allowHeaders: + - "*" + exposeHeaders: + - "*" +paths: + /: + get: + summary: Backend root + operationId: root + responses: + "200": + description: OK + x-yc-apigateway-integration: + type: http + httpMethod: GET + url: ${backend_base_url}/ + /api/{proxy+}: + x-yc-apigateway-any-method: + summary: Proxy API calls + parameters: + - name: proxy + in: path + required: true + schema: + type: string + x-yc-apigateway-integration: + type: http + httpMethod: ANY + url: ${backend_base_url}/api/{proxy} + headers: + X-Forwarded-Host: "{request.path.host}" + diff --git a/infrastructure/serverless/terraform.tfvars.example b/infrastructure/serverless/terraform.tfvars.example new file mode 100644 index 0000000..45ecc82 --- /dev/null +++ b/infrastructure/serverless/terraform.tfvars.example @@ -0,0 +1,34 @@ +yc_token = "" +yc_cloud_id = "" +yc_folder_id = "" +yc_zone = "ru-central1-a" + +backend_image = "cr.yandex/your_registry/backend:latest" +backend_memory_mb = 256 +backend_cpu = 0.1 +backend_concurrency = 8 +backend_env = {} + +frontend_image = "cr.yandex/your_registry/frontend:latest" +frontend_memory_mb = 128 +frontend_cpu = 0.05 +frontend_concurrency = 8 +frontend_env = {} +enable_frontend_container = false + +container_registry_id = "crpXXXX" + +environment = "dev" +static_bucket_name = "kulibin-devops-portfolio" +static_allowed_origins = [ + "https://kulibin-devops-portfolio.website.yandexcloud.net", +] +api_allowed_origins = [ + "https://kulibin-devops-portfolio.website.yandexcloud.net", + "http://localhost:3000", + "http://localhost:5173", +] + + + + diff --git a/infrastructure/serverless/variables.tf b/infrastructure/serverless/variables.tf new file mode 100644 index 0000000..d12ba73 --- /dev/null +++ b/infrastructure/serverless/variables.tf @@ -0,0 +1,156 @@ +variable "yc_token" { + description = "IAM-токен для доступа к Yandex Cloud. Получить через 'yc iam create-token'." + type = string + sensitive = true +} + +variable "yc_cloud_id" { + description = "ID облака (cloud_id). См. 'yc config list'." + type = string +} + +variable "yc_folder_id" { + description = "ID каталога (folder_id). См. 'yc config list'." + type = string +} + +variable "yc_zone" { + description = "Зона размещения ресурсов, например ru-central1-a." + type = string +} + +variable "project_name" { + description = "Идентификатор проекта для тегов/имен." + type = string + default = "devops-portfolio-serverless" +} + +variable "environment" { + description = "Окружение (dev/stage/prod) для тегов." + type = string + default = "dev" +} + +variable "extra_labels" { + description = "Дополнительные теги для ресурсов." + type = map(string) + default = {} +} + +variable "backend_image" { + description = "Публичный URL docker-образа backend-API (например, из Yandex Container Registry)" + type = string +} + +variable "backend_memory_mb" { + description = "ОЗУ backend, МБ (минимум 128, практично 256-512)" + type = number + default = 256 +} + +variable "backend_cpu" { + description = "CPU backend, в ядрах (например 0.05, 0.1, 0.25, 1)" + type = number + default = 0.1 +} + +variable "backend_concurrency" { + description = "Максимум одновременных запросов (чем выше — тем выше instant потребление ресурсов)" + type = number + default = 8 +} + +variable "backend_env" { + description = "Переменные окружения для backend (map, можно пустой)" + type = map(string) + default = {} +} + +variable "frontend_image" { + description = "Публичный URL docker-образа фронтенда (например, из Yandex Container Registry)" + type = string +} + +variable "frontend_memory_mb" { + description = "ОЗУ frontend, МБ (128-512 обычно достаточно)" + type = number + default = 128 +} + +variable "frontend_cpu" { + description = "CPU frontend, в ядрах (например 0.05, 0.1, 0.25, 1)" + type = number + default = 0.05 +} + +variable "frontend_concurrency" { + description = "Максимум одновременных запросов (для фронта обычно хватает 4-16)" + type = number + default = 8 +} + +variable "frontend_env" { + description = "Переменные окружения для frontend (map, можно пустой)" + type = map(string) + default = {} +} + +variable "enable_frontend_container" { + description = "Включить ли развёртывание frontend как serverless контейнера (по умолчанию используем Object Storage)." + type = bool + default = false +} + +variable "container_registry_id" { + description = "ID реестра образов (например crp50gpc30l3tbd4rtj0)." + type = string +} + +variable "static_bucket_name" { + description = "Имя бакета для статического фронтенда (должно быть уникальным)." + type = string +} + +variable "static_default_storage_class" { + description = "Класс хранения (standard / cold / ice)." + type = string + default = "standard" +} + +variable "static_max_size" { + description = "Максимальный размер бакета в байтах (0 = без ограничений)." + type = number + default = 0 +} + +variable "static_index_document" { + description = "Главная страница статического сайта." + type = string + default = "index.html" +} + +variable "static_error_document" { + description = "Страница ошибки (для SPA можно оставить index.html)." + type = string + default = "index.html" +} + +variable "static_allowed_origins" { + description = "Разрешённые домены (CORS) для доступа к статике." + type = list(string) + default = ["*"] +} + +variable "api_allowed_origins" { + description = "Origin'ы, которым API Gateway разрешает доступ." + type = list(string) + default = ["https://example.website.yandexcloud.net", "http://localhost:3000", "http://localhost:5173"] +} + +variable "create_api_gateway" { + description = "Создавать ли API Gateway (false если уже существует и не поддерживает импорт). TODO: ТРЕБУЕТСЯ ПРАВКА - автоматическое обнаружение существующего API Gateway." + type = bool + default = true +} + + diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..d05716f --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,176 @@ +# Kubernetes манифесты для DevOps Portfolio + +Этот каталог содержит Kubernetes манифесты для развертывания приложения в кластере. + +## О minikube и kubectl + +**minikube** — инструмент для запуска локального Kubernetes кластера на одной машине. Он создает виртуальную машину (или использует Docker) с полноценным Kubernetes кластером для разработки и тестирования. + +**kubectl** — командная утилита для управления Kubernetes кластерами. Позволяет развертывать приложения, управлять ресурсами, просматривать логи и т.д. + +## Структура + +- `backend-deployment.yaml` - Deployment для backend сервиса +- `backend-service.yaml` - Service для backend (ClusterIP) +- `frontend-deployment.yaml` - Deployment для frontend сервиса +- `frontend-service.yaml` - Service для frontend (NodePort) + +## Установка + +**На Arch Linux:** +```bash +sudo pacman -S kubectl minikube +``` + +## Запуск minikube + +```bash +# Запуск minikube с драйвером docker +minikube start --driver=docker + +# Проверка статуса +minikube status + +# Проверка подключения к кластеру +kubectl get nodes +``` + +## Работа с Docker образами в minikube + +**Важно:** Minikube использует свой собственный Docker daemon, отдельный от локального Docker. Поэтому образы, собранные локально, не видны в minikube. + +**Рекомендуемый подход (приближенный к CI/CD):** + +Этот подход имитирует реальный процесс CI/CD, где образы собираются локально, а затем загружаются/публикуются в registry (в нашем случае - в minikube): + +```bash +# 1. Собираем образы локально (как будет в CI/CD) +cd backend +docker build -t devops-portfolio-backend:latest . +cd ../frontend +docker build -t devops-portfolio-frontend:latest . + +# 2. Загружаем образы в minikube (имитация публикации в registry) +minikube image load devops-portfolio-backend:latest +minikube image load devops-portfolio-frontend:latest + +# 3. Проверяем, что образы загружены +minikube image ls | grep devops-portfolio +``` + +**Альтернативный подход (для разработки):** + +Если нужно собирать образы напрямую в minikube (быстрее для разработки, но не соответствует CI/CD процессу): + +```bash +# Настраиваем окружение для использования Docker daemon minikube +eval $(minikube -p minikube docker-env) + +# Теперь docker команды работают с minikube +cd backend && docker build -t devops-portfolio-backend:latest . +cd ../frontend && docker build -t devops-portfolio-frontend:latest . + +# Возвращаемся к локальному Docker +eval $(minikube -p minikube docker-env -u) +``` + +**Почему первый подход лучше:** +- Имитирует реальный CI/CD процесс (сборка → публикация → развертывание) +- Образы можно переиспользовать +- Проще отлаживать проблемы с образами + +## Полезные команды minikube + +```bash +# Просмотр логов +minikube logs + +# Открытие dashboard +minikube dashboard + +# Остановка кластера +minikube stop + +# Удаление кластера +minikube delete + +# Получение IP адреса кластера +minikube ip +``` + +## Развертывание в minikube + +### 1. Подготовка образов + +```bash +# Собираем образы локально +cd backend +docker build -t devops-portfolio-backend:latest . +cd ../frontend +docker build -t devops-portfolio-frontend:latest . + +# Загружаем образы в minikube +minikube image load devops-portfolio-backend:latest +minikube image load devops-portfolio-frontend:latest + +# Проверяем загрузку +minikube image ls | grep devops-portfolio +``` + +### 2. Развертывание + +```bash +# Применяем манифесты +kubectl apply -f k8s/ + +# Проверяем статус +kubectl get deployments +kubectl get services +kubectl get pods + +# Просмотр логов +kubectl logs -f deployment/backend +kubectl logs -f deployment/frontend +``` + +### 3. Доступ к приложению + +```bash +# Получаем URL для frontend +minikube service frontend --url + +# Или используем NodePort напрямую +minikube ip +# Затем откройте http://:30080 +``` + +### 4. Проверка работы + +```bash +# Проверяем статус подов +kubectl get pods + +# Проверяем логи +kubectl logs -f deployment/backend +kubectl logs -f deployment/frontend + +# Проверяем доступность через curl +MINIKUBE_IP=$(minikube ip) +curl http://$MINIKUBE_IP:30080 +curl http://$MINIKUBE_IP:30080/api/about +``` + +### 5. Удаление + +```bash +kubectl delete -f k8s/ +``` + +## Особенности конфигурации + +- **Backend**: 2 реплики, ClusterIP сервис (внутренний доступ) +- **Frontend**: 2 реплики, NodePort сервис (внешний доступ на порту 30080) +- **Health checks**: Настроены liveness и readiness пробы +- **Resources**: Ограничения по памяти и CPU для каждого контейнера +- **ImagePullPolicy: Never**: Используем локальные образы в minikube +- **Универсальный nginx.conf**: Использует `backend:8000` - работает в Docker Compose, Kubernetes и Helm diff --git a/k8s/backend-deployment.yaml b/k8s/backend-deployment.yaml new file mode 100644 index 0000000..5e3f694 --- /dev/null +++ b/k8s/backend-deployment.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + labels: + app: backend + component: api +spec: + replicas: 2 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + component: api + spec: + containers: + - name: backend + image: devops-portfolio-backend:latest + imagePullPolicy: Never # Используем локальные образы в minikube + ports: + - containerPort: 8000 + name: http + env: + - name: PYTHONUNBUFFERED + value: "1" + - name: PYTHONDONTWRITEBYTECODE + value: "1" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + diff --git a/k8s/backend-service.yaml b/k8s/backend-service.yaml new file mode 100644 index 0000000..96b20a7 --- /dev/null +++ b/k8s/backend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend + labels: + app: backend + component: api +spec: + type: ClusterIP + ports: + - port: 8000 + targetPort: 8000 + protocol: TCP + name: http + selector: + app: backend + diff --git a/k8s/frontend-deployment.yaml b/k8s/frontend-deployment.yaml new file mode 100644 index 0000000..19f47fd --- /dev/null +++ b/k8s/frontend-deployment.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + app: frontend + component: web +spec: + replicas: 2 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + component: web + spec: + containers: + - name: frontend + image: devops-portfolio-frontend:latest + imagePullPolicy: Never # Используем локальные образы в minikube + ports: + - containerPort: 80 + name: http + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "200m" + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + diff --git a/k8s/frontend-service.yaml b/k8s/frontend-service.yaml new file mode 100644 index 0000000..dade147 --- /dev/null +++ b/k8s/frontend-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app: frontend + component: web +spec: + type: NodePort + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + nodePort: 30080 # Внешний порт для доступа к frontend + selector: + app: frontend + diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..78cdc3c --- /dev/null +++ b/roadmap.md @@ -0,0 +1,54 @@ +# Дорожная карта проекта + +- [ ] Делаем проект сайт визитку, по аналогии с https://tosinonikute.com/ + - [x] Определимся на какой технологии его делаем (Python FastAPI + React) + - [x] Организовать хранение проекта на Git + - [x] Делаем MVP + - [x] Делаем заготовку + - [x] Проверяем что заготовка работает + - [x] Наполняем заготовку базовым содержимым (пока пропускаем, это же не MPV иначе получается) + - [x] Контейнеризуем приложение (Dockerfile, Docker Compose) + - [x] Формируем Dockerfile + - [x] для бэкенда + - [x] для фронтенда + - [x] Собираем Docker образы локально + - [x] Тестируем образы локально (запуск и проверка работы) + - [x] Формируем файл Docker Compose + - [x] Тестируем приложение через Docker Compose + - [x] Отрабатываем локальное развёртывание + - [x] Настраиваем локальное окружение (minikube) + - [x] Создаём Kubernetes манифесты + - [x] Тестируем и отлаживаем развертывание в minikube + - [x] Настраиваем Helm charts + - [x] Тестируем развертывание через Helm + - [x] Настраиваем Terraform для инфраструктуры + - [x] Подготавливаем облачное развёртывание + - [x] Выбор облачного провайдера (Yandex Cloud) + - [x] Настройка доступа к облаку (установлен yc, выполнены yc init и yc config list) + - [x] Настройка инфраструктуры через Terraform для serverless bundle (Yandex Serverless Containers) + - [x] Подключение облачного provider в Terraform + - [x] Описание backend/frontend как serverless containers + - [x] Настройка сервисного аккаунта и IAM (invoker, registry) + - [x] Развёртывание через terraform и получение публичных endpoint-адресов + - [x] Тестирование развернутых сервисов через endpoints (по отдельности) + - [x] Настройки сети внутри облака + - [x] Настроить статический хостинг фронтенда в Object Storage (`kulibin-devops-portfolio.website.yandexcloud.net`) + - [x] Перевести frontend в Object Storage (Terraform, отключить serverless frontend container) + - [x] Настроить API Gateway как прокси к backend serverless container (публичный URL) + - [x] Включить CORS/авторизацию для API, выдать gateway роль `serverless.containers.invoker` + - [x] Проверка, что приложение доступно из Интернета + - [x] Настраиваем CI/CD пайплайн + - [x] Настроить GitHub Actions/GitLab CI для сборки backend-образа и публикации в YCR + - [x] Добавить шаг сборки фронтенда и выгрузки `dist` в Object Storage через CLI + - [x] Подготовить отдельный job для `terraform apply`/`yc serverless` после публикации артефактов + - [x] Подключить секреты (tokens/keys) и уведомления о статусе пайплайна + - [x] Выполнить тестирование CI + - [ ] Мержим проект в master + - [ ] Настраиваем мониторинг (Prometheus, Grafana) + - [ ] Настраиваем логирование + - [ ] Настраиваем GitOps (ArgoCD/Flux) + - [ ] Настраиваем DevSecOps практики + - [ ] Документируем проект (README, архитектура) + - [ ] Переименовать папку "devops-portfolio" в "helm", проверив работоспособность и отразив это в документации + - [ ] Перенести папки "backend" и "frontend" в "src", проверив работоспособность и отразив это в документации + - [ ] Наполняем заготовку базовым содержимым \ No newline at end of file