Files
admin/.tasks/STATUS.md
vitya bcb4dc36be deploy(labtools.pro): LIVE на snolla 0.42.1 — in-place bump живого прода (стек 19)
Финал тиража 0.42.1 (3/3 live-редеплоя закрыты: labtools.ru+emspb+labtools.pro).
Оператор дал отмашку на боевой apply.

- labtools-pro:0610432 (0.28.7→0.42.1, digest 3d543fc). Acceptance С VDS на
  новом образе: 25 sitemap page-locs все 200, 0 потерь контента.
- Order-парити 3 секции MATCH (фикс liquid 0.10.2): presses=plg-20,plg-12;
  milling=milling-jars,grinding-media; press-forms=13 == прод.
- Live-smoke GREEN, TLS CN=labtools.pro не тронут. Rollback labtools-pro:7bd9fae.

Compose source-of-truth обновлён. Прог квитирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:04:04 +03:00

248 KiB
Raw Blame History

Admin Task Board

Updated: 2026-07-04 (8) — 🟢 [tandemmebel-web-vds-deploy] gallery-дефект ЗАКРЫТ на staging — GREEN. Workshop прислал коррекцию sha → ребилд на b02ca18 (core 0.16.2, SlugFeedPage-fix). Собрал tandemmebel:b02ca18 (digest f8672218), передеплой стека 20 (env 8/8, healthy). DoD-чек 12/12 GREEN: gallery-грид staging == prod ТОЧНО на всех роутах (bedrooms 80/80, kids 138/138, office 26/26 и т.д.). Крошки наполнены (Главная/Мебель/Галерея идей, были пустые), title полный (Фото Офисная мебель…), байты 28508≈прод 28253. Я флагал сомнение (мой DB-дамп показал: /gallery — конвенционный саб-роут, а не feed; SlugFeedPage-fix мог промахнуться) — но жёсткий рендер-чек показал GREEN, фикс сработал. Cutover ОТЛОЖЕН оператором (не забыть!) — триггер запуска = владелец сайта меняет DNS reg.ru→89.253.255.94, тогда verify авторит.NS→боевой Host в стек 20→live-smoke (порядок в NEXT_SESSION.md). Прогеру после разноса от оператора отправлен рабочий green-хендофф на свип 188 URL. RUVDS=rollback. Updated: 2026-07-04 (7) — 🔵 [tandemmebel-web-vds-deploy] дефект идентифицирован = gallery FeedPages-VM. Workshop прислал ops-таск: ребилд стека 20 на a173401 (FeedPages page-type портирован, чинит gallery 404→200). Гейт green (origin HEAD=a173401 ⊇ a173401+22bd8f2), собрал tandemmebel:a173401 (snolla 0.35.0/core 0.16.0/data 0.13.0, digest 6962bd97), передеплоил стек 20 (Portainer API, env 8/8, healthy). Роутинг чинится: 12/12 furniture gallery-роутов 404→200. НО рендер-чек RED — grid ПУСТОЙ на всех роутах (staging /galleries/*/images = 0 vs prod 24/80/62) + breadcrumb-подписи пустые (itemprop=name без текста). Оба симптома = один корень: FeedPage-VM не прокидывает item в шаблон. Оператор дефект подтвердил независимо. Мой деплой чист; фикс на прогере (tandemmebel.ru). Пропинговал прогера с уликами (empty-grid + breadcrumb addendum) → ждёт новый sha, прогоню тот же DoD (staging_imgs==prod_imgs + непустые крошки). Cutover HELD. См. NEXT_SESSION.md. Updated: 2026-07-04 (6) — 🔵 [tandemmebel-web-vds-deploy] BLOCKED на новом дефекте. Дал оператору staging-URL (https://tandemmebel.vds.kzntsv.site) на визуальную проверку sharp-сборки → оператор нашёл НОВЫЙ дефект (иной, чем прошлые глиф/imgproxy), специфику не назвал, «чиним в новой сессии». Cutover HELD. Мои автоматические гейты по sharp-staging были green (media/watermark/variant-cache/parity) — значит дефект визуальный, не пойманный curl-смоком. Sharp-staging жив на стеке 20 для разбора в новой сессии. См. NEXT_SESSION.md. Updated: 2026-07-04 (5) — 🟢 [tandemmebel-sharp-staging-rebuild] closed — SHARP staging GREEN. Пивот tandemmebel на in-process sharp завершён. Образ tandemmebel:68b93a9 (snolla 0.34.0/core 0.15.0, digest 12288b15) собран на VDS, стек 20 обновлён (env verbatim 8/8, imgproxy из tandem-пути убран). Parity-smoke GREEN: nav 8/8, sitemap staging⊇prod (prod-only=0, +12 categories benign), 2012-посты 31/31, redirects 301/301; sharp media = slug-URL /galleries/<gid>/images/<variant>/<seq> 200 webp serve-bytes, 0 /imgproxy-рефов (осиротевший imgproxy-конфиг в пине эмпирически мёртв → heads-up воркшопу вычистить), variant-cache пишется (32 объекта content-addressed), watermark визуально ✓ featured+lightbox / gallery-small чистый. [tandemmebel-web-vds-deploy] теперь 🟡 sharp-staging-green, ждёт cutover DNS — gated оператором (reg.ru→89.253.255.94). RUVDS=rollback. Updated: 2026-07-04 (4) — 🟢 [minio-variant-cache-bucket] closed. sharp-prereq. Создан бакет variant-cache в shared MinIO (minio.kzntsv.site/books-vds) — S3-эквивалент легаси App_Data/imageCache, куда sharp кладёт resize+watermark варианты on-demand (без него 500 NoSuchBucket на всех sharp-картинках = agent-блокер 1 из tandemmebel sharp-verify). Грант: snolla S3-креды == MinIO root (сверено) → в mode-server-fs полный доступ by construction, IAM не нужен. Write-verify под деплойными кредами: put→stat→get(md5 match)→delete→gone ✓. Другие 17 бакетов не тронуты, lifecycle не ставил (кэш регенерируем). Готовит почву под sharp-переезд tandemmebel (движок victor/snolla media-sharp-imageprocessor-port в переработке). Updated: 2026-07-04 (3) — 🟢 [imgproxy-stack29-watermark-rollback] closed. Оператор сменил watermark-подход: tandemmebel уходит с imgproxy-watermark на in-process sharp (victor/snolla media-sharp-serve-and-watermark) → глифу на SHARED стеке 29 не место. Снял IMGPROXY_WATERMARK_DATA со стека 29 (books-vds): вырезал строку → результат byte-identical pre-watermark бэкапу (cross-check true) → PUT+redeploy+purge. Verify (не задеть боевых): pilorama98 (главный консюмер) 3 рендера == baseline (43950/172042/160010B) не задет; stostayer НЕ imgproxy-консюмер (картинки /galleries//images//assets/) → не затрагивается; WATERMARK_DATA present:false; tandem featured→чистый (ожидаемо, staging→sharp). Глиф-бинарь 7127e92a оставлен для sharp. [tandemmebel-web-vds-deploy] теперь ждёт ПЕРЕСБОРА образа на sharp-пин перед cutover. Notify workshop. Updated: 2026-07-04 (2) — 🟡 [tandemmebel-web-vds-deploy] STAGING GREEN, ждёт cutover. Финал тиража tandemmebel. Образ tandemmebel:0facb35 (snolla@0.32.2/core@0.13.8, digest 464d2f22, watermark-override в пине) собран на VDS (обход 499). Стек Portainer Id 20 (staging-rule tandemmebel.vds.kzntsv.site, env verbatim из labtools стека 17, 8/8), контейнер healthy MSSQL+S3. imgproxy-nginx кэш (books-vds стек 29) purged → featured отдаёт вотермарк без cache-bust (27820B). Staging-smoke с VDS GREEN: status-parity 10/10, sitemap staging⊇prod (prod-only=0, +12 /projects/categories/*=benign#3, /articles 404 parity), trailing 301/301, blog-post 200 (+3.5KB=benign#2 imgproxy vs /galleries/ 26/26), 2012-посты 31/31, theme 21/22 MD5-OK. Находка (не блокер): prod Gotham-Pro.css=0B пустой, staging=4436B корректный → cutover чинит латентный прод-баг шрифта. HOLD cutover — жду отмашку оператора на DNS reg.ru→89.253.255.94; порядок: verify авторит.NS → ТОЛЬКО потом боевой Host-rule (иначе LE упадёт на RUVDS). RUVDS=rollback. Updated: 2026-07-04 — 🟢 [imgproxy-watermark-glyph-books-vds] closed. Ops-подхват по тиражу tandemmebel: воркшоп попросил выставить глиф-вотермарк на imgproxy стек 29 (books-vds). Первый base64 (inbox И закоммиченный таск) был БИТЫЙ на источнике — tasks_create порезал 10КБ-поле (sha f8f0…e995…, IEND нет, зацикленный хвост FQtAlGLzMfBc×N); поймал sha-сверкой ДО прода (не запушил битьё → imgproxy не упал). Воркшоп до-доставил logo.png бинарём в git (host-stacks/books-vds/tandemmebel-watermark-logo.png, commit 7127e92a, sha256 e995…971fc, 8015B, IEND ok). PUT стека 29 (Portainer portainer.kzntsv.site ep1, X-API-Key, IMGPROXY_WATERMARK_DATA inline, PullImage:false, БЕЗ глобального _OPACITY). Smoke cache-busted: A tandem вотермарк ВИДЕН глазами (27294→27820B); B pilorama контроль byte-identical (160010B) — opacity не просочилась. Notify workshop → tandem-агент на ре-скрин→пин→deploy. Дальше жду [tandemmebel-web-vds-deploy]. Updated: 2026-07-03 — 🟡 заведена [morethencms-s3-filestorage-provider]. Разведка «почему /admin/assets/.../delete → 500» на RUVDS IIS: корень = app pool IIS AppPool\snolla имел на App_Data только RX после scp-миграции → File.Delete UnauthorizedAccessException (не MinIO, провайдер assets=Local; NRE-версии исключены проверкой строк Folders/Files в БД). Fix применён: icacls App_Data /grant "IIS AppPool\snolla:(OI)(CI)(M)" /T (44293 файла, verified). Попутно заменён labtools-price.pdf в ОБА хранилища (MinIO ETag→6426ddd0 + локалка, MD5 сверены). Настоящее закрытие split-brain (админка=Local vs фронт=MinIO) → новая таска на S3-провайдер MoreThenCms.FileStorage.S3 (name approved): ТЗ доставлено в инбокс MoreThenCms, объём=все контентные классы, 1 MinIO, deploy на локальный IIS windows-recovery-host→боевой. DNS-аудит: инфра-хосты не перехватываются (нет 127.0.0.1). Concepts: snolla-admin-appdata-acl-500 + galleries-storage-class-local-not-s3. Updated: 2026-06-29 (4) — 🟢 [pilonuxt-deploy-sitemap-taxonomy] closed. pilonuxt 6b9ae63 LIVE (стек 16) — bump-only sitemap taxonomy: content-api 0.15→0.16, core 0.2→0.4 (taxonomy enum, прокси не менялся). Tree-check .output 5/5 (content-api 0.16.0, core 0.4.0, data 0.9.1 1×, snolla 0, btoa 0). Push словил 499 на .output → ретрай прошёл (digest 226584ce…). Acceptance 4/4: /sitemap.xml индекс 7 записей с store-taxonomy-1; /sitemap-store-taxonomy-1.xml→200 57 url абсолютные (35 categories+22 tags, vendors 0=пусто в CMS); v1 цел (products-1/pages-1 200); taxonomy-999→404. Регресс ///catalog 200, stderr чист. Откат: стек 16→42f9d64. Ack→victor/pilorama98.ru. Updated: 2026-06-29 (3) — 🟢 [pilonuxt-deploy-sitemap] closed. pilonuxt 42f9d64 LIVE (стек 16) — sitemap.xml через content-api@0.15.0 (+core@0.2.0). Build из монорепы (форс schema-gen против прод-MSSQL). Tree-check .output 5/5: content-api 0.15.0, core 0.2.0, data 0.9.1 (1×), snolla 0 (7 grep-хитов = комменты-провенансы, 0 import), btoa 0. Push напрямую (499 не случился). Acceptance 3/3: /sitemap.xml→200 <sitemapindex> 6 источников; /sitemap-pages-1.xml→200 <urlset> 70 url все абсолютные; /sitemap-store-products-999.xml→404. Регресс цел (robots////catalog 200), stderr чист. Откат: стек 16→cf2bba2. Ack→victor/pilorama98.ru. Updated: 2026-06-29 (2) — 🟢 [deploy-pilonuxt-forms-api-drop-snolla] closed. pilonuxt cf2bba2 LIVE (стек 16) — ADR-0010: формы на @snollajs/forms-api, @snollajs/snolla дропнут целиком, core@0.1.1. Перед перекатом tree-check .output 4/4 (0× snolla, 0× btoa, 1× data@0.9.1, core 0.1.1 + content-api 0.14 + forms-api 0.1.0). Smoke зелёный: SSR /+/catalog→200, robots/yandex из БД (ADR-0009 не регресс), форма ?path=/checkout пустой→422 JSON / /nope→404 (valid НЕ слал). Гоча: 422 только с Accept: application/json. Notify victor/pilorama98.ru (монореп-inbox). Updated: 2026-06-29 — 🟢 [deploy-pilonuxt-static-pages-robots] closed. pilonuxt 40bb383 (DB-backed staticPages + robots.txt, ADR-0009) LIVE на проде (Portainer стек 16). Build из монорепы с форс-регенерацией клиента (rm src/generated+schema-gen 0.6 → новые методы getStaticPage/getRobotsTxt); push с дома 499 на .output-слое → fallback docker save | ssh vds load+push с VDS. Smoke acceptance 6/6 verified телами (robots полный из БД с Yandex Clean-param+Sitemap, yandex/google верификации, /nope→404, /+/catalog→200). Notify victor/pilorama98.ru отправлен. Updated: 2026-06-18 (2) — 🟢 [books-task-runner-registry-auth-cred] closed. Кред books-ci (секция registry, полная) в task-runner config-volume на books-vds (644 root, бэкап, EACCES не повторён). После deploy нового кода (master-bf2a8c5) прогнан registryGc {dryRun:true} через scheduler-dispatch: success, 401 нет, repos books-* видны (7), отчёт сгенерён. Acceptance auth/dryRun met. Finding отдан books (НЕ блокер): GC бежит, но drop=0 всегда (keep=tags1) — getCreated→null для всех манифестов (configDigest не извлекается, OCI image-index?), planDeletions защищает null-dated группы → keepLastN не применяется, реестр не чистится. Bug в books lib/registryV2.js, owner — books. Updated: 2026-06-18 — 🔵 [books-task-runner-registry-auth-cred] blocked. Кред books-ci (секция registry, baseUrl+username+password — полная, bind-mount затеняет config образа) положен в task-runner config-volume на books-vds; mode 644 root сохранён (бэкап .bak-pre-registry-2026-06-18, EACCES-инцидент 25.05→18.06 не повторён), container restart→healthy, config.get('registry.*') резолвится. books-ci валиден: /v2/_catalog→200, репы books-* видны, 401 нет. Блокер: работающий образ (88f2bee, 17.06) содержит СТАРЫЙ registryGc (Gitea API), тега master-a3734d5 в реестре нет → CI не собрал v2-DELETE rewrite. In-app dryRun ждёт build+deploy. Inbox-ответ books отправлен. Updated: 2026-06-13 — 🟢 [migrate-assets-originals-to-s3] closed. assets-оригиналы (весь App_Data\assets: 4750 obj / 215.5 MiB, 145 owner-папок) залиты в новый MinIO bucket assets (books-vds) rclone'ом с RUVDS IIS, ключи <ownerId>/<storageFilename> verbatim. Verify count+size == source. Smoke (мимо LAN-DNS): brevno.jpg → 200 image/webp 64556 B (был 404), валидно-подписанный несуществующий объект → 404 (негатив-контроль). Код snolla не менялся (path A); scope намеренно шире pilorama98 — закрывает класс-404 для всех тенантов snolla, регрессии нет (shared bucket, как galleries). ⚠️ snolla-side остаётся content-api/routes/assets.js (пустой stub) — без него картинки не рендерятся на сайте. Inbox victor/snolla отправлен. Updated: 2026-06-12 (4) — 🟢 [migrate-gallery-originals-to-s3] closed (scope pilorama98). Галереи snolla не отдавались (imgproxy 404) т.к. в legacy storageClient="galleries" = тип Local (диск App_Data\galleries\<siteId>), НЕ S3 — оригиналы никогда туда не заливались (продукты — заливались, bucket pilorama98). Развилка A/B закрыта фактами MinIO → A: создан bucket galleries, залиты 301 файл (77 MiB) pilorama98 (siteId 37e67fc4…) с RUVDS IIS через rclone → s3://galleries/<siteId>/<guid>.jpg verbatim (== ровно то, что строит middleware/galleries.js:47). Код snolla НЕ менялся. Verify: count+size == source. Smoke с books-vds: реальный объект imgproxy → 200 webp, fake → 404. Уточнение инфра: imgproxy.kzntsv.site с 08.06 на books-vds (стек 29/30), не на windows-host — minio-imgproxy-on-vds.md был stale. Inbox victor/snolla отправлен. Updated: 2026-06-12 (3) — 🟢 bookva-es snapshot done (не отложен). Portainer stack 37 пересоздан с path.repo=/snapshots+bind (PUT API, том цел, epz/products уцелели), repo kreknin зарегистрирован, snapshot-блок в run.sh. Поймана коллизия basename snapshots (оба ES-каталога одноимённы → сливались в один dest-репо, порча обоих) → источник bookva-es = родительский /usr/docker/bookva-es. Verified: BOOKS-VDS backup OK 10m28s, на kreknin раздельно snapshots/(slovo index-41) + bookva-es/snapshots/(index-4). Таска bookva-es-snapshot-repo 🟢 closed. Updated: 2026-06-12 (2) — 🟢 bookva- backup gap закрыт.* bookva tenant (db/mongo/es/minio, поднят 26.05) не бэкапился — books-скрипт написан 25.05 до bookva, покрытие не расширили (висело wiki-follow-up #2). Добавлены bookva-db (тот же root pw), bookva-mongo (no-auth), bookva-minio (raw volume) в books-vds-backup-daily-kreknin/run.sh; деплой == репо (.bak-pre-bookva). Verified green: BOOKS-VDS backup OK 10m42s, артефакты на kreknin (bookva-mariadb 257M, bookva-mongo 3.2M, bookva-minio 1.4G). bookva-es отложен → bookva-es-snapshot-repo (покрыт slovo-снапшотом, индексы идентичны). Правило в память: новый stateful → бэкап в том же коммите. Updated: 2026-06-12 — 🔴🟢 MSSQL backup incident + estate backup audit. VDS daily backup падал с 12.06 (line 108, exit 1): mssql-блок (added 11.06) использовал WITH ... INIT → упирался в компрессованный media-header майских .bak (Express не пишет COMPRESSION) → молча падал первым cron-запуском. 5 боевых CMS-баз 3 недели без offsite-копии. Fix: INITFORMAT, прогон 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=trueDELETE _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 боевой + переключить <fileStorageClients>. См. 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.comwww.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. 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 + .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.

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 § 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 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 httpswebsecure (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 § 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. 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.

🟢 [unify-backup-notifications] — closed 2026-05-25 — единый формат push + email для VDS/RUVDS/windows-host (3 scripts), VDS run.sh импортирован в repo. См. 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 5b173de8bffd68 (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 § 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) 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 § 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 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. 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.<tld> готов резолвиться на новый IP (фактический switch — в books-dns-cutover-bookva).
  • SSH-ключи: только инженерные (core-разраб + админ Bookva, если есть). Аналитики Bookva — НЕ имеют SSH к новому VDS.

Status: blocked Where I stopped: (not started) Next action: Pre-step: прочитать concepts/tenant-split.md § «Фаза 4» в victor/books (через mcp__projects-meta__knowledge_get slug=concepts/tenant-split или git clone victor/books → .wiki/concepts/tenant-split.md).

  1. Согласовать с учредителем Bookva: provider, configuration (CPU/RAM/disk), регистрация billing'а на юрлицо Bookva.
  2. Provision VDS, базовая настройка (firewall, fail2ban, SSH ключи).
  3. Установить Docker + docker-compose.
  4. Развернуть traefik + certbot из overlay-репо victor/books-bookva/deploy/traefik.compose.yml.
  5. Подготовить DNS-зону bookva.<tld> (создать A-records, TTL=300 для возможности быстрого switch'а).
  6. Smoke: curl https://placeholder.bookva.<tld> → 200 от nginx-placeholder или из traefik.
  7. Документировать в victor/books-bookva/README.md: hostname, IP, как ssh, runbook smoke-теста.
  8. Закрыть с note: «VDS поднят, IP=<...>, готов к stack deploy». Blocker: OpeItcLoc03/admin books-stateful-split-execution (Фаза 1 — physical split на shared VDS, bookva-* контейнеры живут) + victor/books per-tenant-backup-and-observability (Фаза 2 — per-tenant backups настроены до физического переезда) Branch: n/a

🔵 [books-dns-cutover-bookva] — Ops-таска для Фазы 4, шаг 5 дизайна tenant-split из victor/books. DNS switch *.bookva.<tld> → новый VDS IP, с parallel-run старого bookva-стека на shared VDS 7 дней без публичного hostname.

Контекст dev-source: дизайн в victor/books .wiki/concepts/tenant-split.md § «Фаза 4».

Acceptance:

  • DNS *.bookva.<tld> (или конкретный hostname Bookva — определяется учредителем) указывает на новый VDS IP.
  • Старый bookva-стек на shared VDS остаётся запущенным, без публичного hostname (traefik labels убраны или disabled), доступен только по docker-сети для возможного отката.
  • Smoke с двух разных сетей (mobile + офисный wifi) — https://<bookva-hostname> отвечает с нового VDS.
  • Per-VDS backup'ы на новом VDS подтверждены — есть snapshot спустя 24ч после DNS-switch'а.
  • Через 7 дней parallel-run без откатов — decommission старого bookva-стека (см. Фаза 4, шаг 7 в spec).

Status: blocked Where I stopped: (not started) Next action: Pre-step: прочитать concepts/tenant-split.md § «Фаза 4, шаги 47» в victor/books.

  1. Maintenance window 12ч (объявить за 7 дней).
  2. Финальная синхронизация: mysqldump mariadb-bookva на текущем VDS → mysql на новом VDS (если schema/data разошлись с момента Фазы 2).
  3. Smoke на новом VDS до DNS-switch'а: hosts-файл override → проверить login + base flow.
  4. DNS-switch: меняем A-record(s) для bookva-hostname на new IP. TTL=300 уже стоял (Phase 4 step 1).
  5. Через 1ч (TTL expiry + propagation): smoke с двух сетей.
  6. На старом VDS — отключить traefik labels для bookva-стека (контейнеры продолжают работать, но не доступны снаружи).
  7. Parallel-run 7 дней. Каждый день — smoke. Per-VDS backup на новом VDS — проверить хотя бы 1 успешный snapshot.
  8. Если за 7 дней проблем нет → закрыть с note «cutover stable». Если есть — DNS rollback на старый IP, follow-up debug task в victor/books.
  9. Decommission старого bookva-стека на shared VDS (отдельный шаг, не в этой таске — финальный backup в архив + docker compose down + volume drop). Blocker: books-vds-bookva-bootstrap + victor/books pwa-redirect-handoff (SW bump выкачен в shared API за 12 недели до этой таски). Branch: n/a

[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 БД:

SELECT id, login, email, role FROM users WHERE email IN (<list>) AND deleted_at IS NULL;
  1. Разобрать расхождения с учредителем (missing / role mismatch / inactive).
  2. Зафиксировать финальный список в OpeItcLoc03/admin/.wiki/concepts/books-bookva-whitelist.md (или в local-only encrypted file если NDA-чувствительно).
  3. Финальный список становится input'ом для Фазы 3, шаг 1: при настройке Bookva-БД в неё попадают только эти users.
  4. Закрыть с 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-<svc> / slovo-<svc>.

TDD-режим: [skip-tdd: one-off migration on maintenance window] — verify через post-cleanup audit.

Acceptance:

  • bookva-db + slovo-db контейнеры живые, audit SELECT DISTINCT idSeller = {1} / {2} соответственно.
  • bookva-mongo + slovo-mongo контейнеры живые, agenda-история preserved, jobs чужого tenant'а cleanup'нуты.
  • MinIO (один shared) имеет bucket'ы bookva + slovo вместо books; объекты чужого tenant'а удалены.
  • Shared catalog tables (books, editions, epz, epz_pages, warehouses, products) — полные копии в обеих БД.
  • Smoke с двух сетей: login через bookva.<tld> → Bookva-данные; текущий URL → Slovo.
  • Через 7-14 дней parallel-run без откатов → cleanup старых books-* контейнеров и volumes (отдельный заход).

Status: blocked Where I stopped: (not started) Next action: Pre-step: прочитать victor/books/.tasks/stateful-split-volume-copy.md целиком. Убедиться что 7 open questions resolved (закрыто dev-prep таской victor/books stateful-split-volume-copy): FK CASCADE, units/receipts strategy, MinIO key structure, users whitelist, task_runs handling, chrome/traefik shared.

  1. Подтвердить overlay-репо victor/bookva-overlay + victor/slovo-overlay готовы — compose-файлы для bookva-* / slovo-* стеков на месте.
  2. Подтвердить per-tenant backups настроены (victor/books per-tenant-backup-and-observability) — иначе нет точки rollback.
  3. Объявить maintenance window 1-2ч (без записи).
  4. Execute Steps 0-11 из playbook на shared VDS.
  5. Post-execution audit: SELECT DISTINCT idSeller FROM <each>, db.agendaJobs.distinct('data.idSeller'), mc ls on MinIO.
  6. Smoke с двух сетей.
  7. Через 7-14 дней parallel-run без откатов → cleanup старых books-* контейнеров+volumes (отдельный заход).
  8. Closing note: «Physical split done, bookva-* + slovo-* контейнеры живут на shared VDS, audit clean, parallel-run start ». 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. 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


Галерейные (blog/gallery) картинки не отдаются ни на dev (imgproxy 404 "Source image is unreachable"), ни на prod (legacy-ресайз 404). Диагностика проведена в victor/snolla против живого pilorama98 + кода MoreThenCms. Нужен доступ к БОЕВОМУ серверу (локальный диск IIS + MinIO/S3), которого нет у snolla-агента — поэтому задача на admin.

Диагноз (доказан, не гипотеза)

Корень: галерейные оригиналы лежат ТОЛЬКО на локальном диске legacy-IIS (App_Data\galleries\<siteId:N>\<storageFilename-guid>.<ext>). В S3/MinIO их нет. Продукты — мигрированы (s3://pilorama98/products/... отдаёт 200), галереи — нет.

Три независимых следствия одного корня:

  1. Prod не отдаёт картинки блога. Legacy-роут /galleries/<galleryId:N>/images/<size>/<seq>.jpg — ресайзер на лету. Распиновка живого pilorama98 (2 галереи, 98078b74… и c1b53362…):

    • original/001.jpg200 (235KB / 735KB) — оригинал жив, читается с локального диска;
    • 800x450, default, thumbnail, 100x100, 600x400404 всегда. → ресайз-пайплайн legacy-хоста мёртв глобально (System.Drawing / image-cache storage). Это отдельная legacy-поломка; целевая архитектура — отдавать через imgproxy, не чинить legacy-ресайз.
  2. Превью в админке видно. Админка рендерит ОРИГИНАЛ (passthrough по imageId → GetGalleryImageById, без ресайза), читает локальный диск. Тот же путь, что original/001.jpg=200. Сломанную ресайз-ветку не трогает.

  3. Dev imgproxy 404. snolla строит source s3://${storageClient}/${siteId}/${storageFilename} (packages/snolla/middleware/galleries.js:47 и lib/viewModels/index.js:289). Ключ-форма КОРРЕКТНА (реальный StorageFilename guid + siteId, ровно legacy-раскладка). Но imgproxy читает S3, а объекта там нет → "Source unreachable". Код билдера менять НЕ надо.

Развилка, которую закрыть на боевом (нужен доступ к MinIO)

Имя бакета. Продукты: бакет pilorama98, prefix products/. snolla для галерей берёт image.storageClient («galleries») как ИМЯ БАКЕТА → s3://galleries/.... Проверить в MinIO:

  • (A) целевой бакет реально galleries (отдельный) → достаточно залить файлы, код snolla без изменений;
  • (B) всё в бакете pilorama98, галереи должны быть под prefix galleries/ → тогда + маленький код-фикс на стороне snolla (storageClient как prefix, а не bucket) — в этом случае ЗАВЕСТИ follow-up таску обратно на victor/snolla через notify, НЕ чинить snolla из admin.

Acceptance criteria

  1. Определено фактическое состояние MinIO: какие бакеты есть, как лежат рабочие продукты, отсутствуют ли галерейные объекты — зафиксировано в close-note.
  2. Развилка A/B закрыта явно (какой бакет/prefix — целевой).
  3. Если A: legacy-оригиналы залиты в S3 под ключи, которые snolla уже ждёт (<siteId>/<storageFilename>); проверена доступность через imgproxy (хотя бы одна картинка → 200).
  4. Если B: создана follow-up таска на victor/snolla с точной целевой формой ключа (bucket + prefix), миграция файлов под этот prefix выполнена.
  5. В inbox victor/snolla при close: итоговая форма source (bucket/prefix/key), какие галереи/сколько файлов мигрировано, и нужен ли код-фикс на snolla.

Контекст-источники (для воспроизведения)

  • Живой prod: https://www.pilorama98.ru/galleries/98078b74e062443ba0708476e031b6b0/images/original/001.jpg (200) vs /800x450/001.jpg (404).
  • Legacy-код: MoreThenCms/Galleries/GalleryImage.cs (Path/StorageFilename), GalleriesAppFunc.cs (resolve по galleryId+seq, 404 при null/mime-mismatch), Services/GalleryImagesService.cs:135 (StorageClient="galleries", filename=Guid.NewGuid():N), FileStorage.Local/GalleriesStorage.cs (BasePath App_Data\galleries\\).
  • snolla-консьюмер: packages/snolla/middleware/galleries.js, lib/viewModels/index.js:289.
  • siteId pilorama98: 37e67fc4… (бакет-сегмент в source).

Обязательные скилы — вызвать до начала работы

  • invoke systematic-debugging — закрыть развилку A/B по фактам MinIO до любых действий
  • invoke using-tasks — управление статусом задачи
  • invoke project-discipline — дисциплина коммитов/пушей
  • invoke using-vds-ops — если MinIO/imgproxy крутятся на VDS (docker-диагностика)
  • invoke using-projects-meta — кросс-проектный отчёт/ follow-up в victor/snolla

TDD: нет — ops/migration задача (нет тестируемого кодового ядра; если всплывёт код-фикс на snolla — он уйдёт отдельной impl-таской на victor/snolla с TDD). Разрешения: интерны: нет | автопуш: да weight: needs-human notify: victor/snolla

Status: done (closed 2026-06-12, scope pilorama98) Where I stopped: мигрировано. MinIO (books-vds стек 30, за imgproxy.kzntsv.site с 08.06): bucket galleries создан, prefix 37e67fc44b9e4c06a522a26a73bac9b0/ залит (301×.jpg, 77.0 MiB, rclone RUVDS→minio.kzntsv.site, count+size == source). Smoke OK с books-vds (200/404). Next action: — (хвосты в inbox victor/snolla: остальные siteId-папки галерей не мигрированы по скоупу; legacy prod-ресайз остаётся 404 by design). Branch: master Notify: victor/snolla — inbox .claude-inbox/2026-06-12T11-47-33Z-admin.md

Closure note (2026-06-12):

  • Развилка A/B закрыта фактами: в Web.config legacy <fileStorageClients> storageClient galleries = тип Local (GalleriesStorage, диск App_Data\galleries\<siteId>), НЕ S3 → галереи никогда не были в S3 (продукты — да, bucket pilorama98/products). Корень доказан. A (отдельный bucket galleries, key <siteId>/<storageFilename>) выбран user'ом → нулевой код-фикс snolla.
  • MinIO state: bucket'ы до миграции — artmone/books/imgproxytest/manuals/modulair/modules/obsidian/ozon/pilorama98/strapi/test/upload; galleries отсутствовал; pilorama98 = только products/ (шард по hex, без siteId). Зафиксировано.
  • Залив: RUVDS IIS C:\sites\snolla\App_Data\galleries\37e67fc44b9e4c06a522a26a73bac9b0\ (301 файл, dir-name уже lowercase 32-hex == ownerId snolla) → s3://galleries/37e6…/<guid>.jpg verbatim. rclone v1.74.2 inline s3-remote (env-var config, без записи cred). Verify: 301 объект, 77.012 MiB == source.
  • Smoke (с books-vds, мимо LAN-DNS-перехвата воркстейшна): подписанный imgproxy-URL реального объекта → 200 image/webp 32102 B; несуществующий guid → 404 (негативный контроль).
  • Wiki-drift зафиксирован: minio-imgproxy-on-vds.md устарел (говорит pipeline на windows-host; фактически imgproxy на books-vds с 08.06). Добавлен concept galleries-storage-class-local-not-s3.

🟢 [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\assetss3://assets/<ownerId>/<storageFilename> verbatim. Verify: dest == source точь-в-точь — 4750 объектов / 225 935 853 B (215.5 MiB), 145 owner-папок. Smoke (через books-vds, мимо LAN-DNS): brevno.jpg (предподписанный URL из задачи) → 200 image/webp 64556 B (был 404); валидно-подписанный несуществующий объект → 404 (негативный контроль). Код snolla НЕ менялся (path A). Inbox victor/snolla отправлен. ⚠️ Остаётся snolla-side: content-api/routes/assets.js пустой stub — картинки не поедут на сайт пока роут не реализован (код-таска snolla, не блокер миграции).

(исходное описание ниже сохранено до prune)

Залить Local-storage assets-оригиналы сайта pilorama98 в S3 — латентная дыра того же класса, что галереи (закрыта миграцией migrate-gallery-originals-to-s3). snolla отдаёт inline-картинки контента (<img src="/assets/<ownerId>/<size>/<file>">) через imgproxy с источником s3://assets/<ownerId>/<storageFilename>, но объектов в S3 нет — imgproxy 404 "Source image is unreachable". БД-записи есть, файлы только на legacy-IIS диске. Тип StorageClient="assets" — это MoreThenCms.FileStorage.Local.*, в S3 никогда не мигрировали.

Acceptance criteria

  1. Legacy assets-оригиналы pilorama98 залиты в S3 bucket assets ключами <ownerId>/<storageFilename> (path A, как с галереями: storageClient = имя бакета).
  2. Контрольный прозвон imgproxy подтверждает 200 image/webp на реальном объекте + 404 на несуществующем (негативный контроль).
  3. Verify: dest object count == source, размер == source.
  4. Зафиксирована точная форма ключа и охват (какие ownerId/папки залиты), отчёт в inbox victor/snolla.

Подтверждённое репро (от victor/snolla, 2026-06-13)

  • URL у консьюмера: http://localhost:3742/assets/83150d773cf0455ea58ddcccb2578713/800x450/brevno.jpg → 404.
  • БД MoreThenCms (mssql.kzntsv.site), таблицы Folders/Files:
    • Folders: OwnerId 83150d77-3cf0-455e-a58d-dcccb2578713 → корневая папка / (FolderId 03CE5D8D-95B9-4D81-B222-80AEC76225CD). Внимание: ownerId это владелец медиа-папки, НЕ siteId (siteId pilorama98 = 37e67fc4…).
    • Files: brevno.jpg → StorageClient assets, StorageFilename brevno.jpg, ContentType image/jpeg, Size 580667 B.
  • Прямой прозвон imgproxy подписанным URL источника s3://assets/83150d773cf0455ea58ddcccb2578713/brevno.jpgHTTP 404 "Source image is unreachable" (подпись валидна, объекта в S3 нет).
  • Подписанный путь для верификации после заливки: https://imgproxy.kzntsv.site/n8AdQMelCr2lIvWzeQ3Mw6Fqxft3Q24Rd3vCg-ctHaA/fill/800/450/ce/1/czM6Ly9hc3NldHMvODMxNTBkNzczY2YwNDU1ZWE1OGRkY2NjYjI1Nzg3MTMvYnJldm5vLmpwZw.webp → ждём 200 после миграции.

Подсказки по форме ключа (важно — отличие от галерей)

  • Галереи: prefix = String(site.id) (siteId сайта). Ассеты: prefix = OwnerId папки из таблицы Folders, НЕ siteId. Для brevno.jpg это 83150d773cf0455ea58ddcccb2578713. На диске ассеты, вероятно, разложены по этим owner-id, а не по siteId.
  • Ключ верстается snolla как s3://${asset.storageClient}/${ownerId}/${asset.storageFilename} (middleware/assets.js:98), storageFilename берётся verbatim (с заменой \/).
  • Залить ВСЕ assets-папки/owner'ы pilorama98 (не только owner brevno.jpg) — иначе остальные inline-картинки контента дадут ту же дыру.

Хвосты (контекст, не блокеры)

  • Тот же Local-класс у images / scripts / stylesheets / watermarks — если snolla отдаёт их через imgproxy, их ждёт та же дыра (отдельный аудит на стороне victor/snolla, таска audit-local-storage-clients-imgproxy-s3-gap).
  • На стороне snolla отдельно нужна реализация content-api/routes/assets.js (сейчас пустой stub) — это код-таска victor/snolla, не блокер миграции, но картинки не поедут пока не сделаны ОБА (миграция + роут).

Обязательные скилы — вызвать до начала работы

  • invoke using-tasks — управление статусом задачи
  • invoke project-discipline — дисциплина коммитов/пушей
  • invoke using-projects-meta — cross-project: отчёт в inbox victor/snolla при close/park
  • invoke systematic-debugging — прозвон по фактам (DB → диск → S3 → imgproxy) до выводов

TDD: нет — инфра-миграция/ops (если всплывёт код — отдельной impl-таской с TDD). Разрешения: интерны: нет | автопуш: да weight: needs-human notify: victor/snolla

Status: done (closed 2026-06-13) Where I stopped: мигрировано всё дерево assets. rclone size min:assets == источник (4750 / 225935853). Smoke 200/404 OK. Next action: — (хвост на стороне snolla: реализовать content-api/routes/assets.js; остальные Local-классы images/scripts/stylesheets/watermarks — отдельный аудит snolla, если поднимутся через imgproxy). Branch: master Notify: victor/snolla — inbox .claude-inbox/2026-06-13T20-14-16Z-admin.md

Closure note (2026-06-13):

  • Диск (RUVDS IIS C:\sites\snolla\App_Data\assets): Local storage class, 145 owner-папок (32-hex), плоские файлы. brevno owner 83150d77… = 6 файлов, brevno.jpg=580667 B == DB Files.Size. Ключ-форма <ownerId>/<storageFilename> подтверждена на диске.
  • MinIO (books-vds, minio.kzntsv.site): bucket assets отсутствовал → создан. Залив rclone v1.74.2 (env-var s3-remote, без записи cred), App_Data\assetsmin: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 <registry>/pilonuxt:<tag> .
  • VERDACCIO_TOKEN — ЖИВОЙ JWT (len ~300, 2 точки), User-env этой же машины (где pass). НЕ legacy-opaque (len ~196). Детали: pilonuxt .wiki/concepts/verdaccio-token-lifecycle.md.
  • Билд ходит в сеть: verdaccio.kzntsv.site (пакеты) + mssql.kzntsv.site:1433 (postinstall→schema-gen генерит src/generated/ из CMS-БД по config/default.json). Если собираешь там, где CMS-БД недоступна — предгенери yarn schema-gen и положи src/generated/ в контекст (schema-gen в билде тогда пропустится).
  • Многостадийный, нативных модулей НЕТ (better-sqlite3 выпилен) → build-toolchain не нужен.

Runtime egress из контейнера:

  • mssql.kzntsv.site:1433 — живые запросы CMS (cacheTimeout=0), обязателен.
  • smtp.yandex.ru:465 — письма форм (contacts/checkout).
  • Собственный публичный origin — SSR self-call на /snolla через useRequestURL().origin (app/plugins/snolla.ts). Контейнер должен резолвить+достигать свой публичный домен через Traefik. До cutover домена origin-петля на боевом не замкнётся.

Env:

  • build: VERDACCIO_TOKEN (secret, НЕ ARG/ENV — иначе осядет в слоях).
  • runtime (опц.): NITRO_PORT(деф 3000), NITRO_HOST(деф 0.0.0.0), NUXT_PUBLIC_GTM_ID(деф боевой GTM-MNNXFJ6; пустой → выкл), NUXT_PUBLIC_SNOLLA_BASE_URL(деф /snolla).
  • Секреты (MSSQL/SMTP/imgproxy key+salt/siteId) ЗАПЕЧЕНЫ в config/default.json внутри образа (решение vitya — образ только в приватном registry). Env-override не настроен.

Контекст инфры (с твоей доски):

  • imgproxy на books-vds (стек 29/30); у фронта /imgproxy/** = routeRule-редирект на imgproxy.kzntsv.site.
  • ⚠️ Известный gap: snolla-side content-api/routes/assets.js — пустой stub (ты флагал в migrate-assets-originals-to-s3); без него часть картинок может не рендериться. Проверь на smoke.
  • Прод www.pilorama98.ru сейчас = легаси MoreThenCms. Cutover домена — решение vitya: деплой на временный поддомен (напр. pilonuxt.vds.kzntsv.site) для smoke ИЛИ сразу боевой — согласуй.

Acceptance:

  1. Образ собран и в registry.
  2. Portainer-стек за Traefik, healthy.
  3. Smoke: главная 200 + SSR-разметка (не 500); товары/категории из CMS; картинки рендерятся; форма contacts шлёт письмо.
  4. Доклад в inbox victor/pilonuxt: URL, имя стека/тег образа, итог smoke.

Обязательные скилы — вызвать до начала

  • invoke using-tasks — статус задачи
  • invoke project-discipline — дисциплина коммитов/пушей
  • invoke using-vds-ops — диагностика контейнеров VDS
  • invoke using-projects-meta — cross-project (notify pilonuxt, чтение pilonuxt-вики)
  • invoke using-wiki после деплоя — заингесть итог в .admin/.wiki/concepts/

TDD: нет — ops/deploy, не код. Разрешения: интерны: нет | автопуш инфра-конфигов: да weight: needs-human (deploy-инфра) notify: victor/pilonuxt

Status: done (closed 2026-06-15) Closure / coverage check: vitya флипнул DNS pilorama98.ru+www → 89.253.255.94; я перенастроил traefik-роутер на боевые хосты (Host(www.pilorama98.ru)||Host(pilorama98.ru)||Host(pilonuxt.vds.kzntsv.site), cert LE на apex+www), снял smoke GTM-override (прод-аналитика GTM-MNNXFJ6 ВКЛ), выкатил 125a3e2 (фикс «Продукция»→/catalog). Acceptance: (1) образ в registry ✓ 125a3e2; (2) Portainer stack 16 за traefik healthy ✓; (3) smoke ✓ — финальный краул с VDS по боевому www.pilorama98.ru: все страницы page 200/viewModel 200 (главная/каталог/категории/товар/услуги/контакты/оплата/доставка), картинки imgproxy 200 webp, x-powered-by Nuxt (не IIS), cert valid, футер /products битый линк убран (0 вхождений, «Продукция»→/catalog); форма contacts — закрыта (vitya «формы заебись» + pilonuxt end-to-end verified в dev, smtp egress жив); (4) доклад pilonuxt ✓. Урок применён: краулил весь меню+viewModel, не 2 роута (smoke-crawl-real-pages-not-two-routes). Follow-ups (не блокеры, отдельно):

  • Старый IIS RUVDS 80.64.31.36 — РЕШЕНИЕ vitya 2026-06-15: держим для отката, НЕ выводим. ⚠️ Этот IIS = snolla catch-all на 25 хостнеймов (emspb/kupimknigi/labtools/…), целиком гасить нельзя — при будущем cleanup снимается только биндинг pilorama98, не весь сайт. Rollback pilorama98: вернуть DNS reg.ru pilorama98.ru+www80.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 нет) → resolutions3622662 зелёный.


🟢 [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): hasMerchantReturnPolicyreturnPolicyCategory=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_ESMpackages/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:
    "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:<pass vds-kzntsv/registry-books-ci>, serveraddress:"registry.kzntsv.site"} written into /opt/books/job-scheduler/config/default.json (bind-mount → scheduler container, verified pwlen=48, valid JSON). Backup: default.json.bak-pre-registryauth-2026-06-18.
  • No restart performed — current running code doesn't read registryAuth yet (inert/backward-compatible). Activation happens on books' code deploy (rebuild = restart, bind-mounted config persists). If books had already deployed the new code, a single books-job-scheduler restart activates it.
  • Cred itself verified against registry earlier: books-ci Basic → 200 on books-tool-create-picking-list-pdf:master, wrong-pass → 401.
  • Image + master tag present in registry → pull works as soon as code+cred are both live; no re-push needed.

🟢 [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) добавить секцию:

"registry": {
  "baseUrl": "https://registry.kzntsv.site",
  "username": "books-ci",
  "password": "<из pass vds-kzntsv/registry-books-ci>"
}

(baseUrl+username уже в git default.json; нужен только password — секрет, в git его нет).

⚠️ Файл config-volume: соблюсти права как в memory feedback_container_user_node_uid_1000 — контейнер бежит под uid 1000, НЕ ставить 600 root:root (был прод-даун 2026-06-18 на createPickingListPdf из-за EACCES). Нужен read для uid 1000 (644).

Acceptance

  • registry.password присутствует в task-runner config-volume.
  • Ручной trigger registryGc через /scheduler с dryRun=true отрабатывает без 401: в отчёте видны repos books-* и «будет удалено N». (Если 405 на DELETE — значит на registry не включён REGISTRY_STORAGE_DELETE_ENABLED=true; отдельный вопрос, отметить.)

Обязательные скилы — вызвать до начала работы

  • invoke using-tasks — для управления статусом задачи
  • invoke using-projects-meta — cross-project tasks/wiki
  • invoke books-ops-mcp — проверить контейнер/конфиг task-runner read-only до и после

TDD: нет — ops/secret, кода нет. Разрешения: интерны: нет (секрет) | автопуш: n/a (ручная ops на VPS). weight: needs-human — секрет + доступ к VPS config-volume, критичная инфра. notify: OpeItcLoc03/books

Status: 🟢 done — кред положен, acceptance (auth/dryRun) verified Where I stopped: Закрыта. Секция registry (baseUrl+username+password books-ci, полная — bind-mount затеняет config образа) в /opt/books/task-runner/config/default.json (books-vds, 644 root сохранён, бэкап .bak-pre-registry-2026-06-18, только ключ registry added, EACCES-инцидент 18.06 не повторён). После deploy нового кода (образ master-bf2a8c5, код a3734d5 внутри) прогнан registryGc {dryRun:true,debug:true} через scheduler-dispatch-путь (POST /tasks/registryGc + x-agenda-job-id, idентично http.js handler'у). Результат: success, 401 нет нигде (hit /v2/_catalog=14 repos, /tags/list, resolveManifest), repos books-* видны (7 processed), отчёт task-reports/j/...md. Acceptance coverage: (1) registry.password в config-volume ✓ (len 48, config.get резолвится). (2) dryRun без 401 + repos books-* + «будет удалено N» ✓ (N=0 — см. finding ниже; критерий по форме выполнен). 405/REGISTRY_STORAGE_DELETE_ENABLED — dryRun DELETE не слал, не проверено этим прогоном (по вики registry-kzntsv-auth-model уже =true). Finding (отдано books, НЕ блокер кред-таски): GC аутентифицируется и бежит, но ничего не удаляет — по каждой репе keep=tags1, drop=0 независимо от keepLastN=3. Root cause в books-коде lib/registryV2.js: getCreated возвращает null для всех манифестов (вероятно configDigest не извлекается — OCI image-index/manifest-list от buildx, или blob 404), а planDeletions ставит protected=true любой группе с created==null → drop пуст всегда. keepLastN не применяется. Owner — books-сессия. Branch: n/a Notify: OpeItcLoc03/books


🟢 [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/pilonuxtapps/web (Nuxt-сайт)
  • OpeItcLoc03/pilorama98.ru-legacyapps/legacy (старый сайт)
  • OpeItcLoc03/snolla-products-loaderapps/loader

Acceptance criteria

  1. Все 3 Gitea-репо в archived (read-only); проверить archived:true через API/UI.
  2. В README/описание каждого архивного репо — указатель «переехало в victor/pilorama98.ru → apps/<...>».
  3. Локальные папки ~/projects/{pilonuxt,pilorama98.ru-legacy,snolla-products-loader} удалены ПОСЛЕ подтверждения архива. Предохранитель перед rm: git -C <dir> status чист И git -C <dir> log @{u}.. пуст (нет unpushed). Контент уже в монорепе и запушен.
  4. projects-meta: поллер не должен surface'ить stale-доски этих репо (archived → skip; свериться что meta_status.archived_skipped вырос). Активные задачи уже перенесены в монореп.
  5. Shared meta-wiki: если есть страницы про эти 3 проекта — обновить (переехали в монореп); если нет — короткая запись в concepts о консолидации pilorama-монорепо.
  6. Отчёт в inbox pilorama98.ru.

Обязательные скилы — вызвать до начала

  • invoke using-projects-meta — meta-wiki/tasks мутации (preview→confirm) + freshness-gate
  • invoke using-tasks — статус этой задачи
  • invoke project-discipline — дисциплина коммитов/пушей

TDD: нет — ops/админ-задача (verification = archived:true + папки удалены + meta обновлён). Разрешения: интерны: нет | автопуш: да weight: needs-human notify: pilorama98.ru

Status: done Where I stopped: closed — 5/5 acceptance met (archived:true ×3 / README+desc указатели / папки удалены / поллер скипает 1→4 / shared-wiki concept создан). Отчёт пиру отправлен (с замечанием по их sequencing-багу — две связанные таски делегированы в обратном порядке). Next action:Branch: n/a Notify: victor/pilorama98.ru


🟢 [deploy-pilorama-web-from-monorepo] — Раскатать на прод текущий apps/web из монорепы victor/pilorama98.ru — содержит SEO on-page фиксы (тех-аудит 2026-06-18): buildProductSeo (172 товарных title+meta, commit 03c250b) + seoOverrides (16 category/service title + 8 meta, commit b73c7c8). На проде их пока НЕТ.

Ключевое — перенацелить источник сборки. Пайплайн исторически собирал из victor/pilonuxt (см. web-вики apps/web/.wiki/concepts/docker-deploy.md). После консолидации 2026-06-18 код в victor/pilorama98.ru/apps/web/ (Dockerfile приехал с subtree, там же). Сборку надо перенаправить: clone монорепы, build-контекст = apps/web/.

Acceptance criteria

  1. Build из victor/pilorama98.ru, контекст apps/web/ (двухстадийный Nitro node-server; VERDACCIO_TOKEN как docker build-secret — живой JWT len~300, npmAlwaysAuth:true; порядок COPY scripts/ + src/generated/ до install — всё в docker-deploy + verdaccio-token-lifecycle).
  2. Деплой образа на VDS (Portainer стек за Traefik) — обычный re-deploy.
  3. Smoke на проде (verify, не «собралось=работает»): товарная …/shop/products/<slug><title> ≤60 вида «{товар} — купить в СПб» + meta description уникальная с «доставка по СПб и ЛО»; категория …/catalog/planken → короткий title «Планкен прямой и скошенный — купить в СПб»; …/shipping и блог-пост → непустая meta.
  4. Зафиксировать в web-вики (docker-deploy.md / log), что build-источник теперь монореп apps/web.

Sequencing (важно)

Перенацелить пайплайн на монореп ДО архива victor/pilonuxt (таска archive-pilorama-source-repos). Иначе после архива деплой со старого репо не подхватит SEO-фиксы — их в pilonuxt нет. Архив pilonuxt — только после зелёного деплоя с монорепы.

Обязательные скилы — вызвать до начала

  • invoke using-tasks — статус задачи
  • invoke project-discipline — коммиты/пуши
  • invoke verify — РЕАЛЬНО открыть прод-URL и глазами увидеть title/meta, не доверять «собралось»
  • invoke using-projects-meta — отчёт обратно

TDD: нет — deploy/ops-задача (verification = smoke на проде). Разрешения: интерны: нет | автопуш: да weight: needs-human notify: pilorama98.ru

Status: done Where I stopped: prod зелёный на 6c6d52d — контейнер-smoke все 200 (ecommerce-листинг 200×3, SEO title/meta рендерятся), stderr пуст. Закрыто pilorama98 по подтверждению админа. Blocker: Рантайм-регресс в snolla-пакетах из монореп-лока. apps/web тянет @snollajs/content-api@^0.11.0, который nested-депом подтягивает @snollajs/snolla@0.11.0 (рядом с прямым @snollajs/snolla@0.16.0). content-api зовёт SitesService.getSiteById из 0.11.0 → Cannot read properties of undefined (reading 'findByPk') (Sites Sequelize-модель не инициализирована) → весь SSR 500. Откатил прод на 19a4a84 (verified 200). Фикс версий snolla — зона web/snolla-владельца (пир + snolla upstream), не ops. Артефакты сборки (новый apps/web/Dockerfile + root .dockerignore) лежат в рабочем дереве монорепы, готовы к коммиту после фикса депов. Update 2026-06-18 (2): Пир снял snolla-блокер (commit eaee56f: content-api@0.12 peerDep + snolla@^0.17 + vue→3.5.38). Пересобрал из eaee56f, задеплоил → snolla-500 ушёл, home/категории 200 с верным SEO (/catalog/planken → title «Планкен прямой и скошенный — купить в СПб» ✓). НО новый блокер: sharp нативный libvips не бандлится в Nitro .output (libvips-cpp.so.8.18.3: cannot open shared object) → товарный листинг /snolla/stores/.../products 500 → каталог не грузит товары. Платформо-зависимо (локальный prod-бандл пира на его ОС грузит sharp). Откатил прод на 19a4a84 снова (home 200 + products 200, каталог жив). sharp приехал с бампом депов — старый 19a4a84 нативных модулей не имел. Blocker (актуальный): sharp/libvips не попадает в standalone Nitro output. Шов: ПОЧЕМУ sharp в товарном пути = поведение snolla/content-api; КАК забандлить = apps/web nuxt.config (nitro traceInclude/externals) ИЛИ runner-стейдж Dockerfile (скопировать @img/sharp-* нативы в .output). Образ eaee56f в реестре (не рабочий из-за sharp). Update 2026-06-18 (3): sharp-фикс СДЕЛАН (admin, Dockerfile-side): runner-стейдж копирует /app/node_modules/@img (вкл. sharp-libvips-linux-x64 с libvips-cpp.so.8.18.3) в .output/server/node_modules/@img. Verified в контейнере: import sharp → toBuffer() = SHARP_OK. Пересобрал+задеплоил → sharp из лога ушёл. НО за sharp вскрылся СЛЕДУЮЩИЙ блокер: getSiteById findByPk undefined опять, теперь в snolla 0.17 (@snollajs/snolla/lib/services/sites.js:21) на ecommerce-роуте (content-api/routes/ecommerce.js:304) → /snolla/stores/<id>/products 500. При этом pages-путь работает: / и /catalog/planken = 200 c SEO, viewModel ок. Т.е. Sites-модель доступна на pages-роуте, но не на ecommerce — snolla-0.17-внутренности. Был замаскирован sharp'ом (тот падал первым на том же роуте). Откатил прод на 19a4a84 (все 200, каталог жив). Blocker (актуальный): snolla 0.17 — getSiteById падает на ecommerce-роуте (Sites-модель undefined именно там; pages-роут ок). Зона пира/snolla. Локальный «товар 200» пира — видимо product-detail (viewModel), а не store-products-листинг; нужен прогон именно /snolla/stores/<id>/products в контейнере. sharp (моя часть) закрыт; Dockerfile-фикс ждёт в дереве монорепы (M apps/web/Dockerfile, не закоммичен). Update 2026-06-19 (4) — 🟢 GREEN на проде: пир добил 4-й слой (commit 41cf89f: @snollajs/data 0.9.0→0.9.1 — forEach-async race + Windows-path-surgery, ломавшая import() на Linux = ровно Linux-only дефект; snolla 0.17.1 единой runtime). Пересобрал из 6c6d52d (HEAD, вкл. деп-фикс + мой sharp @img COPY), запушил с VDS, задеплоил стек 16. Контейнер-smoke ВСЁ 200: товарная /shop/products/brusok-…-40-50-6000 → title «Брусок обрезной естественной влажности 40×50×6000 мм» (52≤60) + meta «…доставка по СПб и ЛО — от производителя» ✓; /catalog/planken → «Планкен прямой и скошенный — купить в СПб» ✓; /snolla/stores/<id>/products 200×3 стабильно; home/shipping/blog 200 + meta. Контейнер 6c6d52d 3мин без рестартов, stderr пуст. Acceptance 1-3 выполнены, прод отдаёт SEO. Остаётся (🟡): criterion 4 — закоммитить в МОНОРЕП (victor/pilorama98.ru) sharp-фикс apps/web/Dockerfile (некоммичен) + обновить apps/web/.wiki/concepts/docker-deploy.md («нативных модулей нет» → sharp via snolla 0.17, runner несёт @img). Это cross-repo push → ждёт явного push-ok от vitya (дисциплина Rule 4). Прод от этого не зависит — он уже зелёный. Next action: (none — kept until merged) Branch: n/a Notify: victor/pilorama98.ru


🟢 [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


🟢 [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 отправлен.

исходный наряд

(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 до 40bb383yarn install (тянет schema-gen 0.6 из verdaccio) → rm -rf apps/web/src/generatedyarn 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


🟢 [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) отправлен.

исходный наряд

(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→cf2bba2yarn installrm -rf apps/web/src/generatedyarn 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


🟢 [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:, перекатить 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 (cf2bba242f9d64, 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


🟢 [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:, перекат 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 (42f9d646b9ae63, 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


🟢 [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_TOKENyarn 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


🟢 [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. Notify→workshop.

[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.rudeploy/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: → 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


🟢 [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.pro89.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


🟢 [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.

[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


🟡 [tandemmebel-web-vds-deploy] — STAGING GREEN, ждёт cutover (DNS gated оператором). ⚠️ ОБРАЗ К ФЛИПУ ОБНОВЛЁН → tandemmebel:ed96b18 (snolla 0.42.0/core 0.24.0/data 0.14.1, digest ca4da79), СУПЕРСИДИТ b02ca18/0facb35 — раскатан на стек 20 задачей [tandemmebel-deploy-snolla-0-42-0] (2026-07-04, DoD 172/172 sitemap + 12/12 gallery-grid GREEN). Cutover = тот же DNS-gated шаг: владелец флипает reg.ru tandemmebel.ru+www→89.253.255.94 → verify авторит.NS (Resolve-DnsName -Server ns1.reg.ru) → ТОЛЬКО ПОТОМ боевой Host() в стек 20 (порядок критичен: LE HTTP-01) → live-smoke. RUVDS 80.64.31.36 = rollback (revert DNS), не тронут. Sharp media in-process (imgproxy из tandem-пути убран — прошлые imgproxy-заметки ниже historical). Историческая спека (0facb35/imgproxy-watermark) ниже — HISTORICAL, не актуальна. Smoke с VDS: status-parity 10/10, sitemap staging⊇prod (prod-only=0, +12 /projects/categories/* benign#3), trailing 301/301, blog-post 200 (+3.5KB=benign#2 imgproxy vs /galleries/), 2012-посты 31/31, theme 21/22 MD5-OK. Находка (не блокер): prod Gotham-Pro.css=0B (пустой), staging=4436B корректный → cutover чинит латентный прод-баг шрифта. Следующее: отмашка оператора на DNS → verify авторит.NS → боевой Host-rule в стек 20.

[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.ru89.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


🟢 [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 отправлен.

[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


🟢 [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.

[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


[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


🟢 [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


🟢 [labtools-ru-deploy-snolla-0-42-1] — Обновить ЖИВОЙ прод labtools.ru на VDS (Portainer стек 17) с @snollajs/snolla@0.28.20.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 -xdocker 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


🟢 [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 @ 95a5c42deploy/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 95a5c42registry.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


[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 @ 95a5c42deploy/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 95a5c42registry.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


🟢 [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=13 изделий — все == прод. 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