# Admin Task Board _Updated: 2026-07-13 — 🟢 **[tandemmebel-web-vds-deploy] in-place template bump 8df10ee LIVE.** Ops-handoff от tandemmebel-сессии (inbox-запрос): убрать FB/Twitter/Google+ share-кнопки (extremist-icon compliance РФ), оставить ВК+Одноклассники. Коммит `8df10ee` в `victor/tandemmebel.ru` master (template-only, `apps/web/views/social_buttons.liquid`, 1 file 12 deletions, snolla pin 0.42.1 НЕ менялся). **Верификация перед сборкой поймала расхождение:** записка утверждала live=`ed96b18`/0.42.0, фактически на проде крутился `0cd9351`/0.42.1 (in-place bump 2026-07-12). gitea compare `0cd9351...8df10ee` = `total_commits:1` → фикс ровно один коммит поверх 0.42.1 (правильная база), yarn.lock идентичен → регрессии sitemap 184→172 нет. Собрал `tandemmebel:8df10ee` на VDS (чистый архив 8df10ee из gitea API, build-arg VERDACCIO_TOKEN=books-ci JWT, digest `sha256:30a7e5ab82f2bf371042f1b5fa0cd7ac5ceda71379d1da59e148dee8166bd560`, layer-cache hit, push OK). Throwaway-staging :5020 → completeness-gate GREEN: sitemap NEW==PROD **184=184 identical** (0 prod-only/new-only), self-consistency 183×200+1×404(/articles benign parity staging==prod), share-block staging vk+ok/fb-tw-gp=0 vs prod-before vk+ok+fb+tw+gp (доказательство — фикс убирает ровно лишнее). Swap стека 20 (Portainer JWT, PUT `/api/stacks/20?endpointId=1`, env 8/8 preserve, `prune:false`, `pullImage:true`), контейнер healthy ~8s. **Live-smoke С VDS GREEN:** robots/`/`/sitemap 200, /articles 404 parity, sitemap 184 locs, 4 share-block страницы (project-post ×2, /furniture/bedrooms, /furniture/kitchens/classic) все 200 → **vk+ok на месте, fb/tw/gp=0** (экстремистские иконы УБРАНЫ с прода), TLS-серт CN=tandemmebel.ru не дёрнут (in-place swap). `/galleries/` нет на 0.42.1 (sitemap-реструктуризация 0.42.x → gallery рендерится на project-post роуте). Rollback = PUT стека 20 назад на `0cd9351` (0.42.1, {% order %} fix, template-only чистый откат) / `ed96b18` (0.42.0, в registry, стёрт с VDS при disk-cleanup 2026-07-13) / `b02ca18` / revert DNS→80.64.31.36. Source-of-truth compose `host-stacks/vds-kzntsv/tandemmebel.compose.yml` актуализирован (8df10ee + LIVE-комменты + история bump'ов). Hygiene-заметка: Portainer stack 20 file несёт устаревшие STAGING-комменты на строках 16-17/53 (rule line 55 уже LIVE) — косметика, не трогал при деплое. Ответ отправлен в tandemmebel inbox._ _Updated: 2026-07-12 — 🟢 **[tandemmebel-web-vds-deploy] CUTOVER COMPLETE — LIVE на VDS.** Владелец флипанул DNS reg.ru `tandemmebel.ru`+`www`→89.253.255.94 (verify авторит. ns1/ns2.reg.ru = оба 89.253.255.94, не только резолвер). LE-порядок соблюдён: DNS first → traefik `Host()`-rule staging→боевой на стеке 20 (env 8/8 preserve, `prune:false`, `pullImage:false` — образ `ed96b18`/0.42.0 не менялся, без пересборки по решению оператора). LE-серт issued on first hit: CN=tandemmebel.ru, SAN оба, issuer YR2, until 2026-10-10. Live-smoke С VDS GREEN: `/`+`/projects`+3×gallery (office/bedrooms/kids)→200, `/articles`→404 (parity benign#3, так и на RUVDS), sitemap **184 page-locs** (было 172 — рост от blog-archive фичей 0.42.0, benign), self-consistency 30/30 sample (только /articles 404 = тот же parity), sharp media `image/webp 200 35KB`, robots из БД (ADR-0009). **Латентный прод-баг починен cutover'ом:** Gotham-Pro.css был 0B на RUVDS → теперь 200/4436B (шрифт заголовков живой). Rollback = revert DNS→80.64.31.36 (RUVDS IIS жив, не тронут) ИЛИ PUT стека 20 назад на `tandemmebel.vds.kzntsv.site`. Compose source-of-truth обновлён. Тираж snolla 0.42.x теперь **полностью живой** (5/5 стеков на VDS)._ _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//images//` 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 `` 6 источников; `/sitemap-pages-1.xml`→200 `` 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=tags−1) — `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, ключи `/` 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\`), НЕ S3 — оригиналы никогда туда не заливались (продукты — заливались, bucket `pilorama98`). Развилка A/B закрыта фактами MinIO → **A**: создан bucket `galleries`, залиты 301 файл (77 MiB) pilorama98 (siteId `37e67fc4…`) с RUVDS IIS через rclone → `s3://galleries//.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 --- ## 🟡 [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 боевой + переключить ``. См. [morethencms-s3-filestorage-provider.md](morethencms-s3-filestorage-provider.md). **Branch:** n/a (admin ops + external code) --- ## 🟡 [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) --- ## ⚪ [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) --- ## 🟢 [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 --- ## 🟢 [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 --- ## ⚪ [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) --- ## 🟢 [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 --- ## 🟢 [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`. --- ## 🟢 [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 --- ## ⚪ [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 --- ## 🟢 [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. **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 **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 --- ## 🔵 [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.` готов резолвиться на новый 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.` (создать A-records, TTL=300 для возможности быстрого switch'а). 6. Smoke: `curl https://placeholder.bookva.` → 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 --- ## 🔵 [books-dns-cutover-bookva] — Ops-таска для Фазы 4, шаг 5 дизайна `tenant-split` из victor/books. DNS switch `*.bookva.` → новый VDS IP, с parallel-run старого bookva-стека на shared VDS 7 дней без публичного hostname. **Контекст dev-source:** дизайн в victor/books `.wiki/concepts/tenant-split.md` § «Фаза 4». **Acceptance:** - DNS `*.bookva.` (или конкретный hostname Bookva — определяется учредителем) указывает на новый VDS IP. - Старый bookva-стек на shared VDS остаётся запущенным, **без публичного hostname** (traefik labels убраны или disabled), доступен только по docker-сети для возможного отката. - Smoke с двух разных сетей (mobile + офисный wifi) — `https://` отвечает с нового 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, шаги 4–7» в victor/books. 1. Maintenance window 1–2ч (объявить за 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 за 1–2 недели до этой таски). **Branch:** n/a --- ## ⚪ [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 () 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 ## 🔵 [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-` / `slovo-`. **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.` → 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 `, `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 ». **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 --- ## 🟢 [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` — не проверен.
исходный scope (decision-task) 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
--- ## 🟡 [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 --- ## 🟢 [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/.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\\.`). В S3/MinIO их нет. Продукты — мигрированы (`s3://pilorama98/products/...` отдаёт 200), галереи — нет. Три независимых следствия одного корня: 1. **Prod не отдаёт картинки блога.** Legacy-роут `/galleries//images//.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 уже ждёт (`/`); проверена доступность через 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\\\\). - 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 `` storageClient `galleries` = тип **Local** (`GalleriesStorage`, диск `App_Data\galleries\`), НЕ S3 → галереи никогда не были в S3 (продукты — да, bucket `pilorama98/products`). Корень доказан. **A** (отдельный bucket `galleries`, key `/`) выбран 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…/.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`. --- ## 🟢 [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//` 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-картинки контента (``) через imgproxy с источником `s3://assets//`, но объектов в S3 нет — imgproxy 404 "Source image is unreachable". БД-записи есть, файлы только на legacy-IIS диске. Тип `StorageClient="assets"` — это `MoreThenCms.FileStorage.Local.*`, в S3 никогда не мигрировали. ## Acceptance criteria 1. Legacy assets-оригиналы pilorama98 залиты в S3 bucket `assets` ключами `/` (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`. Ключ-форма `/` подтверждена на диске. - **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). --- ## 🟢 [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 /pilonuxt: .` - `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 ✓ **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` зелёный. --- ## 🟢 [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 ✓ --- ## 🟢 [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 ✓ --- ## 🔵 [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 --- ## 🟢 [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 ## Done (admin infra) - `docker.registryAuth = {username:"books-ci", password:, 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. --- ## 🟢 [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=tags−1, 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 --- ## 🟢 [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 status` чист И `git -C 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 --- ## 🟢 [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/` → `` ≤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] — **CLOSED 2026-07-12 — CUTOVER LIVE на VDS.** Владелец флипанул DNS reg.ru `tandemmebel.ru`+`www`→89.253.255.94 (verify ns1/ns2.reg.ru оба 89.253.255.94). LE-порядок соблюдён: DNS first → traefik `Host()` staging→боевой на стеке 20 (env 8/8 preserve, `prune:false`, `pullImage:false` — образ `ed96b18`/0.42.0 не менялся, без пересборки по решению оператора). LE-серт issued on first hit: CN=tandemmebel.ru, SAN оба, until 2026-10-10. Live-smoke С VDS GREEN: `/`+`/projects`+3×gallery→200, `/articles`→404 (parity benign#3), sitemap **184 page-locs** (рост от blog-archive 0.42.0, benign), self-consistency 30/30 sample, sharp media webp 200, robots из БД (ADR-0009). **Gotham-Pro.css 0B→200/4436B** — латентный прод-баг починен cutover'ом. Rollback = revert DNS→80.64.31.36 (RUVDS жив) ИЛИ PUT стека назад на staging-host. Compose/board/wiki обновлены. **Тираж snolla 0.42.x теперь полностью живой (5/5 стеков на VDS).** <!-- 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:** 🟢 done — 2026-07-05. labtools.ru LIVE на snolla 0.42.1, стек 17, образ `registry.kzntsv.site/labtools:566d41c` (digest `ea0649a…`). Оператор дал отмашку на боевой apply (+пуш). **Where I stopped:** GREEN. Собрал `labtools:566d41c` на VDS (BUILD_EXIT=0, secret-guard: config/default.json ABSENT, VERDACCIO_TOKEN build-only). Acceptance на НОВОМ образе (throwaway-staging из env живого стека 17, С VDS против прод-оракула): (1) **completeness** — новый sitemap = **38 page-locs** (7 pages+1 static+6 sections+24 продукта), все 37×200 + 1×301(→200, `/products/laboratory-ball-mills`→`/lshm-750`, идентично проду); (2) **content-not-lost** — 11 реальных prod-страниц 0 потерь; (3) **order-fix 0.42.1** — presses = plg-20,plg-12,plg-25,pgr-10 == прод. NB: старый прод 0.28.2 sitemap был дефицитным (sections=1, catalog=2) — 0.42.1 отдаёт полную корректную структуру (per-category product-sitemaps). **Next action:** — (закрыто). Боевой swap выполнен: PUT стека 17 (env 8/8 сохранён, pullImage=true) → контейнер healthy на 566d41c → **live-smoke labtools.ru зелёный** (/,robots,about,contacts,presses=200; order фикс вживую; sitemap 38; TLS-серт CN=labtools.ru Jul2→Sep30 не тронут, без LE-churn). Rollback = тег `labtools:43e28ba` (0.28.2, в registry) / стек 17 PUT назад. RUVDS 80.64.31.36 = внешний rollback (не тронут). Compose source-of-truth обновлён на 566d41c. **Branch:** master **Notify:** victor/labtools.ru <!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/labtools.ru / 2026-07-05T08:31:44.377Z --> <!-- closed-by: vitya@.admin-exec / 2026-07-05 / live-prod in-place bump 0.28.2→0.42.1, operator-approved, acceptance GREEN, live-smoke GREEN --> --- ## 🟢 [emspb-deploy-snolla-0-42-0] — Пересобрать образ emspb.ru на движке `@snollajs/snolla@0.42.1` (core 0.24.1 / data 0.14.1 / liquid 0.10.2) и обновить деплой на VDS. **Суперсидит** пин образа прошлой закрытой `[emspb-web-vds-deploy]` (текущий live-образ = `b6e361a` / snolla 0.28.4, Portainer stack Id 18). Консюмер-бамп ЗАПУШЕН: `victor/emspb.ru` master `95a5c42` (пин 0.42.1 + yarn.lock: snolla 0.42.1 / core 0.24.1 / data 0.14.1 / liquid 0.10.2). ⚠️ ВАЖНО — emspb.ru УЖЕ ЖИВОЙ на VDS (cutover выполнен 2026-07-02, stack 18 за traefik+LE, бой резолвится в 89.253.255.94). Это НЕ чистый staging — обновление живого прод-контейнера. Топология (пересобрать → отдельный staging-стек → gated swap, ЛИБО in-place PUT stack 18) — на усмотрение админа; rollback = текущий 0.28.4-образ `b6e361a` + RUVDS-IIS (80.64.31.36). ## Источник `victor/emspb.ru` @ `95a5c42` — `deploy/Dockerfile` (multi-stage node:22-slim non-root, build-arg `VERDACCIO_TOKEN` build-time, healthcheck /robots.txt) + `deploy/README.md` (env-лист, все секреты через env) + корневой `.dockerignore`. Dockerfile version-agnostic — пин тянется из package.json/yarn.lock через `yarn install --immutable`. ## Dev-верификация (сделано в emspb-сессии, для справки — админ перепроверяет на образе) - 43/43 тестов GREEN на 0.42.1 (viewModel-graph-перепил ядра, `includeX`-запекание выпилено — регрессии ноль). - Completeness-gate: 29 боевых sitemap-роутов + краул → 0 live-200→local-404 (нет скрытого galleries/feed page-type). - Локальный `docker build` OK; секреты НЕ запечены (history чист, `config/default.json` отсутствует в образе); контейнер healthy; image-sweep 29 роутов + robots/sitemap/theme.css/3.jpg = все 200. ## Acceptance (перепрогнать на НОВОМ 0.42.1-образе — не наследовать GREEN) 1. Собрать на VDS из чистого git-archive `95a5c42` → `registry.kzntsv.site/emspb:95a5c42` → push. 2. Контейнер healthy (healthcheck /robots.txt). 3. Completeness-gate на образе: 29 sitemap-роутов → 200, canonical 301 (trailing/lowercase/collapse //), theme-ассеты из MinIO, `/images/3.jpg` 200. **Гнать смоук С VDS** — воркстейшн перехватывает прод-домены в LAN-DNS. 4. Rollback наготове (0.28.4-образ b6e361a + RUVDS). ## Guards - Секреты в образ/git — НЕТ (env-only, кросс-проверено локально). - VERDACCIO_TOKEN печётся в ARG/ENV build-стадии → build-secret (известный follow-up, как на tandemmebel; НЕ блокер — runtime-стадия отдельная, токена в финальном образе нет). - **Cutover live DNS уже сделан** — если топология = staging-rebuild, финальный swap живого стека = operator-gated. LF/CRLF в HTML — косметика, НЕ чинить. ## Обязательные скилы — вызвать до начала - invoke `using-tasks` — статус задачи - invoke `project-discipline` — дисциплина - invoke `using-vds-ops` — диагностика контейнеров на VDS - invoke `using-wiki` — обновить рунбук `emspb-vds-deploy-runbook` под 0.42.1 **TDD:** n/a (ops-деплой). **Разрешения:** ops-задача админ-сессии. **weight:** needs-human (обновляет ЖИВОЙ прод-контейнер; прошлая emspb-VDS-деплой-таска = needs-human). **notify:** victor/emspb.ru (репортить НАПРЯМУЮ мне, минуя workshop). **Status:** done **Where I stopped:** superseded by emspb-deploy-snolla-0-42-1 (slug выровнен на реальный пин 0.42.1 + класс уточнён на in-place прод-обновление по коррекции workshop); НЕ исполнять этот, работать по -0-42-1 **Next action:** (none — kept until merged) **Branch:** n/a **Notify:** victor/emspb.ru <!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/emspb.ru / 2026-07-05T08:34:29.703Z --> <!-- closed-by: OpeItcLoc03@DESKTOP-NSEF0UK / 2026-07-05T08:36:03.081Z / note: superseded by emspb-deploy-snolla-0-42-1 (slug выровнен на реальный пин 0.42.1 + класс уточнён на in-place прод-обновление по коррекции workshop); НЕ исполнять этот, работать по -0-42-1 --> --- ## ⚪ [emspb-deploy-snolla-0-42-1] — Обновить ЖИВОЙ прод emspb.ru на движке `@snollajs/snolla@0.42.1` (core 0.24.1 / data 0.14.1 / liquid 0.10.2) **in-place**. emspb боевой на VDS: Portainer стек **18**, образ `emspb:b6e361a` (snolla 0.28.4), cutover LIVE 2026-07-02, DNS emspb.ru/www→89.253.255.94. **Суперсидит** пин `b6e361a`. (Пересоздан из `emspb-deploy-snolla-0-42-0` — слаг выровнен на реальный пин 0.42.1 по коррекции workshop.) Консюмер-бамп ЗАПУШЕН: `victor/emspb.ru` master `95a5c42` (пин 0.42.1 + yarn.lock: snolla 0.42.1 / core 0.24.1 / data 0.14.1 / liquid 0.10.2). ## Класс: in-place прод-обновление (НЕ greenfield staging) Собрать/pull образ 0.42.1 → **admin подтверждает боевой apply с ОПЕРАТОРОМ ПЕРЕД подменой тега** на живом стеке 18 → swap. Rollback = `emspb:b6e361a` (+ RUVDS-IIS 80.64.31.36). ## Источник `victor/emspb.ru` @ `95a5c42` — `deploy/Dockerfile` (multi-stage node:22-slim non-root, build-arg `VERDACCIO_TOKEN` build-time, healthcheck /robots.txt) + `deploy/README.md` (env-лист, секреты через env) + корневой `.dockerignore`. Dockerfile version-agnostic — пин тянется из package.json/yarn.lock через `yarn install --immutable`. ## Dev-верификация (сделано в emspb-сессии — админ перепроверяет на образе) - 43/43 тестов GREEN на 0.42.1 (viewModel-graph-перепил ядра, `includeX`-запекание выпилено — регрессии ноль). - Completeness-gate: 29 боевых sitemap-роутов + краул → 0 live-200→local-404 (нет скрытого galleries/feed page-type). - Локальный `docker build` OK; секреты НЕ запечены (history чист, `config/default.json` отсутствует в образе); контейнер healthy; image-sweep 29 роутов + robots/sitemap/theme.css/3.jpg = все 200. ## Acceptance (на НОВОМ 0.42.1-образе ДО подмены — не наследовать GREEN) 1. Собрать на VDS из чистого git-archive `95a5c42` → `registry.kzntsv.site/emspb:95a5c42` → push. 2. Контейнер healthy (healthcheck /robots.txt). 3. Completeness-gate на образе: 29 sitemap-роутов → 200, canonical 301 (trailing/lowercase/collapse //), theme-ассеты из MinIO, `/images/3.jpg` 200. **Гнать смоук С VDS** — воркстейшн перехватывает прод-домены в LAN-DNS. 4. **Боевой apply (swap тега на стеке 18) — с подтверждения оператора.** Rollback `emspb:b6e361a` наготове. ## Guards - Секреты в образ/git — НЕТ (env-only, кросс-проверено локально). - VERDACCIO_TOKEN печётся в ARG/ENV build-стадии → build-secret (известный follow-up, как на tandemmebel; НЕ блокер — runtime-стадия отдельная, токена в финальном образе нет). - LF/CRLF в HTML — косметика, НЕ чинить. ## Обязательные скилы — вызвать до начала - invoke `using-tasks` — статус задачи - invoke `project-discipline` — дисциплина - invoke `using-vds-ops` — диагностика контейнеров на VDS - invoke `using-wiki` — обновить рунбук `emspb-vds-deploy-runbook` под 0.42.1 **TDD:** n/a (ops-деплой). **Разрешения:** ops-задача админ-сессии. **weight:** needs-human (in-place обновление ЖИВОГО прод-контейнера; прошлая emspb-VDS-деплой-таска = needs-human). **notify:** victor/emspb.ru (репортить НАПРЯМУЮ мне, минуя workshop). **Status:** 🟢 done — 2026-07-05. emspb.ru LIVE на snolla 0.42.1, стек 18, образ `registry.kzntsv.site/emspb:95a5c42` (digest `d6299cf…`). Оператор дал отмашку на боевой apply (+пуш). **Where I stopped:** GREEN. Собрал `emspb:95a5c42` на VDS (BUILD_EXIT=0, secret-guard OK). Acceptance на НОВОМ образе (throwaway-staging из env стека 18, С VDS против прод-оракула): completeness — **29 sitemap-роутов все 200** (0 non-2xx/3xx), content-not-lost 29/29 (0 потерь), ключевые страницы 200. Sitemap не реструктурировался (29==29, в отличие от labtools). **Next action:** — (закрыто). Боевой swap: PUT стека 18 (env 8/8 сохранён, pullImage=true) → контейнер healthy на 95a5c42 → **live-smoke emspb.ru зелёный** (/,robots,contacts=200; sitemap 29). TLS OK: SAN-серт покрывает emspb.ru/www.emspb.ru/emspb.vds.kzntsv.site (CN косметически=staging-хост, но curl validates sslverify=0; серт с cutover 07-02, swap не тронул). Rollback = тег `emspb:b6e361a` (0.28.4) / стек 18 PUT назад / RUVDS 80.64.31.36. Compose source-of-truth обновлён на 95a5c42. **Branch:** master **Notify:** victor/emspb.ru <!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/emspb.ru / 2026-07-05T08:35:56.737Z --> <!-- closed-by: vitya@.admin-exec / 2026-07-05 / live-prod in-place bump 0.28.4→0.42.1, operator-approved, acceptance GREEN, live-smoke GREEN --> --- ## 🟢 [labtools-pro-deploy-snolla-0-42-1] — Обновить ЖИВОЙ прод labtools.pro in-place → snolla 0.42.1 (собрать образ из sha 0610432, перепрогнать гейты на образе, operator-gated apply на боевой стек 19, rollback=7bd9fae). ## Goal Обновить **ЖИВОЙ прод** labtools.pro in-place на движке `@snollajs/snolla@0.42.1` (core 0.24.1 / data 0.14.1 / liquid 0.10.2). Класс = обновление живого прода (как labtools.ru), **НЕ staging-first greenfield**. Сайт уже боевой на VDS: Portainer стек **19**, текущий образ `labtools-pro:7bd9fae`, cutover'нут 2026-07-02, DNS→89.253.255.94. Код-часть закрыта проггером (`victor/labtools.pro`): bump 0.28.7→0.42.1, тесты 24/24 GREEN, completeness-gate PASS (27 роутов, order-парити ✓), docker build локально healthy, секреты не запечены. **sha для сборки = `0610432`** (master, pushed). Почему НЕ голый bump: 0.42.0 = viewmodel-graph-перепил ядра (`includeX`-запекание выпилено). 0.42.0 ловил engine-gap с `{% order … by <field> %}` на Drop-backed каталоге (порядок изделий уезжал) — снолла закрыла в liquid 0.10.2 → **целься на 0.42.1, НЕ 0.42.0**. ## Key files - `~/projects/labtools.pro/apps/web/package.json` — пин `@snollajs/snolla@0.42.1` (уже закоммичен в 0610432) - `~/projects/labtools.pro/deploy/Dockerfile` — multi-stage node:22-slim, build context = repo root, VERDACCIO_TOKEN build-time only (не запекается) - `~/projects/labtools.pro/deploy/README.md` — рантайм env-лист (8 секретов: DB_USER/DB_PASSWORD, IMGPROXY_KEY/SALT, S3_ACCESS_KEY_ID/SECRET, SMTP_USER/PASSWORD) - Portainer стек **19** (labtools.pro) — source-of-truth compose, боевой ## Acceptance (перепрогнать на НОВОМ 0.42.1-образе ДО подмены — НЕ наследовать GREEN) 1. Собрать образ на VDS `registry.kzntsv.site/labtools-pro:0610432` (BUILD_EXIT=0), push в registry. 2. **completeness-gate**: полный список боевых роутов www.labtools.pro (sitemap-развёртка + краул) vs новый образ → 0 боевых-200 → образ-non-200 (проггер: 27 роутов, эталон). 3. **каталог-парити (order-фикс 0.42.1)**: порядок изделий в листингах — presses=[plg-20,plg-12], milling=[milling-jars,grinding-media], press-forms=12 шт. Смоук гнать **С VDS** (воркстейшн ловит LAN-DNS-перехват прод-доменов). 4. Секреты не запечены в образ (проверить `docker image inspect ... .Config.Env` — токена/паролей нет). 5. **Rollback наготове = `labtools-pro:7bd9fae`** (текущий боевой). ## Apply — OPERATOR-GATED Подмена тега на живом стеке 19 (Portainer PUT, env сохранён, PullImage) = **боевой apply живого прода — админ подтверждает с оператором ПЕРЕД подменой**. Это НЕ автоматический шаг. После apply: контейнер healthy + running==0610432, повторный смоук ключевых роутов. Рухнуло → откат на 7bd9fae. ## Обязательные скилы — вызвать до начала работы - invoke `using-tasks` — управление статусом задачи - invoke `project-discipline` — дисциплина - invoke `using-vds-ops` — диагностика VDS-контейнеров (стек 19, registry, portainer) - invoke `using-projects-meta` — cross-project (эта таска кросс-проектная) **TDD:** нет — ops/deploy-таска, код уже покрыт тестами проггером (не пишем код). **Разрешения:** интерны: нет | автопуш: n/a (ops, не код-таска) **weight:** needs-human — обновление ЖИВОГО прода in-place; критическая deploy-инфра; боевой apply подтверждается оператором. **notify:** victor/labtools.pro (репортить проггеру напрямую, МИНУЯ workshop) Build-рецепт (зеркало tandemmebel): `git -C ~/projects/labtools.pro archive 0610432 | ssh vitya@89.253.255.94 tar -x`; `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/labtools-pro:0610432 . && push`. Portainer PUT через curl -X + node (НЕ PS Invoke-RestMethod). Cutover DNS уже сделан (07-02) — тут только смена образа на существующем боевом стеке, НЕ новый DNS-флип. **Status:** 🟢 done — 2026-07-05. labtools.pro LIVE на snolla 0.42.1, стек 19, образ `registry.kzntsv.site/labtools-pro:0610432` (digest `3d543fc…`). Оператор дал отмашку на боевой apply (+пуш). **Where I stopped:** GREEN. Собрал `labtools-pro:0610432` на VDS (BUILD_EXIT=0, secret-guard OK). Acceptance на НОВОМ образе (throwaway-staging из env стека 19, С VDS против прод-оракула): completeness — **25 sitemap page-locs все 200** (0 non-2xx/3xx; +robots+sitemap.xml = 27 прогерских), content-not-lost 25/25 (0 потерь). **Order-парити ВСЕ секции MATCH** (фикс liquid 0.10.2): presses=`plg-20,plg-12`, milling=`milling-jars,grinding-media`, press-forms=12 изделий (round-xrf…round-collapsible) — все == прод. (NB: мой первичный ручной счёт дал 13 — задвоил тайл; прог-сверка против прод+образа = ровно 12, парити не задет.) **Next action:** — (закрыто). Боевой swap: PUT стека 19 (env 8/8 сохранён, pullImage=true) → healthy на 0610432 → **live-smoke labtools.pro зелёный** (/,robots,about,contacts,presses,press-forms=200; order вживую plg-20,plg-12; sitemap 25). TLS CN=labtools.pro Jul2→Sep30 не тронут. Rollback = тег `labtools-pro:7bd9fae` (0.28.7) / стек 19 PUT назад / DNS→RUVDS. Compose source-of-truth обновлён на 0610432. **Branch:** master **Notify:** victor/labtools.pro <!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/labtools.pro / 2026-07-05T08:57:28.335Z --> <!-- closed-by: vitya@.admin-exec / 2026-07-05 / live-prod in-place bump 0.28.7→0.42.1, operator-approved, acceptance GREEN (order-parity 3 секции), live-smoke GREEN --> --- ## 🔴 [vehicles-loader-redeploy-per-group-rate-diff] — Пересобрать и задеплоить @stostayer/vehicles-loader на прод клиента с новым кодом per-group rate diff. Код уже на origin/master (запушено 85ca936, 5 коммитов b0e7b58..85ca936). Изменение: rate-группы с неизменённым мультимножеством {standardHours,notes} теперь skip'аются вместо слепого DELETE+INSERT — объём записей в БД 51 203 → 1 963 rate-rows/день (−96%), счётчик replaced=реальные записи, skipped=пропущенные группы. Read-consistency сохранена (изменившиеся группы — та же короткая транзакция). Write-примитив replaceGroup не менялся. Валидировано двумя dry-run против prod (0 ошибок, SG skipped=41089/replaced=1555, SM skipped=5131/replaced=408) + 36 unit-тестов. **Status:** active **Where I stopped:** DEPLOYED + image-verified. Built docker.stostayer.ru/vehicles-loader:0.4.1 from master@b86ed40 (incl 85ca936 rate-diff). Pushed (one shot, digest sha256:228f2b24). Host: pulled + tagged :latest (image c3c4a83b15bd). New code confirmed in image: rateDiff.js(rateSetsEqual×2), replaceGroup.js(replaceRatesForGroups×2). Old tags 0.4.0/0.3.0 retained for rollback. Timer armed, next run Tue 2026-07-14 06:21:38 MSK. PENDING: runtime verify at that run. **Next action:** After 07-14 06:21 MSK run: check new importRun status=ok + counts.serviceGenerationRate={replaced:<сотни>,skipped:<~41k>}, serviceModificationRate={replaced:<сотни>,skipped:<~5k>}, duration < ~93 мин. If replaced still ~44k/6.7k → wrong image (but code-verify already GREEN). Rollback: docker tag 0.4.0 vehicles-loader:latest. **Branch:** n/a <!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/stostayer.new / 2026-07-13T14:08:16.228Z --> --- ## ⚪ [tandemmebel-deploy-drop-fb-tw-gplus-buttons] — Срочный деплой-запрос: убрать с боевого tandemmebel.ru (стек 20) share-кнопки Facebook/Twitter/Google+ (Facebook = экстремистская символика в РФ). Фикс в master victor/tandemmebel.ru как 8df10ee (запушен, template-only, 12 deletions, VK+ОК оставлены). Пересобрать образ tandemmebel на sha 8df10ee (сейчас жив ed96b18/0.42.0, движок не менялся), передеплоить стек 20 (env verbatim 8/8), live-smoke 4 страниц со share-блоком (gallery, blog_post, product_category, product_subcategory) — ВК+ОК на месте, FB/TW/G+ нет, вёрстка цела. Откат = revert 8df10ee. Источник: inbox-письмо от tandemmebel-сессии 2026-07-13T19:21Z. **Status:** ready **Where I stopped:** (not started) **Next action:** Пересобрать образ tandemmebel на sha 8df10ee → передеплоить стек 20 → live-smoke 4 share-страниц → отчёт в inbox tandemmebel.ru **Branch:** master **Notify:** victor/tandemmebel.ru <!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: victor/tandemmebel.ru / 2026-07-13T19:22:38.934Z --> ---