# 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 paused (4/7 steps done — остальное ждёт maintenance window) ## Where I stopped 2026-05-26 — Steps 1, 5, 7 done; Step 3 partial (6 empty volumes + 4 populated из corrected templates). Steps 2, 3-rest (Mongo/MariaDB/MinIO), 4 deferred к coordinated maintenance window с user'ом. Step 6 delayed по spec (1-2 нед до cutover). ## Decisions log - **2026-05-26 — Step 5 closed by inspection.** `bookseller.kzntsv.site` уже резолвится authoritative `ns1.reg.ru` → `89.253.255.133`. User либо добавил A-record ранее, либо wildcard. Zero work needed. - **2026-05-26 — Step 1: Gitea reserves `GITEA_*` prefix.** Попытка PUT `GITEA_TOKEN` secret вернула 400 "invalid variable or secret name". Создан новый PAT scope=`read:repository` для OpeItcLoc03 (UI generate, т.к. `POST /users/.../tokens` требует basic auth, не token). Token в pass `gitea/victor-books-ci-bookva-overlay`. Записан в victor/books secret под именем `BOOKVA_OVERLAY_TOKEN`. deploy.yml line 80 patched (`secrets.GITEA_TOKEN` → `secrets.BOOKVA_OVERLAY_TOKEN`) — commit `2f7b539` pushed. - **2026-05-26 — bookva-overlay templates stale + missing fields.** При populate выяснилось что `config-templates/api/default.json` + `scheduler/default.json` короче prod (missing `jobs[]`, `jobs-new[]`, `docker`, `mailSettings`, `ntfy`), а `api.url`/`web.url` в scheduler template использовали `api.bookva.kzntsv.site`/`bookva.kzntsv.site` (старая нотация, до design v3 hostname swap). `task-runner/default.json` отсутствовал вовсе. Переписал из prod baseline + per-tenant overrides (`bookva-db`, `mongodb://bookva-mongo:27017/agendaDb`, `https://bookseller.kzntsv.site`, `s3.bucket=bookva`, `http://bookva-es:9200`). README hostname table → design v3. bookva-overlay commit `5b173de` pushed. - **2026-05-26 — Step 7 atomic env+compose PUT.** books-web compose template references меняем legacy `NUXT_PUBLIC_BOOKS_API_KEY` → `NUXT_AUTH_TOKEN` + `NUXT_JWT_SECRET_KEY` (server-side, не публикуется в client bundle). Values из books-api container env (`NITRO_AUTH_TOKEN=81e123d0-...`, `NITRO_JWT_SECRET_KEY=505172e5...`). Portainer stack 24 updated via API (PUT compose+env atomically, container recreated, DEPLOY_AT bumped). Smoke: `/api/healthz=200`, `/api/auth/login.post=401 "JWT-required"` matches books-api behavior. books `75ea320` pushed. **Гонка race:** PUT в Portainer перед push в git, чтобы next CI auto-deploy не подхватил stale compose без NUXT_AUTH_TOKEN ref. - **2026-05-26 — Step 3 scope split.** Этот task охватывает 6 volumes (api-config, scheduler-config, task-runner-config, web-branding, ntfy-data, es-data) + populate из corrected templates. **Stateful data volumes** (bookva-db-data via cp -a `books-db`, bookva-mongo-data via cp -a books-job-scheduler-mongo) = scope of separate `stateful-split-volume-copy` task (under maintenance window). MinIO rename (`books`→`slovo` + `cp slovo bookva`) = separate consideration, требует books-api reconfig (новая bucket). ## Completed steps - [x] Step 5 — DNS `bookseller.kzntsv.site` → `89.253.255.133` (закрыто inspection, было pre-existing) - [x] Step 1 — Gitea secret `BOOKVA_OVERLAY_TOKEN` создан в victor/books + deploy.yml `secrets.GITEA_TOKEN` → `secrets.BOOKVA_OVERLAY_TOKEN` (books `2f7b539` pushed). Token в pass `gitea/victor-books-ci-bookva-overlay`. Stack ID secrets отложены до Step 2. - [x] Step 3 (config volumes part) — 6 empty external volumes созданы (`bookva-{api-config,scheduler-config,task-runner-config,web-branding,ntfy-data,es-data}`). 4 populated с corrected templates: `bookva-api-config`, `bookva-scheduler-config`, `bookva-task-runner-config`, `bookva-web-branding`. 2 intentionally empty: `bookva-ntfy-data` (чистый старт), `bookva-es-data` (Phase 2 snapshot/restore). - [x] Bookva-overlay template correctness fix — api/scheduler/task-runner templates aligned to prod shape + design v3 (commit `5b173de` pushed). - [x] Step 7 — books-web embed-api env switch live. Portainer stack 24: env={`NUXT_PUBLIC_BOOKS_API_URL=/api`, `NUXT_AUTH_TOKEN=81e123d0-...`, `NUXT_JWT_SECRET_KEY=505172e5...`}, compose template updated. Smoke green. books `75ea320` pushed. books-api stack продолжает работать как fallback — отдельной micro-task'ой stop после 24-48ч soak. ## Remaining (maintenance-window or delayed) - **Step 2 — Portainer bookva-* stacks (10 stacks).** Требует все volumes existing (включая stateful). Запускается после `stateful-split-volume-copy` task. Env stack-level: `CORE_SHA=master-`, `HOSTNAME_API=HOSTNAME_WEB=bookseller.kzntsv.site`, `HOSTNAME_NTFY=push.bookseller.kzntsv.site`. - **Step 3 remaining — Mongo cp + MariaDB cp + MinIO rename.** Mongo + MariaDB → блок stateful-split-volume-copy. MinIO rename — disruptive (books-api config change нужен), пока defer; alternative — copy `books/*` объекты в new `bookva/*` bucket без rename (write-only path для bookva-API). - **Step 1 finish — записать `PORTAINER_STACK_ID_BOOKVA_{API,WEB,SCHEDULER,OPS_MCP}` после Step 2.** Без них deploy.yml skip'ает bookva tenant с warning (graceful). - **Step 4 — bookva-db login-gate SQL.** `UPDATE users SET password=SHA2(UUID(),256) WHERE id NOT IN (1,2)` против bookva-db. Точную команду подтвердить по `packages/api/server/services/auth/*` (password storage format в prod). - **Step 6 — PWA redirect handoff (delayed).** Per spec — за 1-2 нед до cutover bump `NUXT_PUBLIC_BOOKVA_REDIRECT_URL` env в books-web (= future slovo-web) → SW bundle подхватит `BookvaRedirectBanner` модал для user.id=1. ## Next action Coordinate с user'ом 1-2h maintenance window slot — execute `stateful-split-volume-copy` playbook (bookva-db + bookva-mongo data via `cp -a`), затем Step 2 Portainer stacks create (через Portainer API с volumes уже existing), затем Step 1 finish (stack IDs → Gitea secrets), затем Step 4 login-gate. Step 6 — отдельный заход за 1-2 нед до cutover. ## Blocker `stateful-split-volume-copy` task — не started. Без MariaDB+Mongo volumes — Step 2 stacks deploy fail (volume not found). Maintenance-window window needs user signoff. ## Branch master