# bookva-tenant-cutover-prep Подготовительные ops-шаги на books VDS перед cutover Bookva tenant в отдельный стек на `bookseller.kzntsv.site`. Code/CI part сделан в victor/books master (см. dev-side ниже). ## Контекст dev-source - Дизайн: victor/books `.wiki/concepts/tenant-split.md` rev v3 (2026-05-25). - Overlay-репо: `victor/bookva-overlay` + `victor/slovo-overlay` (compose'ы + config-templates). - Code-side done в master: - `5e28fd1` embed-api в web (server/api/*) + zod^4 fix. - `4a9cafc` ES endpoint per-tenant через `custom-environment-variables.json`. - `c1e58cf` BookvaRedirectBanner — PWA handoff modal для user.id=1. - `88df172` deploy.yml per-tenant (tenant input + per-tenant stack IDs + bookva-overlay clone). ## Goal Bookva стек готов взлететь — все secrets/stacks/volumes/DNS/PWA-redirect на месте. Сам `compose up` для bookva = последний шаг (отдельная задача `bookva-tenant-cutover` будет создана когда юзер скажет). ## Plan ### Step 1 — Gitea secrets (admin via gitea UI или API) Добавить в repo `victor/books` settings → Secrets: ``` PORTAINER_STACK_ID_BOOKVA_API PORTAINER_STACK_ID_BOOKVA_WEB PORTAINER_STACK_ID_BOOKVA_SCHEDULER PORTAINER_STACK_ID_BOOKVA_OPS_MCP GITEA_TOKEN # read-access к victor/bookva-overlay HEALTHCHECK_BOOKVA_API_URL # опционально, http healthcheck HEALTHCHECK_BOOKVA_WEB_URL ``` Значения stack ID — после Step 2. После Phase 2 cutover (отдельная задача) — переименовать `PORTAINER_STACK_ID_{API,WEB,SCHEDULER,OPS_MCP}` → `PORTAINER_STACK_ID_SLOVO_*` для symmetry. До тех пор deploy.yml использует fallback на legacy имена. ### Step 2 — Portainer bookva stacks В Portainer (https://portainer.kzntsv.site) создать stacks из overlay-compose: - `bookva-db` — `~/projects/bookva-overlay/deploy/db.compose.yml` - `bookva-mongo` — `mongo.compose.yml` - `bookva-es` — `elasticsearch.compose.yml` (новый, в-stack ES 7.10.0) - `bookva-api` — `api.compose.yml` - `bookva-web` — `web.compose.yml` - `bookva-scheduler` — `scheduler.compose.yml` - `bookva-task-runner` — `task-runner.compose.yml` - `bookva-ozon-mcp` — `ozon-mcp.compose.yml` - `bookva-ops-mcp` — `ops-mcp.compose.yml` - `bookva-ntfy` — `ntfy.compose.yml` Env-vars stack-level (Portainer Stack → Environment): - `CORE_SHA` — последний master tag из registry (например `master-5e28fd1`). - `HOSTNAME_API` / `HOSTNAME_WEB` — `bookseller.kzntsv.site` (per design v3). - `HOSTNAME_NTFY` — `push.bookseller.kzntsv.site`. После создания каждого stack — Portainer присваивает ID → записать в Gitea secrets (Step 1). ### Step 3 — Volume create + copy (stateful split) См. отдельную задачу [stateful-split-volume-copy](stateful-split-volume-copy.md) — MariaDB. Дополнительно для Phase 2 нужны (текущая задача охватывает только MariaDB): - `bookva-mongo-data` — copy с `books-job-scheduler-mongo_data` через `cp -a` + `deleteMany({'data.idSeller': 2})` для cleanup чужих agenda jobs. - `bookva-es-data` — пустой volume; данные через ES snapshot+restore из shared kzntsv ES в Phase 2. - `bookva-minio-data` ИЛИ FS-rename `books` → `slovo` + `cp -a slovo bookva` в shared MinIO контейнере. - Config volumes — `bookva-{api,scheduler,task-runner}-config` + `bookva-web-branding` + `bookva-ntfy-data` (см. README в bookva-overlay для bootstrap). ### Step 4 — bookva-db login-gate После volume copy + bookva-db up: ```sql -- В bookva-db только (slovo-db не трогать!): UPDATE users SET password = SHA2(UUID(), 256) WHERE id NOT IN (1, 2); ``` (точная команда — после проверки актуального password storage format в `packages/api/server/services/auth/*` или embedded `packages/web/server/api/auth/login.post.js`.) Эффект: только id=1 (Bookva-учредитель) + id=2 (Slovo-учредитель) могут логиниться в `bookseller.kzntsv.site`. Остальные user-записи остаются, но без работающего пароля. ### Step 5 — DNS Добавить A-record `bookseller.kzntsv.site` → books-vds IP (тот же что `bookva.kzntsv.site` сейчас). После cutover (отдельная задача) — `bookva.kzntsv.site` остаётся на тот же IP (роутит на slovo стек). ### Step 6 — PWA redirect handoff (за 1-2 недели до cutover) В Portainer для текущего `books-web` stack (= future slovo-web после cutover) добавить env: ``` NUXT_PUBLIC_BOOKVA_REDIRECT_URL=https://bookseller.kzntsv.site ``` После DEPLOY_AT bump → recreate web container → новый SW bundle подхватывает `BookvaRedirectBanner` компонент. user.id=1 видит modal «Bookva теперь на bookseller.kzntsv.site → Перейти». ### Step 7 — books-web embedded-api env (pending step из embed-api handoff) В Portainer `books-web` stack environment: ADD: ``` NUXT_AUTH_TOKEN=<значение из books-api.NITRO_AUTH_TOKEN> NUXT_JWT_SECRET_KEY=<значение из books-api.NITRO_JWT_SECRET_KEY> ``` CHANGE/REMOVE: ``` NUXT_PUBLIC_BOOKS_API_URL=/api # было https://books.kzntsv.site, переключаем на embedded NUXT_PUBLIC_BOOKS_API_KEY= # удалить (legacy) ``` После DEPLOY_AT bump → recreate. Embedded api начнёт обслуживать `/api/*` (вместо старого books-api стека). После 24-48ч стабильности — остановить `books-api` stack (отдельная micro-task). ## Acceptance - Gitea secrets выставлены (Step 1). - Portainer stacks `bookva-*` созданы (Step 2), stack IDs в Gitea secrets. - Volumes `bookva-*` существуют + data populated (Step 3). - bookva-db login-gate применён (Step 4). - DNS `bookseller.kzntsv.site` → books-vds (Step 5). - `NUXT_PUBLIC_BOOKVA_REDIRECT_URL` в books-web env (Step 6) — за 1-2 недели до cutover. - books-web env обновлён для embedded api (Step 7). - Acceptance не включает `compose up bookva-*` — это **отдельная** задача `bookva-tenant-cutover` (создаётся когда юзер скажет «давай поднимать Bookva»). ## Status ready ## Where I stopped (not started — все code/CI prep на dev-стороне closed, осталась только VDS/Portainer работа) ## Next action Step 1 в Gitea UI: добавить bookva secrets (значения placeholder). Шаги 2-7 — по порядку под maintenance window. ## Blocker — ## Branch main