diff --git a/cspell-words.txt b/cspell-words.txt index f459a738..ff9c2593 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1,6 +1,14 @@ # Глоссарий проекта Лидерра # Формат: одно слово на строке. Кириллица в нижнем регистре. +# Test-deploy Yandex Cloud (2026-05-21) +hba +htpasswd +lsb +nslookup +scp +хостить + # A4 design-tooling integration (v2.8 / v3.8 / v1.22) iconify diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index eb53cf00..4446f0ae 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-21T07:59:50.686Z +Last updated: 2026-05-21T08:00:35.867Z | Контролёр | Состояние | Детали | |---|---|---| diff --git a/docs/superpowers/plans/2026-05-21-test-deploy-yandex-cloud.md b/docs/superpowers/plans/2026-05-21-test-deploy-yandex-cloud.md new file mode 100644 index 00000000..c9623148 --- /dev/null +++ b/docs/superpowers/plans/2026-05-21-test-deploy-yandex-cloud.md @@ -0,0 +1,631 @@ +# Тестовый деплой портала Лидерра в Yandex Cloud — план + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task (inline — план содержит интерактивные шаги заказчика: создание VM, DNS, deploy-key). Steps use checkbox (`- [ ]`) syntax. + +**Goal:** Поднять рабочую копию портала в интернете на одной Linux-VM в Yandex Cloud по адресу `https://<поддомен>` с HTTPS, доступом только для заказчика+Claude, для ручного теста. + +**Architecture:** Одна Ubuntu 24.04 VM: nginx (HTTPS + Basic Auth) → PHP-FPM 8.3 → портал (Laravel 13 + собранный Vue) → PostgreSQL 16 + Redis 7 на той же машине; queue worker + scheduler как systemd-службы. Фронтенд собирается на dev-машине и заливается. Настоящие роли БД (RLS включён). Спека: `docs/superpowers/specs/2026-05-21-test-deploy-yandex-cloud-design.md`. + +**Tech Stack:** Yandex Cloud Compute, Ubuntu 24.04 LTS, nginx, PHP 8.3-FPM, PostgreSQL 16, Redis 7, Certbot/Let's Encrypt, systemd, OpenSSH. + +**Условные обозначения:** 🧑 = шаг заказчика (веб-интерфейс/решение), 🤖 = шаг Claude (Bash/SSH). Плейсхолдеры: ``, `` (например `test.example.ru`), ``/`` (дверь сайта) — заполняются по ходу. + +--- + +## Фаза 0 — Подготовка на dev-машине (🤖, до создания сервера) + +### Task 0.1: Проверить SSH-клиент и сгенерировать ключ деплоя + +**Files:** `~/.ssh/liderra_deploy`, `~/.ssh/liderra_deploy.pub` (на dev-машине) + +- [ ] **Step 1: Проверить наличие OpenSSH** + +Run: `ssh -V; ssh-keygen --help 2>&1 | Select-Object -First 1` +Expected: версия OpenSSH (например `OpenSSH_for_Windows_9.x`). Если нет — поставить «OpenSSH Client» через Settings → Optional Features. + +- [ ] **Step 2: Сгенерировать ключ-пару (без пароля, ed25519)** + +Run (PowerShell): + +```powershell +ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\liderra_deploy" -C "liderra-test-deploy" -N '""' +``` + +Expected: созданы `liderra_deploy` (приватный) и `liderra_deploy.pub` (публичный). + +- [ ] **Step 3: Показать публичный ключ заказчику** + +Run: `Get-Content "$env:USERPROFILE\.ssh\liderra_deploy.pub"` +Expected: строка `ssh-ed25519 AAAA... liderra-test-deploy`. Отдать заказчику для вставки при создании VM (Task 1.2). + +### Task 0.2: Код-правка — временный флаг доступа к админке (TDD) + +**Files:** + +- Modify: `app/config/app.php` (добавить ключ `saas_admin_test_bypass`) +- Modify: `app/app/Http/Middleware/EnsureSaasAdmin.php` +- Test: `app/tests/Feature/Middleware/EnsureSaasAdminTest.php` (создать или дополнить) + +- [ ] **Step 1: Написать падающий тест** + +Создать `app/tests/Feature/Middleware/EnsureSaasAdminTest.php`: + +```php +detectEnvironment(fn () => 'production'); + config(['app.saas_admin_test_bypass' => false]); + + // любой admin-маршрут под EnsureSaasAdmin; подставить реальный из routes + $response = get('/api/admin/tenants'); + expect($response->status())->toBe(503); +}); + +it('allows admin area in production when test bypass flag is on', function () { + app()->detectEnvironment(fn () => 'production'); + config(['app.saas_admin_test_bypass' => true]); + + $response = get('/api/admin/tenants'); + expect($response->status())->not->toBe(503); +}); +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `cd app; C:\tools\php83\php.exe artisan test --filter=EnsureSaasAdmin` +Expected: второй тест FAIL (сейчас middleware всегда 503 вне local/testing). + +- [ ] **Step 3: Добавить ключ конфига** + +В `app/config/app.php` добавить (рядом с другими ключами): + +```php + 'saas_admin_test_bypass' => (bool) env('SAAS_ADMIN_TEST_BYPASS', false), +``` + +- [ ] **Step 4: Поправить middleware** + +В `app/app/Http/Middleware/EnsureSaasAdmin.php` заменить тело `handle`: + +```php + public function handle(Request $request, Closure $next): Response + { + if (app()->environment('local', 'testing')) { + return $next($request); + } + + // ВРЕМЕННО (тест-деплой): пропускаем при включённом флаге. + // TODO: убрать после внедрения Yandex 360 SSO (Б-1 + DO-4). + if (config('app.saas_admin_test_bypass') === true) { + return $next($request); + } + + abort(503, 'SaaS-admin авторизация не настроена (ожидает Б-1 + DO-4).'); + } +``` + +- [ ] **Step 5: Запустить тест — зелёный** + +Run: `cd app; C:\tools\php83\php.exe artisan test --filter=EnsureSaasAdmin` +Expected: оба PASS. + +- [ ] **Step 6: Линт + commit** + +Run: `cd app; composer pint; composer stan` +Expected: 0 ошибок. + +```bash +git add app/config/app.php app/app/Http/Middleware/EnsureSaasAdmin.php app/tests/Feature/Middleware/EnsureSaasAdminTest.php +git commit -m "feat(deploy): temporary SAAS_ADMIN_TEST_BYPASS flag for test server (off by default)" +``` + +> NB: маршрут `/api/admin/tenants` в тесте — подставить реальный admin-маршрут из `app/routes/`. Уточнить на Step 1 (grep по `EnsureSaasAdmin`). + +### Task 0.3: Собрать фронтенд для прода + +- [ ] **Step 1: Прод-сборка** + +Run: `npm --prefix app run build` +Expected: создан `app/public/build/` с манифестом и ассетами, ошибок нет. + +- [ ] **Step 2: Зафиксировать факт сборки** + +Сборка не коммитится (build в .gitignore) — будет залита на сервер в Task 3.3 через scp. Проверить: `Test-Path app/public/build/manifest.json` → True. + +--- + +## Фаза 1 — Создание сервера (🧑 заказчик в консоли YC, по инструкции Claude) + +### Task 1.1: Зарезервировать статический публичный IP + +- [ ] **Step 1:** YC Console → Virtual Private Cloud → IP-адреса → «Зарезервировать адрес» → зона `ru-central1-a`. +- [ ] **Step 2:** Записать выданный IP → это `` (нужен для DNS; статический, чтобы адрес не менялся при перезагрузке). + +### Task 1.2: Создать виртуальную машину + +- [ ] **Step 1:** Compute Cloud → «Создать ВМ». +- [ ] **Step 2:** Параметры: + - Имя: `liderra-test`; зона `ru-central1-a`. + - Образ: **Ubuntu 24.04 LTS**. + - vCPU 2, RAM 2 ГБ, **гарантированная доля vCPU 20%** (дёшево; сборки идут на dev-машине). + - Диск: SSD 20 ГБ. + - Публичный адрес: выбрать **зарезервированный** из Task 1.1. + - Доступ: логин `deploy`; SSH-ключ — вставить публичный ключ из Task 0.1 Step 3. +- [ ] **Step 3:** Создать. Дождаться статуса RUNNING. + +### Task 1.3: Открыть порты (группа безопасности) + +- [ ] **Step 1:** VPC → Группы безопасности → группа сети ВМ → правила входящего трафика. +- [ ] **Step 2:** Разрешить TCP **22, 80, 443** (источник `0.0.0.0/0`; 22 можно сузить до IP заказчика/dev — но для простоты теста оставить открытым). +- [ ] **Step 3:** Сообщить Claude `` → переходим к Фазе 2. + +--- + +## Фаза 2 — Базовая настройка сервера (🤖 по SSH) + +### Task 2.1: Первое подключение + +- [ ] **Step 1: Подключиться** + +Run: `ssh -i "$env:USERPROFILE\.ssh\liderra_deploy" -o StrictHostKeyChecking=accept-new deploy@ "echo OK; lsb_release -d"` +Expected: `OK` + `Ubuntu 24.04`. + +- [ ] **Step 2: Обновить пакеты** + +Run: `ssh ... deploy@ "sudo apt-get update && sudo apt-get -y upgrade"` +Expected: завершается без ошибок. + +### Task 2.2: Установить стек + +- [ ] **Step 1: Установить пакеты** + +Run одной командой по SSH: + +```bash +sudo apt-get install -y nginx \ + php8.3-fpm php8.3-cli php8.3-pgsql php8.3-redis php8.3-mbstring \ + php8.3-xml php8.3-curl php8.3-bcmath php8.3-zip php8.3-gd php8.3-intl \ + postgresql postgresql-contrib redis-server git unzip certbot python3-certbot-nginx \ + apache2-utils +``` + +Expected: установлено без ошибок (`apache2-utils` даёт `htpasswd`). + +- [ ] **Step 2: Установить Composer** + +```bash +php -r "copy('https://getcomposer.org/installer','/tmp/ci.php');" \ + && sudo php /tmp/ci.php --install-dir=/usr/local/bin --filename=composer +``` + +Run: `ssh ... "composer --version; php -v | head -1"` +Expected: Composer 2.x; PHP 8.3. + +- [ ] **Step 3: Проверить службы** + +Run: `ssh ... "systemctl is-active nginx php8.3-fpm postgresql redis-server"` +Expected: `active` × 4. + +--- + +## Фаза 3 — База, код, конфиг (🤖 по SSH) + +> **Порядок исполнения внутри фазы:** 3.2 (код на сервере — db/-скрипты приезжают с репо) → 3.1 (БД и роли) → 3.3 (фронтенд) → 3.4 (.env) → 3.5 (схема через migrate + grants + seed). Здесь нумерация по смыслу, но db-скрипты есть только после clone. +> +> **DB-роли (из `db/00_create_roles.sql` v1.1 + `app/config/database.php`):** пароли передаются psql через `-v` (НЕ `ALTER ROLE`). Схема грузится миграцией `load_initial_schema` (она делает `DB::unprepared(schema.sql)`) под ролью `crm_migrator` (BYPASSRLS+CREATEDB). Гранты — `db/02_grants.sql`. Рантайм — `crm_app_user` (RLS). Supplier-джобы — `crm_supplier_worker` (BYPASSRLS) через connection `pgsql_supplier`. Connection `pgsql_migrator` в конфиге НЕТ → для миграций временно подменяем `DB_USERNAME` на `crm_migrator` (default-connection `pgsql`), потом возвращаем на `crm_app_user`. + +### Task 3.1: Создать БД и роли + +**Files (на сервере):** `db/00_create_roles.sql` (после clone в 3.2). + +- [ ] **Step 1: Сгенерировать пароли ролей (на dev или сервере)** + +Run: `ssh ... "for r in app admin migrator audit supplier; do echo \$r=\$(openssl rand -hex 16); done"` +Expected: 5 строк вида `app=...`. Сохранить как `` / `` / `` / `` / `` (в безопасное место, не в git). + +- [ ] **Step 2: Создать БД** + +```bash +ssh ... "sudo -u postgres createdb liderra" +``` + +Expected: без ошибок. + +- [ ] **Step 3: Создать роли с паролями (через -v)** + +```bash +ssh ... "sudo -u postgres psql -d liderra \ + -v crm_app_password='' \ + -v crm_admin_password='' \ + -v crm_migrator_password='' \ + -v crm_audit_writer_password='' \ + -v crm_supplier_worker_password='' \ + -f /var/www/liderra/db/00_create_roles.sql" +``` + +Run: `ssh ... "sudo -u postgres psql -d liderra -c '\du' | grep -E 'crm_(app|migrator|supplier)'"` +Expected: 5 ролей созданы (`crm_app_user`, `crm_admin_user`, `crm_migrator`, `crm_audit_writer`, `crm_supplier_worker`). + +- [ ] **Step 4: Разрешить TCP-вход ролям (pg_hba)** + +> Роли ходят через 127.0.0.1 (scram). Убедиться, что `pg_hba.conf` имеет строку `host all all 127.0.0.1/32 scram-sha-256` (на Ubuntu по умолчанию есть). Если нет — добавить и `sudo systemctl reload postgresql`. + +Run: `ssh ... "sudo grep -E '127.0.0.1/32' /etc/postgresql/16/main/pg_hba.conf"` +Expected: строка с `scram-sha-256` (или `md5`). + +### Task 3.2: Выложить код (deploy-key + clone) + +- [ ] **Step 1: Сгенерировать deploy-key на сервере** + +```bash +ssh ... "ssh-keygen -t ed25519 -f ~/.ssh/github_deploy -N '' -C 'liderra-server'; cat ~/.ssh/github_deploy.pub" +``` + +Expected: публичный ключ сервера. + +- [ ] **Step 2 (🧑): Добавить ключ в GitHub** + +Заказчик: GitHub → репо `CoralMinister/lidpotok` → Settings → Deploy keys → Add → вставить ключ (read-only, без write). + +- [ ] **Step 3: Настроить SSH для GitHub + clone** + +```bash +ssh ... 'cat >> ~/.ssh/config <`). + +- [ ] **Step 4: composer install** + +```bash +ssh ... "cd /var/www/liderra/app && composer install --no-dev --optimize-autoloader --no-interaction" +``` + +Expected: зависимости установлены, 0 ошибок. + +### Task 3.3: Залить собранный фронтенд + +- [ ] **Step 1: Скопировать build на сервер** + +Run (с dev-машины): + +```powershell +scp -i "$env:USERPROFILE\.ssh\liderra_deploy" -r app/public/build deploy@:/var/www/liderra/app/public/ +``` + +Expected: `manifest.json` + ассеты на сервере. + +### Task 3.4: Production .env + +- [ ] **Step 1: Создать .env на сервере** + +```bash +ssh ... 'cat > /var/www/liderra/app/.env < +APP_LOCALE=ru +APP_FALLBACK_LOCALE=ru +APP_TIMEZONE=Europe/Moscow + +LOG_CHANNEL=stack +LOG_LEVEL=warning + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=liderra +DB_USERNAME=crm_app_user +DB_PASSWORD= +DB_SUPPLIER_USERNAME=crm_supplier_worker +DB_SUPPLIER_PASSWORD= + +SESSION_DRIVER=redis +SESSION_LIFETIME=120 +QUEUE_CONNECTION=redis +CACHE_STORE=redis +REDIS_CLIENT=predis +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_FROM_ADDRESS="hello@" +MAIL_FROM_NAME=Liderra + +SAAS_ADMIN_TEST_BYPASS=true + +AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets +EOF' +``` + +- [ ] **Step 2: APP_KEY** + +```bash +ssh ... "cd /var/www/liderra/app && php artisan key:generate --force && php artisan about | head -20" +``` + +Expected: ключ сгенерирован; `Environment: production`, `Debug Mode: OFF`. + +### Task 3.5: Схема (migrate), гранты, демо-данные, кэши + +> Схему и сиды грузим под BYPASSRLS-ролью `crm_migrator`, потом возвращаем рантайм на `crm_app_user`. Подмена — временно правим `DB_USERNAME`/`DB_PASSWORD` в `.env` (это значения для default-connection `pgsql`, через которую идёт migrate/seed). + +- [ ] **Step 1: Временно переключить .env на crm_migrator** + +```bash +ssh ... "cd /var/www/liderra/app && \ + sed -i 's/^DB_USERNAME=.*/DB_USERNAME=crm_migrator/; s/^DB_PASSWORD=.*/DB_PASSWORD=/' .env && \ + grep -E '^DB_(USERNAME|PASSWORD)=' .env" +``` + +Expected: `DB_USERNAME=crm_migrator`. + +- [ ] **Step 2: Накатить схему (миграция load_initial_schema грузит schema.sql)** + +```bash +ssh ... "cd /var/www/liderra/app && php artisan migrate --force" +``` + +Run: `ssh ... "sudo -u postgres psql -d liderra -c '\dt' | tail -3"` +Expected: миграция `load_initial_schema` отработала; десятки таблиц (схема v8.27). + +- [ ] **Step 3: Создать партиции (как на dev — ручной cron вместо pg_partman)** + +```bash +ssh ... "cd /var/www/liderra/app && php artisan partitions:create-months" +``` + +Expected: партиции созданы (команда из ЭТАЛОН/project_phase1_strategy; если имя иное — `php artisan list | grep partition`). + +- [ ] **Step 4: Применить гранты** + +```bash +ssh ... "sudo -u postgres psql -d liderra -f /var/www/liderra/db/02_grants.sql" +``` + +Expected: гранты применены без ошибок (запуск под postgres-суперюзером — владелец/superuser, см. 00_create_roles doc вариант с crm_admin_user тоже подходит). + +- [ ] **Step 5: Демо-данные (под crm_migrator, BYPASSRLS — cross-tenant сид проходит)** + +```bash +# залить нужные демо-скрипты на сервер +scp -i "$env:USERPROFILE\.ssh\liderra_deploy" app/storage/_demo_5users.php app/storage/_demo_split_tenants.php deploy@:/var/www/liderra/app/storage/ +ssh ... "cd /var/www/liderra/app && php artisan db:seed --force && php artisan tinker storage/_demo_5users.php && php artisan tinker storage/_demo_split_tenants.php" +``` + +Expected: 5 компаний + учётки `admin@demo.local` / `manager1..4@demo.local` (пароль `password`). + +> NB: точный набор демо-скриптов сверить с ЭТАЛОН §4 (там же команда восстановления). Залить только нужные `_demo_*.php`. + +- [ ] **Step 6: Вернуть рантайм-роль crm_app_user** + +```bash +ssh ... "cd /var/www/liderra/app && \ + sed -i 's/^DB_USERNAME=.*/DB_USERNAME=crm_app_user/; s/^DB_PASSWORD=.*/DB_PASSWORD=/' .env && \ + grep -E '^DB_USERNAME=' .env" +``` + +Expected: `DB_USERNAME=crm_app_user` (RLS будет enforce'иться в рантайме). + +- [ ] **Step 7: Права и кэши** + +```bash +ssh ... 'cd /var/www/liderra/app \ + && sudo chown -R deploy:www-data storage bootstrap/cache \ + && sudo chmod -R 775 storage bootstrap/cache \ + && php artisan config:cache && php artisan route:cache && php artisan view:cache' +``` + +Expected: кэши собраны, прав хватает. + +--- + +## Фаза 4 — Веб, HTTPS, дверь (🤖 + 🧑 DNS) + +### Task 4.1: DNS A-запись (🧑) + +- [ ] **Step 1:** В панели домена создать запись `A` для `` → ``. +- [ ] **Step 2 (🤖): Проверить распространение** + +Run: `ssh ... "getent hosts || nslookup "` +Expected: резолвится в `` (может занять до 30–60 мин). + +### Task 4.2: nginx vhost (HTTP) + +- [ ] **Step 1: Конфиг сайта** + +```bash +ssh ... 'sudo tee /etc/nginx/sites-available/liderra <; + root /var/www/liderra/app/public; + index index.php; + + # дверь на весь сайт (Basic Auth), кроме webhook поставщика + location / { + auth_basic "Liderra test"; + auth_basic_user_file /etc/nginx/.htpasswd; + try_files \$uri \$uri/ /index.php?\$query_string; + } + + location ^~ /api/webhook/ { + auth_basic off; + try_files \$uri \$uri/ /index.php?\$query_string; + } + + location ~ \.php\$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php8.3-fpm.sock; + } +} +EOF +sudo ln -sf /etc/nginx/sites-available/liderra /etc/nginx/sites-enabled/liderra +sudo rm -f /etc/nginx/sites-enabled/default +sudo nginx -t && sudo systemctl reload nginx' +``` + +Expected: `nginx -t` syntax ok; reload без ошибок. + +> NB: точный префикс webhook (`/api/webhook/`) сверить с `app/routes/api.php` (grep `webhook`). Если иной — поправить `location ^~`. + +- [ ] **Step 2: Создать пароль двери** + +```bash +ssh ... "sudo htpasswd -bc /etc/nginx/.htpasswd " +``` + +Expected: `.htpasswd` создан. + +- [ ] **Step 3: Проверка по HTTP** + +Run: `ssh ... "curl -s -o /dev/null -w '%{http_code}' -u : http:///"` +Expected: `200` (или `302` на /login). Без креда → `401`. + +### Task 4.3: HTTPS (Let's Encrypt) + +- [ ] **Step 1: Выпустить сертификат** + +```bash +ssh ... "sudo certbot --nginx -d --non-interactive --agree-tos -m --redirect" +``` + +Expected: сертификат выпущен, nginx переписан на 443 + редирект с 80. + +- [ ] **Step 2: Проверить HTTPS + авто-продление** + +Run: `ssh ... "curl -sI -u : https:/// | head -1; sudo certbot renew --dry-run 2>&1 | tail -1"` +Expected: `HTTP/2 200|302`; dry-run `Congratulations` / success. + +--- + +## Фаза 5 — Фоновые службы (🤖) + +### Task 5.1: queue worker как systemd-служба + +- [ ] **Step 1: Юнит** + +```bash +ssh ... 'sudo tee /etc/systemd/system/liderra-queue.service </dev/null; echo "* * * * * cd /var/www/liderra/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1" ) | crontab -' +``` + +Run: `ssh ... "crontab -l | grep schedule:run"` +Expected: строка присутствует. + +--- + +## Фаза 6 — Приёмка и сопровождение (🤖) + +### Task 6.1: Проверка критериев готовности (DoD) + +- [ ] **Step 1: HTTPS + замочек** + +Открыть `https://` в браузере (с логином двери) → валидный сертификат, портал грузится. + +- [ ] **Step 2: Дверь работает** + +Run: `ssh ... "curl -s -o /dev/null -w '%{http_code}' https:///"` → `401` (без креда). + +- [ ] **Step 3: Вход + данные** + +В браузере: `admin@demo.local` / `password` → видно 4 демо-проекта. + +- [ ] **Step 4: Изоляция компаний (RLS)** + +Войти `manager1@demo.local` / `password` → видна только своя компания (чужих проектов нет). Если падает SQL — зафиксировать, чинить (риск из спеки §5.4). + +- [ ] **Step 5: Админка** + +Открыть `/admin/...` под админом → не 503 (флаг bypass работает). + +- [ ] **Step 6: Службы переживают перезагрузку** + +```bash +ssh ... "sudo reboot" # подождать ~40с +ssh ... "systemctl is-active nginx php8.3-fpm postgresql redis-server liderra-queue" +``` + +Expected: все `active`; сайт снова открывается. + +### Task 6.2: Скрипт обновления + инструкция + +**Files:** `/var/www/liderra/deploy.sh` (на сервере), `docs/deploy/test-server-runbook.md` (в репо) + +- [ ] **Step 1: deploy.sh** + +```bash +ssh ... 'cat > /var/www/liderra/deploy.sh < Фронтенд при обновлении: пересобрать на dev (`npm --prefix app run build`) и `scp` build на сервер ПЕРЕД запуском deploy.sh. + +- [ ] **Step 2: Runbook** + +Создать `docs/deploy/test-server-runbook.md`: адрес, доступы (где лежат пароли), команда обновления, как остановить/удалить VM (прекратить оплату), напоминание убрать `SAAS_ADMIN_TEST_BYPASS` при переходе к настоящему SSO. + +- [ ] **Step 3: Commit runbook** + +```bash +git add docs/deploy/test-server-runbook.md +git commit -m "docs(deploy): test-server runbook" +``` + +--- + +## Открытые вопросы (заполнить при исполнении) + +- `` и панель управления доменом — от заказчика. +- Точный admin-маршрут для теста (Task 0.2) и префикс webhook (Task 4.2) — grep по коду. +- Точные seed-шаги демо-учёток (Task 3.5) — по ЭТАЛОН §4. +- Пароли БД-ролей (``, ``, ``, ``, ``) + дверь сайта (``) — сгенерировать (Task 3.1 Step 1), сохранить в безопасном месте (не в git; занести в runbook-ссылку на хранилище). +- `pg_hba.conf` путь зависит от версии PG (`/etc/postgresql/16/main/`) — сверить на сервере. diff --git a/docs/superpowers/specs/2026-05-21-test-deploy-yandex-cloud-design.md b/docs/superpowers/specs/2026-05-21-test-deploy-yandex-cloud-design.md new file mode 100644 index 00000000..5bb12ca6 --- /dev/null +++ b/docs/superpowers/specs/2026-05-21-test-deploy-yandex-cloud-design.md @@ -0,0 +1,141 @@ +# Тестовый деплой портала Лидерра в Yandex Cloud — дизайн + +**Дата:** 2026-05-21 +**Статус:** черновик дизайна (brainstorming) → ожидает вычитки заказчиком → writing-plans +**Автор:** Claude + Дмитрий +**Тип:** инфраструктура / деплой (не фича приложения) + +## 1. Цель + +Поднять рабочую копию портала Лидерры в интернете по стабильному адресу с настоящим +HTTPS, чтобы её могли открывать **только заказчик (Дмитрий) и Claude** для сквозного +ручного тестирования. Это **тестовое/staging-окружение**, не продакшен: без юр.лица, +без реальной почты, без SSO, под снос в любой момент. + +## 2. Что НЕ входит (YAGNI / границы) + +- ❌ Yandex 360 SSO (корпоративный вход админов) — ждёт Б-1 (ООО). +- ❌ Реальный landing, реальная почта (Unisender Go), Sentry-мониторинг, бэкапы, + автодеплой из GitHub (CI/CD). +- ❌ Управляемые БД/Redis Yandex (Managed PostgreSQL/Redis) — это для будущего прода. +- ❌ Перенос текущей dev-базы — на сервере свежие демо-данные. +- ❌ Публичный доступ для чужих тестеров (для этого понадобились бы реальная почта, + закрытие админки, реальная изоляция — отдельный этап). + +## 3. Решения, принятые в brainstorming + +| Развилка | Выбор | +|---|---| +| Где хостить | Отдельный Linux-сервер в **Yandex Cloud** (вариант A — всё на одной VM) | +| Аккаунт YC | Заводится с нуля заказчиком (создан 21.05.2026: облако `cloud-sasha261185`, каталог `default`); ожидает привязки платёжного аккаунта + грант 60 дней | +| Адрес | **Свой домен** (поддомен вида `test.<домен>`) + настоящий HTTPS (Let's Encrypt) | +| Кто настраивает сервер | **Claude по SSH** с dev-машины; заказчик даёт доступ (вставляет публичный ключ при создании VM) | +| Архитектура | Вариант A — один сервер, нативная установка (nginx + PHP-FPM + PostgreSQL + Redis), без Docker, без управляемых сервисов | + +## 4. Архитектура сервера + +Одна VM (Ubuntu LTS, ~2 vCPU / 2–4 ГБ, диск 15–20 ГБ SSD, зона `ru-central1-a`): + +``` + интернет + │ + ваш домен (test.…) ──DNS A-запись──► публичный IP VM + │ + ┌───────┴─ nginx (HTTPS, Let's Encrypt авто-продление) ──────────┐ + │ • HTTP Basic Auth на весь сайт (пускает только нас двоих) │ + │ — кроме пути webhook поставщика (защищён HMAC-подписью) │ + └───────┬─────────────────────────────────────────────────────────┘ + │ + PHP-FPM 8.3 ← код портала + собранный фронтенд (public/build) + │ + ┌───────┼─────────┐ +PostgreSQL 16 Redis 7 (на этой же машине) + │ + systemd-службы: queue worker (queue:work redis) + scheduler + (php artisan schedule:run по cron) — переживают перезагрузку +``` + +**Поток выкладки кода:** + +1. Сервер тянет код из приватного репо `CoralMinister/lidpotok` по **read-only deploy-key** + (генерируется на сервере, заказчик добавляет в GitHub → Deploy keys). +2. `composer install --no-dev --optimize-autoloader`. +3. **Фронтенд собирается на dev-машине** (`npm --prefix app run build`) и заливается + (`app/public/build`) на сервер — чтобы не держать Node и не упираться в RAM при сборке. +4. Накат схемы БД (`db/schema.sql` v8.27) + демо-данные (seed + 5 учёток). +5. `php artisan config:cache route:cache view:cache`. + +**Обновление новой версии** (когда понадобится) — одна идемпотентная команда/скрипт: +`git pull` → composer → залить новый build → migrate → пересобрать кэши → перезапустить +php-fpm + queue. Оформлю как `deploy.sh` на сервере + короткую инструкцию. + +## 5. Безопасность теста + +1. **Edge-дверь:** nginx HTTP Basic Auth на весь сайт (один общий логин/пароль для нас + двоих; хранится в `/etc/nginx/.htpasswd`). Посторонние и поисковики сайт не видят. + Исключение — путь приёма лидов от поставщика (webhook, защищён HMAC), чтобы при + желании протестировать живой приём от `crm.bp-gr.ru`. +2. **Админка:** middleware `EnsureSaasAdmin` в проде отдаёт 503 (ждёт Yandex SSO). + Добавляется **минимальный временный флаг** `SAAS_ADMIN_TEST_BYPASS` (config + `app.saas_admin_test_bypass`, default `false`): когда `true` — middleware пропускает. + Включается только на тест-сервере, помечен «убрать после внедрения реального SSO». + Правка в коде — небольшая, закоммичена, по умолчанию выключена → прод не затронут. +3. **Боевой режим без утечек:** `APP_ENV=production`, `APP_DEBUG=false`. +4. **Реальная изоляция компаний (RLS):** на сервере подключаются настоящие роли БД + (`db/00_create_roles.sql` + `db/02_grants.sql`; приложение ходит как `crm_app_user`, + джобы — как `crm_supplier_worker` BYPASSRLS). В отличие от dev (postgres-суперюзер, + RLS обходится) — изоляция реально работает. + - ⚠️ **Риск:** RLS включается «вживую» впервые. Возможен запрос, работавший под + суперюзером и падающий под RLS. Реакция: чиню точечно либо временно ослабляю роль. + Считается полезным для теста. +5. **SSH:** доступ по ключу (пароли отключены); порт 22 в группе безопасности по + возможности ограничить IP dev-машины + заказчика. Открыты порты 80/443/22. +6. **Почта:** `MAIL_MAILER=log` (письма в лог, не на ящик) — не нужны, заходим под + готовыми демо-учётками. + +## 6. Данные + +Демо-набор как на dev: 5 изолированных компаний, входы `admin@demo.local` + +`manager1..4@demo.local`, пароль у всех `password`. Демо-данные — стираемые. + +## 7. Разделение работ + +**Заказчик (через веб-интерфейсы, по инструкции Claude):** + +1. Завершить регистрацию YC + привязать карту + забрать грант 60 дней. +2. Создать VM (Ubuntu), вставить публичный SSH-ключ Claude. +3. Сообщить публичный IP машины. +4. Прописать у домена A-запись `test.<домен>` → IP. +5. Добавить read-only deploy-key в GitHub-репо. +6. Придумать общий логин/пароль «двери» сайта. + +**Claude (по SSH, сам):** вся установка/настройка сервера, выкладка кода, сборка-загрузка +фронтенда, схема БД + демо-данные, HTTPS, systemd-службы, проверка (портал открывается, +логин работает, изоляция компаний работает), `deploy.sh` + инструкция обновления. + +**Доступ Claude:** только IP сервера + SSH по ключу, который Claude генерирует сам. +Паролей/карт заказчика Claude не получает. + +## 8. Стоимость и жизненный цикл + +- ~1000–1500 ₽/мес за VM (2 vCPU / 2–4 ГБ); грант 60 дней + до 10 000 ₽ — вероятно, + первый период бесплатно. +- Домен — ~200–1500 ₽/год (если ещё нет). +- Тест не нужен → VM остановить/удалить → оплата прекращается. + +## 9. Критерии готовности (Definition of Done) + +- По адресу `https://test.<домен>` открывается портал с валидным HTTPS-замочком. +- Сайт под Basic Auth (посторонний без логина не входит). +- Вход `admin@demo.local` / `password` работает; видны 4 демо-проекта. +- `manager1@demo.local` видит только свою компанию (RLS работает). +- Админка `/admin/*` доступна (через временный флаг). +- queue worker + scheduler работают как службы, переживают перезагрузку VM. +- Есть `deploy.sh` + инструкция «как выложить новую версию». + +## 10. Открытые мелочи (решим в плане) + +- Точный размер VM (2 ГБ vs 4 ГБ) — зависит от того, собираем ли фронт на сервере + (план: собираем на dev → 2 ГБ хватит). +- Точный путь webhook-исключения в nginx — уточнить по `routes/`. +- Имя поддомена и сам домен — от заказчика.