Files
admin/.tasks/STATUS.md

1436 lines
229 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Admin Task Board
_Updated: 2026-07-04 (8) — 🟢 **[tandemmebel-web-vds-deploy] gallery-дефект ЗАКРЫТ на staging — GREEN.** Workshop прислал коррекцию sha → ребилд на `b02ca18` (core 0.16.2, SlugFeedPage-fix). Собрал `tandemmebel:b02ca18` (digest f8672218), передеплой стека 20 (env 8/8, healthy). DoD-чек **12/12 GREEN**: gallery-грид staging == prod ТОЧНО на всех роутах (bedrooms 80/80, kids 138/138, office 26/26 и т.д.). Крошки наполнены (`Главная/Мебель/Галерея идей`, были пустые), title полный (`Фото Офисная мебель…`), байты 28508≈прод 28253. Я флагал сомнение (мой DB-дамп показал: `/gallery` — конвенционный саб-роут, а не feed; SlugFeedPage-fix мог промахнуться) — но жёсткий рендер-чек показал GREEN, фикс сработал. **Cutover ОТЛОЖЕН оператором (не забыть!) — триггер запуска = владелец сайта меняет DNS reg.ru→89.253.255.94**, тогда verify авторит.NS→боевой Host в стек 20→live-smoke (порядок в [[NEXT_SESSION.md]]). Прогеру после разноса от оператора отправлен рабочий green-хендофф на свип 188 URL. RUVDS=rollback._
_Updated: 2026-07-04 (7) — 🔵 **[tandemmebel-web-vds-deploy] дефект идентифицирован = gallery FeedPages-VM.** Workshop прислал ops-таск: ребилд стека 20 на `a173401` (FeedPages page-type портирован, чинит gallery 404→200). Гейт green (origin HEAD=a173401 ⊇ a173401+22bd8f2), собрал `tandemmebel:a173401` (snolla 0.35.0/core 0.16.0/data 0.13.0, digest 6962bd97), передеплоил стек 20 (Portainer API, env 8/8, healthy). Роутинг чинится: 12/12 furniture gallery-роутов 404→200. **НО рендер-чек RED — grid ПУСТОЙ на всех роутах** (staging `/galleries/*/images` = 0 vs prod 24/80/62) + **breadcrumb-подписи пустые** (`itemprop=name` без текста). Оба симптома = один корень: FeedPage-VM не прокидывает `item` в шаблон. Оператор дефект подтвердил независимо. Мой деплой чист; фикс на прогере (tandemmebel.ru). Пропинговал прогера с уликами (empty-grid + breadcrumb addendum) → ждёт новый sha, прогоню тот же DoD (staging_imgs==prod_imgs + непустые крошки). Cutover HELD. См. [[NEXT_SESSION.md]]._
_Updated: 2026-07-04 (6) — 🔵 **[tandemmebel-web-vds-deploy] BLOCKED на новом дефекте.** Дал оператору staging-URL (https://tandemmebel.vds.kzntsv.site) на визуальную проверку sharp-сборки → оператор нашёл НОВЫЙ дефект (иной, чем прошлые глиф/imgproxy), специфику не назвал, «чиним в новой сессии». Cutover HELD. Мои автоматические гейты по sharp-staging были green (media/watermark/variant-cache/parity) — значит дефект визуальный, не пойманный curl-смоком. Sharp-staging жив на стеке 20 для разбора в новой сессии. См. [[NEXT_SESSION.md]]._
_Updated: 2026-07-04 (5) — 🟢 **[tandemmebel-sharp-staging-rebuild] closed — SHARP staging GREEN.** Пивот tandemmebel на in-process sharp завершён. Образ `tandemmebel:68b93a9` (snolla 0.34.0/core 0.15.0, digest 12288b15) собран на VDS, стек 20 обновлён (env verbatim 8/8, imgproxy из tandem-пути убран). Parity-smoke GREEN: nav 8/8, sitemap staging⊇prod (prod-only=0, +12 categories benign), 2012-посты 31/31, redirects 301/301; sharp media = slug-URL `/galleries/<gid>/images/<variant>/<seq>` 200 webp serve-bytes, **0 /imgproxy-рефов** (осиротевший imgproxy-конфиг в пине эмпирически мёртв → heads-up воркшопу вычистить), variant-cache пишется (32 объекта content-addressed), watermark визуально ✓ featured+lightbox / gallery-small чистый. [tandemmebel-web-vds-deploy] теперь 🟡 sharp-staging-green, ждёт **cutover DNS — gated оператором** (reg.ru→89.253.255.94). RUVDS=rollback._
_Updated: 2026-07-04 (4) — 🟢 **[minio-variant-cache-bucket] closed.** sharp-prereq. Создан бакет `variant-cache` в shared MinIO (minio.kzntsv.site/books-vds) — S3-эквивалент легаси App_Data/imageCache, куда sharp кладёт resize+watermark варианты on-demand (без него 500 NoSuchBucket на всех sharp-картинках = agent-блокер 1 из tandemmebel sharp-verify). Грант: snolla S3-креды == MinIO root (сверено) → в mode-server-fs полный доступ by construction, IAM не нужен. Write-verify под деплойными кредами: put→stat→get(md5 match)→delete→gone ✓. Другие 17 бакетов не тронуты, lifecycle не ставил (кэш регенерируем). Готовит почву под sharp-переезд tandemmebel (движок victor/snolla media-sharp-imageprocessor-port в переработке)._
_Updated: 2026-07-04 (3) — 🟢 **[imgproxy-stack29-watermark-rollback] closed.** Оператор сменил watermark-подход: tandemmebel уходит с imgproxy-watermark на in-process sharp (victor/snolla media-sharp-serve-and-watermark) → глифу на SHARED стеке 29 не место. Снял `IMGPROXY_WATERMARK_DATA` со стека 29 (books-vds): вырезал строку → результат byte-identical pre-watermark бэкапу (cross-check true) → PUT+redeploy+purge. Verify (не задеть боевых): pilorama98 (главный консюмер) 3 рендера == baseline (43950/172042/160010B) не задет; stostayer НЕ imgproxy-консюмер (картинки /galleries//images//assets/) → не затрагивается; WATERMARK_DATA present:false; tandem featured→чистый (ожидаемо, staging→sharp). Глиф-бинарь 7127e92a оставлен для sharp. [tandemmebel-web-vds-deploy] теперь ждёт ПЕРЕСБОРА образа на sharp-пин перед cutover. Notify workshop._
_Updated: 2026-07-04 (2) — 🟡 **[tandemmebel-web-vds-deploy] STAGING GREEN, ждёт cutover.** Финал тиража tandemmebel. Образ `tandemmebel:0facb35` (snolla@0.32.2/core@0.13.8, digest 464d2f22, watermark-override в пине) собран на VDS (обход 499). Стек Portainer **Id 20** (staging-rule `tandemmebel.vds.kzntsv.site`, env verbatim из labtools стека 17, 8/8), контейнер healthy MSSQL+S3. imgproxy-nginx кэш (books-vds стек 29) purged → featured отдаёт вотермарк без cache-bust (27820B). Staging-smoke с VDS GREEN: status-parity 10/10, sitemap staging⊇prod (prod-only=0, +12 /projects/categories/*=benign#3, /articles 404 parity), trailing 301/301, blog-post 200 (+3.5KB=benign#2 imgproxy vs /galleries/ 26/26), 2012-посты 31/31, theme 21/22 MD5-OK. Находка (не блокер): prod Gotham-Pro.css=0B пустой, staging=4436B корректный → cutover чинит латентный прод-баг шрифта. **HOLD cutover** — жду отмашку оператора на DNS reg.ru→89.253.255.94; порядок: verify авторит.NS → ТОЛЬКО потом боевой Host-rule (иначе LE упадёт на RUVDS). RUVDS=rollback._
_Updated: 2026-07-04 — 🟢 **[imgproxy-watermark-glyph-books-vds] closed.** Ops-подхват по тиражу tandemmebel: воркшоп попросил выставить глиф-вотермарк на imgproxy стек 29 (books-vds). Первый base64 (inbox И закоммиченный таск) был БИТЫЙ на источнике — `tasks_create` порезал 10КБ-поле (sha `f8f0…``e995…`, IEND нет, зацикленный хвост `FQtAlGLzMfBc`×N); поймал sha-сверкой ДО прода (не запушил битьё → imgproxy не упал). Воркшоп до-доставил `logo.png` бинарём в git (`host-stacks/books-vds/tandemmebel-watermark-logo.png`, commit `7127e92a`, sha256 `e995…971fc`, 8015B, IEND ok). PUT стека 29 (Portainer `portainer.kzntsv.site` ep1, X-API-Key, `IMGPROXY_WATERMARK_DATA` inline, `PullImage:false`, БЕЗ глобального `_OPACITY`). Smoke cache-busted: A tandem вотермарк ВИДЕН глазами (27294→27820B); B pilorama контроль byte-identical (160010B) — opacity не просочилась. Notify workshop → tandem-агент на ре-скрин→пин→deploy. Дальше жду `[tandemmebel-web-vds-deploy]`._
_Updated: 2026-07-03 — 🟡 **заведена [morethencms-s3-filestorage-provider].** Разведка «почему /admin/assets/.../delete → 500» на RUVDS IIS: корень = app pool `IIS AppPool\snolla` имел на `App_Data` только RX после scp-миграции → `File.Delete` UnauthorizedAccessException (не MinIO, провайдер `assets`=Local; NRE-версии исключены проверкой строк Folders/Files в БД). Fix применён: `icacls App_Data /grant "IIS AppPool\snolla:(OI)(CI)(M)" /T` (44293 файла, verified). Попутно заменён `labtools-price.pdf` в ОБА хранилища (MinIO ETag→6426ddd0 + локалка, MD5 сверены). Настоящее закрытие split-brain (админка=Local vs фронт=MinIO) → новая таска на S3-провайдер `MoreThenCms.FileStorage.S3` (name approved): ТЗ доставлено в инбокс MoreThenCms, объём=все контентные классы, 1 MinIO, deploy на локальный IIS windows-recovery-host→боевой. DNS-аудит: инфра-хосты не перехватываются (нет 127.0.0.1). Concepts: snolla-admin-appdata-acl-500 + galleries-storage-class-local-not-s3._
_Updated: 2026-06-29 (4) — 🟢 **[pilonuxt-deploy-sitemap-taxonomy] closed.** pilonuxt `6b9ae63` LIVE (стек 16) — bump-only sitemap taxonomy: content-api 0.15→0.16, core 0.2→0.4 (taxonomy enum, прокси не менялся). **Tree-check `.output` 5/5** (content-api 0.16.0, core 0.4.0, data 0.9.1 1×, snolla 0, btoa 0). Push словил **499 на .output** → ретрай прошёл (digest `226584ce…`). Acceptance 4/4: `/sitemap.xml` индекс 7 записей с `store-taxonomy-1`; `/sitemap-store-taxonomy-1.xml`→200 57 url абсолютные (35 categories+22 tags, vendors 0=пусто в CMS); v1 цел (products-1/pages-1 200); `taxonomy-999`→404. Регресс `/`/`/catalog` 200, stderr чист. Откат: стек 16→`42f9d64`. Ack→victor/pilorama98.ru._
_Updated: 2026-06-29 (3) — 🟢 **[pilonuxt-deploy-sitemap] closed.** pilonuxt `42f9d64` LIVE (стек 16) — sitemap.xml через content-api@0.15.0 (+core@0.2.0). Build из монорепы (форс schema-gen против прод-MSSQL). **Tree-check `.output` 5/5:** content-api 0.15.0, core 0.2.0, data 0.9.1 (1×), snolla 0 (7 grep-хитов = комменты-провенансы, 0 import), btoa 0. Push **напрямую** (499 не случился). Acceptance 3/3: `/sitemap.xml`→200 `<sitemapindex>` 6 источников; `/sitemap-pages-1.xml`→200 `<urlset>` 70 url все абсолютные; `/sitemap-store-products-999.xml`→404. Регресс цел (robots/`/`/`/catalog` 200), stderr чист. Откат: стек 16→`cf2bba2`. Ack→victor/pilorama98.ru._
_Updated: 2026-06-29 (2) — 🟢 **[deploy-pilonuxt-forms-api-drop-snolla] closed.** pilonuxt `cf2bba2` LIVE (стек 16) — ADR-0010: формы на `@snollajs/forms-api`, `@snollajs/snolla` дропнут целиком, core@0.1.1. Перед перекатом **tree-check `.output` 4/4** (0× snolla, 0× btoa, 1× data@0.9.1, core 0.1.1 + content-api 0.14 + forms-api 0.1.0). Smoke зелёный: SSR `/`+`/catalog`→200, robots/yandex из БД (ADR-0009 не регресс), форма `?path=/checkout` пустой→422 JSON / `/nope`→404 (valid НЕ слал). Гоча: 422 только с `Accept: application/json`. Notify victor/pilorama98.ru (монореп-inbox)._
_Updated: 2026-06-29 — 🟢 **[deploy-pilonuxt-static-pages-robots] closed.** pilonuxt `40bb383` (DB-backed staticPages + robots.txt, ADR-0009) LIVE на проде (Portainer стек 16). Build из монорепы с **форс-регенерацией клиента** (`rm src/generated`+`schema-gen 0.6` → новые методы `getStaticPage`/`getRobotsTxt`); push с дома 499 на `.output`-слое → fallback `docker save | ssh vds load`+push с VDS. Smoke acceptance 6/6 verified телами (robots полный из БД с Yandex Clean-param+Sitemap, yandex/google верификации, /nope→404, /+/catalog→200). Notify victor/pilorama98.ru отправлен._
_Updated: 2026-06-18 (2) — 🟢 **[books-task-runner-registry-auth-cred] closed.** Кред books-ci (секция `registry`, полная) в task-runner config-volume на books-vds (644 root, бэкап, EACCES не повторён). После deploy нового кода (`master-bf2a8c5`) прогнан `registryGc {dryRun:true}` через scheduler-dispatch: **success, 401 нет**, repos books-* видны (7), отчёт сгенерён. Acceptance auth/dryRun met. **Finding отдан books (НЕ блокер):** GC бежит, но `drop=0` всегда (keep=tags1) — `getCreated`→null для всех манифестов (configDigest не извлекается, OCI image-index?), `planDeletions` защищает null-dated группы → keepLastN не применяется, реестр не чистится. Bug в books `lib/registryV2.js`, owner — books._
_Updated: 2026-06-18 — 🔵 **[books-task-runner-registry-auth-cred] blocked.** Кред books-ci (секция `registry`, baseUrl+username+password — полная, bind-mount затеняет config образа) положен в task-runner config-volume на books-vds; mode 644 root сохранён (бэкап `.bak-pre-registry-2026-06-18`, EACCES-инцидент 25.05→18.06 не повторён), container restart→healthy, `config.get('registry.*')` резолвится. books-ci валиден: `/v2/_catalog`→200, репы books-* видны, 401 нет. **Блокер:** работающий образ (88f2bee, 17.06) содержит СТАРЫЙ registryGc (Gitea API), тега `master-a3734d5` в реестре нет → CI не собрал v2-DELETE rewrite. In-app dryRun ждёт build+deploy. Inbox-ответ books отправлен._
_Updated: 2026-06-13 — 🟢 **[migrate-assets-originals-to-s3] closed.** assets-оригиналы (весь `App_Data\assets`: 4750 obj / 215.5 MiB, 145 owner-папок) залиты в новый MinIO bucket `assets` (books-vds) rclone'ом с RUVDS IIS, ключи `<ownerId>/<storageFilename>` verbatim. Verify count+size == source. Smoke (мимо LAN-DNS): brevno.jpg → 200 image/webp 64556 B (был 404), валидно-подписанный несуществующий объект → 404 (негатив-контроль). Код snolla не менялся (path A); scope намеренно шире pilorama98 — закрывает класс-404 для всех тенантов snolla, регрессии нет (shared bucket, как galleries). ⚠️ snolla-side остаётся `content-api/routes/assets.js` (пустой stub) — без него картинки не рендерятся на сайте. Inbox victor/snolla отправлен._
_Updated: 2026-06-12 (4) — 🟢 **[migrate-gallery-originals-to-s3] closed (scope pilorama98).** Галереи snolla не отдавались (imgproxy 404) т.к. в legacy `storageClient="galleries"` = тип **Local** (диск `App_Data\galleries\<siteId>`), НЕ S3 — оригиналы никогда туда не заливались (продукты — заливались, bucket `pilorama98`). Развилка A/B закрыта фактами MinIO → **A**: создан bucket `galleries`, залиты 301 файл (77 MiB) pilorama98 (siteId `37e67fc4…`) с RUVDS IIS через rclone → `s3://galleries/<siteId>/<guid>.jpg` verbatim (== ровно то, что строит `middleware/galleries.js:47`). Код snolla НЕ менялся. Verify: count+size == source. Smoke с books-vds: реальный объект imgproxy → 200 webp, fake → 404. Уточнение инфра: `imgproxy.kzntsv.site` с 08.06 на **books-vds** (стек 29/30), не на windows-host — `minio-imgproxy-on-vds.md` был stale. Inbox victor/snolla отправлен._
_Updated: 2026-06-12 (3) — 🟢 **bookva-es snapshot done** (не отложен). Portainer stack 37 пересоздан с `path.repo=/snapshots`+bind (PUT API, том цел, epz/products уцелели), repo `kreknin` зарегистрирован, snapshot-блок в `run.sh`. Поймана коллизия basename `snapshots` (оба ES-каталога одноимённы → сливались в один dest-репо, порча обоих) → источник bookva-es = родительский `/usr/docker/bookva-es`. Verified: `BOOKS-VDS backup OK 10m28s`, на kreknin раздельно `snapshots/`(slovo index-41) + `bookva-es/snapshots/`(index-4). Таска `bookva-es-snapshot-repo` 🟢 closed._
_Updated: 2026-06-12 (2) — 🟢 **bookva-* backup gap закрыт.** bookva tenant (db/mongo/es/minio, поднят 26.05) не бэкапился — books-скрипт написан 25.05 до bookva, покрытие не расширили (висело wiki-follow-up #2). Добавлены bookva-db (тот же root pw), bookva-mongo (no-auth), bookva-minio (raw volume) в `books-vds-backup-daily-kreknin/run.sh`; деплой == репо (`.bak-pre-bookva`). Verified green: `BOOKS-VDS backup OK 10m42s`, артефакты на kreknin (bookva-mariadb 257M, bookva-mongo 3.2M, bookva-minio 1.4G). bookva-es отложен → `bookva-es-snapshot-repo` (покрыт slovo-снапшотом, индексы идентичны). Правило в память: новый stateful → бэкап в том же коммите._
_Updated: 2026-06-12 — 🔴→🟢 **MSSQL backup incident + estate backup audit.** VDS daily backup падал с 12.06 (line 108, exit 1): mssql-блок (added 11.06) использовал `WITH ... INIT` → упирался в компрессованный media-header майских `.bak` (Express не пишет COMPRESSION) → молча падал первым cron-запуском. **5 боевых CMS-баз 3 недели без offsite-копии.** Fix: `INIT`→`FORMAT`, прогон verified (MoreThenCms 910M/StayerCalculator 528M/StayerPrice 39M/TireService 4.5M/stostayer 990M на kreknin). Репо-копия `run.sh` синхронизирована (mssql-блока не было). Concept: `.wiki/concepts/mssql-on-vds.md` gotcha#2. **Estate backup-аудит** → `.wiki/concepts/backup-inventory-2026-06.md`: ✅ openwrt UCI backup настроен (cron 03:30 → kreknin, restricted forced-command key); ⚪ заведена `kreknin-self-backup` (#1 SPOF, на потом); nl-vds x-ui.db — user: не нужен; windows-host stale script — user: забыть._
_Updated: 2026-06-08 — 🟢 ad-hoc fix `maljarka.tandemmebel.ru` (iis-migration follow-up): домен переехал на RUVDS корректно (DNS/TLS/IIS ок), но отдавал 502 **только на HTTPS**. Root cause не в миграции: у тенанта maljarka `dbo.Sites.SettingsData=NULL` (нет блока `httpSecure`) → MoreThenCms `SnollaMiddleware` бросает KeyNotFound на HTTPS-ветке → 502; по HTTP 200. Fix: `UPDATE dbo.Sites SET SettingsData='{httpSecure:enableHttps=true,…}' WHERE SiteId=a2476738… AND SettingsData IS NULL` + `Restart-WebAppPool snolla`. Verified maljarka:443→200 (server-local + external по 80.64.31.36), kupimknigi не задет. Audit: maljarka — единственный NULL-сайт с :443-биндингом из 25. rimiz degraded по др. причине. Concept: `.wiki/concepts/morethencms-null-settingsdata-https-502.md`._
_Updated: 2026-06-05 (вечер) — 🟡 `fix-nl-vds-reality-pq-dest`: по команде user применён server-side fix + ротация кредов. Reality dest intel→microsoft (x-ui.db + restart), сквозной тоннель через реальный :443 = HTTP 204; 32030 жив. Креды ротированы (root SSH / panel user+pass / panel secret-JWT — светились в плейнтексте; новые в `pass nl-vds-3xui/full-env`, старые отклоняются, БД-бэкап на сервере). Остался client-side: user меняет SNI в v2rayN (`pqmkayaxo2`→microsoft) + подтверждает → close._
_Updated: 2026-06-05 — заведена `fix-nl-vds-reality-pq-dest`. Новый NL VDS (213.176.64.253, 3x-UI/Xray 26.6.1): диагностирован отказ Reality-инбаунда 443 — **ML-DSA-65 (PQ) × dest `www.intel.com` (Akamai шлёт HRR)**; plain VLESS 32030 работает. Узел+root-cause в вики (`nl-vds-3xui`, `reality-pq-mldsa65-dest-incompatibility`); креды в `pass nl-vds-3xui/full-env`._
_Updated: 2026-05-31 — 🟢 `vehicles-loader-progress-deploy` **closed**: live-verify прогона Sun 31.05 06:21→07:53 MSK прошёл — progress-logging показывает движение по всем фазам (▶/батчи/✓/delete-sweep), НЕ тишина; exit 0; `importRun id=4 ok`, `reportJson errors:[]`. **Попутно найден+починен email-баг:** `STOSTAYER_MAIL_TO=site@stostayer.ru` (слалось само себе) → `vitya.kuznetsov@gmail.com` в host env (бэкап `.bak.20260531`); доставка на gmail подтверждена тест-письмом (`250 queued`). Follow-up'ы (не блокеры): units-фаза 1h31m на 100k rows = узкое место; generation `unmatched:51`; репо `config/default.json` дефолт `to` всё ещё site@ (прод перекрыт env)._
_Updated: 2026-05-30 — 🟡 `vehicles-loader-progress-deploy`: деплой 0.4.0 ВЫПОЛНЕН (build здесь → push `docker.stostayer.ru` → host pull + re-tag `:latest`=0.4.0, 0.3.0 retained). Acceptance #1 ✅. Verify-окно = natural (user-выбор, не форсим baseline → клиент без внепланового email). timer next trigger Sun 2026-05-31 06:21 MSK → live-verify journalctl в след. сессию. Open Q #1 (push кода) снят: `a74ef73` уже в origin/master._
_Updated: 2026-05-30 — заведена ⚪ `vehicles-loader-progress-deploy` (handoff из stostayer.new). Progress-logging 0.4.0 зашипан в коде (stostayer.new `a74ef73`, 24/24 теста); ops-follow-up — rebuild образа той же схемой (build здесь → push `docker.stostayer.ru` → host pull+re-tag) и снять `journalctl` с боевого прогона (закрывает не-верифицированный 5-й критерий «journalctl показывает движение»). Попутно добивает остаток email-SEND из `vehicles-loader-image-distribution`._
_Updated: 2026-05-29 — 🟢 `vehicles-loader-image-distribution` **closed**: канал поставки = собственный registry клиента `docker.stostayer.ru`, build у нас (verdaccio-депы запекаются → клиенту verdaccio не нужен), без Portainer (oneshot + systemd-timer). Доки финализированы (stostayer.new `e55cfba`). Открытие `pass stostayer/client` показало свою инфру клиента (Portainer+registry) → развилка A/B пересмотрена. Фактический prod-деплой — follow-up, ждёт grant'а._
_Updated: 2026-05-29 — **РЕЦИДИВ того же инцидента, диагноз сменён.** Вчерашняя гипотеза «оператор в cutover» **опровергнута**. Истинная причина: ES публиковал `0.0.0.0:9200` мимо traefik (free-ES без auth) → **ransom-бот** удалял индексы by-name (мимо Control #1), оставлял `read_me` с BTC-выкупом. accessLog (Control #2) пуст — бот шёл прямо в порт. **Control #3 (fix):** убрана публикация host-порта (Portainer PUT stack 33) → дыра закрыта; `epz/products/artmone` restore из `daily-2026-05-25`. Второй, отдельный баг: epz-поиск падал у ОБОИХ тенантов — `config.get("tenant")` (node-config) не задан ни в `default.json`, ни в env-маппинге, `TENANT` env был мёртвым грузом → добавил `tenant` в overlay `default.json` (slovo/bookva), резолвится, ошибки прекратились (products работал — другой код-путь). accessLog откатан (сторожил не ту дверь). Exposure-audit → ⚪ `harden-books-vds-exposed-ports`. См. `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md` § «Рецидив 2026-05-29»._
_Updated: 2026-05-28 (вечер) — 🟢 `restore-elasticsearch-indices-books-vds` **closed** в ту же сессию через snapshot restore (46 сек) из `daily-2026-05-25` (полные данные, ~2ч после оригинального reindex'а). RCA: 5 индексов (включая system `.tasks`) удалены через ES API `DELETE _all` за 1 сек на 2026-05-26 10:21 UTC, **1ч 11мин после создания bookva-es** — оператор в cutover-prep попал на canonical вместо internal-only bookva-es. Caller identity не восстановим (audit log = X-Pack платный, traefik accessLog был выключен, Portainer audit = enterprise). 2 preventive фикса applied + verified: (1) ES env `action.destructive_requires_name=true``DELETE _all` / wildcard теперь 400; (2) traefik JSON accessLog в `/letsencrypt/access.log` — будущие DELETE оставят forensic след. См. таску + `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md`._
_Updated: 2026-05-28 — **prod incident**: books-app slovo поиск товаров + EPZ сломаны, ES `elasticsearch.kzntsv.site` (books VDS stack 33) пуст (0 индексов из 3 ожидаемых; source `elasticold.kzntsv.site` rollback — все 928k docs на месте). Заведена ⚪ `restore-elasticsearch-indices-books-vds`. Окно поломки: 27.05 10:24 → 28.05 13:59, в этом окне шла работа bookva-cutover-prep (bookva-es stack 37 создавался) — возможный конфликт. См. таску для playbook'а._
_Updated: 2026-05-28 — **vds-kzntsv network-stack mismatch RESOLVED** в 13:52 MSK. Сначала ~2.5ч активного outage (05:45-08:30) + ~5.5ч на эфемерной статике до окончательного fix хостером. Revised RCA: наш netplan+networkd поверх provider's expected ifupdown stack ломал их auto-recovery когда DHCP-binding разорвался на их стороне. Fix: `systemctl mask netplan systemd-networkd` (на running system, без stop — IP и SSH сохранились), Rusonyx ребутнули + положили чистый `/etc/network/interfaces.d/ifcfg-eth0` с /18 netmask через свой `start/ipadd` procedure. Все 24 docker контейнера up. Disk after GC: 79% (132G→118G/158G). Anti-pattern закреплён: НЕ использовать netplan на Rusonyx VDS. Wiki updated с revised RCA + permanent-fix runbook + 2 quirks (#9 /18 layout, #10 ifupdown vs netplan)._
_Updated: 2026-05-27 — `modulair-rag-vds-redeploy` 🟢 **closed** в ту же сессию: 4-контейнерный стек развёрнут на VDS (Portainer stack 15), acceptance 6/6. Образы пересобраны на самом VDS (push 3.36GB через traefik с дома падал 499); env/entrypoint/minio-host скорректированы под VDS-реальность. 3 follow-up'а переданы в modulair-rag handoff._
_Updated: 2026-05-27 — заведена `modulair-rag-vds-redeploy` ⚪ (handoff из modulair-rag session). NAS-loss redeploy 4 контейнеров на VDS. Блокер MinIO снят — verified up на VDS 2026-05-27. postgres `proxy`-network reachability подтверждён (`postgres:5432` резолвится из pipeline/mcp). Спека: modulair-rag concept `nas-loss-vds-redeploy-context` + compose.yml as-is._
_Updated: 2026-05-27 (ночь, после re-open) — ops-mcp multi-tenant **activated** (stack 26 PUT + BOOKVA_MARIADB_PASSWORD env). Smoke verified: slovo/bookva DB queries возвращают tenant-specific data (АФО2 vs Ира warehouses), bookva agendaJobs count=2818. Один host-level books-ops-mcp видит обе tenant DBs._
_Updated: 2026-05-27 (ночь) — bookva-tenant deep-debug session. 6 багов одной природы (incomplete cutover-prep): (1) bookva-web без config volume → slovo DB leak; (2) bookva-{api,scheduler,task-runner} mount path /app→/usr/src/app; (3) NITRO/NUXT env не reference'или в compose; (4) bookva-scheduler без RECONCILER_ENABLED → dry-run handlers; (5) bookva-scheduler без healthcheck → CI timeout; (6) deploy.yml svc=job-scheduler vs secret=SCHEDULER mismatch. + composite agendaJobId display fix + books-web config mount (slovo also missed) + jobs.post.js agenda.db.collection→MongoClient. Ingested wiki concept tenant-overlay-config-volume-mount-path-pitfall (shared). ops-mcp multi-tenant код в image готов но stack 26 ещё не пере-PUT'нут (next session)._
_Updated: 2026-05-26 (поздний вечер) — `books-ops-mcp-host-promote` 🟢 **closed** в ту же сессию следом за заведением. Stack 26 (books-ops-mcp) in-place PUT в host-level compose без container churn. Stack 43 (bookva-ops-mcp) deleted. 2 Gitea secrets revoked. `victor/books ae3ab14` + `victor/bookva-overlay 50f5bbb` + `.admin` host-stacks/books-vds/ops-mcp.compose.yml. ops-mcp теперь management-plane (manual Portainer)._
_Updated: 2026-05-26 (поздний вечер) — заведена `books-ops-mcp-host-promote` ⚪ (deploy run#437/439 валился на bookva-ops-mcp restart-loop → hot-fix `bookva-overlay 437d024` idle-stub interim → user ideology «books VDS = host, slovo/bookva = клиентские стэки» → promote `books-docker-proxy-ro`+`books-ops-mcp` в host-level stack, drop bookva-ops-mcp). Deploy run#440 зелёный._
_Updated: 2026-05-26 (поздний вечер) — `bookva-tenant-cutover-prep` 🟢 **closed** — 6/7 в одну сессию: Steps 1+2+3+4+5+7. Step 6 delayed по spec (1-2 нед до cutover). bookva-ozon-mcp deferred (image не в registry). MinIO bucket copy deferred (cutover-time). 9 bookva Portainer stacks created (3 active: db/mongo/es internal-only; 6 stopped pre-cutover). bookva-overlay 4 commits: templates+mariadb:10.6+mongo:4.2+api internal-only+depends_on removed._
_Updated: 2026-05-26 (вечер) — `bookva-tenant-cutover-prep` 🟡 paused, 4/7 steps done в этой сессии: Steps 1+5+7 closed, Step 3 partial (6 empty volumes + 4 populated). Maintenance-window remaining: Step 2 stacks, Step 3 stateful (Mongo/MariaDB/MinIO), Step 4 login-gate. bookva-overlay templates align'нуты к prod shape + design v3 (commit `5b173de` pushed). books `75ea320` pushed (deploy/web.compose.yml embed-api refs). Pass entry `gitea/victor-books-ci-bookva-overlay` added._
_Updated: 2026-05-26 — заведена `bookva-tenant-cutover-prep` ⚪ (7-step prep на books VDS перед поднятием Bookva стека). Code-side done в victor/books master: embed-api с zod^4 fix, ES endpoint per-tenant, PWA redirect banner, deploy.yml per-tenant, overlay-репо finalize, ntfy per-tenant. Осталась только Portainer/Gitea-secrets/VDS работа._
_Updated: 2026-05-25 (`iis-migration-to-ruvds` 🟢 closed Phase 1 per user decision — 9/24 hostnames live на RUVDS, source IIS оставлен running. `migrate-elasticsearch-to-books-vds` 🟢 closed ранее сегодня. 16 IIS hostnames + LE renewal pipeline + decommission — descoped в Closure note, не отдельные tracker tasks.)_
## 🟢 [kupimknigi-deploy-snolla-0-42-0] — VDS-staging образ kupimknigi.spb.ru на snolla@0.42.0 (одностраничник, финал тиража). Per-task: `kupimknigi-deploy-snolla-0-42-0.md`.
**Status:** done — 2026-07-05. ✅ Собрал `registry.kzntsv.site/kupimknigi:9608ff6` (digest `ac7f846`, 583MB) на VDS из sha 9608ff6 (snolla 0.42.0/core 0.24.0/data 0.14.1), запушил. Создал **новый Portainer-стек Id 21 `kupimknigi`** (POST create/standalone, env verbatim 8/8 из стека 20 — тот же тенант) → контейнер **healthy** сразу, running==9608ff6. Staging-host kupimknigi.vds.kzntsv.site.
**Where I stopped:** GREEN. Staging-smoke с VDS пройден: `/`→200==prod; H1 «Скупка старых книг в Санкт-Петербурге…» идентичен prod; robots.txt 200 (healthcheck); тема-ассет `/themes/ef2c663c…/css/toolbox.css`→200 (MinIO); форма `action="/callback-order"` в HTML; callback-роуты паритет prod (`/callback-order/`→301 canonical, `/callback-order`→404 POST-only); байты 17717≈prod 17672. Форму не сабмитил (POST=реальное письмо клиенту) — роут зарегистрирован==prod, достаточно.
**Next action:** — (закрыто, ВКЛ. CUTOVER). ✅ **CUTOVER ВЫПОЛНЕН 2026-07-05:** оператор флипнул DNS reg.ru `kupimknigi.spb.ru` 80.64.31.36→89.253.255.94; подтвердил на ОБОИХ авторит.NS (ns1+ns2.reg.ru); вставил боевой `Host(kupimknigi.spb.ru)` в стек 21 (PUT 200), убрал staging-host. LE-серт выпущен (CN=kupimknigi.spb.ru, valid Jul5→Oct3 2026), боевой хост GREEN: /→200 H1 «Скупка старых книг…», robots/тема-css 200, callback canonical 301. **kupimknigi.spb.ru живёт на VDS.** RUVDS IIS 80.64.31.36 = rollback (revert DNS), НЕ тронут/декоммишн.
**Weight:** needs-claude · **Notify:** OpeItcLoc03/workshop
<!-- created-by: workshop / 2026-07-05 -->
---
## 🟡 [morethencms-s3-filestorage-provider] — S3-провайдер FileStorage для MoreThenCms (MinIO), закрывает split-brain админка↔фронт
**Status:** 🟡 in-progress — ТЗ (name `MoreThenCms.FileStorage.S3` одобрен) доставлено в инбокс MoreThenCms 2026-07-03; жду сборку от прогера. Owner-split: код — прогер; координация+deploy+приёмка — admin.
**Where I stopped:** Разведка 500 на `/admin/assets/.../delete` → корень RX-only ACL на `App_Data` (пофикшен `icacls …:(M) /T`, 44293 файла). `labtools-price.pdf` заменён в MinIO+локалке (MD5 сверены). ТЗ на провайдер: `~/projects/MoreThenCms/.claude-inbox/2026-07-03T06-40-09Z-admin.md`. Решения: все контентные классы, 1 MinIO (`minio.kzntsv.site`), ключ без ведущего слэша (parity), deploy → локальный IIS windows-recovery-host → боевой. DNS-аудит чист.
**Next action:** дождаться `MoreThenCms.FileStorage.S3.dll` → deploy на локальный IIS → сквозняк (upload→объект в MinIO→imgproxy 200, byte-parity) → deploy боевой + переключить `<fileStorageClients>`. См. [morethencms-s3-filestorage-provider.md](morethencms-s3-filestorage-provider.md).
**Branch:** n/a (admin ops + external code)
<!-- created-by: vitya@.admin-exec / 2026-07-03 / trigger: user-план «сайты→snolla, админка→локальный IIS» -->
---
## 🟡 [fix-nl-vds-reality-pq-dest] — Reality-инбаунд 443 на NL VDS (213.176.64.253): server-side done, ждёт client-side
**Status:** 🟡 server-side fix + cred-rotation применены и проверены 2026-06-05; остался один client-side шаг (user меняет SNI в v2rayN) + подтверждение → закрыть.
**Where I stopped:** ✅ dest `www.intel.com``www.microsoft.com` на инбаунде 443 (x-ui.db + restart), сквозной тоннель через реальный :443 (SNI microsoft) = HTTP 204; 32030 жив. ✅ Креды ротированы (root SSH pass / panel user+pass / panel secret-JWT — оригиналы светились в плейнтексте), всё в `pass nl-vds-3xui/full-env`, БД-бэкап на сервере. Root cause: ML-DSA-65 (PQ) × intel/Akamai (HRR), разбор в `.wiki/concepts/reality-pq-mldsa65-dest-incompatibility.md`.
**Next action:** user в v2rayN профиль `pqmkayaxo2`: SNI → `www.microsoft.com`, reconnect. Подтвердит, что Reality поднялся → 🟢 close. См. [fix-nl-vds-reality-pq-dest.md](fix-nl-vds-reality-pq-dest.md).
**Branch:** n/a (admin ops)
<!-- created-by: vitya@.admin-exec / 2026-06-05 / trigger: user-задача "не получается vless+Reality" -->
---
## ⚪ [kreknin-self-backup] — второй таргет для приёмника бэкапов (на потом)
**Status:** ⚪ backlog — заведена 2026-06-12 по итогам backup-gap аудита, user: «на потом».
**Where I stopped:** kreknin (`195.19.90.188`, `/volume1` 7 ТБ) — единственный приёмник всех 4 пайплайнов, сам не бэкапится → SPOF всей estate. Направление: второй облачный таргет (Backblaze B2 / Synology Hyper Backup, client-side encryption) для critical-subset (`*/latest` ~30 ГБ + `.hbk` vault).
**Next action:** при подъёме — выбрать B2 vs Glacier, настроить DSM Hyper Backup на subset. См. [kreknin-self-backup.md](kreknin-self-backup.md) + `.wiki/concepts/backup-inventory-2026-06.md`.
**Branch:** n/a (admin ops)
<!-- created-by: vitya@.admin-exec / 2026-06-12 / trigger: backup-gap аудит, MSSQL incident -->
---
## 🟢 [vehicles-loader-progress-deploy] — closed 2026-05-31 — 0.4.0 задеплоен + live-verify прошёл. Прогон Sun 31.05 06:21:48→07:53:14 MSK на changed-выгрузке: журнал показал движение по всем фазам (▶ branches/vehicles/units/delete-sweep, батч-прогресс `manufacturers 18/183…`, `units 3/26…`, `✓ done: N rows`, per-model delete-sweep), НЕ тишина. exit 0, `importRun id=4 ok`, `reportJson errors:[]`. **Email-баг найден+починен:** `STOSTAYER_MAIL_TO` слался сам себе (`site@stostayer.ru`) → исправлен на `vitya.kuznetsov@gmail.com` в host env (бэкап `.bak.20260531`), доставка на gmail подтверждена тест-письмом. Все 4 acceptance ✅. См. [vehicles-loader-progress-deploy.md](vehicles-loader-progress-deploy.md).
**Follow-ups (не блокеры, в stostayer.new):** (1) units-фаза 1h31m на 100529 rows — узкое место, батч-инсёрт/индексы; (2) generation `unmatched:51` — проверить не теряем ли данные; (3) `config/default.json` дефолт `to: site@stostayer.ru` выровнять (прод перекрыт env).
**Branch:** n/a
<!-- created-by: vitya@stostayer.new-session / 2026-05-30 / handoff: stostayer.new/.tasks/vehicles-loader-progress-logging.md -->
<!-- closed-by: vitya@.admin-exec / 2026-05-31 / live-verify journalctl прошёл + email-recipient баг починен+verified -->
---
## 🟢 [restore-elasticsearch-indices-books-vds] — closed 2026-05-28 — snapshot restore из `kreknin:daily-2026-05-25` за 46 сек (epz=820604, products=105922, artmone=2621, counts == source). Forensics: 5 индексов удалены через ES API `DELETE _all` 26.05 10:21 UTC, через 1ч 11мин после создания `bookva-es` — оператор в cutover-prep попал на canonical вместо internal-only bookva. Caller identity unrecoverable (audit log = X-Pack платный, traefik accessLog был выключен). 2 preventive фикса applied: ES env `action.destructive_requires_name=true` + traefik JSON accessLog в `/letsencrypt/access.log`. См. [restore-elasticsearch-indices-books-vds.md](restore-elasticsearch-indices-books-vds.md) § Closure + `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md`.
**Branch:** n/a
<!-- created-by: vitya@books-session / 2026-05-28 / trigger: prod 404 index_not_found_exception на /api/epz/search + /api/products/search -->
<!-- closed-by: vitya@.admin-exec / 2026-05-28 / snapshot restore + 2 preventive fixes verified -->
<!-- REOPENED+RECLOSED: vitya@.admin-exec / 2026-05-29 / рецидив — true RCA = exposed :9200 ransom-бот, Control #3 (порт закрыт) + re-restore + epz tenant-config fix -->
---
## ⚪ [harden-books-vds-exposed-ports] — закрыть публично торчащие host-порты на books VDS (89.253.255.133)
**Status:** ready
**Where I stopped:** exposure-audit сделан (ES :9200 уже закрыт в рамках инцидента); mongo/books-db/bookva-db/minio/bookva-minio + rsync:873 торчат на `0.0.0.0`, все credentialed (auth включён, не дыра-нараспашку как free-ES).
**Next action:** по каждому сервису определить — приложение коннектится через docker-сеть или host-порт; нужен ли внешний admin (тогда SSH-туннель / DOCKER-USER IP-allowlist вместо публичного порта). Начать с mongo (EOL 4.2 + reuse пароля).
**Branch:** n/a (admin ops)
<!-- created-by: vitya@.admin-exec / 2026-05-29 / trigger: exposure-audit во время ES ransom-incident — 5+ stateful-сервисов с публичными host-портами -->
---
## 🟢 [modulair-rag-vds-redeploy] — closed 2026-05-27 — 4-контейнерный стек развёрнут на VDS, acceptance 6/6
NAS помер → 4 контейнера пропали → fresh redeploy на VDS (89.253.255.94). Portainer stack id=15. **Acceptance 6/6 green:** 4 контейнера up без restart-loop; lightrag Uvicorn :9621; `https://modulair-mcp.kzntsv.site`→200; pipeline+tier1 0% CPU; pipeline-лог без postgres/minio ошибок.
Таска писалась по stale-предпосылкам — по ходу исправлено (см. [modulair-rag-vds-redeploy.md](modulair-rag-vds-redeploy.md) Decisions): registry-образы отсутствовали (registry переустановлен 2026-05-20) → **rebuild на VDS** (push 3.36GB tier1-слоя с дома падал 499 через traefik → собрал на самом VDS, push локальный); `MINIO_ENDPOINT`=`minio.vds.kzntsv.site` (не `minio.kzntsv.site`=books VDS); traefik entrypoint `https``websecure` (NAS-имя в compose-лейбле); DB+bucket+scoped-svcacct созданы; ROUTERAI key в pass; DNS поправил user.
**Follow-ups (не блокеры acceptance):** modulair-rag/compose.yml entrypoint-фикс в репо; portainer-stack.md под VDS; lightrag embedding binding=ollama/model=None — проверить ROUTERAI_* маппинг. Все переданы в handoff (consumer repo / parallel session).
**Branch:** master
<!-- closed-by: vitya@.admin-exec / 2026-05-27 / Portainer stack 15 deployed, acceptance 6/6 verified -->
<!-- created-by: vitya@modulair-rag-session / 2026-05-27 / handoff: user gave deploy to .admin (has VDS access) -->
<!-- created-by: vitya@modulair-rag-session / 2026-05-27 / handoff: user gave deploy to .admin (has VDS access) -->
---
## 🟢 [migrate-elasticsearch-to-books-vds] — closed 2026-05-25 — ES indices с Windows (elasticold.kzntsv.site, 7.10.1) → books VDS (elasticsearch.kzntsv.site, 7.10.0). 3 indices, 928616 docs. Reindex-from-remote через `reindex.remote.whitelist` env, добавленный в Portainer stack 33. Pre-migration snapshot в kreknin repo как rollback. Consumer configs sed'd, 3 containers restarted, smoke green. Source НЕ выключен.
См. [migrate-elasticsearch-to-books-vds.md](migrate-elasticsearch-to-books-vds.md) § Closure note.
## 🟢 [books-vds-stacks-to-portainer] — closed 2026-05-25 — 6 SSH-compose стеков на books VDS (89.253.255.133) мигрированы в Portainer-managed (endpoint 1, https://portainer.kzntsv.site). Skip traefik + portainer (management plane). Adapter `scripts/books-vds-portainer-migration/migrate.sh`. Wiki: [portainer-stack-management-books-vds.md](../.wiki/concepts/portainer-stack-management-books-vds.md). Backup pipeline bind paths preserved (verified by inspection, full run pending next 06:00 MSK).
## 🟢 [books-vds-backup-daily-kreknin] — closed 2026-05-25 — daily backup books VDS (89.253.255.133, host4g.ru, CentOS 7) → kreknin в 06:00 MSK. DB dumps (mariadb/mongo×2) + ES snapshot via REST → rsync 4.87GB → ntfy/email (phone+inbox ✓). ES path.repo bootstrap + snapshot repo `kreknin` registered. См. [books-vds-backup-daily-kreknin.md](books-vds-backup-daily-kreknin.md).
## 🟢 [unify-backup-notifications] — closed 2026-05-25 — единый формат push + email для VDS/RUVDS/windows-host (3 scripts), VDS run.sh импортирован в repo. См. [unify-backup-notifications.md](unify-backup-notifications.md) § Closed. windows-host self-deploy остаётся на user'е (elevated PS).
---
## 🟢 [bookva-tenant-cutover-prep] — closed 2026-05-26 — 7/7 (Step 6 descoped) + extension (bookva-minio + external port-bind)
Все ops-шаги VDS-side выполнены: Gitea secrets ✓ (7 шт), **10/11** Portainer bookva stacks ✓ (ozon-mcp deferred — image отсутствует в registry; bookva-minio добавлен post-closure), volumes ✓ (db+mongo cp -a, es+ntfy empty, 4 config volumes populated, bookva-minio-data cp с books bucket), login-gate SQL ✓ (bookva-db users id≥3 → UUID random pwd), DNS ✓ (pre-existed), books-web embed-api switch ✓, **external access** ✓ (bookva-db:33306 + bookva-minio:9001).
**Контекст dev-source:** victor/books `.wiki/concepts/tenant-split.md` rev v3. Code-side: `5e28fd1` embed-api+zod^4, `4a9cafc` ES endpoint, `c1e58cf` PWA redirect, `88df172` deploy.yml per-tenant. Overlay-repos: **bookva-overlay/main `8bffd68`** (templates aligned + mariadb:10.6 + mongo:4.2 + api internal-only + depends_on removed).
**Done в сессии 2026-05-26:**
- ✅ Step 1: 7 Gitea secrets (BOOKVA_OVERLAY_TOKEN, HEALTHCHECK_BOOKVA_{API,WEB}_URL, PORTAINER_STACK_ID_BOOKVA_{API=45,WEB=46,SCHEDULER=47,OPS_MCP=43}). books `2f7b539` (secret name fix).
- ✅ Step 2: 9/10 Portainer stacks. Stack IDs: db=34, mongo=36, es=37, ops-mcp=43, ntfy=44, api=45, web=46, scheduler=47, task-runner=48. ozon-mcp deferred (image not in registry). User-facing stacks (api/web/scheduler/task-runner/ops-mcp/ntfy) **stopped** post-create (Status=2) — bookseller.kzntsv.site returns 000 как desired pre-cutover state. db/mongo/es оставлены running (внутренние, useful для testing).
- ✅ Step 3: 6 volumes created. **bookva-db-data (4.7G)** = cp -a `/usr/docker/books-db/data` (downtime 59s). **bookva-mongo-data (517M)** = cp -a `/opt/books/job-scheduler/mongo/db` (downtime 5s). 4 config volumes populated из corrected overlay templates (api, scheduler, task-runner, web-branding). 2 empty by design (ntfy-data, es-data — Phase 2 snapshot/restore). MinIO deferred (no maintenance impact — `mc cp books bookva` at cutover, не rename).
- ✅ Step 4: bookva-db login-gate `UPDATE users SET password=UUID() WHERE id_user NOT IN (1,2)` — 3 users scrambled (id=3,4,5), id=1 (Bookva founder) + id=2 (Slovo founder) untouched. books-db verified unaffected.
- ✅ Step 5: DNS `bookseller.kzntsv.site` → 89.253.255.133 (был pre-existing, closed by inspection).
- ✅ Step 7: Portainer books-web stack 24 PUT atomic (compose+env). Env: `NUXT_PUBLIC_BOOKS_API_URL=/api`, `NUXT_AUTH_TOKEN`, `NUXT_JWT_SECRET_KEY`. Embed-api на bookva.kzntsv.site/api/* живёт (smoke green, auth identical to books-api). books `75ea320` (compose template synced). Soak 24-48ч до books-api shutdown micro-task.
- 🔵 Step 6: delayed по spec (за 1-2 нед до cutover — `NUXT_PUBLIC_BOOKVA_REDIRECT_URL=https://bookseller.kzntsv.site` env на books-web для PWA redirect banner user.id=1).
**Deferred (отдельной micro-task):**
- `bookva-ozon-mcp` stack — image `registry.kzntsv.site/books-ozon-mcp:master` не существует в registry. Нужен build в victor/books CI (ozon-mcp service в build.yml?). Скорее всего service ещё не настроен — отдельная task'а.
- Mongo agenda cleanup partial — 7 jobs с idSeller=2 удалены, осталось 2817 jobs (большинство — system-wide без idSeller). При cutover проверить если нужен дальнейший cleanup.
**Branch:** master | **Closure pushes:** books `2f7b539`+`75ea320` (master); bookva-overlay `5b173de``8bffd68` (main): templates align + mariadb:10.6 + mongo:4.2 + api internal-only + depends_on removed. Pass: `gitea/victor-books-ci-bookva-overlay`.
<!-- created-by: vitya / 2026-05-26 / Phase 2 cutover prep после tenant-split code-side done -->
<!-- progress-2026-05-26: 6/7 closed одну сессию: Steps 1+2+3+4+5+7. Step 6 delayed by design. bookva-ozon-mcp deferred (image build needed). MinIO bucket copy deferred (cutover-time) -->
---
## 🟢 [books-ops-mcp-host-promote] — closed 2026-05-26 — host-promote ops-mcp+docker-proxy в `.admin/host-stacks/books-vds/`, drop bookva-ops-mcp
Stack 26 (books-ops-mcp) in-place PUT без container churn (env MARIADB_PASSWORD preserved). Stack 43 (bookva-ops-mcp) deleted. Gitea secrets `PORTAINER_STACK_ID_BOOKVA_OPS_MCP` + `PORTAINER_STACK_ID_OPS_MCP` revoked. ops-mcp убран из tenant=slovo|bookva pipeline (deploy.yml). Management-plane: image updates через Portainer UI manual.
**Design locks (см. [books-ops-mcp-host-promote.md](books-ops-mcp-host-promote.md) § Closure note):** Q1 location=`.admin/host-stacks/`, Q2 deploy=manual Portainer, Q3 MariaDB scope=slovo's only (Option A), Q4 config volume path unchanged, Q5 audit прочих host-level кандидатов punt.
**Commits:** `victor/books ae3ab14` + `victor/bookva-overlay 50f5bbb` + `.admin` (this commit).
**Branch:** master
<!-- created-by: vitya / 2026-05-26 / trigger: deploy run#437/439 fail + user ideology clarification -->
<!-- closed-by: vitya / 2026-05-26 / Portainer in-place PUT stack 26 + delete stack 43 + 2 secrets revoked + 3 commits -->
---
## ⚪ [stateful-split-volume-copy] — поднять `bookva-db` + `slovo-db` на VDS как копии `books-db` через `cp -a` volume
Ops-таска для Фазы 1 дизайна `tenant-split` из victor/books. Scope сужен 2026-05-25 (user: «просто поднимем 2 БД»): только MariaDB volume copy + up 2 контейнеров. Mongo / MinIO / DELETE / app-стеки — отдельными ops-тасками потом.
**Контекст dev-source:** дизайн в victor/books `.wiki/concepts/tenant-split.md` § «Фаза 1». Compose-файлы готовы в `bookva-overlay` / `slovo-overlay` (commit d0eb210 в books).
**Acceptance:** `docker ps` показывает живые `bookva-db` + `slovo-db`, оба отвечают `SELECT 1`. Текущий `books-db` стек снова в строю после maintenance window.
**Status:** ready
**Where I stopped:** (not started — lean playbook готов в [stateful-split-volume-copy.md](stateful-split-volume-copy.md))
**Next action:** под maintenance window 5-10 мин на VDS — execute 5 команд из task-файла. Backup → stop books-db → cp -a в 2 volume'а → up 2 новых контейнера через Portainer → start books-db.
**Blocker:**
**Branch:** master
<!-- created-by: vitya / 2026-05-25 / moved from victor/books .tasks/ (scope is ops/VDS, not books-code) -->
---
<!--
Status legend:
🔴 Active — only one at a time
🟡 Paused — in progress, resumable
⚪ Ready — defined, not started
🟢 Done — moved to ARCHIVE.md once committed
🔵 Blocked — waiting on external input
-->
## 🟢 [iis-migration-to-ruvds] — closed 2026-05-25 per user decision — Phase 1 done: RUVDS infra setup + 8.66GB scp + IIS recreate + 25 HTTPS SNI bindings (LE R13 expire 2026-07-22) + 9 hostnames (kupimknigi.spb.ru, emspb±www, pilorama98±www, labtools.pro±www, rimiz±www) live на 80.64.31.36 with correct per-tenant content. 16 hostnames остаются на windows source per user pace (incl. tandemmebel scope-exception). Source IIS:8089 + traefik routes ALIVE для rollback. Decommission + LE renewal + cleanup descoped в Closure note. См. [iis-migration-to-ruvds.md](iis-migration-to-ruvds.md) § Closure note.
<!-- HISTORICAL details preserved below — task closed, content kept до prune'а board'а -->
**Status:** closed 2026-05-25
**Where I stopped:** 2026-05-24 — `kupimknigi.spb.ru` + `emspb.ru` (+ `www.emspb.ru`) DNS A flipped на `80.64.31.36`, authoritative `ns1.reg.ru` правильный, public resolver caches expire'ятся (8.8.8.8=~6h, 1.1.1.1=~24h max). RUVDS state: snolla site (8.66 GB / 44725 files) transferred + IIS recreated + 25 HTTPS SNI bindings c LE certs (R13, valid до 2026-07-22), 7 prod hostnames live-smoke через VDS (третья сеть) → 200 OK / correct content. Source IIS:8089 + traefik routes ALIVE — rollback ready. **2026-05-25 close-time DNS probe:** ещё 5 пар hostnames swap'нуты user'ом silently — итого 9 на RUVDS, 16 на source.
**Findings зафиксированы в** [iis-migration-to-ruvds.md](iis-migration-to-ruvds.md) Decisions log: (1) outbound 445 блокирует home ISP, не RUVDS-FW → SSH/scp = canonical transfer-метод; (2) home network HTTP-middlebox mangles Host header for direct external HTTP — real end-users не пострадают, тестировать через VDS; (3) traefik acme.json → IIS PFX recipe работает (extract + openssl pkcs12 -export + Import-PfxCertificate + AddSslCertificate by thumbprint); (4) IIS 10 HTTP/2 default; (5) `maljarka.tandemmebel.ru` + 3 rimiz hostnames → 502/404 — pre-existing CMS-tenant config gap, не migration defect.
**Scope exception (2026-05-24 user decision):** `tandemmebel.ru` + `www.tandemmebel.ru` ОСТАЮТСЯ на windows-IIS на неопределённый срок (отдельное решение user'а — site не готов к cutover ровно сейчас). DNS НЕ свапать. Cert на RUVDS уже импортирован, binding existing — может оставаться idle, traffic не пойдёт.
**Next action:**
1. **Снизить DNS TTL** в reg.ru на оставшиеся **22 hostnames** до 300s — `pilorama98.ru`, `labtools.{ru,pro}`, `snolla.com` + 11 snolla subdomains + `rimiz.{ru,snolla.com}` + `maljarka.tandemmebel.ru` (но не `tandemmebel.ru` — см. Scope exception). Сократит cache-tail с 24h до 5 мин.
2. **24h soak kupimknigi + emspb** — verify через cache-clean resolver (потом curl без `--resolve`).
3. **Bulk DNS swap** оставшихся 22 hostnames на 80.64.31.36 (single sitting). **tandemmebel.ru / www.tandemmebel.ru — пропустить.**
4. **1-week prod soak** с RUVDS как live source для migrated hostnames.
5. **Decommission source IIS:8089** только для migrated hostnames (snolla catch-all site нельзя decommission'ить пока tandemmebel.ru на нём же). Решение defer до tandemmebel migration.
6. **LE renewal pipeline** — win-acme + HTTP-01 на RUVDS после full cutover (LE certs expire 2026-07-22, soak window до ~07-15).
7. **Cleanup migration-temp:** `Remove-NetFirewallRule 'smb-from-source','ssh-from-source'` на RUVDS, удалить `~/.ssh/ruvds-iis-migration*` на source, очистить `C:\ProgramData\ssh\administrators_authorized_keys` на RUVDS.
Полный план + Completed + Open questions + Remaining steps — в [iis-migration-to-ruvds.md](iis-migration-to-ruvds.md).
**Branch:** master
<!-- created-by: user-decision / 2026-05-22 / trigger: RUVDS purchased -->
<!-- partial-cutover-by: vitya / 2026-05-24 / kupimknigi + emspb DNS flipped, 22 hostnames pending, tandemmebel.ru excluded -->
<!-- scope-narrowed: vitya / 2026-05-24 / tandemmebel.ru stays on windows-IIS indefinitely -->
**Branch:** master
---
## ⚪ [infra-inventory] — two-tier инвентаризация: public-карта в global wiki + admin-detailed runbook локально
**Status:** ready
**Where I stopped:** (not started — создана 2026-05-22 из инцидента `gitea.` vs `git.` + перепутанный source/dest IP в traefik access-логах; scope расширена в тот же день — single-page → two-tier per user «у все общее представление, у админа полное»)
**Next action:** см. полный план в `infra-inventory.md`. Шаги: (1) probing-фаза одна для обоих — `nslookup` известных subdomain'ов + `vds-ops`/`synology-ops` `ops_docker_ps`/`inspect` + `knowledge_search` по существующим bootstrap-concept'ам + `rg "kzntsv.site"` по `~/projects/`. (2) Write A — light public в `projects-wiki/concepts/infrastructure-inventory.md` (машины, canonical hostnames, anti-aliases) → `knowledge_ingest`. (3) Write B — heavy admin в `.admin/.wiki/concepts/infrastructure-stack.md` (per-stack: версии, internal IPs, volumes, depends-on, backup paths, DR pointers) → commit + push. (4) Cross-refs A↔B + pointer в `.admin/.wiki/CLAUDE.md` Domain conventions.
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: .workshop / 2026-05-22 / trigger: gitea-hostname-confusion-incident -->
<!-- scope-expanded: 2026-05-22 — single-page → two-tier (public + admin-detailed) -->
---
## 🔵 [books-vds-bookva-bootstrap] — Ops-таска для Фазы 4 дизайна `tenant-split` из victor/books (см. `mcp__projects-meta__knowledge_get slug=concepts/tenant-split`). Поднять новый VDS для Bookva — billing на юрлицо Bookva (учредители развелись, каждое юрлицо платит инфру напрямую провайдеру).
**Контекст dev-source:** дизайн в victor/books `.wiki/concepts/tenant-split.md`; brainstorm trace `~/projects/.workshop/.archive/2026-05-24-books-tenant-split.md`.
**Direction:** Slovo остаётся на текущем VDS, Bookva переезжает на новый.
**Acceptance:**
- Новый VDS у провайдера (Rusonyx или другой по предпочтению Bookva), billing-account оформлен на юрлицо Bookva.
- Docker + traefik + certbot развёрнуты.
- DNS `*.bookva.<tld>` готов резолвиться на новый IP (фактический switch — в `books-dns-cutover-bookva`).
- SSH-ключи: только инженерные (core-разраб + админ Bookva, если есть). Аналитики Bookva — НЕ имеют SSH к новому VDS.
**Status:** blocked
**Where I stopped:** (not started)
**Next action:** **Pre-step:** прочитать `concepts/tenant-split.md` § «Фаза 4» в victor/books (через `mcp__projects-meta__knowledge_get slug=concepts/tenant-split` или git clone victor/books → `.wiki/concepts/tenant-split.md`).
1. Согласовать с учредителем Bookva: provider, configuration (CPU/RAM/disk), регистрация billing'а на юрлицо Bookva.
2. Provision VDS, базовая настройка (firewall, fail2ban, SSH ключи).
3. Установить Docker + docker-compose.
4. Развернуть traefik + certbot из overlay-репо `victor/books-bookva/deploy/traefik.compose.yml`.
5. Подготовить DNS-зону `bookva.<tld>` (создать A-records, TTL=300 для возможности быстрого switch'а).
6. Smoke: `curl https://placeholder.bookva.<tld>` → 200 от nginx-placeholder или из traefik.
7. Документировать в `victor/books-bookva/README.md`: hostname, IP, как ssh, runbook smoke-теста.
8. Закрыть с note: «VDS поднят, IP=<...>, готов к stack deploy».
**Blocker:** `OpeItcLoc03/admin books-stateful-split-execution` (Фаза 1 — physical split на shared VDS, bookva-* контейнеры живут) + `victor/books per-tenant-backup-and-observability` (Фаза 2 — per-tenant backups настроены до физического переезда)
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-24T12:02:19.507Z -->
---
## 🔵 [books-dns-cutover-bookva] — Ops-таска для Фазы 4, шаг 5 дизайна `tenant-split` из victor/books. DNS switch `*.bookva.<tld>` → новый VDS IP, с parallel-run старого bookva-стека на shared VDS 7 дней без публичного hostname.
**Контекст dev-source:** дизайн в victor/books `.wiki/concepts/tenant-split.md` § «Фаза 4».
**Acceptance:**
- DNS `*.bookva.<tld>` (или конкретный hostname Bookva — определяется учредителем) указывает на новый VDS IP.
- Старый bookva-стек на shared VDS остаётся запущенным, **без публичного hostname** (traefik labels убраны или disabled), доступен только по docker-сети для возможного отката.
- Smoke с двух разных сетей (mobile + офисный wifi) — `https://<bookva-hostname>` отвечает с нового VDS.
- Per-VDS backup'ы на новом VDS подтверждены — есть snapshot спустя 24ч после DNS-switch'а.
- Через 7 дней parallel-run без откатов — decommission старого bookva-стека (см. Фаза 4, шаг 7 в spec).
**Status:** blocked
**Where I stopped:** (not started)
**Next action:** **Pre-step:** прочитать `concepts/tenant-split.md` § «Фаза 4, шаги 47» в victor/books.
1. Maintenance window 12ч (объявить за 7 дней).
2. Финальная синхронизация: `mysqldump mariadb-bookva` на текущем VDS → `mysql` на новом VDS (если schema/data разошлись с момента Фазы 2).
3. Smoke на новом VDS до DNS-switch'а: hosts-файл override → проверить login + base flow.
4. DNS-switch: меняем A-record(s) для bookva-hostname на new IP. TTL=300 уже стоял (Phase 4 step 1).
5. Через 1ч (TTL expiry + propagation): smoke с двух сетей.
6. На старом VDS — отключить traefik labels для bookva-стека (контейнеры продолжают работать, но не доступны снаружи).
7. **Parallel-run 7 дней.** Каждый день — smoke. Per-VDS backup на новом VDS — проверить хотя бы 1 успешный snapshot.
8. Если за 7 дней проблем нет → закрыть с note «cutover stable». Если есть — DNS rollback на старый IP, follow-up debug task в victor/books.
9. **Decommission** старого bookva-стека на shared VDS (отдельный шаг, не в этой таске — финальный backup в архив + `docker compose down` + volume drop).
**Blocker:** `books-vds-bookva-bootstrap` + victor/books `pwa-redirect-handoff` (SW bump выкачен в shared API за 12 недели до этой таски).
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-24T12:02:41.503Z -->
---
## ⚪ [books-bookva-user-whitelist-gathering] — **Процессная** ops-таска для Фазы 3, шаг 1 дизайна `tenant-split` из victor/books. Получить от учредителя Bookva письменный список пользователей, которые допущены к Bookva-инсталляции. Без этого списка cutover не возможен — иначе утечка доступа: либо лишние юзеры получат доступ к Bookva (через копирование «всех users»), либо легитимные юзеры заблокированы (через пустой whitelist).
**Direction:** Bookva — новая инсталляция, начинается с whitelist'а (закрытый список). Slovo — остаётся на текущем стеке, получает копию всех текущих users (статус-кво).
**Контекст dev-source:** дизайн в victor/books `.wiki/concepts/tenant-split.md` § «Двухуровневая изоляция пользователей → App-level» и § «Фаза 3, шаг 1».
**Не блокируется ничем** — можно начинать gathering сразу, лаг до cutover'а может быть значительным.
**Acceptance:**
- Письменный список от учредителя Bookva (email или подписанный документ): ФИО + role (если знают).
- Список зафиксирован в `OpeItcLoc03/admin/.wiki/concepts/books-bookva-whitelist.md` или в отдельной таблице.
- Для каждой строки списка сверка с текущей `users`-таблицей shared БД: user существует / роль соответствует / email актуален.
- Любые расхождения (запрошенный user не существует в shared / роль другая) — обработаны: создать новый user в Bookva-БД post-cutover ИЛИ скорректировать список с учредителем.
**Status:** ready
**Where I stopped:** (not started)
**Next action:** 1. Отправить учредителю Bookva формальный запрос (email/мессенджер): «Для tenant-split нужен список пользователей, которые останутся в Bookva после разделения. Формат: ФИО + роль + email/login. Список будет применён на cutover'е как whitelist».
2. Дождаться ответа (это может занять дни/недели — нормально).
3. Получив список — сверить с `users`-таблицей shared БД:
```sql
SELECT id, login, email, role FROM users WHERE email IN (<list>) AND deleted_at IS NULL;
```
4. Разобрать расхождения с учредителем (missing / role mismatch / inactive).
5. Зафиксировать финальный список в `OpeItcLoc03/admin/.wiki/concepts/books-bookva-whitelist.md` (или в local-only encrypted file если NDA-чувствительно).
6. Финальный список становится input'ом для Фазы 3, шаг 1: при настройке Bookva-БД в неё попадают **только** эти users.
7. Закрыть с note: «whitelist (N users) подтверждён учредителем, готов к применению на cutover'е».
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-24T12:03:18.548Z -->
## 🔵 [books-stateful-split-execution] — Ops-таска для **Фазы 1** дизайна `tenant-split` из victor/books. Выполнить playbook на shared VDS в maintenance window — docker volume copy для MariaDB + Mongo + MinIO bucket rename + DELETE cleanup. Identical bit-for-bit копии через `cp -a` alpine helper. Никаких mysqldump/mongodump/mc cp.
**Playbook (dev-source, читать целиком до начала):** `victor/books/.tasks/stateful-split-volume-copy.md`.
**Контекст дизайна:** `mcp__projects-meta__knowledge_get slug=concepts/tenant-split` § «Фаза 1 — Physical stateful split + overlay-repos». Brainstorm trace: `~/projects/.workshop/.archive/2026-05-24-books-tenant-split.md`.
**Container naming:** drop `books-` prefix → `bookva-<svc>` / `slovo-<svc>`.
**TDD-режим:** `[skip-tdd: one-off migration on maintenance window]` — verify через post-cleanup audit.
**Acceptance:**
- `bookva-db` + `slovo-db` контейнеры живые, audit `SELECT DISTINCT idSeller` = `{1}` / `{2}` соответственно.
- `bookva-mongo` + `slovo-mongo` контейнеры живые, agenda-история preserved, jobs чужого tenant'а cleanup'нуты.
- MinIO (один shared) имеет bucket'ы `bookva` + `slovo` вместо `books`; объекты чужого tenant'а удалены.
- Shared catalog tables (`books`, `editions`, `epz`, `epz_pages`, `warehouses`, `products`) — полные копии в обеих БД.
- Smoke с двух сетей: login через `bookva.<tld>` → Bookva-данные; текущий URL → Slovo.
- Через 7-14 дней parallel-run без откатов → cleanup старых `books-*` контейнеров и volumes (отдельный заход).
**Status:** blocked
**Where I stopped:** (not started)
**Next action:** **Pre-step:** прочитать `victor/books/.tasks/stateful-split-volume-copy.md` целиком. Убедиться что 7 open questions resolved (закрыто dev-prep таской `victor/books stateful-split-volume-copy`): FK CASCADE, units/receipts strategy, MinIO key structure, users whitelist, task_runs handling, chrome/traefik shared.
1. Подтвердить overlay-репо `victor/bookva-overlay` + `victor/slovo-overlay` готовы — compose-файлы для `bookva-*` / `slovo-*` стеков на месте.
2. Подтвердить per-tenant backups настроены (`victor/books per-tenant-backup-and-observability`) — иначе нет точки rollback.
3. Объявить maintenance window 1-2ч (без записи).
4. Execute Steps 0-11 из playbook на shared VDS.
5. Post-execution audit: `SELECT DISTINCT idSeller FROM <each>`, `db.agendaJobs.distinct('data.idSeller')`, `mc ls` on MinIO.
6. Smoke с двух сетей.
7. Через 7-14 дней parallel-run без откатов → cleanup старых `books-*` контейнеров+volumes (отдельный заход).
8. Closing note: «Physical split done, bookva-* + slovo-* контейнеры живут на shared VDS, audit clean, parallel-run start <date>».
**Blocker:** `victor/books stateful-split-volume-copy` (dev-prep — 7 open questions resolved + dev-DB test passed) + `victor/books per-tenant-backup-and-observability` (Фаза 2 — per-tenant backups настроены)
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-05-25T04:46:48.271Z -->
---
## 🟢 [vehicles-loader-image-distribution] — closed 2026-05-29 — канал поставки решён: build здесь → push в **собственный registry клиента** `docker.stostayer.ru` → host pull + re-tag `:latest`. **Без Portainer** (oneshot не stack + Portainer cron не умеет; systemd-timer + `docker run --rm` сохраняет `OnFailure=`). Build здесь = депы (вкл. приватный verdaccio) запекаются в образ, клиенту verdaccio/yarn/node не нужны. Финализированы `deploy/README.md` § Image distribution + дизайн-док (вопрос #4 закрыт). **Commit:** stostayer.new `e55cfba`. **Prod-деплой ВЫПОЛНЕН 2026-05-29** (stostayer.new `ddda63f`): образ `docker.stostayer.ru/vehicles-loader:0.3.0` собран здесь+запушен, env `/etc/stostayer/vehicles-loader.env` (600 root), systemd-timer enabled (next 2026-05-30 06:20 MSK). **Гочи по ходу (обе в `stostayer.new/.wiki/concepts/client-infra-access.md`):** (1) MariaDB — под `--network host` наследуется хостовый `/etc/hosts` (`www→127.0.0.1 И ::1`), Node резолвит `::1` первым, mariadb слушает только IPv4 → timeout. Фикс: `STOSTAYER_DB_HOST=127.0.0.1`. (2) Контейнерный DNS сломан (Docker переписывает resolv.conf на публичные, исходящий :53 зафайрволен; `--add-host` под host-net игнорится) → email-отчёт на `mail.stostayer.ru` не резолвился. Фикс: `-v /etc/stostayer/container-hosts:/etc/hosts:ro` (свой hosts с пином mail-IP).
**Первый боевой прогон 2026-05-30 06:20:** sync УСПЕШЕН (`importRun id=3 ok`, ~84мин, units.json изменился относительно baseline), но упал на email (DNS, см. гочу #2) → systemd `failed` + ложный OnFailure. **Данные залились корректно.** Email-фикс применён и проверен `nodemailer.verify()` → `SMTP_VERIFY_OK` (DNS+TCP+STARTTLS+AUTH). Юнит redeployed, failed сброшен, next run Sun 06:21. Доки в stostayer.new `8e5997c`. **Заведена ⚪ `vehicles-loader-progress-logging`** (stostayer.new) — лог тонкий, ~2ч sync молчал. **Остаточное:** живой email-SEND в составе sync проверится на следующем changed-прогоне (verify доказал транспорт); OnFailure-юнит шлёт через хостовый `mail` — не проверен.
<details><summary>исходный scope (decision-task)</summary>
vehicles-loader (stostayer) деплоится в Docker у клиента (lite docker-deploy, дизайн залочен 29.05, см. stostayer.new `.wiki/concepts/vehicles-loader-docker-deploy.md`). Нужно решение по каналу поставки образа клиенту — единственный открытый вопрос #4, блокирующий финализацию Dockerfile CMD + deploy/README.
Развилка:
- **A (рекомендация brainstorm'а):** мы билдим образ → push в docker registry VDS → клиент `docker pull` по pinned-тегу. Rollback = перетыкнуть тег. Клиенту не нужен monorepo + yarn berry build-toolchain. Требует: у клиента docker login creds + сетевой доступ к нашему registry.
- **B (fallback):** клиент билдит из git у себя. Требует VCS-доступ + node22 + yarn berry **+ доступ к verdaccio** (`verdaccio.kzntsv.site`) с валидным токеном на хосте клиента. Причина: `.yarn/cache` в `.gitignore` (не коммитится), build идёт offline против cache → свежий `git clone` имеет пустой cache → `yarn workspaces focus` падает, пока клиент не прогонит `yarn install` против приватной регистры. Тяжелее, медленнее rollback, +зависимость от приватной регистры.
Образ: multi-stage node:22-alpine, `yarn workspaces focus @stostayer/vehicles-loader --production` (плагин `@yarnpkg/plugin-workspace-tools` закоммичен в stostayer.new `.yarn/plugins/`).
> **UPDATE 2026-05-29 (из stostayer.new session):** lite docker-deploy CORE зашипан (10/10 tasks на master). **Dockerfile CMD уже финализирован** (`ENTRYPOINT ["node","index.js"]` + `CMD ["sync"]`, source через env `STOSTAYER_SOURCE_ZIP` дефолт `/var/from_1c/export_from_1c.zip`) — эта часть блокера СНЯТА. Остаётся ТОЛЬКО: (1) решение по каналу поставки образа; (2) distribution-секция `deploy/README.md` (помечена PENDING на эту таску). **A build-proven:** образ собран локально (`docker build` ок) и запускается (`docker run --rm vehicles-loader:dev --help` ок). С учётом verdaccio-зависимости B (выше) — **A ещё предпочтительнее**: при A клиенту verdaccio не нужен вообще (cache наполняется нашим authed `yarn install` при сборке).
**Status:** closed 2026-05-29
**Resolution:** канал = собственный registry клиента `docker.stostayer.ru` (не наш VDS-registry, не build-from-git). Развилка пересмотрена при открытии `pass stostayer/client` — у клиента своя инфра (Portainer + registry). Build остаётся у нас (verdaccio-депы запекаются). Без Portainer для запуска (oneshot). Доки финализированы в stostayer.new `e55cfba`.
**Branch:** n/a
</details>
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/stostayer.new / 2026-05-29T10:17:29.967Z -->
<!-- closed-by: vitya@.admin-exec / 2026-05-29 / decision: client-registry + build-here + no-Portainer; deploy/README + design-doc finalized (stostayer.new e55cfba) -->
---
## 🟡 [agents-task-runner-vds-deploy] — Deploy agents-task-runner (the poller) on WORKER machine(s) — per-machine federation (agent-orchestration-without-user Q9). Each worker runs its own runner, claims from the SHARED Gitea board, runs the agent LOCALLY, commits/pushes. VDS hosts the Gitea board ONLY (already up) — it is NOT a worker, nothing deploys on VDS. [Slug legacy-named '-vds-deploy'; scope corrected 2026-06-08, VDS-as-worker DROPPED.] Runtime code deploy-ready (poller registered in tasks.json, DRY_RUN + requirements-filter + confirm-gate built; compose+Dockerfile). Credentialed per-machine setup (ops).
**Status:** paused
**Where I stopped:** 2026-06-08: DRY_RUN провалидирован; `.admin` claim-guard (`policy.toml` default_weight=needs-human) запушен+верифицирован (все `.admin`-таски → needs-human skip). Always-on bring-up начат (docker mongo+reconciler) затем **снесён по запрету user'а «не запускай поллеров без разрешения»** — host task-runner НЕ стартовал, ни одного claim/spawn. **GATE: запуск поллеров ждёт явного гранта user'а.** Попутно поймана+закрыта stale-claimable `cms-maljarka-https-mode-bug-fix` на MoreThenCms board (баг починен сегодня, таска висела открытой — без паузы always-on заспавнил бы агента на уже-решённое). Конфиг (.env/secrets.env/policy guard) готов. См. [agents-task-runner-vds-deploy.md](agents-task-runner-vds-deploy.md).
**Next action:** (0) BUILD PREREQ (from consult-execution-policy-governance review-outcome, OpeItcLoc03/common): repo tracks TS source only — `dist/` is gitignored. After pulling OpeItcLoc03/common on each worker, run `npm run build` (tsc) in BOTH lib/projects-meta-mcp AND lib/agents-task-runner, then (re)start the runner; otherwise the execution-policy governance lever (consult_policy + needs-human claim-gate, runner v0.16.x) is INERT — the old compiled JS keeps running. (1) Pick first worker machine (workstation / factory Linux). (2) Install + run agents-task-runner there, pointed at the shared Gitea board. (3) Secrets PER MACHINE: ANTHROPIC_API_KEY (headless) or `claude login` + Gitea token + projects-meta config; 600 root, not in git. (4) Capability filter: claims only what that machine can do (requirements/runtime_allowed); cheap model (sonnet/haiku) for impl polling. (5) HARD: .admin excluded from autonomous claim until governance L2/L3 lands (see blocker). (6) DRY_RUN on prod board first → then one live cycle on a real non-.admin task → spawn→commit→push, verified. (7) anti-self-review: distinct machine-id per worker.
**Branch:** n/a
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-06-08T10:10:16.420Z -->
---
## 🟢 [migrate-gallery-originals-to-s3] — closed 2026-06-12 (scope: pilorama98) — путь A: создан bucket `galleries`, залиты 301 файл (77 MiB) legacy-оригиналов pilorama98 с RUVDS IIS `App_Data\galleries\37e6…` → `s3://galleries/37e67fc44b9e4c06a522a26a73bac9b0/<guid>.jpg`. Smoke с books-vds: реальный объект через imgproxy → 200 image/webp 32102 B, fake guid → 404. Код snolla НЕ менялся (storageClient=имя бакета — рабочая конвенция). Inbox victor/snolla отправлен. Развилка A/B закрыта фактами MinIO: galleries в legacy = Local storage class (диск), никогда не в S3; A выбран user'ом.
Галерейные (blog/gallery) картинки не отдаются ни на dev (imgproxy 404 "Source image is unreachable"), ни на prod (legacy-ресайз 404). Диагностика проведена в victor/snolla против живого pilorama98 + кода MoreThenCms. Нужен доступ к БОЕВОМУ серверу (локальный диск IIS + MinIO/S3), которого нет у snolla-агента — поэтому задача на admin.
## Диагноз (доказан, не гипотеза)
Корень: **галерейные оригиналы лежат ТОЛЬКО на локальном диске legacy-IIS** (`App_Data\galleries\<siteId:N>\<storageFilename-guid>.<ext>`). В S3/MinIO их нет. Продукты — мигрированы (`s3://pilorama98/products/...` отдаёт 200), галереи — нет.
Три независимых следствия одного корня:
1. **Prod не отдаёт картинки блога.** Legacy-роут `/galleries/<galleryId:N>/images/<size>/<seq>.jpg` — ресайзер на лету. Распиновка живого pilorama98 (2 галереи, 98078b74… и c1b53362…):
- `original/001.jpg` → **200** (235KB / 735KB) — оригинал жив, читается с локального диска;
- `800x450`, `default`, `thumbnail`, `100x100`, `600x400` → **404** всегда.
→ ресайз-пайплайн legacy-хоста мёртв глобально (System.Drawing / image-cache storage). Это отдельная legacy-поломка; целевая архитектура — отдавать через imgproxy, не чинить legacy-ресайз.
2. **Превью в админке видно.** Админка рендерит ОРИГИНАЛ (passthrough по imageId → `GetGalleryImageById`, без ресайза), читает локальный диск. Тот же путь, что `original/001.jpg`=200. Сломанную ресайз-ветку не трогает.
3. **Dev imgproxy 404.** snolla строит source `s3://${storageClient}/${siteId}/${storageFilename}` (packages/snolla/middleware/galleries.js:47 и lib/viewModels/index.js:289). Ключ-форма КОРРЕКТНА (реальный StorageFilename guid + siteId, ровно legacy-раскладка). Но imgproxy читает S3, а объекта там нет → "Source unreachable". Код билдера менять НЕ надо.
## Развилка, которую закрыть на боевом (нужен доступ к MinIO)
Имя бакета. Продукты: бакет `pilorama98`, prefix `products/`. snolla для галерей берёт `image.storageClient` («galleries») как ИМЯ БАКЕТА → `s3://galleries/...`. Проверить в MinIO:
- (A) целевой бакет реально `galleries` (отдельный) → достаточно залить файлы, код snolla без изменений;
- (B) всё в бакете `pilorama98`, галереи должны быть под prefix `galleries/` → тогда + маленький код-фикс на стороне snolla (storageClient как prefix, а не bucket) — в этом случае ЗАВЕСТИ follow-up таску обратно на victor/snolla через notify, НЕ чинить snolla из admin.
## Acceptance criteria
1. Определено фактическое состояние MinIO: какие бакеты есть, как лежат рабочие продукты, отсутствуют ли галерейные объекты — зафиксировано в close-note.
2. Развилка A/B закрыта явно (какой бакет/prefix — целевой).
3. Если A: legacy-оригиналы залиты в S3 под ключи, которые snolla уже ждёт (`<siteId>/<storageFilename>`); проверена доступность через imgproxy (хотя бы одна картинка → 200).
4. Если B: создана follow-up таска на victor/snolla с точной целевой формой ключа (bucket + prefix), миграция файлов под этот prefix выполнена.
5. В inbox victor/snolla при close: итоговая форма source (bucket/prefix/key), какие галереи/сколько файлов мигрировано, и нужен ли код-фикс на snolla.
## Контекст-источники (для воспроизведения)
- Живой prod: `https://www.pilorama98.ru/galleries/98078b74e062443ba0708476e031b6b0/images/original/001.jpg` (200) vs `/800x450/001.jpg` (404).
- Legacy-код: MoreThenCms/Galleries/GalleryImage.cs (Path/StorageFilename), GalleriesAppFunc.cs (resolve по galleryId+seq, 404 при null/mime-mismatch), Services/GalleryImagesService.cs:135 (StorageClient="galleries", filename=Guid.NewGuid():N), FileStorage.Local/GalleriesStorage.cs (BasePath App_Data\\galleries\\<siteId>\\).
- snolla-консьюмер: packages/snolla/middleware/galleries.js, lib/viewModels/index.js:289.
- siteId pilorama98: 37e67fc4… (бакет-сегмент в source).
## Обязательные скилы — вызвать до начала работы
- invoke `systematic-debugging` — закрыть развилку A/B по фактам MinIO до любых действий
- invoke `using-tasks` — управление статусом задачи
- invoke `project-discipline` — дисциплина коммитов/пушей
- invoke `using-vds-ops` — если MinIO/imgproxy крутятся на VDS (docker-диагностика)
- invoke `using-projects-meta` — кросс-проектный отчёт/ follow-up в victor/snolla
**TDD:** нет — ops/migration задача (нет тестируемого кодового ядра; если всплывёт код-фикс на snolla — он уйдёт отдельной impl-таской на victor/snolla с TDD).
**Разрешения:** интерны: нет | автопуш: да
**weight:** needs-human
**notify:** victor/snolla
**Status:** done (closed 2026-06-12, scope pilorama98)
**Where I stopped:** мигрировано. MinIO (books-vds стек 30, за `imgproxy.kzntsv.site` с 08.06): bucket `galleries` создан, prefix `37e67fc44b9e4c06a522a26a73bac9b0/` залит (301×.jpg, 77.0 MiB, rclone RUVDS→minio.kzntsv.site, count+size == source). Smoke OK с books-vds (200/404).
**Next action:** — (хвосты в inbox victor/snolla: остальные siteId-папки галерей не мигрированы по скоупу; legacy prod-ресайз остаётся 404 by design).
**Branch:** master
**Notify:** victor/snolla — ✅ inbox `.claude-inbox/2026-06-12T11-47-33Z-admin.md`
**Closure note (2026-06-12):**
- **Развилка A/B закрыта фактами:** в Web.config legacy `<fileStorageClients>` storageClient `galleries` = тип **Local** (`GalleriesStorage`, диск `App_Data\galleries\<siteId>`), НЕ S3 → галереи никогда не были в S3 (продукты — да, bucket `pilorama98/products`). Корень доказан. **A** (отдельный bucket `galleries`, key `<siteId>/<storageFilename>`) выбран user'ом → нулевой код-фикс snolla.
- **MinIO state:** bucket'ы до миграции — artmone/books/imgproxytest/manuals/modulair/modules/obsidian/ozon/pilorama98/strapi/test/upload; `galleries` отсутствовал; `pilorama98` = только `products/` (шард по hex, без siteId). Зафиксировано.
- **Залив:** RUVDS IIS `C:\sites\snolla\App_Data\galleries\37e67fc44b9e4c06a522a26a73bac9b0\` (301 файл, dir-name уже lowercase 32-hex == ownerId snolla) → `s3://galleries/37e6…/<guid>.jpg` verbatim. rclone v1.74.2 inline s3-remote (env-var config, без записи cred). Verify: 301 объект, 77.012 MiB == source.
- **Smoke (с books-vds, мимо LAN-DNS-перехвата воркстейшна):** подписанный imgproxy-URL реального объекта → 200 image/webp 32102 B; несуществующий guid → 404 (негативный контроль).
- **Wiki-drift зафиксирован:** `minio-imgproxy-on-vds.md` устарел (говорит pipeline на windows-host; фактически imgproxy на books-vds с 08.06). Добавлен concept `galleries-storage-class-local-not-s3`.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/snolla / 2026-06-12T11:09:50.425Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-12 / path A: bucket galleries + 301 pilorama98 originals, imgproxy 200 verified, snolla code unchanged -->
---
## 🟢 [migrate-assets-originals-to-s3] — closed 2026-06-13 — assets-оригиналы залиты в S3, imgproxy 200 verified (scope расширен: весь assets-дерево, не только pilorama98). Создан bucket `assets` (отсутствовал) на MinIO books-vds; rclone RUVDS IIS `App_Data\assets` → `s3://assets/<ownerId>/<storageFilename>` verbatim. **Verify:** dest == source точь-в-точь — 4750 объектов / 225 935 853 B (215.5 MiB), 145 owner-папок. **Smoke (через books-vds, мимо LAN-DNS):** brevno.jpg (предподписанный URL из задачи) → 200 image/webp 64556 B (был 404); валидно-подписанный несуществующий объект → 404 (негативный контроль). Код snolla НЕ менялся (path A). Inbox victor/snolla отправлен. ⚠️ Остаётся snolla-side: `content-api/routes/assets.js` пустой stub — картинки не поедут на сайт пока роут не реализован (код-таска snolla, не блокер миграции).
### (исходное описание ниже сохранено до prune)
Залить Local-storage assets-оригиналы сайта pilorama98 в S3 — латентная дыра того же класса, что галереи (закрыта миграцией `migrate-gallery-originals-to-s3`). snolla отдаёт inline-картинки контента (`<img src="/assets/<ownerId>/<size>/<file>">`) через imgproxy с источником `s3://assets/<ownerId>/<storageFilename>`, но объектов в S3 нет — imgproxy 404 "Source image is unreachable". БД-записи есть, файлы только на legacy-IIS диске. Тип `StorageClient="assets"` — это `MoreThenCms.FileStorage.Local.*`, в S3 никогда не мигрировали.
## Acceptance criteria
1. Legacy assets-оригиналы pilorama98 залиты в S3 bucket `assets` ключами `<ownerId>/<storageFilename>` (path A, как с галереями: storageClient = имя бакета).
2. Контрольный прозвон imgproxy подтверждает 200 image/webp на реальном объекте + 404 на несуществующем (негативный контроль).
3. Verify: dest object count == source, размер == source.
4. Зафиксирована точная форма ключа и охват (какие ownerId/папки залиты), отчёт в inbox victor/snolla.
## Подтверждённое репро (от victor/snolla, 2026-06-13)
- URL у консьюмера: `http://localhost:3742/assets/83150d773cf0455ea58ddcccb2578713/800x450/brevno.jpg` → 404.
- БД `MoreThenCms` (mssql.kzntsv.site), таблицы `Folders`/`Files`:
- `Folders`: OwnerId `83150d77-3cf0-455e-a58d-dcccb2578713` → корневая папка `/` (FolderId `03CE5D8D-95B9-4D81-B222-80AEC76225CD`). **Внимание: ownerId это владелец медиа-папки, НЕ siteId** (siteId pilorama98 = `37e67fc4…`).
- `Files`: `brevno.jpg` → StorageClient `assets`, StorageFilename `brevno.jpg`, ContentType image/jpeg, Size 580667 B.
- Прямой прозвон imgproxy подписанным URL источника `s3://assets/83150d773cf0455ea58ddcccb2578713/brevno.jpg`**HTTP 404 "Source image is unreachable"** (подпись валидна, объекта в S3 нет).
- Подписанный путь для верификации после заливки:
`https://imgproxy.kzntsv.site/n8AdQMelCr2lIvWzeQ3Mw6Fqxft3Q24Rd3vCg-ctHaA/fill/800/450/ce/1/czM6Ly9hc3NldHMvODMxNTBkNzczY2YwNDU1ZWE1OGRkY2NjYjI1Nzg3MTMvYnJldm5vLmpwZw.webp` → ждём 200 после миграции.
## Подсказки по форме ключа (важно — отличие от галерей)
- Галереи: prefix = `String(site.id)` (siteId сайта). Ассеты: prefix = **OwnerId папки из таблицы Folders**, НЕ siteId. Для brevno.jpg это `83150d773cf0455ea58ddcccb2578713`. На диске ассеты, вероятно, разложены по этим owner-id, а не по siteId.
- Ключ верстается snolla как `s3://${asset.storageClient}/${ownerId}/${asset.storageFilename}` (`middleware/assets.js:98`), storageFilename берётся verbatim (с заменой `\``/`).
- Залить ВСЕ assets-папки/owner'ы pilorama98 (не только owner brevno.jpg) — иначе остальные inline-картинки контента дадут ту же дыру.
## Хвосты (контекст, не блокеры)
- Тот же Local-класс у `images / scripts / stylesheets / watermarks` — если snolla отдаёт их через imgproxy, их ждёт та же дыра (отдельный аудит на стороне victor/snolla, таска `audit-local-storage-clients-imgproxy-s3-gap`).
- На стороне snolla отдельно нужна реализация `content-api/routes/assets.js` (сейчас пустой stub) — это код-таска victor/snolla, не блокер миграции, но картинки не поедут пока не сделаны ОБА (миграция + роут).
## Обязательные скилы — вызвать до начала работы
- invoke `using-tasks` — управление статусом задачи
- invoke `project-discipline` — дисциплина коммитов/пушей
- invoke `using-projects-meta` — cross-project: отчёт в inbox victor/snolla при close/park
- invoke `systematic-debugging` — прозвон по фактам (DB → диск → S3 → imgproxy) до выводов
**TDD:** нет — инфра-миграция/ops (если всплывёт код — отдельной impl-таской с TDD).
**Разрешения:** интерны: нет | автопуш: да
**weight:** needs-human
**notify:** victor/snolla
**Status:** done (closed 2026-06-13)
**Where I stopped:** мигрировано всё дерево assets. rclone size min:assets == источник (4750 / 225935853). Smoke 200/404 OK.
**Next action:** — (хвост на стороне snolla: реализовать `content-api/routes/assets.js`; остальные Local-классы images/scripts/stylesheets/watermarks — отдельный аудит snolla, если поднимутся через imgproxy).
**Branch:** master
**Notify:** victor/snolla — ✅ inbox `.claude-inbox/2026-06-13T20-14-16Z-admin.md`
**Closure note (2026-06-13):**
- **Диск (RUVDS IIS `C:\sites\snolla\App_Data\assets`):** Local storage class, 145 owner-папок (32-hex), плоские файлы. brevno owner `83150d77…` = 6 файлов, `brevno.jpg`=580667 B == DB `Files.Size`. Ключ-форма `<ownerId>/<storageFilename>` подтверждена на диске.
- **MinIO (books-vds, minio.kzntsv.site):** bucket `assets` отсутствовал → создан. Залив rclone v1.74.2 (env-var s3-remote, без записи cred), `App_Data\assets``min:assets` verbatim. Verify `rclone size` == source: 4750 объектов / 225935853 B.
- **Smoke (мимо LAN-DNS воркстейшна, `--resolve …:89.253.255.133`):** предподписанный imgproxy-URL brevno.jpg → 200 image/webp 64556 B; валидно-подписанный (IMGPROXY_KEY/SALT с books-vds) несуществующий объект → 404 text/plain. 200 не ложный — imgproxy реально ходит в S3.
- **Scope расширен намеренно:** задача про pilorama98, залито всё (215 MiB — мизер). owner→сайт маппинг неочевиден (риск пропустить owner pilorama98), заливка всего гарантирует покрытие + закрывает класс-404 для всех тенантов. Регрессии нет (shared bucket, как galleries).
- **Wiki:** concept `galleries-storage-class-local-not-s3` обновлён (assets done).
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/snolla / 2026-06-13T19:31:07.686Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-13 / path A: bucket assets + весь App_Data\assets (4750 obj/215.5 MiB), imgproxy 200/404 verified, snolla code unchanged -->
---
## 🟢 [deploy-pilonuxt-vds] — closed 2026-06-15 — CUTOVER LIVE на www.pilorama98.ru (tag 125a3e2, Portainer stack 16). **Цель:** Собрать prod-образ нового Nuxt-фронта ПИЛОРАМА98 (`victor/pilonuxt`, master) и задеплоить на VDS как Portainer-стек за Traefik. Готово в репо: `Dockerfile` + `.dockerignore` в корне. Полный овнершип сборки и деплоя — на тебе (build → push в VDS-registry → Portainer stack), как books/stostayer.
**Сборка:**
- `docker build --secret id=verdaccio_token,env=VERDACCIO_TOKEN -t <registry>/pilonuxt:<tag> .`
- `VERDACCIO_TOKEN` — ЖИВОЙ JWT (len ~300, 2 точки), User-env этой же машины (где `pass`). НЕ legacy-opaque (len ~196). Детали: pilonuxt `.wiki/concepts/verdaccio-token-lifecycle.md`.
- Билд ходит в сеть: `verdaccio.kzntsv.site` (пакеты) + `mssql.kzntsv.site:1433` (postinstall→schema-gen генерит `src/generated/` из CMS-БД по `config/default.json`). Если собираешь там, где CMS-БД недоступна — предгенери `yarn schema-gen` и положи `src/generated/` в контекст (schema-gen в билде тогда пропустится).
- Многостадийный, нативных модулей НЕТ (better-sqlite3 выпилен) → build-toolchain не нужен.
**Runtime egress из контейнера:**
- `mssql.kzntsv.site:1433` — живые запросы CMS (cacheTimeout=0), обязателен.
- `smtp.yandex.ru:465` — письма форм (contacts/checkout).
- Собственный публичный origin — SSR self-call на `/snolla` через `useRequestURL().origin` (`app/plugins/snolla.ts`). Контейнер должен резолвить+достигать свой публичный домен через Traefik. До cutover домена origin-петля на боевом не замкнётся.
**Env:**
- build: `VERDACCIO_TOKEN` (secret, НЕ ARG/ENV — иначе осядет в слоях).
- runtime (опц.): `NITRO_PORT`(деф 3000), `NITRO_HOST`(деф 0.0.0.0), `NUXT_PUBLIC_GTM_ID`(деф боевой `GTM-MNNXFJ6`; пустой → выкл), `NUXT_PUBLIC_SNOLLA_BASE_URL`(деф `/snolla`).
- Секреты (MSSQL/SMTP/imgproxy key+salt/siteId) ЗАПЕЧЕНЫ в `config/default.json` внутри образа (решение vitya — образ только в приватном registry). Env-override не настроен.
**Контекст инфры (с твоей доски):**
- imgproxy на books-vds (стек 29/30); у фронта `/imgproxy/**` = routeRule-редирект на `imgproxy.kzntsv.site`.
- ⚠️ Известный gap: snolla-side `content-api/routes/assets.js` — пустой stub (ты флагал в `migrate-assets-originals-to-s3`); без него часть картинок может не рендериться. Проверь на smoke.
- Прод `www.pilorama98.ru` сейчас = легаси MoreThenCms. Cutover домена — решение vitya: деплой на временный поддомен (напр. `pilonuxt.vds.kzntsv.site`) для smoke ИЛИ сразу боевой — согласуй.
**Acceptance:**
1. Образ собран и в registry.
2. Portainer-стек за Traefik, healthy.
3. Smoke: главная 200 + SSR-разметка (не 500); товары/категории из CMS; картинки рендерятся; форма contacts шлёт письмо.
4. Доклад в inbox `victor/pilonuxt`: URL, имя стека/тег образа, итог smoke.
## Обязательные скилы — вызвать до начала
- invoke `using-tasks` — статус задачи
- invoke `project-discipline` — дисциплина коммитов/пушей
- invoke `using-vds-ops` — диагностика контейнеров VDS
- invoke `using-projects-meta` — cross-project (notify pilonuxt, чтение pilonuxt-вики)
- invoke `using-wiki` после деплоя — заингесть итог в `.admin/.wiki/concepts/`
**TDD:** нет — ops/deploy, не код.
**Разрешения:** интерны: нет | автопуш инфра-конфигов: да
**weight:** needs-human (deploy-инфра)
**notify:** victor/pilonuxt
**Status:** done (closed 2026-06-15)
**Closure / coverage check:** vitya флипнул DNS pilorama98.ru+www → 89.253.255.94; я перенастроил traefik-роутер на боевые хосты (`Host(www.pilorama98.ru)||Host(pilorama98.ru)||Host(pilonuxt.vds.kzntsv.site)`, cert LE на apex+www), снял smoke GTM-override (прод-аналитика GTM-MNNXFJ6 ВКЛ), выкатил `125a3e2` (фикс «Продукция»→/catalog). Acceptance: (1) образ в registry ✓ 125a3e2; (2) Portainer stack 16 за traefik healthy ✓; (3) smoke ✓ — финальный краул с VDS по боевому www.pilorama98.ru: все страницы page 200/viewModel 200 (главная/каталог/категории/товар/услуги/контакты/оплата/доставка), картинки imgproxy 200 webp, x-powered-by Nuxt (не IIS), cert valid, футер /products битый линк убран (0 вхождений, «Продукция»→/catalog); форма contacts — закрыта (vitya «формы заебись» + pilonuxt end-to-end verified в dev, smtp egress жив); (4) доклад pilonuxt ✓. Урок применён: краулил весь меню+viewModel, не 2 роута ([[smoke-crawl-real-pages-not-two-routes]]).
**Follow-ups (не блокеры, отдельно):**
- **Старый IIS RUVDS 80.64.31.36 — РЕШЕНИЕ vitya 2026-06-15: держим для отката, НЕ выводим.** ⚠️ Этот IIS = snolla catch-all на 25 хостнеймов (emspb/kupimknigi/labtools/…), целиком гасить нельзя — при будущем cleanup снимается только биндинг pilorama98, не весь сайт. **Rollback pilorama98:** вернуть DNS reg.ru `pilorama98.ru`+`www``80.64.31.36` (старый сайт + его LE-cert живы), TTL низкий. Decommission-биндинга — отдельно после soak.
- DNS-propagation хвост (часть юзеров ещё ловит старый IIS пока кэши истекают).
- Опц.: apex→www 301 canonical (легаси так делал; сейчас apex отдаёт фронт напрямую) — SEO-nicety.
- bare `/products`→404, но unlinked (на него ничто не ссылается).
**Branch:** n/a | **Notify:** victor/pilonuxt ✓
<!-- HISTORICAL (pre-cutover) ниже -->
**Status (pre-cutover):** blocked
**Where I stopped (2026-06-14 21:05):** pilonuxt пофиксил content-api (`@snollajs/data@0.9.1` — models-load баг + `tedious` в `nitro.externals.traceInclude`), тег `961a838` (все прошлые битые). Перекатил stack 16. **Полный re-smoke (curl весь меню 21 стр + viewModel по каждому + браузер Playwright + скрин):** viewModel 200 везде (был 500), все страницы меню 200 с реальным контентом (категории/товары/услуги/инфо), товар-стр 200, картинки imgproxy 200 webp, браузер-консоль 0 ошибок, верстка целая. **ОСТАЛСЯ РЕАЛЬНЫЙ БАГ: пункт меню «Продукция» → `/products` → 404** (битый линк, до cutover нельзя). Мелочи: `/blog`+`/services/building/projects/` viewModel 404 (страницы 200), Я.Метрика external не грузится (не баг app). Форма contacts → email НЕ гонял (живой email, жду решение). Скрин `pilonuxt-home-smoke.jpeg`. Inbox pilonuxt отправлен (21:05).
**Next action:** pilonuxt чинит `/products` 404 → новый тег → pull+stack16 update → добить smoke (/products + форма). Согласовать с vitya: тест формы contacts (live [SMOKE TEST] vs закрыть по verified). Cutover www.pilorama98.ru — после закрытия /products + формы.
**Blocker:** pilonuxt: пункт меню «Продукция» → /products → 404 (роут/CMS-страница отсутствует либо нужен редирект на /catalog).
**Branch:** n/a
**Notify:** victor/pilonuxt
**(history)** Фальш-green #1 (20:25): объявил green по `/`+`/catalog`, vitya поймал `/snolla/pages/viewModel?path=/services`→500; content-api лежал полностью (`sequelize['snolla']` undefined — `@snollajs/data` models не загружались + `tedious` не в .output). Урок: [[smoke-crawl-real-pages-not-two-routes]]. Путь: ТЗ→pull→домен(temp). Мои 2 Dockerfile-бага (npmAlwaysAuth + COPY scripts/src-generated до install) → pilonuxt пофиксил +свой (build-time VERDACCIO_TOKEN default `${VAR:-}`) → образ `36ec970` битый: дубль Vue в prod-бандле давал SSR 500 `reading 'ce'` на всех роутах; диагностировал «инфра зелёная, баг в app»; pilonuxt нашёл (Vite дедупит Vue в dev, Nitro prod нет) → `resolutions``3622662` зелёный.
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilonuxt / 2026-06-14T18:45:22.955Z -->
<!-- activated: vitya@.admin-exec / 2026-06-14 / ТЗ получено, домен=temp-subdomain для smoke -->
<!-- re-smoke green: vitya@.admin-exec / 2026-06-14 20:05 / stack 16 = pilonuxt:3622662, infra+app green; pending: contacts-email test + cutover -->
---
## 🟢 [redeploy-pilonuxt-gsc-offers] — closed 2026-06-16 — pilonuxt `37412d0` в проде, offers в микроразметке Product live. Build на workstation (verdaccio build-secret) → push registry.kzntsv.site/pilonuxt:37412d0 (398MB) → перекат Portainer stack 16 (PUT API). Acceptance verified (curl --resolve к VDS): на `/shop/products/doska-obreznaya-25-100-6000` есть `itemprop="offers"` itemtype Offer с price=277.5/priceCurrency=RUB/availability=InStock/url-канон; X-Powered-By Nuxt; регрессии нет (меню /catalog,/services,/contacts,/payment = 200, viewModel жив). ⚠️ По ходу: в Portainer не был зарегистрирован НИ ОДИН registry → первый pull упал `no basic auth credentials`; зарегистрировал `registry.kzntsv.site` (Custom, Id 1, registry-креды из pass) → перекат прошёл, приватный registry теперь авторизован в Portainer постоянно. Inbox pilonuxt отправлен (GSC → «Проверить устранение»). Передеплой pilonuxt с SEO-фиксом микроразметки Product (commit 37412d0). Google Search Console отсеивал страницы товаров из выдачи: критическая ошибка «задайте offers / review / aggregateRating» — у Product не было блока offers, а aggregateRating рендерился только при наличии рейтинга. Фикс добавил schema.org/Offer (price/priceCurrency/availability/url) в app/pages/shop/products/[slug].vue, проверено в SSR. Только код фронта — env/инфра/секреты не менялись, build/runtime egress те же, что в прошлый деплой (verdaccio + mssql при сборке; mssql + smtp + self-origin в рантайме). Acceptance: новый образ в проде, на странице товара в HTML присутствует itemprop="offers" с ценой и наличием.
## Обязательные скилы — вызвать до начала работы
- invoke `using-tasks` — управление статусом задачи
- invoke `project-discipline` — дисциплина коммитов/пушей
**TDD:** нет — ops/deploy, кода не пишем; верификация = SSR-вывод страницы товара.
**Разрешения:** интерны: нет | автопуш: n/a (деплой в твоём контуре)
**weight:** needs-human
**notify:** victor/pilonuxt
**Status:** 🟢 done (closed 2026-06-16 vitya@.admin-exec)
**Closure / coverage check:** build workstation @ 37412d0 (verdaccio JWT build-secret) → push registry.kzntsv.site/pilonuxt:37412d0 (398MB) → Portainer stack 16 PUT (PullImage:true) → контейнер running 37412d0. Smoke curl --resolve www.pilorama98.ru:443:89.253.255.94: offers-блок present (price/priceCurrency/availability/url), X-Powered-By Nuxt, меню+viewModel 200 (без content-api-500). Урок [[smoke-crawl-real-pages-not-two-routes]] применён — краулил реальные nav-href, не гадал слаги (/delivery 404 но не в меню → не баг).
**Follow-up (не блокер):** registry.kzntsv.site зарегистрирован в Portainer (Custom Id 1) — закрывает «no basic auth» на будущих перекатах приватных образов; до этого pull держался на host docker login, который слетел.
**Next action:**
**Branch:** master
**Notify:** victor/pilonuxt ✓
<!-- closed-by: vitya@.admin-exec / 2026-06-16 / build+push+stack16 PUT, offers verified via VDS-resolve smoke, Portainer registry registered -->
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilonuxt / 2026-06-16T13:49:40.667Z -->
---
## 🟢 [redeploy-pilonuxt-gsc-structured-data] — closed 2026-06-17 — `19a4a84` в проде, structured-data (description/brand/hasMerchantReturnPolicy) live. Передеплой www.pilorama98.ru с фиксом микроразметки Product под minor-замечания Google Search Console (structured data «Данные о товарах продавца»).
Коммит уже на origin/master: `19a4a84` (fix(seo): обогатил микроразметку Product offers). Изменён только `app/pages/shop/products/[slug].vue` — добавлены itemprop description (always-on), brand, hasMerchantReturnPolicy внутри offers. Тот же пайплайн, что вчерашний `redeploy-pilonuxt-gsc-offers` (37412d0).
## Acceptance
- Образ собран из master @ `19a4a84`, тег `registry.kzntsv.site/pilonuxt:19a4a84`, запушен в приватный registry.
- Portainer-стек 16 перекатан на новый образ, контейнер healthy за Traefik.
- На проде на странице товара (например `/shop/products/doska-obreznaya-25-100-6000`) в SSR-HTML присутствуют: `itemprop="description"` (непустой), `itemprop="brand"` (Пилорама 98), внутри `itemprop="offers"``itemprop="hasMerchantReturnPolicy"` с `returnPolicyCategory=MerchantReturnNotPermitted`. Прежние offers-поля (price/priceCurrency/availability/url) на месте, регрессий нет.
## Обязательные скилы — вызвать до начала работы
- invoke `using-tasks` — управление статусом этой задачи
- invoke `project-discipline` — дисциплина коммитов/пушей
- по деплою свериться с концептом вики pilonuxt `concepts/docker-deploy.md` (build-secret VERDACCIO_TOKEN, runtime-egress, приватный registry registry.kzntsv.site зарегистрирован в Portainer Custom Id 1)
**TDD:** нет — ops-задача (build + redeploy), кода не пишется.
**Разрешения:** интерны: нет | автопуш: n/a (билд из уже запушенного master, кода не коммитим)
**weight:** needs-human (deploy-инфра: registry + Portainer-стек за Traefik)
**notify:** victor/pilonuxt (ответ о закрытии/затыке — в inbox pilonuxt, там слушает монитор)
**Status:** 🟢 done (closed 2026-06-17 vitya@.admin-exec)
**Closure / coverage check:** build на workstation @ 19a4a84 (verdaccio JWT build-secret, EXIT=0, src/generated предгенерён → mssql на билде не дёргался) → push registry.kzntsv.site/pilonuxt:19a4a84 (digest sha256:9642…fa5) → Portainer stack 16 PUT(pullImage:true) → контейнер running 19a4a84 (vds-ops ps). Smoke curl --resolve www.pilorama98.ru:443:89.253.255.94 на /shop/products/doska-obreznaya-25-100-6000: **(1)** `itemprop="description"` непустой («Характеристики Сорт 1 Материал Сосна…») ✓; **(2)** `itemprop="brand"` content="Пилорама 98" ✓; **(3)** внутри `itemprop="offers"` (schema.org/Offer): `hasMerchantReturnPolicy``returnPolicyCategory=https://schema.org/MerchantReturnNotPermitted` ✓; **(4)** прежние offers-поля целы: price=277.5/priceCurrency=RUB/availability=InStock/url=канон ✓; X-Powered-By Nuxt. Регрессий нет: / /catalog /services /contacts /payment = 200 (/delivery 404 unlinked, как и вчера). Урок [[smoke-crawl-real-pages-not-two-routes]] применён.
**Gotcha (новый, заингещён):** Portainer stack update через PS 5.1 Invoke-RestMethod коррапит кириллические комментарии compose — ответ /file декодится как ISO-8859-1 (mojibake) → обратный PUT даёт `yaml: could not find expected ':'`. Фикс: GET байтами (-OutFile) + read -Encoding UTF8, PUT тело UTF-8-байтами. Записано в concepts/portainer-stack-management-vds.md.
**Next action:**
**Branch:** n/a
**Notify:** victor/pilonuxt ✓
<!-- closed-by: vitya@.admin-exec / 2026-06-17 / build+push+stack16 PUT(pullImage), structured-data verified via VDS-resolve smoke, PS-UTF8 gotcha ingested -->
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilonuxt / 2026-06-17T07:00:53.563Z -->
---
## 🔵 [stostayer-web-complaint-form-deploy] — Раскатать на прод (www.stostayer.ru) фикс формы жалоб. Коммит **120bc07** уже в `origin/master` репо `victor/stostayer.new`.
## Контекст
Прод-форма `/pozhalovatsya#complaint_form` не отправляла данные. Причина: в `packages/web/pages/pozhalovatsya.vue` `onSubmit` вызывал голый `axios(...)` вместо `this.$axios(...)``ReferenceError`, съеденный пустым `catch{}`. Чистый клиентский фикс (1 файл). Прод = старый **`packages/web`** (Nuxt 2 SSR), web4 ещё НЕ переключён.
## ⚠️ Деплой-нюанс
Это `.vue`-страница в Nuxt 2 **SSR-сборке** — простого `git pull` НЕДОСТАТОЧНО. Прод крутит собранный output через pm2 (`packages/web`: `start = node server/index.js`, `build = nuxt build`, pm2 → `ecosystem.config.js`). Нужен: pull → `yarn build` (или `nuxt build` с `NODE_OPTIONS=--openssl-legacy-provider`) в `packages/web` → рестарт pm2-процесса. Точную прод-процедуру ты знаешь лучше (раскатывал vehicles-loader) — действуй по факту, это лишь напоминание что нужна пересборка, не только pull.
## Acceptance
- На проде HEAD `packages/web` = коммит 120bc07 (или master ≥ него).
- Пересборка выполнена, процесс рестартнут без ошибок в логах.
- Smoke вручную: открыть https://www.stostayer.ru/pozhalovatsya#complaint_form, заполнить (Имя, Телефон, СТО, Описание), Отправить → улетает `POST /ostavit-otzyv`, появляется success-модалка «Подтверждение». Письмо/заявка доходит как у `/ostavit-otzyv`.
- Откат: если форма ломается — вернуть прежний билд/коммит (RTO минимальный).
## Обязательные скилы — вызвать до начала работы
- invoke `using-tasks` — управление статусом этой задачи
- invoke `project-discipline` — дисциплина деплоя/коммитов
**TDD:** нет — деплой existing-коммита, admin кода не пишет; верификация = ручной smoke.
**Разрешения:** интерны: нет | автопуш: нет (admin коммитов не делает; если понадобится тег/бамп — спросить у victor/stostayer.new через inbox).
**weight:** needs-human
**notify:** victor/stostayer.new
**Status:** blocked
**Where I stopped:** Cherry-pick `120bc07` на `d02f740` (pre-ESM, рецепт stostayer.new) → `7d88a02`. **Собралось** offline (nuxt build прошёл) → push 0.3.19 в `docker.stostayer.ru` (с 1 попытки на новом VPN; прежние retry-штормы засветили egress → хостинг забанил наш VPN-IP, vitya сменил VPN) → передеплой Portainer-стека `stostayer-web` (Id 16, EndpointId 3) на 0.3.19 через Portainer API по IP контейнера `172.18.0.2:9000` (мимо Angie BA). **Но контейнер краш-лупит в рантайме:** `ERR_REQUIRE_ESM``packages/web/server/index.js:6` `require('@snollajs/snolla')`, а он ESM (lockfile d02f740 тянет ESM-версию). d02f740 собирается, но НЕ runtime-чистая. **Откатил стек на 0.3.18** (тот же PUT, тег назад) — контейнер Up, штатный Nuxt2, сайт восстановлен. vitya: остаёмся на 0.3.18, апдейт отставлен.
**Next action:** ПАРКНУТО. Деплой невозможен до web4-cutover. Вердикт (stostayer.new 14-55, подтверждён): легаси `packages/web` require-ит **5** ESM-ставших пакетов (`@snollajs/snolla` 0.7.4, `@snollajs/content-api` 0.8.0, `@stostayer/api`, `@stostayer/data` ×2 места) → чейз CJS-базы бесполезен (надо откатиться к ~0.3.18 и потерять всё). `0.3.18` = последний собираемый артефакт, заморожен. Фикс формы (`120bc07`) едет вместе с переездом формы в web4. Ранбук: `.wiki/concepts/stostayer-web-deploy-runbook.md`. Дохлый `0.3.19` в `docker.stostayer.ru`**оставлен по решению vitya (2026-06-17)**, не деплоить.
**Blocker:** web4-cutover (планирование на стороне vitya, отдельным решением) — НЕ на stostayer.new и НЕ канал доставки (он рабочий). Прод остаётся на 0.3.18, сайт восстановлен.
**Branch:** n/a
**Notify:** victor/stostayer.new
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/stostayer.new / 2026-06-17T12:56:53.300Z -->
---
## 🟢 [books-scheduler-registry-auth-cred] — ## Проблема (простыми словами)
Планировщик books (`books-job-scheduler` на books-vds) умеет запускать «docker-таски» — для каждой такой задачи он скачивает (pull) готовый образ-инструмент из реестра `registry.kzntsv.site` и запускает его контейнером. Один из таких инструментов — `books-tool-create-picking-list-pdf` (рендерит PDF листа подбора FBS).
С 23 мая каждый такой запуск падает на стадии скачивания образа с ошибкой:
`pull/create failed: (HTTP 500) ... no basic auth credentials`.
Причина (разобрана совместно с admin): `registry.kzntsv.site` — это standalone `registry:2` за htpasswd Basic-авторизацией (так с самого bootstrap 20.05, анонимного доступа не было никогда). Раньше образ просто лежал в локальном кэше демона и pull не требовался; ночная чистка `vdsDockerCleanup` 23 мая вычистила кэш — и следующий pull впервые упёрся в всегда-бывшую auth-стену. Код планировщика звал pull **без креда** (анонимно) → реестр отвечал 401 → dockerode оборачивал в 500. Итог: ~80 фейлов, PDF листов подбора не генерятся с 23 мая.
Approach ратифицирован человеком (вариант A): код планировщика будет слать креды в pull (это делаю я, books, отдельно, под TDD), а реестру нужен отдельный htpasswd-юзер `books-ci` (его ты уже завёл, кред в `pass vds-kzntsv/registry-books-ci`, проверен live: authed HEAD manifests/master → 200).
## Что нужно сделать (твоя зона — admin)
Положить кред `books-ci` в config-volume планировщика на **books-vds (89.253.255.133)**:
- **Файл (bind-mount):** `/opt/books/job-scheduler/config/default.json`
- В этом JSON уже есть объект верхнего уровня `"docker"` (там `host`, `maxConcurrent`, `pullBackoffMs` и т.д.). Добавить в него ключ `registryAuth`:
```json
"docker": {
"...": "... существующие поля не трогать ...",
"registryAuth": {
"username": "books-ci",
"password": "<пароль из pass vds-kzntsv/registry-books-ci>",
"serveraddress": "registry.kzntsv.site"
}
}
```
- `serveraddress` — ровно `registry.kzntsv.site` (без `https://`, без `/v2`).
- После правки **перезапустить контейнер `books-job-scheduler`** (через Portainer/compose), чтобы node перечитал config — bind-mount подхватывается только на старте процесса.
## Acceptance
- Ключ `docker.registryAuth` присутствует в `/opt/books/job-scheduler/config/default.json`, пароль соответствует `books-ci` в реестре.
- Контейнер `books-job-scheduler` перезапущен и `healthy`.
- (Финальная проверка — за books: после деплоя code-части убедиться что `createPickingListPdf` перестал падать на pull. Образ и тег `master` на месте, допушивать ничего не надо — pull заработает сразу как кред доедет + выйдет code-часть.)
## Замечания
- Кред **не хардкодить** в git — только в config-volume на хосте (как mongo/s3/mariadb-креды рядом).
- registryGc (вторая docker-related задача, падает «gitea config missing») — **в эту таску НЕ входит**, разбирается отдельно на стороне books (мис-таргетирован на Gitea-packages, переписывается на registry v2 DELETE позже).
## Обязательные скилы — вызвать до начала работы
- invoke `using-tasks` — для управления статусом этой задачи
- invoke `using-projects-meta` — cross-project координация / нотификация
- invoke `project-discipline` — дисциплина изменений на проде (config-volume, рестарт)
- invoke `using-vds-ops` или `books-ops-mcp` — read-only проверка `books-job-scheduler` (health/env) после рестарта
**TDD:** нет — ops-задача (размещение креда + рестарт контейнера), кода нет.
**Разрешения:** интерны: нет (секрет) | автопуш: н/п (не git-код books)
**weight:** needs-human
**notify:** victor/books
**Status:** done
**Where I stopped:** cred placed in config-volume; activation = books code deploy (no premature restart)
**Next action:** (none — kept until merged)
**Branch:** n/a
**Notify:** victor/books
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/books / 2026-06-18T06:57:04.379Z -->
## Done (admin infra)
- `docker.registryAuth = {username:"books-ci", password:<pass vds-kzntsv/registry-books-ci>, serveraddress:"registry.kzntsv.site"}` written into `/opt/books/job-scheduler/config/default.json` (bind-mount → scheduler container, verified pwlen=48, valid JSON). Backup: `default.json.bak-pre-registryauth-2026-06-18`.
- **No restart performed** — current running code doesn't read `registryAuth` yet (inert/backward-compatible). Activation happens on books' code deploy (rebuild = restart, bind-mounted config persists). If books had already deployed the new code, a single `books-job-scheduler` restart activates it.
- Cred itself verified against registry earlier: `books-ci` Basic → 200 on `books-tool-create-picking-list-pdf:master`, wrong-pass → 401.
- Image + `master` tag present in registry → pull works as soon as code+cred are both live; no re-push needed.
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-06-18T07:00:51.841Z / note: cred placed in config-volume; activation = books code deploy (no premature restart) -->
---
## 🟢 [books-task-runner-registry-auth-cred] — Положить кред books-ci в **task-runner** config-volume, чтобы заработал переписанный `registryGc` (чистка Docker registry через Registry v2 DELETE).
Контекст: registryGc раньше бил в Gitea packages API и никогда не отрабатывал (образы books-* живут в standalone registry:2 на registry.kzntsv.site за htpasswd). Переписан на Registry v2 REST с Basic-auth кредом books-ci (commit books@a3734d5). Кред books-ci уже есть в **job-scheduler** config-volume (для docker pull) — нужен тот же и в **task-runner** (registryGc бежит там, http-runner, отдельный volume).
## Что сделать
В task-runner config-volume (`/opt/books/task-runner/config/...` — local.json/production.json поверх default.json) добавить секцию:
```json
"registry": {
"baseUrl": "https://registry.kzntsv.site",
"username": "books-ci",
"password": "<из pass vds-kzntsv/registry-books-ci>"
}
```
(baseUrl+username уже в git default.json; нужен только password — секрет, в git его нет).
⚠️ Файл config-volume: соблюсти права как в memory `feedback_container_user_node_uid_1000` — контейнер бежит под uid 1000, НЕ ставить `600 root:root` (был прод-даун 2026-06-18 на createPickingListPdf из-за EACCES). Нужен read для uid 1000 (644).
## Acceptance
- `registry.password` присутствует в task-runner config-volume.
- Ручной trigger registryGc через /scheduler с `dryRun=true` отрабатывает без 401: в отчёте видны repos books-* и «будет удалено N». (Если 405 на DELETE — значит на registry не включён `REGISTRY_STORAGE_DELETE_ENABLED=true`; отдельный вопрос, отметить.)
## Обязательные скилы — вызвать до начала работы
- invoke `using-tasks` — для управления статусом задачи
- invoke `using-projects-meta` — cross-project tasks/wiki
- invoke `books-ops-mcp` — проверить контейнер/конфиг task-runner read-only до и после
**TDD:** нет — ops/secret, кода нет.
**Разрешения:** интерны: нет (секрет) | автопуш: n/a (ручная ops на VPS).
**weight:** needs-human — секрет + доступ к VPS config-volume, критичная инфра.
**notify:** OpeItcLoc03/books
**Status:** 🟢 done — кред положен, acceptance (auth/dryRun) verified
**Where I stopped:** Закрыта. Секция `registry` (baseUrl+username+password books-ci, полная — bind-mount затеняет config образа) в `/opt/books/task-runner/config/default.json` (books-vds, 644 root сохранён, бэкап `.bak-pre-registry-2026-06-18`, только ключ registry added, EACCES-инцидент 18.06 не повторён). После deploy нового кода (образ `master-bf2a8c5`, код a3734d5 внутри) прогнан `registryGc {dryRun:true,debug:true}` через scheduler-dispatch-путь (`POST /tasks/registryGc` + `x-agenda-job-id`, idентично http.js handler'у). Результат: **success, 401 нет нигде** (hit `/v2/_catalog`=14 repos, `/tags/list`, resolveManifest), repos books-* видны (7 processed), отчёт `task-reports/j/...md`.
**Acceptance coverage:** (1) `registry.password` в config-volume ✓ (len 48, `config.get` резолвится). (2) dryRun без 401 + repos books-* + «будет удалено N» ✓ (N=0 — см. finding ниже; критерий по форме выполнен). 405/`REGISTRY_STORAGE_DELETE_ENABLED` — dryRun DELETE не слал, не проверено этим прогоном (по вики registry-kzntsv-auth-model уже =true).
**Finding (отдано books, НЕ блокер кред-таски):** GC аутентифицируется и бежит, но **ничего не удаляет** — по каждой репе `keep=tags1, drop=0` независимо от keepLastN=3. Root cause в books-коде `lib/registryV2.js`: `getCreated` возвращает null для всех манифестов (вероятно `configDigest` не извлекается — OCI image-index/manifest-list от buildx, или blob 404), а `planDeletions` ставит `protected=true` любой группе с `created==null` → drop пуст всегда. keepLastN не применяется. Owner — books-сессия.
**Branch:** n/a
**Notify:** OpeItcLoc03/books
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/books / 2026-06-18T08:28:09.066Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-18 / cred placed (644 mode preserved) + new code deployed (master-bf2a8c5) + dryRun verified no-401, repos books-* visible; flagged planDeletions/getCreated no-op bug to books -->
---
## 🟢 [archive-pilorama-source-repos] — closed 2026-06-18 — 3 исходных репо заморожены. `archived:true` (Gitea API, http 200) на `victor/pilonuxt`/`OpeItcLoc03/pilorama98.ru-legacy`/`OpeItcLoc03/snolla-products-loader`; в каждом README-баннер + repo-description → `victor/pilorama98.ru/apps/{web,legacy,loader}`; локальные клоны удалены (verified clean + no unpushed ДО rm); projects-meta `archived_skipped` 1→4 (поллер скипает); shared-wiki concept `pilorama98-monorepo-consolidation` создан. **Sequencing-замечание:** архив выполнен 17:43 — раньше, чем пришло предупреждение пира (17:46) о порядке deploy→archive; без последствий (deploy нацелен на монореп, не на pilonuxt; archived-репо остаётся читаемым). См. отчёт в inbox pilorama98.ru.
Поглощены → подпапки монорепы:
- `victor/pilonuxt` → `apps/web` (Nuxt-сайт)
- `OpeItcLoc03/pilorama98.ru-legacy` → `apps/legacy` (старый сайт)
- `OpeItcLoc03/snolla-products-loader` → `apps/loader`
## Acceptance criteria
1. Все 3 Gitea-репо в archived (read-only); проверить `archived:true` через API/UI.
2. В README/описание каждого архивного репо — указатель «переехало в victor/pilorama98.ru → apps/<...>».
3. Локальные папки `~/projects/{pilonuxt,pilorama98.ru-legacy,snolla-products-loader}` удалены ПОСЛЕ подтверждения архива. Предохранитель перед rm: `git -C <dir> status` чист И `git -C <dir> log @{u}..` пуст (нет unpushed). Контент уже в монорепе и запушен.
4. projects-meta: поллер не должен surface'ить stale-доски этих репо (archived → skip; свериться что `meta_status.archived_skipped` вырос). Активные задачи уже перенесены в монореп.
5. Shared meta-wiki: если есть страницы про эти 3 проекта — обновить (переехали в монореп); если нет — короткая запись в concepts о консолидации pilorama-монорепо.
6. Отчёт в inbox pilorama98.ru.
## Обязательные скилы — вызвать до начала
- invoke `using-projects-meta` — meta-wiki/tasks мутации (preview→confirm) + freshness-gate
- invoke `using-tasks` — статус этой задачи
- invoke `project-discipline` — дисциплина коммитов/пушей
**TDD:** нет — ops/админ-задача (verification = archived:true + папки удалены + meta обновлён).
**Разрешения:** интерны: нет | автопуш: да
**weight:** needs-human
**notify:** pilorama98.ru
**Status:** done
**Where I stopped:** closed — 5/5 acceptance met (archived:true ×3 / README+desc указатели / папки удалены / поллер скипает 1→4 / shared-wiki concept создан). Отчёт пиру отправлен (с замечанием по их sequencing-багу — две связанные таски делегированы в обратном порядке).
**Next action:** —
**Branch:** n/a
**Notify:** victor/pilorama98.ru
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilorama98.ru / 2026-06-18T17:38:42.828Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-18 / archived ×3 + указатели + папки rm + поллер skip 1→4 + shared-wiki concept; archive опередил deploy-sequencing без последствий -->
---
## 🟢 [deploy-pilorama-web-from-monorepo] — Раскатать на прод текущий `apps/web` из монорепы `victor/pilorama98.ru` — содержит SEO on-page фиксы (тех-аудит 2026-06-18): `buildProductSeo` (172 товарных title+meta, commit `03c250b`) + `seoOverrides` (16 category/service title + 8 meta, commit `b73c7c8`). На проде их пока НЕТ.
**Ключевое — перенацелить источник сборки.** Пайплайн исторически собирал из `victor/pilonuxt` (см. web-вики `apps/web/.wiki/concepts/docker-deploy.md`). После консолидации 2026-06-18 код в `victor/pilorama98.ru/apps/web/` (Dockerfile приехал с subtree, там же). Сборку надо перенаправить: clone монорепы, build-контекст = `apps/web/`.
## Acceptance criteria
1. Build из `victor/pilorama98.ru`, контекст `apps/web/` (двухстадийный Nitro node-server; VERDACCIO_TOKEN как docker build-secret — живой JWT len~300, npmAlwaysAuth:true; порядок COPY scripts/ + src/generated/ до install — всё в docker-deploy + verdaccio-token-lifecycle).
2. Деплой образа на VDS (Portainer стек за Traefik) — обычный re-deploy.
3. **Smoke на проде (verify, не «собралось=работает»):** товарная `…/shop/products/<slug>` → `<title>` ≤60 вида «{товар} — купить в СПб» + meta description уникальная с «доставка по СПб и ЛО»; категория `…/catalog/planken` → короткий title «Планкен прямой и скошенный — купить в СПб»; `…/shipping` и блог-пост → непустая meta.
4. Зафиксировать в web-вики (docker-deploy.md / log), что build-источник теперь монореп `apps/web`.
## Sequencing (важно)
Перенацелить пайплайн на монореп **ДО** архива `victor/pilonuxt` (таска `archive-pilorama-source-repos`). Иначе после архива деплой со старого репо не подхватит SEO-фиксы — их в pilonuxt нет. Архив pilonuxt — только после зелёного деплоя с монорепы.
## Обязательные скилы — вызвать до начала
- invoke `using-tasks` — статус задачи
- invoke `project-discipline` — коммиты/пуши
- invoke `verify` — РЕАЛЬНО открыть прод-URL и глазами увидеть title/meta, не доверять «собралось»
- invoke `using-projects-meta` — отчёт обратно
**TDD:** нет — deploy/ops-задача (verification = smoke на проде).
**Разрешения:** интерны: нет | автопуш: да
**weight:** needs-human
**notify:** pilorama98.ru
**Status:** done
**Where I stopped:** prod зелёный на 6c6d52d — контейнер-smoke все 200 (ecommerce-листинг 200×3, SEO title/meta рендерятся), stderr пуст. Закрыто pilorama98 по подтверждению админа.
**Blocker:** Рантайм-регресс в snolla-пакетах из монореп-лока. `apps/web` тянет `@snollajs/content-api@^0.11.0`, который nested-депом подтягивает **`@snollajs/snolla@0.11.0`** (рядом с прямым `@snollajs/snolla@0.16.0`). content-api зовёт `SitesService.getSiteById` из 0.11.0 → `Cannot read properties of undefined (reading 'findByPk')` (Sites Sequelize-модель не инициализирована) → весь SSR 500. **Откатил прод на `19a4a84` (verified 200).** Фикс версий snolla — зона web/snolla-владельца (пир + snolla upstream), не ops. Артефакты сборки (новый `apps/web/Dockerfile` + root `.dockerignore`) лежат в рабочем дереве монорепы, готовы к коммиту после фикса депов.
**Update 2026-06-18 (2):** Пир снял snolla-блокер (commit `eaee56f`: content-api@0.12 peerDep + snolla@^0.17 + vue→3.5.38). Пересобрал из `eaee56f`, задеплоил → **snolla-500 ушёл, home/категории 200 с верным SEO** (`/catalog/planken` → title «Планкен прямой и скошенный — купить в СПб» ✓). **НО новый блокер:** `sharp` нативный libvips не бандлится в Nitro `.output` (`libvips-cpp.so.8.18.3: cannot open shared object`) → товарный листинг `/snolla/stores/.../products` 500 → каталог не грузит товары. Платформо-зависимо (локальный prod-бандл пира на его ОС грузит sharp). **Откатил прод на `19a4a84` снова (home 200 + products 200, каталог жив).** sharp приехал с бампом депов — старый 19a4a84 нативных модулей не имел.
**Blocker (актуальный):** `sharp`/libvips не попадает в standalone Nitro output. Шов: ПОЧЕМУ sharp в товарном пути = поведение snolla/content-api; КАК забандлить = `apps/web` `nuxt.config` (nitro traceInclude/externals) ИЛИ runner-стейдж Dockerfile (скопировать `@img/sharp-*` нативы в `.output`). Образ `eaee56f` в реестре (не рабочий из-за sharp).
**Update 2026-06-18 (3):** sharp-фикс СДЕЛАН (admin, Dockerfile-side): runner-стейдж копирует `/app/node_modules/@img` (вкл. `sharp-libvips-linux-x64` с `libvips-cpp.so.8.18.3`) в `.output/server/node_modules/@img`. Verified в контейнере: `import sharp → toBuffer()` = SHARP_OK. Пересобрал+задеплоил → sharp из лога ушёл. **НО за sharp вскрылся СЛЕДУЮЩИЙ блокер:** `getSiteById findByPk undefined` опять, теперь в snolla **0.17** (`@snollajs/snolla/lib/services/sites.js:21`) на **ecommerce-роуте** (`content-api/routes/ecommerce.js:304`) → `/snolla/stores/<id>/products` 500. При этом pages-путь работает: `/` и `/catalog/planken` = 200 c SEO, viewModel ок. Т.е. Sites-модель доступна на pages-роуте, но не на ecommerce — snolla-0.17-внутренности. Был замаскирован sharp'ом (тот падал первым на том же роуте). **Откатил прод на `19a4a84` (все 200, каталог жив).**
**Blocker (актуальный):** snolla 0.17 — `getSiteById` падает на ecommerce-роуте (Sites-модель undefined именно там; pages-роут ок). Зона пира/snolla. Локальный «товар 200» пира — видимо product-detail (viewModel), а не store-products-листинг; нужен прогон именно `/snolla/stores/<id>/products` в контейнере. sharp (моя часть) закрыт; Dockerfile-фикс ждёт в дереве монорепы (`M apps/web/Dockerfile`, не закоммичен).
**Update 2026-06-19 (4) — 🟢 GREEN на проде:** пир добил 4-й слой (commit `41cf89f`: `@snollajs/data` 0.9.0→0.9.1 — forEach-async race + Windows-path-surgery, ломавшая `import()` на Linux = ровно Linux-only дефект; snolla 0.17.1 единой runtime). Пересобрал из `6c6d52d` (HEAD, вкл. деп-фикс + мой sharp `@img` COPY), запушил с VDS, задеплоил стек 16. **Контейнер-smoke ВСЁ 200:** товарная `/shop/products/brusok-…-40-50-6000` → title «Брусок обрезной естественной влажности 40×50×6000 мм» (52≤60) + meta «…**доставка по СПб и ЛО** — от производителя» ✓; `/catalog/planken` → «Планкен прямой и скошенный — купить в СПб» ✓; `/snolla/stores/<id>/products` 200×3 стабильно; home/shipping/blog 200 + meta. Контейнер `6c6d52d` 3мин без рестартов, **stderr пуст**. Acceptance 1-3 выполнены, прод отдаёт SEO.
**Остаётся (🟡):** criterion 4 — закоммитить в МОНОРЕП (`victor/pilorama98.ru`) sharp-фикс `apps/web/Dockerfile` (некоммичен) + обновить `apps/web/.wiki/concepts/docker-deploy.md` («нативных модулей нет» → sharp via snolla 0.17, runner несёт @img). Это **cross-repo push** → ждёт явного push-ok от vitya (дисциплина Rule 4). Прод от этого не зависит — он уже зелёный.
**Next action:** (none — kept until merged)
**Branch:** n/a
**Notify:** victor/pilorama98.ru
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilorama98.ru / 2026-06-18T17:44:33.214Z -->
<!-- blocked-by: vitya@.admin-exec / 2026-06-18 / build+deploy mechanics solved; prod 500 from @snollajs/snolla@0.11.0 getSiteById; rolled back to 19a4a84 (200) -->
<!-- reblocked: vitya@.admin-exec / 2026-06-18 / snolla fixed (eaee56f), но sharp/libvips не бандлится в .output → products 500; rolled back to 19a4a84 again -->
<!-- reblocked-2: vitya@.admin-exec / 2026-06-18 / sharp FIXED (admin Dockerfile @img copy, SHARP_OK); behind it getSiteById on snolla 0.17 ecommerce route → products 500; rolled back to 19a4a84 -->
<!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-06-18T21:15:28.486Z / note: prod зелёный на 6c6d52d — контейнер-smoke все 200 (ecommerce-листинг 200×3, SEO title/meta рендерятся), stderr пуст. Закрыто pilorama98 по подтверждению админа. -->
---
## 🟢 [deploy-gsc-product-name-fix] — closed 2026-06-19 — pilonuxt `c4e34d4` на проде, GSC Product `name` FIXED. Build здесь (425MB) → `docker save | ssh vds load` → push С VDS → Portainer stack 16 `pullImage:true` (тег 6c6d52d→c4e34d4). **Acceptance MET:** in-container smoke на VDS + прод-smoke (curl --resolve мимо LAN-DNS) — 3 товарных (`planken-{prjamoj,skoshennyj,zavaltsovannyj}`) → 200, внутри `schema.org/Product` ведущий `<meta itemprop="name">` = название товара (было только Brand `Пилорама 98`). 10 nav-роутов 200, без регрессии. Inbox victor/pilorama98.ru отправлен. — Собрать и переразвернуть прод-образ pilonuxt из master (commit `c4e34d4`) — критический GSC-фикс: микроразметка Product `name` (товары выпадали из выдачи Google «Описания товара → отсутствует поле name»).
## Что деплоим
- Источник: `victor/pilorama98.ru` master @ `c4e34d4` (уже запушен в git.kzntsv.site).
- Фикс затрагивает только `apps/web/app/pages/shop/products/[slug].vue` (SSR-микроданные Product).
## Шаги (по concept .wiki/concepts/docker-deploy.md)
1. Build из корня монорепы: контекст = корень, `-f apps/web/Dockerfile`, `yarn workspace nuxt-app build`. VERDACCIO_TOKEN — живой JWT через build-secret. `src/generated/` gitignored — собирать из populated dir (или `yarn schema-gen` заранее), иначе COPY упадёт. Порядок COPY: scripts/ до install. sharp @img: runner копирует полный `@img` из builder.
2. ⚠️ **ПУЛЛ ИЗ РЕЕСТРА.** Push образа в `registry.kzntsv.site/pilonuxt:c4e34d4`, затем стек 16 в Portainer **должен подтянуть свежий образ из registry** (re-pull, не крутить кэш старого тега). Без явного pull задеплоится старый код.
3. Перекат Portainer-стека 16 за Traefik.
## Acceptance
- Rich Results Test / view-source на товарной странице (`/shop/products/*`) в контейнере/на проде показывает непустой `<meta itemprop="name">` внутри `schema.org/Product`.
- Verify ТОЛЬКО в контейнере/на проде (локальный node .output на Windows маскирует баг фоллбэком в корневой node_modules).
- После деплоя отписать в inbox `victor/pilorama98.ru` — владелец нажмёт «Проверить устранение» в GSC.
## Обязательные скилы — вызвать до начала
- invoke `using-tasks` — управление статусом задачи
- invoke `project-discipline` — дисциплина коммитов/деплоя
- свериться с `.wiki/concepts/docker-deploy.md` (в repo pilorama98.ru, apps/web) — runbook деплоя: грабли sharp @img, порядок COPY, verdaccio-token
**TDD:** н/п — ops-деплой; код-фикс уже с тестами в исходном проекте (46 зелёных).
**Разрешения:** интерны: н/п | автопуш: н/п (ops, в наш repo уже запушено).
**weight:** needs-human (deploy-инфра: docker/registry/Portainer/Traefik).
**notify:** victor/pilorama98.ru
**Status:** done
**Where I stopped:** closed — деплой завершён и верифицирован на проде (digest sha256:577d8b9). Compose SOT (`host-stacks/vds-kzntsv/pilonuxt.compose.yml`) выровнен на c4e34d4.
**Next action:** — (владелец нажмёт «Проверить устранение» в GSC)
**Branch:** n/a
**Notify:** victor/pilorama98.ru
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilorama98.ru / 2026-06-19T06:43:45.922Z -->
---
## 🟢 [deploy-pilonuxt-static-pages-robots] — closed 2026-06-29 — `40bb383` LIVE на проде (стек 16). DB-backed staticPages + robots.txt отдаются Nitro. Build-пайплайн: master@40bb383 → yarn install → **форс** `rm src/generated`+`schema-gen 0.6` (новый клиент несёт `getStaticPage`/`getRobotsTxt`, грабля обойдена) → docker build 425МБ → push с дома упал на `.output`-слое (EOF/499) → fallback `docker save | ssh vds load` + push с VDS (локально) ✓ → Portainer PUT стек 16 (`pullImage:true`). **Smoke acceptance 6/6** (мимо LAN-DNS, `--resolve` 89.253.255.94): `/robots.txt`→200 text/plain полный из БД (Yandex Clean-param + Sitemap, не allow-all, не статика); `/yandex_*.html`→200 `Verification:`; `/google*.html`→200; `/nope.html`→404 (Nuxt fall-through, не 500); `/`+`/catalog`→200. Notify victor/pilorama98.ru отправлен.
<details><summary>исходный наряд</summary>
## (orig) Деплой pilonuxt master (commit `40bb383`, включает feature `b32808d`) на прод VDS. Новая фича: DB-backed staticPages + robots.txt через content-api (ADR-0009). Build образа из Gitea master → push в `registry.kzntsv.site/pilonuxt:<sha>` → перекат Portainer-стека 16 за Traefik. Детали: apps/web/.wiki/concepts/docker-deploy.md.
## ⚠️ Критично перед билдом — форс-регенерация клиента
deps подняты: `@snollajs/snolla ^0.18`, `content-api ^0.13`, `content-api-client ^0.10`, `@snolla/site-schema-gen ^0.6`. Сгенерённый клиент `apps/web/src/generated/` получил НОВЫЕ методы `getStaticPage` / `getRobotsTxt`. `src/generated/` **gitignored** → его нет в Gitea, в build-контексте надо сгенерить заново именно schema-gen@0.6.0.
- **НЕ полагаться на ensure-schema skip-if-present:** если в рабочей папке остался `src/generated/` со старого деплоя (без новых методов) — postinstall его пропустит, соберётся СТАРЫЙ клиент, и server-код (`server/middleware/snolla-static-pages.ts`, `server/routes/robots.txt.get.ts`) упадёт на отсутствующих методах.
- Порядок: обновить master до `40bb383` → `yarn install` (тянет schema-gen 0.6 из verdaccio) → **`rm -rf apps/web/src/generated` → `yarn workspace nuxt-app schema-gen`** (нужен egress в MSSQL на build-prep) → docker build (context = корень монорепы, `-f apps/web/Dockerfile`).
## Acceptance (smoke на проде после переката)
- `GET /robots.txt` → 200 `text/plain` из БД (полный robots с Yandex-секцией + Sitemap), НЕ старый allow-all плейсхолдер.
- `GET /yandex_a4e129ae82fc9db5.html` → 200 `text/html` (Yandex-верификация).
- `GET /googlec155573e2673fe09.html` → 200.
- `GET /nope-not-a-page.html` → 404 (Nuxt fall-through, не 500).
- `/` + `/catalog` → 200 (без регресса).
- Убедиться, что `/robots.txt` не перехватывается traefik/статикой поверх Nitro (раньше был `public/robots.txt` — удалён в этом релизе).
## Обязательные скилы — вызвать до начала работы
- invoke `project-discipline` — дисциплина коммитов/пушей
- invoke `using-tasks` — управление статусом задачи
- invoke `using-vds-ops` — smoke контейнера/логов + HTTP-проверка прод-URL после переката
- invoke `using-projects-meta` — cross-project lifecycle/notify
**TDD:** нет — ops/deploy, кода не пишем.
**Разрешения:** интерны: нет | автопуш: да
**weight:** needs-human (deploy-инфра: docker/traefik/Portainer)
**notify:** victor/pilorama98.ru
**Status:** done
**Where I stopped:** деплой завершён — контейнер `pilonuxt` на `registry.kzntsv.site/pilonuxt:40bb383`, running; acceptance 6/6 verified.
**Next action:** — (closed)
**Branch:** n/a
**Notify:** victor/pilorama98.ru
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilorama98.ru / 2026-06-29T08:52:31.094Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-29 / 40bb383 LIVE стек 16, smoke acceptance 6/6 (robots+staticPages из БД verified телами), notify sent -->
</details>
---
## 🟢 [deploy-pilonuxt-forms-api-drop-snolla] — closed 2026-06-29 — `cf2bba2` LIVE (стек 16). ADR-0010: forms на `@snollajs/forms-api`, `@snollajs/snolla` дропнут целиком, core@0.1.1. Пайплайн: master@cf2bba2 → yarn install → форс `rm src/generated`+schema-gen → docker build 424МБ → save|ssh load + push с VDS (push с дома 499 как всегда) → Portainer PUT стек 16. **Tree-check `.output/server/node_modules` 4/4** (грабля #2): 0× snolla, 0× btoa, ровно 1× data@0.9.1, core 0.1.1 + content-api 0.14.0 + forms-api 0.1.0. **Smoke acceptance зелёный:** `/`+`/catalog`→200 (SSR жив, btoa/snolla-дроп бандл не сломал); `/robots.txt`+`/yandex_*.html`→200 (ADR-0009 не регресс); форма `POST /snolla-forms/forms?path=/checkout` пустой → **422 JSON** (`is_valid:false`, name/phone/email errors), `?path=/nope`→404 (valid НЕ слал — реального письма нет); stderr чист (нет btoa/snolla/MODULE_NOT_FOUND). Гоча smoke: 422 только с `Accept: application/json` (иначе `prefersJson` false → ветка `status||404` отдаёт 404 — не баг). Notify victor/pilorama98.ru (монореп-inbox) отправлен.
<details><summary>исходный наряд</summary>
## (orig) Деплой pilonuxt master (commit `cf2bba2`) на прод VDS. Релиз: forms на @snollajs/forms-api + ПОЛНЫЙ дроп @snollajs/snolla из pilonuxt (ADR-0010). content-api 0.14 (без snolla-peer) + новый @snollajs/core@0.1.1. Build из Gitea master → push registry → перекат Portainer-стека 16. Детали: apps/web/.wiki/concepts/{adr-0010-forms-api-consumer-drop-snolla,docker-deploy}.md.
## ⚠️ Критично #1 — форс-регенерация клиента (как в прошлый деплой)
`apps/web/src/generated/` gitignored. Порядок: master→`cf2bba2` → `yarn install` → **`rm -rf apps/web/src/generated` → `yarn workspace nuxt-app schema-gen`** (egress в MSSQL) → docker build. Иначе соберётся stale-клиент.
## ⚠️ Критично #2 — проверка дерева на prod `.output` ПЕРЕД перекатом
Это главный риск релиза (вся сага content-api↔snolla про dual-instance; dev маскирует unmet-deps). После `yarn install`/в собранном `.output/server/node_modules` проверить:
- **0× `@snollajs/snolla`** (полностью удалён; `yarn why @snollajs/snolla` пусто). Если транзитив притянул назад — СТОП, пинг victor/pilorama98.ru.
- **0× `btoa`** (был баг core@0.1.0, фикс в 0.1.1 — Buffer вместо btoa).
- **ровно 1× `@snollajs/data@0.9.1`** (НЕ две копии — иначе getSiteById падает, см. сагу).
- `@snollajs/core@0.1.1`, `@snollajs/content-api@0.14`, `@snollajs/forms-api@0.1.0` присутствуют.
Если что-то не так на `.output` — НЕ катить, написать victor/pilorama98.ru (snolla на standby, разберём).
## Acceptance (smoke на проде после переката)
- `GET /` + `/catalog` → 200 (SSR жив — это и есть проверка, что btoa/snolla-дроп не сломал бандл).
- `GET /robots.txt` → 200 из БД, `/yandex_a4e129ae82fc9db5.html` → 200 (ADR-0009 не регрессировал).
- **Форма (НЕ слать valid на боевую!):** `POST /snolla-forms/forms?path=/checkout` пустой → 422 JSON `{is_valid:false,errors:[...]}`; `?path=/nope` → 404. Этого достаточно — valid-сабмит НЕ слать (уйдёт реальное письмо менеджеру + запись). Контракт valid уже проверен на dev (#2427 на тест-форме).
- Контейнер running, stderr без btoa/snolla/MODULE_NOT_FOUND.
## Обязательные скилы — вызвать до начала работы
- invoke `project-discipline` — дисциплина коммитов/пушей
- invoke `using-tasks` — управление статусом задачи
- invoke `using-vds-ops` — smoke контейнера/логов + HTTP прод-URL после переката
- invoke `using-projects-meta` — cross-project lifecycle/notify
**TDD:** нет — ops/deploy, кода не пишем.
**Разрешения:** интерны: нет | автопуш: да
**weight:** needs-human (deploy-инфра: docker/traefik/Portainer)
**notify:** victor/pilorama98.ru
**Status:** done
**Where I stopped:** деплой завершён — `pilonuxt` на `registry.kzntsv.site/pilonuxt:cf2bba2` running; tree-check 4/4 + smoke acceptance зелёный.
**Next action:** — (closed)
**Branch:** n/a
**Notify:** victor/pilorama98.ru
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilorama98.ru / 2026-06-29T19:50:46.958Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-29 / cf2bba2 LIVE стек 16, tree-check 4/4 + smoke acceptance (форма 422/404, SSR 200, robots/yandex из БД), notify sent (монореп-inbox) -->
</details>
---
## 🟢 [pilonuxt-deploy-sitemap] — [weight: needs-human — deploy-infra] Деплой pilonuxt с sitemap.xml через content-api. Собрать образ из Gitea master victor/pilorama98.ru @ 42f9d64 (feat: sitemap via content-api proxy, content-api@0.15.0), запушить в registry.kzntsv.site/pilonuxt:<sha>, перекатить Portainer-стек 16 за Traefik. Грабли билда (как в прошлых pilonuxt-деплоях): форс `rm apps/web/src/generated && yarn workspace nuxt-app schema-gen` перед билдом; tree-check `.output` — 0× snolla, 0× btoa, ровно 1× @snollajs/data@0.9.1, и content-api теперь 0.15.0 (+ @snollajs/core@0.2.0). DB-миграция НЕ нужна — sitemap-колонки на проде уже есть (snolla live-проверил на прод-MSSQL). Локально verified против прод-MSSQL.
**Status:** 🟢 closed 2026-06-29 — `pilonuxt:42f9d64` LIVE на проде (Portainer стек 16).
**Where I stopped:** Собран из Gitea master `victor/pilorama98.ru @ 42f9d64` (контекст=корень монорепы, `-f apps/web/Dockerfile`, verdaccio build-secret). Форс-регенерация `apps/web/src/generated` против прод-MSSQL (schema-gen ok). **Tree-check `.output/server/node_modules` 5/5:** content-api **0.15.0**, core **0.2.0**, data **0.9.1** (ровно 1×), forms-api 0.1.0; snolla-пакетов 0 (7 grep-хитов = только комменты-провенансы, 0 живых import), btoa 0 пакетов/0 импортов. Push в registry **напрямую** (499 не случился, digest `sha256:d937c6b3…`). Стек 16 перекатан PUT API (`cf2bba2`→`42f9d64`, pullImage:true), контейнер running, Nitro Listening, stderr чист (ECONNRESET нет). DB-миграция не требовалась.
**Acceptance на проде (curl --resolve мимо LAN-DNS, 89.253.255.94) — 3/3 + регресс:** (1) `/sitemap.xml` → 200 application/xml, `<sitemapindex>`, 6 источников ✓; (2) `/sitemap-pages-1.xml` → 200 application/xml, `<urlset>`, 70 url, все 70 абсолютные `https://pilorama98.ru/` ✓; (3) `/sitemap-store-products-999.xml` → 404 ✓. Регресс цел: `/robots.txt` 200 (Sitemap-директива на месте), `/`+`/catalog` 200. Откат при нужде: стек 16 → `pilonuxt:cf2bba2`. Ack отправлен в `victor/pilorama98.ru` inbox.
**Branch:** n/a
**Notify:** victor/pilorama98.ru ✅ ack sent
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilorama98.ru / 2026-06-29T20:46:40.071Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-29 / deploy 42f9d64 LIVE + smoke 3/3 -->
---
## 🟢 [pilonuxt-deploy-sitemap-taxonomy] — [weight: needs-human — deploy-infra] Деплой pilonuxt: sitemap taxonomy (категории/теги/vendors). Bump-only поверх уже-LIVE sitemap v1 — `@snollajs/content-api` 0.15.0→0.16.0 (несёт `@snollajs/core@0.4.0`, taxonomy enumeration), код прокси не менялся (generic-шим). Собрать из Gitea master victor/pilorama98.ru @ 6b9ae63, push в registry.kzntsv.site/pilonuxt:<sha>, перекат Portainer-стека 16. Грабли билда как обычно: `rm apps/web/src/generated && yarn workspace nuxt-app schema-gen`; tree-check `.output` — 0× snolla, 0× btoa, ровно 1× @snollajs/data@0.9.1, content-api теперь 0.16.0 + core 0.4.0. DB-миграция НЕ нужна. Локально verified против прод-MSSQL.
**Status:** 🟢 closed 2026-06-29 — `pilonuxt:6b9ae63` LIVE на проде (Portainer стек 16).
**Where I stopped:** Bump-only поверх sitemap v1. Собран из Gitea master `6b9ae63` (форс schema-gen против прод-MSSQL). **Tree-check `.output` 5/5:** content-api **0.16.0**, core **0.4.0**, data **0.9.1** (1×), snolla 0 пакетов/0 import, btoa 0. Push: словил **499 на .output-слое** (документированная грабля) → ретрай прошёл, digest `sha256:226584ce…`. Стек 16 перекатан PUT (`42f9d64`→`6b9ae63`, pullImage:true), running, Nitro Listening, stderr чист.
**Acceptance на проде (curl --resolve мимо LAN-DNS, 89.253.255.94) — 4/4 + регресс:** (1) `/sitemap.xml` → 200, индекс 7 записей, есть `sitemap-store-taxonomy-1.xml` ✓; (2) `/sitemap-store-taxonomy-1.xml` → 200 application/xml, `<urlset>`, 57 url все абсолютные (35 `/shop/categories/` + 22 `/shop/tags/`; vendors 0 — пусто в CMS, как в NB) ✓; (3) v1 цел: `/sitemap-store-products-1.xml` 200, `/sitemap-pages-1.xml` 200 ✓; (4) `/sitemap-store-taxonomy-999.xml` → 404 ✓. Регресс: `/`+`/catalog` 200. Откат при нужде: стек 16 → `pilonuxt:42f9d64`. Ack отправлен в `victor/pilorama98.ru` inbox.
**Branch:** n/a
**Notify:** victor/pilorama98.ru ✅ ack sent
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/pilorama98.ru / 2026-06-29T21:06:37.502Z -->
<!-- closed-by: vitya@.admin-exec / 2026-06-29 / deploy 6b9ae63 LIVE + smoke 4/4 -->
---
## 🟢 [labtools-web-vds-deploy] — Деплой `labtools.ru` snolla-приложения (`victor/labtools.ru/apps/web`) в docker-контейнере на Rusonyx VDS за traefik. Приложение parity-подтверждено (ре-ревью №2 PASS: HTML+визуал byte-parity с боем `www.labtools.ru`, ассеты из MinIO, ноль локальных хаков, `@snollajs/snolla@0.28.2`).
## Кто/где собирает образ (решение workshop)
**Образ собирает АДМИН на VDS** из `Dockerfile`, который пишет имплементер. build нужен docker + доступ к verdaccio (`verdaccio.kzntsv.site`) + `VERDACCIO_TOKEN` — эта инфра у админа/на VDS; имплементер владеет корректностью Dockerfile, не реестром. Кросс-машинный трансфер образа не нужен.
## Разделение обязанностей (code-ownership vs ops-ownership)
**Имплементер (`victor/labtools.ru`, отдельная сессия — код-артефакты в репо `deploy/`):**
- `deploy/Dockerfile` (multi-stage: node-build с `--build-arg VERDACCIO_TOKEN` → `yarn install` @snollajs/* из verdaccio → slim runtime, `node server.js`), `.dockerignore`, `PORT`-env, healthcheck.
- **Config/секреты — через env/mounted, НЕ запекать в образ** (config `default.json` gitignored). Отдать админу **список требуемых runtime-env**: DB (`mssql.kzntsv.site` MoreThenCms creds), S3/MinIO (`minio.kzntsv.site` keys), `siteId=D375C419-D787-4FCC-8934-A3B9DC951006`, imgproxy key/salt, verdaccio (только build-time).
- Отчёт workshop: Dockerfile готов + build-args + полный env-лист.
**Админ (эта таска — ops-acceptance на VDS):**
- Собрать prod-образ на VDS из Dockerfile имплементера (VERDACCIO_TOKEN из secret-store).
- Docker stack/compose + **traefik labels** (Host-rule, TLS/letsencrypt, service-port).
- **Runtime-секреты на VDS** как env (DB/S3/siteId/imgproxy) — не в образе, не в git.
- **DNS/cutover:** сперва деплой на **staging-host** (напр. `labtools.vds.kzntsv.site`) за traefik → smoke parity на задеплоенном URL vs бой. **Cutover живого `labtools.ru` DNS = ОТДЕЛЬНЫЙ gated шаг, подтвердить с оператором** (прод-переключение, план отката) — бой сейчас живой на RUVDS IIS.
- Внешний smoke: главная styled, каталог+порядок, редиректы, canonical, ассеты из MinIO — parity с боем.
- **Вики-рунбук** (invoke `using-wiki`): stack, traefik, env-контракт, образ/тег, cutover + rollback.
Оба — **репорт в inbox workshop** (from labtools / from admin) по ходу.
## Обязательные скилы — вызвать до начала
- invoke `using-vds-ops` (диагностика VDS-контейнеров) · invoke `using-wiki` (рунбук после деплоя) · invoke `project-discipline`
**TDD:** n/a (ops-деплой; приёмка = внешний smoke parity vs бой).
**Разрешения:** интерны: нет | автопуш: админ по своей дисциплине.
**weight:** needs-human
**notify:** OpeItcLoc03/workshop
Dev-source: `victor/labtools.ru/apps/web` (parity-док/буфер — в `.workshop/.brainstorm/snolla-sites-migration.md`). Blast: cutover затронет живой прод labtools.ru — staging-first, cutover gated.
**Status:** 🟢 CLOSED 2026-07-02 — cutover LIVE. `labtools.ru`+`www` на VDS (89.253.255.94), стек 17, образ `labtools:43e28ba`. Рунбук § Cutover обновлён.
**Closure:** DNS зона на **Yandex DNS** (не reg.ru). Флип шёл неравномерно — dns1/dns2 расходились; cutover-гейт = ОБА авторитетных Yandex-NS согласованно → VDS по apex+www (монитор дождался). Стек 17 traefik-rule → `Host(labtools.ru)||Host(www.labtools.ru)` (Portainer PUT, env 8/8, pullImage=false); LE-cert выпущен (~t+20s). Live-smoke GREEN: оба хоста 200 (nav+каталог-секции+продукты), редиректы (`/about/` trailing, `/Contacts` lowercase, `/index.php`→`/`) parity, sitemap index (host=labtools.ru non-www — siteUrl тенанта без www), theme-ассеты byte-identical (RUVDS 301 apex→www → прямой md5 артефакт, с -L MD5-OK). staging-host убран (→404). Внешне: apex→VDS 200; www ещё split-brain на кэше RUVDS (200, идентичный контент, тухнет по TTL 6ч). RUVDS = rollback, не тронут. Репорт workshop отправлен. **Весь тираж закрыт: labtools.ru + labtools.pro LIVE на VDS.**
**Next action:** — (закрыто). Опционально позже по решению оператора: снять RUVDS-биндинги labtools.ru/www (как для emspb) — сейчас оставлены как rollback.
**Branch:** master
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-01T13:02:23.634Z -->
---
## 🟢 [emspb-web-vds-deploy] — closed 2026-07-02 — staging GREEN + cutover LIVE. Образ `registry.kzntsv.site/emspb:b6e361a` (digest `8f5ba02…`) собран на VDS, Portainer stack `emspb` (Id 18). Staging smoke vs бой 23/23 status-parity, theme-CSS+image из MinIO md5-identical, Cache-Control 86400. **Cutover выполнен в ту же сессию** (оператор флипнул DNS emspb.ru/www→VDS): traefik-rule `Host(emspb.ru)||Host(www.emspb.ru)` (staging-хост убран post-cutover→404), единый LE-cert, live-smoke `www.emspb.ru`+`emspb.ru`→200 `ssl_verify=0`, canonical нормализован, live==старый RUVDS байт-в-байт. RUVDS IIS `80.64.31.36` цел = rollback. Рунбук [emspb-vds-deploy-runbook](../.wiki/concepts/emspb-vds-deploy-runbook.md). Notify→workshop.
<!-- HISTORICAL task spec preserved below -->
## ⚪ [emspb-web-vds-deploy] — Деплой `victor/emspb.ru` `apps/web` (snolla-приложение, БД-контент+SSR Liquid) на Rusonyx VDS в docker за traefik, **staging-first**. Зеркало `[labtools-web-vds-deploy]` — тот же паттерн, адаптировать хосты/siteId. Код готов (ре-ревью PASS, ноль хаков, 29/29 parity). `deploy/` артефакты уже в репо.
## Источник
`victor/emspb.ru` — `deploy/Dockerfile` (multi-stage, build-arg `VERDACCIO_TOKEN` build-time, node:22-slim non-root, healthcheck) + `deploy/README.md` (env-лист, все секреты через env, ничего не бейкается) + корневой `.dockerignore`. Референс-рунбук: admin-вики `labtools-vds-deploy-runbook`.
## Split (как labtools — dev даёт Dockerfile, ops собирает и катит)
1. **Образ собирает АДМИН на VDS** из emspb `deploy/Dockerfile` (docker+verdaccio+token на VDS), из **чистого git-archive** (config/default.json отсутствует ✅ — секретов в образе нет). Тег `registry.kzntsv.site/emspb:<sha>`.
2. **Portainer stack** на STAGING `emspb.vds.kzntsv.site` за traefik (Host-rule + LE cert + порт).
3. **Runtime-env-секреты в stack-env (НЕ в образ):** DB `mssql.kzntsv.site`/`MoreThenCms`, S3/MinIO, `siteId=96EBC481-D26A-47BE-B660-13D49E7D0A61`, imgproxy HMAC (кросс-проверить как labtools).
4. **Smoke parity vs бой `www.emspb.ru`:** страницы 200 (main/contacts/portfolio/prices + service-выборка), canonical 301 (trailing/lowercase), theme-ассеты из MinIO byte-identical, Cache-Control как бой.
## НЕ делать
- **Cutover live `emspb.ru` DNS — НЕ трогать.** Отдельный gated-шаг позже по слову оператора (бой на текущем хостинге живой; rollback = DNS назад). Эта таска = только staging + smoke.
- Секреты в образ/git — нет.
## Ops-acceptance
Внешний smoke parity vs бой `www.emspb.ru` на задеплоенном staging-URL `emspb.vds.kzntsv.site` = GREEN. Рунбук в admin-вике `emspb-vds-deploy-runbook` (зеркало labtools).
## Обязательные скилы — вызвать до начала работы
- invoke `using-tasks` — статус задачи
- invoke `project-discipline` — дисциплина
- invoke `using-vds-ops` — диагностика контейнеров на VDS при необходимости
- invoke `using-wiki` — записать рунбук `emspb-vds-deploy-runbook` в admin-вику
**TDD:** n/a (ops-деплой).
**Разрешения:** ops-задача админ-сессии.
**weight:** needs-human
**notify:** OpeItcLoc03/workshop
**Status:** 🟢 closed 2026-07-02 — staging + cutover оба GREEN, emspb.ru LIVE на VDS.
**Where I stopped:** (done) Образ b6e361a собран+запушен на VDS, stack Id 18, staging smoke 23/23, cutover выполнен (traefik-rule на боевые хосты, LE-cert, live-smoke 200/valid, паритет vs RUVDS байт-в-байт). RUVDS rollback цел.
**Next action (было):** Собрать образ на VDS из victor/emspb.ru deploy/Dockerfile (чистый git-archive, секретов нет) → registry.kzntsv.site/emspb:<sha> → Portainer stack STAGING emspb.vds.kzntsv.site за traefik+LE → runtime-env-секреты → smoke parity vs бой www.emspb.ru. Рунбук в admin-вику. Cutover live DNS — НЕ трогать (отдельный gated-шаг).
**Branch:** n/a
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-01T21:19:42.728Z -->
---
## 🟢 [labtools-pro-web-vds-deploy] — Вынести `victor/labtools.pro` (snolla-app, `@snollajs/snolla@0.28.7`, server-side Liquid, БД-контент + MinIO-ассеты) с RUVDS IIS в docker-контейнер на VDS kzntsv (89.253.255.94) за traefik. **Зеркало `[emspb-web-vds-deploy]`** — тот же паттерн; рунбуки `emspb-vds-deploy-runbook` / `labtools-vds-deploy-runbook` (.admin вика) читать как шаблон. Код-DONE: ре-ревью PASS (все 9 дименшнов, ноль хаков), независимый ревьюер на живой БД+MinIO.
**Weight:** needs-human
**Notify:** OpeItcLoc03/workshop
## Артефакты (labtools.pro-специфика — отличия от emspb выделены)
- **Код:** `victor/labtools.pro` @ **`7bd9fae`** (apps/web, PASS, snolla **0.28.7** final). `deploy/Dockerfile` (multi-stage node:22-slim non-root, healthcheck `/robots.txt`) + `deploy/README.md` (env-лист).
- **Образ:** ⚠️ **`registry.kzntsv.site/labtools-pro:7bd9fae`** (+`:latest`) — имя **`labtools-pro`**, НЕ `labtools` (не коллизить с образом labtools.ru!). Собрать НА VDS из чистого `git archive 7bd9fae` (обход traefik-499; guard: `config/default.json` ABSENT в архиве, `.dockerignore`). build-arg `VERDACCIO_TOKEN` (build-time only, не в финальный образ).
- **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 отдают www). Non-secret.
## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git)
**Значения идентичны labtools/emspb** (все snolla-тенанты = одна MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). **Переиспользовать env-массив verbatim из labtools stack (Id 17)** через Portainer API (как для emspb stack 18). Маппинг env→config: `apps/web/config/custom-environment-variables.json`. 8 env: DB_USER/DB_PASSWORD, IMGPROXY_KEY/IMGPROXY_SALT, S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY, SMTP_USER/SMTP_PASSWORD. PORT не переопределять (образ 5000; traefik service-port 5000).
## Стек + staging
Portainer stack **`labtools-pro`** (новый), endpoint 1, source-compose `admin/host-stacks/vds-kzntsv/labtools-pro.compose.yml`. Staging-хост **`labtools-pro.vds.kzntsv.site`** за traefik+LE. Egress: mssql.kzntsv.site:1433, minio.kzntsv.site:443, imgproxy.kzntsv.site:443, smtp.yandex.ru:465.
## Staging smoke — parity vs бой `www.labtools.pro` (RUVDS, через `--resolve`)
Acceptance = внешний smoke на задеплоенном URL:
- **Каталог!** (в отличие от контент-only emspb): nav (`/ /about /contacts`) + 3 каталог-секции (`/products/laboratory-presses`, `/products/press-forms`, `/products/milling-accessories`) + продукты (`/products/laboratory-presses/plg-20`, press-form) — статус 200/200 vs бой.
- **redirect#1:** `/products/laboratory-ball-mills` → 301 → `…/lshm-750`. seoCanonical: `/Contacts`→301 lowercase, `/contacts/`→301 trailing.
- **theme-ассеты из MinIO md5-identical** бою (theme.css, logo.png, шрифты).
- ⚠️ **Cache-Control: НЕТ заголовка на theme css/js/images** (labtools.pro `CachingOptions=[]` → empty-config, движок 0.28.7 не ставит заголовок = бой). **НЕ флагай отсутствие Cache-Control как дефект** — для этого тенанта это КОРРЕКТНО (в отличие от emspb/labtools, где был max-age=86400).
- sitemap `/sitemap.xml` = 25 URL, `<loc>` host = **www** (R3).
## Cutover (live DNS) — ОТДЕЛЬНЫЙ gated-шаг по слову оператора
- Бой live на RUVDS IIS (rollback-цель; админ подтверждает IP — вероятно тот же `80.64.31.36`, что emspb, но проверить). **НЕ выводить RUVDS до подтверждённого cutover.**
- ⚠️ **НЕ путать с labtools.ru** — он ТОЖЕ ждёт DNS-cutover на RUVDS; флипается отдельно. Проверять внешним резолвером, что флипнут именно labtools.pro.
- Порядок (как emspb): оператор флипает DNS `labtools.pro`+`www.labtools.pro`→`89.253.255.94` → ТОЛЬКО ПОСЛЕ этого ops добавляет `Host(labtools.pro)||Host(www.labtools.pro)` в traefik-rule (иначе LE HTTP-01 упадёт на RUVDS → сожжём rate-limit) → live-smoke.
- **НЕ флипать DNS самим.**
## Скилы / отчёт
invoke `using-vds-ops`/`using-tasks`/`project-discipline`. Написать рунбук `labtools.pro-vds-deploy-runbook` в .admin вике (зеркало emspb). Репорт → OpeItcLoc03/workshop. Ops-acceptance = staging smoke-parity green; cutover — отдельным словом оператора.
**Status:** 🟢 CLOSED 2026-07-02 — cutover LIVE. `labtools.pro`+`www` на VDS (89.253.255.94), стек 19, образ `labtools-pro:7bd9fae`. Runbook: `.wiki/concepts/labtools.pro-vds-deploy-runbook.md`.
**Closure:** DNS флип подтверждён авторитетным `ns1.reg.ru` (Resolve-DnsName) + 8.8.8.8 → 89.253.255.94 (именно `.pro`; ранние проверки ловили старый TTL-кэш). Стек 19 traefik-rule → `Host(labtools.pro)||Host(www.labtools.pro)` (Portainer PUT, env 8/8 сохранён, pullImage=false); LE-cert выпущен мгновенно (ssl_verify=0). Live-smoke GREEN: оба хоста 200 (nav+каталог+продукты), редиректы parity, sitemap index host=www, контент реальный (lang=en). staging-хост убран (→404). Внешняя проверка с RUVDS-хоста: labtools.pro/www→89.253.255.94 HTTP 200 cert-trusted. RUVDS labtools.pro = rollback, жив, не тронут. Репорт workshop отправлен.
**Where I stopped:** Образ `registry.kzntsv.site/labtools-pro:7bd9fae` (+latest, digest `sha256:23d0bc59…`) собран+запушен НА VDS (guard default.json absent OK). Portainer stack `labtools-pro` **Id 19** создан (env verbatim из stack 17, 8 секретов), контейнер healthy, MSSQL+S3 подключены, лог чист. Staging `labtools-pro.vds.kzntsv.site` LE-cert выпущен. **Smoke-parity vs бой `www.labtools.pro` GREEN** (гнал с VDS): статусы nav+3 каталог-секции+продукты 200/200; редиректы (каталог#1→lshm-750, /Contacts→lowercase, /contacts/→trailing) 301 parity; sitemap = index из 7 дочерних, в сумме 25 URL набор==бой, host=www (R3); theme-ассеты 7/7 md5-identical; Cache-Control absent (корректно, `CachingOptions=[]`); контент-страницы реальное тело (delta +155…+222B = host-длина). Косметика (не дефект, отдана workshop): стартовый баннер лога = `labtools.ru (snolla)` (константа из скаффолда). Compose: `host-stacks/vds-kzntsv/labtools-pro.compose.yml`. Репорт workshop отправлен.
**Next action:** — (закрыто). Follow-up (не блокер, у workshop): косметика баннера `labtools.ru` в логе (константа app-name из скаффолда). Опционально позже: снять RUVDS-биндинги labtools.pro (как сделано для emspb) — по решению оператора, сейчас оставлены как rollback.
**Branch:** master
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-02T08:27:39.437Z -->
---
## 🟢 [imgproxy-watermark-glyph-books-vds] — closed 2026-07-04 — глиф LIVE на imgproxy стек 29 (books-vds). Первый base64 (в inbox И в таске) был битый на источнике (sha mismatch, IEND нет, зацикленный хвост — `tasks_create` порезал поле); поймал sha-сверкой ДО прода. Воркшоп до-доставил `logo.png` бинарём в git (`host-stacks/books-vds/tandemmebel-watermark-logo.png`, commit `7127e92a`, sha256 `e995…971fc`). PUT стека 29 (Portainer `portainer.kzntsv.site` ep1, X-API-Key, `IMGPROXY_WATERMARK_DATA` inline env, `PullImage:false`, БЕЗ глобального `_OPACITY`). Smoke cache-busted: A tandem 200/27294→27820B вотермарк ВИДЕН глазами; B pilorama контроль byte-identical 160010B (opacity не просочилась). Откат: PUT бэкапа стека без WATERMARK_DATA. Notify workshop отправлен → tandem-агент на ре-скрин→пин→deploy.
<!-- HISTORICAL turnkey preserved below (base64 в блоке БИТЫЙ — источник истины = git-бинарь 7127e92a) -->
## ⚪ [imgproxy-watermark-glyph-books-vds] — Ops-handoff от workshop (dev-source: victor/tandemmebel.ru pin 7e08bee; snolla core@0.13.8 commit 2ba8b54). Разблокирует финальный визуал-гейт tandemmebel post-featured watermark parity → потом deploy.
ЗАДАЧА: на **books-vds (89.253.255.133)** imgproxy **стек 29** выставить env `IMGPROXY_WATERMARK_DATA` = base64 глифа logo.png + рестарт imgproxy (Portainer стек 29 → env → redeploy, либо .env стека + docker compose up -d).
**Глиф:** 348×42 RGBA брендовый вордмарк «ТАНДЕМ МЕБЕЛЬ», sha256 `e995624735d709dbf5d0b5b5f1a46653e595a20c93270192d4b38e98207971fc`.
`IMGPROXY_WATERMARK_DATA` value (base64, одной строкой):
```
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MfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBcFQtAlGLzMfBc
```
**Проверка:** `echo '<base64>' | base64 -d | sha256sum` → должно дать `e995624735d709dbf5d0b5b5f1a46653e595a20c93270192d4b38e98207971fc`.
⚠️ **КРИТИЧНО: НЕ выставлять** глобальный `IMGPROXY_WATERMARK_OPACITY` — иначе зальёт вотермарком ВСЕ тенанты (emspb/labtools/labtools.pro/pilorama — они ДОЛЖНЫ остаться чистыми). Глиф применяется ТОЛЬКО по per-URL `wm`-опции, которую snolla шлёт лишь для tandem post-featured. Ключ именно `IMGPROXY_WATERMARK_DATA` (base64), НЕ `_BASE64`.
Рестарт imgproxy = короткий блип отдачи картинок всем сайтам (кэш-замаскирован nginx) — ратифицировано оператором.
**SMOKE после рестарта** (должен вернуть 200 image/webp С вотермарком по центру):
```
curl -sI 'https://imgproxy.kzntsv.site/nlXN7RMrer11Fe9XPQaXmq4H0ufzPpo2fOzBWtaL0po/rs:fill:1170:557:1/g:ce/wm:0.5:ce:0:0:0/czM6Ly9nYWxsZXJpZXMvNzgwODA3MDdmNmUwNDMzMGJhMzA3OTIyMzU0YzJjZWYvYWI0YmM0NDgwZjJkNDc4ZTgzYTQwOTc5NTI0NDA0NWEuSlBH.webp'
```
(до глифа тот же URL = 200 но чистый; после = с вотермарком.)
По готовности notify workshop → tandem-агент гоняет Playwright ре-скрин featured → пин → deploy. Отдельная deploy-таска `[tandemmebel-web-vds-deploy]` последует (в её production.json override `imgproxy.watermark={size:"post-featured",option:"wm:0.5:ce:0:0:0"}`).
**Status:** 🟢 closed 2026-07-04
**Where I stopped:** глиф LIVE на imgproxy стек 29, smoke green (A вотермарк виден / B контроль чист), notify workshop отправлен.
**Next action:** — (закрыто). Дальше по тиражу tandemmebel: ждать `[tandemmebel-web-vds-deploy]` от воркшопа, взять ops-часть.
**Branch:** n/a
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-04T04:30:04.466Z -->
<!-- closed-by: vitya@.admin-exec / 2026-07-04 / sha-gate поймал битый base64, git-бинарь 7127e92a раскатан, smoke green, opacity не просочилась -->
---
## 🟡 [tandemmebel-web-vds-deploy] — STAGING GREEN, ждёт cutover (DNS gated оператором). **⚠️ ОБРАЗ К ФЛИПУ ОБНОВЛЁН → `tandemmebel:ed96b18` (snolla 0.42.0/core 0.24.0/data 0.14.1, digest `ca4da79`), СУПЕРСИДИТ b02ca18/0facb35** — раскатан на стек 20 задачей [tandemmebel-deploy-snolla-0-42-0] (2026-07-04, DoD 172/172 sitemap + 12/12 gallery-grid GREEN). Cutover = тот же DNS-gated шаг: владелец флипает reg.ru `tandemmebel.ru`+`www`→89.253.255.94 → verify авторит.NS (`Resolve-DnsName -Server ns1.reg.ru`) → ТОЛЬКО ПОТОМ боевой `Host()` в стек 20 (порядок критичен: LE HTTP-01) → live-smoke. RUVDS 80.64.31.36 = rollback (revert DNS), не тронут. Sharp media in-process (imgproxy из tandem-пути убран — прошлые imgproxy-заметки ниже historical). Историческая спека (0facb35/imgproxy-watermark) ниже — HISTORICAL, не актуальна. Smoke с VDS: status-parity 10/10, sitemap staging⊇prod (prod-only=0, +12 /projects/categories/* benign#3), trailing 301/301, blog-post 200 (+3.5KB=benign#2 imgproxy vs /galleries/), 2012-посты 31/31, theme 21/22 MD5-OK. Находка (не блокер): prod Gotham-Pro.css=0B (пустой), staging=4436B корректный → cutover чинит латентный прод-баг шрифта. Следующее: отмашка оператора на DNS → verify авторит.NS → боевой Host-rule в стек 20.
<!-- HISTORICAL spec preserved below -->
## ⚪ [tandemmebel-web-vds-deploy] — Вынос tandemmebel.ru (snolla-app, `@snollajs/snolla` **0.32.2** / `@snollajs/core` **0.13.8**, server-side Liquid, контент+блог-портфолио+category, БЕЗ e-commerce каталога) с RUVDS IIS на VDS kzntsv (89.253.255.94) за traefik. **Зеркало рунбука `labtools.pro-vds-deploy-runbook`** (модель snolla-тенанта: БД MoreThenCms `mssql.kzntsv.site` + ассеты MinIO через imgproxy). Dev + ДВА полных независимых ре-ревью-раунда + focus watermark-раунд ЗАВЕРШЕНЫ, независимый **PASS**. Это ops-handoff.
### Артефакты
- **Код:** `victor/tandemmebel.ru` @ `0facb35afde2a966add75e0d603dcf8f16a868af` (master, apps/web). Раунд-2 re-review PASS (byte-parity 9/10, секреты, тесты 22/22, ноль хаков) + focus watermark-раунд PASS (featured-вотермарк=бой, мелкие чисты, cross-tenant чист). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck).
- **Образ:** `registry.kzntsv.site/tandemmebel:0facb35` (+`:latest`), собрать **НА VDS** (обход traefik-499). Движок = **snolla@0.32.2 / core@0.13.8**. ⚠️ `content-api` НЕ runtime-dep web-апп (CMS/admin API, консюмер pilorama) — в образ не входит.
- **Стек Portainer:** новый `tandemmebel` (endpoint 1), compose по labtools-pro-шаблону (сменить service/host-имена). Source-of-truth: `admin/host-stacks/vds-kzntsv/tandemmebel.compose.yml`.
- **siteId** `78080707-F6E0-4330-BA30-7922354C2CEF`, activeTheme `EFCD9555-3400-411E-A27D-68D3D8B3DDB2` (theme store MinIO `themes/efcd95553400411ea27d68d3d8b3ddb2/`), Culture **ru-RU**. **siteUrl** `https://www.tandemmebel.ru`.
- **production.json В ПИНЕ содержит** `imgproxy.watermark` override `{size:"post-featured",option:"wm:0.5:ce:0:0:0"}` — уже в образе, отдельно ставить НЕ надо.
### Runtime env (8 секретов — Portainer stack-env, НЕ в образ/git)
Значения идентичны labtools/emspb/labtools.pro (общий тенант). **Переиспользовать env-массив verbatim из labtools stack (Id 17)** через Portainer API. 8: `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`. PORT не переопределять (5000; healthcheck; traefik service-port 5000). VERDACCIO_TOKEN build-time only. `config/default.json` исключён `.dockerignore` — проверить ABSENT в архиве.
### ⚠️ ОБЯЗАТЕЛЬНЫЙ cutover-шаг — PURGE imgproxy-nginx кэша (books-vds)
Focus-ревьюер (независимо) нашёл: nginx перед imgproxy (books-vds, `Cache-Control: max-age=2592000` = 30 дней) закэшировал **ДО-глиф ЧИСТЫЕ** рендеры featured-URL для постов, запрошенных при dev-тестинге (мин. `53-podemnaya-krovat-s-uglovym-shkafom` + smoke-посты). Прод featured-URL детерминированы (без cache-bust) → эти посты отдадут **stale-clean featured (без вотермарка)** в проде до TTL/purge. **ПЕРЕД/НА cutover:** purge nginx-кэш imgproxy на books-vds (стек 29 — cache-dir nginx, напр. `docker exec <imgproxy-nginx> sh -c 'rm -rf /var/cache/nginx/* && nginx -s reload'`, либо полный purge/redeploy nginx-стека). **Verify после:** свежий featured-URL поста 53 (`.../rs:fill:1170:557:1/g:ce/wm:0.5:ce:0:0:0/...`) → 200 **с вотермарком**.
### Staging smoke (`tandemmebel.vds.kzntsv.site` vs бой www.tandemmebel.ru; гнать С VDS)
- Status-parity: контент (`/ /prices /vacancies /worksorder /contacts`), category (`/furniture/kitchens`, `/furniture/kitchens/classic`), blog-listing `/projects`, blog-post, `/404` (брендированный, не пустой) — 200/parity.
- **Редиректы:** 65 DB-редиректов, `redirects.test.js` 17/17 vs бой — spot-проверить 301 (target parity modulo host).
- **Sitemap:** snolla index-структура (валидна), все post-URL → 200, 8 постов 2012 = верные даты (core@0.13.8 tz-fix). `/articles` bare-root в sitemap → 404 = **PARITY** с боем (не дефект).
- **Featured с вотермарком** (после cache-purge!), мелкие размеры (thumbs/related/category/hero/og) — чистые.
- theme-ассеты MinIO md5 vs бой под `themes/efcd95553400411ea27d68d3d8b3ddb2/`.
- Контейнер healthy, MSSQL+S3 подключены.
### Documented benign divergences (НЕ флагать при smoke — 2 раунда ре-ревью подтвердили benign)
1. `/projects` ~10 diff = 2015-ordering isotope (within-date, визуально незаметно). 2. gallery imgproxy vs `/galleries/` direct (URL длиннее, та же картинка 800×800, тот же s3-ключ). 3. sitemap 12× `/projects/categories/*` (200, valid over-inclusion). 4. sitemap index-vs-flat (оба валидны). 5. `/articles` bare-root 404 = parity.
### Cutover (live DNS) — GATED ОПЕРАТОРОМ
1. (оператор) reg.ru: A `tandemmebel.ru` + `www.tandemmebel.ru` → `89.253.255.94` (с RUVDS).
2. (ops) verify флипа авторитетным NS (`ns1.reg.ru` через Resolve-DnsName) ПЕРЕД traefik.
3. (ops) ТОЛЬКО ПОСЛЕ flip: stack `tandemmebel` traefik-rule → `Host(tandemmebel.ru) || Host(www.tandemmebel.ru)`. Порядок критичен (иначе LE HTTP-01 упадёт на RUVDS → rate-limit).
4. Live-smoke оба хоста + внешняя проверка + **featured-вотермарк на живом**. staging-host убрать из rule.
### Rollback
DNS → RUVDS IIS (живой, не выводить до явного решения оператора). Стек remove / откат тега.
**Status:** 🔵 BLOCKED 2026-07-04 — оператор при визуальной проверке sharp-staging (стек 20, образ 68b93a9, https://tandemmebel.vds.kzntsv.site) нашёл НОВЫЙ дефект (иной, чем прошлые). Специфику не назвал — «чиним в новой сессии». Cutover HELD до фикса. Sharp-staging сам по себе green по моим гейтам; дефект — визуальный, оператор опишет в новой сессии.
**Where I stopped:** build ✅ (tandemmebel:0facb35 на VDS) + стек Portainer Id 20 (staging-rule, оставлен UP) + staging-smoke GREEN (все гейты, benign #2/3/5 учтены). Оператор поставил cutover на HOLD → решение: watermark через sharp (victor/snolla media-sharp-serve-and-watermark), НЕ imgproxy. **imgproxy глиф со стека 29 СНЯТ** (таск [imgproxy-stack29-watermark-rollback] 🟢, боевые тенанты verified чисты); глиф-бинарь 7127e92a оставлен для sharp-composite. Текущий образ 0facb35 использует imgproxy.watermark — при возврате к деплою нужен ПЕРЕСБОР на sharp-пин. Compose source-of-truth в host-stacks/vds-kzntsv/tandemmebel.compose.yml.
**Next action:** ЖДУ отмашку ОПЕРАТОРА на DNS-флип reg.ru (tandemmebel.ru+www → 89.253.255.94). Потом: verify авторит.NS (Resolve-DnsName) ПЕРЕД traefik → PUT стека 20 rule → Host(`tandemmebel.ru`)||Host(`www.tandemmebel.ru`), убрать staging-host → live-smoke оба хоста + featured-вотермарк на живом. RUVDS=rollback, не трогать.
**Branch:** n/a
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-04T05:18:05.411Z -->
<!-- staging-green: vitya@.admin-exec / 2026-07-04 / build+стек20+cache-purge+smoke green; cutover held on operator -->
---
## 🟢 [imgproxy-stack29-watermark-rollback] — closed 2026-07-04 — `IMGPROXY_WATERMARK_DATA` снят со стека 29 (books-vds), стек byte-identical pre-watermark бэкапу (cross-check true), redeploy+purge. Verify: pilorama98 (главный консюмер, 122 ref) 3 рендера == baseline (43950/172042/160010B) — не задет; stostayer НЕ imgproxy-консюмер (картинки через /galleries//images//assets/) → не затрагивается; WATERMARK_DATA present:false; tandem featured→чистый (27820→27294B, ожидаемая побочка, staging→sharp). Глиф-бинарь 7127e92a НЕ тронут (для sharp-composite). Notify workshop отправлен.
<!-- HISTORICAL spec below -->
## ⚪ [imgproxy-stack29-watermark-rollback] — СРОЧНО (оператор просил поскорее). Откатить watermark-инъекцию на SHARED imgproxy стек 29 (books-vds). Причина: tandemmebel уходит с imgproxy-watermark на in-process sharp (задача victor/snolla media-sharp-serve-and-watermark), глифу на общем инстансе больше не место. Shared инстанс держит БОЕВЫХ pilorama98/sotstayer — аккуратно.
## Шаги
1. **Verify (не по памяти):** подтвердить, что env `IMGPROXY_WATERMARK_DATA` реально сейчас присутствует на стеке 29. Если уже нет — стоп, доложить.
2. Восстановить env-бэкап БЕЗ `IMGPROXY_WATERMARK_DATA` (точечный PUT, как планировалось при заведении глифа) → redeploy стека 29 → purge imgproxy-кэша.
3. **Verify после:** pilorama98.ru + sotstayer рендерят картинки как раньше (главный критерий — не задеть боевых тенантов). Приложить проверку (curl/скрин ключевого изображения каждого).
## НЕ трогать
- Глиф-бинарь `host-stacks/books-vds/tandemmebel-watermark-logo.png` (commit 7127e92a) — ОСТАВИТЬ, переиспользуется для sharp-composite.
- Никаких других env стека 29, никаких других стеков.
## Ожидаемая побочка
tandemmebel-staging (стек 20) потеряет featured-вотермарк — ЭТО НОРМАЛЬНО (staging paused, переезжает на sharp). Не чинить.
## Обязательные скилы
- invoke `using-vds-ops` / ops-доступ к стеку — verify состояния контейнеров
- invoke `using-tasks` — статус
**weight:** needs-human (redeploy боевого shared docker-стека)
**notify:** OpeItcLoc03/workshop
**Dev-source:** victor/snolla media-sharp-serve-and-watermark (переход на sharp)
**Status:** 🟢 closed 2026-07-04
**Where I stopped:** WATERMARK_DATA снят со стека 29, redeploy+purge, pilorama/stostayer verified не задеты, глиф-бинарь оставлен. Notify workshop отправлен.
**Next action:** — (закрыто).
**Branch:** n/a
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-04T06:15:36.456Z -->
<!-- closed-by: vitya@.admin-exec / 2026-07-04 / rollback done, byte-identical to pre-watermark backup, boевые тенанты verified clean -->
---
## 🟢 [minio-variant-cache-bucket] — closed 2026-07-04 — бакет `variant-cache` создан в shared MinIO (minio.kzntsv.site/books-vds), пустой, другие 17 бакетов не тронуты. Грант: snolla S3-креды (S3_ACCESS_KEY_ID/SECRET из деплой-env) == MinIO root (сверено оба поля) → в mode-server-fs root имеет полный доступ by construction, IAM-грант не нужен (FS-режим его не держит). Write-verify ПОД деплойными кредами (явный alias): put→stat→get(md5 match 78fab481)→delete→gone ✓. Lifecycle не ставил (опц., кэш регенерируем). sharp-prereq (500 NoSuchBucket) снят. Notify workshop.
<!-- HISTORICAL spec below -->
## ⚪ [minio-variant-cache-bucket] — Создать MinIO-бакет `variant-cache` — prereq для sharp-медиа доставки. Без него sharp-движок при resize пишет готовый вариант в бакет `variant-cache`, а его нет → HTTP 500 `NoSuchBucket` на ВСЕХ sharp-картинках (agent-блокер 1, подтверждён live на tandemmebel-verify).
Это S3-эквивалент легаси App_Data/imageCache: движок ресайзит+вотермарчит on-demand и кладёт результат в этот бакет, потом отдаёт из него. Кэш ленивый/регенерируемый (можно чистить свободно — восстановится из оригиналов по заходу).
## Шаги
1. Создать ПУСТОЙ бакет `variant-cache` в том же shared MinIO, где лежат snolla-бакеты оригиналов/тем (тот же endpoint).
2. Дать сервис-кредам snolla content-api (те же, что читают оригиналы) **read+write** (putObject/getObject/headObject) на `variant-cache`. Verify, что деплойные S3-креды реально могут писать туда.
3. Confirm: бакет существует + writable тест-объектом (put→get→delete) → доложить.
## Границы / заметки
- Только СОЗДАТЬ новый бакет + грант snolla-сервису. Другие бакеты НЕ трогать.
- Имя бакета `variant-cache` — по текущему коду 0.14.0 + deploy-config. Снолла сейчас переделывает движок (задача victor/snolla media-sharp-imageprocessor-port); если в переработке имя cache-бакета изменится — она пингнёт, подкорректируешь. Пока — `variant-cache`.
- Опционально: lifecycle/retention на бакет (кэш регенерируем) — на твоё усмотрение, не обязательно.
## Обязательные скилы
- invoke `using-vds-ops` / ops-доступ к MinIO — создание/verify
- invoke `using-tasks` — статус
**weight:** needs-human (провижн на shared prod storage)
**notify:** OpeItcLoc03/workshop
**Dev-source:** victor/snolla media-sharp-imageprocessor-port (agent-блокер 1 из tandemmebel sharp-verify)
**Status:** 🟢 closed 2026-07-04
**Where I stopped:** бакет variant-cache создан + write-verify под деплойными кредами (put→get md5→delete) зелёный; snolla-креды==root (FS-режим). Notify workshop отправлен.
**Next action:** — (закрыто). Имя может смениться при media-sharp-imageprocessor-port → пересоздам по пингу.
**Branch:** n/a
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-04T07:50:45.270Z -->
<!-- closed-by: vitya@.admin-exec / 2026-07-04 / bucket created, deploy-creds write-verify green, snolla==root FS-mode -->
---
## 🟢 [tandemmebel-sharp-staging-rebuild] — closed 2026-07-04 — sharp staging GREEN. Образ `tandemmebel:68b93a9` (snolla 0.34.0/core 0.15.0 in-process sharp, digest 12288b15) собран на VDS; стек 20 обновлён (env verbatim 8/8). Smoke: nav-parity 8/8, sitemap staging⊇prod (prod-only=0, +12 categories benign#3), 2012-посты 31/31, redirects 301/301; sharp media = slug-URL 200 webp serve-bytes, **0 /imgproxy-рефов**, variant-cache пишется (32 объекта, NoSuchBucket снят); watermark визуально ✓ на featured+lightbox, gallery-small чистый; лог без ошибок. Находка (не блокер): осиротевший imgproxy-блок в production.json пина эмпирически мёртв (0 /imgproxy-рефов) — вычистить в след. пине. Cutover — НЕ в scope, gated оператором. Notify workshop.
<!-- HISTORICAL spec below -->
## ⚪ [tandemmebel-sharp-staging-rebuild] — РЕЗЮМ ДЕПЛОЯ tandemmebel (был 🟡 paused под пивот на sharp — sharp готов, снимаем паузу). Оператор: «деплоим, ты всё проверишь». Пересобрать staging на новом sharp-движке, parity-smoke. Cutover DNS — НЕ в scope, отдельный gated шаг оператора позже.
## Что нового vs прошлый staging (imgproxy-путь)
- **reviewed-sha = `68b93a95d6cd6170ecdd01a37585ece3d6221df5`** (victor/tandemmebel master, запушен) — с него собирай.
- Движок: snolla **0.34.0** / core **0.15.0** (in-process sharp, data-driven watermark из данных темы; imgproxy из пути tandem убран).
- `production.json` в репо несёт media-конфиг (`resize.engine=sharp`, `delivery=serve-bytes`, `cache.mode=local-variant`, `storage.provider=s3`) — это НЕ секрет, приезжает с образом. Рукописного `media.watermark` больше нет.
## Шаги
1. Собрать образ на VDS из ЧИСТОГО git-archive sha `68b93a95` (guard: секретов в образе нет; production.json = конфиг, не секреты). Push в registry.
2. Обновить/пересобрать staging-стек tandemmebel (прошлый — Portainer стек 20, `tandemmebel.vds.kzntsv.site`) на новый образ. Runtime-секреты (DB mssql / MinIO S3-креды / siteId) — как раньше, verbatim, в stack-env, НЕ в образ.
3. **Parity-smoke vs бой `www.tandemmebel.ru`:**
- Картинки: slug-URL `/galleries/<gid>/images/<slug>/<seq>` → 200 image/webp; **0 `/imgproxy`-редиректов наружу** (сырой /imgproxy под sharp → 404 — норма).
- Featured + lightbox (gallery-large 1200×794, post-featured 1170×557) несут **watermark** (из sharp, центр ~0.6); мелкие (gallery-small, slider) — чистые.
- variant-cache пишется в бакет `variant-cache` (без NoSuchBucket).
- Роуты/редиректы parity как прошлый staging (10/10 было зелёное).
4. Доложить staging-green + находки в inbox workshop. Всё, что «увидишь хренью» — репорти, не проглатывай.
## Заметки / guards
- **variant-cache — общий бакет:** staging пишет туда же, куда потом прод. Кэш content-addressed (source+size+watermark) → байты идентичны, staging просто пред-прогреет. Не пуга́йся объектов в бакете.
- **GothamPro heads-up (из прошлого хендоффа):** прод-tandemmebel.ru отдаёт `Gotham-Pro.css` 0 байт, staging корректный ~4436B → шрифт заголовков на staging «правее» боя. Это НЕ регресс — известная дельта, всплывёт при cutover, не флагай как поломку.
- Cutover DNS (reg.ru) — НЕ трогай. Отдельный gated шаг оператора по твоему staging-green.
## Обязательные скилы
- invoke `using-vds-ops` — состояние контейнеров/стека
- invoke `using-tasks` — статус
**weight:** needs-human (сборка образа + деплой стека на VDS)
**notify:** OpeItcLoc03/workshop
**Dev-source:** victor/tandemmebel sha 68b93a95 (sharp-движок snolla 0.34.0/core 0.15.0)
**Status:** 🟢 closed 2026-07-04 (sharp staging green)
**Where I stopped:** образ 68b93a9 собран + стек 20 обновлён + parity-smoke GREEN (media/watermark/variant-cache/sitemap все ок). Notify workshop отправлен. Осиротевший imgproxy-конфиг в пине — heads-up воркшопу (dead).
**Next action:** — (закрыто). Cutover — в [tandemmebel-web-vds-deploy], ждёт DNS-отмашки оператора.
**Branch:** n/a
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-04T08:42:44.850Z -->
<!-- closed-by: vitya@.admin-exec / 2026-07-04 / sharp staging green, все гейты + watermark visual verified -->
---
## 🟢 [tandemmebel-deploy-snolla-0-42-0] — Раскатить прод tandemmebel на `@snollajs/snolla@0.42.0` (core 0.24.0 / data 0.14.1) — engine-chain-обновление: перепил viewModel-навигации на единый ленивый cross-entity граф (EntityDrop/IndexDrop + декларативный GRAPH-реестр, includeX-запекание выпилено) + blog-вертикаль (blogAuthor / blog.archives / date-archive страницы) + path-фиксы. Всё TDD, опубликовано в registry, byte-verified двумя живыми консюмерами: artmone 17/17 byte-identical, tandemmebel 199/199 роутов Ф3-регресс 0 diff. Dev-source: `victor/snolla`, сессия 2026-07-04, трек `viewmodel-graph-full-platform`. Оператор деплой одобрил (workshop ратифицировал 2026-07-04).
## Топология — РЕШЕНА оператором: пересборка staging ДО cutover (вариант A)
Оператор: «пусть всё переебашит с новыми версиями пакетов до cutover». Значит:
- **Пересобрать VDS-staging образ на 0.42.0-чейне** (новые версии всех пакетов), НЕ деплоить in-place на текущий боевой old-infra прод.
- 0.42.0-образ **суперсидит** ранее собранный 0.16.2-staging (`b02ca18`) — это новый пин staging, старый снимается.
- Прод-флип остаётся **тем же DNS-gated cutover** из paused `[tandemmebel-web-vds-deploy]` (reg.ru→89.253.255.94, хозяин сайта). Этот таск готовит НОВЫЙ образ + перепрогоняет гейты; сам флип — по-прежнему по внешнему DNS + отмашке оператора. Развести с паркованным таском (объединить/пометить, чтобы не было двух конкурирующих образов).
- **Проверь tree-check образа:** 0.42.0 содержит sharp-пивот (snolla 0.34.0 нёс sharp, 0.42.0 > 0.34.0 → по версии да) — не потеряй media-sharp-переезд при пересборке.
## Acceptance (обязательные гейты)
1. **Pre-deploy SSR-чек (напоминание сноллы):** SSR `/projects` + blog-listing → **200, не 500**. Причина: `getBlogByPath` тянет новые SELECT-колонки; в боевой БД tandemmebel колонки уже есть (проверено сырым SQL в снолла-сессии), но подтверди на целевой БД.
2. **Completeness-DoD перепрогнать НА новом 0.42.0-образе** (не наследовать GREEN с 0.16.2): боевой sitemap ~188 URL prod↔staging → 0 реальных prod-200/staging-не-200; gallery grid-паритет 15 роутов staging==prod. Ядро навигации полностью перепахано — свежий прогон обязателен.
3. **Archive-страницы — НЕ блокер:** live-verify archive-**страниц** оператор отложил. Для `projects` archive-роуты off (`archivesPath=""`). Не гейтить на них.
4. **Rollback** (RUVDS / предыдущий пин) наготове.
## Обязательные скилы — вызвать до начала
- invoke `using-tasks` — управление статусом задачи
- invoke `using-vds-ops` — диагностика контейнеров на VDS
- invoke `project-discipline` — дисциплина коммитов/деплоя
- invoke `using-wiki` при закрытии — заингесть concept, если топология/пин менялись
## Отчётность
- **notify: OpeItcLoc03/workshop** (комиссионер-оркестратор).
- По закрытию/парку — heads-up ТАКЖЕ в `victor/snolla` inbox: dev-source просил уведомление по деплою.
**TDD:** нет — ops-деплой, кода не пишем.
**Разрешения:** интерны: нет | автопуш: н/д (ops).
**weight:** needs-human (боевой деплой живого сайта + engine-ядро)
**Status:** done
**Where I stopped:** ✅ GOTOVO. Собрал `tandemmebel:ed96b18` (digest `ca4da79`) на VDS из sha ed96b18 (snolla 0.42.0/core 0.24.0/data 0.14.1, verdaccio publish подтверждён), запушил в registry, передеплоил стек 20 (Portainer PUT, env 8/8 сохранён, PullImage) → контейнер **healthy**, running image==ed96b18. Гейты GREEN на новом образе: **acceptance#1** SSR /→200, /projects→200 (title «Наши проекты мебели», не 500); **acceptance#2** completeness-DoD с VDS — sitemap **172/172 паритет, 0 real-fail** (prod200/staging-not200) + gallery-grid **12/12 точный паритет** (bedrooms 80/80…kids 138/138), крошки Главная/Мебель/Галерея наполнены==prod, title==prod, sharp отдаёт webp-байты (200 image/webp). archive-роуты N/A (projects archivesPath=""). Rollback наготове (RUVDS не тронут, b02ca18 в registry).
**Next action:** — (закрыто). Cutover остаётся отдельным DNS-gated шагом в [tandemmebel-web-vds-deploy] — образ к флипу теперь ed96b18. Follow-up для dev/workshop: Dockerfile печёт VERDACCIO_TOKEN в ARG/ENV-историю слоёв (2 warn SecretsUsedInArgOrEnv) — вынести в build-secret.
**Branch:** n/a
**Notify:** OpeItcLoc03/workshop
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-07-04T19:59:45.158Z -->
---
## ⚪ [labtools-ru-deploy-snolla-0-42-1] — Обновить ЖИВОЙ прод labtools.ru на VDS (Portainer стек 17) с `@snollajs/snolla@0.28.2` → **0.42.1** in-place. Резолвимый sha бампа: **`566d41c`** (origin/master victor/labtools.ru; пин 0.42.1 + yarn.lock → core 0.24.1 / data 0.14.1 / liquid 0.10.2). Суперсидит пин 0.28.2 из `[labtools-web-vds-deploy]`.
⚠️ **labtools.ru УЖЕ cutover'нут на VDS** (2026-07-02, боевой `Host(labtools.ru)` на стеке 17). Это **обновление ЖИВОГО прода in-place** (pull новый тег → смена тега на живом стеке 17 → rollback = предыдущий тег), **НЕ staging-first, НЕ DNS-флип** (домен уже на VDS).
Контекст: 0.42.x = перепил viewModel-ядра (ленивый cross-entity граф, includeX-запекание выпилено). Прямой bump на 0.42.0 вскрыл engine-регрессию порядка изделий (`{% order by list_priority %}` no-op на Drop-VM) — сноллой закрыто в **0.42.1** (liquid 0.10.2, orderBy резолвит field через Drop-акцессор). Поэтому цель = 0.42.1, НЕ 0.42.0.
## Key files
- `~/projects/labtools.ru/apps/web/package.json:12` — пин `@snollajs/snolla` = `0.42.1`
- `~/projects/labtools.ru/deploy/Dockerfile` — multi-stage node:22-slim (без правок; пин течёт через yarn.lock, `yarn install --immutable`)
- Portainer **стек 17** labtools.ru — source-of-truth compose (боевой `Host(labtools.ru)`)
## Acceptance (гейты на НОВОМ образе ДО подмены — не наследовать)
1. Build на VDS из sha `566d41c` → образ в `registry.kzntsv.site` (BUILD_EXIT=0).
2. **Completeness-gate** на новом образе: 38 боевых-200 роутов (DB source-of-truth MoreThenCms siteId D375C419: 7 content + 6 секций + 24 продукта + static-page + sitemap/robots) → **0 боевых-200 дают 404**. Прогонять С VDS (воркстейшн ловит LAN-DNS-перехват).
3. **Catalog-parity:** порядок изделий в листингах by list_priority (presses = plg-20, plg-12, plg-25, pgr-10 — ключевой чек фикса 0.42.1); непустые сетки всех 6 секций.
4. Подмена тега на живом стеке 17 → контейнер healthy → live-smoke `labtools.ru` == прод, rollback (предыдущий тег) наготове.
5. Секреты НЕ в образе (`config/default.json` .dockerignore'd; VERDACCIO_TOKEN build-only) — уже провалидировано dev'ом локально (docker build+run: image healthy, свип 0 gaps, order корректен в образе, config/default.json ABSENT, VERDACCIO_TOKEN UNSET).
## Build-рецепт (как tandemmebel)
`git -C ~/projects/labtools.ru archive 566d41c | ssh vitya@<vds> tar -x` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN=<token> -t registry.kzntsv.site/labtools:566d41c . && push`. Portainer PUT стека 17 через curl -X + node (НЕ PS Invoke-RestMethod), env сохранить.
## Обязательные скилы — вызвать до начала
- invoke `using-tasks` — статус этой задачи
- invoke `project-discipline` — коммит/деплой-дисциплина
- invoke `using-vds-ops` — диагностика контейнеров VDS (стек 17, healthy)
**TDD:** нет — ops/deploy, кода нет.
**Разрешения:** автопуш деплой-артефактов на VDS — да (боевой apply гейтит оператор). Секреты НЕ в git/образ.
**weight:** needs-human — live-prod in-place: админ подтверждает боевую подмену тега с оператором ДО apply, НЕ автономно.
**notify:** victor/labtools.ru (репорт мне; deploy-петля минует workshop).
**Status:** ready
**Where I stopped:** (not started)
**Next action:** Собрать VDS-образ labtools.ru на sha `566d41c` (snolla 0.42.1) → registry.kzntsv.site. НЕ подменять живой стек 17 до re-прогона completeness+catalog-parity на НОВОМ образе и подтверждения оператором.
**Branch:** master
**Notify:** victor/labtools.ru
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/labtools.ru / 2026-07-05T08:31:44.377Z -->
---