# 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 🟢 closed 2026-05-26 — 7/7 (Step 6 descoped) + extension: bookva-minio container + external port-bind для bookva-db/bookva-minio. ## Post-closure follow-up в той же сессии - **bookva-minio container added** (user request — separate MinIO instance вместо bucket-split в shared MinIO). Image `minio:RELEASE.2020-07-13` (parity с books-minio), volume bookva-minio-data (1.2G — cp -a `/usr/docker/minio/data/books`), Portainer stack ID 49. Same MINIO creds для zero-config-change. Bookva apps reach через `http://bookva-minio:9000` (internal). bookva-overlay templates updated (s3.endpoint, bucket=books). - **External access для bookva-db** + **bookva-minio**: port-bind на ALT портах (books-db занимает 3306, books-minio занимает 9000). - `bookva-db` → `89.253.255.133:33306` (MariaDB). Verified — handshake `10.6.26-MariaDB-ubu2204` ✓. - `bookva-minio` → `http://89.253.255.133:9001` (S3 API). Verified — 403 anon (sig required) = service alive ✓. - Traefik HTTPS labels добавлены для `bookva-minio.kzntsv.site` (активируется когда DNS A-record добавлен в reg.ru). - **Step 6 descoped** — PWA redirect modal для одного user'а (id=1, Bookva founder) = over-engineering. Substitute = manual heads-up учредителю Bookva перед cutover. Trade-off acceptable (краткое недоумение «где Bookva?» если откроет старый bookmark post-cutover). ## Closure note Scope полностью выполнен per acceptance кроме Step 6 (delayed by design) и одной дискретной деференции (bookva-ozon-mcp stack — image отсутствует в registry, требует build в victor/books). Все остальные шаги closed: - **Step 1** ✅ 7 Gitea secrets созданы (BOOKVA_OVERLAY_TOKEN scope=read:repository, HEALTHCHECK_BOOKVA_{API,WEB}_URL, PORTAINER_STACK_ID_BOOKVA_{API,WEB,SCHEDULER,OPS_MCP}). - **Step 2** ✅ 9/10 Portainer stacks (ozon-mcp deferred). User-facing stopped pre-cutover (Status=2), infra (db/mongo/es) активны. - **Step 3** ✅ 6 volumes + 4 populated из corrected overlay templates. bookva-db-data (4.7G), bookva-mongo-data (517M) cp -a с downtimes 59s + 5s. MinIO bucket copy deferred (cutover-time через `mc cp books bookva`). - **Step 4** ✅ bookva-db login-gate `UPDATE users SET password=UUID() WHERE id_user NOT IN (1,2)` — 3 users (id≥3) scrambled, books-db verified untouched. - **Step 5** ✅ DNS bookseller.kzntsv.site pre-existed (closed by inspection). - **Step 6** 🔵 delayed — выставить `NUXT_PUBLIC_BOOKVA_REDIRECT_URL` за 1-2 нед до cutover. - **Step 7** ✅ books-web embed-api live (Portainer stack 24 PUT + books `75ea320` push). bookva.kzntsv.site/api/* via embed. books-api stack fallback 24-48ч soak до stop micro-task'и. Discovered + fixed cross-repo (bookva-overlay 4 commits, books 2 commits): - bookva-overlay `5b173de` — config-templates align prod shape + design v3 hostname - bookva-overlay `264373d` — .env.example design v3 hostnames - bookva-overlay `23eb374` — db.compose.yml mariadb:10.11 → 10.6 (match books-db для cp -a) - bookva-overlay `93c2ea2` — mongo.compose.yml mongo:7 → 4.2 (WiredTiger format compat) - bookva-overlay `2b20a3e` — api.compose.yml remove traefik (internal-only per v3) - bookva-overlay `8bffd68` — depends_on dropped (per-stack model) - books `2f7b539` — secrets.GITEA_TOKEN → BOOKVA_OVERLAY_TOKEN (Gitea reserved prefix) - books `75ea320` — deploy/web.compose.yml embed-api refs (NUXT_AUTH_TOKEN + NUXT_JWT_SECRET_KEY) Workflow E2E verified: dispatch tenant=bookva, all 4 svc deploy gracefully — BOOKVA_OVERLAY_TOKEN clone bookva-overlay OK, скип per-svc если stack_id отсутствовал (до записи). ## Where I stopped closed — handoff to `bookva-tenant-cutover` task (когда user скажет «давай поднимать Bookva»). Cutover task должна: re-start stopped Portainer stacks (api/web/scheduler/task-runner/ops-mcp/ntfy), `mc cp books bookva` MinIO bucket, ES snapshot+restore from shared kzntsv → bookva-es (Phase 2), smoke с двух сетей, books-api stack stop после 24-48ч soak post-Step-7. ## 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