diff --git a/.wiki/concepts/books-vds-memory-optimize-runbook.md b/.wiki/concepts/books-vds-memory-optimize-runbook.md index 5770f7a..5e6b161 100644 --- a/.wiki/concepts/books-vds-memory-optimize-runbook.md +++ b/.wiki/concepts/books-vds-memory-optimize-runbook.md @@ -1,102 +1,3 @@ ---- -title: "books VDS — оптимизация памяти (mongo wiredTiger / ES heap / CI-остатки)" -type: concept -tags: [runbook, books, vds, mongo, elasticsearch, oom, memory, backup, admin] -related: [concepts/runbooks-index.md, concepts/gitea-project-create-runbook.md] -updated: 2026-08-28 ---- +# ⛔ Файловый канал закрыт -# books VDS — оптимизация памяти (OOM → ECONNREFUSED) - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -## Scope - -Остановить **OOM-цикл** на books VDS (`89.253.255.133`, host4g VPS, CentOS 7), который -вызывает `[sched:slovo] sync.failed: ECONNREFUSED 172.20.0.5:27017`. Причина — дефицит ОЗУ: -хост 3.9GB RAM, free ~136MB, **swap 100%**, OOM-kill `mongod` (dmesg `Out of memory: Kill -process (mongod) score ~114`). Симптом-задача — «ozon fbs products syncronization» (books-sched, прод slovo). - -## Артефакты - -- **Хост:** `89.253.255.133` (books VDS). SSH: `ssh -i ~/.ssh/id_ed25519_books_ops root@89.253.255.133`. -- **Креды:** `<из pass books-vds/full-env>` (`BOOKS_DB_*`, `BOOKS_MONGO_*`, `BOOKS_PORTAINER_*`). -- **mongo (БД sched):** `books-job-scheduler-mongo` = `172.20.0.5:27017`, image `mongo:4.2`, - БД `agendaDb` (sched-задачи: `sched_tasks`, `sched_schedules`, `sched_runs`). Source-of-truth compose: - `/usr/docker/books-job-scheduler/docker-compose.yml` (**files-based**, НЕ Portainer-kzntsv). -- **ES:** `elasticsearch` (стек 33, `/usr/docker/elasticsearch/docker-compose.yml`, `ES_JAVA_OPTS=-Xmx256m`), - `bookva-es` (**Portainer стек 37, endpoint 1**; `ES_JAVA_OPTS` урезан 512m→`-Xmx256m`). -- **CI-остатки:** `buildx_buildkit_builder-*` (`moby/buildkit:buildx-stable-1`), удалены. - -## Диагностика (как понять, что это OOM) - -```bash -ssh root@89.253.255.133 ' - free -m; dmesg | grep -iE "oom|killed process" | tail; # free ~136MB, swap 100%, Killed (mongod) - docker stats --no-stream --format "{{.MemUsage}}\t{{.Name}}" | sort -rh | head; # кто жрёт -' -``` -Кто жрёт (на 2026-08-28): `books-job-scheduler-mongo` 715MiB (главный), `minio` 248+140, `elasticsearch` 242, `bookva-es` 223. - -## Шаги (по порядку эффекта) - -### 1. Урезать `books-job-scheduler-mongo` wiredTiger (главный фикс, ~+450MB) -```bash -cd /usr/docker/books-job-scheduler -cp docker-compose.yml docker-compose.yml.bak -# вставить после " restart: always" в сервисе books-job-scheduler-mongo: -# command: ["mongod", "--wiredTigerCacheSizeGB", "0.25"] -docker compose up -d books-job-scheduler-mongo -``` -Verify: `docker logs books-job-scheduler-mongo | grep -i "cache_size"` → `cache_size=256M`; -`docker exec books-job-scheduler-mongo mongo --quiet --eval "db.runCommand({ping:1}).ok"` = 1. - -### 2. Снести CI-остатки `buildx_buildkit_builder-*` (сироты от CI) -```bash -docker rm -f buildx_buildkit_builder-1ba06752-bf56-4b50-88c5-ce1b48f44a4e0 buildx_buildkit_builder-f61c259c-48e3-4401-b2ca-2404c5cd76070 -``` - -### 3. Урезать ES heap (вторично; elasticsearch уже 256m) -`bookva-es` → `ES_JAVA_OPTS=-Xms512m -Xmx512m` → `-Xms256m -Xmx256m`. -Сделано: Portainer PUT стека 37 (`PUT /api/stacks/37?endpointId=1` с реконструированным compose). Verify: `docker logs bookva-es | grep heap` → `heap size [256mb]`. - -### 4. `minio` / `bookva-minio` / `books-ops-mcp` — cgroup-потолки памяти -Реконструкция compose вслепую рискованна (данные minio-тома, files-секреты, сеть) → `docker update --memory` (без пересоздания): -```bash -docker update --memory 512m --memory-swap 512m minio - docker update --memory 384m --memory-swap 384m bookva-minio -# books-ops-mcp (Node) — 'memory.memsw.limit_in_bytes: device or resource busy' (cgroup v1) — не применилось. -``` -Потолки (защита от раздувания в пик; не освобождают текущие буферы). - -## Verify / smoke - -- `free -m` — free растёт, swap падает (mongo cache_size=256M). -- Нет новых `Out of memory: Kill (mongod)` в dmesg на следующем окне (~06:06). -- `docker stats` — `books-job-scheduler-mongo` ≪715MiB. -- sched-задачи (`ozon fbs products syncronization`) больше не дают `ECONNREFUSED 172.20.0.5:27017`. - -## Rollback - -- mongo: `git checkout`-возврат `command` в compose (или `cp docker-compose.yml.bak docker-compose.yml`) → `docker compose up -d books-job-scheduler-mongo`. -- ES: вернуть `ES_JAVA_OPTS`. - -## Gotchas - -1. **OOM ежедневно ~06:06** — пик в бэкап-окно (mongodump+mariadb-dump+ES snapshot в 06:00 MSK). Урезка снижает, но ХОСТ обделён (3.9GB) — радикальный фикс = апгрейд RAM или Manticore вместо ES. -2. **Книги-стеки — через `portainer.kzntsv.site` endpoint 1 (= books VDS)**: `GET /api/stacks` показывает id 22–51 (books/web/api/ntfy/ops-mcp/chrome/proxy/imgproxy/minio/mongo/db/elasticsearch + bookva-*). Часть из них — **files-based** `/usr/docker/*/docker-compose.yml` (напр. books-job-scheduler-mongo урезан файлом + `docker compose up -d`). При правке стека держать Portainer-источник и файл синхронными, не плодить расхождения. -3. **`com.docker.compose.project.config_files=/data/compose/22`** (label) — старый путь Portainer; `/data/compose/*` на диске НЕ существует. Реальный источник — `/usr/docker/*/docker-compose.yml`. -4. **compose-файлы в CRLF** (`\r\n`) — python-патч учитывать line-ending (иначе needle не матчит). -5. **`bookva-es`** — **Portainer стек 37 (endpoint 1)**. Доступ есть: `BOOKS_PORTAINER_URL` + `BOOKS_PORTAINER_API_KEY` из `pass books-vds/full-env`. НО `GET /api/stacks/:id` НЕ отдаёт `stackFileContent` (в этой версии Portainer), поэтому compose реконструируется из `docker inspect` и обновляется через `PUT` стека. Не менять вслепую без этих данных. -6. **mongo warning XFS** — предупреждение, не ошибка (данные на ext4). -7. **`command` массив** в compose: `command: ["mongod", "--wiredTigerCacheSizeGB", "0.25"]` — строку тоже можно, но список надёжнее. - -## Будущее направление — замена ES → Manticore - -ES в стеке книг — источник перегруза (Java JVM). **Manticore Search** — без Java, легче -(сотни MB против 500+MB), быстрее на полнотекстовом поиске. Оправдано, если ES используется -для полнотекстового поиска товаров/книг (не анализа/DSL-агрегаций). Тогда урезать оба ES, -переписать поиск на Manticore-протокол (SphinxQL/HTTP) и бэкапы. Отдельная задача/ранбук. +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3304]] (concepts/books-vds-memory-optimize-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/emspb-vds-deploy-runbook.md b/.wiki/concepts/emspb-vds-deploy-runbook.md index ff5ccfe..da17a15 100644 --- a/.wiki/concepts/emspb-vds-deploy-runbook.md +++ b/.wiki/concepts/emspb-vds-deploy-runbook.md @@ -1,114 +1,3 @@ ---- -title: emspb.ru snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback) -type: concept -tags: [emspb, snolla, vds, deploy, docker, portainer, traefik, runbook, migration] -related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[labtools-vds-deploy-runbook]], [[snolla-live-prod-inplace-image-bump]] -updated: 2026-07-05 ---- +# ⛔ Файловый канал закрыт -# emspb.ru → VDS deploy runbook - -Вынос `emspb.ru` (snolla-приложение, `@snollajs/snolla` 0.28.4, server-side Liquid) с [[../entities/ruvds-iis-host]] -(catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] (89.253.255.94), за traefik. -Модель — snolla-app (НЕ pilorama98.ru/Nuxt-contentApi): читает БД-контент MoreThenCms (`mssql.kzntsv.site`) -+ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. **Зеркало [[labtools-vds-deploy-runbook]]** — тот же паттерн. - -## Артефакты -- **Код:** `victor/emspb.ru` @ `b6e361a` (apps/web, ре-ревью PASS, 29/29 parity, snolla 0.28.4 final). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck `/robots.txt`). -- **Образ:** `registry.kzntsv.site/emspb:b6e361a` (+`:latest`). Собран НА VDS (обход traefik-499). digest `sha256:8f5ba02651b71f340fa9bc3b079fd2853b01b1461e6c479fb2cf788691d31a74`. -- **Стек Portainer:** `emspb` (Id 18, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/emspb.compose.yml`. -- **siteId:** `96EBC481-D26A-47BE-B660-13D49E7D0A61`, activeTheme `DD8D6F7A-CEDC-4D5A-8BFB-E5F9BE9DC035` (theme store MinIO `themes/dd8d6f7acedc4d5a8bfbe5f9be9dc035/`). Non-secret — в `production.json`, НЕ env. - -## Сборка образа (на VDS) -```bash -# чистый git-archive (только tracked → без config/default.json и node_modules) на VDS -git -C ~/projects/emspb.ru archive --format=tar b6e361a \ - | ssh vitya@89.253.255.94 'rm -rf ~/build/emspb && mkdir -p ~/build/emspb && tar -x -C ~/build/emspb' -# build с build-arg VERDACCIO_TOKEN (pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN), передаётся через env (не в argv), push из VDS -ssh vitya@89.253.255.94 "cd ~/build/emspb && VERDACCIO_TOKEN='' docker build -f deploy/Dockerfile \ - --build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/emspb:b6e361a -t registry.kzntsv.site/emspb:latest . \ - && docker push registry.kzntsv.site/emspb:b6e361a && docker push registry.kzntsv.site/emspb:latest" -``` -- VERDACCIO_TOKEN — build-time only (discarded build-стейдж; в финальный образ не попадает; docker linter warn-only ×2 — ожидаемо). -- `config/default.json` (dev-секреты) исключён `.dockerignore` — в образе только `production.json` + `custom-environment-variables.json` (guard проверен на архиве: default.json ABSENT). - -## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git) -Маппинг env→config: `apps/web/config/custom-environment-variables.json`. Non-secret (host/siteId/endpoints) — в `production.json`. -**Значения идентичны labtools** (все snolla-тенанты читают одну MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). При создании стека env-массив переиспользован verbatim из labtools stack (Id 17) через Portainer API. - -| ENV | Значение | Источник | -|---|---|---| -| `DB_USER` | `snolla` | web.config боя MoreThenCmsEntities | -| `DB_PASSWORD` | ⟨секрет⟩ | web.config боя MoreThenCmsEntities (User Id=snolla) | -| `IMGPROXY_KEY` | ⟨128hex⟩ | `pass minio-vds/full-env` (== сервер imgproxy books-vds — HMAC совпадает) | -| `IMGPROXY_SALT` | ⟨128hex⟩ | `pass minio-vds/full-env` | -| `S3_ACCESS_KEY_ID` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_USER) | -| `S3_SECRET_ACCESS_KEY` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_PASSWORD) | -| `SMTP_USER` | `noreply@snolla.com` | `pass snolla-smtp/full-env` | -| `SMTP_PASSWORD` | ⟨секрет⟩ | `pass snolla-smtp/full-env` (SMTP_PASS) | - -PORT — не переопределять (образ дефолтит 5000; EXPOSE 5000; healthcheck `/robots.txt` на $PORT). traefik service-port = 5000. - -## Создание стека (Portainer API, на VDS — bash+curl+jq, обходит PS-кириллица-гочу) -```bash -# JWT (pass vds-kzntsv/full-env PORTAINER_PASS); env переиспользован из labtools stack 17 -JWT=$(curl -ksS -X POST https://portainer.vds.kzntsv.site/api/auth -d '{"username":"vitya","password":""}' | jq -r .jwt) -ENV=$(curl -ksS -H "Authorization: Bearer $JWT" https://portainer.vds.kzntsv.site/api/stacks/17 | jq '.Env') -COMPOSE=$(cat ~/build/emspb.compose.yml) -PAYLOAD=$(jq -n --arg name emspb --arg compose "$COMPOSE" --argjson env "$ENV" '{name:$name,stackFileContent:$compose,env:$env,fromAppTemplate:false}') -curl -ksS -X POST "https://portainer.vds.kzntsv.site/api/stacks/create/standalone/string?endpointId=1" \ - -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" --data "$PAYLOAD" # → Id=18 -``` -Предусловие pull: `registry.kzntsv.site` зарегистрирован в Portainer как Custom registry Id 1 (иначе `no basic auth`). - -## Runtime egress (наружу из proxy-сети) -`mssql.kzntsv.site:1433` (БД-контент, обязателен) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы). - -## Staging smoke (2026-07-02, GREEN) — `emspb.vds.kzntsv.site` vs бой `www.emspb.ru` (RUVDS IIS 80.64.31.36, через `--resolve`) -- **Status-паритет 23/23:** 5 nav (`/ /contacts /portfolio /prices`) + 17 service-детальных (`/ustanovka-*`, `/almaznoe-burenie-*` …) все 200/200; `/services` 404/404 (nav-якорь, не страница); `/index.php` 404/404; `/robots.txt` 200 байт-в-байт (31 B). -- **Дельта размера контент-страниц стабильна +8 B**, полностью объяснена: +9 host-строка в canonical/og itemprop (`emspb.vds.kzntsv.site` на 9 символов длиннее `www.emspb.ru`), −1 — новый snolla почистил битый двойной слэш боя `/images//3.jpg`→`/images/3.jpg` (оба варианта → 200, картинка не сломана). `/` — байт-в-байт (canonical из DB-домена, не request-host). -- **theme CSS ×3 (framework/bootstrap/slick) из MinIO — md5 IDENTICAL** с боем; `Cache-Control: max-age=86400, public, must-revalidate`, `text/css`. -- **theme image `/images/3.jpg` из MinIO — md5 IDENTICAL** (115618 B, image/jpeg). -- Редиректы: `/contacts/`→301→`/contacts` (trailing-slash), `/Contacts`→301→lowercase, `/Portfolio`→301→lowercase — как бой. -- Контейнер healthy (t+10s), MSSQL (tedious) + S3 (aws-sdk) подключены, `listening at http://localhost:5000`, лог чист (только deprecation-warnings tedious/aws-sdk-v2). - -## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02 -Порядок был: -1. **(оператор)** reg.ru: A-записи `emspb.ru` + `www.emspb.ru` → `89.253.255.94` (с RUVDS `80.64.31.36`). — сделал оператор (сигнал «поменял DNS у провайдера»). Проверено внешним резолвером (8.8.8.8): оба хоста → VDS; labtools остался на RUVDS (не путать домены!). -2. **(ops)** В стеке `emspb` (Id 18) traefik-rule расширен: `Host(\`emspb.ru\`) || Host(\`www.emspb.ru\`)` — Portainer PUT (env сохранён, pullImage=false). LE HTTP-01 выпустил cert на первом хите после flip (~t+20-70s). **staging-хост `emspb.vds.kzntsv.site` убран из rule post-cutover** (проверено: → 404). Cert перевыпустится на 2 боевых SAN при renewal. -3. **Live-smoke GREEN:** `www.emspb.ru`+`emspb.ru` → 200 `ssl_verify=0` (cert доверенный); все страницы 200; canonical нормализован на `www.emspb.ru`; live VDS `/contacts` == старый RUVDS **байт-в-байт** (host уравнялся). - -⚠️ **Порядок критичен:** Host-правило добавлено ТОЛЬКО ПОСЛЕ flip DNS (иначе LE HTTP-01 challenge упал бы на RUVDS → сожгли бы rate-limit). Перед PUT — обязательно проверить внешним резолвером, что DNS реально на VDS. - -### Guard (как labtools) -- **НЕ добавлять `Host(emspb.ru)` в traefik-rule ДО флипа DNS** — traefik на reload проактивно тянет LE-cert (HTTP-01), challenge упадёт на RUVDS → сожжём LE rate-limit. -- **НЕ выводить RUVDS IIS emspb из эксплуатации** — это rollback-путь. -- **НЕ флипать DNS самим** — это делает хозяин домена/оператор. - -## Rollback -- **DNS:** вернуть A-записи `emspb.ru`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS. -- **Стек:** Portainer → stack `emspb` (Id 18) → remove (или откат тега образа). Образ в registry остаётся. - -## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05 -Тираж snolla 0.42.1. emspb.ru обновлён на живом стеке 18 **in-place** (домен уже на VDS, DNS не трогали). -Рецепт — [[snolla-live-prod-inplace-image-bump]]. Оператор дал отмашку на боевой apply. -- **Образ:** `registry.kzntsv.site/emspb:95a5c42` (snolla 0.28.4→**0.42.1**, digest `d6299cf…`). Собран на VDS. -- **Acceptance С VDS (throwaway-staging из env стека 18):** **29 sitemap-роутов все 200** (0 non-2xx/3xx), content-not-lost 29/29 (0 потерь). Sitemap НЕ реструктурировался (29==29, в отличие от каталожного labtools.ru). -- **TLS:** серт — SAN покрывает `emspb.ru`/`www.emspb.ru`/`emspb.vds.kzntsv.site`, CN косметически = staging-хост (curl validates `sslverify=0`). Это тот самый серт с cutover 07-02 (см. выше: «Cert перевыпустится на 2 боевых SAN при renewal») — swap его не тронул. При renewal можно пере-выпустить с CN=emspb.ru, опционально. -- **Swap:** Portainer PUT стека 18 (env 8/8 сохранён) → healthy → live-smoke GREEN, sitemap 29 вживую. -- **Rollback:** тег `emspb:b6e361a` (0.28.4, в registry) / стек 18 PUT назад / DNS→RUVDS. -- Compose обновлён на `95a5c42`. Таска `[emspb-deploy-snolla-0-42-1]` 🟢. Notify=victor/emspb.ru. - -## 0.43.2 v3 cutover (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30 -5-й сайт тиража v3 (после on-snolla, kupimknigi, labtools.ru, labtools.pro — 4 GREEN). In-place bump стека 18 (DNS на VDS с 2026-07-02, не трогали). Рецепт — [[minio-split-cutover-runbook]] §B3-v3. -- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` (commit `5afd8a1`) + `resolutions`-блок в root `package.json` (verbatim labtools.ru: @aws-sdk/client-s3 3.1005.0 + @smithy chain 5.3.11) + реген `yarn.lock` (single @smithy/signature-v4@5.3.11 deduped). Pushed victor/emspb.ru master `218c3ae..5afd8a1`. Endpoint fix `218c3ae` (s3.endpoint→minio.vds.kzntsv.site) в master с 2026-07-29, но в бегущем образе `95a5c42` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. -- **Build на VDS:** `git archive 5afd8a1` → `~/build/emspb` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/emspb:5afd8a1`(+latest), digest `sha256:72bb59fc355d4f020fe49e1c410e6be411779ee1002d91cd4db7c3250f2c3358`, EXIT=0. `yarn install --immutable` в Docker прошёл (yarn.lock matches, no YN0028). -- **Staging :5082 parity-gate GREEN** (throwaway контейнер из env живого стека 18, 8 секретов, `--network proxy`): - - robots.txt md5 `940a0903…` byte-identical (31 B); sitemap index→`sitemap-pages-1.xml` **29 locs == live 29**. - - **29/29 sitemap-роутов 200/200, 0 status diffs**, content present (34-38 KB real bodies). Дельта **+2 B** = `localhost:5082` (14) vs `www.emspb.ru` (13) ×2 (canonical+og:url) — benign host-context, post-cutover byte-identical. - - **theme assets 7/7 md5 IDENTICAL** (v3 aws-sdk v3 S3-direct read из vds MinIO 2025): snolla-framework `5cb1031e…` 7529B, snolla-bootstrap `9a56500a…` 19624B, slick `e604b364…` 1183B, toolbox `382a2ccc…` 10215B, theme `e02370e3…` 5640B, images/2.jpg `0898d8df…` 387884B, **/images/3.jpg `48562843…` 115618B** (== runbook 2026-07-02 baseline). - - redirects parity: `/contacts/`→301→`/contacts`, `/Contacts`→301→lowercase, `/Portfolio`→301→lowercase, `/Services`→301 (target modulo host); `/index.php`+`/services`→404 parity. -- **Portainer PUT stack 18 LIVE:** node in-memory `put-stack` (НЕ PS — глобальный fetch, compose в JS-var без /tmp round-trip; GET `/stacks/18` для Env + `/stacks/18/file` для StackFileContent), env 8/8 preserved, `prune:false pullImage:true mem_limit 512m`. PUT 200. Контейнер `5afd8a1` healthy сразу (~1s). -- **Live smoke GREEN (via traefik):** `/` 200 54615B, `/contacts`+`/portfolio`+`/prices` 200, `/services`+`/index.php` 404, robots md5 `940a0903…`, sitemap 29 locs, theme assets md5 LIVE==staging (v3 S3-direct), redirects 301 (target `https://www.emspb.ru/...` — byte-identical post-cutover), apex `emspb.ru` 200. Лог чист (tedious deprecation + `emspb.ru (snolla) listening`). TLS не тронут. -- **Rollback:** `emspb:95a5c42` (0.42.1 v2, жив в registry) — Portainer PUT назад / compose revert. `b6e361a` (0.28.4) — deeper rollback. DNS→RUVDS 80.64.31.36 — крайний. -- Compose (admin source-of-truth) синхронно закоммичен `95a5c42`→`5afd8a1`. **5/6 v3 сайтов GREEN.** Остался: tandemmebel (стек 20 — был BLOCKED на sharp-staging визуальном дефекте, проверить перед бампом). +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:160]] (concepts/emspb-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/gitea-project-create-runbook.md b/.wiki/concepts/gitea-project-create-runbook.md index bc988b6..fd75f29 100644 --- a/.wiki/concepts/gitea-project-create-runbook.md +++ b/.wiki/concepts/gitea-project-create-runbook.md @@ -1,81 +1,3 @@ ---- -title: "Создание Gitea-репо (проект в Gitea)" -type: concept -tags: [runbook, gitea, repo, project, ops, admin] -related: [concepts/runbooks-index.md, concepts/project-create-canon.md] -updated: 2026-08-27 ---- +# ⛔ Файловый канал закрыт -# Создание Gitea-репо (private) - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -## Scope - -Создание **private** Gitea-репо для нового проекта. Вход: ops-задача на борде `.admin` -(`task_create`, **priority P0**) + покрывающее письмо от заказчика (таска не пингует -живую сессию). Парная review **НЕ нужна** (ops-задача). - -Механика: репо создаёт admin (`OpeItcLoc03`) через `POST /user/repos` → **transfer** -к целевому владельцу. Это канон (скил `project-create`): admin-endpoint отказывает без -`write:admin`. - -## Артефакты - -- **Gitea:** `https://git.kzntsv.site`, API base `/api/v1`. -- **Admin-токен:** `<из pass gitea/admin-token>` (login `OpeItcLoc03`, id 1, is_admin). -- **Push-токен:** user-токен целевого владельца с `repo:write` (для `victor` — `<из pass gitea/victor-books-ci-bookva-overlay>`). -- **SSH-порт** git.kzntsv.site — **2222** (не 22). - -## Шаги - -1. **Получить имя и владельца от оператора.** Никогда не выводить сам (hard rule - скила `project-create`; прецедент tg-digest — решили по аналогии, оператор поправил). -2. **Pre-flight (с admin-токеном):** - - `GET /users/` → 200 = владелец существует; - - `GET /repos//` → **404 = имя свободно**. - > Без auth приватные/скрытые сущности могут давать 404 даже если существуют — проверять с токеном. -3. **Создать** (от admin, owner = `OpeItcLoc03`): - ```bash - TOK=$(pass show gitea/admin-token) - curl -s -X POST "$BASE/user/repos" \ - -H "Authorization: token $TOK" -H "Content-Type: application/json" \ - -d '{"name":"","private":true,"description":"","auto_init":false}' - ``` - `auto_init:false` — без seed-коммита (репо пустое, готово к bootstrap). -4. **Transfer к владельцу:** - ```bash - curl -s -X POST "$BASE/repos/OpeItcLoc03//transfer" \ - -H "Authorization: token $TOK" -H "Content-Type: application/json" \ - -d '{"new_owner":"","team_ids":[]}' - ``` -5. **Verify:** в ответе transfer — `full_name` = `/`, `private:true`, - `empty:true`, `default_branch` (ожид. `main`). -6. **Проверить push-токен:** `GET /repos//` с user-токеном → - `permissions.push == true`. -7. **Ответить письмом** (`inbox_send`, from `.admin`, type action) заказчику: - clone URLs (HTTPS + SSH), push-токен как **pass-реф** (только имя записи, не значение), - подтверждение что репо пустое. Закрыть ops-таску (`task_close`, reason + verify). - -## Verify / smoke - -- Репо указано как private и пустое (в списке репо владельца). -- User-токен владельца видит репо и имеет push-права. -- Ответ-письмо ушло (clone URLs + токен-реф + подтверждение). - -## Rollback - -- Сразу после создания (до transfer): `DELETE /repos/OpeItcLoc03/`. -- После transfer: `DELETE /repos//` (с admin-токеном). - -## Gotchas - -1. **Gitea-эндпоинты без auth** могут 404 на существующие (скрытые/приватные) сущности — все проверки с токеном. (2026-08-27) -2. **Имя/владелец — решение оператора**, не по аналогии из соседних проектов (hard rule, tg-digest кейс). (2026-08-27) -3. **SSH-порт Gitea — 2222**, не 22. (2026-08-27) -4. **`POST /user/repos`** создаёт на владельца токена (`OpeItcLoc03`), потом transfer — нужно admin-право (иначе 403). (2026-08-27) -5. **Секреты:** в ранбуке/письме только pass-рефы; значения токенов не сыпать (write-time secret-scan блокирует 422). (2026-08-27) -6. **`/users/?` без auth → 404 даже для существующего** (victor) — проверять с admin-токеном. (2026-08-27) +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3329]] (concepts/gitea-project-create-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/labtools-vds-deploy-runbook.md b/.wiki/concepts/labtools-vds-deploy-runbook.md index fde7734..b5ebe4b 100644 --- a/.wiki/concepts/labtools-vds-deploy-runbook.md +++ b/.wiki/concepts/labtools-vds-deploy-runbook.md @@ -1,90 +1,3 @@ ---- -title: labtools.ru snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback) -type: concept -tags: [labtools, snolla, vds, deploy, docker, portainer, traefik, runbook, migration] -related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[snolla-live-prod-inplace-image-bump]] -updated: 2026-07-30 ---- +# ⛔ Файловый канал закрыт -# labtools.ru → VDS deploy runbook - -Вынос `labtools.ru` (snolla-приложение, `@snollajs/snolla`, server-side Liquid) с [[../entities/ruvds-iis-host]] -(catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] (89.253.255.94), за traefik. -Модель — snolla-app (НЕ pilorama98.ru/Nuxt-contentApi): читает БД-контент MoreThenCms (`mssql.kzntsv.site`) -+ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. - -## Артефакты -- **Код:** `victor/labtools.ru` @ `43e28ba` (apps/web, parity ре-ревью №2 PASS). `deploy/Dockerfile` (multi-stage node:22-slim). -- **Образ:** `registry.kzntsv.site/labtools:43e28ba` (+`:latest`). Собран НА VDS (обход traefik-499), 114 MB content. -- **Стек Portainer:** `labtools` (Id 17, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/labtools.compose.yml`. - -## Сборка образа (на VDS) -```bash -# чистый git-archive (только tracked → без config/default.json и node_modules) на VDS -git -C ~/projects/labtools.ru archive --format=tar 43e28ba \ - | ssh vitya@89.253.255.94 'rm -rf ~/build/labtools && mkdir -p ~/build/labtools && tar -x -C ~/build/labtools' -# build с build-arg VERDACCIO_TOKEN (pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN), push из VDS -ssh vitya@89.253.255.94 'cd ~/build/labtools && docker build -f deploy/Dockerfile \ - --build-arg VERDACCIO_TOKEN= -t registry.kzntsv.site/labtools:43e28ba -t registry.kzntsv.site/labtools:latest . \ - && docker push registry.kzntsv.site/labtools:43e28ba && docker push registry.kzntsv.site/labtools:latest' -``` -- VERDACCIO_TOKEN — build-time only (discarded build-стейдж; в финальный образ не попадает; docker linter warn-only). -- `config/default.json` (dev-секреты) исключён `.dockerignore` — в образе только `production.json` + `custom-environment-variables.json`. - -## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git) -Маппинг env→config: `apps/web/config/custom-environment-variables.json`. Non-secret (host/siteId/endpoints) — в `production.json`. - -| ENV | Значение | Источник | -|---|---|---| -| `DB_USER` | `snolla` | web.config боя MoreThenCmsEntities | -| `DB_PASSWORD` | ⟨секрет⟩ | web.config боя MoreThenCmsEntities (User Id=snolla) | -| `IMGPROXY_KEY` | ⟨128hex⟩ | `pass minio-vds/full-env` (== сервер imgproxy books-vds — HMAC должен совпадать) | -| `IMGPROXY_SALT` | ⟨128hex⟩ | `pass minio-vds/full-env` | -| `S3_ACCESS_KEY_ID` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_USER) | -| `S3_SECRET_ACCESS_KEY` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_PASSWORD) | -| `SMTP_USER` | `noreply@snolla.com` | `pass snolla-smtp/full-env` | -| `SMTP_PASSWORD` | ⟨секрет⟩ | `pass snolla-smtp/full-env` (SMTP_PASS; default.json имел dev-значение — НЕ брать оттуда) | - -PORT — не переопределять (образ дефолтит 5000; EXPOSE 5000; healthcheck `/robots.txt` на $PORT). traefik service-port = 5000. - -## Runtime egress (наружу из proxy-сети) -`mssql.kzntsv.site:1433` (БД-контент, обязателен) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы). - -## Staging smoke (2026-07-01, GREEN) — `labtools.vds.kzntsv.site` vs бой `www.labtools.ru` -- Все страницы меню (home/about/clients/contacts/cookie/privacy/6×products) — 200/200, дельта размера стабильна (длина хоста в canonical/og). -- Редиректы: `.php`→301→`/`, `/about/`→301 (trailing-slash), `/Products/Press-Forms`→301→lowercase — 1:1 с боем. -- `theme.css` из MinIO — **байт-в-байт** (12248), `Cache-Control: max-age=86400, public, must-revalidate`. -- `/assets//*.webp` из MinIO — **байт-в-байт** с боем (44444, 24354); content-type `application/octet-stream` — как на бою. -- Контейнер healthy (MSSQL+S3 подключены). - -## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02 -⚠️ **DNS зоны `labtools.ru` — на Yandex DNS (`dns1/dns2.yandex.net`), НЕ reg.ru** (в отличие от `labtools.pro`/reg.ru). Порядок был: -1. **(оператор)** Yandex DNS: A-записи `labtools.ru` + `www.labtools.ru` → `89.253.255.94` (с RUVDS `80.64.31.36`). -2. **(ops) verify перед traefik — критично:** флип шёл неравномерно, **dns1 и dns2 расходились** (dns2 обновился первым, dns1 отставал). Гейт cutover = **ОБА** авторитетных NS согласованно отдают VDS по apex+www (иначе LE мог попасть на dns1→RUVDS→сжечь rate-limit). Публичные резолверы кэшировали до 6ч (TTL 21600) — для LE не критично (резолвит по авторитетному). Cм. memory [operator-dns-flip-verify-domain-before-cutover]. -3. **(ops) после подтверждения обоих NS:** стек `labtools` (Id 17) traefik-rule → `Host(\`labtools.ru\`) || Host(\`www.labtools.ru\`)` (Portainer PUT, env 8/8 сохранён, pullImage=false). LE-cert выпущен (~t+20s; первые хиты ловили self-signed в окне issuance, потом ssl_verify=0). -4. **Live-smoke GREEN:** оба хоста 200 (nav + каталог-секции + продукты); редиректы (`/about/`→trailing, `/Contacts`→lowercase, `/index.php`→`/`) parity; sitemap = index (host=`labtools.ru`, non-www — siteUrl этого тенанта без www, в отличие от .pro/R3); theme-ассеты из MinIO byte-identical бою (RUVDS отдаёт 301 apex→www, поэтому прямой md5 «DIFF» — артефакт; с `-L` MD5-OK). staging-хост `labtools.vds.kzntsv.site` убран (→404). **Внешняя проверка с RUVDS-хоста:** `labtools.ru`→VDS 200; `www` ещё на кэше RUVDS (200, идентичный контент) — split-brain окно, тухнет по TTL. -5. `labtools.pro` — **закрыт отдельно** ([[labtools.pro-vds-deploy-runbook]], LIVE 2026-07-02). - -## Rollback -- **DNS:** вернуть A-записи `labtools.ru`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS. -- **Стек:** Portainer → stack `labtools` → remove (или откат тега образа). Образ в registry остаётся. -- RUVDS IIS labtools **не выводить из эксплуатации** до явного решения оператора (директива 2026-07-01). - -## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05 -Тираж snolla 0.42.1. labtools.ru обновлён на живом стеке 17 **in-place** (домен уже на VDS, DNS не трогали). -Рецепт — [[snolla-live-prod-inplace-image-bump]] (общий для labtools.ru/emspb/labtools.pro). Оператор дал отмашку на боевой apply. -- **Образ:** `registry.kzntsv.site/labtools:566d41c` (snolla 0.28.2→**0.42.1**, digest `ea0649a…`). Собран на VDS. -- **Acceptance С VDS (throwaway-staging из env стека 17):** новый sitemap = **38 page-locs** (7 pages+1 static+6 sections+24 продукта) — 37×200 + 1×301(`/products/laboratory-ball-mills`→`/lshm-750`, идентично проду). content-not-lost 11/11 (0 потерь). **Order-фикс 0.42.1** (liquid 0.10.2): presses = `plg-20,plg-12,plg-25,pgr-10` == прод. -- **Находка:** старый 0.28.2 sitemap был дефицитным (sections=1, catalog=2 продукта) — 0.42.1 отдаёт полную корректную структуру. Ещё: sitemap листит 301-секцию как `` (прод идентичен, engine-intended; heads-up прогу→снолле). -- **Swap:** Portainer PUT стека 17 (env 8/8 сохранён, pullImage=true) → healthy → live-smoke GREEN, TLS CN=labtools.ru не тронут (без LE-churn). -- **Rollback:** тег `labtools:43e28ba` (0.28.2, в registry) / стек 17 PUT назад / DNS→RUVDS. -- Compose обновлён на `566d41c`. Таска `[labtools-ru-deploy-snolla-0-42-1]` 🟢. Notify=victor/labtools.ru. - -## 0.43.2 v3 bump (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30 -Тираж v3 (MinIO split Track B3-v3, 3-й сайт после on-snolla + kupimknigi). Домен на VDS, DNS не трогали. -Рецепт — [[snolla-live-prod-inplace-image-bump]] + [[minio-split-cutover-runbook]] §B3-v3. -- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` + `resolutions`-блок в root `package.json` (commit `0dc0b4e`, pushed victor/labtools.ru master). yarn install: single `@smithy/signature-v4@5.3.11` (deduped). Endpoint fix `4f3314f` (s3.endpoint→minio.vds.kzntsv.site) был в master с 2026-07-29, но в бегущем образе `566d41c` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. -- **Build на VDS:** `git archive 0dc0b4e` → `~/build/labtools` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/labtools:0dc0b4e`(+latest), EXIT=0. -- **Throwaway staging :5080 parity-gate GREEN** (из env живого стека 17, 8 секретов): `/` byte-identical с real Host (18594B), theme asset `lato.css` 200 9239B byte-identical (v3 aws-sdk v3 S3-direct read из vds MinIO 2025), robots 200/200, sitemap = index 38 page-locs == live (identical set, 0 diff), self-consistency 0 bad (все 200/3xx), catalog section `/products/accessories` byte-identical (order-parity ✅). -- **Portainer PUT stack 17 LIVE:** node in-memory `put-stack` (НЕ PS), env 8/8 preserved, `mem_limit 512m` уже в compose, `prune:false pullImage:true`. PUT 200. Контейнер `0dc0b4e` healthy сразу. Source-of-truth compose синхронно закоммичен (admin). -- **Live smoke GREEN:** `/` 200, www 200, robots/sitemap 200, menu pages (about/clients/contacts/cookie/privacy) 200, `/products/press-forms` + `/products/accessories` + `/products/laboratory-ball-mills` 200, theme `lato.css` 200 9239B (v3 S3-direct). TLS CN=labtools.ru не тронут (LE до Sep 30). `/products` (bare) + `/products/presses` → 404 = ожидаемо (не в sitemap, не real pages; real section = `/products/press-forms`). **Rollback = `566d41c`** (v2 0.42.1, жив в registry). +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:168]] (concepts/labtools-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/labtools.pro-vds-deploy-runbook.md b/.wiki/concepts/labtools.pro-vds-deploy-runbook.md index dcce292..6845067 100644 --- a/.wiki/concepts/labtools.pro-vds-deploy-runbook.md +++ b/.wiki/concepts/labtools.pro-vds-deploy-runbook.md @@ -1,107 +1,3 @@ ---- -title: labtools.pro snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback) -type: concept -tags: [labtools-pro, labtools, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, catalog] -related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[emspb-vds-deploy-runbook]], [[labtools-vds-deploy-runbook]], [[snolla-live-prod-inplace-image-bump]] -updated: 2026-07-30 ---- +# ⛔ Файловый канал закрыт -# labtools.pro → VDS deploy runbook - -Вынос `labtools.pro` (snolla-приложение, `@snollajs/snolla` **0.28.7**, server-side Liquid, **каталожный сайт**) -с [[../entities/ruvds-iis-host]] (catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] -(89.253.255.94), за traefik. Модель — snolla-app: читает БД-контент MoreThenCms (`mssql.kzntsv.site`) -+ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. **Зеркало [[emspb-vds-deploy-runbook]]** — тот же паттерн; -отличие: каталог `/products/*` + `Culture=en` (англоязычный близнец labtools.ru) + Cache-Control отсутствует (см. ниже). - -## Артефакты -- **Код:** `victor/labtools.pro` @ `7bd9fae` (apps/web, ре-ревью PASS все 9 дименшнов, ноль хаков, snolla 0.28.7 final). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck `/robots.txt`). -- **Образ:** ⚠️ `registry.kzntsv.site/labtools-pro:7bd9fae` (+`:latest`) — имя **`labtools-pro`**, НЕ `labtools` (тот у labtools.ru!). Собран НА VDS (обход traefik-499). digest `sha256:23d0bc597c8930a1bf443c418a4af68d63b692903963c0e1f9f25baa4a1dd236`. -- **Стек Portainer:** `labtools-pro` (**Id 19**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/labtools-pro.compose.yml`. -- **siteId:** `663F9410-A6CC-4651-9A5C-62844A313957`, activeTheme `389AD745-E3EE-4F23-BE16-DF38440A0941` (theme store MinIO `themes/389ad745e3ee4f23be16df38440a0941/`). Non-secret — в `production.json`, НЕ env. -- **siteUrl:** `https://www.labtools.pro` — в `production.json` (R3: sitemap/robots/canonical отдают www). - -## Сборка образа (на VDS) -```bash -git -C ~/projects/labtools.pro archive --format=tar 7bd9fae \ - | ssh vitya@89.253.255.94 'rm -rf ~/build/labtools-pro && mkdir -p ~/build/labtools-pro && tar -x -C ~/build/labtools-pro' -ssh vitya@89.253.255.94 "cd ~/build/labtools-pro && VERDACCIO_TOKEN='' docker build -f deploy/Dockerfile \ - --build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/labtools-pro:7bd9fae -t registry.kzntsv.site/labtools-pro:latest . \ - && docker push registry.kzntsv.site/labtools-pro:7bd9fae && docker push registry.kzntsv.site/labtools-pro:latest" -``` -- VERDACCIO_TOKEN — `pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN`, build-time only (не в финальный образ; docker linter warn-only). -- `config/default.json` (dev-секреты) исключён `.dockerignore` — guard проверен на архиве: default.json ABSENT (только production.json + custom-environment-variables.json + default.example.json). - -## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git) -**Значения идентичны labtools/emspb** (все snolla-тенанты = одна MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). При создании стека env-массив **переиспользован verbatim из labtools stack (Id 17)** через Portainer API. 8 env: `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`. Маппинг env→config: `apps/web/config/custom-environment-variables.json`. -PORT — не переопределять (образ 5000; EXPOSE 5000; healthcheck `/robots.txt`). traefik service-port = 5000. - -## Создание стека (Portainer API, X-API-Key) -```bash -K=$(pass show vds-kzntsv/full-env | sed -n 's/^PORTAINER_API_KEY=//p'); BASE=https://portainer.vds.kzntsv.site -ENV=$(curl -ksS -H "X-API-Key: $K" $BASE/api/stacks/17 | jq '.Env') # verbatim из labtools stack 17 -COMPOSE=$(cat ~/build/labtools-pro.compose.yml) -PAYLOAD=$(jq -n --arg name labtools-pro --arg compose "$COMPOSE" --argjson env "$ENV" '{name:$name,stackFileContent:$compose,env:$env,fromAppTemplate:false}') -curl -ksS -X POST "$BASE/api/stacks/create/standalone/string?endpointId=1" -H "X-API-Key: $K" -H 'Content-Type: application/json' --data "$PAYLOAD" # → Id 19 -``` -Предусловие pull: `registry.kzntsv.site` зарегистрирован в Portainer как Custom registry Id 1 (иначе `no basic auth`). - -## Runtime egress -`mssql.kzntsv.site:1433` (БД-контент) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы). - -## Staging smoke (2026-07-02, GREEN) — `labtools-pro.vds.kzntsv.site` vs бой `www.labtools.pro` (RUVDS IIS 80.64.31.36) -Гнать с самого VDS (воркстейшн ловит LAN-DNS-перехват на `*.labtools.pro`; с VDS бой резолвится публично → RUVDS). -- **Status-паритет:** nav (`/ /about /contacts`) + 3 каталог-секции (`/products/laboratory-presses`, `/products/press-forms`, `/products/milling-accessories`) + продукты (`plg-20`, `press-forms/ring`) — все 200/200. -- **Редиректы (301 parity, target идентичен modulo host):** каталог#1 `/products/laboratory-ball-mills`→`…/laboratory-ball-mills/lshm-750`; `/Contacts`→lowercase `/contacts`; `/contacts/`→trailing `/contacts`. -- **Sitemap — структура index (snolla) vs плоский (legacy), покрытие идентично:** бой отдаёт плоский `/sitemap.xml` с 25 URL; snolla отдаёт **sitemap-INDEX** из 7 дочерних (`sitemap-pages-1` 3, `sitemap-static-pages-1` 2, `sitemap-sections-1` 3, 4× `sitemap-catalog--products-1` = 1+2+2+12=17), все 200, **в сумме ровно 25 URL, набор == бой** (comm — ноль расхождений). `` host = `www.labtools.pro` (R3). **НЕ дефект** — index-структура штатна для snolla (как у pilorama98.ru). -- **theme-ассеты из MinIO md5-IDENTICAL бою** (проверены css/js/png/svg под `/themes/389ad745e3ee4f23be16df38440a0941/`): 7/7 MD5-OK, http 200. -- ⚠️ **Cache-Control ОТСУТСТВУЕТ на theme-ассетах — КОРРЕКТНО** (labtools.pro `CachingOptions=[]` empty-config; движок 0.28.7 не ставит заголовок = бой). НЕ флагать как дефект (в отличие от emspb/labtools где был `max-age=86400`). -- **Контент-страницы рендерят реальное тело** (12–22 KB), дельта vs бой +155…+222 B — стабильна, объясняется длиной staging-host в canonical/og (на 11 симв. длиннее www; на cutover уравняется в byte-parity). -- Контейнер healthy, MSSQL (tedious) + S3 (aws-sdk) подключены, `listening at http://localhost:5000`, лог чист (только deprecation-warnings tedious/aws-sdk-v2). - -### Косметика (не дефект, отдано workshop) -Стартовый баннер в логе — `labtools.ru (snolla) listening…` — строка-константа унаследована из labtools.ru-референса при скаффолде. Идентификация сайта = siteId в production.json (663F9410…), на рендер/контент не влияет. - -## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02 -Порядок был (как emspb): -1. **(оператор)** reg.ru: A-записи `labtools.pro` + `www.labtools.pro` → `89.253.255.94` (с RUVDS `80.64.31.36`). -2. **(ops) verify флипа перед traefik:** предыдущие проверки ловили старый TTL-кэш (публичные резолверы отдавали 80.64.31.36); **авторитетный `ns1.reg.ru` через `Resolve-DnsName` подтвердил** `labtools.pro`+`www` → `89.253.255.94` (TTL 3600), 8.8.8.8 догнал. Именно `.pro` (НЕ `.ru`). Урок: при "поменял DNS" проверять авторитетный NS напрямую, а не только кэширующие резолверы. Cм. memory [operator-dns-flip-verify-domain-before-cutover]. -3. **(ops) ТОЛЬКО ПОСЛЕ flip:** стек `labtools-pro` (Id 19) traefik-rule → `Host(\`labtools.pro\`) || Host(\`www.labtools.pro\`)` (Portainer PUT, env сохранён 8/8, pullImage=false). LE HTTP-01 выпустил cert **мгновенно** (ssl_verify=0 с первого хита). -4. **Live-smoke GREEN:** оба хоста 200 (nav+каталог+продукты); редиректы (ball-mills→lshm-750, /Contacts→lowercase, /contacts/→trailing) parity; sitemap index 7 детей host=www; контент реальный (12728 B, `lang="en"`). staging-хост `labtools-pro.vds.kzntsv.site` убран из rule (→ 404). **Внешняя проверка с RUVDS-хоста (Королёв, public DNS):** `labtools.pro`+`www` → 89.253.255.94 HTTP 200, cert доверенный. -5. RUVDS labtools.pro — rollback-путь, жив (отвечает 301, TLS ок), НЕ тронут. - -⚠️ **Порядок критичен:** Host-правило добавлять ТОЛЬКО ПОСЛЕ flip DNS (иначе traefik проактивно тянет LE-cert HTTP-01, challenge упадёт на RUVDS → сожжём rate-limit). - -## Rollback -- **DNS:** вернуть A-записи `labtools.pro`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS. -- **Стек:** Portainer → stack `labtools-pro` (Id 19) → remove (или откат тега образа). Образ в registry остаётся. -- RUVDS IIS labtools.pro **не выводить** до явного решения оператора (директива 2026-07-01). (Ср.: emspb.ru биндинги на RUVDS сняты 2026-07-02 после подтверждённого cutover — см. [[../entities/ruvds-iis-host]].) - -## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05 -Тираж snolla 0.42.1. labtools.pro обновлён на живом стеке 19 **in-place** (домен уже на VDS, DNS не трогали). -Рецепт — [[snolla-live-prod-inplace-image-bump]]. Оператор дал отмашку на боевой apply. -- **Образ:** `registry.kzntsv.site/labtools-pro:0610432` (snolla 0.28.7→**0.42.1**, digest `3d543fc…`). Собран на VDS. -- **Acceptance С VDS (throwaway-staging из env стека 19):** **25 sitemap page-locs все 200** (+robots+sitemap.xml = 27 прогерских), content-not-lost 25/25 (0 потерь). -- **Order-парити ВСЕ 3 секции MATCH == прод** (фикс liquid 0.10.2): presses=`plg-20,plg-12`, milling=`milling-jars,grinding-media`, press-forms=**12** изделий (round-xrf…round-collapsible). NB: первичный ручной счёт дал 13 (задвоил тайл), прог-сверка = 12, парити не задет. -- **Swap:** Portainer PUT стека 19 (env 8/8 сохранён) → healthy → live-smoke GREEN (nav+presses+press-forms 200, order вживую), TLS CN=labtools.pro не тронут. -- **Rollback:** тег `labtools-pro:7bd9fae` (0.28.7, в registry) / стек 19 PUT назад / DNS→RUVDS. -- Compose обновлён на `0610432`. Таска `[labtools-pro-deploy-snolla-0-42-1]` 🟢. Notify=victor/labtools.pro. - -## 0.43.2 v3 cutover (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30 -4-й сайт тиража v3 (после on-snolla, kupimknigi, labtools.ru — все GREEN). MinIO split Track B3-v3. -Паттерн отработан x3; labtools.pro = English-twin (`Culture=en`) labtools.ru, отдельный репо `victor/labtools.pro`. -Оператор дал GO на боевой apply после staging parity-gate. -- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` (commit `12f8992`) + `resolutions`-блок в root `package.json` (verbatim labtools.ru: @aws-sdk/client-s3 3.1005.0 + @smithy chain 5.3.11) + реген `yarn.lock` (commit `a106622`, single @smithy/signature-v4@5.3.11 deduped). Pushed victor/labtools.pro master `6c7ea88..a106622`. -- **Endpoint fix `6c7ea88`** (s3.endpoint→minio.vds.kzntsv.site) был в master с 2026-07-29, но в бегущем образе `0610432` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. -- **Build на VDS:** `git archive a106622` → `~/build/labtools-pro` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/labtools-pro:a106622` (+latest), digest `sha256:2152642c4b17…`, 476MB, EXIT=0. -- **Staging :5081 parity-gate GREEN** (throwaway контейнер из env живого стека 19, 8 секретов): - - `/` nav catalog = **4-байт diff = `og:url`+`og:image` request-host** (`localhost:5081` 13 chars vs `www.labtools.pro` 15 chars × 2 = 4B). **Benign request-context** — post-cutover (оба за traefik Host=www.labtools.pro) byte-identical. (Ср. kupimknigi 3-байт form-url diff.) - - Redirects (`/contacts/`→`/contacts`, `/products/laboratory-ball-mills`→`/lshm-750`) **IDENT** (301). - - `/robots.txt` `/sitemap.xml` **IDENT** (7 sitemap детей == live, locs identical, host=www). - - **theme `lato.css` 200 9994B md5-identical** — v3 aws-sdk v3 S3-direct read из vds MinIO 2025 WORKS (ключевая v3-верификация). - - **Order-parity** `/products/press-forms` = 12 изделий identical order (multi-position…vacuum-ring). - - Self-consistency: всё 200/3xx, 0 × 4xx/5xx. -- **Portainer PUT stack 19 LIVE:** node in-memory `put-stack` (НЕ PS Invoke-RestMethod — корраптит кириллицу compose; скрипт `scripts/tmp/put-stack-labtools-pro.mjs`), env 8/8 preserved, `prune:false pullImage:true mem_limit 512m`. PUT 200. Контейнер `a106622` healthy сразу. -- **Live smoke GREEN (via traefik):** `/` 200 12736B, `/about`+`/contacts` 200, `/robots.txt`+`/sitemap.xml` 200 (7 детей все 200), 3 каталог-секции 200, продукты (`press-forms/ring` 17255B, `laboratory-presses/plg-20` 22077B) 200, theme `lato.css` 200 9994B (v3 S3-direct), redirects parity, `labtools.pro` apex 200. Лог чист (tedious deprecation + cosmetic `labtools.ru listening` banner — known benign). TLS CN=labtools.pro не тронут (LE notBefore Jul 2 / notAfter Sep 30 2026). -- **Rollback:** `labtools-pro:0610432` (0.42.1, жив в registry) — Portainer PUT назад / compose revert. -- Compose (admin source-of-truth) синхронно закоммичен `0610432`→`a106622`. 4/6 v3 сайтов GREEN. Остались: **emspb** (стек 18), **tandemmebel** (стек 20 — был BLOCKED на sharp-staging визуальном дефекте, проверить перед бампом). +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:169]] (concepts/labtools.pro-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/maljarka-vds-deploy-runbook.md b/.wiki/concepts/maljarka-vds-deploy-runbook.md index 24ecd83..56ca113 100644 --- a/.wiki/concepts/maljarka-vds-deploy-runbook.md +++ b/.wiki/concepts/maljarka-vds-deploy-runbook.md @@ -1,62 +1,3 @@ ---- -title: maljarka.tandemmebel.ru snolla-app — VDS deploy runbook (restore 2026-07-31) -type: concept -tags: [maljarka, tandemmebel, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, landing, ruvds-decomm] -related: [[../entities/vds-kzntsv], [on-snolla-vds-deploy-runbook], [tandemmebel-vds-deploy-runbook], [morethencms-null-settingsdata-https-502], [snolla-local-admin-and-on-snolla-migration-design]] -updated: 2026-07-31 ---- +# ⛔ Файловый канал закрыт -# maljarka.tandemmebel.ru → VDS deploy runbook (restore) - -Восстановление лендинга `maljarka.tandemmebel.ru` («Малярка от Тандеммебель», snolla-app `@snollajs/snolla` **0.43.2** / aws-sdk v3, server-side Liquid) — **7-й сайт тиража snolla на VDS**. Паттерн = [[on-snolla-vds-deploy-runbook]] (прецедент: тоже лендинг с RUVDS catch-all origin, 1 Page `/` + empty content + StaticPages, реконструкция layout.liquid). - -## Почему восстанавливали (root cause) - -maljarka была **.NET MoreThenCms tenant** (тема «Reversal», siteId `A2476738-E26C-49D5-95AD-958F42DCCDD0`), служила на **RUVDS IIS catch-all** (`80.64.31.36`). В v3 тираж (6 Node snolla-app сайтов, 2026-07-30) **не входила** — осталась на RUVDS. **RUVDS декоммишнен 2026-07-21** ([[../entities/ruvds-iis-host]] §DECOMM, погашен у провайдера) → maljarka потеряла единственный публичный хост. DNS перебросили на VDS (CNAME → `tandemmebel.ru` → `89.253.255.94`), но `Host()`-правила и snolla-app с siteId `A2476738` на VDS не было → traefik 404. Аудит 21.07 «maljarka VDS=200» = **false-positive** с локального IIS воркстейшна (local .NET-admin catch-all поднят 20.07, LAN-DNS отдавал его; memory `workstation-lan-dns-serves-local-cms-copy`). Контент в shared `MoreThenCms` DB (`mssql.kzntsv.site`) остался цел — восстанавливаемо. - -## Артефакты -- **Код:** `victor/maljarka.tandemmebel.ru` @ `7f03d67` (apps/web, snolla 0.43.2). Клон `victor/on.snolla.com` (lean-база: 4 view-файла, root `package.json` v3 resolutions). `deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root, healthcheck `/robots.txt`). -- **Образ:** `registry.kzntsv.site/maljarka:7f03d67` (+`:latest`), digest `sha256:df4e3642…`. Имя **`maljarka`** (= стек/контейнер). Собран НА VDS (обход traefik-499). -- **Стек Portainer:** `maljarka` (**Id 23**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/maljarka.compose.yml`. -- **siteId:** `A2476738-E26C-49D5-95AD-958F42DCCDD0` (non-secret, в `production.json` + `default.json`). -- **siteUrl:** `https://maljarka.tandemmebel.ru`. -- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD` — Portainer stack env (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). НЕ в образе, НЕ в git. -- **mem_limit:** `512m`. - -## Site model (MoreThenCms DB, siteId A2476738) -- 5 Pages (intro/services/portfolio/about/contacts), но **Published=true только `/`** (content пуст — лендинг baked в теме) → sitemap = 1 loc `/`. -- 4 StaticPages (all published, sitemap=false): `/index.html` (18401B), `/portfolio-single.html` (1336B), `/portfolio-single-gallery.html` (1567B), `/portfolio-single-video.html` (822B) — отдаются snolla из DB verbatim. -- 2 Domains rows (→ robotsTxt-гоча НЕ applies, `app.locals.domain` set; index.js null-guard остался как defensive). -- SettingsData `httpSecure` блок на месте (fix 2026-06-08, [[morethencms-null-settingsdata-https-502]]). -- Тема «Reversal» `4881FC7F` → MinIO bucket `themes` prefix `4881fc7f52034f8eb371fe5637b1cce1/` (78 файлов, УЖЕ была в MinIO — заливать не понадобилось). - -## Реконструкция шаблонов -- `views/layout.liquid` = **byte-identical** рендеру локального .NET-admin (`curl --noproxy '*' --resolve maljarka.tandemmebel.ru:80:127.0.0.1`, 43730B). `{{ item.content }}` вставлен **inline** после последней `` (content пуст → empty output → byte-parity сохранён). -- `error_404.liquid` / `error_500.liquid` — Reversal-тема + maljarka-бренд (вместо on.snolla SNOLLA-бренда). -- Источник реконструкции = локальный catch-all IIS `snolla` ([[snolla-local-admin-and-on-snolla-migration-design]] §Task A) — тот же MoreThenCms DB + та же тема. - -## Deploy 2026-07-31 (DNS уже на VDS — без DNS-flip-шага) -1. **Репо:** `victor/maljarka.tandemmebel.ru` создан на gitea (API, admin-token). Клон on.snolla.com → swap `production.json` (siteId A2476738, siteUrl, appName) + package names → `yarn install` (regen lockfile под переименованный workspace) → push. SHA `ebebe4e` → `7f03d67` (lockfile). -2. **Build на VDS:** `git archive 7f03d67` → scp → `~/build/maljarka` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN=` → `registry.kzntsv.site/maljarka:7f03d67`(+latest). Guard: `config/default.json` ABSENT в архиве (.dockerignore), snolla 0.43.2, `yarn install --immutable` GREEN (lockfile перегенерён под maljarka workspace). Push OK. -3. **Throwaway-staging :5081** из env live tandemmebel-контейнера (8 секретов, `--network proxy`). Healthy. **Completeness-gate С VDS GREEN:** `/` **byte-identical** oracle (sha256 `64011be6…`, 43730B), robots 200, sitemap-pages-1 1 loc `/` (+ пустой blog-index — benign parity, как on-snolla `/c`), 4 staticpages 200 (sizes == DB clen), theme asset `bootstrap.min.css` 200 103320B (MinIO Reversal подтверждена), unpublished /about/services/contacts/intro → 404 (parity). 0 регрессий. -4. **Portainer стек 23** создан node-скриптом `create-maljarka-stack.mjs` (JWT auth, **НЕ PS** — кириллица compose; memory `ps51-iso8851-corrupts-portainer-stackfile`), env 8/8, `mem_limit 512m`, `Host(\`maljarka.tandemmebel.ru\`)`. DNS уже `89.253.255.94` → **LE-серт issued on first hit**: CN=maljarka.tandemmebel.ru, issuer YR1, until 2026-10-29. -5. **Live smoke GREEN:** `/` 200 43730B byte-identical, `Малярка от Тандеммебель`, 4 staticpages 200 (sizes == DB), theme asset 200, robots/sitemap 200, TLS LE. Log чист. - -## Гочи -- **`variant-cache` bucket на vds MinIO 2025 (КРИТИЧНО, shared v3-регрессия)** — v3 minio-split перевёл сайты на `minio.vds.kzntsv.site`, но `variant-cache` bucket не создали там (был на старом `minio.kzntsv.site`/books-vds 2020). snolla sharp-pipeline (`variantCache.js` `DEFAULT_NAMESPACE='variant-cache'`, `storage.js: bucket=storageClient`) → **NoSuchBucket → 500 на ALL sharp gallery-images** (не только maljarka — на всех 6 v3 сайтах с 2026-07-30). Заметно на maljarka-портфолио (35 фото). v3-gate не поймал т.к. проверял theme CSS (`themes` bucket, themeFiles — без sharp), не gallery-image URLs. **Fix 2026-07-31:** `mc mb vds/variant-cache` (пустой, кэш регенерируем — миграция старых entries НЕ нужна). После: portfolio 34/34 → 200 image/webp. См. [[minio-split-cutover-runbook]] Track A + memory `variant-cache-bucket-missing-on-vds-2025`. -- **yarn.lock workspace-rename** — переименование root package (`on.snolla.com`→`maljarka.tandemmebel.ru`) и `apps/web` name (`@on-snolla/web`→`@maljarka/web`) требует regen yarn.lock, иначе `yarn install --immutable` падает (lockfile ссылается старые workspace-дескрипторы). Fix: `yarn install` локально под vitya JWT → commit обновлённого lockfile. -- **workstation-curl отравлен локальным HTTP-прокси** — `--resolve` игнорируется прокси (CONNECT), обе пробы уходят на VDS. External verify prod-maljarka с воркстейшна = только через VDS-ssh curl (или `curl --resolve :443:89.253.255.94 --noproxy`, но прокси всё равно может перехватить). Локальный IIS-рендер для реконструкции — через `--noproxy '*' --resolve :80:127.0.0.1`. -- **hosts override на воркстейшне** — Task A оставил hosts-override для .snolla.com (НЕ maljarka), но local IIS `*:80` catch-all + LAN-DNS могут перехватить. Оракул-рендер снимался явно через 127.0.0.1. -- **robotsTxt-гоча НЕ applies** — у maljarka 2 Domains rows (`app.locals.domain` set). index.js null-guard (из on.snolla) остался как defensive, не вредит. -- **RUVDS DECOMM** — rollback только образ-тегом в registry, НЕ DNS (RUVDS погашен 2026-07-21). - -## Rollback -- **Образный:** PUT стека 23 назад на предыдущий тег в registry. С первого deploy ведём rollback-теги (пока только `7f03d67` = `:latest`). -- DNS-rollback НЕ доступен (RUVDS DECOMM). -- Если layout сломан: revert Host-правила (вернёт 404) ИЛИ поправить `layout.liquid` + rebuild. - -## Связанное -- Тираж snolla на VDS теперь **7/7**: labtools.ru(17) / emspb(18) / labtools.pro(19) / tandemmebel(20) / kupimknigi(21) / on-snolla(22) / **maljarka(23)**. -- Прецедент-паттерн: [[on-snolla-vds-deploy-runbook]] (лендинг, реконструкция из .NET-admin рендера). -- 502-на-HTTPS fix (RUVDS-эпоха, 2026-06-08): [[morethencms-null-settingsdata-https-502]]. -- Локальный .NET-admin (источник реконструкции): [[snolla-local-admin-and-on-snolla-migration-design]] §Task A. \ No newline at end of file +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:170]] (concepts/maljarka-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/mappa-vds-deploy-runbook.md b/.wiki/concepts/mappa-vds-deploy-runbook.md index f17f14e..22ac306 100644 --- a/.wiki/concepts/mappa-vds-deploy-runbook.md +++ b/.wiki/concepts/mappa-vds-deploy-runbook.md @@ -1,123 +1,3 @@ ---- -title: mappa — VDS deploy runbook (БД / образ / Portainer-стек 27 / редеплой / verify) -type: concept -tags: [mappa, vds, deploy, docker, portainer, traefik, postgres, runbook, meta] -related: [[../entities/vds-kzntsv]], [[portainer-stack-management-vds]], [[mappa-prod-infra]] -updated: 2026-08-28 ---- +# ⛔ Файловый канал закрыт -# mappa → VDS deploy runbook - -Развёртывание **mappa** (единый meta-сервис цеха: HTTP-ядро + MCP-адаптер, Postgres) -на [[../entities/vds-kzntsv]] (89.253.255.94) рядом с общей БД (решения 1/17/18 спеки -`concepts/mappa`). Первый деплой — 2026-08-23 (#1012, закрыта). - -**Разделение труда (директива 2026-08-23):** деплой/редеплой = АДМИН (таска + письмо); -прод-импорт, verify счётчиков, read-only+фолбэк, ntfy-алерт, артефакты, MCP-регистрация = -mappa (#985). Не делать чужую часть. - -## Артефакты - -- **Код:** `OpeItcLoc03/mappa` @ master. Сборка TS: `npm ci && npm run build` → `dist/`. -- **Образ:** `registry.kzntsv.site/mappa:` — тег = version из package.json, GIT_SHA передаётся - `--build-arg GIT_SHA=` (Dockerfile в корне репо, multi-stage node:22-alpine; - **обязательный COPY `src/lib/schema.sql` → `dist/src/lib/schema.sql`** — tsc не копирует .sql, - без него migrate() падает «schema.sql not found»). Прод на 2026-08-29 = **v0.32.0** (GIT_SHA 27d1d2a, - CC-диспатчер mappa.ps1 + mappa-statusline.ps1, только hooks/ — серверный код не менялся). - Deploy-паттерн: релизная ветка `deploy/vX.Y.Z` из батч-tip → бамп package.json → merge в master. -- **Стек Portainer:** `mappa` (Id 27, endpoint 1). Source-of-truth compose: - `admin/host-stacks/vds-kzntsv/mappa.compose.yml`. -- **БД:** база `mappa` + role `mappa` на общем `postgres:16` VDS (owner = mappa). - Креды: **`pass show mappa/full-env`** (`MAPPA_DB_PASS`, `DATABASE_URL` docker-сетевой: - `postgres://mappa:…@postgres:5432/mappa`; внешний host `postgres.vds.kzntsv.site:5432` — TLS-verify-skip). -- **Endpoint:** `https://mappa.vds.kzntsv.site` (traefik websecure, letsEncrypt, service-port 8731). - -## Реденплой (обновление образа) — быстрый путь - -```bash -# 1. Собрать и запушить новый тег образа (с воркстейшена, Docker Desktop) -cd ~/projects/mappa -docker build -t registry.kzntsv.site/mappa: . -docker push registry.kzntsv.site/mappa: - -# 2. НА VDS: docker pull ДО restart стека (иначе застрянет на старом digest!) -ssh vds.kzntsv.site 'docker pull registry.kzntsv.site/mappa:' - -# 3. Поменять image: в host-stacks/vds-kzntsv/mappa.compose.yml И в Portainer stack 27 -# (обе копии синхронизировать — иначе следующий redeploy из гита откатит тег) - -# 4. Обновить стек через Portainer API (НЕ ad-hoc docker compose up!): PUT /api/stacks/27?endpointId=1 -# с {stackFileContent (новый тег), env 3/3, pullImage:true} — X-API-Key: pass vds-kzntsv/full-env PORTAINER_API_KEY. -# PUT без env сбрасывает env-массив (gotcha 4b) — env обязателен. pullImage:true тянет новый digest сам. -# Рестарт-only (POST /restart) НЕ меняет тег образа — только для рестарта без смены образа. - -# 5. Verify -curl -ksS https://mappa.vds.kzntsv.site/health # {"ok":true,"service":"mappa"} -curl -ksS https://mappa.vds.kzntsv.site/admin/status # entities/by_type/projects -curl -ksS https://mappa.vds.kzntsv.site/graph/stats # nodes/edges/components -ssh vds.kzntsv.site 'docker ps --filter name=mappa --format "{{.Status}}"' -``` - -## Обновление env (пароль БД и т.п.) - -Через Portainer → Stack 27 → Edit (env array), НЕ файлом на VDS (Portainer string-mode не -материализует .env; env передаётся API-массивом — gotcha 1 `portainer-stack-management-vds`). -**Env 3/3** (все из `pass mappa/full-env`): `MAPPA_DB_PASS` (подставляется в DATABASE_URL compose), -`MAPPA_GITEA_TOKEN`, `MAPPA_API_TOKEN`. - -## Первичный деплой с нуля (cold start) - -1. **БД**: на VDS `docker exec postgres psql -U postgres`: - `CREATE ROLE mappa LOGIN PASSWORD ''; CREATE DATABASE mappa OWNER mappa;` - Пароль → `pass insert -m mappa/full-env`. -2. **Образ**: собрать (см. выше), `docker push`. -3. **Compose**: `admin/host-stacks/vds-kzntsv/mappa.compose.yml` → Portainer create - (standalone string-mode, endpointId=1, env `[{"name":"MAPPA_DB_PASS","value":…}]`). -4. **Проверка миграций**: при старте migrate() сам создаёт таблицы (6 шт: projects/counters/ - entities/edges/leases/logs, owner=mappa) — `docker logs mappa` + `\dt` в БД. -5. **Пинг mappa**: письмо в `mappa/.agents/inbox/` (event: mappa-deployed) — импорт и остальное за ними. - -## Мониторинг живости (монитор → ntfy) - -Остаток #985 (решение 15/18: алерт при падении сервиса) — реализован #1013 (2026-08-23). - -- **Скрипт:** `/root/mappa-ntfy-monitor/monitor.py` (python3 stdlib; источник: `admin/scripts/mappa-ntfy-monitor/monitor.py`). -- **Проверка:** GET `https://mappa.vds.kzntsv.site/health` — HTTP 200 + JSON `{"ok":true,"service":"mappa"}` (таймаут 15s). -- **Алерт:** ntfy топик `mappa-alerts` (basic-auth vitya, admin-role → rw все топики), после 3 подряд фейлов (~15 мин), раз за эпизод (dedupe); recovery-сообщение при возврате. -- **Крон:** `/etc/cron.d/mappa-ntfy-monitor` — `*/5 * * * *` (root, лог `/var/log/mappa-ntfy-monitor.log`). -- **Конфиг:** `/root/.mappa-ntfy-monitor.env` (600; NTFY_* из `pass vds-kzntsv/full-env`). State: `/root/.mappa-ntfy-monitor.state`. -- ⚠️ Монитор НЕ трогает стек 27 — только наблюдение; редеплой/рестарт — отдельной задачей. - -## Gotchas (из первых деплоев) - -1. **schema.sql в dist** — tsc компилирует только .ts; `migrate()` ищет `.sql` рядом с - `dist/src/lib/`. Без COPY в Dockerfile → контейнер в restart-loop («schema.sql not found»). -2. **docker pull до restart** — Portainer start/stop НЕ тянет новый digest сам по себе; - перезалитый тег без pull на VDS = старый образ (ловил 2× за сессию 2026-08-23; - начиная с v0.23.0 tag-смена идёт через PUT + pullImage:true — pull не нужен отдельно, но безвреден). -3. **Не ad-hoc compose** — все стеки VDS через Portainer (VDS ops rule). `docker rm -f` - вручную снёс контейнер — Portainer recreate поднял, но это не путь. -4. **mem_limit 512m** — канон app-стеков, не убирать (cgroup-лимит vs вся память хоста). -4b. **PUT stack сбрасывает env!** — при обновлении compose через `PUT /api/stacks/:id` Portainer обнуляет env-массив стека (Env: null), если не передать `env` в payload. Симптом: контейнер в restart-loop с `SASL: client password must be a string` (подстановка `${MAPPA_DB_PASS}` падает). Фикс: повторный PUT с `{stackFileContent, env:[{name,value}×3]}` (значения из `pass mappa/full-env`). Поймано на деплое v0.13.1 (2026-08-24). -5. **Внешний DATABASE_URL** — контейнерный `@postgres:5432` валиден только внутри docker-сети; - для импорта с воркстейшена — `postgres.vds.kzntsv.site:5432` (+ TLS-verify-skip, self-signed). -6. **Закрытие деплоя** — всегда пара «таска + письмо mappa» (канон `mappa-deploy-workflow`). -7. **E2 backfill с воркстейшена (task:1274, 2026-08-26)** — скрипт `scripts/backfill-frontmatter-tags.ts` - в репо (tsx). Нужен ВНЕШНИЙ DATABASE_URL (не docker-сетевой): - `postgres://mappa:@postgres.vds.kzntsv.site:5432/mappa?sslmode=require` + - `NODE_TLS_REJECT_UNAUTHORIZED=0` (self-signed, gotcha 5). dry-run → `--apply`. - Идемпотентен (ON CONFLICT DO NOTHING); повторный прогон добавляет только новые страницы. - Первый прогон: 2539 тегов / 6555 рёбер; проверку «секреты не стали тегами» — скан имён - на sk-/AKIA/JWT перед apply (имена password/token — теги-темы, не значения). -8. **Одноразовый ре-индекс tsvec при создании конфига токенизатора (v0.28.0, 2026-08-28)** — - миграция создаёт PG-конфиг `mappa` (гейт newly_created) и перестраивает tsvec ВСЕХ сущностей - (~13.5k коммитов). Старт после деплоя дольше обычного; health может отвечать 404/не стартовать - до ~1 мин — это ожидаемо, не паника. Проверять `docker ps` (health: starting) + повторять /health. -9. **attachment upload: boundary регистрозависим, content-type lowercases (v0.29.0, 2026-08-28)** — - сервер приводит content-type к lowercase (`ct.toLowerCase()`), а boundary в multipart — - регистрозависим (RFC 2046). Клиент с boundary, содержащим ЗАГЛАВНЫЕ (напр. `----LV0290`), - получает 400 «file field required»: delimiter не матчится. Рабочий вариант — lowercase-boundary - (`----lv0290`). Плюс: curl на Windows (mingw) шлёт multipart с LF-разделителями — серверный - парсер ждёт CRLF; надёжно — собрать multipart с CRLF вручную (`--data-binary`). Кандидат на - фикс в mappa: не lowercasing'ить boundary / матчить регистронезависимо. -10. **Контролируемый апдейт core-скилов (v0.30.0, 2026-08-28)** — PATCH `/skills` kind=core больше НЕ 403: требуется `source=admin` (event-аудит skill.core_update, task:1466). deploy-catalog.mjs шлёт source=admin → core-обновления применяются программно (пробел закрыт). DELETE core — по-прежнему 403. +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:1254]] (concepts/mappa-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/minio-split-cutover-runbook.md b/.wiki/concepts/minio-split-cutover-runbook.md index 73d608f..22a6225 100644 --- a/.wiki/concepts/minio-split-cutover-runbook.md +++ b/.wiki/concepts/minio-split-cutover-runbook.md @@ -1,280 +1,3 @@ ---- -title: MinIO split — cutover runbook (Track A books-vds 2020→2025 + Track B CMS-media → vds) -type: concept -tags: [minio, vds, books-vds, cutover, runbook, migration, snolla, s3, imgproxy, obsidian] -related: [[../entities/vds-kzntsv], [../entities/books-vds], [on-snolla-vds-deploy-runbook], [tandemmebel-vds-deploy-runbook], [snolla-live-prod-inplace-image-bump], [minio-imgproxy-on-vds], [portainer-stack-management-vds]] -updated: 2026-07-30 ---- +# ⛔ Файловый канал закрыт -# MinIO split — cutover runbook - -> **Режим:** подготовка. **На железе — НИЧЕГО без команды user.** Все команды готовы к выполнению по «го». -> Owner: admin. Создано 2026-07-29. Перенесён из `scripts/minio-split-cutover/` 2026-07-30 (дисциплина: runbook'и → `.wiki/concepts/`). - -## Goal - -Разнести MinIO-контент на 2 сервера: -- **books-vds shared `minio`** (`minio.kzntsv.site`) → только slovo-бакеты books-приложений. Апгрейд 2020→2025. -- **books-vds `bookva-minio`** → bookva-бакеты (уже там с 2026-05). Апгрейд 2020→2025. Консоль наружу НЕ выставляем. -- **vds `minio`** (`minio.vds.kzntsv.site`, уже 2025-09-07) → CMS-медиа snolla-сайтов + artmone + obsidian + modules + modulair + maxmind + strapi. -- Креды **не трогаем** (один root-ключ на все инстансы; **доступ → `pass show minio-vds/full-env`**, поля `MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD`). Креды НЕ в git. - -## Два независимых трека - -- **Track A — апгрейд books-vds minio (shared + bookva).** Не требует правок apps (transparent: hostname/creds/контент сохраняются). Можно делать отдельно, до/после Track B. -- **Track B — сплит CMS-медиа на vds.** Координированный cutover: миррор → DNS-flip imgproxy → snolla deploy. Требует гейтов. - -## Progress (2026-07-30) - -| Шаг | Статус | Примечание | -|---|---|---| -| B1 миррор CMS-медиа books-vds → vds minio | ✅ DONE | galleries/pilorama98/themes/assets смиррорены (свежим миррором поверх stale 2026-05) | -| B2 DNS-flip `imgproxy.kzntsv.site` → 89.253.255.94 | ✅ LIVE | imgproxy.kzntsv.site/ → 200 webp (vds imgproxy ← vds minio); CMS-сайты берут картинки через vds | -| **B3 on-snolla v2-пилот (S3-direct-path)** | ✅ LIVE 2026-07-30 | in-place bump стека 22 `473923e494db→07db7e3` (snolla 0.42.1 v2, config endpoint → minio.vds.kzntsv.site, без бампа package.json). Theme-assets `/themes/c406…/…` → 200 97324 B == live (aws-sdk v2 → vds minio 2025 — compat подтверждена, главный риск закрыт). Sitemap 3/3 parity, `/c` 404 parity. Rollback-тег `473923e494db` в registry. **Промежуточный** шаг — v2 совместим, но v3 — конечная цель (user 2026-07-30). | -| **B3v3 on-snolla v3-пилот** | ✅ LIVE 2026-07-30 | `@snollajs/snolla 0.43.2` published (verdaccio latest). Live MinIO verify ЗЕЛЁНЫЙ (snolla, оба конфига). **v3 cutover 2026-07-30T11:28Z**: bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (root package.json), commit `eb8c143` pushed, build на VDS → `registry.kzntsv.site/on-snolla:eb8c143`, Portainer PUT stack 22 env-preserving (8/8), prune:false pullImage:true, mem_limit 512m. Throwaway staging :5078 parity-gate GREEN (theme asset 200 97324 B byte-identical, sitemapindex+3 sub-sitemaps byte-identical, /c 404 parity). Live smoke GREEN: / 200, robots/sitemap/sub-sitemaps 200, /yandex 200, theme asset 200 97324 B (v3 aws-sdk v3 S3-direct read из vds MinIO 2025). TLS не тронут (LE до Oct 18). Rollback = образ-тег `07db7e3` (v2, жив). | -| **B3v3 kupimknigi.spb.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.0→0.43.2` + resolutions (commit `5364353`, apps/web + root). Build на VDS (archive → `~/build/kupimknigi`) → `registry.kzntsv.site/kupimknigi:5364353`(+latest). Bundles a83e2a9 endpoint fix (minio.vds.kzntsv.site) — был в репо, но в бегущем образе 9608ff6 (v2) его не было. Throwaway staging :5079 parity-gate GREEN: `/` byte-identical с real Host, theme asset `toolbox.css` 200 35799 B byte-identical (v3 aws-sdk v3 S3-direct read из vds MinIO 2025), robots 200, `/callback-order/`→301. Portainer PUT stack 21 env-preserving (8/8, node in-memory put-stack), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: `/` 200, robots/sitemap 200, H1 «Скупка книг на дому в СПб…», theme asset 200 35799 B. TLS LE до Oct 3 (не тронут). **Rollback = `9608ff6`** (v2, жив в registry). | -| **B3v3 labtools.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commit `0dc0b4e`, apps/web + root). Endpoint fix `4f3314f` в master, но в бегущем образе `566d41c` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. Build на VDS (archive → `~/build/labtools`) → `registry.kzntsv.site/labtools:0dc0b4e`(+latest). Throwaway staging :5080 parity-gate GREEN: `/` byte-identical (18594B), theme `lato.css` 200 9239B byte-identical (v3 S3-direct), sitemap 38 locs == live (0 diff, 0 bad), section order-parity byte-identical. Portainer PUT stack 17 env-preserving (8/8, node in-memory), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: menu pages 200, theme 200 9239B, TLS CN не тронут (LE до Sep 30). **Rollback = `566d41c`** (v2, жив). См. [[labtools-vds-deploy-runbook]] §0.43.2. | -| **B3v3 labtools.pro** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commits `12f8992` bump + `a106622` yarn.lock реген — обязателен, иначе `yarn install --immutable` падает YN0028). Endpoint fix `6c7ea88` в master, но в бегущем образе `0610432` (v2) его не было. Build на VDS (archive → `~/build/labtools-pro`) → `registry.kzntsv.site/labtools-pro:a106622`(+latest), digest `2152642c4b17`. Throwaway staging :5081 parity-gate GREEN: `/`+nav+catalog 4-байт diff = `og:url`/`og:image` request-host (benign), redirects IDENT, robots/sitemap IDENT (7 детей == live), theme `lato.css` 200 9994B md5-identical (v3 S3-direct), order-parity press-forms 12 изделий. Portainer PUT stack 19 env-preserving (8/8, node in-memory put-stack), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: nav+каталог+продукты 200, theme 200 9994B, TLS CN=labtools.pro не тронут (LE до Sep 30). **Rollback = `0610432`** (v2, жив). См. [[labtools.pro-vds-deploy-runbook]] §0.43.2. | -| **B3v3 emspb.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commit `5afd8a1`, apps/web + root). Endpoint fix `218c3ae` в master, но в бегущем образе `95a5c42` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. Build на VDS (archive → `~/build/emspb`) → `registry.kzntsv.site/emspb:5afd8a1`(+latest), digest `72bb59fc…`. Throwaway staging :5082 parity-gate GREEN: robots md5 identical (31B), sitemap 29 locs == live, 29/29 routes 200 (+2B host-context benign), theme assets 7/7 md5 IDENTICAL (v3 S3-direct: snolla-framework/bootstrap/toolbox/theme/slick + images/2.jpg + /images/3.jpg 115618B). Portainer PUT stack 18 env-preserving (8/8, node in-memory put-stack), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: nav 200, /services+/index.php 404, theme assets md5 LIVE==staging, redirects 301 byte-identical post-cutover, apex 200. TLS не тронут. **Rollback = `95a5c42`** (v2, жив). См. [[emspb-vds-deploy-runbook]] §0.43.2. | -| **B3v3 tandemmebel.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commit `9d51c09`, apps/web + root) + реген `yarn.lock` (single `@smithy/signature-v4@5.3.11`). Endpoint fix `f4ddce6` в master, но в бегущем образе `8df10ee` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. Build на VDS (archive → `~/build/tandemmebel`) → `registry.kzntsv.site/tandemmebel:9d51c09`(+latest), digest `d14982c24dc7…`, 476MB (v2 8df10ee 583MB — v3 leaner). Throwaway staging :5020 parity-gate GREEN: robots md5 `a01e4c60…` identical (87B), sitemap 5 sub-sitemaps → **184 locs == live 184 IDENTICAL** (0 prod-only/new-only), self-consistency 183×200 + 1×404 (`/articles` parity benign), `/` +557B host-context benign, theme `Gotham-Pro.css` 200 4436B (v3 S3-direct). Portainer PUT stack 20 env-preserving (8/8, node in-memory put-stack — кириллица compose), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: `/` 200 45742B, `/projects` 200, sitemap 184 locs, robots md5 identical, theme 200 4436B. TLS CN=tandemmebel.ru не тронут (LE до Oct 10). **Rollback = `8df10ee`** (v2, жив). См. [[tandemmebel-vds-deploy-runbook]] §0.43.2. **6/6 v3 сайтов GREEN — тираж v3 ЗАВЕРШЁН.** | -| Track A (books-vds minio 2020→2025) | ✅ DONE 2026-08-03 | **Оба инстанса переведены** на `RELEASE.2025-09-07T16-13-09Z` (тот же digest что vds). Shared `minio` (стек 30): зеркало всех 17 бакетов → `/usr/docker/minio/data-new`, Portainer swap, traefik/imgproxy работают. `bookva-minio` (стек 49): зеркало `books` (38692 obj) → том `bookva-minio-data-new` (docker volume rename НЕдоступен — старый daemon API; живёт на -new, бэкап-скрипт обновлён). Старые данные (`/usr/docker/minio/data`, `bookva-minio-data`) сохранены 48ч. Gotchas: (1) mc multipart-баг `ContentLength=16777216` на крупных файлах → `--disable-multipart`; (2) транзиентные `Connection closed by foreign host` при дефиците памяти (3.8G) при ПАРАЛЛЕЛЬНЫХ зеркалах → последовательно + retry-цикл; (3) compose файлы CRLF → `\r` в секрете ломает Authorization → `tr -d '\r'`; (4) Portainer stack file — через API `/api/stacks/{id}/file`. | -| **variant-cache bucket на vds 2025** | ✅ DONE 2026-07-31 | v3 cutover перевёл сайты на vds MinIO 2025, но `variant-cache` bucket не создали там (был на books-vds 2020). NoSuchBucket → 500 на ALL sharp gallery-images всех 6 v3 сайтов с 2026-07-30 (gate проверял theme CSS, не gallery URLs). Fix: `mc mb vds/variant-cache` (пустой, регенерируем). maljarka portfolio 34/34 → 200. См. memory `variant-cache-bucket-missing-on-vds-2025` + [[maljarka-vds-deploy-runbook]] §Гочи. | -| B4 .NET catch-all админ s3 endpoint | ✅ DONE 2026-07-30 | `C:\sites\snolla\Web.config` `` 6/6 endpoint `minio.kzntsv.site`→`minio.vds.kzntsv.site` (galleries/assets/themes×4). region `us-east-1`, forcePathStyle (bin: AWSSDK.Core/S3 v3 модульный, bindingRedirect AWSSDK 1.3.19 — legacy-мусор). Pre-flip verify: `MINIO_ROOT_USER`==Web.config accessKey `AKIAJ2YJP72W6ZHCRE6Q` (reuse, no rotation) → mc alias Web.config-кредами → `minio.vds.kzntsv.site` → galleries/assets/themes читаются (B1 mirror жив). IIS auto-recycle по Web.config; login 200. **Превью:** оказался pre-existing баг S3-провайдера (НЕ от flip): `GalleriesStorage`/`AssetsStorage` ctor падал `KeyNotFoundException` на `config["ownerId"]` — контроллер-пути gallery preview/upload/delete и media-assets передавали только `siteContext`. Self-test провайдера gap — покрывал `GalleryImagesService` + unit-тесты `S3Ids`/`S3ObjectKey`, но preview-контроллер не тестировался. Фикс в провайдере (drop-in 1 DLL, MoreThenCms.FileStorage.S3.dll — `ToolsController` в `MoreThenCms.Web.dll` не собирается на боксе): `S3CloudStorage.ReadOwnerId` null-safe (explicit ownerId винит; иначе `siteContext.AssetsOwnerId`==SiteId; пусто → null без throw). Тесты 21/21 (4 новых). Live-verified: превью грузятся против vds MinIO. Коммит MoreThenCms `09cb072d`. Backup: `Web.config.b4.bak` + `MoreThenCms.FileStorage.S3.dll.b4.bak`. | -| B5 Obsidian Remotely Save endpoint | ✅ DONE 2026-07-30 | endpoint flipped `minio.kzntsv.site`→`minio.vds.kzntsv.site` в vault `C:\Users\vitya\obsidian\main\.obsidian\plugins\remotely-save\data.json` (rev+base64 обфускация), bucket `obsidian`, s3Region `us-east-1`, forcePathStyle. Verified деобфускацией config-блока. См. memory `obsidian-remotely-save-endpoint-after-mirror` (менять ТОЛЬКО после B1 миррора — что и было). | - -**Решение user (2026-07-30):** сплит без бампа snolla-пакетов — сайты остаются на 0.42.1 + aws-sdk v2, только config endpoint. snolla-евый v3-трек (0.43.1/0.7.8/0.17.2) — HOLD, отдельная история (stostayer). Пилот on-snolla подтвердил: **v2 vs MinIO 2025 работает** (без @smithy pin) — SignatureDoesNotMatch нет. - -**Разворот 2026-07-30T11:00Z:** v3 — конечная цель (user). v3 опубликована (см. §B3-v3). Промежуточный v2-пилот доказал совместимость; v3-пилот on-snolla — следующий шаг после live MinIO verify. - ---- - -## §B3-v3 — v3-бамп (resolutions = optional determinism, НЕ mandatory) - -`@snollajs/snolla 0.43.2` опубликована 2026-07-30 на verdaccio (latest): `@snollajs/liquid 0.11.0` + `@snollajs/data 0.15.0` + `@snollajs/core 0.26.4` (peerDep `@snollajs/data` widen `<0.15.0`→`<0.16.0` — баг-фикс: кап не расширен в `21c73aa`) + `@snollajs/snolla 0.43.2` (@smithy pin + `@aws-sdk/client-s3 3.1005.0` exact baked в deps). Коммит snolla `a8b198d`. Тесты core 259/259, snolla 120/120. - -### Live MinIO verify 2026-07-30 — ЗЕЛЁНЫЙ (оба конфига) - -snolla гоняла clean-consumer install `@snollajs/snolla@0.43.2` против живого `minio.vds.kzntsv.site`: -- **Run A** (с resolutions, `@smithy/signature-v4` 5.3.11 на signing-path): LIST/READ-PARITY/PUT/GET/MULTIPART — все OK, EXIT 0. -- **Run B** (БЕЗ resolutions, `@smithy/signature-v4` **5.6.12** на signing-path): те же тесты — **все OK, EXIT 0**. - -**Оба зелёных.** 5.6.12 НЕ даёт SignatureDoesNotMatch против vds MinIO 2025. Regression-гипотеза (v3 SigV4 vs MinIO) **не воспроизвелась end-to-end** — была halted-bisect гипотезой, зацементированной как «CONFIRMED» без live-теста (косяк коммуникации, исправлен 2026-07-30). См. admin memory `aws-sdk-v3-smithy-sigv4-minio-regression`. - -### Resolutions = optional determinism, НЕ SigV4-необходимость - -Dual-instance механика реальна (5.3.11 exact из snolla/core + 5.6.12 из `@aws-sdk/core` рядом, без resolutions), но 5.6.12 на signing-path **работает** против vds MinIO 2025. Поэтому: -- `resolutions`-блок в package.json сайта — **опциональный** determinism-hardening (зафиксировать @smithy от свежего re-resolve, version-stability), НЕ обязательный для SigV4. -- Бамп только версии `@snollajs/snolla` без resolutions — **работает** (verify доказал). -- Рекомендую всё же ставить resolutions (детерминизм, безобидно, 5.3.11 тоже зелёный) — но framing «упадёт без resolutions» **ложен**, убрать. - -### Copy-paste resolutions-блок (optional, recommended для determinism) - -```json -"resolutions": { - "@aws-sdk/client-s3": "3.1005.0", - "@aws-sdk/lib-storage": "3.1005.0", - "@smithy/signature-v4": "5.3.11", - "@smithy/node-http-handler": "4.4.14", - "@smithy/fetch-http-handler": "5.3.13", - "@smithy/protocol-http": "5.3.11", - "@smithy/middleware-endpoint": "4.4.23", - "@smithy/middleware-retry": "4.4.40" -} -``` - -### v3 bump procedure (per site, после live MinIO verify ⚪) - -1. `package.json`: `@snollajs/snolla` → `0.43.2` + вставить resolutions-блок выше (optional но recommended). -2. `yarn install` (clean resolve — убедиться `npm ls @smithy/signature-v4` == single instance). -3. Build на VDS из site-репо (`deploy/Dockerfile`, build-arg VERDACCIO_TOKEN) → `registry.kzntsv.site/-web:`. -4. Throwaway staging `:50XX` из env живого контейнера → parity-gate (theme asset 200 97324 B == live, sitemap 3/3, `/c` 404 parity). -5. Portainer PUT env-preserving (8 секретов), `prune:false, pullImage:true`, mem_limit 512m. -6. Live smoke (crawl menu-страниц + viewModel, не только `/`). -7. Rollback = образ-тег (текущий v2-тег). - -### Breaking changes v3 (минимизированы back-compat шимом) - -- `s3ForcePathStyle` → `forcePathStyle` — рантайм **НЕ breaking** (back-compat шим в storage.js v3 читает оба ключа). Config-правка recommended (forward-clean), не обязательна. -- `sslEnabled` (config) + `S3_SSL_ENABLED` (env) — удалены (unused), игнорируются если остались. -- `aws-sdk` / `s3fs` удалены из snolla deps (s3fs был unused). Consumer-код, импортивший их из snolla-deps напрямую (неофициальная поверхность) — сломается; по коду 6 сайтов такого нет. -- `populate-theme-store.mjs` мигрирован v2→v3 (script, не рантайм) — admin-side, рантайм не трогает. - ---- - -## S3-клиент совместимость (audit — сделано) - -| consumer | S3 client | версия | риск vs MinIO 2025 | -|---|---|---|---| -| books (api/web/...) | @aws-sdk/client-s3 (v3) | ^3.450.0 | ✅ низкий | -| modules-db | minio (minio-js) | ^8.0.7 | ✅ низкий | -| snolla (7 сайтов + framework) | **aws-sdk v2** | ^2.1257.0 | ⚠️ EOL, тест обязателен | -| modulair-rag | client inside lightrag:latest image | — | проверить (bucket пуст) | -| obsidian Remotely Save | bundled | 0.5.25 | ✅ низкий | -| .NET admin (AWSSDK) | AWSSDK.S3 | ? | проверить (region `local`) | - -**snolla ДО кутовера** запускает локальный node-скрипт (aws-sdk v2) → `https://minio.vds.kzntsv.site`, bucket `galleries` (stale-миррор там есть): listObjectsV2 + getObject. 200 = ок. Падение = бамп v2→v3 в фреймворке до кутовера. - ---- - -# Track A — апгрейд books-vds minio 2020→2025 - -> Все шаги — на books-vds (`root@89.253.255.133`, ключ `~/.ssh/id_ed25519_books_ops`). -> Transparent: НЕ меняем hostname/creds/имя-контейнера для apps. Только swap бэкенда под тем же именем. -> Креды для `mc alias` — из `pass show minio-vds/full-env` (`MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD`). - -## A1. Shared `minio` 2020→2025 - -Принцип: новый 2025-контейнер `minio-new` (новый data-dir), `mc mirror` slovo-бакетов, swap traefik route, retire old. Контейнер-имя `minio` + internal DNS `http://minio:9000` сохранить для imgproxy/зависимостей (или imgproxy на books-vds выводится из эксплуатации после Track B DNS-flip — тогда можно переименовать). - -**Бакеты, что остаются на shared minio (slovo):** `books` (37442 obj, 1.2 GiB), `ozon` (0B), `manuals` (0B), `test`/`upload`/`imgproxytest`. + почистить bookva-дубли (см. A3). - -```bash -# Креды из pass (НЕ хардкодить, НЕ коммитить): -source <(pass show minio-vds/full-env | grep -E 'MINIO_ROOT_(USER|PASSWORD)') - -# 0. Стоп-условие: books apps живы на старом minio (baseline). Снимок логов. -ssh root@89.253.255.133 'docker logs --tail 50 books-api; docker exec minio sh -c "echo ok"' - -# 1. Новый 2025 контейнер, новый data-dir, тот же network=proxy, креды из env -ssh root@89.253.255.133 'mkdir -p /usr/docker/minio-new/data' -# compose: image minio/minio:RELEASE.2025-09-07..., command server /data --console-address :9001, -# env MINIO_ROOT_USER/PASSWORD (== $MINIO_ROOT_USER/$MINIO_ROOT_PASSWORD), volume /usr/docker/minio-new/data:/data, -# network proxy, traefik labels minio-console.kzntsv.site→:9001 (новая консоль) — написать compose-файл -# (template ниже; НЕ поднимать до готовности миррора) - -# 2. mc mirror slovo-бакетов old(2020) → new(2025) -docker run --rm --network host --entrypoint sh minio/mc:latest -c ' - mc alias set old http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null - mc alias set new http://127.0.0.1:900X "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null - for b in books ozon manuals test upload imgproxytest; do mc mb --ignore-existing new/$b; mc mirror --overwrite old/$b new/$b; done -' -# 3. Verify per-bucket object count old==new -# for b in ...; mc ls --recursive old/$b | wc -l ; mc ls --recursive new/$b | wc -l - -# 4. Swap: traefik route minio.kzntsv.site → new:9000 (rule в dynamic config / compose labels new-контейнера) -# + minio-console.kzntsv.site → new:9001 (новая веб-консоль 2025; старый /minio/login на :9000 умрёт) -# 5. Smoke: books-api/web/task-runner пишут/читают S3. Picking-list report GET. -# 6. Retire old 2020 minio (docker stop minio; НЕ удалять data 48ч — rollback). -``` - -**Rollback:** traefik route → old minio:9000; old container ещё running 48ч. - -## A2. `bookva-minio` 2020→2025 - -Контейнер `bookva-minio`, volume `bookva-minio-data` (37074 obj в бакете `books`). НЕ пуст (мигрирован ~2026-05). Console наружу НЕ выставляем. - -```bash -source <(pass show minio-vds/full-env | grep -E 'MINIO_ROOT_(USER|PASSWORD)') -# 1. Новый 2025 контейнер bookva-minio-new (новый volume bookva-minio-data-new), тот же creds -# 2. mc mirror bookva-minio(2020):9001 → bookva-minio-new(2025):900X, бакет books (37074 obj) -docker run --rm --network host --entrypoint sh minio/mc:latest -c ' - mc alias set bvold http://127.0.0.1:9001 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null - mc alias set bvnew http://127.0.0.1:900X "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null - mc mb --ignore-existing bvnew/books; mc mirror --overwrite bvold/books bvnew/books -' -# 3. Verify count: mc ls --recursive bvold/books | wc -l == mc ls --recursive bvnew/books | wc -l (37074) -# 4. Swap: container name bookva-minio (чтобы bookva-api http://bookva-minio:9000 не менялся) → новый -# (down old, up new с именем bookva-minio). Или compose-имя сохранить. -# 5. Smoke: bookva-api report/picking-list GET. -# 6. Retire old (48ч rollback). -``` - -## A3. Почистить bookva-дубли из shared `books` (после A1, перед mirror-verify) - -В shared `books` остались 95+4 bookva-файла от до-миграции (дубли того, что уже на bookva-minio). Bookva-unique ключи вычислить из bookva-db и удалить из shared `books`. - -```bash -# Выгрузить bookva-unique s3-пути (на books-vds): -docker exec -i bookva-db mariadb -ubooks -pFNud4Um47y books -N -e \ - "SELECT DISTINCT report_remote_path FROM fbs_picking_lists WHERE report_remote_path!=''" > /tmp/bv-paths.txt -# (плюс pdf_remote_path, и из других таблиц — receipts/reports/task-reports — полный набор S3-путей bookva) -# Пересечь с shared-minio books-бакетом; bookva-unique (не в slovo-db) → mc rm из shared books -``` -**Гейт:** до удаления убедиться, что КАЖДЫЙ bookva-путь жив на bookva-minio (после A2). Иначе не удалять. - ---- - -# Track B — сплит CMS-медиа на vds - -## B0. vds backup (precondition) - -`scripts/vds-backup-rsync-kreknin/run.sh` rsync sources — добавить `/opt/stacks/storage/minio-imgproxy/data` (и compose/nginx.conf для воспроизводимости): - -```diff - /opt/stacks/owncloud \ -+ /opt/stacks/storage/minio-imgproxy/data \ - /etc/ssh \ -``` -+ обновить EMAIL_BODY components. На vds: `sudo systemctl restart cron` не нужен (cron читает файл). **Verify:** дождаться реального прогона (05:00) ИЛИ manual `sudo bash /opt/stacks/backup/scripts/run.sh` → ntfy push на `vds-backup` topic (phone-confirm, был открыт). - -## B1. Миррор CMS-медиа books-vds → vds minio (свежий) - -vds minio уже 2025-09-07. Stale-копия от 2026-05 — перезаписать свежим миррором. - -```bash -source <(pass show minio-vds/full-env | grep -E 'MINIO_ROOT_(USER|PASSWORD)') -# На books-vds (источник) → vds minio (назначение), через mc. Creds одинаковые. -docker run --rm --network host --entrypoint sh minio/mc:latest -c ' - mc alias set src http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null - mc alias set dst https://minio.vds.kzntsv.site "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" --insecure >/dev/null - for b in galleries artmone themes assets pilorama98 maxmind obsidian strapi; do - mc mb --ignore-existing dst/$b - mc mirror --overwrite --preserve src/$b dst/$b --insecure - done -' -# variant-cache НЕ миррорим (кэш imgproxy, перестроится на vds). -# modulair / modules — ВНЕ СПЛИТА (user: забыть). Остаются на books-vds shared minio, потребители не трогаются. -``` -**Verify (обязательно):** recursive-count src==dst по каждому бакету (mc ls --recursive | wc -l). Совпадение = ок. -- Ожидаемые объёмы: galleries 9984, artmone 16273, themes 10864, assets 4750, pilorama98 225, maxmind 2, obsidian 179, strapi 0. ~8.9 GiB. - -## B2. DNS-flip `imgproxy.kzntsv.site` → 89.253.255.94 (reg.ru) - -- В reg.ru `kzntsv.site` A-запись `imgproxy` → `89.253.255.94` (сейчас → books-vds 89.253.255.133). -- vds imgproxy-nginx traefik label: добавить `Host(imgproxy.kzntsv.site)` (сейчас serves `imgproxy.vds.kzntsv.site`). -- TTL дождаться (был 86400 — лучше заранее снизить). -- **Verify:** `curl -sI https://imgproxy.kzntsv.site/` → vds traefik; sample CMS image URL (signed) → 200. - -## B3. snolla deploy (6 сайтов) — по "го", после B1+B2 - -Образы собирает admin на VDS (snolla MCP read-only, нет build-канала; либо user даёт канал). Registry = `registry.kzntsv.site` (НЕ stostayer). - -```bash -# На vds, для каждого из 6 репо (on-snolla, kupimknigi, tandemmebel, labtools, labtools.pro, emspb): -cd /tmp && git clone git@git.kzntsv.site:victor/.git && cd -git pull # правка s3.endpoint уже в master (snolla запушил: 07db7e3, a83e2a9, 218c3ae, 4f3314f, 6c7ea88, f4ddce6) -docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN= \ - -t registry.kzntsv.site/-web: . -docker push registry.kzntsv.site/-web: -``` -Затем parity-gate (throwaway staging `:50XX`) + Portainer PUT env-preserving (по [[snolla-live-prod-inplace-image-bump]]). Smoke каждого сайта (crawl menu pages + viewModel, не только /). - -## B4. .NET catch-all админ (воркстейшн) — s3 endpoint - -`C:\sites\snolla\Web.config` → `` serviceURL: `https://minio.kzntsv.site` → `https://minio.vds.kzntsv.site` (galleries/assets/themes). imgproxy НЕ трогать (хардкод `imgproxy.kzntsv.site` → DNS-flip решает). IIS auto-recycle по Web.config. Smoke: `/admin/account/login` → Asset Manager превью. - -## B5. Obsidian Remotely Save — endpoint (ПОСЛЕ B1 миррора obsidian) - -Гейт: бакет `obsidian` смиррорен на vds (B1). Менять ТОЛЬКО тогда. -- Obsidian → Settings → Remotely Save → S3 endpoint: `https://minio.kzntsv.site/` → `https://minio.vds.kzntsv.site/` (бакет `obsidian`, креды те же). ИЛИ ре-обфусцировать `C:\Users\vitya\obsidian\main\.obsidian\plugins\remotely-save\data.json` (rev+base64) при ЗАКРЫТОМ Obsidian. -- Verify: sync run → no errors. - ---- - -# P3 — cleanup / verify (после cutover) - -- Retire books-vds 2020 minio containers (после 48ч rollback-окна): `docker stop minio` (old), `docker stop bookva-minio` (old). НЕ удалять data до подтверждения бэкапа. -- Retire books-vds imgproxy stack 29 (после DNS-flip — изображения идут через vds imgproxy). Проверить, что никто не ходит на books-vds imgproxy. -- Удалить с shared `books` bookva-дубли (A3). -- vds-backup: подтвердить что `/opt/stacks/storage/minio-imgproxy/data` попадает в nightly snapshot (B0 verify). -- Smoke: все 6 snolla-сайтов (crawl), books slovo reports, bookva reports, obsidian sync, modules-db. - ---- - -# Открытые/verify (до кутовера) - -1. **snolla compat-тест aws-sdk v2 vs vds minio** — snolla делает сейчас (без железа). -2. **.NET admin AWSSDK версия + region** — `local` работает с MinIO 2025? (stostayer нужен был `us-west-1`, но там minio настроен `us-west-1`; books-vds minio = `local`). Verify. -3. **modulair-rag lightrag:latest S3-клиент** — compat с MinIO 2025 (bucket пуст, низкий приоритет). -4. **artmone** — деплой неясен (контейнера на vds нет). Бакет миррорим (B1), но потребителя для endpoint-смены нет. Отдельный вопрос. -5. **bookva-minio расход 368 obj** (shared 37442 vs bookva-minio 37074) — проверить, не stranded ли bookva-файлы в shared (не попали в миррор 2026-05). До A3. \ No newline at end of file +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:173]] (concepts/minio-split-cutover-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/on-snolla-vds-deploy-runbook.md b/.wiki/concepts/on-snolla-vds-deploy-runbook.md index 007d868..0009311 100644 --- a/.wiki/concepts/on-snolla-vds-deploy-runbook.md +++ b/.wiki/concepts/on-snolla-vds-deploy-runbook.md @@ -1,101 +1,3 @@ ---- -title: on.snolla.com snolla-app — VDS deploy runbook (cutover 2026-07-20) -type: concept -tags: [on-snolla, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, landing] -related: [[../entities/vds-kzntsv], [../entities/ruvds-iis-host], [tandemmebel-vds-deploy-runbook], [snolla-live-prod-inplace-image-bump], [portainer-stack-management-vds], [snolla-local-admin-and-on-snolla-migration-design]] -updated: 2026-07-20 ---- +# ⛔ Файловый канал закрыт -# on.snolla.com → VDS deploy runbook - -Вынос посадочной `on.snolla.com` (snolla-app, `@snollajs/snolla` **0.42.1**, server-side Liquid) с -[[../entities/ruvds-iis-host]] (catch-all CMS, 80.64.31.36) в docker-контейнер на [[../entities/vds-kzntsv]] -(89.253.255.94), за traefik. Паттерн = [[tandemmebel-vds-deploy-runbook]]. Отличие: **лендинг** (1 Page `/` -+ 2 StaticPages), БЕЗ блога/каталога/e-commerce → reconstruction = layout.liquid byte-identical prod-HTML. -Culture `en`. Спека: [[snolla-local-admin-and-on-snolla-migration-design]] §Task B. - -## Артефакты -- **Код:** `victor/on.snolla.com` @ `473923e494db` (apps/web, snolla 0.42.1). Структура-калька tandemmebel.ru. - `deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root, healthcheck `/robots.txt`). -- **Образ:** `registry.kzntsv.site/on-snolla:473923e494db` (+`:latest`). Имя **`on-snolla`** (= стек/контейнер). - Собран НА VDS (обход traefik-499). -- **Стек Portainer:** `on-snolla` (**Id 22**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/on-snolla.compose.yml`. -- **siteId:** `B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7` (non-secret, в `production.json`). -- **siteUrl:** `https://on.snolla.com` (в `production.json`). -- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD` - — Portainer stack env (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). НЕ в образе, НЕ в git. -- **mem_limit:** `512m`. - -## Site model (MoreThenCms DB, siteId B9ECDB50) -- 1 Page `/` (template `content_page`, `customDefaultLayout=layout`). **Content-поля пустые** → весь - bootstraptor-лендинг baked в `views/layout.liquid` (byte-identical prod). `{{ item.content }}` (пуст) wired. -- Form "Join us" (Path `/`, FormTemplate `join_us`: first_name/last_name/email/website/description/captcha) - baked в `layout.liquid`; POST `/` → snolla `forms` middleware → `form_ajax_response.liquid` (AJAX replace). - Legacy reCAPTCHA v1 в разметке мертва (Google shut down v1) — parity с продом, НЕ чиним. -- StaticPages: `/yandex_7a510ac1f8311fca.html` (verification, IncludeInSitemap=true), `/4a052808276c.html` (not in sitemap). -- `/c` → **404** (stale dead-link в .NET sitemap; snolla-app sitemap тоже его несёт → parity `/`+`/c`+`/yandex`). -- Theme assets `/themes/c406a987ebe14244b0eefe7b8f959a6f/{css,js,images}/*` — из MinIO bucket `themes` (snolla - `themeFiles` middleware; пути literal в `layout.liquid`). -- `/admin` — **публичного `on.snolla.com/admin` не существует** (никогда не было: RUVDS catch-all IIS не - маршрутизировал /admin на публичном on.snolla.com — cert на 80.64.31.36:443 SNI on.snolla.com = чужой - `CN=kupimknigi.spb.ru`, /admin → 404). Админка — только **локальный catch-all IIS `snolla`** (Task A, - воркстейшн). Site alias переименован `on`→`internal` (2026-07-20), доступ = `http://internal.snolla.com/admin` - через hosts override → 127.0.0.1 → 302→login. **Web.config `primaryAlias`** тоже обновлён `on`→`internal` - (default-site resolution; иначе catch-all NRE-500 на ВСЕХ запросах — primaryAlias ссылается на алиас, - переименование сайта без обновления primaryAlias ломает весь catch-all). НЕ часть snolla-app. - -## Cutover 2026-07-20 (первый вынос на VDS — DNS-gated) -1. **Build на VDS** (archive `473923e494db` → `~/build/on-snolla` → `docker build -f deploy/Dockerfile - --build-arg VERDACCIO_TOKEN -t .../on-snolla:473923e494db -t .../on-snolla:latest . && push`). Guard: - `config/default.json` ABSENT в архиве (.dockerignore), пин snolla = 0.42.1. -2. **Throwaway-staging :5077** из собранного env (8 секретов inline, т.к. нового live-контейнера ещё нет — - секреты общие с тиражом). `docker run -d --name on-snolla-staging --env-file .staging.env -p 127.0.0.1:5077:5000`. - Healthy. robots/`/`/sitemap 200. -3. **Completeness-gate С VDS** — sitemapindex разворот. **NEW==PROD locs: 3 = 3 IDENTICAL** (`/`, `/c`, `/yandex_…`). - Self-consistency: `/`→200, `/yandex`→200, `/c`→404 (dead-link, parity с прод-оракулом — prod /c тоже 404). - **Byte-parity:** homepage + /yandex **byte-identical** staging vs prod (RUVDS, via `--resolve :443:80.64.31.36`). - GREEN = 0 регрессий. -4. **Portainer стек 22** создан со **staging-Host** `on-snolla.vds.kzntsv.site` (wildcard → VDS), LE=default cert. - Healthy за traefik. (`create-stack.mjs` node-скрипт, Portainer JWT auth, env array 8/8, `mem_limit 512m`.) -5. **DNS flip** — operator: reg.ru `on.snolla.com` A `80.64.31.36` → `89.253.255.94`. -6. **Verify авторит. NS** — `nslookup on.snolla.com ns1/ns2.reg.ru` = `89.253.255.94` (BEFORE traefik rule swap — - memory `operator-dns-flip-verify-domain-before-cutover`: wrong domain burns LE rate-limit). Public resolvers propagated. -7. **PUT стека 22** — rule `Host(on-snolla.vds.kzntsv.site)` → `Host(on.snolla.com)`, env-preserving (8/8), - `prune:false, pullImage:false`. node `cutover-put.mjs` (Portainer JWT; **re-auth right before PUT** — - jwt из GET-фазы успел протухнуть к моменту PUT, первый PUT дал 401). PUT 200. -8. **Poll** — контейнер healthy, **LE-серт issued on first hit**: CN=on.snolla.com, issuer YR1, until 2026-10-18. -9. **Live-smoke С VDS + external** — `https://on.snolla.com/{robots.txt,/,/sitemap.xml,/yandex_…}` = 200, - `/c` = 404 parity. TLS verify `ssl_verify_result=0` (trusted LE chain). Homepage **byte-identical** prod-HTML. - -## In-place bumps (после cutover) - -- **07db7e3 — MinIO split config-endpoint (v2, 2026-07-30).** In-place bump стека 22 `473923e494db→07db7e3` без правки `package.json`: только `config/production.json` `s3.endpoint` `minio.kzntsv.site→minio.vds.kzntsv.site`. snolla остался 0.42.1 (aws-sdk v2). Пилот S3-direct-path — theme asset 200 97324 B == live, v2 vs vds MinIO 2025 работает без @smithy pin. Rollback-тег `473923e494db`. См. [[minio-split-cutover-runbook]] §B3 (Track B). -- **eb8c143 — v3 bump (aws-sdk v2→v3, 2026-07-30T11:28Z).** End-goal (user). `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` + `resolutions` (@aws-sdk/client-s3 3.1005.0 exact + @smithy chain 5.3.11) в root `package.json` (optional determinism, single-instance — live verify доказал что 5.6.12 тоже работает, pin для version-stability). Commit `eb8c143` pushed. Build на VDS (archive → `~/build/on-snolla` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN`) → `registry.kzntsv.site/on-snolla:eb8c143` (+`:latest`). Throwaway staging :5078 из env live-контейнера (8 секретов) → parity-gate GREEN (theme asset 200 97324 B byte-identical, sitemapindex+3 sub-sitemaps byte-identical, `/c` 404 parity). Portainer PUT stack 22 env-preserving (8/8, node in-memory `put-stack` — НЕ PS Invoke-RestMethod), `prune:false, pullImage:true`, `mem_limit 512m` preserved. Live smoke GREEN: `/` 200, robots/sitemap/sub-sitemaps 200, `/yandex` 200, theme asset 200 97324 B (v3 aws-sdk v3 S3-direct read из vds MinIO 2025). TLS не тронут (LE до Oct 18). Back-compat шим `s3ForcePathStyle`→`forcePathStyle` (v2-key ещё читается) — config-правки не требовались. **Rollback = `07db7e3`** (v2, жив в registry). См. [[minio-split-cutover-runbook]] §B3-v3 + [[snolla-live-prod-inplace-image-bump]]. - -## Гочи (специфичные) -- **snolla `robotsTxt` middleware падает на undefined `app.locals.domain`** — on.snolla.com НЕ имеет row в - MoreThenCms `Domains` (served by primaryDomain без per-domain row). `robotsTxt.js` читает - `res.app.locals.domain.robotsTxt` без null-guard → GET /robots.txt (и Dockerfile healthcheck) крашится 500. - **Фикс в `apps/web/index.js`**: после `initApp`, если `!app.locals.domain`, synthesized `{robotsTxt: site.robotsTxt}` - (only robotsTxt.js reads locals.domain; theme resolution uses site.activeThemeId — safe). Без мутации shared DB. - **Альтернатива** (не применена): добавить Domains row (Name=on.snolla.com, SiteId=B9ECDB50, Public=1, ThemeId=null) - — но это мутация shared prod MoreThenCms; предпочтён app-code fallback. -- **Form "Join us" не имеет Liquid form-tag** — snolla не предоставляет form-rendering tag. Форма HTML baked в - `layout.liquid` (поля по FormTemplate `join_us`); POST обрабатывает `forms` middleware (мэтч по Path `/`). - `form_ajax_response.liquid` — AJAX-ответ (`$form.replaceWith(response)`). -- **`/c` в sitemap — dead-link** — snolla SitemapService эмбитит `/c` из sections-source, но страница 404. - Prod-оракул идентичен (404) → benign parity, не регрессия. -- **hosts override на воркстейшне** — Task A оставил `127.0.0.1 on.snolla.com` в hosts (локальный админ catch-all). - External verify прод-on.snolla.com с воркстейшна = только через `curl --resolve on.snolla.com:443:89.253.255.94` - (иначе попадёшь в локальный IIS). См. memory `workstation-lan-dns-serves-local-cms-copy`. -- **Portainer JWT short-lived** — GET stack + GET file + PUT в одном скрипте: jwt из GET-фазы может протухнуть - к PUT. Re-auth (`POST /api/auth`) прямо перед PUT. API-key `ptr_*` даёт 401 (см. [[portainer-stack-management-vds]]). -- **Образных rollback-тегов пока нет** — первый deploy, `473923e494db` = `:latest`. ~~Rollback = revert DNS (RUVDS жив).~~ RUVDS DECOMM 2026-07-21 — rollback только образ-тегом. - -## Rollback -- ~~**DNS (предпочт, мгновенный):** revert reg.ru `on.snolla.com` A → `80.64.31.36` (RUVDS IIS жив, не тронут).~~ **НЕАКТУАЛЬНО с 2026-07-21** — [[../entities/ruvds-iis-host]] DECOMM (погашен у провайдера). RUVDS более не rollback-target. -- **Образный (рабочий):** PUT стека 22 назад на предыдущий тег (rollback-теги в registry). `473923e494db` = `:latest` — первый deploy, предыдущего тега нет; с этого дня ведём rollback-теги на каждый bump. - -## Связанное -- on.snolla.com admin (`/admin`) остаётся на RUVDS .NET — catch-all IIS `snolla` (см. [[snolla-local-admin-and-on-snolla-migration-design]] §Task A). -- Тираж snolla на VDS: labtools.ru(17), emspb(18), labtools.pro(19), tandemmebel(20), kupimknigi(21), **on-snolla(22)**. \ No newline at end of file +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:182]] (concepts/on-snolla-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/pilorama98-vds-deploy-runbook.md b/.wiki/concepts/pilorama98-vds-deploy-runbook.md index 9b9dc32..f62c7a4 100644 --- a/.wiki/concepts/pilorama98-vds-deploy-runbook.md +++ b/.wiki/concepts/pilorama98-vds-deploy-runbook.md @@ -1,160 +1,3 @@ ---- -title: "pilorama98.ru (pilonuxt) — VDS deploy runbook (стек 16, рестарт/пересборка)" -type: concept -tags: [pilorama98, pilonuxt, vds, deploy, docker, portainer, nuxt, runbook, cache] -related: [concepts/portainer-stack-management-vds.md, concepts/pilonuxt-restart-vs-redeploy-2026-08-16.md, concepts/admin-api-embed-pilonuxt.md] -sources: [] -updated: 2026-08-26 ---- +# ⛔ Файловый канал закрыт -# pilorama98.ru (pilonuxt) — деплой на прод - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -Прод `https://pilorama98.ru` / `www.pilorama98.ru` = Nuxt-фронт из монорепы -`victor/pilorama98.ru` (`apps/web`, Nuxt 4 / Nitro node-server preset). Хостится -контейнером на VDS kzntsv, управляется Portainer-стеком. Легаси IIS (RUVDS) — -rollback-путь, не используется. - -## Параметры - -| Что | Значение | -|---|---| -| Хост | VDS kzntsv `89.253.255.94` | -| Стек Portainer | **Id 16**, name `pilonuxt`, endpointId **1** | -| Образ | `registry.kzntsv.site/pilonuxt:` — тег = короткий sha монорепы | -| Compose source-of-truth | `admin/host-stacks/vds-kzntsv/pilonuxt.compose.yml` (в репо `.admin`) | -| Репо/код | `victor/pilorama98.ru` @ master; `apps/web/Dockerfile` | -| БД | `mssql.kzntsv.site:1433` MoreThenCms (конфиг запечён в образ, см. ниже) | -| Версии на проде | см. инвентарь ниже (текущий 2026-08-26: `8943333`) | - -## Артефакты - -- **Образ**: двухстадийный `apps/web/Dockerfile` (node:22-bookworm-slim; builder - `yarn workspace nuxt-app build` → `.output`; runner копирует `@img` нативы sharp). - Сборка — **workspace-сборка из КОРНЯ монорепы**: `-f apps/web/Dockerfile .` - (lockfile/.yarnrc.yml в корне). Тег = короткий git-sha коммита. -- **VERDACCIO_TOKEN** — build-secret (`--secret id=verdaccio_token,env=VERDACCIO_TOKEN`), - живой JWT, не ARG/ENV (не оседает в слоях). Источник: `pass vds-kzntsv/full-env` - → `VERDACCIO_CI_TOKEN` (books-ci, len ~312, 2 точки). -- **Registry auth**: `docker login registry.kzntsv.site -u vitya` (пароль - `REGISTRY_PASS` из `pass vds-kzntsv/full-env`). -- **Portainer auth**: JWT `POST /api/auth` — `vitya` / `PORTAINER_ADMIN_PASS` - из `pass vds-kzntsv/full-env`. -- **Конфиг запечён** в `apps/web/config/` внутри образа (MSSQL/SMTP/imgproxy - ключи, siteId) — runtime env-override не настроен (решение 2026-06-14). - -## Две разные операции (не путать!) - -| Операция | Когда нужна | Как делается | Что сбрасывает | -|---|---|---|---| -| **Рестарт контейнера** | Контент/цены в БД изменились, код тот же | Portainer `POST /api/endpoints/1/docker/containers/pilonuxt/restart` | **In-memory viewModel кэш content-api** — свежие данные из MoreThenCms подтягиваются без пересборки | -| **Пересборка (деploy)** | Изменился код (новый git-sha в монорепе) | build → push → Portainer PUT стек 16 | Код + всё вышеперечисленное | - -Симптом «данные в БД есть, на фронте нет» (цены/контент устарели) → **рестарт**, не деплой. -Симптом «500 на странице со стеком @snolla/core / lodash _.find» → **код/данные, не кэш** — рестарт не лечит, нужна пересборка (см. [[concepts/pilonuxt-restart-vs-redeploy-2026-08-16.md]]). - -## Рестарт контейнера (кэш viewModel) - -```python -# Portainer docker-proxy (Python, urllib; либо .tmp/restart-stack16.ps1) -POST https://portainer.vds.kzntsv.site/api/auth {"username":"vitya","password":""} -POST https://portainer.vds.kzntsv.site/api/endpoints/1/docker/containers/pilonuxt/restart # body пустой, 204 -``` - -Verify: `GET .../containers/pilonuxt/json` → `State.StartedAt` обновился, образ тот же. - -## Пересборка + деплой (новый код) - -```bash -# 1. Проверить коммит: локально master == origin/master, коммит — потомок -cd ~/projects/pilorama98.ru -git fetch origin master && git log --oneline -1 master - -# 2. Build (из корня монорепы, build-secret). Предусловия: -# - apps/web/src/generated/ populated (после yarn install) — иначе ensure-schema лезет в mssql -# - .yarnrc.yml c npmAlwaysAuth: true (канон в репо) -VERDACCIO_TOKEN=$(pass show vds-kzntsv/full-env | grep '^VERDACCIO_CI_TOKEN=' | cut -d= -f2-) -docker build -f apps/web/Dockerfile --secret id=verdaccio_token,env=VERDACCIO_TOKEN \ - -t registry.kzntsv.site/pilonuxt: . - -# 3. Push -echo '' | docker login registry.kzntsv.site -u vitya --password-stdin -docker push registry.kzntsv.site/pilonuxt: - -# 4. Portainer PUT стека 16 (замена тега, env preserve, pullImage) -# GET /api/stacks/16/file → regexp замена pilonuxt: → pilonuxt: -# PUT /api/stacks/16?endpointId=1 {stackFileContent, env: , prune:false, pullImage:true} -``` - -⚠️ **PUT сбрасывает env-массив**, если не передать `env` в payload — у стека 16 env пуст -(`Env: []`), передавать literal `[]` (плейсхолдер-трюк при PS: `-replace '"__ENV__"','[]'`). -⚠️ Кодировка: тело PUT слать UTF-8-байтами (PowerShell 5.1 коррапит кириллицу, gotcha 9 -[[concepts/portainer-stack-management-vds.md]]). -⚠️ Синхронизировать source-of-truth `host-stacks/vds-kzntsv/pilonuxt.compose.yml` (тег) после PUT. - -## Verify - -```bash -# контейнер на новом образе, Up -GET https://portainer.vds.kzntsv.site/api/endpoints/1/docker/containers/pilonuxt/json -# → Config.Image == registry.kzntsv.site/pilonuxt:, State.running - -# страницы (SSR, live): категория бруска + карточки товаров -curl -ksS https://pilorama98.ru/catalog/brus-estestvennoj-vlazhnosti | grep -c '19 500' -curl -ksS https://pilorama98.ru/shop/products/brusok-estestvennoj-vlazhnosti-40-50-6000 | grep -c '19 500' -# viewModel API (content-api, свежие данные из БД): -# GET https://pilorama98.ru/snolla/stores/F42F4113-EC3E-4644-8D0E-70D3F430219D/products/viewModel?slug= -# → volume_price (каноническое поле). ⚠️ voulme_price — legacy-опечатка, НЕ использовать (фикс 38c26c7 + 8943333). -# консоль контейнера: docker logs pilonuxt — без краш-лупа -``` - -**БД напрямую** (если надо сверить запечённое): docker exec mssql через Portainer -(`/containers/mssql/exec`, sqlcmd `-U snolla -P `), -`SELECT ... FROM Products WHERE Slug IN (...)`, Content — JSON-массив `[{name,value}]` -(array-canon 0.11.0). - -## Rollback - -Тот же PUT стека 16 с прежним тегом (`` → ``), `pullImage:true`. -Старые теги висят в registry (инвентарь ниже). RTO ~30-60с (пересоздание контейнера). - -## Инвентарь образов (откат-цепочка) - -- `8943333` — **текущий** (2026-08-26: fix catalog voulme_price→volume_price) -- `64c2539` — предыдущий (admin-api 0.11.0 array-canon) -- `1128ddc` — core 0.26.7 (фикс 500 `_.find`) -- `f876e70` — заглушки каталога → фото (3 шт.) -- `24a13da` — «Брус антисептированный» фото -- `40806b1` — services-[slug] настоящий SSR 404 -- `340c0e7` — выпил «Строительство каркасных домов» -- `8b7e8c4` — SMTP-фикс (noreply@snolla.com) - -## Gotchas - -1. **Рестарт ≠ деплой.** Письма прогера «подтяни цены» = рестарт (кэш), письма - «задеплой коммит X» = пересборка. Не путать (см. инцидент 2026-08-16 и 2026-08-26). -2. **`voulme_price` — legacy-опечатка** (в БД застыла на старых значениях). Канон — - `volume_price` (loader пишет сюда с фикса 38c26c7). Категории рендерят `volume_price` - с фикса 8943333 (2026-08-26). Если где-то снова всплывёт voulme_price — это баг кода. -3. **PUT стека сбрасывает env** — всегда передавать `env` в payload (у 16 — `[]`). -4. **src/generated/ обязателен в build-контексте** — иначе билд лезет в mssql - (ensure-schema), а в чистом git clone COPY упадёт. -5. **VERDACCIO_TOKEN — build-secret, не ARG** (слой-чистота). Токен — живой JWT, - протухает — при 401 на verdaccio перевыпустить (books-ci, см. verdaccio-token-lifecycle). -6. **Kэш viewModel в памяти контейнера** — рестарт сбрасывает, пересборка не нужна - при смене данных. Проверено 2026-08-16 («цена за м³ подтянулась») и 2026-08-26 - (категории voulme_price — это был баг кода, не кэш). -7. **Конфиг запечён в образ** — правки `config/` требуют пересборки; runtime - env-override не настроен (решение 2026-06-14). - -## Связи - -- [[concepts/pilonuxt-restart-vs-redeploy-2026-08-16.md]] — рестарт vs пересборка, инцидент 2026-08-16 -- [[concepts/portainer-stack-management-vds.md]] — общий Portainer-канон (JWT, gotchas 9/10) -- [[concepts/admin-api-embed-pilonuxt.md]] — admin-api embed, array-canon -- Репо-вики: `apps/web/.wiki/concepts/docker-deploy.md` (сборка образа, деплой) -- Ранбук stostayer: [[concepts/stostayer-web-deploy-runbook.md]] — похожий канал (другой хост/стек) +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3330]] (concepts/pilorama98-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/runbooks-index.md b/.wiki/concepts/runbooks-index.md index 3c78e93..4ab7fb5 100644 --- a/.wiki/concepts/runbooks-index.md +++ b/.wiki/concepts/runbooks-index.md @@ -1,66 +1,3 @@ ---- -title: "Ранбуки — индекс (что есть, где лежит, что покрывает)" -type: concept -tags: [runbook, index, ops, deploy, admin] -related: [concepts/portainer-stack-management-vds.md] -updated: 2026-08-26 ---- +# ⛔ Файловый канал закрыт -# Индекс ранбуков зоны админа - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -Единый источник «какой ранбук для чего». **Обязательный первый шаг перед любой -прод-операцией** (деплой/редеплой/рестарт/ротация/миграция/инцидент) — скил -`admin-runbooks` §0. Если строки для проекта нет — ранбука не существует: -создать (см. `admin-runbooks` §WRITE) до/вместе с операцией. - -## VDS-проекты (`.admin/.wiki/concepts/`) - -| Проект / операция | Ранбук | Что покрывает | -|---|---|---| -| pilorama98.ru (pilonuxt, стек 16) | [`pilorama98-vds-deploy-runbook.md`](pilorama98-vds-deploy-runbook.md) | рестарт (кэш viewModel) vs пересборка; build с workstation (VERDACCIO secret); PUT стек 16; verify; rollback; инвентарь образов | -| pilorama98.ru — диагностика кэша vs кода | [`pilonuxt-restart-vs-redeploy-2026-08-16.md`](pilonuxt-restart-vs-redeploy-2026-08-16.md) | когда рестарт, когда пересборка; симптом 500 @snolla/core; инвентарь образов | -| stostayer.ru (legacy web, стек клиента) | [`stostayer-web-deploy-runbook.md`](stostayer-web-deploy-runbook.md) | build 0.3.x (offline-кэш, временные .dockerignore/.yarnrc); push 1 попыткой (egress-бан); redeploy через Portainer на хосте клиента; verify | -| mappa (стек 27) | [`mappa-vds-deploy-runbook.md`](mappa-vds-deploy-runbook.md) | build+push; pull-ДО-рестарта; compose sync; PUT стек 27 env 3/3; verify; gotcha env-сброс | -| emspb.ru (snolla-app, стек 18) | [`emspb-vds-deploy-runbook.md`](emspb-vds-deploy-runbook.md) | build на VDS (git archive); env-контракт 8 секретов; cutover | -| labtools.ru (стек 19) | [`labtools-vds-deploy-runbook.md`](labtools-vds-deploy-runbook.md) | то же семейство snolla-app | -| labtools.pro (стек 20) | [`labtools.pro-vds-deploy-runbook.md`](labtools.pro-vds-deploy-runbook.md) | то же семейство snolla-app | -| on.snolla.com (стек 22) | [`on-snolla-vds-deploy-runbook.md`](on-snolla-vds-deploy-runbook.md) | cutover 2026-07-20, семейство snolla-app | -| tandemmebel.ru (стек 20/иное) | [`tandemmebel-vds-deploy-runbook.md`](tandemmebel-vds-deploy-runbook.md) | cutover + snolla bumps | -| maljarka.tandemmebel.ru | [`maljarka-vds-deploy-runbook.md`](maljarka-vds-deploy-runbook.md) | restore-деплой 2026-07-31 | -| oCIS / owncloud | [`ocis-on-vds-deploy-recipe.md`](ocis-on-vds-deploy-recipe.md) | deploy recipe + gotchas | -| Локальный стенд sched-pipelines (sched + воркеры @apilki, мок-тест) | [`sched-pipelines-local-stack-runbook.md`](sched-pipelines-local-stack-runbook.md) | команды стенда, таймаут-сценарий (deadline-stop), gotchas 1-7 | -| sched → VDS (ядро daemon + admin API, MariaDB) | [`sched-vds-deploy-runbook.md`](sched-vds-deploy-runbook.md) | стек sched Id 28, storage-mysql + TLS к mariadb, admin API, verify, gotchas | -| tg-digest (sched cron + HTTP-воркер, паттерн sched+worker) | [`tg-digest-vds-deploy-runbook.md`](tg-digest-vds-deploy-runbook.md) | стек tg-digest (воркер internal, mem_limit 256m), runtime-регистрация задачи POST /tasks, env-контракт, smoke, gotchas (сессия Telethon, TZ UTC); первый ранбук паттерна sched+worker (модель для yt-digest) | - -## Публикация на GitHub (OSS) - -| Проект / операция | Ранбук | Что покрывает | -|---|---|---| -| sched → GitHub (dev=Gitea, pub=github.com/schedjs/sched) | [`sched-publish-runbook.md`](sched-publish-runbook.md) | топология dev/pub, синк курируемой копии, выбеливание, CI (unit+Pages), verify, rollback, gotchas (baseURL/ipx/favicon/logo.svg/yarn.lock) | - -## Инфраструктурные ранбуки - -| Операция | Ранбук | Что покрывает | -|---|---|---| -| Создание Gitea-репо (проект в Gitea) | [`gitea-project-create-runbook.md`](gitea-project-create-runbook.md) | private-репо: admin POST /user/repos → transfer к владельцу; pre-flight; verify; rollback; gotchas (SSH-порт 2222, pass-рефы) | -| Деплой каталога скилов на сервер mappa | [`skills-catalog-deploy-runbook.md`](skills-catalog-deploy-runbook.md) | синк репо-каталогов (mappa-skills/mappa-vitya-skills) → /skills на mappa.vds.kzntsv.site; deploy-catalog.mjs (dry-run → apply); стоящее правило «деплой при каждом изменении исходников» (письмо от держателя каталога); core-PATCH программный (1466 live с 0.30.0, source=admin) | -| books VDS — оптимизация памяти (OOM → ECONNREFUSED) | [`books-vds-memory-optimize-runbook.md`](books-vds-memory-optimize-runbook.md) | mongo wiredTiger 0.25 / ES heap / CI-остатки buildx; files-compose (НЕ Portainer-kzntsv); verify; rollback; Manticore-задел | -| MinIO split (books-vds + CMS-media) | [`minio-split-cutover-runbook.md`](minio-split-cutover-runbook.md) | cutover Track A/B, rollback | -| Общий Portainer-канон VDS kzntsv | [`portainer-stack-management-vds.md`](portainer-stack-management-vds.md) | JWT auth, gotchas 1-10, redeploy-рецепт, инвентарь стеков | -| Политика деплоя mappa | [`deploy-policy-mappa.md`](deploy-policy-mappa.md) | кто деплоит, канон таска+письмо | - -## Репо-вики проектов (второй источник, если не в .admin) - -- pilorama98.ru: `apps/web/.wiki/concepts/docker-deploy.md` — сборка образа, деплой. -- snolla: `concepts/` — сборка snolla-пакетов (не деплой сайтов). - -## Ведение индекса - -- Новая строка при создании ранбука (WRITE) или при изменении покрытия. -- Актуализировать при UPDATE ранбука (тег/стек/операции). -- Расхождение «ранбук есть, индекс молчит» — тоже баг ведения, чинить сразу. +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3316]] (concepts/runbooks-index). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/sched-pipelines-local-stack-runbook.md b/.wiki/concepts/sched-pipelines-local-stack-runbook.md index 50f2d70..a1a8b13 100644 --- a/.wiki/concepts/sched-pipelines-local-stack-runbook.md +++ b/.wiki/concepts/sched-pipelines-local-stack-runbook.md @@ -1,91 +1,3 @@ ---- -title: sched-pipelines-local-stack — ранбук локального стенда (sched + воркеры @apilki) -type: concept -tags: [runbook, sched, apilki, worker, deadline, cancel, local-stand] -related: [concepts/runbooks-index.md] -updated: 2026-08-26 ---- +# ⛔ Файловый канал закрыт -# sched-pipelines-local-stack — локальный стенд пайплайнов клиентов - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -Локальный стенд: sched daemon + HTTP-воркеры (yandex-market-partner-api-client, ozon-seller-api-client) -+ CDP-browser + локальный ntfy + alert-bridge. Цель — обкатка пайплайнов клиентов `@apilki` -против моков (verdaccio + Gitea) перед миграцией на VDS. - -## Локация и команды - -```bash -cd ~/projects/.admin/host-stacks/local/sched-pipelines -node render-tasks.cjs # рендер tasks.generated.json из tasks.json + .env -docker compose up -d --build # сборка + запуск -docker compose up -d --force-recreate schedd # применить НОВЫЙ tasks.json (см. gotcha 2) -docker compose logs -f schedd -``` - -- Admin API sched: `127.0.0.1:18080`, ключ `SCHED_ADMIN_KEY` (pass `sched-pipelines/local/*`). -- Триггер рана: `POST /api/tasks//run` (Bearer admin key). -- Секреты: `.env` (gitignored) ← `pass sched-pipelines/local/*`. - -## Компоненты - -| Сервис | Хост-порт | Образ | -|---|---|---| -| schedd | 127.0.0.1:18080 | `sched-pipelines/schedd:local` (daemon с verdaccio) | -| ym-client-builder | 127.0.0.1:18090 | `sched-pipelines/ym-client-builder:local` (из репо yandex) | -| ozon-seller-builder | 127.0.0.1:18091 | `sched-pipelines/ozon-seller-builder:local` (из репо ozon) | -| browser-cdp, ntfy, alert-bridge, unisender-mock | — | локальные | - -Воркеры собираются из dev-репо: `../../../../yandex-market-partner-api-client` (Dockerfile), -`../../../../ozon-seller-api-client` (service/Dockerfile). **Свежие фичи на стенд = pull репо + `docker compose build ym-client-builder ozon-seller-builder` + up -d.** - -## Таймаут-сценарий (deadline-stop, ратифицирован 2026-08-26) - -Инцидент: sched при poll-timeout помечал ран failed, но НЕ слал воркеру cancel — воркер доделывал -пайплайн с реальным publish. Вердикт расходился с реальностью. Закрыт двумя слоями: - -1. **Воркер** (yandex #1276, ozon #1278, паттерн C): `data.timeoutMs` (relative) / `data.deadlineMs` - (absolute epoch) / env `WORKER_TIMEOUT_MS` → локальный таймер ставит флаг, пайплайн стопается - на границе стадий ДО мутаций (publish/githubDistro). Вердикт: `failed`, `error: deadline exceeded`, - `result {verdict: 'timeout', timedOut: true, failedStage}`. Операторский cancel остаётся `cancelled`. -2. **sched** (#1277, core 0.53.0): при task-ceiling (`task.timeoutMs`) сам шлёт `POST /cancel` воркеру - перед записью вердикта failed (`run timeout after Nms`). - -Проверено на стенде 2026-08-26: -- воркер: `[deadline] fired ... stop before mutations` → `[cancel] stop before stage fix` → failed, verdaccio чист; -- sched: `[cancel] received/flag set runId=` в логах воркера → failed, verdaccio чист. - -## Gotchas (2026-08-26) - -1. **`task.timeoutMs` ≠ `config.timeoutMs`.** `task.timeoutMs` — task-level ceiling (run-deadline, - при срабатывании sched шлёт cancel + failed «run timeout after Nms»). `config.timeoutMs` — - транспортный таймаут ОДНОГО poll-запроса (дефолт 30s). Если транспортный < ceiling — ран упадёт - по транспортному (в ветке poll-ошибки cancel НЕ шлётся!) раньше, чем сработает ceiling. - Настройка: `task.timeoutMs: 120000` (ceiling) + `config.timeoutMs: 300000` (транспортный > ceiling). -2. **`applyTask` при upsert существующей таски НЕ обновляет `data` и `config.timeoutMs`** (переносит - только task.timeoutMs). Изменение `data` в tasks.json требует `docker compose up -d --force-recreate schedd` - (просто `up -d` не пересоздаёт контейнер, таска в БД остаётся старой). Диагностика: `GET /api/tasks` - — сверить, что новое поле реально в API. -3. **Состояние воркеров** — bind-mount `data-ym/state.json`, `data-ozon/state.json` (не в слое образа). - Перед форс-тестом полного пайплайна — сбросить state (`{"verdictHistory": []}`) с бэкапом. - После теста — восстановить из бэкапа, иначе следующий ран пойдёт по-старому. -4. **Логи ранов двух воркеров перемешиваются** в одном stdout — различать по `runId` в строках - `[deadline]/[cancel]` и по таймштампам (`docker logs -t`). -5. **schedd молчит в stdout** — диагностика ранов через admin API (`/api/runs`, error-поле), - события cancel — только в логах ВОРКЕРА (`[cancel] received/flag set`). -6. **`classify: таблица рецептов не прочитана (/app/recipes.json)`** — рецепты не смонтированы в - образ ym-client-builder, все изменения классифицируются как auto. Некритично для мок-стенда, - при миграции на VDS смонтировать. -7. **Локальный дедлайн теста**: deadlineMs/таймер проверяются на границе стадий — если стадия долгая - (generate ~15 мин), стоп случится ПОСЛЕ неё (лог `[cancel] stop before stage `), это ок. - -## Связи - -- `runbooks-index.md` — индекс ранбуков. -- Таска-источник: `.tasks/done/2026-08-20-00746-sched-pipelines-local-stack.md` (мок-кампания 2026-08-20/21). -- `publish-model` (yandex-market-partner-api-client) — модель пайплайна клиентов. -- `worker-deadline-contract.md` (yandex wiki:3254) — конвенция deadline-stop паттерна C. +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3331]] (concepts/sched-pipelines-local-stack-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/sched-publish-runbook.md b/.wiki/concepts/sched-publish-runbook.md index 9c62b80..1e4e161 100644 --- a/.wiki/concepts/sched-publish-runbook.md +++ b/.wiki/concepts/sched-publish-runbook.md @@ -1,96 +1,3 @@ ---- -title: sched — публикация на GitHub (private-dev-public-publish, CI, Pages, синк dev→pub) -type: concept -tags: [sched, runbook, publish, github, pages, private-dev-public-publish, docus, ci] -related: [concepts/runbooks-index.md, concepts/docus-github-pages-pitfalls.md] -updated: 2026-08-27 ---- +# ⛔ Файловый канал закрыт -# sched → GitHub: публикация и синк (runbook) - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -Публикация **sched** (шедулер, OSS, MIT) на GitHub по паттерну -private-dev-public-publish: разработка в приватном Gitea, наружу — курируемая -копия. Первый паблиш — 2026-08-27 (task:1149, по «го» vitya). - -## Топология - -| Роль | Папка | Origin | Мета | -|---|---|---|---| -| dev | `~/projects/sched` | `git.kzntsv.site/victor/sched` (Gitea) | `.wiki/.tasks/.agents` внутри | -| pub | `~/projects/sched-upstream` | `github.com/schedjs/schedjs` (main) | нет | - -## Артефакты - -- **Репо pub:** `schedjs/schedjs` (PUBLIC, MIT, 12 topics: cron/scheduler/job-queue/task-scheduler/background-jobs/self-hosted/nodejs/typescript/queue/devtools/mcp/docker; description+homepage `https://schedjs.github.io/sched`). -- **Сайт доков:** `https://schedjs.github.io/schedjs/` (Pages, workflow `ci.yml` → docs job; llms.txt/llms-full.txt). -- **CI:** `.github/workflows/ci.yml` — test (unit, без БД) + docs (Pages). Конфиг юнит-сьюта: `vitest.ci.config.ts`. -- **Доки:** `docs/` (Docus 5.12.3 на Nuxt), dist = `docs/dist` (трекается, Pages-сборка). -- **Storage-тесты с БД:** локально (docker: mongo 27017, pg 5433 `postgres:test`, mysql 3308 `root:test`, mariadb 3307 `root:test`). - -## Синк dev → pub (курируемая копия) - -1. **Список файлов:** `git -C ~/projects/sched ls-files` минус исключения: - `^\.wiki/|^\.tasks/|^\.agents/|^AGENTS\.md$|^CLAUDE\.md$|^branding/pora-|^pngtree-|^327274997_|^cat-clock-|^MIGRATION\.md$`. - Untracked в dev (canva/istock/logo.ai/logo.png/logo.svg/черновики) НЕ копируются. -2. **Копировать** в `~/projects/sched-upstream` (mkdir -p + cp по списку). -3. **Выбелить** (dev-приватное → публичное), файлы в pub-копии: - - `.yarnrc.yml` → `npmRegistryServer: "https://registry.npmjs.org"`, без токена; - - `docs/nuxt.config.ts` + `docs/modules/sched-links.ts` → дефолты `github.com/schedjs/schedjs`, `registry.npmjs.org`, `ghcr.io/schedjs` (env-оверрайд сохранён); - - `apps/daemon/Dockerfile` → секрет `id=npm_token` (не verdaccio_token), комменты; - - `docker-compose.dev.yml` → убрать NPM_REGISTRY/NPM_TOKEN (публичный npm); - - `CHANGELOG.md` → «→ npm» вместо verdaccio, docker-образ без registry.kzntsv; - - `scripts/publish-check.mjs` + `publish-check.test.mjs` → `ghcr.io/schedjs/sched-daemon`; - - `examples/*/generate.ps1`, README, test → убрать «verdaccio subpath export». -4. **dist:** `rm -rf docs/dist && cp -r dev/docs/dist` + скраб остатков `git.kzntsv` (в т.ч. JSON-экранированных `\u002F`). `docs/dist/logo.svg` НЕ коммитить (user: svg не едет; источник в корне dev untracked). -5. **Проверка:** `grep -rnE 'git\.kzntsv|verdaccio|vds\.kzntsv|registry\.kzntsv|VERDACCIO'` → 0; мета → 0; один чистый коммит + push main. - -## CI (workflow ci.yml) - -- **test (unit):** checkout → setup-node 24 → corepack enable → `yarn install --immutable` → `yarn workspaces foreach --all -pt --exclude docs run build` → `yarn tsc --noEmit` → `yarn vitest run --config vitest.ci.config.ts` (исключает `packages/storage-*/test/**` — им нужны живые БД). -- **docs:** на push в main: install → `yarn workspace docs build` → upload-pages-artifact (docs/dist) → deploy-pages. Pages включён (`gh api repos/schedjs/schedjs/pages -X POST -f build_type=workflow`). -- **Ручной запуск:** `workflow_dispatch`. - -## Verify (после синка/деплоя) - -- `https://schedjs.github.io/schedjs/` → meta-refresh на `/schedjs/docs/introduction` (200), CSS (`/schedjs/_nuxt/…`), favicon (`/schedjs/favicon.ico` 200), лого (full URL `https://schedjs.github.io/schedjs/logo.png` 200), llms.txt 200. -- Футер: 1 иконка GitHub (не 2). -- `gh run list` зелёный; `gh repo view` topics/description. - -## Rollback - -- **Репо:** содержимое pub = курируемая копия; откат = revert коммита pub (история чистая). -- **Pages:** перезапуск предыдущего успешного deploy-pages (Pages → последний деплой) или push реверта. -- **CI-конфиг:** фиксы в dev → синк → push. - -## Gotchas - -1. **`app.baseURL: '/schedjs/'`** обязателен (Pages-subpath), но Docus игнорирует его для двух ссылок: index meta-refresh (`url=/docs/introduction`) и favicon (`href="/favicon.ico"`) → пост-фикс в `scripts/docs-dist.mjs` (запускается после generate; срабатывает и в CI). -2. **NuxtImg двойная база лого:** header logo `'/logo.png'` → `/_ipx/_/schedjs/logo.png` (404, _ipx не генерится статически). Фикс: полный URL в `header.logo.light/dark` (`https://schedjs.github.io/schedjs/logo.png`) — ipx пропускает внешние. -3. **Футер: 2 GitHub-иконки** — Docus сам подставляет github из git-remote + explicit `socials.github`. Убрать explicit (pub remote = github.com → 1 иконка). В dev-сборке (Gitea remote) футер покажет Gitea-ссылку — при синке dist скрабить. -4. **`docs/dist/logo.svg` регенерируется** из `docs/public/logo.svg` при каждой сборке (2026-08-27: вынесен в корень dev, untracked) — не коммитить в pub. -5. **CI: dist нет в git** (gitignored) → `build` до `tsc` (cross-package types через dist). -6. **corepack vs setup-node `cache: yarn`** — кэш дёргает системный yarn 1 → падает на packageManager. Кэш не использовать. -7. **mariadb:11 первый старт** дольше health-окна GH Actions (76s+) — healthcheck не вешать; ждать готовности node-скриптом на драйверах (pg/mysql2/mongodb) или юнит-only CI. -8. **yarn.lock** в Berry-формате без URL; может быть устаревшим (nodemailer) — `yarn install` в dev, синк. До npm-паблиша `@sched/mcp@npm:^0.4.0` резолвится в workspace (transparent) — install с npmjs работает. -9. **Процесс-раннер в тестах:** спавнит с чистым env (без PATH) → `['node', …]` падает ENOENT на Linux CI → в real-spawn тестах `process.execPath`. - -## Осталось (на 2026-08-27) - -- **Чистый старт репо 2026-08-27:** `schedjs/sched` удалён → создан `schedjs/schedjs` с ОДНИМ root-коммитом (нет истории доко-сборки). MIGRATION.md исключён из паблик-сета (история миграций — dev-only). Все URL/бейджи/baseURL переведены на `schedjs.github.io/schedjs/` + `github.com/schedjs/schedjs`. Синк: `git checkout --orphan` + копия + выбеливание + один коммит + push. - -- ~~npm-паблиш~~ **СДЕЛАНО 2026-08-27: скоуп `@schedjs/*`, НЕ `@sched`** (имя `sched` занято на npmjs юзером-профилем → оргу `@sched` создать нельзя; `@schedjs` свободен и совпадает с GitHub-org). Опубликованы все 9 пакетов: `@schedjs/{core,admin-api,cli,mcp,storage-mongo,storage-mysql,storage-postgres,ui,daemon}`. В dev-манифестах/доках/README тоже переименовано `@sched/*` → `@schedjs/*` (коммит 137f78b). npm-токен — `npm/admin-npm-token` (pass), owner org schedjs, bypass 2FA; старый apilki-токен удалён из pass. -- docker-образ daemon: НЕ паблишен (publish-check ругается на `registry.kzntsv.site/sched-daemon`); цель — `ghcr.io/schedjs/sched-daemon` (как в выбеленных publish-check/Dockerfile). -- task:720 `pkg-repo-metadata` — **сделано** (repository/homepage/bugs/author добавлены во все 9 манифестов в волне переименования); -- Context7-сабмит (Pages живы); README npm-бейдж после паблиша — README во всех пакетах уже с бейджами; -- schedjs.com — фаза SaaS (CNAME → Pages), обновить llms.domain и header.logo URL. - -## Связи - -- `concepts/runbooks-index.md` — индекс (строка sched). -- Shared wiki (mappa): Docus-подводные камни — `docus-github-pages-pitfalls` (тема создана из этого опыта). -- Скил `private-dev-public-publish` (глобальный) — общая топология двух репо. +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3332]] (concepts/sched-publish-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/sched-vds-deploy-runbook.md b/.wiki/concepts/sched-vds-deploy-runbook.md index f4ec562..1b1f417 100644 --- a/.wiki/concepts/sched-vds-deploy-runbook.md +++ b/.wiki/concepts/sched-vds-deploy-runbook.md @@ -1,133 +1,3 @@ ---- -title: "sched — VDS deploy runbook (ядро daemon + admin API, MariaDB, стек sched)" -type: concept -tags: [sched, vds, deploy, docker, portainer, traefik, mariadb, runbook, scheduler] -related: [concepts/runbooks-index.md, concepts/portainer-stack-management-vds.md, concepts/sched-publish-runbook.md] -sources: [] -updated: 2026-08-27 ---- +# ⛔ Файловый канал закрыт -# sched → VDS deploy runbook (ядро daemon) - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. - -Развёртывание **ядра sched** (шедулер: daemon + in-process admin API) на инфра-VDS -(`89.253.255.94`), storage-адаптер **MariaDB** (общая MariaDB-стек 11, отдельная база `sched`). -Первый деплой — 2026-08-27 (task:790). Вводные с 2026-08-27: org/scope **@schedjs** -(github.com/schedjs/schedjs), self-host на MariaDB (не sqlite). - -## Параметры - -| Что | Значение | -|---|---| -| Хост | VDS kzntsv `89.253.255.94` | -| Стек Portainer | **Id 28**, name `sched`, endpointId **1** | -| Образ | `registry.kzntsv.site/sched-custom:0.12.1-mysql` (custom single-binary) -| Compose source-of-truth | `admin/host-stacks/vds-kzntsv/sched.compose.yml` | -| tasks.json source-of-truth | `admin/host-stacks/vds-kzntsv/sched.tasks.json` → VDS `/opt/stacks/sched/tasks.json` | -| Даемон | published `@schedjs/daemon@0.12.1` + `@schedjs/storage-mysql@0.4.0` + `@schedjs/admin-api@0.3.0` + `@schedjs/mcp@0.4.1` + `@schedjs/ui@0.3.0` | -| БД | MariaDB `mariadb:3306` (сеть `shared-dbs`), база `sched`, роль `sched@%`. Креды: `pass sched/mysql` | -| Admin API | `https://sched.vds.kzntsv.site` (traefik websecure, letsEncrypt). `SCHED_ADMIN_KEY` = `pass sched/admin-key` | -| mem_limit | `512m` (канон app-стеков) | -| Сети | `shared-dbs` + `proxy` (обе external) | - -## Артефакты - -- **Код/пакеты:** публикуются на npm `@schedjs/*` (см. `sched-publish-runbook`). Dev-исходники — репо `sched` (Gitea). -- **Dockerfile.** Стандартный `apps/daemon/Dockerfile` собирает из `@schedjs/daemon`. Для MariaDB — **композиция** адаптера. В репо добавлен `apps/daemon/Dockerfile.mysql` (public npm, без токена): - ```dockerfile - FROM node:24-alpine - WORKDIR /app; ENV NODE_ENV=production - RUN echo '{"name":"schedd","private":true,"packageManager":"yarn@4.18.0"}' > package.json \ - && corepack enable && printf 'nodeLinker: node-modules\nnpmMinimalAgeGate: 0\n' > .yarnrc.yml \ - && CI=1 yarn add @schedjs/daemon@0.12.1 @schedjs/storage-mysql@0.4.0 \ - && rm -f .yarnrc.yml && yarn cache clean - ENV PATH="/app/node_modules/.bin:${PATH}"; EXPOSE 8080; VOLUME ["/data"]; ENTRYPOINT ["schedd"] - ``` - Сборка: `docker build -f apps/daemon/Dockerfile.mysql -t registry.kzntsv.site/sched-daemon:0.12.1-mysql apps/daemon`. - Единственный слой, который важно композить — `@schedjs/storage-mysql` (иначе `--storage mysql` падает `ERR_MODULE_NOT_FOUND`). - -## Первичный деплой (сделано 2026-08-27) - -**0. База (однократно).** -```bash -# на VDS, root внутри mariadb контейнера -docker exec mariadb mariadb -uroot -p"$ROOT_PW" -e " - CREATE DATABASE IF NOT EXISTS sched CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - CREATE USER IF NOT EXISTS 'sched'@'%' IDENTIFIED BY ''; - GRANT ALL PRIVILEGES ON sched.* TO 'sched'@'%'; FLUSH PRIVILEGES;" -``` -Креды → `pass insert -m sched/mysql` (user/pass/db/host). - -**1. Образ (custom single-binary).** `apps/daemon/Dockerfile.custom` собирает один образ с опубликованными `@schedjs/{daemon,storage-mysql,admin-api,mcp,ui}` + `custom-entry.mjs` — один HTTP-сервер на `:8080`: - - `/api/*` → админ API (health open, остальное Bearer `SCHED_ADMIN_KEY`) - - `/mcp` → Streamable HTTP MCP (projection of admin API, `SCHED_MCP_READONLY=1` read-only) - - `/` → UI morda (web-components) - - `/sched-ui.bundle.js` → статический UI-бандл - Сборка: `docker build -f apps/daemon/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql apps/daemon`. - → ЕДИНЫЙ бинарь: даемон + UI-морда + MCP не плодит отдельные контейнеры (обратная связь vitya). - `createDaemon` даёт `engine`+`storage` (admin server НЕ bind — маршрутизация своя). MCP/морда импортируются по **file-URL** (subpath-импорты заблокированы npm `exports`-маской). - -**2. tasks.json.** Ядро стартует с `{"tasks":[]}` (воркеры — отдельными тасками). Закинуть на VDS: -```bash -scp sched.tasks.json vds.kzntsv.site:/tmp/ ; ssh vds.kzntsv.site 'sudo mkdir -p /opt/stacks/sched && sudo cp /tmp/sched.tasks.json /opt/stacks/sched/tasks.json && sudo chmod 644 /opt/stacks/sched/tasks.json' -``` - -**3. Стек Portainer.** compose в `host-stacks/vds-kzntsv/sched.compose.yml`, env через Portainer API (`SCHED_DB_PASS`, `SCHED_ADMIN_KEY`). Create `standalone/string?endpointId=1`. - -**Ключевой момент — MYSQL_URL + TLS.** MariaDB-стек 11 запущен с `--require-secure-transport=ON` и self-signed TLS. Подключение через `mariadb:3306` (внутренняя сеть `shared-dbs`) **без TLS НЕ проходит**: `fatal: Connections using insecure transport are prohibited`. Фикс — ssl в connstring: -``` -MYSQL_URL=mysql://sched:${SCHED_DB_PASS}@mariadb:3306/sched?ssl={"rejectUnauthorized":false} -``` -`mysql2` парсит `ssl` из query-string (`ConnectionConfig.parseUrl`), self-signed → `rejectUnauthorized:false`. - -## Реденплой (обновление образа) - -```bash -# 1. build + push нового тега (см. артефакты) -# 2. на VDS docker pull ДО restart (иначе застрянет на старом digest) -ssh vds.kzntsv.site 'docker pull registry.kzntsv.site/sched-daemon:' -# 3. source-of-truth compose: сменить image: тег -# 4. Portainer PUT /api/stacks/28?endpointId=1 {stackFileContent, env:[...], prune:false, pullImage:true} -``` -⚠️ PUT сбрасывает env-массив, если не передать `env` (gotcha 4b portainer-канон). -⚠️ Синхронизировать source-of-truth compose после PUT (иначе следующий redeploy из гита откатит тег). - -## Verify - -```bash -# контейнер -docker ps -a --filter name=schedd --format "{{.Status}}" # Up ... (healthy) -docker logs --tail 5 schedd # "daemon started (tasks=..., storage=mysql db=sched, ...)" -# health (публичный, открыт) -curl -ksS https://sched.vds.kzntsv.site/api/health # {"ok":true,"uptimeMs":...,"version":"0.12.1"} -# авторизованный доступ (SCHED_ADMIN_KEY = Bearer) -curl -ksS -H "Authorization: Bearer $AK" https://sched.vds.kzntsv.site/api/tasks # {"tasks":[]} -curl -ksS https://sched.vds.kzntsv.site/api/tasks # 401 (без Bearer) -# таблицы в MariaDB -docker exec mariadb mariadb -uroot -p"$ROOT_PW" -e "USE sched; SHOW TABLES; SELECT version FROM sched_schema_version;" -# → scheduled_tasks, schedules, task_runs, sched_schema_version; version = 6 -``` - -## Rollback - -PUT стека 28 с предыдущим тегом образа (или `{"tasks":[]}` пустой cron). Старые теги в registry. - -## Gotchas - -1. **TLS к MariaDB обязателен** — `--require-secure-transport=ON`, без `?ssl={"rejectUnauthorized":false}` → daemon в restart-loop с `fatal: insecure transport prohibited`. -2. **basicAuth — оставлен ВЫКЛ** (первый деплой). У daemon самодостаточный `SCHED_ADMIN_KEY` (Bearer) — второй слой. `/api/health` открыт (для мониторинга), всё остальное 401 без Bearer. Если нужен front-layer basicAuth — добавить middleware в traefik labels (сгенерировать bcrypt-хэш `htpasswd -nbB`), НО хэш передавать аккуратно (traefik может не принять `$2y$` при кривой передаче). -3. **`--admin-host 0.0.0.0`** — daemon слушает на всех интерфейсах внутри `proxy`-сети для traefik. Наружу VDS-порт НЕ публикуется (нет `ports:`), только через traefik. -4. **Воркеры НЕ в этом ядре** — `task:1190` (yt-digest), `task:1311` (tg-digest) отдельные; добавляются как задачи в `tasks.json`. -5. **`SCHED_ADMIN_KEY`** — в `pass sched/admin-key` (не basic-auth; это разные вещи). -6. **storage-mysql сам применяет миграции** на старте (`sched_schema_version`, версия 6) — внешний инструментарий не нужен, но БД должна быть доступна с правами на CREATE/ALTER. - -## Связи - -- [[concepts/portainer-stack-management-vds.md]] — общий Portainer-канон (JWT, gotchas, mem_limit) -- [[concepts/sched-publish-runbook.md]] — паблиш @schedjs на npm/GitHub -- [[concepts/runbooks-index.md]] — индекс ранбуков -- Репо sched: `docs/content/docs/13.self-hosting.md` — композиция storage-адаптеров, security model +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3333]] (concepts/sched-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/skills-catalog-deploy-runbook.md b/.wiki/concepts/skills-catalog-deploy-runbook.md index 0e6c6db..7078a1c 100644 --- a/.wiki/concepts/skills-catalog-deploy-runbook.md +++ b/.wiki/concepts/skills-catalog-deploy-runbook.md @@ -1,142 +1,3 @@ ---- -title: "Деплой каталога скилов на сервер mappa (skills-catalog deploy)" -type: concept -tags: [runbook, mappa, skills, catalog, deploy, ops, admin] -related: [concepts/runbooks-index.md, concepts/mappa-vds-deploy-runbook.md, concepts/gitea-project-create-runbook.md] -updated: 2026-08-28 ---- +# ⛔ Файловый канал закрыт -# Деплой каталога скилов на сервер mappa - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -> ✅ **Финализирован (2026-08-28, task:1462); core-путь обновлён 2026-08-28 (деплой 0.30.0, task:1469).** -> Команды деплоя — из README каталогов (deploy-catalog.mjs, task:1461 done). **Core-immutable — ЗАКРЫТ:** -> с mappa **0.30.0** PATCH /skills kind=core работает (source=admin, task:1466 live); deploy-catalog -> (149ca36) применяет core программно. Ранбук отражает программный путь. - -## Scope - -Синк **каталога скилов** (skill-сущности mappa) на сервер mappa из репо-источников. -Направление — **репо → mappa** (один источник истины = исходники SKILL.md в git). - -- Каталоги-источники (оба private, owner victor, созданы 2026-08-28 task:1458): - - `victor/mappa-skills` → `kind=core` (техника, глобально); - - `victor/mappa-vitya-skills` → `kind=methodology` (методология, фильтр `mappa-vitya-`). -- Сервер: `mappa.vds.kzntsv.site` (стек 27, Portainer), API `/skills` (task:1375/1376). -- **Деплой НЕ автоматический**: прогон только по письму-уведомлению от владельцев - репо (см. «Стоящее правило») + ops-таска на борде `.admin`. - -## Артефакты - -- **Репо-источники (clone):** - - `https://git.kzntsv.site/victor/mappa-skills.git` - - `https://git.kzntsv.site/victor/mappa-vitya-skills.git` - - SSH-порт git.kzntsv.site — **2222** (не 22). -- **Endpoint:** `https://mappa.vds.kzntsv.site/skills` — GET (список), GET `/skills/:name`, - POST (create). PATCH core — с source=admin (1466 live); DELETE core — 403. -- **Токен:** `MAPPA_API_TOKEN` — `<из pass mappa/full-env>` (тот же, что env 3/3 стека mappa). -- **Deploy-скрипт (task:1461 done):** `scripts/deploy-catalog.mjs` в каждом каталоге-репо - (синк изменений вместо oneshot-засева seed-skills.ts, task:1448; kind по имени репо - или `--kind`). Тесты: `scripts/deploy-catalog.test.mjs` (`node --test scripts/`). - Семантика: отсутствующие → POST (create); изменённые → PATCH (update); core — PATCH - с source=admin (1466 live с 0.30.0, deploy-catalog 149ca36); DELETE core — 403. -- **Обратное направление (НЕ этот ранбук):** `scripts/sync-skills.ts` (task:1381) — - синк каталога mappa → диск харнесса (pi/CC). Разделять: здесь деплой исходников в mappa. - -## Шаги (команды из README каталогов, task:1461) - -0. **Вход:** письмо-уведомление от проекта-держателя каталога (что изменилось: - скилы/версии) → ops-таска на борде `.admin` (если ещё нет) → прогон по ранбуку. -1. **Pre-flight:** `git pull` каталога-репо (mappa-skills / mappa-vitya-skills); - ENV: `MAPPA_API_TOKEN` из `pass mappa/full-env` (+ `MAPPA_CORE_URL` опц., дефолт - https://mappa.vds.kzntsv.site); `admin_secret_scan` — 0 хитов (тела с секретом → 422). -2. **Dry-run** (из чекаута каталога): - ```bash - node scripts/deploy-catalog.mjs --dry-run # дефолт тоже dry-run - ``` - Сверить план: `create` / `update` / `up-to-date` (core-правки помечаются - `(core, source=admin)`) — нет ли неожиданного (лишний create = чужой каталог). -3. **Apply:** - ```bash - node scripts/deploy-catalog.mjs --apply - ``` - Применяет create + update (PATCH; core — с source=admin, см. п.4). -4. **core-обновление (программный путь, 1466 live с 0.30.0):** если в плане есть - `~ (core, source=admin)` — правки core применяются PATCH-ом (source=admin, - event-аудит skill.core_update). Ручной шаг НЕ нужен. DELETE core — по-прежнему 403 - (удаление — только фикс исходника / пересоздание каталога-сущности). -5. **Verify** (см. ниже). -6. **Фиксация:** письмо-отчёт проекту-держателю (verify-таблица) + закрытие ops-таски. - -> Конкретика: `mappa-skills` → kind=core (10 скилов), `mappa-vitya-skills` → -> kind=methodology (2 скила, фильтр mappa-vitya-). Скрипт определяет kind по имени -> репо (или `--kind core|methodology`). - -## Стоящее правило: деплой при каждом изменении исходников - -**Прогон деплоя каталога скилов — при каждом изменении исходников в каталогах-репо.** -Уведомление — **письмом от владельцев репо** (mappa-зона — держатель исходников). - -1. Проект, который пушит изменения в каталог-репо, при каждом замерженном изменении - исходников (новый SKILL.md / правка / удаление) шлёт письмо-уведомление в `.admin` - (`inbox_send`, from проект-владелец, type action): какие скилы затронуты, что изменилось. - Адресант НЕ фиксирован на проекте («кто пришлёт, тот и держатель»): сейчас mappa → - mappa-skills (core), skills → mappa-vitya-skills (methodology); сменится владелец - каталога — сменится адресант. -2. `.admin` по письму: ops-таска (при необходимости) → прогон деплоя по этому ранбуку - (dry-run → apply → verify) → письмо-отчёт + закрытие таски. -3. Триггер — **только письмо**. Без CI/крона/автоматики (подтверждено оператором 2026-08-28). - -## Verify / smoke - -- `GET https://mappa.vds.kzntsv.site/skills` (с `MAPPA_API_TOKEN`) — затронутые скилы - присутствуют, `version` актуальна (из frontmatter SKILL.md), `enabled:true`. -- Прогон без ошибок: created/updated без 4xx/5xx (ошибки → в письме); PATCH применён - (в т.ч. core — source=admin). -- Потребительская сторона: `sync-skills.ts` подхватит обновления на дисках харнессов - (после `/reload`, спека wiki:3300 п.11) — verify-контур, не часть деплоя. -- **База (первый прогон 2026-08-28 после деплоя 0.30.0):** mappa-skills — 8 up-to-date + - 2 updated (mappa-bootstrap, mappa-brainstorm-promote — через core-PATCH source=admin); - mappa-vitya-skills — 2 up-to-date. - -## Rollback - -- **Откат содержимого (methodology):** revert коммита в репо-источнике → повторный - `--apply` (PATCH вернёт прежнее тело). -- **Откат (core):** revert коммита в репо-источнике → повторный `--apply` — PATCH с - source=admin вернёт прежнее тело (программный путь). -- **Удаление ошибочного скила:** methodology — DELETE /skills; core — DELETE 403 (только - фикс исходника / пересоздание каталога-сущности). - -## Gotchas - -1. **`kind=core` PATCH — закрыт (1466 live с 0.30.0)** — PATCH /skills kind=core с source=admin - работает (event-аудит skill.core_update; deploy-catalog шлёт source=admin, 149ca36). - Без source=admin — 403 (иммутабельность core для обычных агентов сохраняется). DELETE core — - 403 всегда. (2026-08-28) -2. **422 secret-scan** — тело SKILL.md с секретом (token/password/… ≥6 симв.) → - `POST /skills` → 422. Перед деплоем прогон `mcp__mappa__admin_secret_scan` (0 hits). -3. **Направление не путать** — репо → mappa (этот ранбук, deploy) vs mappa → диск - (`sync-skills.ts`). Перепутать = «обновление с диска» не имеет смысла: каталог — - источник, диск — потребитель. (2026-08-28) -4. **Триггер письмом, не таской** — таска на борде .admin не пингует живую сессию; - уведомление = письмо (канон mappa-messaging). Без письма деплой не стартует. (2026-08-28) - -## Решения оператора (2026-08-28, inbox:2620) - -1. **Стоящее правило** — подтверждено как сформулировано: деплой каталога при каждом - изменении исходников; уведомление письмом → ops-таска → dry-run → apply → verify; - триггер только письмом, без CI/автоматики. -2. **Адресант уведомлений** — НЕ фиксировать на проекте: «кто пришлёт, тот и держатель». - Проект, пушащий изменения в каталог-репо, шлёт письмо в .admin. Сейчас: mappa (core), - skills (methodology); сменится владелец каталога — сменится адресант. -3. **Обновление core (immutable)** — ДОДЕЛАТЬ: core должны обновляться деплоем по - определённому пути. **ВЫПОЛНЕНО (2026-08-28, деплой 0.30.0, task:1469):** доработка - /skills — task:1466 → PATCH core с source=admin; deploy-catalog применяет core программно. - -**Статус (2026-08-28):** core-путь программный (1466 live); ранбук актуален; первый прогон -после деплоя: 2 core-скила обновлены через core-PATCH (mappa-bootstrap, mappa-brainstorm-promote). +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3315]] (concepts/skills-catalog-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/stostayer-web-deploy-runbook.md b/.wiki/concepts/stostayer-web-deploy-runbook.md index a01fd4b..25393cf 100644 --- a/.wiki/concepts/stostayer-web-deploy-runbook.md +++ b/.wiki/concepts/stostayer-web-deploy-runbook.md @@ -1,134 +1,3 @@ ---- -title: stostayer-web — деплой легаси web на прод клиента (runbook) -status: live -tags: [stostayer, docker, portainer, deployment, nuxt2, esm, ops, rollback] -related: [[portainer-stack-management-vds]] -updated: 2026-06-17 ---- +# ⛔ Файловый канал закрыт -# stostayer-web — деплой легаси web на прод клиента - -Прод `https://www.stostayer.ru` = легаси `packages/web` (Nuxt 2, CJS, SSR) из монорепо `victor/stostayer.new`. web4 (Nuxt 4) ещё НЕ переключён. Хостится контейнером, управляется Portainer-стеком у клиента. - -> **Машина клиента, настраивает их админ — мы только деплоим свой web-образ, конфиги хоста не трогаем.** Деталь доступов/инфры — в `stostayer.new/.wiki/concepts/client-infra-access.md`. Все креды — `pass stostayer/client`. - -## Параметры - -| Что | Значение | -|---|---| -| Хост | `91.222.236.225` (SSH `:20435`, юзер `victor` + sudo-с-паролем; парольный SSH) | -| Reverse-proxy | **Angie** (форк nginx) на `:443`, навешивает **Basic-auth** на сервисные сабдомены | -| Registry | `docker.stostayer.ru` (BA те же, что Angie BA) — образы `stostayer-web:` | -| Стек | Portainer **`stostayer-web` Id 16, EndpointId 3**, compose в Portainer-volume `/data/compose/16/docker-compose.yml` | -| Прод-тег (2026-08-13) | `0.3.23`. На хосте лежат старые теги под откат (`0.3.22` и ранее) | -| Образ | `packages/web/Dockerfile`: `node:16` → `yarn install` → `yarn build` (nuxt build) → `CMD yarn start`. pm2/`ecosystem.config.js` в проде НЕ используется | - -## Канал деплоя (build здесь → registry → Portainer) - -### 1. Build образа (локально, offline) - -Тег образа независим от `packages/web/package.json` version — это чисто image-тег, инкремент от прода (`0.3.18` → `0.3.19`). Кода/версий не бампать. - -```bash -# в чекауте нужного коммита, .yarn/cache populated -docker build -f packages/web/Dockerfile -t docker.stostayer.ru/stostayer-web: . # контекст = корень репо -``` - -Гочи build: -- **`.dockerignore` обязателен** (его нет в репо) — иначе `node_modules`/`.git` улетают в build-контекст. Создать временный: `node_modules **/node_modules .git apps docs build *.zip` и т.п. (на явные `COPY` не влияет). -- **Локальная `npmAuthToken: "${VERDACCIO_TOKEN}"` в `.yarnrc.yml`** (рабочая модификация, не в коммите) **ломает build**: yarn внутри образа падает `Usage Error: Environment variable not found (VERDACCIO_TOKEN)`. Убрать эту строку из build-копии (потом вернуть). -- **Offline против `.yarn/cache`:** добавить `enableNetwork: false` в `.yarnrc.yml` — depы пекутся из cache, verdaccio-токен в образе не нужен. (Cache содержит third-party зипы; их версии ESM-миграция не меняла, так что cache от master-tip обычно покрывает и более старые коммиты.) -- **✅ Offline-кэш ПОЧИНЕН (2026-08-13, коммит `5c583f8`):** в `.yarnrc.yml` добавлен `supportedArchitectures: {cpu:[x64], os:[linux,win32,darwin], libc:[glibc,musl]}` — раньше без `libc` yarn пропускал fetch `@rollup/rollup-linux-x64-gnu` (условие libc=glibc). `.yarn/cache` gitignored → один раз на build-машине прогнать сетевой `yarn install` (с `VERDACCIO_TOKEN`) — yarn дольёт `@esbuild/linux-x64`/`@rollup/rollup-linux-x64-gnu` в локальный кэш, дальше offline (`enableNetwork:false`) работает. Сделано 2026-08-13 на этой машине (зипы в кэше). - -### 2. Push в registry - -```bash -echo '' | docker login docker.stostayer.ru -u Victor --password-stdin -docker push docker.stostayer.ru/stostayer-web: -``` - -> ⚠️ **ОДИН push, без retry-циклов.** Инцидент 2026-06-17: retry-шторм (6 попыток, образ 3.29GB) на `docker.stostayer.ru` засветил egress нашего **VPN** → хостинг-провайдер забанил VPN-IP → ВЕСЬ HTTPS дом→хост (`:443`: сайт + registry + portainer) стал TLS-fail (`schannel: failed to receive handshake`), выглядело как «сайт лёг» (а TCP 443/SSH 20435 — открыты). Лечится сменой VPN. **На client-инфру: одна попытка, сбой → стоп и к человеку, не долбить.** (см. [[verify-on-real-client-not-own-curl-tests]]) -> -> ⚠️ **Инцидент 2026-07-21: единичный large-push ТОЖЕ триггерит бан** (не только retry-storm). Первый push `0.3.20` (3.33GB) с VPN → тот же VPN-IP бан → `:443` к хосту TLS-fail с операторского IP (сайт для остальных посетителей работал — Angie access-log это подтвердил; SSH:20435 оставался открыт). **Бан НЕ IP-специфичный для всех — для остальных сайт жив, режется только egress-IP источника push'а.** Митигация: (a) **resume-push после смены egress** — `docker push` resumable, уже залитые слои «Layer already exists» (без re-upload), добивается только остаток + manifest → egress мал → повторный бан не триггерится; (b) **build-on-host + push в localhost-registry** (registry-контейнер на хосте, `127.0.0.1` — вообще без внешнего egress) — самый чистый путь для крупных образов. **Не пытаться долбить упавший push повторно с того же egress.** - -### 3. Передеплой стека — с ХОСТА, мимо Angie BA - -Portainer published **только в docker-сеть** (на host-localhost его НЕТ — там MinIO на `:9000`). Прямой Portainer-API из дома ломает Angie BA (Bearer затирает Basic → 401; auth-cookie не ставится). Решение — бить API по **IP контейнера portainer с самого хоста**: - -```bash -# SSH (PuTTY plink; host-key уже известен) -plink -ssh -batch -hostkey SHA256:6Zk14J/UakVqBYm/fMPFrL3gosdBdyZOKZnbA0GVnD0 \ - -P 20435 -pw '' victor@91.222.236.225 "bash -s" <<'EOF' -base=http://172.18.0.2:9000 # docker inspect portainer → IP в bridge; ПОРТ :9000 обязателен (голый IP → curl на :80 → HTTP=000) -JWT=$(curl -s $base/api/auth -H 'Content-Type: application/json' \ - -d '{"username":"victor","password":""}' | jq -r .jwt) -FILE=$(curl -s -H "Authorization: Bearer $JWT" $base/api/stacks/16/file | jq -r .StackFileContent) -NEW=$(printf '%s' "$FILE" | sed 's#stostayer-web:0.3.18#stostayer-web:#') -PAYLOAD=$(jq -n --arg c "$NEW" '{stackFileContent:$c, env:[], prune:false, pullImage:true}') -curl -s -w "\nHTTP=%{http_code}\n" -X PUT -H "Authorization: Bearer $JWT" \ - -H "Content-Type: application/json" -d "$PAYLOAD" "$base/api/stacks/16?endpointId=3" -EOF -``` - -- `docker` на хосте — через `sudo` (victor не в группе docker): `echo '' | sudo -S `. Sudo-пароль = SSH-пароль victor. -- `jq` на хосте есть. Скрипт через `bash -s`/stdin — чисто с кавычками; UTF-8 round-trip compose не портит (в отличие от PowerShell Invoke-RestMethod, [[portainer-stack-management-vds]] gotcha #9). -- `pullImage:true` → Portainer сам тянет новый тег из `docker.stostayer.ru` (хост-docker уже авторизован в registry). - -### 4. Verify (read-only, без нагрузки на публичный сайт) - -```bash -docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' | grep stostayer-web # образ = новый тег, Up -docker logs --tail 20 stostayer-web # чистый старт Nuxt, без ошибок/краш-лупа -``` - -**Verify контента (напр. цен) — с хоста, прямым curl в nuxt-контейнер** (Angie даёт 403 на no-UA запрос с самого хоста; обход — `localhost:3000`, host-network): -```bash -# на хосте (sudo docker + host-network контейнер на :3000) -curl -s -H "Host: www.stostayer.ru" -A "Mozilla/5.0" http://localhost:3000/ -o /tmp/v.html -grep -c "" /tmp/v.html -``` -**Гоча verify-URL:** bare `/remont-kondicionerov` 301-редиректит на `/remont/remont-kondicionerov` (DB-driven `oldPageRedirect`/unit-slug mapping) — это CMS-контент-страница БЕЗ nuxt-компонента. Компоненты `pages//*.vue` реально live на своих nuxt-маршрутах (напр. `/remont-kondicionerov/{zapravka,diagnostika}-kondicionera`) — верифицируй по ним, не по bare-URL. - -**Прод-дрейф mount-конфига (2026-08-16, инцидент content-api):** хост монтирует -`/var/docker/stostayer-web/config` → `/usr/src/app/packages/web/config` — это **источник истины -для прод-конфига**. node-config читает из cwd (`packages/web/config`), т.е. репо-правки -`packages/*/config/default.json` (напр. c43d168: `s3.region local → us-west-1`) на прод **не -влияют** — образ подхватывает mount. После деплоя, меняющего конфиг: синхронизировать -хост-конфиг (или осознанно задокументировать mount как truth). Нюанс node18: `localhost` → -`::1` (IPv6-first) — в образе `NODE_OPTIONS=--dns-result-order=ipv4first` (MariaDB/MSSQL на -хосте IPv4-only). - -**Гоча локального smoke-контейнера:** `packages/web/config/default.json` содержит **прод-БД** (`www.stostayer.ru:3306` MariaDB + `:1433` MSSQL), и `www.stostayer.ru` резолвится в публичный IP хоста → smoke-контейнер без env-overrides **подключается к prod-БД клиента по интернету** (read-only SSR, без writes, но всё равно нежелательно). Для чистого локального smoke — переопределяй `data.sequelize.*.host` env'ом на localhost/заглушку, либо глуши контейнер сразу после `Server Listening`. - -### Откат - -Тот же PUT с прежним тегом (`` → `0.3.18`), `pullImage:true`. 0.3.18 на хосте есть. RTO ~30-60с (пересоздание контейнера). - -## ✅ БЛОКЕР РАЗРЕШЁН 2026-07-21 (retrofit + snolla 0.7.6) - -> **Legacy web снова собирается+запускается с master.** Решение: ретрофит `packages/web` (`require()`→dynamic `import()` в 5 местах, 2 serverMiddleware → CJS) + фикс `@snollajs/snolla@0.7.6` (bare-`get`-баг, см. ниже). Образ `0.3.20` собран, запушен, передеплоен на прод stostayer.ru, verify зелёный. Ретрофит лежит в working-tree stostayer.new (незакоммичен на 2026-07-21) — **stostayer.new должен закоммитить** его в master, иначе следующий legacy-web деплой упрётся в ту же стену. Секция ниже оставлена как история. - -## ⛔ БЛОКЕР (история, 2026-06-17): легаси web НЕ пересобирается с текущего master (ESM-стена) - -> На 2026-06-17 фикс формы жалоб (`120bc07`, задача `stostayer-web-complaint-form-deploy`) **выкатить не удалось** — два ESM-барьера: - -1. **Build-time:** с master-tip `yarn build` (nuxt) падает `SyntaxError: await is only valid in async functions...` — `@stostayer/data` после таски `esm-data-dual` грузит модели через **top-level await**; цепочка `nuxt.config.js → @stostayer/api → @stostayer/data`, Nuxt2 читает конфиг через jiti → top-level await недопустим. Корень — коммит `c805e7e` (ESM-миграция data/api). -2. **Runtime:** cherry-pick `120bc07` на `d02f740` (родитель c805e7e, «последний CJS-собираемый master») **собирается**, но контейнер **краш-лупит**: `ERR_REQUIRE_ESM` — `packages/web/server/index.js:6` `require('@snollajs/snolla')`, а версия `@snollajs/snolla` из lockfile d02f740 уже **ESM**. Т.е. d02f740 не runtime-чистая. - -Прод `0.3.18` жив только потому, что собран на ещё более старом стейте (CJS `@snollajs/snolla`, CJS data/api). - -**Вердикт (stostayer.new подтвердил 2026-06-17):** легаси-web `require()`-ит **5** ESM-ставших пакетов — `@snollajs/snolla` (0.7.4, `server/index.js:6`), `@stostayer/api` (`nuxt.config.js:3`), `@snollajs/content-api` (0.8.0, `nuxt.config.js:4`), `@stostayer/data` (`serverMiddleware/oldPagesRedirections.js:14` + `redirections.js:14`). Чтобы найти base где ВСЕ пять ещё CJS — надо к ~0.3.18 и потерять всё с тех пор. **Чейз базы бесполезен. `packages/web` (node16/CJS/Nuxt2) не пересобираем ни с какого свежего дерева — весь dep-граф pure-ESM. `0.3.18` заморожен (последний собираемый артефакт).** - -**Путь:** фикс формы (`120bc07`) едет вместе с **переездом формы в web4** (Opt 3). Ретрофит `require`→dynamic `import()` в 5 местах технически возможен на node16, но это часы на стек, который удаляется web4 — не рекомендуется. До web4-cutover — **остаёмся на 0.3.18**, admin-сессия код не правит ([[verify-on-real-client-not-own-curl-tests]]). - -> Образ `0.3.19` (краш-лупный) лежит в `docker.stostayer.ru` — **НЕ деплоить**. Оставлен в registry по решению vitya (2026-06-17), не удаляем. - -## ✅ Деплой 0.3.23 (2026-08-13) - -Запрос их прогера (письмо `.agents/inbox/2026-08-13T08-43-41Z-stostayer-new.md`): calculator-баннер про цены при покупке запчастей в компании Стайер (exactPrices>0) + фикс краша (snolla `loadCity`/`loadSite` в asyncData) + стек-фикс легаси (явные vue/vuex/vue-router/mariadb deps, `nmHoistingLimits: workspaces`). Коммиты `e5f7bd6`+`7e51cbf`+`d49b4bb` (master-tip). Собран (см. гоча offline выше), запушен **1 попыткой** (слои mostly already-exists → egress мал, бана нет), стек 16 передеплоен 0.3.22→0.3.23 (`pullImage:true`, env re-supply — у стека env пуст). Verify зелёный: Focus II → 200 + баннер, регресс `/`+ремонтная 200, Focus IV (exactPrices=0) без баннера + алерт «уточняйте у мастеров». Прод до этого был на **0.3.22** (0.3.21/0.3.22 собраны 24.07; деплой 0.3.22 шёл через pscp+`sudo docker load` без push в :443). - -## Связи - -- [[portainer-stack-management-vds]] — родственный паттерн (наш VDS), оттуда PowerShell-кодировочные гочи и redeploy-рецепт. -- Доступы/инфра клиента (детально): `stostayer.new/.wiki/concepts/client-infra-access.md`. -- Задача: `.tasks/stostayer-web-complaint-form-deploy.md` (🔵 blocked на victor/stostayer.new). +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:207]] (concepts/stostayer-web-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/tandemmebel-vds-deploy-runbook.md b/.wiki/concepts/tandemmebel-vds-deploy-runbook.md index 6ee759b..c9b64cd 100644 --- a/.wiki/concepts/tandemmebel-vds-deploy-runbook.md +++ b/.wiki/concepts/tandemmebel-vds-deploy-runbook.md @@ -1,161 +1,3 @@ ---- -title: tandemmebel.ru snolla-app — VDS deploy runbook (cutover + 0.42.1 bumps + 0.43.2 v3 cutover) -type: concept -tags: [tandemmebel, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, blog-portfolio, v3, aws-sdk] -related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[snolla-live-prod-inplace-image-bump]], [[labtools-vds-deploy-runbook]], [[labtools.pro-vds-deploy-runbook]], [[emspb-vds-deploy-runbook]], [[minio-split-cutover-runbook]] -updated: 2026-07-30 ---- +# ⛔ Файловый канал закрыт -# tandemmebel.ru → VDS deploy runbook - -Вынос `tandemmebel.ru` (snolla-приложение, `@snollajs/snolla` **0.42.1**, server-side Liquid, **блог-портфолио БЕЗ e-commerce каталога**) -с [[../entities/ruvds-iis-host]] (catch-all CMS, 80.64.31.36) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] -(89.253.255.94), за traefik. Модель — snolla-app: читает БД-контент MoreThenCms (`mssql.kzntsv.site`) -+ ассеты из MinIO (`minio.kzntsv.site`). In-process sharp (resize+data-driven watermark), delivery=serve-bytes, -variant-cache бакет в MinIO. imgproxy ИЗ ПУТИ tandem УБРАН (сырой `/imgproxy` → 404 норма). - -Паттерн = [[emspb-vds-deploy-runbook]] / [[labtools.pro-vds-deploy-runbook]]. Отличие: **блог-портфолио** (не каталожный → -order-парити секций НЕ применимо — см. 0.42.1 bump ниже). Culture `ru-RU`. - -## Артефакты -- **Код:** `victor/tandemmebel.ru` @ `0cd9351` (apps/web, snolla **0.42.1** / core 0.24.1 / liquid 0.10.2 / data 0.14.1). - `deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root uid `node`, healthcheck `/robots.txt`). -- **Образ:** `registry.kzntsv.site/tandemmebel:0cd9351` (+`:latest`). Имя **`tandemmebel`** (= имя стека/контейнера). - Собран НА VDS (обход traefik-499). digest `sha256:f29c187fe6114d6ab3926132f4b3eb09df9b606d448cd8fd873780687cb46b8f`. -- **Стек Portainer:** `tandemmebel` (**Id 20**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/tandemmebel.compose.yml`. -- **siteId:** `78080707-F6E0-4330-BA30-7922354C2CEF` (non-secret, в `production.json` — НЕ env). -- **siteUrl:** `https://www.tandemmebel.ru` (в `production.json`). -- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD` — Portainer stack env (переиспользованы verbatim из labtools стека 17). НЕ в образе, НЕ в git. -- **mem_limit:** `512m` (guardrail; см. [[portainer-stack-management-vds]] § Convention). - -## Cutover 2026-07-12 (первый вынос на VDS — DNS-gated) -Последовательность (LE-критичность соблюдена) — см. `NEXT_SESSION.md`: -1. **Verify авторит. NS** — `nslookup tandemmebel.ru ns1/ns2.reg.ru` = оба `89.253.255.94` (apex + www), не только резолвер. -2. **GET стек 20** — образ `tandemmebel:ed96b18` (0.42.0, staging cutover-подготовлен), env 8/8, rule staging `Host(tandemmebel.vds.kzntsv.site)`. -3. **PUT стек 20** — `Host(tandemmebel.ru) || Host(www.tandemmebel.ru)`, env-preserving, `prune:false, pullImage:false`. Python urllib UTF-8 (НЕ PS Invoke-RestMethod — gotcha кириллицы, см. [[portainer-stack-management-vds]]). -4. **Poll** — контейнер healthy, LE-серт issued on first hit: CN=tandemmebel.ru, SAN оба, issuer YR2, until 2026-10-10. -5. **Live-smoke С VDS** — GREEN. -6. Compose source-of-truth + борд обновлены. - -**Латентный прод-баг починен cutover'ом:** `Gotham-Pro.css` был 0B на RUVDS → теперь 200/4436B. - -## In-place bump 2026-07-12: 0.42.0 → 0.42.1 (order-tag Drop-field fix) -По рецепту [[snolla-live-prod-inplace-image-bump]] (поверх cutover'а того же дня). - -### Консюмер-бамп (решён оператором, без dev-source) -Тот же блокер-паттерн 2026-07-04 (заявленная snolla-версия ≠ консюмер-пин) — на этот раз решён самим оператором: -- `apps/web/package.json:12` — `0.42.0` → `0.42.1`. -- `yarn install` в корне монорепы → обновил корневой `yarn.lock` (snolla 0.42.1 / core 0.24.1 / liquid 0.10.2 / data 0.14.1). Dockerfile `yarn install --immutable` — lock обязан совпадать с пином. -- commit `0cd9351` + push origin (git.kzntsv.site/victor/tandemmebel), подтверждён `ls-remote`. -- Тег образа = sha монорепы `0cd9351`. - -### Build → staging → gate → swap -1. **Build на VDS** (archive `0cd9351` → `~/build/tandemmebel` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN -t .../tandemmebel:0cd9351 -t .../tandemmebel:latest . && push`). Guard: `config/default.json` ABSENT в архиве (.dockerignore), пин = 0.42.1. -2. **Throwaway-staging :5020** из env живого контейнера (`docker inspect tandemmebel` → `.staging.env`, фильтр `^(HOSTNAME|PATH|NODE_VERSION|YARN_VERSION|HOME)=`). `docker run -d --name tandemmebel-staging --env-file .staging.env -p 127.0.0.1:5020:5000 .../tandemmebel:0cd9351` → healthy. robots.txt+`/`+sitemap 200. -3. **Completeness-gate С VDS** — sitemapindex разворот (`expand()`), два чека: - - **NEW==PROD locs: 184 = 184 IDENTICAL** (вкл. 0.42.x sitemap-реструктуризацию — parity сохранился). - - **self-consistency + content-not-lost** (locs идентичны → один проход): 183×2xx + 1×404. - - Единственный 404 `/articles` — **идентичен прод-оракулу** (404 и на live `https://www.tandemmebel.ru/articles`) → benign (пустая секция-без-индекса, не регрессия). Tandemmebel НЕ каталожный → order-парити **не применимо** (0.42.1 order-fix инертен на этой теме — как archive-роуты на 0.40→0.42). - - **GREEN = 0 регрессий в 404/5xx.** -4. **Боевой swap** — operator-gated, node `put-stack.js` (env-preserving: 8/8 name+value, `prune:false, pullImage:true`): - `META=st20.json node put-stack.js 20 1 ed96b18 0cd9351` → PUT 200. Контейнер 0cd9351+healthy за ~8s. -5. **Live-smoke** — `https://www.tandemmebel.ru/{robots.txt,/,/projects,/sitemap.xml}` = 200. TLS-серт CN=tandemmebel.ru **не дёрнут** (in-place swap серт не трогает). `docker rm -f tandemmebel-staging`. - -### Rollback -- Тег `ed96b18` (0.42.0) в registry (+ `b02ca18`, 0.16.2-staging) → PUT стека 20 назад. -- ИЛИ revert DNS → 80.64.31.36 (RUVDS IIS жив, не тронут — rollback всех мигрированных). - -## In-place bump 2026-07-13: 0.42.1 → 0.42.1 (sha 0cd9351 → 8df10ee, template-only) - -Ops-handoff от tandemmebel-сессии (inbox-запрос): убрать FB/Twitter/Google+ share-кнопки -(экстремистистская символика РФ), оставить ВК+Одноклассники. Коммит `8df10ee` в `victor/tandemmebel.ru` -master, меняет только `apps/web/views/social_buttons.liquid` (1 file, 12 deletions), snolla pin 0.42.1 -НЕ менялся → in-place bump на той же 0.42.1 (не консюмер-бамп). - -### Предсборочная верификация (поймала расхождение) -Записка от tandemmebel-сессии утверждала: «сейчас живой на стеке 20 — `ed96b18`/0.42.0». Фактически -на проде крутился **`0cd9351`/0.42.1** (in-place bump 0.42.0→0.42.1 был 2026-07-12). Без проверки это -не повлияло бы (см. ниже), но вслепую строить нельзя. Проверки С VDS + gitea API (токен `gitea/admin-token`): -- `docker inspect tandemmebel` → `registry.kzntsv.site/tandemmebel:0cd9351`, healthy, started 2026-07-12. -- gitea `compare/0cd9351...8df10ee` → `total_commits:1`: фикс ровно один коммит поверх 0.42.1 (правильная база). -- `git show 8df10ee:apps/web/package.json` → `@snollajs/snolla: 0.42.1` (pin не менялся); yarn.lock snolla 0.42.1 / - core 0.24.1 / liquid 0.10.2 / data 0.14.1 — идентично live 0cd9351. -- `git show 8df10ee --stat` → только `apps/web/views/social_buttons.liquid` (modified). -- 8df10ee full sha `8df10eedcba9979ac91dce2025b2ff827d60e808`, master HEAD = 8df10ee. - -Если бы 8df10ee был на базе 0.42.0 (172 locs) → completeness-gate поймал бы регрессию 184→172. Он на 0.42.1 → -parity 184=184. - -### Build → staging → gate → swap -1. **Build на VDS** из чистого архива `8df10ee` (gitea API `/archive/.tar.gz`, token- auth `oauth2:`- - НЕ работает, `Authorization: token`-header на API endpoints работает; git clone по HTTPS фейлится даже - с oauth2:token — gitea admin-token = API-only, не git-transport). Extract `--strip-components=1` в - `~/build/tandemmebel-8df10ee`. `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN= - -t registry.kzntsv.site/tandemmebel:8df10ee -t .../tandemmebel:latest .` → digest - `sha256:30a7e5ab82f2bf371042f1b5fa0cd7ac5ceda71379d1da59e148dee8166bd560`, layer-cache hit (package.json+ - yarn.lock идентичны 0cd9351), push OK. -2. **Throwaway-staging :5020** из env живого контейнера (`docker inspect tandemmebel` → `.staging.env`, - фильтр `^(HOSTNAME|PATH|NODE_VERSION|YARN_VERSION|HOME|PORT|NODE_ENV)=`). `docker run -d --name - tandemmebel-staging --env-file .staging.env -p 127.0.0.1:5020:5000 .../tandemmebel:8df10ee` → healthy 8s. -3. **Completeness-gate С VDS** — sitemapindex разворот. **Важно:** staging sitemapindex содержит АБСОЛЮТНЫЕ - prod-URLs (siteUrl=prod в production.json) → sub-sitemaps надо фетчить со staging ПО ПУТЯМ - (`http://127.0.0.1:5020/sitemap-pages-1.xml`), а не следовать абсолютным URL (иначе сравнишь PROD==PROD). - - NEW==PROD locs: **184 = 184 IDENTICAL** (0 prod-only, 0 new-only). - - self-consistency: 183×200 + 1×404 (`/articles` benign parity staging==prod). - - share-block: staging vk+ok / fb-tw-gp=0 vs prod-before (0cd915 live ещё имел) vk+ok+fb+tw+gp — - фикс убирает ровно лишнее, остальное не трогает. - - GREEN = 0 регрессий. -4. **Боевой swap** — Portainer JWT auth (`POST /api/auth` vitya/Pryakhin9-VDS-2026; API-key `ptr_*` даёт 401, - workaround = JWT). GET `/api/stacks/20` (env 8/8) + `/api/stacks/20/file` (compose). Python urllib UTF-8 - (НЕ PS Invoke-RestMethod — gotcha кириллицы #9). Замена `tandemmebel:0cd9351`→`8df10ee` в StackFileContent - (ровно 1 замена, traefik rule line не тронута). PUT `/api/stacks/20?endpointId=1` `{stackFileContent, - env: <8 preserved>, prune:false, pullImage:true}` → HTTP 200. Контейнер 8df10ee+healthy ~8s. -5. **Live-smoke С VDS** — robots/`/`/sitemap 200, /articles 404 parity, sitemap 184 locs, 4 share-block - страницы (project-post ×2 `/projects/2011/...`, `/furniture/bedrooms`, `/furniture/kitchens/classic`) все - 200 → **vk+ok на месте, fb/tw/gp=0**. TLS-серт CN=tandemmebel.ru (LE YR2, until 2026-10-10) **не дёрнут**. - `docker rm -f tandemmebel-staging`. - -### Rollback (8df10ee) -- **Образный (предпочт):** PUT стека 20 назад на `0cd9351` (0.42.1, {% order %} fix) — template-only фикс, - откатится чисто. Тег в registry. -- `ed96b18` (0.42.0) / `b02ca18` — в registry (ed96b18 стёрт с VDS при disk-cleanup 2026-07-13, `pullImage:true` - дотянет из registry). -- DNS: revert reg.ru → 80.64.31.36 (RUVDS IIS жив, не тронут). - -### Hygiene-заметка -Portainer stack 20 file несёт устаревшие STAGING-комменты (строки 16-17 «Домен: STAGING», 53 -«STAGING-RULE. Cutover: после flip DNS сменить на Host(...)») — но сама `traefik...rule` строка (55) уже LIVE -(`Host(tandemmebel.ru) || Host(www.tandemmebel.ru)`, совпадает с label'ом живого контейнера). Косметика, -функционально нейтральна. При деплое 8df10ee обновлён только image-line коммент; шапочные STAGING-комменты -не трогались (минимальное изменение). Source-of-truth compose `host-stacks/vds-kzntsv/tandemmebel.compose.yml` -актуализирован полностью (8df10ee + LIVE-комменты + история bump'ов). При следующем full-sync можно выровнять -Portainer file под git-source. - -## Гочи (специфичные) -- **Блокер-паттерн консюмер-пина** — snolla-репо релизит версию, но консюмер-пин в tandemmebel-репо отстаёт. На 0.42.0 (2026-07-04) ждали dev-source; на 0.42.1 (2026-07-12) оператор сделал сам. Всегда byte-verify пин+yarn.lock+`ls-remote` перед build. -- **`yarn install --immutable`** в Dockerfile — корневой `yarn.lock` обязан быть снапшотом целевого пина, иначе build падает на checksum-mismatch. Бамп = package.json + `yarn install` (обновляет lock) + commit обоих. -- **0.42.x sitemap-реструктуризация** — parity сверять по **page-locs** (развёрнутый sitemapindex), НЕ по именам под-sitemap'ов. 184 тут (было 172 на 0.42.0 — выросло, но NEW==PROD). -- **`/articles` 404 benign** — loc из sitemap отдаёт 404, если прод-оракул идентичен → не дефект (см. рецепт § Completeness-gate). -- Staging-порт `5020` — проверять `docker ps | grep 127.0.0.1:50` перед bind (stale throwaway от прошлого тиража). -- Smoke гнать **С VDS** (воркстейшн ловит LAN-DNS-перехват прод-доменов — см. memory `workstation-lan-dns-serves-local-cms-copy`). -- build-secret (VERDACCIO_TOKEN в ARG/ENV build-стадии) — известный follow-up, НЕ блокер (runtime-стадия отдельная, токена в финальном образе нет). - -## Тираж snolla 0.42.1 — статус -Все 5 snolla-сайтов на VDS: -- labtools.ru (17), emspb.ru (18), labtools.pro (19) — 0.42.1 in-place bump 2026-07-05. -- kupimknigi (21), **tandemmebel (20)** — 0.42.0 cutover, tandemmebel in-place bump до 0.42.1 2026-07-12. (kupimknigi остался на 0.42.0 по решению оператора — см. `NEXT_SESSION.md`.) - -## 0.43.2 v3 cutover (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30 -6-й сайт тиража v3 (после on-snolla, kupimknigi, labtools.ru, labtools.pro, emspb — 5 GREEN). In-place bump стека 20 (DNS на VDS с 2026-07-12, не трогали). Рецепт — [[minio-split-cutover-runbook]] §B3-v3. **6/6 v3 сайтов GREEN = тираж завершён.** -- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` (commit `9d51c09`) + `resolutions`-блок в root `package.json` (verbatim labtools.ru: `@aws-sdk/client-s3 3.1005.0` + `@smithy` chain 5.3.11 — optional determinism, live verify доказал 5.6.12 работает vs vds MinIO 2025) + реген `yarn.lock` (single `@smithy/signature-v4@5.3.11` deduped, no dual-instance). Pushed victor/tandemmebel.ru master `f4ddce6..9d51c09`. Endpoint fix `f4ddce6` (s3.endpoint→minio.vds.kzntsv.site) был в master с 2026-07-29, но в бегущем образе `8df10ee` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. -- **Build на VDS:** `git archive 9d51c09` → `~/build/tandemmebel` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/tandemmebel:9d51c09`(+latest), digest `sha256:d14982c24dc7c61a334c8973e6f1c48147c7d3cbcc772b28af6c629431947b16`, 476MB (v2 8df10ee был 583MB — v3 leaner), EXIT=0. `yarn install --immutable` в Docker прошёл (yarn.lock matches). -- **Staging :5020 parity-gate GREEN** (throwaway контейнер из env живого стека 20, 8 секретов, `--network proxy`): - - robots.txt md5 `a01e4c60…` byte-identical (87 B). - - sitemap index→5 sub-sitemaps → **184 locs == live 184 IDENTICAL** (0 prod-only / 0 new-only — regression нет). - - **self-consistency 183×200 + 1×404** (`/articles` — prod=404 parity benign, не регрессия). 0×301/5xx. - - `/` 200 staging 45738B vs prod 45181B (+557B = og:url/og:image request-host context — benign, byte-identical post-cutover). - - theme `Gotham-Pro.css` 200 4436B (v3 aws-sdk v3 S3-direct read из vds MinIO 2025 — латентный прод-баг 0B→4436B починен ещё cutover'ом 2026-07-12, сохранён). -- **Portainer PUT stack 20 LIVE:** node in-memory `put-stack` (НЕ PS — кириллица compose; Node 22 global fetch, GET `/stacks/20` для Env + `/stacks/20/file` для StackFileContent), env 8/8 preserved, `prune:false pullImage:true mem_limit 512m`. PUT 200. Контейнер `9d51c09` healthy ~9s. -- **Live smoke GREEN (via traefik):** `/` 200 45742B, `/projects` 200 90758B, `/sitemap.xml` 200 (184 locs == parity), robots md5 `a01e4c60…`, `/articles` 404 parity, theme `Gotham-Pro.css` 200 4436B (v3 S3-direct). TLS CN=tandemmebel.ru, expire Oct 10 2026 (не тронут — in-place swap). Лог чист (tedious deprecation + `tandemmebel.ru (snolla) listening`). -- **Rollback:** `tandemmebel:8df10ee` (0.42.1 v2, template-only share-buttons, жив в registry) — Portainer PUT назад / compose revert. `0cd9351` (0.42.1) + `ed96b18` (0.42.0) — deeper rollback. DNS→RUVDS 80.64.31.36 — крайний. -- Compose (admin source-of-truth) синхронно закоммичен `8df10ee`→`9d51c09`. **6/6 v3 сайтов GREEN — тираж v3 завершён.** \ No newline at end of file +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:208]] (concepts/tandemmebel-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`. diff --git a/.wiki/concepts/tg-digest-vds-deploy-runbook.md b/.wiki/concepts/tg-digest-vds-deploy-runbook.md index 5081ba9..bdda788 100644 --- a/.wiki/concepts/tg-digest-vds-deploy-runbook.md +++ b/.wiki/concepts/tg-digest-vds-deploy-runbook.md @@ -1,168 +1,3 @@ ---- -title: "tg-digest — VDS deploy runbook (sched cron + HTTP-воркер)" -type: concept -tags: [tg-digest, sched, vds, deploy, docker, portainer, runbook, worker, telegram] -related: [concepts/runbooks-index.md, concepts/sched-vds-deploy-runbook.md, concepts/portainer-stack-management-vds.md] -updated: 2026-08-28 ---- +# ⛔ Файловый канал закрыт -# tg-digest — VDS deploy runbook (sched cron + HTTP-воркер) - -> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.** -> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять. -> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать. -> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют. - -Развёртывание **воркера tg-digest** (дневной дайджест телеграм-каналов) на инфра-VDS: -контейнер-воркер (internal, сеть `proxy`, без traefik-роута) + задача в **sched** (ядро уже -задеплоено, стек 28, см. `sched-vds-deploy-runbook`): cron `0 5 * * *`, runner http, simple mode. -Опер-таска: `task:1311` (tg-digest-ops-stack). Аналог: yt-digest `task:1190` (не задеплоен — -этот ранбук первый для паттерна sched+worker). - -## Параметры - -| Что | Значение | -|---|---| -| Хост | VDS kzntsv `89.253.255.94` | -| Стек Portainer | **`tg-digest`** (новый, создаётся первым деплоем), endpointId **1**; после создания id записать сюда | -| Образ | `registry.kzntsv.site/tg-digest-worker:` (python:3.13-alpine, telethon+requests) | -| Compose source-of-truth | `admin/host-stacks/vds-kzntsv/tg-digest.compose.yml` | -| Воркер | контейнер `tg-digest`, HTTP `:8080`: `GET /healthz`, `POST /run` (simple mode: 2xx = ok) | -| Задача sched | runtime-регистрация `POST /tasks` (fileManaged:false, без рестарта ядра); cron `0 5 * * *` (**tz UTC** = 08:00 MSK) | -| Sched admin key | `pass sched/admin-key` (Bearer) | -| Per-task auth | `x-sched-api-key` = `WORKER_API_KEY` воркера = `pass sched/tg-digest-api-key` (завести при деплое) | -| mem_limit | `256m` | -| Сети | `proxy` (external, internal — НЕТ traefik-labels, наружу не публикуется) | -| Registry | `REGISTRY_URL/REGISTRY_USER/REGISTRY_PASS` = `pass vds-kzntsv/full-env` | - -## Артефакты - -- **Код:** репо victor/tg-digest, ветка main, минимум `d3aefb2` (raw-ингест, task:1475 — - старый `ingest_wiki` удалён, per-post ингест не работает). Воркер: `python -m src.worker`. -- **Dockerfile + HTTP-обёртка** (`http_worker.py`): коммитятся в **корень репо** victor/tg-digest - (dev-сторона); черновики — `admin/host-stacks/vds-kzntsv/tg-digest/`. Без них image не собрать. -- **Env-контракт воркера** (см. таблицу ниже) — все значения env (pass в контейнере НЕТ). - -### Env-контракт (воркер) - -| Переменная | Источник (pass) | Обязательна | Назначение | -|---|---|---|---| -| `TG_API_ID` | `telegram/api-id` — **НЕТ в pass, оператор** | да | MTProto app id (my.telegram.org) | -| `TG_API_HASH` | `telegram/api-hash` — **НЕТ в pass, оператор** | да | MTProto app hash | -| `TG_SESSION` | `/data/session.session` (volume; создаётся интерактивным логином) | да | Telethon-сессия | -| `TG_PHONE` | оператор (только при первом логине) | первый запуск | телефон аккаунта | -| `TG_CACHE` | — (дефолт `tg-cache`; на деплое `/data/tg-cache`) | опц | кэш коллектора + state.json (персист) | -| `TG_WINDOW_HOURS` | — (дефолт 24; бэкфилл 26-27 → 48-72) | опц | окно сбора | -| `TELEGRAM_BOT_TOKEN` | `telegram/full-env` | да | доставка дайджеста | -| `TELEGRAM_CHAT_ID` | `telegram/full-env` | да | чат доставки | -| `LLM_API_KEY` | pass-запись **отсутствует — уточнить у оператора** (deepseek; локально в gitignored `src/config/llm.json`) | да | стадия-2 (LLM-находки) | -| `LLM_BASE_URL` | — (дефолт `https://api.deepseek.com`) | опц | | -| `LLM_MODEL` | — (дефолт `deepseek-chat`) | опц | | -| `MAPPA_API_TOKEN` | `mappa/full-env` | да | ингест raw (wiki:3312) | -| `MAPPA_URL` | — (дефолт `https://mappa.vds.kzntsv.site`) | опц | | -| `MAPPA_PROJECT` | — (дефолт `tg-digest`) | опц | | -| `WORKER_API_KEY` | `sched/tg-digest-api-key` (завести) | да | проверка `x-sched-api-key` в /run | -| `TG_SEND` | — (`0` — dry-run smoke без отправки) | опц | доставка | - -### Задача sched (POST /tasks, runtime) - -```json -{ - "name": "tg-digest", - "schedules": [{ "cron": "0 5 * * *" }], - "config": { - "url": "http://tg-digest:8080/run", - "method": "POST", - "timeoutMs": 900000, - "auth": { "apiKey": "<из pass sched/tg-digest-api-key>" } - } -} -``` - -Простой режим (без `envelope`): sched шлёт POST, ждёт ответ до `config.timeoutMs`, 2xx = succeeded. -**`task.timeoutMs` НЕ ставить** (ceiling шлёт воркеру cancel — воркер его не реализует). - -## Первый деплой - -**0. Предусловия (оператор):** -- `pass telegram/api-id` + `pass telegram/api-hash` (my.telegram.org → App `tg-digest`); -- **сессия Telethon**: первый `python -m src.collector --live` интерактивен (телефон + код из СМС) — - создать session-файл заранее, положить в volume (см. gotcha 1); -- `LLM_API_KEY` (deepseek) — pass-запись или env; -- `pass sched/tg-digest-api-key` (новая запись, ключ для `x-sched-api-key`). - -**1. Dockerfile + http_worker.py в репо** (корень victor/tg-digest), main. Билд строго из main. - -**2. Build + push образа:** -```bash -docker login $REGISTRY_URL -u $REGISTRY_USER -p "$(pass show vds-kzntsv/full-env | awk -F= '/^REGISTRY_PASS=/{print $2}')" -docker build -t registry.kzntsv.site/tg-digest-worker: <чекаут victor/tg-digest> -docker push registry.kzntsv.site/tg-digest-worker: -``` - -**3. Стек Portainer** (канон `portainer-stack-management-vds`): создать `tg-digest` -(`POST /api/stacks/create/standalone/string?endpointId=1`, stackFileContent = compose из -source-of-truth, env-массив = секреты из таблицы; `pullImage: true`). **Без traefik-labels** -(воркер internal, доступа снаружи нет). - -**4. Регистрация задачи (без рестарта sched):** -```bash -AK=$(pass show sched/admin-key) -curl -ksS -X POST https://sched.vds.kzntsv.site/api/tasks \ - -H "Authorization: Bearer $AK" -H 'Content-Type: application/json' \ - -d '{"name":"tg-digest","schedules":[{"cron":"0 5 * * *"}], - "config":{"url":"http://tg-digest:8080/run","method":"POST","timeoutMs":900000, - "auth":{"apiKey":"<из pass sched/tg-digest-api-key>"}}}' -``` - -**5. Smoke (ручной триггер):** -```bash -curl -ksS -X POST https://sched.vds.kzntsv.site/api/tasks/tg-digest/run \ - -H "Authorization: Bearer $AK" -H 'Content-Type: application/json' \ - -d '{"temporary": true, "triggeredBy": "admin-smoke"}' -``` -Блокируется до конца рана (sync runner) → ответ = терминальный run. -Сухой деплой без ключей: контейнер Up, `/healthz` 200, ран падает `die 2 creds missing` — ок как -проверка плумбинга, не как acceptance. - -## Verify - -- Контейнер: `Up (healthy)`; `docker exec tg-digest python -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:8080/healthz').read())"` → `{"ok": true}`. -- sched: `GET /api/tasks` (Bearer) → `tg-digest` в списке, `nextRunAt` завтра 05:00 UTC. -- Smoke run: `GET /api/runs?task=tg-digest` → последний run `succeeded` (с ключами) с полем `result`. -- Артефакт дня: raw-запись в mappa `raw/research/<дата>-` (конвенция wiki:3312) + доставка в TG. - -## Rollback - -- Задача: `DELETE /api/tasks/tg-digest` (runtime-таска, 204) — расписание снято. -- Стек: удалить `tg-digest` в Portainer. -- Образ: старый тег в registry (повторный деплой с предыдущим тегом). - -## Gotchas - -1. **Сессия Telethon обязательна, api_id/api_hash недостаточны** (2026-08-28): первый `--live` - интерактивен — телефон + СМС-код; без session-файла в контейнере коллектор не стартует. - Сессию создать заранее (локально или TTY контейнера), смонтировать в `/data/session.session`. -2. **pass в контейнере НЕТ** (2026-08-28): коллектор умеет читать pass через bash (`_pass`), но в - образе его нет → только env. Все секреты — env при деплое (chmod 600 / Portainer env, вне git). -3. **TZ cron = UTC** (дефолт sched): `0 5 * * *` = 08:00 MSK (утренний дайджест, как yt-digest). -4. **`config.timeoutMs` (транспортный) — 900000** (пайплайн collect→stage2 LLM→ingest→tg занимает - минуты); `task.timeoutMs` (ceiling) НЕ ставить — при срабатывании sched шлёт `POST /cancel`, - воркер (не паттерн C, не @apilki) его не реализует → ран пометится failed при живом воркере. -5. **mem_limit 256m**: python+telethon на старте ~150-200m — достаточно; не поднимать без нужды. -6. **Образ строго из main с `d3aefb2`** (raw-ингест): старый `ingest_wiki` удалён, per-post - ингест не работает; билд из старого коммита → падение на ингесте. -7. **Бэкфилл за 26-27 августа**: `TG_WINDOW_HOURS` 48-72 на один прогон (или два прогона); - `ingest_raw` идемпотентен по дню (`raw/<дата>-`). -8. **Runtime-регистрация = fileManaged:false**: `POST /tasks` не трогает `sched.tasks.json` - (остаётся `{"tasks": []}`); таска живёт до `DELETE /tasks/:name`. tasks.json — только - декларативный источник (рестарт ядра НЕ нужен). -9. **Portainer env не env_file** — секреты в env-массив стека (gotcha 4b портайнер-канона: - PUT сбрасывает env, если не передать массив целиком). - -## Связи - -- [[concepts/runbooks-index.md]] — индекс ранбуков -- [[concepts/sched-vds-deploy-runbook.md]] — ядро sched (стек 28, admin API, MariaDB) -- [[concepts/portainer-stack-management-vds.md]] — общий Portainer-канон (JWT, gotchas) -- Репо victor/tg-digest: `src/worker.py` (оркестратор), спека wiki:3260, конвенция raw wiki:3312 +**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3320]] (concepts/tg-digest-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.