Files
admin/.tasks/STATUS.md
vitya 2e5adcf11e deploy(cms-s3): S3 FileStorage provider LIVE on RUVDS — admin↔MinIO split-brain closed
Разбор 500 на /admin/assets/<owner>/delete → корень ACL (app pool RX-only на
App_Data после scp-миграции) → полная развязка хранилища CMS:

- S3 FileStorage провайдер (MoreThenCms.FileStorage.S3) построен (координация с
  интерн-сессией) и раскатан LIVE на прод RUVDS: 6 контентных классов web.config
  → S3/MinIO, кэши остались Local. Смок 7 тенантов 200/301, 0×500, S3-read byte-parity.
  Upload-гоча: UseChunkEncoding=false (MinIO ⊥ AWSSDK aws-chunked).
- Весь локальный контент RUVDS → MinIO (galleries 5.3G, themes 2.7G, maxMind);
  imageCache-блоат (2.1G) выпилен из бакета themes.
- stostayer.old определён как stale-копия (не мигрировать).

Новый concept: snolla-admin-appdata-acl-500-after-scp-migration.
Трекеры: morethencms-s3-filestorage-provider (LIVE), reconcile-local-assets-to-minio (done).
Rollback: web.config.bak-pre-s3-2026-07-03 на хосте.

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

1125 lines
172 KiB
Markdown
Raw Blame History

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