Compare commits
197 Commits
a7d87faf01
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d7ddf2889 | |||
| bb2b76b8b0 | |||
| 28c70c7093 | |||
| 560711af5c | |||
| 6dfe9813b2 | |||
| 47fba6d993 | |||
| c8cc17047d | |||
| 201fc6aa58 | |||
| ad731e014f | |||
| ca265bfe2f | |||
| d1cb4d932a | |||
| 9d1ca60ddd | |||
| 7939ce3611 | |||
| e3fb31112b | |||
| d8b5344a72 | |||
| 2f695c2808 | |||
| 77f23a0614 | |||
| d1b2e3efe7 | |||
| d8bfde9e5d | |||
| 72c9b74bc8 | |||
| 0502726440 | |||
| a0fdb46958 | |||
|
|
10b02d7a9a | ||
|
|
2ed8a0a8df | ||
|
|
3ce97bc2fe | ||
|
|
078ddcbc74 | ||
|
|
cf97ab897f | ||
|
|
cc4e22e126 | ||
| 540e6b8b17 | |||
| 85b8e665e0 | |||
| 38b99da440 | |||
| 36f3c88d02 | |||
| 2173e888df | |||
| afbabb0bbd | |||
| 048478fc9f | |||
| 497d6ad036 | |||
| df98ec9038 | |||
| 3605cbc594 | |||
| bb12fb4159 | |||
| dab61342f0 | |||
| e7ef8c6529 | |||
| 9774a2b5b2 | |||
| 83aca8e1d8 | |||
| 29a5f06259 | |||
| 48bbf106d1 | |||
| a3dcc9589e | |||
| f7e132f630 | |||
| 22023d71cc | |||
| 59b16a9466 | |||
| abeb1deca7 | |||
| e60bd3acb3 | |||
| 9e1ce522da | |||
| 7388d2cb06 | |||
| e08938f656 | |||
| d518774311 | |||
| 8328e2d9a7 | |||
| 004bc0bab6 | |||
| 8463740fef | |||
| cd0937b1a7 | |||
| e1d1bd01c3 | |||
| ce11e2710e | |||
| aec6114456 | |||
| f8db4a4337 | |||
| 8b52046bb0 | |||
| c0b43d6035 | |||
| f80631ffc5 | |||
| fd50916851 | |||
| 6d29949d55 | |||
| 46f8efdca5 | |||
| 6f7f7f09be | |||
| bb9618d493 | |||
| d9d0b58251 | |||
| 446ae387c1 | |||
| 2cbd0c930f | |||
| 970556a979 | |||
| ec3d2ea52d | |||
| b07069c315 | |||
| bb9ff93024 | |||
| 3312de53c1 | |||
| 05fb1f22c8 | |||
| 67eebab07d | |||
| 38ebf02387 | |||
| 5983625370 | |||
| cbffb4a0ca | |||
| dcfb420f73 | |||
| 09a613d4bb | |||
| 2299e2fed5 | |||
| 9ce4940cf2 | |||
| 4179fb1ba6 | |||
| e2bf57f50e | |||
| 3322cd5a6c | |||
| b1cf3a5785 | |||
| 1b98232093 | |||
| fa11c4430a | |||
| 8226177153 | |||
| 5edf328ed0 | |||
| d12003b043 | |||
| 91d25a8888 | |||
| 3e5b29bc1a | |||
| 692739ab75 | |||
| f23f819d4c | |||
| 6723afb148 | |||
| 21a0219287 | |||
| 4a41be7943 | |||
| a38a4ba137 | |||
| 6547558833 | |||
| b6e0e776e8 | |||
| f2078c1c8a | |||
| e694518ec4 | |||
| 9da36ddb27 | |||
| 73f2a74644 | |||
| f6ccb5ecf2 | |||
| 6317c9b1b5 | |||
| b9f248949e | |||
| bc27f1e7af | |||
| 6143c1c3e1 | |||
| 0e33f103f0 | |||
| 5801ca90cf | |||
| 4def67cd0d | |||
| e3f17ece50 | |||
| d1964e6710 | |||
| 5844d8dddf | |||
| 048baf588b | |||
| 3a0e930e59 | |||
| 386fc7f750 | |||
| eb0fa6cbcc | |||
| ee28de7552 | |||
| 90d76a797f | |||
| 308a53530e | |||
| 9b5cb390a7 | |||
| 3872208a92 | |||
| 85b53598bf | |||
| c400081d3d | |||
| e169355498 | |||
| ceae683233 | |||
| a4d5304885 | |||
| 5edd19748f | |||
| 82aa9fb88e | |||
| 7892cafccc | |||
| b972942d3c | |||
| 67648a839a | |||
| 89aec64030 | |||
| 9bbebbc4e2 | |||
| 2faa61bd26 | |||
| b24d0d5c4f | |||
| edf13d9a31 | |||
| 24c3fef6ea | |||
| 9683216f7d | |||
| 1f03251a9d | |||
| ca55a7951e | |||
| 3f70988d04 | |||
| d364c4081f | |||
| fd3bcdd418 | |||
| e8345716c7 | |||
| 9d6befdffe | |||
| d7758cada5 | |||
| 4a75fd7415 | |||
| 026d3ca4f0 | |||
| 71b85fafaf | |||
| bf3b26f3a0 | |||
| dfbb964cbe | |||
| 06f1e3b485 | |||
| a05b3c75e9 | |||
| a4237bb8d4 | |||
| 7d336de9db | |||
| 2f79f4319e | |||
| c8da30f93d | |||
| 1d2c72d557 | |||
| d622c451c1 | |||
| f9e7111493 | |||
| 0672a8dfb2 | |||
| 7fc4de2ea1 | |||
| 730f067e13 | |||
| 15eff6bd01 | |||
| 55f6f90131 | |||
| f9ffe29c30 | |||
| 1acb80b764 | |||
| d7ba438f64 | |||
| f02915d750 | |||
| 134a789a4d | |||
| 0e3d91a430 | |||
| 9b7e9b2aef | |||
| 8cb45e04a5 | |||
| 1a5888e59a | |||
| 8e721d2667 | |||
| 5c55348ee7 | |||
| 8b064b1ad8 | |||
| 1f876f7330 | |||
| b9bd860015 | |||
| 1712538722 | |||
| b2f3e0f9a4 | |||
| 606b1e5b6b | |||
| 1f0b7f0cc9 | |||
| b64887f301 | |||
| 5e73b20806 | |||
| 5105f130a9 | |||
| c40f2f415c |
5
.agents/inbox/README.md
Normal file
5
.agents/inbox/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# ⛔ Файловый инбокс закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канал почты — mappa (`mcp__mappa__inbox_*`): письма = inbox-сущности проекта. Скилы: `mappa-messaging`, `mappa-session-orient` (raise on start).
|
||||||
|
|
||||||
|
Файлы ниже — легаси-история (файловый канал закрыт решением 2026-08-25).
|
||||||
146
.agents/skills/admin-runbooks/SKILL.md
Normal file
146
.agents/skills/admin-runbooks/SKILL.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
---
|
||||||
|
name: admin-runbooks
|
||||||
|
author: ours
|
||||||
|
version: 1.1.0
|
||||||
|
description: >
|
||||||
|
Единый контур ранбуков зоны админа (.admin): НАЙТИ нужный ранбук перед любой
|
||||||
|
прод-операцией → ИСПОЛНИТЬ по нему (чек-лист + verify + откат + письмо/таска) →
|
||||||
|
ВЕСТИ (обновлять после инцидентов/изменений) и СОЗДАВАТЬ новые. Поглощает
|
||||||
|
writing-runbooks (v1.0.0, superseded) — секция WRITE это его канон.
|
||||||
|
Trigger (user): деплой/редеплой/рестарт/ротация/миграция любого сайта или стека;
|
||||||
|
«по ранбуку», «как деплоить X», «напиши/обнови ранбук», «есть ли ранбук по X»;
|
||||||
|
любой прод-инцидент на сайте, который может потребовать операции.
|
||||||
|
---
|
||||||
|
|
||||||
|
# admin-runbooks
|
||||||
|
|
||||||
|
Один контур: **индекс → USE → EXECUTE → WRITE/UPDATE**. Цель — устранить
|
||||||
|
«рыскание» (поиск рецепта по сессиям/чатам/grep), когда ранбук есть или должен быть.
|
||||||
|
|
||||||
|
> ⚠️ **СЛУЖЕБНАЯ зона `.admin`.** Ранбуки — служебные документы `.admin`:
|
||||||
|
> применять/выполнять может только `.admin`; другие проекты читают по запросу,
|
||||||
|
> не выполняют; наружу не выносить. Прод-операция по ранбуку = таска на борде
|
||||||
|
> `.admin` + письмо от заказчика (исполнитель-не-админ не деплоит сам).
|
||||||
|
|
||||||
|
## 0. ИНДЕКС РАНБУКОВ (всегда первый шаг)
|
||||||
|
|
||||||
|
**Индекс:** mappa wiki-сущность [[wiki:3316]] (`concepts/runbooks-index`, .admin) —
|
||||||
|
единственный источник «какой ранбук для чего». **Прибит в AGENTS.md** — контур обязателен.
|
||||||
|
> Файловый канал `.admin/.wiki/concepts/` закрыт (2026-08-29, task:1507): файлы — стубы
|
||||||
|
> «не читать, не править», канон — mappa wiki-сущности.
|
||||||
|
|
||||||
|
Перед ЛЮБОЙ прод-операцией (деплой/редеплой/рестарт/ротация/миграция/инцидент):
|
||||||
|
|
||||||
|
1. **Прочитай индекс** (`wiki_get(project=".admin", slug="concepts/runbooks-index")`,
|
||||||
|
или [[wiki:3316]]) — найди строку по проекту/операции → ранбук (wiki-сущность).
|
||||||
|
2. Ранбук может жить в двух местах (индекс указывает, где):
|
||||||
|
- mappa wiki-сущности `.admin` (`wiki_get`/`wiki_search`) — канон для VDS-проектов
|
||||||
|
([[wiki:160]], [[wiki:1254]], [[wiki:3330]] и т.д.; полный список — в индексе);
|
||||||
|
- `<репо-проекта>/.wiki/concepts/docker-deploy.md` и т.п. — репо-вики проекта
|
||||||
|
(pilorama98.ru: `apps/web/.wiki/concepts/docker-deploy.md`) — репо-вики живут файлом,
|
||||||
|
это канал самого проекта, не `.admin`.
|
||||||
|
3. **Нет ранбука → НЕ деплоить молча.** Создать (см. WRITE) до/вместе с операцией:
|
||||||
|
отдельной таской или в теле операционной таски (пункт «нужен ранбук»).
|
||||||
|
Это не бюрократия: отсутствие ранбука = повторное рыскание в следующий раз.
|
||||||
|
|
||||||
|
## 1. USE — найти и прочитать ранбук
|
||||||
|
|
||||||
|
- Сначала индекс → ранбук. Не начинать grep по сессиям/чатам — это анти-паттерн
|
||||||
|
(источник этой секции: инцидент 2026-08-26 pilonuxt — рыскал, ранбука не было).
|
||||||
|
- Прочитать ранбук **целиком** перед операцией (секреты — плейсхолдеры, реальные
|
||||||
|
значения — из pass по указателям; ранбук сам секретов не несёт).
|
||||||
|
- Сверить актуальность: теги/версии на проде могут обогнать ранбук (пример:
|
||||||
|
stostayer-web ранбук показывал 0.3.23, прод был 0.3.26 → тег брать с проде,
|
||||||
|
а ранбук потом обновить — WRITE). Если расхождение → пометить на апдейт.
|
||||||
|
|
||||||
|
## 2. EXECUTE — деплой/операция по ранбуку
|
||||||
|
|
||||||
|
Выполнять шаги ранбука как чек-лист, **не пропуская**:
|
||||||
|
|
||||||
|
1. **Предусловия** (auth: JWT/токены из pass; docker login; pull-ДО-рестарта;
|
||||||
|
.dockerignore/.yarnrc временные модификации — пометить на возврат).
|
||||||
|
2. **Шаги операции** — дословно по ранбуку, порядок важен (gotchas в ранбуке).
|
||||||
|
3. **Verify** по секции ранбука (страницы/эндпоинты/логи/uptime) — зелёный перед
|
||||||
|
закрытием. При красном — rollback-путь из ранбука, не импровизация.
|
||||||
|
4. **Фиксация результата:**
|
||||||
|
- sync source-of-truth compose (если менялся тег) → commit + push;
|
||||||
|
- письмо-отчёт заказчику (inbox_send, from `.admin`, с verify-таблицей);
|
||||||
|
- закрыть операционную таску (reason = что сделано + verify);
|
||||||
|
- при необходимости — обновить ранбук (WRITE): новый тег, новые gotchas.
|
||||||
|
|
||||||
|
Ограничения: одна попытка там, где ранбук велит одну (egress-баны, retry-штормы);
|
||||||
|
не отклоняться от ранбука без причины — отклонение фиксировать в письме.
|
||||||
|
|
||||||
|
## 3. WRITE / UPDATE — создание и ведение ранбуков
|
||||||
|
|
||||||
|
Канон (наследие writing-runbooks v1.0.0, поглощено):
|
||||||
|
|
||||||
|
### Жёсткие правила
|
||||||
|
|
||||||
|
1. **Ранбук — служебный документ `.admin`.** Применять может только `.admin`.
|
||||||
|
2. **Из проекта не выносить.** Не копировать в другие вики, не публиковать.
|
||||||
|
3. **Деплой = таска + письмо админу.** Любая прод-операция — по задаче на борде
|
||||||
|
`.admin` и письму. Исполнитель-не-админ ставит задачу, не деплоит сам.
|
||||||
|
4. **Служебный блок — в первых абзацах** (машиночитаемый маркер служебности):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.**
|
||||||
|
> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять.
|
||||||
|
> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать.
|
||||||
|
> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют.
|
||||||
|
```
|
||||||
|
|
||||||
|
Дословно, без перефразирования.
|
||||||
|
|
||||||
|
### Гигиена секретов (решение 5, write-тайм сканер mappa)
|
||||||
|
|
||||||
|
**В ранбуках никогда не бывает реальных секретов.** Только плейсхолдеры +
|
||||||
|
указатели на pass: `<из pass <path>/<FIELD>>`. Сканер блокирует запись (422)
|
||||||
|
на password/token/api_key/secret+значение ≥6 симв., AKIA…, JWT, ssh-ключи.
|
||||||
|
Обход плейсхолдером: значение <6 симв. (`'<...>'`), либо ключ-слово не латиницей,
|
||||||
|
либо разрыв `: ` коротким словом (`PASSWORD: см. pass (…)`). После записи —
|
||||||
|
проверить `admin_secret_scan` (0 хитов).
|
||||||
|
|
||||||
|
### Структура ранбука
|
||||||
|
|
||||||
|
1. Frontmatter: `title`, `type: concept`, `tags: [.., runbook]`, `related`, `updated`.
|
||||||
|
2. H1 + служебный блок (обязательно, дословно).
|
||||||
|
3. Scope (VDS, стек Portainer Id, endpointId, source-of-truth compose).
|
||||||
|
4. Артефакты (код/образ/стек/БД/эндпоинт; тег = как на проде).
|
||||||
|
5. Шаги операции (копируемые команды) + таблица «рестарт vs пересборка» если уместно.
|
||||||
|
6. Verify/smoke после операции.
|
||||||
|
7. Rollback-путь всегда.
|
||||||
|
8. Gotchas из практики (нумерованные, с датой).
|
||||||
|
|
||||||
|
### Ведение (UPDATE)
|
||||||
|
|
||||||
|
Обновлять ранбук когда:
|
||||||
|
- прод ушёл вперёд (новый тег/версия) — поправить инвентарь/таблицы;
|
||||||
|
- появилась новая gotcha (инцидент с неочевидной причиной);
|
||||||
|
- изменился канал деплоя (адреса/стек/порядок).
|
||||||
|
После правки: bump `updated:`, синхронизировать с индексом (что покрывает),
|
||||||
|
проверить secret_scan. Коммит отдельный от операционного («docs(runbook): …»).
|
||||||
|
|
||||||
|
## Порядок записи (мутации вики)
|
||||||
|
|
||||||
|
- **Ранбук живёт ТОЛЬКО в mappa как wiki-сущность** (канон стуб wiki:3328 р.10, 2026-08-29):
|
||||||
|
`wiki_create(project=".admin", slug="concepts/<имя>")` (или `wiki_update` при правках).
|
||||||
|
Файл `.admin/.wiki/concepts/<имя>.md` — стуб-указатель «не читать, не править»;
|
||||||
|
git-история стуба хранит прежний контент (`git show <parent>:<path>`).
|
||||||
|
- Индекс ([[wiki:3316]]) — обновить строку/ссылку на новый ранбук.
|
||||||
|
- Секрет-сканер: `mcp__mappa__admin_secret_scan` после записи — 0 hits.
|
||||||
|
|
||||||
|
## Место жительства этого скила
|
||||||
|
|
||||||
|
Скил лежит в `.admin/.agents/skills/admin-runbooks/SKILL.md` — проектные скилы
|
||||||
|
`.admin` (pi: `.agents/skills/` в cwd). НЕ дублировать в `~/.agents/skills/`
|
||||||
|
(глобальная установка, перезаписывается update-skills) и НЕ в общий skills-репо.
|
||||||
|
Обновление — правкой файла в проекте + commit/push (.admin).
|
||||||
|
|
||||||
|
## Связи
|
||||||
|
|
||||||
|
- [[wiki:3316]] (`concepts/runbooks-index`) — индекс ранбуков (обязательный первый шаг).
|
||||||
|
- [[wiki:2608]] (AGENTS .admin) — канон зоны: runbooks rule, artifact placement rule.
|
||||||
|
- `writing-skills` — RED-GREEN-REFACTOR для скилов (если править сам скил).
|
||||||
|
- `using-vds-ops` — диагностика контейнеров (read-only), не деплой.
|
||||||
|
- `project-discipline` — семвер/пуш-правила при правке версионируемых артефактов.
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,3 +43,6 @@ pilonuxt-home-smoke.jpeg
|
|||||||
host-stacks/local/sched-pipelines/tasks.generated.json
|
host-stacks/local/sched-pipelines/tasks.generated.json
|
||||||
host-stacks/local/sched-pipelines/data-ym/
|
host-stacks/local/sched-pipelines/data-ym/
|
||||||
host-stacks/local/sched-pipelines/data-ntfy/
|
host-stacks/local/sched-pipelines/data-ntfy/
|
||||||
|
.tmp-*
|
||||||
|
daemon3.log
|
||||||
|
t2.db
|
||||||
|
|||||||
12
.mappa/config.yaml
Normal file
12
.mappa/config.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# mappa project marker — machine-readable identifier of a mappa project folder
|
||||||
|
# (спека brainstorm:151 р.5 · task:1538 · контракт: concepts/dot-mappa-marker)
|
||||||
|
# Только публичные данные реестра; секреты сюда не попадают.
|
||||||
|
schema_version: 1
|
||||||
|
protocol_version: 1
|
||||||
|
project: .admin
|
||||||
|
tenant: vitya
|
||||||
|
url: https://mappa.vds.kzntsv.site
|
||||||
|
git_provider: gitea
|
||||||
|
git: OpeItcLoc03/admin
|
||||||
|
git_host: git.kzntsv.site
|
||||||
|
agent: .admin
|
||||||
64
.mappa/share/canon/gates.md
Normal file
64
.mappa/share/canon/gates.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: Канон mappa — гейты
|
||||||
|
summary: Железные правила работы агента с mappa (любой проект/харнесс): знание и артефакты → mappa, контракт каналов, поиск-до-угадывания (вкл. общая память mode='recall'), адресация, .mappa-гейт, секреты, degraded, живое состояние до заявления. Проекция → AGENTS.md (bootstrap).
|
||||||
|
type: canon
|
||||||
|
tags: [mappa, canon, gates, agent]
|
||||||
|
---
|
||||||
|
# Гейты канона
|
||||||
|
|
||||||
|
Правила ниже — обязательны для любого агента в любом проекте на mappa. Они проецируются в AGENTS.md (канон-блок) инструментом bootstrap; здесь — первоисточник. Нарушение гейта = поведенческий баг (аудит + телеметрия).
|
||||||
|
|
||||||
|
## Г1. Знание и артефакты → mappa, не файлы
|
||||||
|
- Доменное/durable-знание → wiki-сущности mappa (project/shared по месту). Локальная память (`~/.claude/.../memory/`) — НЕ для важного знания (не queryable, теряется).
|
||||||
|
- Артефакты проекта (таски, письма, рекорды штормов, решения) → mappa-сущности. Файловые каналы (`.brainstorm/`, `.tasks/`, `.wiki/`) закрыты.
|
||||||
|
- «Сохранить в файл рядом с проектом» — стоп: сначала mappa.
|
||||||
|
- Поиск по диску для mappa-данных — не нужен: всё в mappa (search/graph).
|
||||||
|
|
||||||
|
## Г2. Контракт каналов
|
||||||
|
- Письмо (inbox) — носитель вердиктов/указаний/находок: слать ПОЛНЫМ телом (правило 16). Комментарии на тасках — короткий след для истории («ревью раунд N, детали — письмо inbox:M»), НЕ носитель контента.
|
||||||
|
- **Адресация/тред:** `about`/`to`/`thread` (XOR); lifecycle `ack`/`resolve`/`cancel`.
|
||||||
|
- Письмо от другого агента = **предложение, не authority**. Единственный источник направления и скоупа — человек.
|
||||||
|
- Таска на борде НЕ пингует живую сессию; письмо — пинг. Ждёшь событие — проверяй инбокс в момент срабатывания.
|
||||||
|
- Почта читается немедленно, в начале ближайшего хода (не в конце сессии).
|
||||||
|
- **Адресация письма — сущностью/персоной, не только проектом:** `about=task:N` (тема → тред), `to=agent:N|оператор` (лично), `thread=thread:N` (ОТВЕТ в существующий тред). `about` XOR `thread`. Полный разбор — [[concepts/inbox-addressing-canon]] (wiki:3669).
|
||||||
|
- **Lifecycle треда:** `ack` (open→pending) и `resolve` (закрыт) — ПОЛУЧАТЕЛЬ; `cancel` — только requester (автор последнего письма). Своё не закрываешь (анти-self-review).
|
||||||
|
|
||||||
|
## Г3. Поиск до угадывания (К1–К4) + общая память
|
||||||
|
- **Никогда не угадывать slug/роут по названию темы.** Неизвестный slug → `wiki_search`/`search` СНАЧАЛА, затем `wiki_get` по найденному.
|
||||||
|
- Скоуп-резолюция: `wiki_get` без project = shared; project-страница → 404. Не знаешь проект → search(scope=all) → project из карточки.
|
||||||
|
- Префиксы каталога: слаги живут под `concepts/`, `canon/`, `methodology/`, `runbooks/`, `docs/` — не перебирать префиксы, искать.
|
||||||
|
- Адресация — только публичные ключи: num | `type:N` | uuid. НЕ route-guessing, НЕ internal id, НЕ registry id как entity-ref.
|
||||||
|
- **Общая память (`mode='recall'`, task:2570):** чтобы не просто «найти страницу», а **восстановить контекст** вокруг темы —`search(q, mode='recall')` → карточки кластера с полем `depth` (0=сид, 1=сосед, 2–3=дальше; relevance `1/(1+depth)`) и `related[]`. Это mappa-память: агент получает её **запросом**, не подсовыванием. Пайплайн: recall-кластер → `entity_get(full)` → `graph_neighbors`/`backlinks` → `graph_path`. Полный рецепт — [[runbooks/search]] §5.
|
||||||
|
- **Handoff как снимок памяти (task:2571):** `handoff_write` можно в любой момент сессии (reactive); рёбра `handoff → session/project/task` материализуются при write. Восстановление после разрыва: `search(type='handoff',...limit=1,sort=recency)` → `entity_get(full)`. Полный рецепт — [[runbooks/search]] §6.
|
||||||
|
|
||||||
|
## Г4. Адресация и слаги
|
||||||
|
- Рефы в прозе/телах: полными именами (`[[task:N]]`/`[[wiki:slug]]`/`[[requirements:N]]`), slug по имени + ref-якорь.
|
||||||
|
- Слаги: kebab-case, латиница; кириллица транслитерируется.
|
||||||
|
- Номера task:N — только после создания (сервер выдаёт, не резервирует).
|
||||||
|
|
||||||
|
## Г5. .mappa-гейт
|
||||||
|
- Папка участвует в mappa-операциях ТОЛЬКО с маркером `.mappa`. Проверка — единый helper (dot-mappa-gate).
|
||||||
|
- no-marker → операции игнорируются: ЯВНО сказать человеку + «маркер ставит mappa install / project-create»; мутации — ОТКАЗ.
|
||||||
|
- stale (маркер есть, слаг не резолвится/тенант не тот) → не писать.
|
||||||
|
|
||||||
|
## Г6. Секреты
|
||||||
|
- Секреты не пишутся в mappa (тела/комменты/письма → 422). Секреты — только `secret:<path>` рефы; значения мимо mappa.
|
||||||
|
- Ozon-креды и подобное — приватный канал оператора, не спрашивать, тема закрыта.
|
||||||
|
|
||||||
|
## Г7. Degraded-режим (mappa недоступна)
|
||||||
|
- Читать локальный кэш: `.mappa/snapshot/` + толстый кэш `.mappa/` (canon/methodology/runbooks — task:2068) → manifest → index → файлы.
|
||||||
|
- Мутации → `.mappa/pending/` (реплей после восстановления).
|
||||||
|
- Не импровизировать: если кэша нет — СТОП, сообщить человеку, не уходить в файловые суррогаты.
|
||||||
|
|
||||||
|
## Г8. Перед работой с вики/каноном
|
||||||
|
- Первым действием — прочитать канон-блок AGENTS.md проекта (+ AGENTS-сущность при работе с конкретной вики). Нарушение дважды фиксировалось как баг.
|
||||||
|
|
||||||
|
## Г9. Живое состояние до заявления
|
||||||
|
- Перед тем как **заявить** статус/состояние сущности (таска `done`/`ready`, `intent:1 approved`, план, требования, релиз, версия сервера) — **сверься с mappa живым чтением**: `task_get`/`entity_get`/`plan_get`/`requirements_get`/`meta_status`. Не по памяти, не по ответу `*_create`, не по снимку из прошлого хода, не по кэшу.
|
||||||
|
- **Источник правды о состоянии — только mappa-граф в момент чтения.** Ответ create-инструмента фиксирует состояние на момент создания — это НЕ текущее состояние.
|
||||||
|
- Каждый заявленный статус = результат свежего чтения; иначе честная пометка «по памяти/снимку — перепроверь».
|
||||||
|
- После любого изменения/перезапуска/внешней мутации — перечитать, прежде чем ручаться.
|
||||||
|
- Частный случай Г3: Г3 — про адресацию/слаг («не угадывай путь»), Г9 — про состояние («не угадывай статус»).
|
||||||
|
|
||||||
|
## Связано
|
||||||
|
[[canon/index]] · [[runbooks/index]] · [[runbooks/search]] · brainstorm:173 · wiki:3401 · requirements:23 · [[concepts/dot-mappa-marker]] (wiki:3340) · [[concepts/telemetry]] (wiki:3256) · [[task:2570]] · [[task:2571]] · [[intent:4]]
|
||||||
38
.mappa/share/canon/index.md
Normal file
38
.mappa/share/canon/index.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: mappa канон — карта
|
||||||
|
summary: Канон = обязательные правила работы с mappa как системой (любой проект, любой харнесс): гейты целостности. Первоисточник канон-блока в AGENTS.md. Источник: brainstorm:173 (промоут 2026-09-03).
|
||||||
|
type: canon
|
||||||
|
tags: [mappa, canon, agent]
|
||||||
|
---
|
||||||
|
# Канон mappa (shared)
|
||||||
|
|
||||||
|
Канон — **обязательный слой для любого проекта на mappa**: правила, защищающие целостность системы и данных. Проекция канон-блока доставляется в `AGENTS.md` каждого проекта инструментом `mappa-bootstrap` (источник правды — здесь, не дублируется в телах скилов).
|
||||||
|
|
||||||
|
## Слои знаний (границы)
|
||||||
|
| Слой | Что | Кто читает | Где |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **canon/** (этот слой) | гейты-правила (обязательно всем) | агенты всех проектов | shared |
|
||||||
|
| **methodology/\\<имя\\>/** | стиль зоны (ритуалы/дисциплина), выбор ≤1 | проект, выбравший методологию | shared |
|
||||||
|
| **runbooks/** | процедуры «как сделать X с сущностью» | агенты по необходимости | shared |
|
||||||
|
| **docs/** | ответы человеку «как пользоваться mappa» | агент отвечает человеку | проект mappa |
|
||||||
|
| **concepts/** | «почему так устроено» (решения) | по месту | project/shared |
|
||||||
|
|
||||||
|
## Гейт записи (фаза 1, brainstorm:173 р.4)
|
||||||
|
Запись в `canon/*`, `methodology/*`, `runbooks/*` (shared) — **только через промоут** (босс/оператор, дистилляция из шторма). Прямые правки агентов запрещены. Enforcement: конвенция + event-аудит (entity_versions/admin_logs) + телеметрия-детект (источник ≠ admin/boss → аномалия). Фаза 2 (серверный write-ACL) — отложена. Откат — entity_restore.
|
||||||
|
|
||||||
|
## Методология проекта — где искать
|
||||||
|
- Выбор методологии проекта — **свойство реестра**: поле `methodology` (слаг, ≤1) на project-записи. **Агент узнаёт её из mappa, НЕ из файла** (канон Г1: знание → mappa).
|
||||||
|
- **Первичный источник:** `projects_list` → `methodology` (слаг или null) у каждого проекта. Контент методологии — из shared-вики `methodology/<имя>/index`.
|
||||||
|
- ⚠️ **`projects_resolve` методологию НЕ отдаёт** (минимальная проекция адреса: id/name/qualified/aliases/source/verified) — НЕ используй его как источник по методологии. Если агент резолвит проект и нужна методология — смотри `projects_list`/карточку. (Gap: добавить methodology в resolve — задача dev-агенту, task:…)
|
||||||
|
- `methodology: null` (не задано) → действует только канон, методология-слой не подключён.
|
||||||
|
- Запись `methodology:` в AGENTS.md — **НЕ источник правды** (файловая проекция, per-machine, может дрейфовать). Если bootstrap её проецирует — это кэш/удобство, реальный выбор — в реестре.
|
||||||
|
- Нарушение (агент ссылается на методологию проекта из файла/памяти, а не из mappa) = поведенческий баг (Г9 — живое состояние до заявления).
|
||||||
|
|
||||||
|
## Разделы
|
||||||
|
- [[canon/gates]] — гейты: знание→mappa, каналы, поиск-до-угадывания, адресация, .mappa-гейт, секреты, degraded, живое состояние (Г1–Г9)
|
||||||
|
- [[runbooks/index]] — каталог сущностей: тип → тулы → runbook
|
||||||
|
- [[methodology/kzntsv/index]] — методология kzntsv (пример выбора: ≤1 на проект)
|
||||||
|
- [[concepts/project-methodology-property]] — концепт «методология = свойство проекта» (wiki:3459)
|
||||||
|
|
||||||
|
## Связано
|
||||||
|
brainstorm:173 · wiki:3401 (.workshop, дизайн) · requirements:23 (контракт, mappa) · [[concepts/dot-mappa-marker]] (wiki:3340) · [[mappa-wiki-topology]] (wiki:3378) · [[concepts/wiki-ingest-convention]] (wiki:3312) · [[concepts/project-methodology-property]] (wiki:3459) · task:2437 (поле methodology, live v0.58.2)
|
||||||
26
.mappa/share/index.md
Normal file
26
.mappa/share/index.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# mappa shared-cache
|
||||||
|
|
||||||
|
> Собрано: 2026-09-19T06:44:56.166Z · схема 1 · канал '.mappa/share/' (degraded, task:1641)
|
||||||
|
|
||||||
|
| ref | файл |
|
||||||
|
|---|---|
|
||||||
|
| wiki:3402 | `canon/index.md` |
|
||||||
|
| wiki:3403 | `canon/gates.md` |
|
||||||
|
| wiki:3405 | `runbooks/index.md` |
|
||||||
|
| wiki:3413 | `runbooks/tasks.md` |
|
||||||
|
| wiki:3408 | `runbooks/wiki.md` |
|
||||||
|
| wiki:3407 | `runbooks/inbox.md` |
|
||||||
|
| wiki:3410 | `runbooks/session.md` |
|
||||||
|
| wiki:3414 | `runbooks/search.md` |
|
||||||
|
| wiki:3404 | `methodology/kzntsv/index.md` |
|
||||||
|
|
||||||
|
## expected_files
|
||||||
|
- `canon/gates.md`
|
||||||
|
- `canon/index.md`
|
||||||
|
- `methodology/kzntsv/index.md`
|
||||||
|
- `runbooks/inbox.md`
|
||||||
|
- `runbooks/index.md`
|
||||||
|
- `runbooks/search.md`
|
||||||
|
- `runbooks/session.md`
|
||||||
|
- `runbooks/tasks.md`
|
||||||
|
- `runbooks/wiki.md`
|
||||||
41
.mappa/share/manifest.json
Normal file
41
.mappa/share/manifest.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"snapshot_at": "2026-09-19T06:44:56.166Z",
|
||||||
|
"schema_version": 1,
|
||||||
|
"project": "mappa",
|
||||||
|
"cache_kind": "shared-canon",
|
||||||
|
"ref_to_file": {
|
||||||
|
"wiki:3402": "canon/index.md",
|
||||||
|
"wiki:3403": "canon/gates.md",
|
||||||
|
"wiki:3405": "runbooks/index.md",
|
||||||
|
"wiki:3413": "runbooks/tasks.md",
|
||||||
|
"wiki:3408": "runbooks/wiki.md",
|
||||||
|
"wiki:3407": "runbooks/inbox.md",
|
||||||
|
"wiki:3410": "runbooks/session.md",
|
||||||
|
"wiki:3414": "runbooks/search.md",
|
||||||
|
"wiki:3404": "methodology/kzntsv/index.md"
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"wiki:3402": 2,
|
||||||
|
"wiki:3403": 4,
|
||||||
|
"wiki:3405": 3,
|
||||||
|
"wiki:3413": 1,
|
||||||
|
"wiki:3408": 0,
|
||||||
|
"wiki:3407": 2,
|
||||||
|
"wiki:3410": 1,
|
||||||
|
"wiki:3414": 2,
|
||||||
|
"wiki:3404": 12
|
||||||
|
},
|
||||||
|
"expected_files": [
|
||||||
|
"canon/gates.md",
|
||||||
|
"canon/index.md",
|
||||||
|
"methodology/kzntsv/index.md",
|
||||||
|
"runbooks/inbox.md",
|
||||||
|
"runbooks/index.md",
|
||||||
|
"runbooks/search.md",
|
||||||
|
"runbooks/session.md",
|
||||||
|
"runbooks/tasks.md",
|
||||||
|
"runbooks/wiki.md"
|
||||||
|
],
|
||||||
|
"source": "entity_search(scope=shared) → entity_get(full=true)",
|
||||||
|
"note": "Толстый кэш shared-слоя канона (task:2068/2882). Полные тела canon + runbooks + методологии; чтение офлайн (degraded, task:1641)."
|
||||||
|
}
|
||||||
70
.mappa/share/methodology/kzntsv/index.md
Normal file
70
.mappa/share/methodology/kzntsv/index.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: Методология kzntsv — карта
|
||||||
|
summary: Методология имени kzntsv (стиль зоны, выбор ≤1 на проект — свойство реестра): предпочтительно ПОСЛЕДОВАТЕЛЬНАЯ работа агентов — исполнение (интерактив/поллер) в master, таски цепочкой blocked_by; параллельные ветки/worktree — второстепенный вариант (коллизионные зоны/изоляция интерактива). Источник: brainstorm:182.
|
||||||
|
type: methodology
|
||||||
|
tags: [mappa, methodology, kzntsv, agent]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Методология kzntsv
|
||||||
|
|
||||||
|
Методология — **опциональный поведенческий слой зоны** (не канон): проект выбирает ≤1. **Выбор = свойство проекта** (поле `methodology` в реестре, [[canon/index]] «Методология проекта — где искать»); агент узнаёт её из mappa (`projects_list`), НЕ из файла (канон Г1+Г9). Без выбора (`methodology: null`) действует только канон.
|
||||||
|
|
||||||
|
**Центр:** методология управляет **последовательной работой агентов** — **предпочтительно вся работа в master**, таски идут цепочкой (`blocked_by`: следующая заблокирована предыдущей). **Параллельные ветки/worktree — второстепенный вариант** (только для коллизионных зон / изоляции от интерактива).
|
||||||
|
|
||||||
|
## Зоны ролей (двери boss/ops) — intent:2
|
||||||
|
|
||||||
|
Карта зон: **[[zones/index]]** (shared-вики). Зоны — wiki-холдеры (НЕ entity-тип),
|
||||||
|
сессионное состояние `session.zone` = `boss`|`ops` (отдельное поле от `mode`).
|
||||||
|
Вход/выход — MCP `session.enter_zone` / `session.exit_zone` (тонкая обвязка,
|
||||||
|
конфиг MCP НЕ мутирует). **Доступно всем, всегда** (это дверь — иначе
|
||||||
|
курица-яйцо); гейтится только спец-контент (доки + гранты зоны).
|
||||||
|
|
||||||
|
- **`zone:boss`** — брейншторм/дистилляция/промоут ([[zones/boss/index]]). Проект-зона `.workshop` (реестр role: boss-зона).
|
||||||
|
- **`zone:ops`** — деплой/прод/секреты ([[zones/ops/index]]). Проект-зона `.admin` (реестр role: ops-зона).
|
||||||
|
- Вики зон: project-scope вики `.workshop`/`.admin` (AGENTS-каноны wiki:2656 / wiki:2608); грант-мапы — [[zones/boss/grants]] / [[zones/ops/grants]].
|
||||||
|
- **Грант = ПРАВИЛО, не контракт** (brainstorm:181): инструменты настроены у всех; зона — состояние, агент сверяет сам. **Секреты = реальный забор** (креды/токен, значения мимо mappa, канон Г6).
|
||||||
|
- **Семантика enter/exit:** вход = `session.zone` выставляется в boss|ops (колонка); выход = NULL. Зона — гигиена контекста и разделение ролей, а не технический лок.
|
||||||
|
|
||||||
|
## Исполнение: интерактив или поллер (гейт)
|
||||||
|
На старте работы оценить, **как она исполняется**, и предложить выбор. **Босс оценивает и предлагает; оператор решает, куда идёт работа** («рекомендую, не мени»). От этого выбора — структура исполнения (последовательность в master / ветки).
|
||||||
|
- **ПОЛЛЕР (agensyn, `pool:runner`)** — атомарная, явный done-критерий, независимая или цепочно-зависимая (`blocked_by`), **не коллизионная зона** (§11.4: нет общих файлов/миграций/инфра; «клиенты не отвалятся» — не свойство системы), не требует человеческого суждения.
|
||||||
|
- **ИНТЕРАКТИВ (human-in-loop / босс-сессия)** — коллизионная зона, кросс-каттинг, «клиенты не отвалятся» = свойство системы, требует человеческого суждения/refinement/дизайна.
|
||||||
|
|
||||||
|
## Ветки (второстепенный вариант)
|
||||||
|
**Предпочтительный путь: вся работа в master + таски последовательно (следующая заблокирована предыдущей, `blocked_by`-цепочка).**
|
||||||
|
Ветки (`worker/<slug>` + `git worktree`) — **второстепенный вариант**, только когда нужна изоляция: коллизионная зона (§11.4), интерактив сидит на master, надо изолировать прогоны.
|
||||||
|
- **Кластер = одна интеграционная линия** `worker/<slug>` (не «куча в пул», §11.2) — ПРИ использовании веток.
|
||||||
|
- Зависимые — `blocked_by`-цепочкой; **мерж по одной** — следующая стартует поверх смёрженного (§11.1, §11.3).
|
||||||
|
- Экзекутор **пушит в свою линию** (`origin==HEAD`), не в master — ПРИ ветках.
|
||||||
|
- **Merge в master — босс+человек** отдельным шагом (§11.5; НЕ агentsyn, НЕ экзекутор).
|
||||||
|
- **Disk-проекция:** ветка = рабочее дерево (папка). Параллельные ветки = **отдельные `git worktree`** → отдельные связки/сессии; клейм по `(repo,branch)` уникален (одна ветка = одно извлечение). Команды/детали — [[runbooks/session]].
|
||||||
|
|
||||||
|
## Сессии
|
||||||
|
- Идентичность: связка **{project, runtime, machine, folder}**, ключ — **`sourceRef`**; `project` — единственный кросс-агентный ключ. [[concepts/session-live-ingest]]
|
||||||
|
- **Клейм `(repo, branch)`** — ≤1 живая сессия (409 при занятости); detached HEAD → вне клейма.
|
||||||
|
- Параллельные worktree на **разные** ветки = разные сессии/клеймы.
|
||||||
|
|
||||||
|
## Ритуалы сессии
|
||||||
|
- **Старт (orient):** pull --ff-only → handoff → инбокс (raise+обработка) → liveness одной строкой → welcome по запросу. Не диг.
|
||||||
|
- **Финиш (closing):** handoff-write + PROPOSE wiki-ingest + PROPOSE закрытия тасок; мутации после подтверждения человека.
|
||||||
|
|
||||||
|
## Шторм-методика (дивергенция → конвергенция)
|
||||||
|
- Буфер шторма — mappa brainstorm-сущность (crash-safe).
|
||||||
|
- Дивергенция: вопросы по одному → подходы с трейд-оффами → дизайн; HARD-GATE до импла.
|
||||||
|
- Конвергенция (grilling): номерные вопросы с рекомендованным ответом; фронт пуст = shared understanding.
|
||||||
|
- Зрелость — по критерию, не по ощущению.
|
||||||
|
- Промоут: **intent-слой → `intent`** ([[concepts/entity-intent]], гейт оператора); **НИЖЕ ПО ПОТОКУ НЕЛЬЗЯ без `approved` intent — одобрить ДО создания requirements/plan/task (operator approval, source:admin)**; импл-контент → requirements; этапы → plan; durable → wiki. Refinement issue→task — [[concepts/issue-task-refinement]]. Исполнение через agensyn — [[concepts/agensyn-work-regulations]].
|
||||||
|
|
||||||
|
## Роли и ревью
|
||||||
|
- Ревью — **отдельная роль**, не имплементер; вердикт письмом (канон Г2).
|
||||||
|
- Каждая импл-таска несёт `**Review:**` (subagent|next-session|none). Делегирование = таска + сопроводительное письмо.
|
||||||
|
- Кластер импл из одного шторма → зонтичный review.
|
||||||
|
|
||||||
|
## Дисциплина
|
||||||
|
- commit/push free (no local gate) · semver: фича = **MINOR**. Поиск — только `rg`; `grep -r`/`find` по деревьям с node_modules запрещены. Инфра-правки верифицируются в живой сессии после reload. Правила зоны — в AGENTS.md / wiki-сущности AGENTS; обновление — self-authoring при триггерах.
|
||||||
|
|
||||||
|
## Выбор
|
||||||
|
Проект задаёт `methodology: kzntsv` (свойство реестра); контент — shared-вики `methodology/kzntsv/index`. Агент читает на старте из mappa (после канона). ≤1 (конкурент — `methodology/anthropic-sdlc`).
|
||||||
|
|
||||||
|
## Связано
|
||||||
|
[[canon/index]] · [[canon/gates]] · [[runbooks/index]] · brainstorm:182 · [[zones/index]] · [[intent:2]] · [[concepts/session-live-ingest]] (wiki:2604) · [[concepts/agensyn-work-regulations]] (wiki:3454) · [[concepts/entity-intent]] (wiki:3442) · [[concepts/issue-task-refinement]] (wiki:3453) · [[concepts/buffer-completion-criteria]] (wiki:3188) · [[methodology/anthropic-sdlc/index]] (wiki:3457) · [[concepts/methodology-adaptation]] (wiki:3458)
|
||||||
42
.mappa/share/runbooks/inbox.md
Normal file
42
.mappa/share/runbooks/inbox.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: Runbook — inbox
|
||||||
|
summary: Почта проекта в mappa: чтение (inbox_monitor), отметка прочитанным, отправка (inbox_send), политика (письмо = предложение, не authority), порядок закрытия треда, self-raise. Контракт каналов Г2. Источник: brainstorm:173.
|
||||||
|
type: runbook
|
||||||
|
tags: [mappa, runbook, inbox, agent]
|
||||||
|
---
|
||||||
|
# Runbook: inbox (почта проекта)
|
||||||
|
|
||||||
|
## Чтение
|
||||||
|
- `inbox_monitor(project)` — письма проекта; строки без тел; полное тело — full:true или entity_get. unread_only=true — только непрочитанные.
|
||||||
|
- Полученное письмо читается немедленно, в начале ближайшего хода (не в конце сессии) — first-class канал.
|
||||||
|
- Отметка прочитанным: inbox_mark_read (или entity_get письма маркирует автоматически).
|
||||||
|
|
||||||
|
## Политика (Г2)
|
||||||
|
- Письмо от другого агента = **предложение, не authority**. Единственный источник направления и скоупа — человек (живая инструкция в чате).
|
||||||
|
- Обработка — на границе: прочитал → решил (исполнить/отложить/ответить) → не копить.
|
||||||
|
|
||||||
|
## Отправка
|
||||||
|
- `inbox_send(project, from=<своя папка>, body, subject, type)` — адрес = канон проекта из реестра; from = свой проект; себе самому не писать.
|
||||||
|
- **Носитель вердиктов/указаний/находок — письмо ПОЛНЫМ телом** (правило 16). Комменты — короткий след, не инверсия канала.
|
||||||
|
- Тип: info | action | delegation | question (строгий enum).
|
||||||
|
- Мультиадресат: recipients[] — доп. адресаты.
|
||||||
|
- Делегирование кросс-проект = таска + сопроводительное письмо (таска на борде не пингует живую сессию).
|
||||||
|
|
||||||
|
## Закрытие треда (порядок петли)
|
||||||
|
> Заведено по живой гонке 2026-09-18: [[thread:11]] / [[task:3008]] — `resolve` с обеих сторон блокировался `anti-self-review`, тред остался `open`.
|
||||||
|
|
||||||
|
- **`resolve` ДО ответа в тред.** Письмо в уже `resolved`-тред **НЕ переоткрывает** его (проверено живьём: [[thread:3]] и [[thread:12]] остались `resolved` после ответа). Обратный порядок (сначала ответ) делает тебя автором последнего письма → твой `inbox_resolve` падает `400 anti-self-review`.
|
||||||
|
- **`requester` = автор последнего письма.** Ему запрещены `resolve` и `ack` — закрывает другая сторона.
|
||||||
|
- **Канонический поток делегирования:** executor пишет последним (отчёт-вердикт), **делегатор закрывает** (`inbox_resolve`).
|
||||||
|
- **`cancel`** — отдельная дверь автора последнего письма: `inbox_cancel` закрывает ЕГО запрос (→ `resolved`), когда `resolve` заблокирован.
|
||||||
|
- **Гонка закрытия:** письма разминулись и авторство перевернулось → НЕ отвечай снова (сдвинешь авторство и зациклишь). Разблокировка: одна короткая инструкция «ты не requester, закрой» + обещание молчать в треде.
|
||||||
|
- **`require-ack` задерживает разблокирующий пинг**: пока получатель не прочитал тред, письмо помечается `suppressed: require-ack` — учитывать при ожидании.
|
||||||
|
|
||||||
|
## Self-raise (своё письмо проекта)
|
||||||
|
- **Не отвечай на своё же письмо**, поднятое монитором: self-raise (автор видит own-письмо непрочитанным) — [[issue:99]], фикс в 0.64.8 ([[release:18]]).
|
||||||
|
- Фикс срабатывает **только в момент доставки** (`POST /inbox` → `markRead` тому же проекту). **Исторический self-хвост не чистится автоматически** — у проекта, который много пишет себе (агент-зона `.admin`: отчёты о деплоях уходят и в `mappa`, и в `.admin`), в инбоксе могут висеть сотни «непрочитанных» self-писем (живой замер: `unread_count 468`). **Это старый хвост, а не непрочитанная работа.**
|
||||||
|
- Разовый бэкфилл — [[task:3011]] (`POST /inbox/backfill-self-read {project}`, идемпотентный, per-project).
|
||||||
|
- Правило «свой отправитель»: `from == <имя проекта>` или `agent:<имя проекта>` (`isSelfSenderLetter`, `server/src/inbox-policy.ts`); мультиадресат метится per читателя-проекта (письмо `from: .admin` → для `.admin` self, для `mappa` — чужое).
|
||||||
|
|
||||||
|
## Связано
|
||||||
|
[[runbooks/index]] · [[canon/gates]] (Г2) · [[runbooks/tasks]] · [[runbooks/session]] (raise на старте) · brainstorm:173 · requirements:23 · issue:99 (self-raise) · task:3011 (бэкфилл self-хвоста)
|
||||||
42
.mappa/share/runbooks/index.md
Normal file
42
.mappa/share/runbooks/index.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: Runbooks — каталог сущностей mappa
|
||||||
|
summary: Каталог сущностей mappa (тип → тулы → runbook). Единая карта «что существует и как с этим работать» для агента. 21 сущность покрыта runbook'ами (2026-09-18: +thread). Обновлять при добавлении новых типов. Источник: brainstorm:173.
|
||||||
|
type: runbook
|
||||||
|
tags: [mappa, runbooks, entities, agent]
|
||||||
|
---
|
||||||
|
# Каталог сущностей mappa
|
||||||
|
|
||||||
|
Карта «тип → MCP-тулы → процедура». Runbook — агентский слой «как сделать X» (по запросу). Справочные описания тулов — в MCP-поверхности/docs; здесь маршрутизация.
|
||||||
|
|
||||||
|
## Сущности (есть runbook)
|
||||||
|
| Тип | Что это | Тул-поверхность | Процедура |
|
||||||
|
|---|---|---|---|
|
||||||
|
| task | таска на доске | task_list/get/create/update/close, comment_* | [[runbooks/tasks]] |
|
||||||
|
| wiki | страница вики (project/shared/private) | wiki_get/search/create/update, entity_* | [[runbooks/wiki]] |
|
||||||
|
| inbox | почта проекта — **канал** | inbox_monitor/send/mark_read | [[runbooks/inbox]] |
|
||||||
|
| **thread** | тред-разговор — **сущность** (тема); канал — inbox | `/threads?persona=`, `GET /thread/<ref>?actor=`, inbox_ack/resolve/cancel, catch_stuck_threads | **[[runbooks/threads]]** |
|
||||||
|
| session/handoff | сессии live-ingest, handoff | session_*, handoff_write | [[runbooks/session]] |
|
||||||
|
| поиск/граф | найти/связать | search/entity_search/wiki_search, graph_* | [[runbooks/search]] |
|
||||||
|
| issue | репорт bug/feature/question/incident | issue_create/get/list/update, add_addresses, mark_duplicate | [[runbooks/issue]] |
|
||||||
|
| **intent** | SDKЛ-намерение («что и зачем», без техники) | **intent_create/get/list/update/add_addresses/promote** | **[[runbooks/intent]]** |
|
||||||
|
| requirements | спека-контракт «что должно быть» | requirements_create/get/list/update/add_addresses | [[runbooks/requirements]] |
|
||||||
|
| plan | этапы/фазы планирования | plan_create/get/list/update/add_addresses | [[runbooks/plan]] |
|
||||||
|
| comment | короткий след на таске (Г2: не носитель вердиктов) | comment_create/list | [[runbooks/comment]] |
|
||||||
|
| tag | метки, scope=project | tag_create/list/get/apply | [[runbooks/tag]] |
|
||||||
|
| attachment | вложения (url/blob) к task/wiki/brainstorm/session | attachment_create/get/list/delete | [[runbooks/attachment]] |
|
||||||
|
| release | версии + fixed_in-рёбра (деплой-гейт через .admin) | release_create/list/get/close | [[runbooks/release]] |
|
||||||
|
| brainstorm | шторм-буфер → промоут (wiki+archive) | brainstorm_create/update/promote | [[runbooks/brainstorm]] |
|
||||||
|
| agent/operator | реестры субъектов | agent_*, operator_* | [[runbooks/agent-operator]] |
|
||||||
|
| repo/commit | git-индекс (поиск кода через mappa) | git_index_*, code_* | [[runbooks/repo-commit]] |
|
||||||
|
| project | реестр проектов (канон = папка) | projects_list/register/resolve/create_repo/owners | [[runbooks/project]] |
|
||||||
|
| skill | каталог скилов сервера | skill_list/get/create/update/delete | [[runbooks/skill]] |
|
||||||
|
| entity-слой | общий слой сущностей | entity_get/search/move/versions/diff/restore/transform | [[runbooks/entity]] |
|
||||||
|
| sched/telemetry | расписания, метрики (зона админа) | scheduler_*, telemetry_* | [[runbooks/sched-telemetry]] |
|
||||||
|
|
||||||
|
## Правила каталога
|
||||||
|
- Каталог — единая карта. **При добавлении нового типа сущности** — расширение этого каталога обязательным шагом в постановке серверной таски (конвенция).
|
||||||
|
- Slug'и: `runbooks/<тип>` (kebab, латиница).
|
||||||
|
- Связь: [[canon/index]] (гейты) · [[canon/gates]] · [[docs/index]] (wiki:3341, ответы человеку) · [[mappa-wiki-topology]] (wiki:3378)
|
||||||
|
|
||||||
|
## Связано
|
||||||
|
[[canon/index]] · [[canon/gates]] · [[runbooks/tasks]] · [[runbooks/wiki]] · [[runbooks/inbox]] · [[runbooks/threads]] · [[runbooks/session]] · [[runbooks/search]] · [[runbooks/issue]] · [[runbooks/intent]] · brainstorm:173 · requirements:23
|
||||||
47
.mappa/share/runbooks/search.md
Normal file
47
.mappa/share/runbooks/search.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: Runbook — search/граф и общая память
|
||||||
|
summary: Поиск в mappa: карточный search/entity_search/wiki_search (без тел), правила К1–К4 (поиск до угадывания), скоуп-резолюция project/shared, анти-паттерн wiki_get(slug) на shared-слое, граф (backlinks/neighbors/path) для реляционных вопросов, mode='recall' (общая память: поиск → граф-кластер, depth/related).
|
||||||
|
type: runbook
|
||||||
|
tags: [mappa, runbook, search, agent, memory]
|
||||||
|
---
|
||||||
|
# runbooks/search — как искать в mappa
|
||||||
|
|
||||||
|
Процедура «найти сущность/страницу, не угадывая» + «восстановить контекст/память». Канон-контекст: [[canon/gates]] Г3 (поиск до угадывания). Обновляется при изменениях поиска.
|
||||||
|
|
||||||
|
## 1. Поиск до угадывания (К1–К4)
|
||||||
|
- Не знаешь ref/существование → `entity_search(q)` / `wiki_search(q)` (карточки: ref/type/project/slug/snippet). Никогда не «угадывать» slug/номер.
|
||||||
|
- Скоупы: `project=X` (жёсткий) | `shared` | `all` (boost текущего). Результат карточки несёт project — переключайся по нему.
|
||||||
|
- Префиксы каталога: слаги живут под `canon/`, `methodology/`, `runbooks/`, `concepts/`, `docs/` — не перебирать префиксы, искать.
|
||||||
|
- Тип-фильтры: `types=[task|wiki|inbox|brainstorm|…]` — сузить до нужного.
|
||||||
|
- Карточка найдена → полное тело: `entity_get(ref, full=true)` (не «собрать по кусочкам»).
|
||||||
|
|
||||||
|
## 2. Скоуп-резолюция (project vs shared)
|
||||||
|
- `wiki_get(slug)` без project = shared-скоуп. Для проектной страницы — `wiki_get(project, slug)`.
|
||||||
|
- Одна и та же тема может жить в shared (канон/методология/рунбуки) И в проекте (спека/ретро) — сначала search, потом читать правильный скоуп.
|
||||||
|
|
||||||
|
## 3. Анти-паттерн: wiki_get(slug) для shared-слоя (после task:2073)
|
||||||
|
> **Анти-паттерн:** `wiki_get(slug)` для shared-канона/рунбуков (`canon/*`, `methodology/*`, `runbooks/*`) мис-резолвится → 404 или чужая страница. Если увидел 404/не-тот контент — это shared-слой → переключись на `entity_search(scope='shared')` → `entity_get(ref, full=true)`. Не повторяй `wiki_get(slug)` (21.7% ошибок на /wiki/:slug по телеметрии, дайджест 2069).
|
||||||
|
|
||||||
|
## 4. Граф (когда «кто ссылается / связь»)
|
||||||
|
- «Кто ссылается на X» → `graph_backlinks(ref)`; «куда ведёт X» → `graph_neighbors(ref)`; «связь X–Y» → `graph_path(from, to)`.
|
||||||
|
|
||||||
|
## 5. Общая память — `mode='recall'` (поиск → граф-кластер)
|
||||||
|
|
||||||
|
> **Это и есть «общая память» mappa** (intent:4, task:2570). Агент получает память **запросом**, а не подсовыванием.
|
||||||
|
|
||||||
|
- **Когда:** хочешь не «найти страницу по названию», а **восстановить контекст** вокруг темы: поиск-кластер сущностей и связей между ними. `search(q, mode='recall', scope=…)`.
|
||||||
|
- **Что вернёт:** карточки кластера с полем **`depth`** (0 = сид, 1 = сосед, 2–3 = дальше по рёбрам; relevance затухает `1/(1+depth)`) и **`related[]`** (внутрикластерные связи). Лимит ~50.
|
||||||
|
- **Пайплайн (память как восстановление):**
|
||||||
|
1. `search(q, mode='recall')` → кластер (сиды + depth + related);
|
||||||
|
2. выбрать 1–3 ядра; `entity_get(ref, full=true)` → полное тело;
|
||||||
|
3. `graph_neighbors`/`graph_backlinks` → развернуть вглубь по рёбрам;
|
||||||
|
4. `graph_path(A, B)` → связать два полюса.
|
||||||
|
- **Без `mode='recall'`** — обычный карточный поиск (регресс-контракт, depth нет). Ищешь `mode='recall'` тогда, когда ответ — это «связка сущностей», а не «одна страница».
|
||||||
|
- **Опора качества:** recall опирается на FTS-safe (task:2572) + тег-канал (task:2573) для релевантности — если сырой ввод/спецсимволы ломают поиск, используй чистый запрос.
|
||||||
|
|
||||||
|
## 6. Handoff как снимок памяти (после task:2571)
|
||||||
|
- `handoff_write(project, session_id, status, summary, open_treks?, ask_user?, guards?, recent_commits?)` — писать можно **в любой момент сессии** (reactive-чекпоинт), не только в closing ritual.
|
||||||
|
- Рёбра `handoff → session (in_session) / project (belongs_to) / task` материализуются при write → **handoff не сирота**.
|
||||||
|
- **Восстановление после разрыва:** `search(type='handoff', project=X, limit=1, sort=recency)` → последний → `entity_get(handoff:N, full=true)` → снимок: что сделано / что открыто. Канон-контекст: [[canon/gates]] Г3/Г9.
|
||||||
|
|
||||||
|
**Связи:** [[runbooks/index]] · [[canon/gates]] · [[canon/index]] · [[runbooks/session]] · [[task:2073]] · [[task:2570]] · [[task:2571]] · [[intent:4]] · [[requirements:32]] · [[concepts/memdb-recall-search-patterns]] (wiki:3395)
|
||||||
37
.mappa/share/runbooks/session.md
Normal file
37
.mappa/share/runbooks/session.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Runbook — session
|
||||||
|
summary: Сессии и handoff в mappa: live-ingest (session_upsert/heartbeat), ритуал старта (orient: pull → handoff → inbox → liveness), ритуал финиша (closing: handoff-write), ветки на диск (git worktree). Источник: brainstorm:173.
|
||||||
|
type: runbook
|
||||||
|
tags: [mappa, runbook, session, handoff, agent]
|
||||||
|
---
|
||||||
|
# Runbook: session / handoff
|
||||||
|
|
||||||
|
## Ритуал старта сессии (orient)
|
||||||
|
1. **Pull before work**: `git pull --ff-only` (никогда auto-merge/rebase/stash; грязное дерево → скип одной строкой).
|
||||||
|
2. **Handoff-чтение**: последняя handoff-сущность проекта (`entity_search type=handoff`) → summary/open_treks/ask_user/guards → «предыдущая сессия предложила X — делаем?» → ждать человека (не авто-исполнять).
|
||||||
|
3. **Инбокс**: inbox_monitor → обработать письма (Г2); письма в начале ближайшего хода.
|
||||||
|
4. **Liveness**: «alive/dead» одной строкой; deep-диагностика — вне ритуала (человеку/диагностической сессии).
|
||||||
|
5. Welcome-снапшот — по запросу, НЕ авто на первом ходу.
|
||||||
|
|
||||||
|
## Live-ingest (телесные хуки)
|
||||||
|
- Сессия = сущность (session:N): `session_upsert(sourceRef, project, runtime, machine, folder)` — create/update; `session_heartbeat(id, working_on)` — пульс; action=finalize → clean-завершение.
|
||||||
|
- Claim-гейт (repo,branch): `session_hold(repo, branch)` — свободно → работай; чужой клейм → НИЧЕГО не делать, сообщить человеку «кто держит», ждать (ревок — только оператор).
|
||||||
|
- Stale-active (обрыв) → crash-detect: сказать человеку, предложить (игнор/напоминание письмом/продолжить).
|
||||||
|
|
||||||
|
## Ветки на диск (git worktree)
|
||||||
|
- **Ветка = рабочее дерево (папка).** Одна ветка — одно извлечение; одну ветку нельзя выкатить в двух worktree одновременно (`fatal: already checked out`).
|
||||||
|
- **Параллельные ветки** → отдельные рабочие деревья:
|
||||||
|
```bash
|
||||||
|
git worktree add C:/Users/vitya/projects/.worktrees/mappa-<slug> worker/<slug>
|
||||||
|
git worktree list # карта: folder → branch → commit
|
||||||
|
git worktree remove C:/Users/vitya/projects/.worktrees/mappa-<slug>
|
||||||
|
```
|
||||||
|
- **Что на диске:** у worktree `.git` — это ФАЙЛ (`gitdir: <главный-репо>/.git/worktrees/<имя>`), не папка. Объекты/refs — ОБЩИЕ (главный `.git/objects`, у нас 51M); per-worktree — только HEAD/index/refs/logs в `<главный>/.git/worktrees/<имя>/`; `commondir` → на общий пул.
|
||||||
|
- **Имя папки ≠ маппинг проекта.** Клиент мапит `project=basename(folder)`: для параллельных кластеров либо слать явный `project="mappa"`, либо класть worktree под именем, не ломающим project-резолв. Каждый worktree = отдельная связка сессии (`sourceRef`/folder) → разные ветки = разные клеймы `(repo,branch)` → 409 нет.
|
||||||
|
|
||||||
|
## Ритуал финиша (closing)
|
||||||
|
- `handoff_write(project, session_id, status, summary, open_treks, ask_user, guards, recent_commits)` — по решению человека (не авто-sweep).
|
||||||
|
- PROPOSE wiki-ingest (важное знание → вики) + PROPOSE закрытия тасок. Мутации — после подтверждения.
|
||||||
|
|
||||||
|
## Связано
|
||||||
|
[[runbooks/index]] · [[runbooks/inbox]] · [[runbooks/tasks]] · [[canon/gates]] · контракт wiki:2660 · [[concepts/session-live-ingest]] (wiki:2604) · [[methodology/kzntsv/index]] (wiki:3404) · brainstorm:173
|
||||||
28
.mappa/share/runbooks/tasks.md
Normal file
28
.mappa/share/runbooks/tasks.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Runbook — tasks
|
||||||
|
summary: Как работать с тасками mappa: ориентация (task_list/board_digest), взятие (status+owner, version+409), ведение (comment-след, blocked_by), optimistic-конфликт 409→retry, закрытие с Review-методом. Контракт: wiki:2660.
|
||||||
|
type: runbook
|
||||||
|
tags: [mappa, runbook, task, agent]
|
||||||
|
---
|
||||||
|
# runbooks/tasks — как работать с тасками
|
||||||
|
|
||||||
|
Процедура «взять → вести → закрыть таску». Канон-контекст: [[canon/gates]] Г2 (каналы), Г4 (адресация). Обновляется при изменениях.
|
||||||
|
|
||||||
|
## 1. Ориентация / выбор
|
||||||
|
- Доска: `task_list(project, status=…)` (компакт: ref/status/priority/title) или `board_digest(project)` (счётчики + overdue/blocked).
|
||||||
|
- Взять: `task_get(ref)` → свежая `version` → `task_update(status='active', owner=<identity>, version)` — CAS; конфликт → 409 (см. п.3).
|
||||||
|
- Owner — только реально работающий; не брать чужую active-таску.
|
||||||
|
|
||||||
|
## 2. Ведение
|
||||||
|
- `comment_create` — короткий след «где остановился» (не носитель вердиктов — они письмами, канон Г2).
|
||||||
|
- blocked_by: `edge_create(from, to, blocked_by)` / снять `edge_delete` (парные review-таски).
|
||||||
|
- Пауза/переключение: `task_update(status='paused'|'ready', reason)`.
|
||||||
|
|
||||||
|
## 3. Optimistic-конфликт (version+409 → retry)
|
||||||
|
> **Optimistic-конфликт:** update/close идут с `version` (из `task.get`) → конфликт → **409** → перечитай `task.get` свежим → retry с новой `version`. НЕ работай со stale-version (виден по POST /session 14.7%, POST /requirements/update 23.1% — телеметрия, дайджест 2069).
|
||||||
|
|
||||||
|
## 4. Закрытие
|
||||||
|
- `task_close(ref, version)`: критерии приёмки выполнены И review-метод из `**Review:**` поля исполнен (правило 14: subagent прогнан / next-session review-таска существует / none обоснован).
|
||||||
|
- done → парная review-таска разблокируется (если была).
|
||||||
|
|
||||||
|
**Связи:** [[runbooks/index]] · [[canon/gates]] · [[canon/index]]
|
||||||
28
.mappa/share/runbooks/wiki.md
Normal file
28
.mappa/share/runbooks/wiki.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Runbook — wiki
|
||||||
|
summary: Как работать с вики mappa: первый шаг (AGENTS/canon), чтение, ингест, обновление (version+409), summary-frontmatter, поиск страниц. Источник: brainstorm:173.
|
||||||
|
type: runbook
|
||||||
|
tags: [mappa, runbook, wiki, agent]
|
||||||
|
---
|
||||||
|
# Runbook: wiki
|
||||||
|
|
||||||
|
## Первый шаг любой операции
|
||||||
|
1. Прочитать канон-блок AGENTS.md проекта (+ AGENTS-сущность зоны: `wiki_get(project, 'AGENTS')`), затем каталог/index.
|
||||||
|
2. Скоупы: project (вики проекта), shared (общая тенанта), private (оператора, RLS). `wiki_get` без project = shared → project-страница = 404.
|
||||||
|
|
||||||
|
## Чтение
|
||||||
|
- Страница: `wiki_get(project?, slug)` (компакт-excerpt по умолчанию; полное тело — full:true). Slug искать, не угадывать (Г3, К1–К4).
|
||||||
|
- Поиск: `wiki_search(q, scope, project)` / `entity_search(q, type='wiki')` — карточки {ref, slug, summary, snippet} без тел; полное тело — wiki_get/entity_get.
|
||||||
|
- Реляционные вопросы («что связывает X и Y», backlinks, сироты) — НЕ чтением, а graph_* (BFS сервером): graph_path/neighbors/backlinks/stats.
|
||||||
|
|
||||||
|
## Запись
|
||||||
|
- `wiki_create(project?, slug, body, scope?)` — create (карв-аут). project-скоуп требует project; shared — scope='shared' без project. summary в frontmatter (одна строка) — карточки поиска читают её.
|
||||||
|
- `wiki_update(project, id, body, version)` — update ТОЛЬКО с версией (fresh из wiki_get); конфликт → 409 → re-GET → retry. Без версии — last-write-wins, риск затереть чужое.
|
||||||
|
- Противоречие источников — помечать блоком `> **Contradiction:**`, не затирать молча.
|
||||||
|
- Op-log ведёт сервис (admin_logs) — ручной log-страницы не вести.
|
||||||
|
|
||||||
|
## Ингест
|
||||||
|
Прочитать источник → извлечь сущности/концепты → summaries/<slug> (иммутабельно) → затронутые страницы create/update → отчёт. Один ингест может трогать 10–15 страниц — нормально. Сырой контент в summaries не дампить (только summary + raw_path).
|
||||||
|
|
||||||
|
## Связано
|
||||||
|
[[runbooks/index]] · [[canon/gates]] (Г1/Г3/Г8) · [[runbooks/search]] · контракт wiki:2660 · [[concepts/wiki-ingest-convention]] (wiki:3312) · brainstorm:173
|
||||||
22
.tasks/2026-08-24-01055-mappa-deploy-v080.md
Normal file
22
.tasks/2026-08-24-01055-mappa-deploy-v080.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# mappa-deploy-v080
|
||||||
|
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/workshop / 2026-08-24T18:43:26.245Z -->
|
||||||
|
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Деплой mappa v0.8.0 (session-live-ingest, таска #1022) на прод (стек 26, Portainer).
|
||||||
|
|
||||||
|
Что в релизе: POST /session (upsert по sourceRef, meta {project,runtime,machine,folder}), PATCH /session/:id (finalize/heartbeat), GET /session?project= (latest-first), GET /session/:id, stale-active (query-параметр), MCP session_upsert/session_heartbeat/session_list/session_get. Миграция: колонка ended_at TIMESTAMPTZ (DO-block, идемпотентно). Сессии лизы НЕ берут (решение 19).
|
||||||
|
|
||||||
|
Прод сейчас: v0.7.3 (последний деплой). Коммит: fcb8ca5 (master), тег v0.8.0. Спека: w:2604 concepts/session-live-ingest (mappa wiki).
|
||||||
|
|
||||||
|
**Кто делает:** админ (.admin). Это ops-таска — деплой по правилу #985 (редеплой = админу, один деплойщик на стек).
|
||||||
|
|
||||||
|
## Key files
|
||||||
|
|
||||||
|
## Decisions log
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Completed steps
|
||||||
|
|
||||||
|
## Notes
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# diagnose-books-task-runner-empty-logs-2026-08-25
|
||||||
|
<!-- created-by: OpeItcLoc03@DESKTOP-NSEF0UK / from: OpeItcLoc03/admin / 2026-08-25T11:41:56.972Z -->
|
||||||
|
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
При попытке достать Excel-артефакт задачи fbsPickingReserve (9:00 MSK) с slovo-инстанса (books-task-runner, books VDS 89.253.255.133): docker logs books-task-runner --since 2026-08-25T05:55 --until 06:40 вернул ПУСТО. Задача по конфигу есть (slovo-tasks.json: fbsPickingReserve, cron 0 9 * * *, createReport:true, reportLocal:false), но в логах task-runner за утро — ни строки. Возможные причины: логи идут в другой стрим (stdout/stderr), логгер буферизует, sched не дёрнул runner, другая таймзона логов (docker logs по UTC?), рестарт контейнера сбросил. Надо разобраться: почему за период 05:55-06:40 UTC (9:00 MSK) нет логов — проверить docker logs без фильтров, рестарты (docker inspect StartedAt), правильный контейнер (bookva-task-runner vs books-task-runner vs bookva-sched vs books-sched).
|
||||||
|
|
||||||
|
## Key files
|
||||||
|
|
||||||
|
## Decisions log
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
## Completed steps
|
||||||
|
|
||||||
|
## Notes
|
||||||
@@ -1,32 +1,3 @@
|
|||||||
_Updated: 2026-08-23 — сессия деплоя books agenda-remove (#1007–#1010)._
|
# ⛔ Файловая доска закрыта
|
||||||
|
|
||||||
# NEXT_SESSION
|
**Не читать. Не править.** Канон — mappa (`mcp__mappa__task_*`): task-сущности проекта. Скил: `mappa-task-work`.
|
||||||
|
|
||||||
## Последняя сессия (2026-08-23, .admin)
|
|
||||||
|
|
||||||
**Деплой books agenda-remove ЗАВЕРШЁН**: 6 контейнеров (slovo+bookva api/web/task-runner) на `master-7ffb560`. Образы собраны ЛОКАЛЬНО (docker build + curl-upload блобов), деплой — прямыми Portainer PUT. Отчёт books: mappa inbox id 6185 (19:12Z).
|
|
||||||
|
|
||||||
**Директивы юзера (vitya) — навсегда:**
|
|
||||||
1. **Gitea Actions — НЕ ИСПОЛЬЗОВАТЬ вообще.** Удалены и запушены workflow: books (build/deploy/build-tools/build-sched-daemon) + vds-ops-mcp (там был реальный автобилд `on: push`).
|
|
||||||
2. Раннер на books-vds выпилен полностью: systemd `gitea-runner.service` удалён, `/opt/gitea-runner` стёрт, job-контейнеры `GITEA-ACTIONS-TASK-*` удалены, юзер `gitea-runner` удалён. vds-kzntsv чист. Run 587 заморожен.
|
|
||||||
3. **Сборка образов — только локально на этой машине** (docker build + curl-upload в registry). Деплой — прямыми Portainer PUT (не workflow).
|
|
||||||
|
|
||||||
## Открыто
|
|
||||||
|
|
||||||
- 🟢 **mappa #1020 `mappa-deploy-git-index-token` ЗАКРЫТА** (2026-08-23, эта сессия): деплой mappa **v0.5.1** (не v0.5.0 — findings #1021–#1025 закрыты в 0.5.1, юзер подтвердил) + `MAPPA_GITEA_TOKEN` (read-only токен `mappa-gitindex`, scope read:repository, в pass mappa/full-env). Образ `registry.kzntsv.site/mappa:0.5.1`, стек 26 Portainer (image+env), контейнер healthy. `/gitindex/sync`: 48/61 проектов, 11253 коммита. **Verify на проде невозможен по дизайну** (контейнер без чек-аутов/git, MAPPA_PROJECTS_ROOT пуст) — сходимость подтверждена вручную (rev-list vs индекс: mappa 156=156, snolla 883=883). Прод-импорт из файлов прогнан (таски 865→869, wiki 2595→2599, inbox 2012→2034, счётчики t:1020/s:611/wiki:2601). Письмо mappa (closed) отправлено.
|
|
||||||
- 🟢 books: **yarn.lock — ложная тревога** (их письмо 19:27Z): `yarn install --immutable` зелёный, 3 ссылки `@agendajs/mongo-backend` легитимны (rollback-путь job-scheduler, код импортит MongoBackend). Чистить нечего; удаление agenda = выпил job-scheduler целиком (к человеку). config-диры принято, урок в их wiki.
|
|
||||||
- Функциональный смоук (upload-history, POST /fbs/pickingLists, карточка товара) — за books-агентом.
|
|
||||||
- graphify `ci.yml` (GitHub Actions, не Gitea) — проверить/решить при случае.
|
|
||||||
|
|
||||||
## Wiki-ingest кандидаты (предложить юзеру)
|
|
||||||
|
|
||||||
1. `.wiki/concepts/books-vds-no-gitea-actions.md` — решение юзера: Actions выпилены, сборка локальная; процедура локальной сборки (docker build + curl-upload блобов, OCI-индекс → amd64 child manifest). [уже в index/log]
|
|
||||||
2. `.wiki/concepts/registry-traefik-buffering-gotcha.md` — большие (>1.5GB) chunked-пуши через traefik (buffering middleware 4GB) умирают «Client Closed Request» на books-vds (RAM 3.9GB, OOM); обход: curl монолитными PUT / POST+PATCH+finalize с drop_caches. [уже в index/log]
|
|
||||||
3. Находка: `custom-environment-variables.json` должен жить в примонтированных конфиг-дирах, иначе `config.get('sched')` THROW — env бесполезен без mapping-файла. [уже в index/log]
|
|
||||||
4. (эта сессия) mappa git-index verify на проде невозможен без чек-аутов — verify = инструмент воркстейшна; прод-импорт из файлов: `DATABASE_URL` внешний + `uselibpqcompat=true&sslmode=require` (pg8 трактует require как verify-full — нужен libpq-compat).
|
|
||||||
|
|
||||||
## Не делать
|
|
||||||
|
|
||||||
- Не диспатчить никакие Gitea Actions (раннер мёртв, workflow удалены — если где-то остались — выпиливать).
|
|
||||||
- Не собирать образы на VDS/раннере — только локально.
|
|
||||||
- Не трогать traefik buffering middleware на registry (4GB) без явной отмашки.
|
|
||||||
|
|||||||
2226
.tasks/STATUS.md
2226
.tasks/STATUS.md
File diff suppressed because one or more lines are too long
@@ -1,24 +1,3 @@
|
|||||||
# .wiki/ — Karpathy LLM Wiki
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
Curated knowledge base. Structure:
|
**Не читать. Не править.** Канон — mappa (`mcp__mappa__*`): wiki-сущности проекта, конвенции — AGENTS-сущность. Скил: `mappa-knowledge`.
|
||||||
|
|
||||||
- `entities/<slug>.md` — physical/logical assets (hosts, NAS, VDS, router, recovery-VM)
|
|
||||||
- `concepts/<slug>.md` — patterns, root-cause analyses, recipes, postmortems
|
|
||||||
- `packages/<slug>.md` — third-party / first-party software packages relevant to this stack
|
|
||||||
- `sources/<slug>.md` — chronologies, session traces, primary records
|
|
||||||
- `raw/` — immutable external clippings (transcripts, screenshots, vendor docs)
|
|
||||||
- `index.md` — catalog of all pages
|
|
||||||
- `log.md` — op-log of ingest / promote / lint operations
|
|
||||||
- `overview.md` — high-level project orientation
|
|
||||||
|
|
||||||
## Domain conventions
|
|
||||||
|
|
||||||
### Mandatory: read design context before implementation
|
|
||||||
|
|
||||||
Before picking up any task in `.tasks/`, load the full design context. It does **not** live in this repo as a standalone source — only pointers do. Sources, in order:
|
|
||||||
|
|
||||||
1. **Local design (canonical):** `.wiki/concepts/admin-infra-project.md` — ingested via promote 2026-05-21. Identity, scope, content inventory, migration recipe (subtree-split + read-tree merge for populated prefixes), initial agenda.
|
|
||||||
2. **Brainstorm process trace (rationale):** `~/projects/.workshop/.archive/2026-05-21-admin-infra-project.md`. Why each decision was made (recommend vs menu trade-offs), what was rejected (filter-repo vs subtree-split, "admin-only" skills, splitting roadmap from ops), anti-patterns flagged during brainstorm.
|
|
||||||
3. **Local `overview.md`** — thin summary, quick orientation only — never the source of truth.
|
|
||||||
|
|
||||||
Do **not** invent migration recipes, file lists, taxonomies, or scope decisions from task `where_stopped` lines alone — those are pointers, not specifications. The concept doc and archive contain the rationale.
|
|
||||||
|
|||||||
3
.wiki/concepts/books-vds-memory-optimize-runbook.md
Normal file
3
.wiki/concepts/books-vds-memory-optimize-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3304]] (concepts/books-vds-memory-optimize-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
@@ -20,10 +20,48 @@ tags: [books-vds, ci, gitea-actions, build, decision]
|
|||||||
## Процедура локальной сборки + публикации (проверена 2026-08-23)
|
## Процедура локальной сборки + публикации (проверена 2026-08-23)
|
||||||
1. `git archive <sha> | tar -x -C /c/tmp/build` (чистый контекст, без node_modules/.git)
|
1. `git archive <sha> | tar -x -C /c/tmp/build` (чистый контекст, без node_modules/.git)
|
||||||
2. `VERDACCIO_TOKEN=<jwt> docker build --secret id=verdaccio_token,env=VERDACCIO_TOKEN -f packages/<svc>/Dockerfile -t <svc>:<sha> .`
|
2. `VERDACCIO_TOKEN=<jwt> docker build --secret id=verdaccio_token,env=VERDACCIO_TOKEN -f packages/<svc>/Dockerfile -t <svc>:<sha> .`
|
||||||
3. `docker save` → tar (OCI layout: `index.json` + `blobs/sha256/*`)
|
3. `docker save` → tar. **Docker 29 на этой машине сразу отдаёт OCI-layout** (`oci-layout` + `index.json` + `blobs/sha256/*`) — конвертация не нужна, `--format oci` отсутствует и не требуется. `index.json` → OCI **index** (multi-arch: amd64 child + attestation `platform: unknown/unknown`). Дочерний **amd64-манифест** лежит в `blobs/sha256/<index-digest>` (прочитать index-блоб, взять `manifests[]` с `platform.architecture == 'amd64'`).
|
||||||
4. Заливка блобов curl'ом: HEAD-check → если нет — `POST /v2/<repo>/blobs/uploads/` → `PUT <Location>&digest=sha256:<d>` с телом (монолитный, Content-Length). Большой слой: `cat blob | curl -X PATCH -T -` (chunked) + финальный `PUT &digest` (Location брать из PATCH-ответа). Перед большим чтением файла — `sync; echo 3 > /proc/sys/vm/drop_caches` (books-vds 3.9GB RAM → OOM на 2GB файле).
|
4. Заливка блобов curl'ом: HEAD-check → если нет — `POST /v2/<repo>/blobs/uploads/` → `PUT <Location>&digest=sha256:<d>` с телом (монолитный, Content-Length). Большой слой: `cat blob | curl -X PATCH -T -` (chunked) + финальный `PUT &digest` (Location брать из PATCH-ответа). Перед большим чтением файла — `sync; echo 3 > /proc/sys/vm/drop_caches` (books-vds 3.9GB RAM → OOM на 2GB файле).
|
||||||
5. Манифест: если `index.json` → OCI index (multi-arch, у Docker Desktop так) — **класть дочерний amd64-манифест** (`application/vnd.oci.image.manifest.v1+json`), не индекс (реестр: MANIFEST_INVALID).
|
5. Манифест: если `index.json` → OCI index (multi-arch, у Docker Desktop так) — **класть дочерний amd64-манифест** (`application/vnd.oci.image.manifest.v1+json`), не индекс (реестр: MANIFEST_INVALID).
|
||||||
6. Креды реестра: `docker login registry.kzntsv.site -u books-ci --password-stdin` (актуальный пароль books-ci — в `/opt/books/sched/docker-config.json` на books-vds; в `/root/.docker/config.json` был УСТАРЕВШИЙ → 401 на push).
|
6. Креды реестра: `docker login registry.kzntsv.site -u books-ci --password-stdin` (актуальный пароль books-ci — в `/opt/books/sched/docker-config.json` на books-vds; в `/root/.docker/config.json` был УСТАРЕВШИЙ → 401 на push).
|
||||||
|
|
||||||
## Gotcha: большие пуши через traefik умирают
|
## Gotcha: большие пуши через traefik умирают
|
||||||
`docker buildx --push` 1.5-2GB слоёв через `registry.kzntsv.site` (traefik buffering middleware `maxRequestBodyBytes=4GB`) → `Client Closed Request`: traefik буферизует тело в RAM vds-kzntsv (1GB free) + чтение с buildkit-кэша медленное + OOM. curl монолитными PUT/PATCH работает (54MB/s). Связано: [[registry-traefik-buffering-gotcha]].
|
`docker buildx --push` 1.5-2GB слоёв через `registry.kzntsv.site` (traefik buffering middleware `maxRequestBodyBytes=4GB`) → `Client Closed Request`: traefik буферизует тело в RAM vds-kzntsv (1GB free) + чтение с buildkit-кэша медленное + OOM. curl монолитными PUT/PATCH работает (54MB/s). Связано: [[registry-traefik-buffering-gotcha]].
|
||||||
|
|
||||||
|
## Деплой: Portainer PUT gotchas (2026-08-23, деплой master-a448cad)
|
||||||
|
|
||||||
|
**`PUT /api/stacks/<id>?endpointId=1` — синхронный и тянет образ ВНУТРИ запроса** (676МБ с registry через интернет = минуты). Клиентский таймаут (90с urllib) рвёт ожидание, но операция НЕ отменяется — Portainer доделывает server-side (файл стека обновляется, pull идёт, контейнер пересоздаётся). Правильный флоу: `PUT` с timeout 900+, при обрыве — НЕ пере-PUT (задвоит деплой), а проверить контейнеры:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/endpoints/1/docker/containers/json?all=1 # фильтр по Names: books-web / bookva-web → Image + Status
|
||||||
|
GET /api/endpoints/1/docker/images/json # образ уже на VDS? (показывает in-flight pull)
|
||||||
|
```
|
||||||
|
|
||||||
|
Смоук/верификация: `DELETE /api/fbs/pickingLists/:id` → `{taskId}`, статус `GET /api/fbs/tasks/<uuid>`, run в sched daemon: `docker exec books-sched node -e 'fetch("http://localhost:3031/api/runs?task=<name>&limit=3",{headers:{"Authorization":"Bearer <SCHED_ADMIN_KEY>"}})...'` (**curl в контейнере нет**, node fetch есть; ключ — из env стека). Nuxt-прокси `/admin/scheduler/**` для внешних проверок бесполезен: гейтится `requireAdmin` (JWT isAdmin), `?token=` API-ключ → 403.
|
||||||
|
|
||||||
|
## Sched docker-runner: pull auth + mediaType (2026-08-23, разбор `createPickingListPdf`)
|
||||||
|
|
||||||
|
**docker CLI читает креды ТОЛЬКО из `${DOCKER_CONFIG}/config.json`.** Файл `docker-config.json` (как велели старые доки) → игнорируется → pull без auth → 401 «no basic auth credentials». Фикс: `mv /opt/books/sched/docker-config.json /opt/books/sched/config.json` (bookva volume уже был правильный). Рестарт daemon не нужен — файл читается при каждом pull.
|
||||||
|
|
||||||
|
**Вторая засада — mediaType образа.** Теги, запушенные buildx (и моим curl-флоу из OCI-save) = **OCI image-index**. registry 2.8.3 отдаёт их ТОЛЬКО на Accept с `application/vnd.oci.image.index.v1+json`; docker daemon (26.1.4) получает «manifest unknown», если child-манифест отсутствует (а он отсутствует после прежних host-GC — см. [[registry-oci-image-index-gc]]). Диагностика: `curl` манифеста тега с docker-only Accept → 404, с OCI-index → 200; child по digest → 404. **Фикс: перепуш образа как docker schema2-манифест** (mediaType `application/vnd.docker.distribution.manifest.v2+json`, config `application/vnd.docker.container.image.v1+json`, layers `application/vnd.docker.image.rootfs.diff.tar.gzip`) — блобы те же (content-addressed), только обёртка; pull снова работает. Сборка: `docker save` → OCI → взять amd64 child → трансформ mediaTypes → PUT в тег с docker Content-Type. Важно: docker config-манифест пишется `container.image.v1+json`, НЕ `distribution.image.config.v2+json` (последний docker не знает → «(unknown)» при pull).
|
||||||
|
|
||||||
|
**Третий слой — конфиг daemon.** `createPickingListPdf` рендерит print-роут: env берётся из tasks.json на VDS; плейсхолдер `API_TOKEN: "<SET_IN_PROD_VOLUME_MOUNT>"` = production-конфиг daemon'а не задеплоен в монтированную диру (в репо tasks.json — `${config:api.authToken}`). Конфиг-дир `/opt/books/sched/` должен содержать production-overrides (api.authToken, puppeteer.browserWSEndpoint, s3).
|
||||||
|
|
||||||
|
## 2026-08-24 — deploy sched-daemon 0.11.1 + createPickingListPdf ЗАРАБОТАЛ (admin)
|
||||||
|
|
||||||
|
Деплой демона на 0.11.1 (docker-runner `config.network`) + фикс tasks.json обоих тенантов → смоук оба тенанта ✅ (slovo 429, bookva 219: PDF в S3 + `pdf_remote_path` в БД).
|
||||||
|
|
||||||
|
**Процедура bump books-sched-daemon (без CI, локальная сборка):**
|
||||||
|
1. `deploy/sched/Dockerfile` FROM → `registry.kzntsv.site/sched-daemon:0.11.1`;
|
||||||
|
2. `VERDACCIO_TOKEN=<jwt> docker build --secret id=verdaccio_token,env=VERDACCIO_TOKEN -f deploy/sched/Dockerfile -t registry.kzntsv.site/books-sched-daemon:master-0.11.1 /c/tmp/schedctx` (пустой контекст — Dockerfile без COPY; НЕ `.` — иначе поедет node_modules);
|
||||||
|
3. пуш schema2-флоу (`docker save` → OCI → amd64 child → трансформ mediaTypes → curl блобы + PUT манифеста) в теги `master` + `master-0.11.1` (скрипт-образец `/c/tmp/pushimg/push.py`);
|
||||||
|
4. Portainer PUT обоих sched-стеков (Id 50 books-sched, Id 51 bookva-sched на portainer.kzntsv.site) — DEPLOY_AT bump + pullImage:true;
|
||||||
|
5. verify: `docker exec books-sched node -e 'require("/app/node_modules/@sched/daemon/package.json").version'` → 0.11.1; admin-api `/api/tasks` → `config.network: "proxy"`.
|
||||||
|
|
||||||
|
**Задачи docker-тасков спавнятся на сети `proxy`** (`config.network: "proxy"` в tasks.json; docker-runner 0.11.1 `docker run --network proxy`) → тул видит `books-web:3000`/`chrome:3000`/`books-api:3021`. tasks.json live-reload — директорный бинд `/opt/books/sched/` (slovo) / volume `bookva-sched-config` (bookva), поллинг ~60с, рестарт не нужен.
|
||||||
|
|
||||||
|
**Готча 1: chrome WS-эндпоинт БЕЗ `--proxy-server`.** Эндпоинт `ws://chrome:3000?token=…&stealth&--proxy-server=http://proxy-chain:8000` ЛОМАЕТ рендер внутренней страницы: proxy-chain гонит весь трафик (включая внутренний `books-web:3000`) во внешний пул → navigation timeout → browserless job timeout → SIGKILL → puppeteer «Protocol error (Page.navigate): Target closed». Внутренний рендер print-роута должен идти напрямую: `ws://chrome:3000?token=0ffbbc13-…` (без stealth, без proxy-флага). `--proxy-server` наследовался от мёртвого внешнего chrome.kzntsv.site.
|
||||||
|
|
||||||
|
**Готча 2: bookva-api слушает 3000, НЕ 3021.** (slovo books-api — 3021, bookva-api — 3000; проверка `docker exec bookva-sched node -e 'fetch("http://bookva-api:3000")'` → 401, на 3021 → ECONNREFUSED). В tasks.json bookva `API_URL` был `http://bookva-api:3021` → PATCH `/fbs/pickingLists/:id/pdf` упал бы. Исправлено на `:3000` (live volume + репо-шаблон).
|
||||||
|
|
||||||
|
**Готча 3: `pdf_remote_path` не виден в `GET /api/fbs/pickingLists/:id`** — api-сериализация листа его не отдаёт; проверять в БД напрямую: `docker exec books-db mysql -ubooks -p… books -e "SELECT id_picking_list, pdf_remote_path FROM fbs_picking_lists WHERE id_picking_list=…"` (таблица `fbs_picking_lists`, колонка `pdf_remote_path`).
|
||||||
|
|||||||
17
.wiki/concepts/deploy-policy-mappa.md
Normal file
17
.wiki/concepts/deploy-policy-mappa.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: deploy-policy-mappa
|
||||||
|
type: concept
|
||||||
|
ingested_at: '2026-08-24T19:16:54.665Z'
|
||||||
|
ingested_by: OpeItcLoc03@DESKTOP-NSEF0UK
|
||||||
|
source_project: OpeItcLoc03/workshop
|
||||||
|
---
|
||||||
|
# Деплой-политика mappa
|
||||||
|
|
||||||
|
**Правило (решение оператора 2026-08-24, уточняет #985).**
|
||||||
|
|
||||||
|
1. **Деплой — когда ВСЯ очередь готова + стабильность.** Не по одному скоупу: закрытие одной таски (напр. #1022 → v0.8.0) НЕ триггер деплоя. Один общий деплой, когда закрыты все таски очереди (все накопленные версии: v0.8.0–0.10.0 и т.п.) и сервис стабилен.
|
||||||
|
2. **Деплой — только через .admin** (правило #985, директивы i:2067/2068): прод (стек Portainer, образы в registry, compose-источник правды) трогает только админ. Роль mappa — письмо-запрос + верификация после деплоя. Сама ничего не собирает, в registry не пушит, Portainer не читает.
|
||||||
|
3. **Нарушение повторно = жёсткий откат.** Первое нарушение (2026-08-24, i:2069) — откат + предупреждение; повторное — жёсткая реакция оператора.
|
||||||
|
4. Пока очередь не готова — версии остаются в репо, образы не собираются.
|
||||||
|
|
||||||
|
**Контекст:** правило из инцидента 2026-08-24 (mappa самовольно собрала/запушила образ v0.7.3 и читала Portainer) + уточнение оператора: #1022 закрыта (v0.8.0) → деплой-запрос придержан (#1055 blocked в .admin) до завершения всей очереди (#1054 → #1030 → #1031 → #1032 → #1033).
|
||||||
@@ -1,114 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: emspb.ru snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback)
|
|
||||||
type: concept
|
|
||||||
tags: [emspb, snolla, vds, deploy, docker, portainer, traefik, runbook, migration]
|
|
||||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[labtools-vds-deploy-runbook]], [[snolla-live-prod-inplace-image-bump]]
|
|
||||||
updated: 2026-07-05
|
|
||||||
---
|
|
||||||
|
|
||||||
# emspb.ru → VDS deploy runbook
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:160]] (concepts/emspb-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Вынос `emspb.ru` (snolla-приложение, `@snollajs/snolla` 0.28.4, server-side Liquid) с [[../entities/ruvds-iis-host]]
|
|
||||||
(catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] (89.253.255.94), за traefik.
|
|
||||||
Модель — snolla-app (НЕ pilorama98.ru/Nuxt-contentApi): читает БД-контент MoreThenCms (`mssql.kzntsv.site`)
|
|
||||||
+ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. **Зеркало [[labtools-vds-deploy-runbook]]** — тот же паттерн.
|
|
||||||
|
|
||||||
## Артефакты
|
|
||||||
- **Код:** `victor/emspb.ru` @ `b6e361a` (apps/web, ре-ревью PASS, 29/29 parity, snolla 0.28.4 final). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck `/robots.txt`).
|
|
||||||
- **Образ:** `registry.kzntsv.site/emspb:b6e361a` (+`:latest`). Собран НА VDS (обход traefik-499). digest `sha256:8f5ba02651b71f340fa9bc3b079fd2853b01b1461e6c479fb2cf788691d31a74`.
|
|
||||||
- **Стек Portainer:** `emspb` (Id 18, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/emspb.compose.yml`.
|
|
||||||
- **siteId:** `96EBC481-D26A-47BE-B660-13D49E7D0A61`, activeTheme `DD8D6F7A-CEDC-4D5A-8BFB-E5F9BE9DC035` (theme store MinIO `themes/dd8d6f7acedc4d5a8bfbe5f9be9dc035/`). Non-secret — в `production.json`, НЕ env.
|
|
||||||
|
|
||||||
## Сборка образа (на VDS)
|
|
||||||
```bash
|
|
||||||
# чистый git-archive (только tracked → без config/default.json и node_modules) на VDS
|
|
||||||
git -C ~/projects/emspb.ru archive --format=tar b6e361a \
|
|
||||||
| ssh vitya@89.253.255.94 'rm -rf ~/build/emspb && mkdir -p ~/build/emspb && tar -x -C ~/build/emspb'
|
|
||||||
# build с build-arg VERDACCIO_TOKEN (pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN), передаётся через env (не в argv), push из VDS
|
|
||||||
ssh vitya@89.253.255.94 "cd ~/build/emspb && VERDACCIO_TOKEN='<token>' docker build -f deploy/Dockerfile \
|
|
||||||
--build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/emspb:b6e361a -t registry.kzntsv.site/emspb:latest . \
|
|
||||||
&& docker push registry.kzntsv.site/emspb:b6e361a && docker push registry.kzntsv.site/emspb:latest"
|
|
||||||
```
|
|
||||||
- VERDACCIO_TOKEN — build-time only (discarded build-стейдж; в финальный образ не попадает; docker linter warn-only ×2 — ожидаемо).
|
|
||||||
- `config/default.json` (dev-секреты) исключён `.dockerignore` — в образе только `production.json` + `custom-environment-variables.json` (guard проверен на архиве: default.json ABSENT).
|
|
||||||
|
|
||||||
## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git)
|
|
||||||
Маппинг env→config: `apps/web/config/custom-environment-variables.json`. Non-secret (host/siteId/endpoints) — в `production.json`.
|
|
||||||
**Значения идентичны labtools** (все snolla-тенанты читают одну MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). При создании стека env-массив переиспользован verbatim из labtools stack (Id 17) через Portainer API.
|
|
||||||
|
|
||||||
| ENV | Значение | Источник |
|
|
||||||
|---|---|---|
|
|
||||||
| `DB_USER` | `snolla` | web.config боя MoreThenCmsEntities |
|
|
||||||
| `DB_PASSWORD` | ⟨секрет⟩ | web.config боя MoreThenCmsEntities (User Id=snolla) |
|
|
||||||
| `IMGPROXY_KEY` | ⟨128hex⟩ | `pass minio-vds/full-env` (== сервер imgproxy books-vds — HMAC совпадает) |
|
|
||||||
| `IMGPROXY_SALT` | ⟨128hex⟩ | `pass minio-vds/full-env` |
|
|
||||||
| `S3_ACCESS_KEY_ID` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_USER) |
|
|
||||||
| `S3_SECRET_ACCESS_KEY` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_PASSWORD) |
|
|
||||||
| `SMTP_USER` | `noreply@snolla.com` | `pass snolla-smtp/full-env` |
|
|
||||||
| `SMTP_PASSWORD` | ⟨секрет⟩ | `pass snolla-smtp/full-env` (SMTP_PASS) |
|
|
||||||
|
|
||||||
PORT — не переопределять (образ дефолтит 5000; EXPOSE 5000; healthcheck `/robots.txt` на $PORT). traefik service-port = 5000.
|
|
||||||
|
|
||||||
## Создание стека (Portainer API, на VDS — bash+curl+jq, обходит PS-кириллица-гочу)
|
|
||||||
```bash
|
|
||||||
# JWT (pass vds-kzntsv/full-env PORTAINER_PASS); env переиспользован из labtools stack 17
|
|
||||||
JWT=$(curl -ksS -X POST https://portainer.vds.kzntsv.site/api/auth -d '{"username":"vitya","password":"<pw>"}' | jq -r .jwt)
|
|
||||||
ENV=$(curl -ksS -H "Authorization: Bearer $JWT" https://portainer.vds.kzntsv.site/api/stacks/17 | jq '.Env')
|
|
||||||
COMPOSE=$(cat ~/build/emspb.compose.yml)
|
|
||||||
PAYLOAD=$(jq -n --arg name emspb --arg compose "$COMPOSE" --argjson env "$ENV" '{name:$name,stackFileContent:$compose,env:$env,fromAppTemplate:false}')
|
|
||||||
curl -ksS -X POST "https://portainer.vds.kzntsv.site/api/stacks/create/standalone/string?endpointId=1" \
|
|
||||||
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" --data "$PAYLOAD" # → Id=18
|
|
||||||
```
|
|
||||||
Предусловие pull: `registry.kzntsv.site` зарегистрирован в Portainer как Custom registry Id 1 (иначе `no basic auth`).
|
|
||||||
|
|
||||||
## Runtime egress (наружу из proxy-сети)
|
|
||||||
`mssql.kzntsv.site:1433` (БД-контент, обязателен) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы).
|
|
||||||
|
|
||||||
## Staging smoke (2026-07-02, GREEN) — `emspb.vds.kzntsv.site` vs бой `www.emspb.ru` (RUVDS IIS 80.64.31.36, через `--resolve`)
|
|
||||||
- **Status-паритет 23/23:** 5 nav (`/ /contacts /portfolio /prices`) + 17 service-детальных (`/ustanovka-*`, `/almaznoe-burenie-*` …) все 200/200; `/services` 404/404 (nav-якорь, не страница); `/index.php` 404/404; `/robots.txt` 200 байт-в-байт (31 B).
|
|
||||||
- **Дельта размера контент-страниц стабильна +8 B**, полностью объяснена: +9 host-строка в canonical/og itemprop (`emspb.vds.kzntsv.site` на 9 символов длиннее `www.emspb.ru`), −1 — новый snolla почистил битый двойной слэш боя `/images//3.jpg`→`/images/3.jpg` (оба варианта → 200, картинка не сломана). `/` — байт-в-байт (canonical из DB-домена, не request-host).
|
|
||||||
- **theme CSS ×3 (framework/bootstrap/slick) из MinIO — md5 IDENTICAL** с боем; `Cache-Control: max-age=86400, public, must-revalidate`, `text/css`.
|
|
||||||
- **theme image `/images/3.jpg` из MinIO — md5 IDENTICAL** (115618 B, image/jpeg).
|
|
||||||
- Редиректы: `/contacts/`→301→`/contacts` (trailing-slash), `/Contacts`→301→lowercase, `/Portfolio`→301→lowercase — как бой.
|
|
||||||
- Контейнер healthy (t+10s), MSSQL (tedious) + S3 (aws-sdk) подключены, `listening at http://localhost:5000`, лог чист (только deprecation-warnings tedious/aws-sdk-v2).
|
|
||||||
|
|
||||||
## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02
|
|
||||||
Порядок был:
|
|
||||||
1. **(оператор)** reg.ru: A-записи `emspb.ru` + `www.emspb.ru` → `89.253.255.94` (с RUVDS `80.64.31.36`). — сделал оператор (сигнал «поменял DNS у провайдера»). Проверено внешним резолвером (8.8.8.8): оба хоста → VDS; labtools остался на RUVDS (не путать домены!).
|
|
||||||
2. **(ops)** В стеке `emspb` (Id 18) traefik-rule расширен: `Host(\`emspb.ru\`) || Host(\`www.emspb.ru\`)` — Portainer PUT (env сохранён, pullImage=false). LE HTTP-01 выпустил cert на первом хите после flip (~t+20-70s). **staging-хост `emspb.vds.kzntsv.site` убран из rule post-cutover** (проверено: → 404). Cert перевыпустится на 2 боевых SAN при renewal.
|
|
||||||
3. **Live-smoke GREEN:** `www.emspb.ru`+`emspb.ru` → 200 `ssl_verify=0` (cert доверенный); все страницы 200; canonical нормализован на `www.emspb.ru`; live VDS `/contacts` == старый RUVDS **байт-в-байт** (host уравнялся).
|
|
||||||
|
|
||||||
⚠️ **Порядок критичен:** Host-правило добавлено ТОЛЬКО ПОСЛЕ flip DNS (иначе LE HTTP-01 challenge упал бы на RUVDS → сожгли бы rate-limit). Перед PUT — обязательно проверить внешним резолвером, что DNS реально на VDS.
|
|
||||||
|
|
||||||
### Guard (как labtools)
|
|
||||||
- **НЕ добавлять `Host(emspb.ru)` в traefik-rule ДО флипа DNS** — traefik на reload проактивно тянет LE-cert (HTTP-01), challenge упадёт на RUVDS → сожжём LE rate-limit.
|
|
||||||
- **НЕ выводить RUVDS IIS emspb из эксплуатации** — это rollback-путь.
|
|
||||||
- **НЕ флипать DNS самим** — это делает хозяин домена/оператор.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
- **DNS:** вернуть A-записи `emspb.ru`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS.
|
|
||||||
- **Стек:** Portainer → stack `emspb` (Id 18) → remove (или откат тега образа). Образ в registry остаётся.
|
|
||||||
|
|
||||||
## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05
|
|
||||||
Тираж snolla 0.42.1. emspb.ru обновлён на живом стеке 18 **in-place** (домен уже на VDS, DNS не трогали).
|
|
||||||
Рецепт — [[snolla-live-prod-inplace-image-bump]]. Оператор дал отмашку на боевой apply.
|
|
||||||
- **Образ:** `registry.kzntsv.site/emspb:95a5c42` (snolla 0.28.4→**0.42.1**, digest `d6299cf…`). Собран на VDS.
|
|
||||||
- **Acceptance С VDS (throwaway-staging из env стека 18):** **29 sitemap-роутов все 200** (0 non-2xx/3xx), content-not-lost 29/29 (0 потерь). Sitemap НЕ реструктурировался (29==29, в отличие от каталожного labtools.ru).
|
|
||||||
- **TLS:** серт — SAN покрывает `emspb.ru`/`www.emspb.ru`/`emspb.vds.kzntsv.site`, CN косметически = staging-хост (curl validates `sslverify=0`). Это тот самый серт с cutover 07-02 (см. выше: «Cert перевыпустится на 2 боевых SAN при renewal») — swap его не тронул. При renewal можно пере-выпустить с CN=emspb.ru, опционально.
|
|
||||||
- **Swap:** Portainer PUT стека 18 (env 8/8 сохранён) → healthy → live-smoke GREEN, sitemap 29 вживую.
|
|
||||||
- **Rollback:** тег `emspb:b6e361a` (0.28.4, в registry) / стек 18 PUT назад / DNS→RUVDS.
|
|
||||||
- Compose обновлён на `95a5c42`. Таска `[emspb-deploy-snolla-0-42-1]` 🟢. Notify=victor/emspb.ru.
|
|
||||||
|
|
||||||
## 0.43.2 v3 cutover (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30
|
|
||||||
5-й сайт тиража v3 (после on-snolla, kupimknigi, labtools.ru, labtools.pro — 4 GREEN). In-place bump стека 18 (DNS на VDS с 2026-07-02, не трогали). Рецепт — [[minio-split-cutover-runbook]] §B3-v3.
|
|
||||||
- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` (commit `5afd8a1`) + `resolutions`-блок в root `package.json` (verbatim labtools.ru: @aws-sdk/client-s3 3.1005.0 + @smithy chain 5.3.11) + реген `yarn.lock` (single @smithy/signature-v4@5.3.11 deduped). Pushed victor/emspb.ru master `218c3ae..5afd8a1`. Endpoint fix `218c3ae` (s3.endpoint→minio.vds.kzntsv.site) в master с 2026-07-29, но в бегущем образе `95a5c42` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio.
|
|
||||||
- **Build на VDS:** `git archive 5afd8a1` → `~/build/emspb` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/emspb:5afd8a1`(+latest), digest `sha256:72bb59fc355d4f020fe49e1c410e6be411779ee1002d91cd4db7c3250f2c3358`, EXIT=0. `yarn install --immutable` в Docker прошёл (yarn.lock matches, no YN0028).
|
|
||||||
- **Staging :5082 parity-gate GREEN** (throwaway контейнер из env живого стека 18, 8 секретов, `--network proxy`):
|
|
||||||
- robots.txt md5 `940a0903…` byte-identical (31 B); sitemap index→`sitemap-pages-1.xml` **29 locs == live 29**.
|
|
||||||
- **29/29 sitemap-роутов 200/200, 0 status diffs**, content present (34-38 KB real bodies). Дельта **+2 B** = `localhost:5082` (14) vs `www.emspb.ru` (13) ×2 (canonical+og:url) — benign host-context, post-cutover byte-identical.
|
|
||||||
- **theme assets 7/7 md5 IDENTICAL** (v3 aws-sdk v3 S3-direct read из vds MinIO 2025): snolla-framework `5cb1031e…` 7529B, snolla-bootstrap `9a56500a…` 19624B, slick `e604b364…` 1183B, toolbox `382a2ccc…` 10215B, theme `e02370e3…` 5640B, images/2.jpg `0898d8df…` 387884B, **/images/3.jpg `48562843…` 115618B** (== runbook 2026-07-02 baseline).
|
|
||||||
- redirects parity: `/contacts/`→301→`/contacts`, `/Contacts`→301→lowercase, `/Portfolio`→301→lowercase, `/Services`→301 (target modulo host); `/index.php`+`/services`→404 parity.
|
|
||||||
- **Portainer PUT stack 18 LIVE:** node in-memory `put-stack` (НЕ PS — глобальный fetch, compose в JS-var без /tmp round-trip; GET `/stacks/18` для Env + `/stacks/18/file` для StackFileContent), env 8/8 preserved, `prune:false pullImage:true mem_limit 512m`. PUT 200. Контейнер `5afd8a1` healthy сразу (~1s).
|
|
||||||
- **Live smoke GREEN (via traefik):** `/` 200 54615B, `/contacts`+`/portfolio`+`/prices` 200, `/services`+`/index.php` 404, robots md5 `940a0903…`, sitemap 29 locs, theme assets md5 LIVE==staging (v3 S3-direct), redirects 301 (target `https://www.emspb.ru/...` — byte-identical post-cutover), apex `emspb.ru` 200. Лог чист (tedious deprecation + `emspb.ru (snolla) listening`). TLS не тронут.
|
|
||||||
- **Rollback:** `emspb:95a5c42` (0.42.1 v2, жив в registry) — Portainer PUT назад / compose revert. `b6e361a` (0.28.4) — deeper rollback. DNS→RUVDS 80.64.31.36 — крайний.
|
|
||||||
- Compose (admin source-of-truth) синхронно закоммичен `95a5c42`→`5afd8a1`. **5/6 v3 сайтов GREEN.** Остался: tandemmebel (стек 20 — был BLOCKED на sharp-staging визуальном дефекте, проверить перед бампом).
|
|
||||||
|
|||||||
3
.wiki/concepts/gitea-project-create-runbook.md
Normal file
3
.wiki/concepts/gitea-project-create-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3329]] (concepts/gitea-project-create-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
@@ -1,90 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: labtools.ru snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback)
|
|
||||||
type: concept
|
|
||||||
tags: [labtools, snolla, vds, deploy, docker, portainer, traefik, runbook, migration]
|
|
||||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[snolla-live-prod-inplace-image-bump]]
|
|
||||||
updated: 2026-07-30
|
|
||||||
---
|
|
||||||
|
|
||||||
# labtools.ru → VDS deploy runbook
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:168]] (concepts/labtools-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Вынос `labtools.ru` (snolla-приложение, `@snollajs/snolla`, server-side Liquid) с [[../entities/ruvds-iis-host]]
|
|
||||||
(catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]] (89.253.255.94), за traefik.
|
|
||||||
Модель — snolla-app (НЕ pilorama98.ru/Nuxt-contentApi): читает БД-контент MoreThenCms (`mssql.kzntsv.site`)
|
|
||||||
+ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy.
|
|
||||||
|
|
||||||
## Артефакты
|
|
||||||
- **Код:** `victor/labtools.ru` @ `43e28ba` (apps/web, parity ре-ревью №2 PASS). `deploy/Dockerfile` (multi-stage node:22-slim).
|
|
||||||
- **Образ:** `registry.kzntsv.site/labtools:43e28ba` (+`:latest`). Собран НА VDS (обход traefik-499), 114 MB content.
|
|
||||||
- **Стек Portainer:** `labtools` (Id 17, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/labtools.compose.yml`.
|
|
||||||
|
|
||||||
## Сборка образа (на VDS)
|
|
||||||
```bash
|
|
||||||
# чистый git-archive (только tracked → без config/default.json и node_modules) на VDS
|
|
||||||
git -C ~/projects/labtools.ru archive --format=tar 43e28ba \
|
|
||||||
| ssh vitya@89.253.255.94 'rm -rf ~/build/labtools && mkdir -p ~/build/labtools && tar -x -C ~/build/labtools'
|
|
||||||
# build с build-arg VERDACCIO_TOKEN (pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN), push из VDS
|
|
||||||
ssh vitya@89.253.255.94 'cd ~/build/labtools && docker build -f deploy/Dockerfile \
|
|
||||||
--build-arg VERDACCIO_TOKEN=<token> -t registry.kzntsv.site/labtools:43e28ba -t registry.kzntsv.site/labtools:latest . \
|
|
||||||
&& docker push registry.kzntsv.site/labtools:43e28ba && docker push registry.kzntsv.site/labtools:latest'
|
|
||||||
```
|
|
||||||
- VERDACCIO_TOKEN — build-time only (discarded build-стейдж; в финальный образ не попадает; docker linter warn-only).
|
|
||||||
- `config/default.json` (dev-секреты) исключён `.dockerignore` — в образе только `production.json` + `custom-environment-variables.json`.
|
|
||||||
|
|
||||||
## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git)
|
|
||||||
Маппинг env→config: `apps/web/config/custom-environment-variables.json`. Non-secret (host/siteId/endpoints) — в `production.json`.
|
|
||||||
|
|
||||||
| ENV | Значение | Источник |
|
|
||||||
|---|---|---|
|
|
||||||
| `DB_USER` | `snolla` | web.config боя MoreThenCmsEntities |
|
|
||||||
| `DB_PASSWORD` | ⟨секрет⟩ | web.config боя MoreThenCmsEntities (User Id=snolla) |
|
|
||||||
| `IMGPROXY_KEY` | ⟨128hex⟩ | `pass minio-vds/full-env` (== сервер imgproxy books-vds — HMAC должен совпадать) |
|
|
||||||
| `IMGPROXY_SALT` | ⟨128hex⟩ | `pass minio-vds/full-env` |
|
|
||||||
| `S3_ACCESS_KEY_ID` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_USER) |
|
|
||||||
| `S3_SECRET_ACCESS_KEY` | ⟨секрет⟩ | `pass minio-vds/full-env` (MINIO_ROOT_PASSWORD) |
|
|
||||||
| `SMTP_USER` | `noreply@snolla.com` | `pass snolla-smtp/full-env` |
|
|
||||||
| `SMTP_PASSWORD` | ⟨секрет⟩ | `pass snolla-smtp/full-env` (SMTP_PASS; default.json имел dev-значение — НЕ брать оттуда) |
|
|
||||||
|
|
||||||
PORT — не переопределять (образ дефолтит 5000; EXPOSE 5000; healthcheck `/robots.txt` на $PORT). traefik service-port = 5000.
|
|
||||||
|
|
||||||
## Runtime egress (наружу из proxy-сети)
|
|
||||||
`mssql.kzntsv.site:1433` (БД-контент, обязателен) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы).
|
|
||||||
|
|
||||||
## Staging smoke (2026-07-01, GREEN) — `labtools.vds.kzntsv.site` vs бой `www.labtools.ru`
|
|
||||||
- Все страницы меню (home/about/clients/contacts/cookie/privacy/6×products) — 200/200, дельта размера стабильна (длина хоста в canonical/og).
|
|
||||||
- Редиректы: `.php`→301→`/`, `/about/`→301 (trailing-slash), `/Products/Press-Forms`→301→lowercase — 1:1 с боем.
|
|
||||||
- `theme.css` из MinIO — **байт-в-байт** (12248), `Cache-Control: max-age=86400, public, must-revalidate`.
|
|
||||||
- `/assets/<ownerId>/*.webp` из MinIO — **байт-в-байт** с боем (44444, 24354); content-type `application/octet-stream` — как на бою.
|
|
||||||
- Контейнер healthy (MSSQL+S3 подключены).
|
|
||||||
|
|
||||||
## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02
|
|
||||||
⚠️ **DNS зоны `labtools.ru` — на Yandex DNS (`dns1/dns2.yandex.net`), НЕ reg.ru** (в отличие от `labtools.pro`/reg.ru). Порядок был:
|
|
||||||
1. **(оператор)** Yandex DNS: A-записи `labtools.ru` + `www.labtools.ru` → `89.253.255.94` (с RUVDS `80.64.31.36`).
|
|
||||||
2. **(ops) verify перед traefik — критично:** флип шёл неравномерно, **dns1 и dns2 расходились** (dns2 обновился первым, dns1 отставал). Гейт cutover = **ОБА** авторитетных NS согласованно отдают VDS по apex+www (иначе LE мог попасть на dns1→RUVDS→сжечь rate-limit). Публичные резолверы кэшировали до 6ч (TTL 21600) — для LE не критично (резолвит по авторитетному). Cм. memory [operator-dns-flip-verify-domain-before-cutover].
|
|
||||||
3. **(ops) после подтверждения обоих NS:** стек `labtools` (Id 17) traefik-rule → `Host(\`labtools.ru\`) || Host(\`www.labtools.ru\`)` (Portainer PUT, env 8/8 сохранён, pullImage=false). LE-cert выпущен (~t+20s; первые хиты ловили self-signed в окне issuance, потом ssl_verify=0).
|
|
||||||
4. **Live-smoke GREEN:** оба хоста 200 (nav + каталог-секции + продукты); редиректы (`/about/`→trailing, `/Contacts`→lowercase, `/index.php`→`/`) parity; sitemap = index (host=`labtools.ru`, non-www — siteUrl этого тенанта без www, в отличие от .pro/R3); theme-ассеты из MinIO byte-identical бою (RUVDS отдаёт 301 apex→www, поэтому прямой md5 «DIFF» — артефакт; с `-L` MD5-OK). staging-хост `labtools.vds.kzntsv.site` убран (→404). **Внешняя проверка с RUVDS-хоста:** `labtools.ru`→VDS 200; `www` ещё на кэше RUVDS (200, идентичный контент) — split-brain окно, тухнет по TTL.
|
|
||||||
5. `labtools.pro` — **закрыт отдельно** ([[labtools.pro-vds-deploy-runbook]], LIVE 2026-07-02).
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
- **DNS:** вернуть A-записи `labtools.ru`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS.
|
|
||||||
- **Стек:** Portainer → stack `labtools` → remove (или откат тега образа). Образ в registry остаётся.
|
|
||||||
- RUVDS IIS labtools **не выводить из эксплуатации** до явного решения оператора (директива 2026-07-01).
|
|
||||||
|
|
||||||
## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05
|
|
||||||
Тираж snolla 0.42.1. labtools.ru обновлён на живом стеке 17 **in-place** (домен уже на VDS, DNS не трогали).
|
|
||||||
Рецепт — [[snolla-live-prod-inplace-image-bump]] (общий для labtools.ru/emspb/labtools.pro). Оператор дал отмашку на боевой apply.
|
|
||||||
- **Образ:** `registry.kzntsv.site/labtools:566d41c` (snolla 0.28.2→**0.42.1**, digest `ea0649a…`). Собран на VDS.
|
|
||||||
- **Acceptance С VDS (throwaway-staging из env стека 17):** новый sitemap = **38 page-locs** (7 pages+1 static+6 sections+24 продукта) — 37×200 + 1×301(`/products/laboratory-ball-mills`→`/lshm-750`, идентично проду). content-not-lost 11/11 (0 потерь). **Order-фикс 0.42.1** (liquid 0.10.2): presses = `plg-20,plg-12,plg-25,pgr-10` == прод.
|
|
||||||
- **Находка:** старый 0.28.2 sitemap был дефицитным (sections=1, catalog=2 продукта) — 0.42.1 отдаёт полную корректную структуру. Ещё: sitemap листит 301-секцию как `<loc>` (прод идентичен, engine-intended; heads-up прогу→снолле).
|
|
||||||
- **Swap:** Portainer PUT стека 17 (env 8/8 сохранён, pullImage=true) → healthy → live-smoke GREEN, TLS CN=labtools.ru не тронут (без LE-churn).
|
|
||||||
- **Rollback:** тег `labtools:43e28ba` (0.28.2, в registry) / стек 17 PUT назад / DNS→RUVDS.
|
|
||||||
- Compose обновлён на `566d41c`. Таска `[labtools-ru-deploy-snolla-0-42-1]` 🟢. Notify=victor/labtools.ru.
|
|
||||||
|
|
||||||
## 0.43.2 v3 bump (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30
|
|
||||||
Тираж v3 (MinIO split Track B3-v3, 3-й сайт после on-snolla + kupimknigi). Домен на VDS, DNS не трогали.
|
|
||||||
Рецепт — [[snolla-live-prod-inplace-image-bump]] + [[minio-split-cutover-runbook]] §B3-v3.
|
|
||||||
- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` + `resolutions`-блок в root `package.json` (commit `0dc0b4e`, pushed victor/labtools.ru master). yarn install: single `@smithy/signature-v4@5.3.11` (deduped). Endpoint fix `4f3314f` (s3.endpoint→minio.vds.kzntsv.site) был в master с 2026-07-29, но в бегущем образе `566d41c` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio.
|
|
||||||
- **Build на VDS:** `git archive 0dc0b4e` → `~/build/labtools` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/labtools:0dc0b4e`(+latest), EXIT=0.
|
|
||||||
- **Throwaway staging :5080 parity-gate GREEN** (из env живого стека 17, 8 секретов): `/` byte-identical с real Host (18594B), theme asset `lato.css` 200 9239B byte-identical (v3 aws-sdk v3 S3-direct read из vds MinIO 2025), robots 200/200, sitemap = index 38 page-locs == live (identical set, 0 diff), self-consistency 0 bad (все 200/3xx), catalog section `/products/accessories` byte-identical (order-parity ✅).
|
|
||||||
- **Portainer PUT stack 17 LIVE:** node in-memory `put-stack` (НЕ PS), env 8/8 preserved, `mem_limit 512m` уже в compose, `prune:false pullImage:true`. PUT 200. Контейнер `0dc0b4e` healthy сразу. Source-of-truth compose синхронно закоммичен (admin).
|
|
||||||
- **Live smoke GREEN:** `/` 200, www 200, robots/sitemap 200, menu pages (about/clients/contacts/cookie/privacy) 200, `/products/press-forms` + `/products/accessories` + `/products/laboratory-ball-mills` 200, theme `lato.css` 200 9239B (v3 S3-direct). TLS CN=labtools.ru не тронут (LE до Sep 30). `/products` (bare) + `/products/presses` → 404 = ожидаемо (не в sitemap, не real pages; real section = `/products/press-forms`). **Rollback = `566d41c`** (v2 0.42.1, жив в registry).
|
|
||||||
|
|||||||
@@ -1,107 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: labtools.pro snolla-app — VDS deploy runbook (stack / traefik / env / cutover / rollback)
|
|
||||||
type: concept
|
|
||||||
tags: [labtools-pro, labtools, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, catalog]
|
|
||||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[emspb-vds-deploy-runbook]], [[labtools-vds-deploy-runbook]], [[snolla-live-prod-inplace-image-bump]]
|
|
||||||
updated: 2026-07-30
|
|
||||||
---
|
|
||||||
|
|
||||||
# labtools.pro → VDS deploy runbook
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:169]] (concepts/labtools.pro-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Вынос `labtools.pro` (snolla-приложение, `@snollajs/snolla` **0.28.7**, server-side Liquid, **каталожный сайт**)
|
|
||||||
с [[../entities/ruvds-iis-host]] (catch-all CMS) в отдельный docker-контейнер на [[../entities/vds-kzntsv]]
|
|
||||||
(89.253.255.94), за traefik. Модель — snolla-app: читает БД-контент MoreThenCms (`mssql.kzntsv.site`)
|
|
||||||
+ ассеты из MinIO (`minio.kzntsv.site`) через imgproxy. **Зеркало [[emspb-vds-deploy-runbook]]** — тот же паттерн;
|
|
||||||
отличие: каталог `/products/*` + `Culture=en` (англоязычный близнец labtools.ru) + Cache-Control отсутствует (см. ниже).
|
|
||||||
|
|
||||||
## Артефакты
|
|
||||||
- **Код:** `victor/labtools.pro` @ `7bd9fae` (apps/web, ре-ревью PASS все 9 дименшнов, ноль хаков, snolla 0.28.7 final). `deploy/Dockerfile` (multi-stage node:22-slim, non-root, healthcheck `/robots.txt`).
|
|
||||||
- **Образ:** ⚠️ `registry.kzntsv.site/labtools-pro:7bd9fae` (+`:latest`) — имя **`labtools-pro`**, НЕ `labtools` (тот у labtools.ru!). Собран НА VDS (обход traefik-499). digest `sha256:23d0bc597c8930a1bf443c418a4af68d63b692903963c0e1f9f25baa4a1dd236`.
|
|
||||||
- **Стек Portainer:** `labtools-pro` (**Id 19**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/labtools-pro.compose.yml`.
|
|
||||||
- **siteId:** `663F9410-A6CC-4651-9A5C-62844A313957`, activeTheme `389AD745-E3EE-4F23-BE16-DF38440A0941` (theme store MinIO `themes/389ad745e3ee4f23be16df38440a0941/`). Non-secret — в `production.json`, НЕ env.
|
|
||||||
- **siteUrl:** `https://www.labtools.pro` — в `production.json` (R3: sitemap/robots/canonical отдают www).
|
|
||||||
|
|
||||||
## Сборка образа (на VDS)
|
|
||||||
```bash
|
|
||||||
git -C ~/projects/labtools.pro archive --format=tar 7bd9fae \
|
|
||||||
| ssh vitya@89.253.255.94 'rm -rf ~/build/labtools-pro && mkdir -p ~/build/labtools-pro && tar -x -C ~/build/labtools-pro'
|
|
||||||
ssh vitya@89.253.255.94 "cd ~/build/labtools-pro && VERDACCIO_TOKEN='<token>' docker build -f deploy/Dockerfile \
|
|
||||||
--build-arg VERDACCIO_TOKEN -t registry.kzntsv.site/labtools-pro:7bd9fae -t registry.kzntsv.site/labtools-pro:latest . \
|
|
||||||
&& docker push registry.kzntsv.site/labtools-pro:7bd9fae && docker push registry.kzntsv.site/labtools-pro:latest"
|
|
||||||
```
|
|
||||||
- VERDACCIO_TOKEN — `pass vds-kzntsv/full-env VERDACCIO_CI_TOKEN`, build-time only (не в финальный образ; docker linter warn-only).
|
|
||||||
- `config/default.json` (dev-секреты) исключён `.dockerignore` — guard проверен на архиве: default.json ABSENT (только production.json + custom-environment-variables.json + default.example.json).
|
|
||||||
|
|
||||||
## Runtime env-контракт (8 секретов — Portainer stack-env, НЕ в образ/git)
|
|
||||||
**Значения идентичны labtools/emspb** (все snolla-тенанты = одна MoreThenCms + общий MinIO/imgproxy/smtp; tenant-разница = siteId, не секрет). При создании стека env-массив **переиспользован verbatim из labtools stack (Id 17)** через Portainer API. 8 env: `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`. Маппинг env→config: `apps/web/config/custom-environment-variables.json`.
|
|
||||||
PORT — не переопределять (образ 5000; EXPOSE 5000; healthcheck `/robots.txt`). traefik service-port = 5000.
|
|
||||||
|
|
||||||
## Создание стека (Portainer API, X-API-Key)
|
|
||||||
```bash
|
|
||||||
K=$(pass show vds-kzntsv/full-env | sed -n 's/^PORTAINER_API_KEY=//p'); BASE=https://portainer.vds.kzntsv.site
|
|
||||||
ENV=$(curl -ksS -H "X-API-Key: $K" $BASE/api/stacks/17 | jq '.Env') # verbatim из labtools stack 17
|
|
||||||
COMPOSE=$(cat ~/build/labtools-pro.compose.yml)
|
|
||||||
PAYLOAD=$(jq -n --arg name labtools-pro --arg compose "$COMPOSE" --argjson env "$ENV" '{name:$name,stackFileContent:$compose,env:$env,fromAppTemplate:false}')
|
|
||||||
curl -ksS -X POST "$BASE/api/stacks/create/standalone/string?endpointId=1" -H "X-API-Key: $K" -H 'Content-Type: application/json' --data "$PAYLOAD" # → Id 19
|
|
||||||
```
|
|
||||||
Предусловие pull: `registry.kzntsv.site` зарегистрирован в Portainer как Custom registry Id 1 (иначе `no basic auth`).
|
|
||||||
|
|
||||||
## Runtime egress
|
|
||||||
`mssql.kzntsv.site:1433` (БД-контент) · `minio.kzntsv.site:443` (ассеты) · `imgproxy.kzntsv.site:443` · `smtp.yandex.ru:465` (формы).
|
|
||||||
|
|
||||||
## Staging smoke (2026-07-02, GREEN) — `labtools-pro.vds.kzntsv.site` vs бой `www.labtools.pro` (RUVDS IIS 80.64.31.36)
|
|
||||||
Гнать с самого VDS (воркстейшн ловит LAN-DNS-перехват на `*.labtools.pro`; с VDS бой резолвится публично → RUVDS).
|
|
||||||
- **Status-паритет:** nav (`/ /about /contacts`) + 3 каталог-секции (`/products/laboratory-presses`, `/products/press-forms`, `/products/milling-accessories`) + продукты (`plg-20`, `press-forms/ring`) — все 200/200.
|
|
||||||
- **Редиректы (301 parity, target идентичен modulo host):** каталог#1 `/products/laboratory-ball-mills`→`…/laboratory-ball-mills/lshm-750`; `/Contacts`→lowercase `/contacts`; `/contacts/`→trailing `/contacts`.
|
|
||||||
- **Sitemap — структура index (snolla) vs плоский (legacy), покрытие идентично:** бой отдаёт плоский `/sitemap.xml` с 25 URL; snolla отдаёт **sitemap-INDEX** из 7 дочерних (`sitemap-pages-1` 3, `sitemap-static-pages-1` 2, `sitemap-sections-1` 3, 4× `sitemap-catalog-<GUID>-products-1` = 1+2+2+12=17), все 200, **в сумме ровно 25 URL, набор == бой** (comm — ноль расхождений). `<loc>` host = `www.labtools.pro` (R3). **НЕ дефект** — index-структура штатна для snolla (как у pilorama98.ru).
|
|
||||||
- **theme-ассеты из MinIO md5-IDENTICAL бою** (проверены css/js/png/svg под `/themes/389ad745e3ee4f23be16df38440a0941/`): 7/7 MD5-OK, http 200.
|
|
||||||
- ⚠️ **Cache-Control ОТСУТСТВУЕТ на theme-ассетах — КОРРЕКТНО** (labtools.pro `CachingOptions=[]` empty-config; движок 0.28.7 не ставит заголовок = бой). НЕ флагать как дефект (в отличие от emspb/labtools где был `max-age=86400`).
|
|
||||||
- **Контент-страницы рендерят реальное тело** (12–22 KB), дельта vs бой +155…+222 B — стабильна, объясняется длиной staging-host в canonical/og (на 11 симв. длиннее www; на cutover уравняется в byte-parity).
|
|
||||||
- Контейнер healthy, MSSQL (tedious) + S3 (aws-sdk) подключены, `listening at http://localhost:5000`, лог чист (только deprecation-warnings tedious/aws-sdk-v2).
|
|
||||||
|
|
||||||
### Косметика (не дефект, отдано workshop)
|
|
||||||
Стартовый баннер в логе — `labtools.ru (snolla) listening…` — строка-константа унаследована из labtools.ru-референса при скаффолде. Идентификация сайта = siteId в production.json (663F9410…), на рендер/контент не влияет.
|
|
||||||
|
|
||||||
## Cutover (live DNS) — ✅ ВЫПОЛНЕН 2026-07-02
|
|
||||||
Порядок был (как emspb):
|
|
||||||
1. **(оператор)** reg.ru: A-записи `labtools.pro` + `www.labtools.pro` → `89.253.255.94` (с RUVDS `80.64.31.36`).
|
|
||||||
2. **(ops) verify флипа перед traefik:** предыдущие проверки ловили старый TTL-кэш (публичные резолверы отдавали 80.64.31.36); **авторитетный `ns1.reg.ru` через `Resolve-DnsName` подтвердил** `labtools.pro`+`www` → `89.253.255.94` (TTL 3600), 8.8.8.8 догнал. Именно `.pro` (НЕ `.ru`). Урок: при "поменял DNS" проверять авторитетный NS напрямую, а не только кэширующие резолверы. Cм. memory [operator-dns-flip-verify-domain-before-cutover].
|
|
||||||
3. **(ops) ТОЛЬКО ПОСЛЕ flip:** стек `labtools-pro` (Id 19) traefik-rule → `Host(\`labtools.pro\`) || Host(\`www.labtools.pro\`)` (Portainer PUT, env сохранён 8/8, pullImage=false). LE HTTP-01 выпустил cert **мгновенно** (ssl_verify=0 с первого хита).
|
|
||||||
4. **Live-smoke GREEN:** оба хоста 200 (nav+каталог+продукты); редиректы (ball-mills→lshm-750, /Contacts→lowercase, /contacts/→trailing) parity; sitemap index 7 детей host=www; контент реальный (12728 B, `lang="en"`). staging-хост `labtools-pro.vds.kzntsv.site` убран из rule (→ 404). **Внешняя проверка с RUVDS-хоста (Королёв, public DNS):** `labtools.pro`+`www` → 89.253.255.94 HTTP 200, cert доверенный.
|
|
||||||
5. RUVDS labtools.pro — rollback-путь, жив (отвечает 301, TLS ок), НЕ тронут.
|
|
||||||
|
|
||||||
⚠️ **Порядок критичен:** Host-правило добавлять ТОЛЬКО ПОСЛЕ flip DNS (иначе traefik проактивно тянет LE-cert HTTP-01, challenge упадёт на RUVDS → сожжём rate-limit).
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
- **DNS:** вернуть A-записи `labtools.pro`/`www` → `80.64.31.36` (RUVDS IIS живой, нетронут). Откат = смена DNS.
|
|
||||||
- **Стек:** Portainer → stack `labtools-pro` (Id 19) → remove (или откат тега образа). Образ в registry остаётся.
|
|
||||||
- RUVDS IIS labtools.pro **не выводить** до явного решения оператора (директива 2026-07-01). (Ср.: emspb.ru биндинги на RUVDS сняты 2026-07-02 после подтверждённого cutover — см. [[../entities/ruvds-iis-host]].)
|
|
||||||
|
|
||||||
## 0.42.1 in-place bump — ✅ ВЫПОЛНЕН 2026-07-05
|
|
||||||
Тираж snolla 0.42.1. labtools.pro обновлён на живом стеке 19 **in-place** (домен уже на VDS, DNS не трогали).
|
|
||||||
Рецепт — [[snolla-live-prod-inplace-image-bump]]. Оператор дал отмашку на боевой apply.
|
|
||||||
- **Образ:** `registry.kzntsv.site/labtools-pro:0610432` (snolla 0.28.7→**0.42.1**, digest `3d543fc…`). Собран на VDS.
|
|
||||||
- **Acceptance С VDS (throwaway-staging из env стека 19):** **25 sitemap page-locs все 200** (+robots+sitemap.xml = 27 прогерских), content-not-lost 25/25 (0 потерь).
|
|
||||||
- **Order-парити ВСЕ 3 секции MATCH == прод** (фикс liquid 0.10.2): presses=`plg-20,plg-12`, milling=`milling-jars,grinding-media`, press-forms=**12** изделий (round-xrf…round-collapsible). NB: первичный ручной счёт дал 13 (задвоил тайл), прог-сверка = 12, парити не задет.
|
|
||||||
- **Swap:** Portainer PUT стека 19 (env 8/8 сохранён) → healthy → live-smoke GREEN (nav+presses+press-forms 200, order вживую), TLS CN=labtools.pro не тронут.
|
|
||||||
- **Rollback:** тег `labtools-pro:7bd9fae` (0.28.7, в registry) / стек 19 PUT назад / DNS→RUVDS.
|
|
||||||
- Compose обновлён на `0610432`. Таска `[labtools-pro-deploy-snolla-0-42-1]` 🟢. Notify=victor/labtools.pro.
|
|
||||||
|
|
||||||
## 0.43.2 v3 cutover (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30
|
|
||||||
4-й сайт тиража v3 (после on-snolla, kupimknigi, labtools.ru — все GREEN). MinIO split Track B3-v3.
|
|
||||||
Паттерн отработан x3; labtools.pro = English-twin (`Culture=en`) labtools.ru, отдельный репо `victor/labtools.pro`.
|
|
||||||
Оператор дал GO на боевой apply после staging parity-gate.
|
|
||||||
- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` (commit `12f8992`) + `resolutions`-блок в root `package.json` (verbatim labtools.ru: @aws-sdk/client-s3 3.1005.0 + @smithy chain 5.3.11) + реген `yarn.lock` (commit `a106622`, single @smithy/signature-v4@5.3.11 deduped). Pushed victor/labtools.pro master `6c7ea88..a106622`.
|
|
||||||
- **Endpoint fix `6c7ea88`** (s3.endpoint→minio.vds.kzntsv.site) был в master с 2026-07-29, но в бегущем образе `0610432` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio.
|
|
||||||
- **Build на VDS:** `git archive a106622` → `~/build/labtools-pro` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/labtools-pro:a106622` (+latest), digest `sha256:2152642c4b17…`, 476MB, EXIT=0.
|
|
||||||
- **Staging :5081 parity-gate GREEN** (throwaway контейнер из env живого стека 19, 8 секретов):
|
|
||||||
- `/` nav catalog = **4-байт diff = `og:url`+`og:image` request-host** (`localhost:5081` 13 chars vs `www.labtools.pro` 15 chars × 2 = 4B). **Benign request-context** — post-cutover (оба за traefik Host=www.labtools.pro) byte-identical. (Ср. kupimknigi 3-байт form-url diff.)
|
|
||||||
- Redirects (`/contacts/`→`/contacts`, `/products/laboratory-ball-mills`→`/lshm-750`) **IDENT** (301).
|
|
||||||
- `/robots.txt` `/sitemap.xml` **IDENT** (7 sitemap детей == live, locs identical, host=www).
|
|
||||||
- **theme `lato.css` 200 9994B md5-identical** — v3 aws-sdk v3 S3-direct read из vds MinIO 2025 WORKS (ключевая v3-верификация).
|
|
||||||
- **Order-parity** `/products/press-forms` = 12 изделий identical order (multi-position…vacuum-ring).
|
|
||||||
- Self-consistency: всё 200/3xx, 0 × 4xx/5xx.
|
|
||||||
- **Portainer PUT stack 19 LIVE:** node in-memory `put-stack` (НЕ PS Invoke-RestMethod — корраптит кириллицу compose; скрипт `scripts/tmp/put-stack-labtools-pro.mjs`), env 8/8 preserved, `prune:false pullImage:true mem_limit 512m`. PUT 200. Контейнер `a106622` healthy сразу.
|
|
||||||
- **Live smoke GREEN (via traefik):** `/` 200 12736B, `/about`+`/contacts` 200, `/robots.txt`+`/sitemap.xml` 200 (7 детей все 200), 3 каталог-секции 200, продукты (`press-forms/ring` 17255B, `laboratory-presses/plg-20` 22077B) 200, theme `lato.css` 200 9994B (v3 S3-direct), redirects parity, `labtools.pro` apex 200. Лог чист (tedious deprecation + cosmetic `labtools.ru listening` banner — known benign). TLS CN=labtools.pro не тронут (LE notBefore Jul 2 / notAfter Sep 30 2026).
|
|
||||||
- **Rollback:** `labtools-pro:0610432` (0.42.1, жив в registry) — Portainer PUT назад / compose revert.
|
|
||||||
- Compose (admin source-of-truth) синхронно закоммичен `0610432`→`a106622`. 4/6 v3 сайтов GREEN. Остались: **emspb** (стек 18), **tandemmebel** (стек 20 — был BLOCKED на sharp-staging визуальном дефекте, проверить перед бампом).
|
|
||||||
|
|||||||
@@ -1,62 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: maljarka.tandemmebel.ru snolla-app — VDS deploy runbook (restore 2026-07-31)
|
|
||||||
type: concept
|
|
||||||
tags: [maljarka, tandemmebel, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, landing, ruvds-decomm]
|
|
||||||
related: [[../entities/vds-kzntsv], [on-snolla-vds-deploy-runbook], [tandemmebel-vds-deploy-runbook], [morethencms-null-settingsdata-https-502], [snolla-local-admin-and-on-snolla-migration-design]]
|
|
||||||
updated: 2026-07-31
|
|
||||||
---
|
|
||||||
|
|
||||||
# maljarka.tandemmebel.ru → VDS deploy runbook (restore)
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:170]] (concepts/maljarka-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Восстановление лендинга `maljarka.tandemmebel.ru` («Малярка от Тандеммебель», snolla-app `@snollajs/snolla` **0.43.2** / aws-sdk v3, server-side Liquid) — **7-й сайт тиража snolla на VDS**. Паттерн = [[on-snolla-vds-deploy-runbook]] (прецедент: тоже лендинг с RUVDS catch-all origin, 1 Page `/` + empty content + StaticPages, реконструкция layout.liquid).
|
|
||||||
|
|
||||||
## Почему восстанавливали (root cause)
|
|
||||||
|
|
||||||
maljarka была **.NET MoreThenCms tenant** (тема «Reversal», siteId `A2476738-E26C-49D5-95AD-958F42DCCDD0`), служила на **RUVDS IIS catch-all** (`80.64.31.36`). В v3 тираж (6 Node snolla-app сайтов, 2026-07-30) **не входила** — осталась на RUVDS. **RUVDS декоммишнен 2026-07-21** ([[../entities/ruvds-iis-host]] §DECOMM, погашен у провайдера) → maljarka потеряла единственный публичный хост. DNS перебросили на VDS (CNAME → `tandemmebel.ru` → `89.253.255.94`), но `Host()`-правила и snolla-app с siteId `A2476738` на VDS не было → traefik 404. Аудит 21.07 «maljarka VDS=200» = **false-positive** с локального IIS воркстейшна (local .NET-admin catch-all поднят 20.07, LAN-DNS отдавал его; memory `workstation-lan-dns-serves-local-cms-copy`). Контент в shared `MoreThenCms` DB (`mssql.kzntsv.site`) остался цел — восстанавливаемо.
|
|
||||||
|
|
||||||
## Артефакты
|
|
||||||
- **Код:** `victor/maljarka.tandemmebel.ru` @ `7f03d67` (apps/web, snolla 0.43.2). Клон `victor/on.snolla.com` (lean-база: 4 view-файла, root `package.json` v3 resolutions). `deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root, healthcheck `/robots.txt`).
|
|
||||||
- **Образ:** `registry.kzntsv.site/maljarka:7f03d67` (+`:latest`), digest `sha256:df4e3642…`. Имя **`maljarka`** (= стек/контейнер). Собран НА VDS (обход traefik-499).
|
|
||||||
- **Стек Portainer:** `maljarka` (**Id 23**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/maljarka.compose.yml`.
|
|
||||||
- **siteId:** `A2476738-E26C-49D5-95AD-958F42DCCDD0` (non-secret, в `production.json` + `default.json`).
|
|
||||||
- **siteUrl:** `https://maljarka.tandemmebel.ru`.
|
|
||||||
- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD` — Portainer stack env (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). НЕ в образе, НЕ в git.
|
|
||||||
- **mem_limit:** `512m`.
|
|
||||||
|
|
||||||
## Site model (MoreThenCms DB, siteId A2476738)
|
|
||||||
- 5 Pages (intro/services/portfolio/about/contacts), но **Published=true только `/`** (content пуст — лендинг baked в теме) → sitemap = 1 loc `/`.
|
|
||||||
- 4 StaticPages (all published, sitemap=false): `/index.html` (18401B), `/portfolio-single.html` (1336B), `/portfolio-single-gallery.html` (1567B), `/portfolio-single-video.html` (822B) — отдаются snolla из DB verbatim.
|
|
||||||
- 2 Domains rows (→ robotsTxt-гоча НЕ applies, `app.locals.domain` set; index.js null-guard остался как defensive).
|
|
||||||
- SettingsData `httpSecure` блок на месте (fix 2026-06-08, [[morethencms-null-settingsdata-https-502]]).
|
|
||||||
- Тема «Reversal» `4881FC7F` → MinIO bucket `themes` prefix `4881fc7f52034f8eb371fe5637b1cce1/` (78 файлов, УЖЕ была в MinIO — заливать не понадобилось).
|
|
||||||
|
|
||||||
## Реконструкция шаблонов
|
|
||||||
- `views/layout.liquid` = **byte-identical** рендеру локального .NET-admin (`curl --noproxy '*' --resolve maljarka.tandemmebel.ru:80:127.0.0.1`, 43730B). `{{ item.content }}` вставлен **inline** после последней `</section>` (content пуст → empty output → byte-parity сохранён).
|
|
||||||
- `error_404.liquid` / `error_500.liquid` — Reversal-тема + maljarka-бренд (вместо on.snolla SNOLLA-бренда).
|
|
||||||
- Источник реконструкции = локальный catch-all IIS `snolla` ([[snolla-local-admin-and-on-snolla-migration-design]] §Task A) — тот же MoreThenCms DB + та же тема.
|
|
||||||
|
|
||||||
## Deploy 2026-07-31 (DNS уже на VDS — без DNS-flip-шага)
|
|
||||||
1. **Репо:** `victor/maljarka.tandemmebel.ru` создан на gitea (API, admin-token). Клон on.snolla.com → swap `production.json` (siteId A2476738, siteUrl, appName) + package names → `yarn install` (regen lockfile под переименованный workspace) → push. SHA `ebebe4e` → `7f03d67` (lockfile).
|
|
||||||
2. **Build на VDS:** `git archive 7f03d67` → scp → `~/build/maljarka` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN=<vitya JWT>` → `registry.kzntsv.site/maljarka:7f03d67`(+latest). Guard: `config/default.json` ABSENT в архиве (.dockerignore), snolla 0.43.2, `yarn install --immutable` GREEN (lockfile перегенерён под maljarka workspace). Push OK.
|
|
||||||
3. **Throwaway-staging :5081** из env live tandemmebel-контейнера (8 секретов, `--network proxy`). Healthy. **Completeness-gate С VDS GREEN:** `/` **byte-identical** oracle (sha256 `64011be6…`, 43730B), robots 200, sitemap-pages-1 1 loc `/` (+ пустой blog-index — benign parity, как on-snolla `/c`), 4 staticpages 200 (sizes == DB clen), theme asset `bootstrap.min.css` 200 103320B (MinIO Reversal подтверждена), unpublished /about/services/contacts/intro → 404 (parity). 0 регрессий.
|
|
||||||
4. **Portainer стек 23** создан node-скриптом `create-maljarka-stack.mjs` (JWT auth, **НЕ PS** — кириллица compose; memory `ps51-iso8851-corrupts-portainer-stackfile`), env 8/8, `mem_limit 512m`, `Host(\`maljarka.tandemmebel.ru\`)`. DNS уже `89.253.255.94` → **LE-серт issued on first hit**: CN=maljarka.tandemmebel.ru, issuer YR1, until 2026-10-29.
|
|
||||||
5. **Live smoke GREEN:** `/` 200 43730B byte-identical, `<title>Малярка от Тандеммебель</title>`, 4 staticpages 200 (sizes == DB), theme asset 200, robots/sitemap 200, TLS LE. Log чист.
|
|
||||||
|
|
||||||
## Гочи
|
|
||||||
- **`variant-cache` bucket на vds MinIO 2025 (КРИТИЧНО, shared v3-регрессия)** — v3 minio-split перевёл сайты на `minio.vds.kzntsv.site`, но `variant-cache` bucket не создали там (был на старом `minio.kzntsv.site`/books-vds 2020). snolla sharp-pipeline (`variantCache.js` `DEFAULT_NAMESPACE='variant-cache'`, `storage.js: bucket=storageClient`) → **NoSuchBucket → 500 на ALL sharp gallery-images** (не только maljarka — на всех 6 v3 сайтах с 2026-07-30). Заметно на maljarka-портфолио (35 фото). v3-gate не поймал т.к. проверял theme CSS (`themes` bucket, themeFiles — без sharp), не gallery-image URLs. **Fix 2026-07-31:** `mc mb vds/variant-cache` (пустой, кэш регенерируем — миграция старых entries НЕ нужна). После: portfolio 34/34 → 200 image/webp. См. [[minio-split-cutover-runbook]] Track A + memory `variant-cache-bucket-missing-on-vds-2025`.
|
|
||||||
- **yarn.lock workspace-rename** — переименование root package (`on.snolla.com`→`maljarka.tandemmebel.ru`) и `apps/web` name (`@on-snolla/web`→`@maljarka/web`) требует regen yarn.lock, иначе `yarn install --immutable` падает (lockfile ссылается старые workspace-дескрипторы). Fix: `yarn install` локально под vitya JWT → commit обновлённого lockfile.
|
|
||||||
- **workstation-curl отравлен локальным HTTP-прокси** — `--resolve` игнорируется прокси (CONNECT), обе пробы уходят на VDS. External verify prod-maljarka с воркстейшна = только через VDS-ssh curl (или `curl --resolve :443:89.253.255.94 --noproxy`, но прокси всё равно может перехватить). Локальный IIS-рендер для реконструкции — через `--noproxy '*' --resolve :80:127.0.0.1`.
|
|
||||||
- **hosts override на воркстейшне** — Task A оставил hosts-override для .snolla.com (НЕ maljarka), но local IIS `*:80` catch-all + LAN-DNS могут перехватить. Оракул-рендер снимался явно через 127.0.0.1.
|
|
||||||
- **robotsTxt-гоча НЕ applies** — у maljarka 2 Domains rows (`app.locals.domain` set). index.js null-guard (из on.snolla) остался как defensive, не вредит.
|
|
||||||
- **RUVDS DECOMM** — rollback только образ-тегом в registry, НЕ DNS (RUVDS погашен 2026-07-21).
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
- **Образный:** PUT стека 23 назад на предыдущий тег в registry. С первого deploy ведём rollback-теги (пока только `7f03d67` = `:latest`).
|
|
||||||
- DNS-rollback НЕ доступен (RUVDS DECOMM).
|
|
||||||
- Если layout сломан: revert Host-правила (вернёт 404) ИЛИ поправить `layout.liquid` + rebuild.
|
|
||||||
|
|
||||||
## Связанное
|
|
||||||
- Тираж snolla на VDS теперь **7/7**: labtools.ru(17) / emspb(18) / labtools.pro(19) / tandemmebel(20) / kupimknigi(21) / on-snolla(22) / **maljarka(23)**.
|
|
||||||
- Прецедент-паттерн: [[on-snolla-vds-deploy-runbook]] (лендинг, реконструкция из .NET-admin рендера).
|
|
||||||
- 502-на-HTTPS fix (RUVDS-эпоха, 2026-06-08): [[morethencms-null-settingsdata-https-502]].
|
|
||||||
- Локальный .NET-admin (источник реконструкции): [[snolla-local-admin-and-on-snolla-migration-design]] §Task A.
|
|
||||||
|
|||||||
@@ -1,98 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: mappa — VDS deploy runbook (БД / образ / Portainer-стек 26 / редеплой / verify)
|
|
||||||
type: concept
|
|
||||||
tags: [mappa, vds, deploy, docker, portainer, traefik, postgres, runbook, meta]
|
|
||||||
related: [[../entities/vds-kzntsv]], [[portainer-stack-management-vds]], [[mappa-prod-infra]]
|
|
||||||
updated: 2026-08-23
|
|
||||||
---
|
|
||||||
|
|
||||||
# mappa → VDS deploy runbook
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:1254]] (concepts/mappa-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Развёртывание **mappa** (единый meta-сервис цеха: HTTP-ядро + MCP-адаптер, Postgres)
|
|
||||||
на [[../entities/vds-kzntsv]] (89.253.255.94) рядом с общей БД (решения 1/17/18 спеки
|
|
||||||
`concepts/mappa`). Первый деплой — 2026-08-23 (#1012, закрыта).
|
|
||||||
|
|
||||||
**Разделение труда (директива 2026-08-23):** деплой/редеплой = АДМИН (таска + письмо);
|
|
||||||
прод-импорт, verify счётчиков, read-only+фолбэк, ntfy-алерт, артефакты, MCP-регистрация =
|
|
||||||
mappa (#985). Не делать чужую часть.
|
|
||||||
|
|
||||||
## Артефакты
|
|
||||||
|
|
||||||
- **Код:** `OpeItcLoc03/mappa` @ master. Сборка TS: `npm ci && npm run build` → `dist/`.
|
|
||||||
- **Образ:** `registry.kzntsv.site/mappa:0.1.0`. Dockerfile в корне репо (multi-stage node:22-alpine;
|
|
||||||
**обязательный COPY `src/lib/schema.sql` → `dist/src/lib/schema.sql`** — tsc не копирует .sql,
|
|
||||||
без него migrate() падает «schema.sql not found»).
|
|
||||||
- **Стек Portainer:** `mappa` (Id 26, endpoint 1). Source-of-truth compose:
|
|
||||||
`admin/host-stacks/vds-kzntsv/mappa.compose.yml`.
|
|
||||||
- **БД:** база `mappa` + role `mappa` на общем `postgres:16` VDS (owner = mappa).
|
|
||||||
Креды: **`pass show mappa/full-env`** (`MAPPA_DB_PASS`, `DATABASE_URL` docker-сетевой:
|
|
||||||
`postgres://mappa:…@postgres:5432/mappa`; внешний host `postgres.vds.kzntsv.site:5432` — TLS-verify-skip).
|
|
||||||
- **Endpoint:** `https://mappa.vds.kzntsv.site` (traefik websecure, letsEncrypt, service-port 8731).
|
|
||||||
|
|
||||||
## Реденплой (обновление образа) — быстрый путь
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Собрать и запушить новый тег образа (с воркстейшена, Docker Desktop)
|
|
||||||
cd ~/projects/mappa
|
|
||||||
docker build -t registry.kzntsv.site/mappa:<tag> .
|
|
||||||
docker push registry.kzntsv.site/mappa:<tag>
|
|
||||||
|
|
||||||
# 2. НА VDS: docker pull ДО restart стека (иначе застрянет на старом digest!)
|
|
||||||
ssh vds.kzntsv.site 'docker pull registry.kzntsv.site/mappa:<tag>'
|
|
||||||
|
|
||||||
# 3. Поменять image: в host-stacks/vds-kzntsv/mappa.compose.yml И в Portainer stack 26
|
|
||||||
# (обе копии синхронизировать — иначе следующий redeploy из гита откатит тег)
|
|
||||||
|
|
||||||
# 4. Restart стека через Portainer API (НЕ ad-hoc docker compose up!)
|
|
||||||
# JWT: pass vds-kzntsv/full-env PORTAINER_ADMIN_PASS → POST /api/auth
|
|
||||||
curl -ksS -X POST "https://portainer.vds.kzntsv.site/api/stacks/26/restart?endpointId=1" \
|
|
||||||
-H "Authorization: Bearer $JWT"
|
|
||||||
|
|
||||||
# 5. Verify
|
|
||||||
curl -ksS https://mappa.vds.kzntsv.site/health # {"ok":true,"service":"mappa"}
|
|
||||||
curl -ksS https://mappa.vds.kzntsv.site/admin/status # entities/by_type/projects
|
|
||||||
curl -ksS https://mappa.vds.kzntsv.site/graph/stats # nodes/edges/components
|
|
||||||
ssh vds.kzntsv.site 'docker ps --filter name=mappa --format "{{.Status}}"'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Обновление env (пароль БД и т.п.)
|
|
||||||
|
|
||||||
Через Portainer → Stack 26 → Edit (env array), НЕ файлом на VDS (Portainer string-mode не
|
|
||||||
материализует .env; env передаётся API-массивом — gotcha 1 `portainer-stack-management-vds`).
|
|
||||||
`MAPPA_DB_PASS` — единственный env в стеке.
|
|
||||||
|
|
||||||
## Первичный деплой с нуля (cold start)
|
|
||||||
|
|
||||||
1. **БД**: на VDS `docker exec postgres psql -U postgres`:
|
|
||||||
`CREATE ROLE mappa LOGIN PASSWORD '<pw>'; CREATE DATABASE mappa OWNER mappa;`
|
|
||||||
Пароль → `pass insert -m mappa/full-env`.
|
|
||||||
2. **Образ**: собрать (см. выше), `docker push`.
|
|
||||||
3. **Compose**: `admin/host-stacks/vds-kzntsv/mappa.compose.yml` → Portainer create
|
|
||||||
(standalone string-mode, endpointId=1, env `[{"name":"MAPPA_DB_PASS","value":…}]`).
|
|
||||||
4. **Проверка миграций**: при старте migrate() сам создаёт таблицы (6 шт: projects/counters/
|
|
||||||
entities/edges/leases/logs, owner=mappa) — `docker logs mappa` + `\dt` в БД.
|
|
||||||
5. **Пинг mappa**: письмо в `mappa/.agents/inbox/` (event: mappa-deployed) — импорт и остальное за ними.
|
|
||||||
|
|
||||||
## Мониторинг живости (монитор → ntfy)
|
|
||||||
|
|
||||||
Остаток #985 (решение 15/18: алерт при падении сервиса) — реализован #1013 (2026-08-23).
|
|
||||||
|
|
||||||
- **Скрипт:** `/root/mappa-ntfy-monitor/monitor.py` (python3 stdlib; источник: `admin/scripts/mappa-ntfy-monitor/monitor.py`).
|
|
||||||
- **Проверка:** GET `https://mappa.vds.kzntsv.site/health` — HTTP 200 + JSON `{"ok":true,"service":"mappa"}` (таймаут 15s).
|
|
||||||
- **Алерт:** ntfy топик `mappa-alerts` (basic-auth vitya, admin-role → rw все топики), после 3 подряд фейлов (~15 мин), раз за эпизод (dedupe); recovery-сообщение при возврате.
|
|
||||||
- **Крон:** `/etc/cron.d/mappa-ntfy-monitor` — `*/5 * * * *` (root, лог `/var/log/mappa-ntfy-monitor.log`).
|
|
||||||
- **Конфиг:** `/root/.mappa-ntfy-monitor.env` (600; NTFY_* из `pass vds-kzntsv/full-env`). State: `/root/.mappa-ntfy-monitor.state`.
|
|
||||||
- ⚠️ Монитор НЕ трогает стек 26 — только наблюдение; редеплой/рестарт — отдельной задачей.
|
|
||||||
|
|
||||||
## Gotchas (из первых деплоев)
|
|
||||||
|
|
||||||
1. **schema.sql в dist** — tsc компилирует только .ts; `migrate()` ищет `.sql` рядом с
|
|
||||||
`dist/src/lib/`. Без COPY в Dockerfile → контейнер в restart-loop («schema.sql not found»).
|
|
||||||
2. **docker pull до restart** — Portainer start/stop НЕ тянет новый digest сам по себе;
|
|
||||||
перезалитый `:0.1.0` без pull на VDS = старый образ (ловил 2× за сессию 2026-08-23).
|
|
||||||
3. **Не ad-hoc compose** — все стеки VDS через Portainer (VDS ops rule). `docker rm -f`
|
|
||||||
вручную снёс контейнер — Portainer recreate поднял, но это не путь.
|
|
||||||
4. **mem_limit 512m** — канон app-стеков, не убирать (cgroup-лимит vs вся память хоста).
|
|
||||||
5. **Внешний DATABASE_URL** — контейнерный `@postgres:5432` валиден только внутри docker-сети;
|
|
||||||
для импорта с воркстейшена — `postgres.vds.kzntsv.site:5432` (+ TLS-verify-skip, self-signed).
|
|
||||||
6. **Закрытие деплоя** — всегда пара «таска + письмо mappa» (канон `mappa-deploy-workflow`).
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ related: [[vds-kzntsv]], [[recovery-architecture-snapshot]], [[mssql-on-vds]], [
|
|||||||
| Endpoint (внешний, TLS) | `https://minio.kzntsv.site` (traefik → `minio:9000`, LE cert, port 443) |
|
| Endpoint (внешний, TLS) | `https://minio.kzntsv.site` (traefik → `minio:9000`, LE cert, port 443) |
|
||||||
| Endpoint (сырой host-порт) | `http://89.253.255.133:9000` (тот же бэкенд, ssl:false) |
|
| Endpoint (сырой host-порт) | `http://89.253.255.133:9000` (тот же бэкенд, ssl:false) |
|
||||||
| Endpoint (inter-container на books-vds) | `http://minio:9000` (сеть `proxy`) — так ходит imgproxy |
|
| Endpoint (inter-container на books-vds) | `http://minio:9000` (сеть `proxy`) — так ходит imgproxy |
|
||||||
| accessKeyId | `AKIAJ2YJP72W6ZHCRE6Q` (= `MINIO_ACCESS_KEY`, root, read+write all) |
|
| accessKeyId | `secret:pass/minio-vds/full-env` (= `MINIO_ACCESS_KEY`, root, read+write all) |
|
||||||
| secretAccessKey | в `pass minio-vds/full-env` (`MINIO_ROOT_PASSWORD`) |
|
| secretAccessKey | в `pass minio-vds/full-env` (`MINIO_ROOT_PASSWORD`) |
|
||||||
| region / pathStyle / ssl | `local` / `true` / **`true` для https-хостнейма** (false для сырого `:9000`) |
|
| region / pathStyle / ssl | `local` / `true` / **`true` для https-хостнейма** (false для сырого `:9000`) |
|
||||||
|
|
||||||
|
|||||||
@@ -1,280 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: MinIO split — cutover runbook (Track A books-vds 2020→2025 + Track B CMS-media → vds)
|
|
||||||
type: concept
|
|
||||||
tags: [minio, vds, books-vds, cutover, runbook, migration, snolla, s3, imgproxy, obsidian]
|
|
||||||
related: [[../entities/vds-kzntsv], [../entities/books-vds], [on-snolla-vds-deploy-runbook], [tandemmebel-vds-deploy-runbook], [snolla-live-prod-inplace-image-bump], [minio-imgproxy-on-vds], [portainer-stack-management-vds]]
|
|
||||||
updated: 2026-07-30
|
|
||||||
---
|
|
||||||
|
|
||||||
# MinIO split — cutover runbook
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:173]] (concepts/minio-split-cutover-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
> **Режим:** подготовка. **На железе — НИЧЕГО без команды user.** Все команды готовы к выполнению по «го».
|
|
||||||
> Owner: admin. Создано 2026-07-29. Перенесён из `scripts/minio-split-cutover/` 2026-07-30 (дисциплина: runbook'и → `.wiki/concepts/`).
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Разнести MinIO-контент на 2 сервера:
|
|
||||||
- **books-vds shared `minio`** (`minio.kzntsv.site`) → только slovo-бакеты books-приложений. Апгрейд 2020→2025.
|
|
||||||
- **books-vds `bookva-minio`** → bookva-бакеты (уже там с 2026-05). Апгрейд 2020→2025. Консоль наружу НЕ выставляем.
|
|
||||||
- **vds `minio`** (`minio.vds.kzntsv.site`, уже 2025-09-07) → CMS-медиа snolla-сайтов + artmone + obsidian + modules + modulair + maxmind + strapi.
|
|
||||||
- Креды **не трогаем** (один root-ключ на все инстансы; **доступ → `pass show minio-vds/full-env`**, поля `MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD`). Креды НЕ в git.
|
|
||||||
|
|
||||||
## Два независимых трека
|
|
||||||
|
|
||||||
- **Track A — апгрейд books-vds minio (shared + bookva).** Не требует правок apps (transparent: hostname/creds/контент сохраняются). Можно делать отдельно, до/после Track B.
|
|
||||||
- **Track B — сплит CMS-медиа на vds.** Координированный cutover: миррор → DNS-flip imgproxy → snolla deploy. Требует гейтов.
|
|
||||||
|
|
||||||
## Progress (2026-07-30)
|
|
||||||
|
|
||||||
| Шаг | Статус | Примечание |
|
|
||||||
|---|---|---|
|
|
||||||
| B1 миррор CMS-медиа books-vds → vds minio | ✅ DONE | galleries/pilorama98/themes/assets смиррорены (свежим миррором поверх stale 2026-05) |
|
|
||||||
| B2 DNS-flip `imgproxy.kzntsv.site` → 89.253.255.94 | ✅ LIVE | imgproxy.kzntsv.site/<pilorama98-image> → 200 webp (vds imgproxy ← vds minio); CMS-сайты берут картинки через vds |
|
|
||||||
| **B3 on-snolla v2-пилот (S3-direct-path)** | ✅ LIVE 2026-07-30 | in-place bump стека 22 `473923e494db→07db7e3` (snolla 0.42.1 v2, config endpoint → minio.vds.kzntsv.site, без бампа package.json). Theme-assets `/themes/c406…/…` → 200 97324 B == live (aws-sdk v2 → vds minio 2025 — compat подтверждена, главный риск закрыт). Sitemap 3/3 parity, `/c` 404 parity. Rollback-тег `473923e494db` в registry. **Промежуточный** шаг — v2 совместим, но v3 — конечная цель (user 2026-07-30). |
|
|
||||||
| **B3v3 on-snolla v3-пилот** | ✅ LIVE 2026-07-30 | `@snollajs/snolla 0.43.2` published (verdaccio latest). Live MinIO verify ЗЕЛЁНЫЙ (snolla, оба конфига). **v3 cutover 2026-07-30T11:28Z**: bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (root package.json), commit `eb8c143` pushed, build на VDS → `registry.kzntsv.site/on-snolla:eb8c143`, Portainer PUT stack 22 env-preserving (8/8), prune:false pullImage:true, mem_limit 512m. Throwaway staging :5078 parity-gate GREEN (theme asset 200 97324 B byte-identical, sitemapindex+3 sub-sitemaps byte-identical, /c 404 parity). Live smoke GREEN: / 200, robots/sitemap/sub-sitemaps 200, /yandex 200, theme asset 200 97324 B (v3 aws-sdk v3 S3-direct read из vds MinIO 2025). TLS не тронут (LE до Oct 18). Rollback = образ-тег `07db7e3` (v2, жив). |
|
|
||||||
| **B3v3 kupimknigi.spb.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.0→0.43.2` + resolutions (commit `5364353`, apps/web + root). Build на VDS (archive → `~/build/kupimknigi`) → `registry.kzntsv.site/kupimknigi:5364353`(+latest). Bundles a83e2a9 endpoint fix (minio.vds.kzntsv.site) — был в репо, но в бегущем образе 9608ff6 (v2) его не было. Throwaway staging :5079 parity-gate GREEN: `/` byte-identical с real Host, theme asset `toolbox.css` 200 35799 B byte-identical (v3 aws-sdk v3 S3-direct read из vds MinIO 2025), robots 200, `/callback-order/`→301. Portainer PUT stack 21 env-preserving (8/8, node in-memory put-stack), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: `/` 200, robots/sitemap 200, H1 «Скупка книг на дому в СПб…», theme asset 200 35799 B. TLS LE до Oct 3 (не тронут). **Rollback = `9608ff6`** (v2, жив в registry). |
|
|
||||||
| **B3v3 labtools.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commit `0dc0b4e`, apps/web + root). Endpoint fix `4f3314f` в master, но в бегущем образе `566d41c` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. Build на VDS (archive → `~/build/labtools`) → `registry.kzntsv.site/labtools:0dc0b4e`(+latest). Throwaway staging :5080 parity-gate GREEN: `/` byte-identical (18594B), theme `lato.css` 200 9239B byte-identical (v3 S3-direct), sitemap 38 locs == live (0 diff, 0 bad), section order-parity byte-identical. Portainer PUT stack 17 env-preserving (8/8, node in-memory), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: menu pages 200, theme 200 9239B, TLS CN не тронут (LE до Sep 30). **Rollback = `566d41c`** (v2, жив). См. [[labtools-vds-deploy-runbook]] §0.43.2. |
|
|
||||||
| **B3v3 labtools.pro** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commits `12f8992` bump + `a106622` yarn.lock реген — обязателен, иначе `yarn install --immutable` падает YN0028). Endpoint fix `6c7ea88` в master, но в бегущем образе `0610432` (v2) его не было. Build на VDS (archive → `~/build/labtools-pro`) → `registry.kzntsv.site/labtools-pro:a106622`(+latest), digest `2152642c4b17`. Throwaway staging :5081 parity-gate GREEN: `/`+nav+catalog 4-байт diff = `og:url`/`og:image` request-host (benign), redirects IDENT, robots/sitemap IDENT (7 детей == live), theme `lato.css` 200 9994B md5-identical (v3 S3-direct), order-parity press-forms 12 изделий. Portainer PUT stack 19 env-preserving (8/8, node in-memory put-stack), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: nav+каталог+продукты 200, theme 200 9994B, TLS CN=labtools.pro не тронут (LE до Sep 30). **Rollback = `0610432`** (v2, жив). См. [[labtools.pro-vds-deploy-runbook]] §0.43.2. |
|
|
||||||
| **B3v3 emspb.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commit `5afd8a1`, apps/web + root). Endpoint fix `218c3ae` в master, но в бегущем образе `95a5c42` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. Build на VDS (archive → `~/build/emspb`) → `registry.kzntsv.site/emspb:5afd8a1`(+latest), digest `72bb59fc…`. Throwaway staging :5082 parity-gate GREEN: robots md5 identical (31B), sitemap 29 locs == live, 29/29 routes 200 (+2B host-context benign), theme assets 7/7 md5 IDENTICAL (v3 S3-direct: snolla-framework/bootstrap/toolbox/theme/slick + images/2.jpg + /images/3.jpg 115618B). Portainer PUT stack 18 env-preserving (8/8, node in-memory put-stack), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: nav 200, /services+/index.php 404, theme assets md5 LIVE==staging, redirects 301 byte-identical post-cutover, apex 200. TLS не тронут. **Rollback = `95a5c42`** (v2, жив). См. [[emspb-vds-deploy-runbook]] §0.43.2. |
|
|
||||||
| **B3v3 tandemmebel.ru** | ✅ LIVE 2026-07-30 | bump `@snollajs/snolla 0.42.1→0.43.2` + resolutions (commit `9d51c09`, apps/web + root) + реген `yarn.lock` (single `@smithy/signature-v4@5.3.11`). Endpoint fix `f4ddce6` в master, но в бегущем образе `8df10ee` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio. Build на VDS (archive → `~/build/tandemmebel`) → `registry.kzntsv.site/tandemmebel:9d51c09`(+latest), digest `d14982c24dc7…`, 476MB (v2 8df10ee 583MB — v3 leaner). Throwaway staging :5020 parity-gate GREEN: robots md5 `a01e4c60…` identical (87B), sitemap 5 sub-sitemaps → **184 locs == live 184 IDENTICAL** (0 prod-only/new-only), self-consistency 183×200 + 1×404 (`/articles` parity benign), `/` +557B host-context benign, theme `Gotham-Pro.css` 200 4436B (v3 S3-direct). Portainer PUT stack 20 env-preserving (8/8, node in-memory put-stack — кириллица compose), `prune:false pullImage:true mem_limit 512m`. Live smoke GREEN: `/` 200 45742B, `/projects` 200, sitemap 184 locs, robots md5 identical, theme 200 4436B. TLS CN=tandemmebel.ru не тронут (LE до Oct 10). **Rollback = `8df10ee`** (v2, жив). См. [[tandemmebel-vds-deploy-runbook]] §0.43.2. **6/6 v3 сайтов GREEN — тираж v3 ЗАВЕРШЁН.** |
|
|
||||||
| Track A (books-vds minio 2020→2025) | ✅ DONE 2026-08-03 | **Оба инстанса переведены** на `RELEASE.2025-09-07T16-13-09Z` (тот же digest что vds). Shared `minio` (стек 30): зеркало всех 17 бакетов → `/usr/docker/minio/data-new`, Portainer swap, traefik/imgproxy работают. `bookva-minio` (стек 49): зеркало `books` (38692 obj) → том `bookva-minio-data-new` (docker volume rename НЕдоступен — старый daemon API; живёт на -new, бэкап-скрипт обновлён). Старые данные (`/usr/docker/minio/data`, `bookva-minio-data`) сохранены 48ч. Gotchas: (1) mc multipart-баг `ContentLength=16777216` на крупных файлах → `--disable-multipart`; (2) транзиентные `Connection closed by foreign host` при дефиците памяти (3.8G) при ПАРАЛЛЕЛЬНЫХ зеркалах → последовательно + retry-цикл; (3) compose файлы CRLF → `\r` в секрете ломает Authorization → `tr -d '\r'`; (4) Portainer stack file — через API `/api/stacks/{id}/file`. |
|
|
||||||
| **variant-cache bucket на vds 2025** | ✅ DONE 2026-07-31 | v3 cutover перевёл сайты на vds MinIO 2025, но `variant-cache` bucket не создали там (был на books-vds 2020). NoSuchBucket → 500 на ALL sharp gallery-images всех 6 v3 сайтов с 2026-07-30 (gate проверял theme CSS, не gallery URLs). Fix: `mc mb vds/variant-cache` (пустой, регенерируем). maljarka portfolio 34/34 → 200. См. memory `variant-cache-bucket-missing-on-vds-2025` + [[maljarka-vds-deploy-runbook]] §Гочи. |
|
|
||||||
| B4 .NET catch-all админ s3 endpoint | ✅ DONE 2026-07-30 | `C:\sites\snolla\Web.config` `<fileStorageClients>` 6/6 endpoint `minio.kzntsv.site`→`minio.vds.kzntsv.site` (galleries/assets/themes×4). region `us-east-1`, forcePathStyle (bin: AWSSDK.Core/S3 v3 модульный, bindingRedirect AWSSDK 1.3.19 — legacy-мусор). Pre-flip verify: `MINIO_ROOT_USER`==Web.config accessKey `AKIAJ2YJP72W6ZHCRE6Q` (reuse, no rotation) → mc alias Web.config-кредами → `minio.vds.kzntsv.site` → galleries/assets/themes читаются (B1 mirror жив). IIS auto-recycle по Web.config; login 200. **Превью:** оказался pre-existing баг S3-провайдера (НЕ от flip): `GalleriesStorage`/`AssetsStorage` ctor падал `KeyNotFoundException` на `config["ownerId"]` — контроллер-пути gallery preview/upload/delete и media-assets передавали только `siteContext`. Self-test провайдера gap — покрывал `GalleryImagesService` + unit-тесты `S3Ids`/`S3ObjectKey`, но preview-контроллер не тестировался. Фикс в провайдере (drop-in 1 DLL, MoreThenCms.FileStorage.S3.dll — `ToolsController` в `MoreThenCms.Web.dll` не собирается на боксе): `S3CloudStorage.ReadOwnerId` null-safe (explicit ownerId винит; иначе `siteContext.AssetsOwnerId`==SiteId; пусто → null без throw). Тесты 21/21 (4 новых). Live-verified: превью грузятся против vds MinIO. Коммит MoreThenCms `09cb072d`. Backup: `Web.config.b4.bak` + `MoreThenCms.FileStorage.S3.dll.b4.bak`. |
|
|
||||||
| B5 Obsidian Remotely Save endpoint | ✅ DONE 2026-07-30 | endpoint flipped `minio.kzntsv.site`→`minio.vds.kzntsv.site` в vault `C:\Users\vitya\obsidian\main\.obsidian\plugins\remotely-save\data.json` (rev+base64 обфускация), bucket `obsidian`, s3Region `us-east-1`, forcePathStyle. Verified деобфускацией config-блока. См. memory `obsidian-remotely-save-endpoint-after-mirror` (менять ТОЛЬКО после B1 миррора — что и было). |
|
|
||||||
|
|
||||||
**Решение user (2026-07-30):** сплит без бампа snolla-пакетов — сайты остаются на 0.42.1 + aws-sdk v2, только config endpoint. snolla-евый v3-трек (0.43.1/0.7.8/0.17.2) — HOLD, отдельная история (stostayer). Пилот on-snolla подтвердил: **v2 vs MinIO 2025 работает** (без @smithy pin) — SignatureDoesNotMatch нет.
|
|
||||||
|
|
||||||
**Разворот 2026-07-30T11:00Z:** v3 — конечная цель (user). v3 опубликована (см. §B3-v3). Промежуточный v2-пилот доказал совместимость; v3-пилот on-snolla — следующий шаг после live MinIO verify.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## §B3-v3 — v3-бамп (resolutions = optional determinism, НЕ mandatory)
|
|
||||||
|
|
||||||
`@snollajs/snolla 0.43.2` опубликована 2026-07-30 на verdaccio (latest): `@snollajs/liquid 0.11.0` + `@snollajs/data 0.15.0` + `@snollajs/core 0.26.4` (peerDep `@snollajs/data` widen `<0.15.0`→`<0.16.0` — баг-фикс: кап не расширен в `21c73aa`) + `@snollajs/snolla 0.43.2` (@smithy pin + `@aws-sdk/client-s3 3.1005.0` exact baked в deps). Коммит snolla `a8b198d`. Тесты core 259/259, snolla 120/120.
|
|
||||||
|
|
||||||
### Live MinIO verify 2026-07-30 — ЗЕЛЁНЫЙ (оба конфига)
|
|
||||||
|
|
||||||
snolla гоняла clean-consumer install `@snollajs/snolla@0.43.2` против живого `minio.vds.kzntsv.site`:
|
|
||||||
- **Run A** (с resolutions, `@smithy/signature-v4` 5.3.11 на signing-path): LIST/READ-PARITY/PUT/GET/MULTIPART — все OK, EXIT 0.
|
|
||||||
- **Run B** (БЕЗ resolutions, `@smithy/signature-v4` **5.6.12** на signing-path): те же тесты — **все OK, EXIT 0**.
|
|
||||||
|
|
||||||
**Оба зелёных.** 5.6.12 НЕ даёт SignatureDoesNotMatch против vds MinIO 2025. Regression-гипотеза (v3 SigV4 vs MinIO) **не воспроизвелась end-to-end** — была halted-bisect гипотезой, зацементированной как «CONFIRMED» без live-теста (косяк коммуникации, исправлен 2026-07-30). См. admin memory `aws-sdk-v3-smithy-sigv4-minio-regression`.
|
|
||||||
|
|
||||||
### Resolutions = optional determinism, НЕ SigV4-необходимость
|
|
||||||
|
|
||||||
Dual-instance механика реальна (5.3.11 exact из snolla/core + 5.6.12 из `@aws-sdk/core` рядом, без resolutions), но 5.6.12 на signing-path **работает** против vds MinIO 2025. Поэтому:
|
|
||||||
- `resolutions`-блок в package.json сайта — **опциональный** determinism-hardening (зафиксировать @smithy от свежего re-resolve, version-stability), НЕ обязательный для SigV4.
|
|
||||||
- Бамп только версии `@snollajs/snolla` без resolutions — **работает** (verify доказал).
|
|
||||||
- Рекомендую всё же ставить resolutions (детерминизм, безобидно, 5.3.11 тоже зелёный) — но framing «упадёт без resolutions» **ложен**, убрать.
|
|
||||||
|
|
||||||
### Copy-paste resolutions-блок (optional, recommended для determinism)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"resolutions": {
|
|
||||||
"@aws-sdk/client-s3": "3.1005.0",
|
|
||||||
"@aws-sdk/lib-storage": "3.1005.0",
|
|
||||||
"@smithy/signature-v4": "5.3.11",
|
|
||||||
"@smithy/node-http-handler": "4.4.14",
|
|
||||||
"@smithy/fetch-http-handler": "5.3.13",
|
|
||||||
"@smithy/protocol-http": "5.3.11",
|
|
||||||
"@smithy/middleware-endpoint": "4.4.23",
|
|
||||||
"@smithy/middleware-retry": "4.4.40"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### v3 bump procedure (per site, после live MinIO verify ⚪)
|
|
||||||
|
|
||||||
1. `package.json`: `@snollajs/snolla` → `0.43.2` + вставить resolutions-блок выше (optional но recommended).
|
|
||||||
2. `yarn install` (clean resolve — убедиться `npm ls @smithy/signature-v4` == single instance).
|
|
||||||
3. Build на VDS из site-репо (`deploy/Dockerfile`, build-arg VERDACCIO_TOKEN) → `registry.kzntsv.site/<site>-web:<sha>`.
|
|
||||||
4. Throwaway staging `:50XX` из env живого контейнера → parity-gate (theme asset 200 97324 B == live, sitemap 3/3, `/c` 404 parity).
|
|
||||||
5. Portainer PUT env-preserving (8 секретов), `prune:false, pullImage:true`, mem_limit 512m.
|
|
||||||
6. Live smoke (crawl menu-страниц + viewModel, не только `/`).
|
|
||||||
7. Rollback = образ-тег (текущий v2-тег).
|
|
||||||
|
|
||||||
### Breaking changes v3 (минимизированы back-compat шимом)
|
|
||||||
|
|
||||||
- `s3ForcePathStyle` → `forcePathStyle` — рантайм **НЕ breaking** (back-compat шим в storage.js v3 читает оба ключа). Config-правка recommended (forward-clean), не обязательна.
|
|
||||||
- `sslEnabled` (config) + `S3_SSL_ENABLED` (env) — удалены (unused), игнорируются если остались.
|
|
||||||
- `aws-sdk` / `s3fs` удалены из snolla deps (s3fs был unused). Consumer-код, импортивший их из snolla-deps напрямую (неофициальная поверхность) — сломается; по коду 6 сайтов такого нет.
|
|
||||||
- `populate-theme-store.mjs` мигрирован v2→v3 (script, не рантайм) — admin-side, рантайм не трогает.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## S3-клиент совместимость (audit — сделано)
|
|
||||||
|
|
||||||
| consumer | S3 client | версия | риск vs MinIO 2025 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| books (api/web/...) | @aws-sdk/client-s3 (v3) | ^3.450.0 | ✅ низкий |
|
|
||||||
| modules-db | minio (minio-js) | ^8.0.7 | ✅ низкий |
|
|
||||||
| snolla (7 сайтов + framework) | **aws-sdk v2** | ^2.1257.0 | ⚠️ EOL, тест обязателен |
|
|
||||||
| modulair-rag | client inside lightrag:latest image | — | проверить (bucket пуст) |
|
|
||||||
| obsidian Remotely Save | bundled | 0.5.25 | ✅ низкий |
|
|
||||||
| .NET admin (AWSSDK) | AWSSDK.S3 | ? | проверить (region `local`) |
|
|
||||||
|
|
||||||
**snolla ДО кутовера** запускает локальный node-скрипт (aws-sdk v2) → `https://minio.vds.kzntsv.site`, bucket `galleries` (stale-миррор там есть): listObjectsV2 + getObject. 200 = ок. Падение = бамп v2→v3 в фреймворке до кутовера.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Track A — апгрейд books-vds minio 2020→2025
|
|
||||||
|
|
||||||
> Все шаги — на books-vds (`root@89.253.255.133`, ключ `~/.ssh/id_ed25519_books_ops`).
|
|
||||||
> Transparent: НЕ меняем hostname/creds/имя-контейнера для apps. Только swap бэкенда под тем же именем.
|
|
||||||
> Креды для `mc alias` — из `pass show minio-vds/full-env` (`MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD`).
|
|
||||||
|
|
||||||
## A1. Shared `minio` 2020→2025
|
|
||||||
|
|
||||||
Принцип: новый 2025-контейнер `minio-new` (новый data-dir), `mc mirror` slovo-бакетов, swap traefik route, retire old. Контейнер-имя `minio` + internal DNS `http://minio:9000` сохранить для imgproxy/зависимостей (или imgproxy на books-vds выводится из эксплуатации после Track B DNS-flip — тогда можно переименовать).
|
|
||||||
|
|
||||||
**Бакеты, что остаются на shared minio (slovo):** `books` (37442 obj, 1.2 GiB), `ozon` (0B), `manuals` (0B), `test`/`upload`/`imgproxytest`. + почистить bookva-дубли (см. A3).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Креды из pass (НЕ хардкодить, НЕ коммитить):
|
|
||||||
source <(pass show minio-vds/full-env | grep -E 'MINIO_ROOT_(USER|PASSWORD)')
|
|
||||||
|
|
||||||
# 0. Стоп-условие: books apps живы на старом minio (baseline). Снимок логов.
|
|
||||||
ssh root@89.253.255.133 'docker logs --tail 50 books-api; docker exec minio sh -c "echo ok"'
|
|
||||||
|
|
||||||
# 1. Новый 2025 контейнер, новый data-dir, тот же network=proxy, креды из env
|
|
||||||
ssh root@89.253.255.133 'mkdir -p /usr/docker/minio-new/data'
|
|
||||||
# compose: image minio/minio:RELEASE.2025-09-07..., command server /data --console-address :9001,
|
|
||||||
# env MINIO_ROOT_USER/PASSWORD (== $MINIO_ROOT_USER/$MINIO_ROOT_PASSWORD), volume /usr/docker/minio-new/data:/data,
|
|
||||||
# network proxy, traefik labels minio-console.kzntsv.site→:9001 (новая консоль) — написать compose-файл
|
|
||||||
# (template ниже; НЕ поднимать до готовности миррора)
|
|
||||||
|
|
||||||
# 2. mc mirror slovo-бакетов old(2020) → new(2025)
|
|
||||||
docker run --rm --network host --entrypoint sh minio/mc:latest -c '
|
|
||||||
mc alias set old http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null
|
|
||||||
mc alias set new http://127.0.0.1:900X "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null
|
|
||||||
for b in books ozon manuals test upload imgproxytest; do mc mb --ignore-existing new/$b; mc mirror --overwrite old/$b new/$b; done
|
|
||||||
'
|
|
||||||
# 3. Verify per-bucket object count old==new
|
|
||||||
# for b in ...; mc ls --recursive old/$b | wc -l ; mc ls --recursive new/$b | wc -l
|
|
||||||
|
|
||||||
# 4. Swap: traefik route minio.kzntsv.site → new:9000 (rule в dynamic config / compose labels new-контейнера)
|
|
||||||
# + minio-console.kzntsv.site → new:9001 (новая веб-консоль 2025; старый /minio/login на :9000 умрёт)
|
|
||||||
# 5. Smoke: books-api/web/task-runner пишут/читают S3. Picking-list report GET.
|
|
||||||
# 6. Retire old 2020 minio (docker stop minio; НЕ удалять data 48ч — rollback).
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rollback:** traefik route → old minio:9000; old container ещё running 48ч.
|
|
||||||
|
|
||||||
## A2. `bookva-minio` 2020→2025
|
|
||||||
|
|
||||||
Контейнер `bookva-minio`, volume `bookva-minio-data` (37074 obj в бакете `books`). НЕ пуст (мигрирован ~2026-05). Console наружу НЕ выставляем.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source <(pass show minio-vds/full-env | grep -E 'MINIO_ROOT_(USER|PASSWORD)')
|
|
||||||
# 1. Новый 2025 контейнер bookva-minio-new (новый volume bookva-minio-data-new), тот же creds
|
|
||||||
# 2. mc mirror bookva-minio(2020):9001 → bookva-minio-new(2025):900X, бакет books (37074 obj)
|
|
||||||
docker run --rm --network host --entrypoint sh minio/mc:latest -c '
|
|
||||||
mc alias set bvold http://127.0.0.1:9001 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null
|
|
||||||
mc alias set bvnew http://127.0.0.1:900X "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null
|
|
||||||
mc mb --ignore-existing bvnew/books; mc mirror --overwrite bvold/books bvnew/books
|
|
||||||
'
|
|
||||||
# 3. Verify count: mc ls --recursive bvold/books | wc -l == mc ls --recursive bvnew/books | wc -l (37074)
|
|
||||||
# 4. Swap: container name bookva-minio (чтобы bookva-api http://bookva-minio:9000 не менялся) → новый
|
|
||||||
# (down old, up new с именем bookva-minio). Или compose-имя сохранить.
|
|
||||||
# 5. Smoke: bookva-api report/picking-list GET.
|
|
||||||
# 6. Retire old (48ч rollback).
|
|
||||||
```
|
|
||||||
|
|
||||||
## A3. Почистить bookva-дубли из shared `books` (после A1, перед mirror-verify)
|
|
||||||
|
|
||||||
В shared `books` остались 95+4 bookva-файла от до-миграции (дубли того, что уже на bookva-minio). Bookva-unique ключи вычислить из bookva-db и удалить из shared `books`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Выгрузить bookva-unique s3-пути (на books-vds):
|
|
||||||
docker exec -i bookva-db mariadb -ubooks -pFNud4Um47y books -N -e \
|
|
||||||
"SELECT DISTINCT report_remote_path FROM fbs_picking_lists WHERE report_remote_path!=''" > /tmp/bv-paths.txt
|
|
||||||
# (плюс pdf_remote_path, и из других таблиц — receipts/reports/task-reports — полный набор S3-путей bookva)
|
|
||||||
# Пересечь с shared-minio books-бакетом; bookva-unique (не в slovo-db) → mc rm из shared books
|
|
||||||
```
|
|
||||||
**Гейт:** до удаления убедиться, что КАЖДЫЙ bookva-путь жив на bookva-minio (после A2). Иначе не удалять.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Track B — сплит CMS-медиа на vds
|
|
||||||
|
|
||||||
## B0. vds backup (precondition)
|
|
||||||
|
|
||||||
`scripts/vds-backup-rsync-kreknin/run.sh` rsync sources — добавить `/opt/stacks/storage/minio-imgproxy/data` (и compose/nginx.conf для воспроизводимости):
|
|
||||||
|
|
||||||
```diff
|
|
||||||
/opt/stacks/owncloud \
|
|
||||||
+ /opt/stacks/storage/minio-imgproxy/data \
|
|
||||||
/etc/ssh \
|
|
||||||
```
|
|
||||||
+ обновить EMAIL_BODY components. На vds: `sudo systemctl restart cron` не нужен (cron читает файл). **Verify:** дождаться реального прогона (05:00) ИЛИ manual `sudo bash /opt/stacks/backup/scripts/run.sh` → ntfy push на `vds-backup` topic (phone-confirm, был открыт).
|
|
||||||
|
|
||||||
## B1. Миррор CMS-медиа books-vds → vds minio (свежий)
|
|
||||||
|
|
||||||
vds minio уже 2025-09-07. Stale-копия от 2026-05 — перезаписать свежим миррором.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source <(pass show minio-vds/full-env | grep -E 'MINIO_ROOT_(USER|PASSWORD)')
|
|
||||||
# На books-vds (источник) → vds minio (назначение), через mc. Creds одинаковые.
|
|
||||||
docker run --rm --network host --entrypoint sh minio/mc:latest -c '
|
|
||||||
mc alias set src http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null
|
|
||||||
mc alias set dst https://minio.vds.kzntsv.site "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" --insecure >/dev/null
|
|
||||||
for b in galleries artmone themes assets pilorama98 maxmind obsidian strapi; do
|
|
||||||
mc mb --ignore-existing dst/$b
|
|
||||||
mc mirror --overwrite --preserve src/$b dst/$b --insecure
|
|
||||||
done
|
|
||||||
'
|
|
||||||
# variant-cache НЕ миррорим (кэш imgproxy, перестроится на vds).
|
|
||||||
# modulair / modules — ВНЕ СПЛИТА (user: забыть). Остаются на books-vds shared minio, потребители не трогаются.
|
|
||||||
```
|
|
||||||
**Verify (обязательно):** recursive-count src==dst по каждому бакету (mc ls --recursive | wc -l). Совпадение = ок.
|
|
||||||
- Ожидаемые объёмы: galleries 9984, artmone 16273, themes 10864, assets 4750, pilorama98 225, maxmind 2, obsidian 179, strapi 0. ~8.9 GiB.
|
|
||||||
|
|
||||||
## B2. DNS-flip `imgproxy.kzntsv.site` → 89.253.255.94 (reg.ru)
|
|
||||||
|
|
||||||
- В reg.ru `kzntsv.site` A-запись `imgproxy` → `89.253.255.94` (сейчас → books-vds 89.253.255.133).
|
|
||||||
- vds imgproxy-nginx traefik label: добавить `Host(imgproxy.kzntsv.site)` (сейчас serves `imgproxy.vds.kzntsv.site`).
|
|
||||||
- TTL дождаться (был 86400 — лучше заранее снизить).
|
|
||||||
- **Verify:** `curl -sI https://imgproxy.kzntsv.site/` → vds traefik; sample CMS image URL (signed) → 200.
|
|
||||||
|
|
||||||
## B3. snolla deploy (6 сайтов) — по "го", после B1+B2
|
|
||||||
|
|
||||||
Образы собирает admin на VDS (snolla MCP read-only, нет build-канала; либо user даёт канал). Registry = `registry.kzntsv.site` (НЕ stostayer).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# На vds, для каждого из 6 репо (on-snolla, kupimknigi, tandemmebel, labtools, labtools.pro, emspb):
|
|
||||||
cd /tmp && git clone git@git.kzntsv.site:victor/<site>.git && cd <site>
|
|
||||||
git pull # правка s3.endpoint уже в master (snolla запушил: 07db7e3, a83e2a9, 218c3ae, 4f3314f, 6c7ea88, f4ddce6)
|
|
||||||
docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN=<from pass vds-kzntsv/verdaccio-ci> \
|
|
||||||
-t registry.kzntsv.site/<site>-web:<sha> .
|
|
||||||
docker push registry.kzntsv.site/<site>-web:<sha>
|
|
||||||
```
|
|
||||||
Затем parity-gate (throwaway staging `:50XX`) + Portainer PUT env-preserving (по [[snolla-live-prod-inplace-image-bump]]). Smoke каждого сайта (crawl menu pages + viewModel, не только /).
|
|
||||||
|
|
||||||
## B4. .NET catch-all админ (воркстейшн) — s3 endpoint
|
|
||||||
|
|
||||||
`C:\sites\snolla\Web.config` → `<fileStorageClients>` serviceURL: `https://minio.kzntsv.site` → `https://minio.vds.kzntsv.site` (galleries/assets/themes). imgproxy НЕ трогать (хардкод `imgproxy.kzntsv.site` → DNS-flip решает). IIS auto-recycle по Web.config. Smoke: `/admin/account/login` → Asset Manager превью.
|
|
||||||
|
|
||||||
## B5. Obsidian Remotely Save — endpoint (ПОСЛЕ B1 миррора obsidian)
|
|
||||||
|
|
||||||
Гейт: бакет `obsidian` смиррорен на vds (B1). Менять ТОЛЬКО тогда.
|
|
||||||
- Obsidian → Settings → Remotely Save → S3 endpoint: `https://minio.kzntsv.site/` → `https://minio.vds.kzntsv.site/` (бакет `obsidian`, креды те же). ИЛИ ре-обфусцировать `C:\Users\vitya\obsidian\main\.obsidian\plugins\remotely-save\data.json` (rev+base64) при ЗАКРЫТОМ Obsidian.
|
|
||||||
- Verify: sync run → no errors.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# P3 — cleanup / verify (после cutover)
|
|
||||||
|
|
||||||
- Retire books-vds 2020 minio containers (после 48ч rollback-окна): `docker stop minio` (old), `docker stop bookva-minio` (old). НЕ удалять data до подтверждения бэкапа.
|
|
||||||
- Retire books-vds imgproxy stack 29 (после DNS-flip — изображения идут через vds imgproxy). Проверить, что никто не ходит на books-vds imgproxy.
|
|
||||||
- Удалить с shared `books` bookva-дубли (A3).
|
|
||||||
- vds-backup: подтвердить что `/opt/stacks/storage/minio-imgproxy/data` попадает в nightly snapshot (B0 verify).
|
|
||||||
- Smoke: все 6 snolla-сайтов (crawl), books slovo reports, bookva reports, obsidian sync, modules-db.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Открытые/verify (до кутовера)
|
|
||||||
|
|
||||||
1. **snolla compat-тест aws-sdk v2 vs vds minio** — snolla делает сейчас (без железа).
|
|
||||||
2. **.NET admin AWSSDK версия + region** — `local` работает с MinIO 2025? (stostayer нужен был `us-west-1`, но там minio настроен `us-west-1`; books-vds minio = `local`). Verify.
|
|
||||||
3. **modulair-rag lightrag:latest S3-клиент** — compat с MinIO 2025 (bucket пуст, низкий приоритет).
|
|
||||||
4. **artmone** — деплой неясен (контейнера на vds нет). Бакет миррорим (B1), но потребителя для endpoint-смены нет. Отдельный вопрос.
|
|
||||||
5. **bookva-minio расход 368 obj** (shared 37442 vs bookva-minio 37074) — проверить, не stranded ли bookva-файлы в shared (не попали в миррор 2026-05). До A3.
|
|
||||||
|
|||||||
@@ -1,101 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: on.snolla.com snolla-app — VDS deploy runbook (cutover 2026-07-20)
|
|
||||||
type: concept
|
|
||||||
tags: [on-snolla, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, landing]
|
|
||||||
related: [[../entities/vds-kzntsv], [../entities/ruvds-iis-host], [tandemmebel-vds-deploy-runbook], [snolla-live-prod-inplace-image-bump], [portainer-stack-management-vds], [snolla-local-admin-and-on-snolla-migration-design]]
|
|
||||||
updated: 2026-07-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# on.snolla.com → VDS deploy runbook
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:182]] (concepts/on-snolla-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Вынос посадочной `on.snolla.com` (snolla-app, `@snollajs/snolla` **0.42.1**, server-side Liquid) с
|
|
||||||
[[../entities/ruvds-iis-host]] (catch-all CMS, 80.64.31.36) в docker-контейнер на [[../entities/vds-kzntsv]]
|
|
||||||
(89.253.255.94), за traefik. Паттерн = [[tandemmebel-vds-deploy-runbook]]. Отличие: **лендинг** (1 Page `/`
|
|
||||||
+ 2 StaticPages), БЕЗ блога/каталога/e-commerce → reconstruction = layout.liquid byte-identical prod-HTML.
|
|
||||||
Culture `en`. Спека: [[snolla-local-admin-and-on-snolla-migration-design]] §Task B.
|
|
||||||
|
|
||||||
## Артефакты
|
|
||||||
- **Код:** `victor/on.snolla.com` @ `473923e494db` (apps/web, snolla 0.42.1). Структура-калька tandemmebel.ru.
|
|
||||||
`deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root, healthcheck `/robots.txt`).
|
|
||||||
- **Образ:** `registry.kzntsv.site/on-snolla:473923e494db` (+`:latest`). Имя **`on-snolla`** (= стек/контейнер).
|
|
||||||
Собран НА VDS (обход traefik-499).
|
|
||||||
- **Стек Portainer:** `on-snolla` (**Id 22**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/on-snolla.compose.yml`.
|
|
||||||
- **siteId:** `B9ECDB50-2B93-4CE5-B238-A9918B3F9CF7` (non-secret, в `production.json`).
|
|
||||||
- **siteUrl:** `https://on.snolla.com` (в `production.json`).
|
|
||||||
- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD`
|
|
||||||
— Portainer stack env (переиспользованы verbatim из tandemmebel стека 20 — общая snolla-инфра). НЕ в образе, НЕ в git.
|
|
||||||
- **mem_limit:** `512m`.
|
|
||||||
|
|
||||||
## Site model (MoreThenCms DB, siteId B9ECDB50)
|
|
||||||
- 1 Page `/` (template `content_page`, `customDefaultLayout=layout`). **Content-поля пустые** → весь
|
|
||||||
bootstraptor-лендинг baked в `views/layout.liquid` (byte-identical prod). `{{ item.content }}` (пуст) wired.
|
|
||||||
- Form "Join us" (Path `/`, FormTemplate `join_us`: first_name/last_name/email/website/description/captcha)
|
|
||||||
baked в `layout.liquid`; POST `/` → snolla `forms` middleware → `form_ajax_response.liquid` (AJAX replace).
|
|
||||||
Legacy reCAPTCHA v1 в разметке мертва (Google shut down v1) — parity с продом, НЕ чиним.
|
|
||||||
- StaticPages: `/yandex_7a510ac1f8311fca.html` (verification, IncludeInSitemap=true), `/4a052808276c.html` (not in sitemap).
|
|
||||||
- `/c` → **404** (stale dead-link в .NET sitemap; snolla-app sitemap тоже его несёт → parity `/`+`/c`+`/yandex`).
|
|
||||||
- Theme assets `/themes/c406a987ebe14244b0eefe7b8f959a6f/{css,js,images}/*` — из MinIO bucket `themes` (snolla
|
|
||||||
`themeFiles` middleware; пути literal в `layout.liquid`).
|
|
||||||
- `/admin` — **публичного `on.snolla.com/admin` не существует** (никогда не было: RUVDS catch-all IIS не
|
|
||||||
маршрутизировал /admin на публичном on.snolla.com — cert на 80.64.31.36:443 SNI on.snolla.com = чужой
|
|
||||||
`CN=kupimknigi.spb.ru`, /admin → 404). Админка — только **локальный catch-all IIS `snolla`** (Task A,
|
|
||||||
воркстейшн). Site alias переименован `on`→`internal` (2026-07-20), доступ = `http://internal.snolla.com/admin`
|
|
||||||
через hosts override → 127.0.0.1 → 302→login. **Web.config `primaryAlias`** тоже обновлён `on`→`internal`
|
|
||||||
(default-site resolution; иначе catch-all NRE-500 на ВСЕХ запросах — primaryAlias ссылается на алиас,
|
|
||||||
переименование сайта без обновления primaryAlias ломает весь catch-all). НЕ часть snolla-app.
|
|
||||||
|
|
||||||
## Cutover 2026-07-20 (первый вынос на VDS — DNS-gated)
|
|
||||||
1. **Build на VDS** (archive `473923e494db` → `~/build/on-snolla` → `docker build -f deploy/Dockerfile
|
|
||||||
--build-arg VERDACCIO_TOKEN -t .../on-snolla:473923e494db -t .../on-snolla:latest . && push`). Guard:
|
|
||||||
`config/default.json` ABSENT в архиве (.dockerignore), пин snolla = 0.42.1.
|
|
||||||
2. **Throwaway-staging :5077** из собранного env (8 секретов inline, т.к. нового live-контейнера ещё нет —
|
|
||||||
секреты общие с тиражом). `docker run -d --name on-snolla-staging --env-file .staging.env -p 127.0.0.1:5077:5000`.
|
|
||||||
Healthy. robots/`/`/sitemap 200.
|
|
||||||
3. **Completeness-gate С VDS** — sitemapindex разворот. **NEW==PROD locs: 3 = 3 IDENTICAL** (`/`, `/c`, `/yandex_…`).
|
|
||||||
Self-consistency: `/`→200, `/yandex`→200, `/c`→404 (dead-link, parity с прод-оракулом — prod /c тоже 404).
|
|
||||||
**Byte-parity:** homepage + /yandex **byte-identical** staging vs prod (RUVDS, via `--resolve :443:80.64.31.36`).
|
|
||||||
GREEN = 0 регрессий.
|
|
||||||
4. **Portainer стек 22** создан со **staging-Host** `on-snolla.vds.kzntsv.site` (wildcard → VDS), LE=default cert.
|
|
||||||
Healthy за traefik. (`create-stack.mjs` node-скрипт, Portainer JWT auth, env array 8/8, `mem_limit 512m`.)
|
|
||||||
5. **DNS flip** — operator: reg.ru `on.snolla.com` A `80.64.31.36` → `89.253.255.94`.
|
|
||||||
6. **Verify авторит. NS** — `nslookup on.snolla.com ns1/ns2.reg.ru` = `89.253.255.94` (BEFORE traefik rule swap —
|
|
||||||
memory `operator-dns-flip-verify-domain-before-cutover`: wrong domain burns LE rate-limit). Public resolvers propagated.
|
|
||||||
7. **PUT стека 22** — rule `Host(on-snolla.vds.kzntsv.site)` → `Host(on.snolla.com)`, env-preserving (8/8),
|
|
||||||
`prune:false, pullImage:false`. node `cutover-put.mjs` (Portainer JWT; **re-auth right before PUT** —
|
|
||||||
jwt из GET-фазы успел протухнуть к моменту PUT, первый PUT дал 401). PUT 200.
|
|
||||||
8. **Poll** — контейнер healthy, **LE-серт issued on first hit**: CN=on.snolla.com, issuer YR1, until 2026-10-18.
|
|
||||||
9. **Live-smoke С VDS + external** — `https://on.snolla.com/{robots.txt,/,/sitemap.xml,/yandex_…}` = 200,
|
|
||||||
`/c` = 404 parity. TLS verify `ssl_verify_result=0` (trusted LE chain). Homepage **byte-identical** prod-HTML.
|
|
||||||
|
|
||||||
## In-place bumps (после cutover)
|
|
||||||
|
|
||||||
- **07db7e3 — MinIO split config-endpoint (v2, 2026-07-30).** In-place bump стека 22 `473923e494db→07db7e3` без правки `package.json`: только `config/production.json` `s3.endpoint` `minio.kzntsv.site→minio.vds.kzntsv.site`. snolla остался 0.42.1 (aws-sdk v2). Пилот S3-direct-path — theme asset 200 97324 B == live, v2 vs vds MinIO 2025 работает без @smithy pin. Rollback-тег `473923e494db`. См. [[minio-split-cutover-runbook]] §B3 (Track B).
|
|
||||||
- **eb8c143 — v3 bump (aws-sdk v2→v3, 2026-07-30T11:28Z).** End-goal (user). `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` + `resolutions` (@aws-sdk/client-s3 3.1005.0 exact + @smithy chain 5.3.11) в root `package.json` (optional determinism, single-instance — live verify доказал что 5.6.12 тоже работает, pin для version-stability). Commit `eb8c143` pushed. Build на VDS (archive → `~/build/on-snolla` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN`) → `registry.kzntsv.site/on-snolla:eb8c143` (+`:latest`). Throwaway staging :5078 из env live-контейнера (8 секретов) → parity-gate GREEN (theme asset 200 97324 B byte-identical, sitemapindex+3 sub-sitemaps byte-identical, `/c` 404 parity). Portainer PUT stack 22 env-preserving (8/8, node in-memory `put-stack` — НЕ PS Invoke-RestMethod), `prune:false, pullImage:true`, `mem_limit 512m` preserved. Live smoke GREEN: `/` 200, robots/sitemap/sub-sitemaps 200, `/yandex` 200, theme asset 200 97324 B (v3 aws-sdk v3 S3-direct read из vds MinIO 2025). TLS не тронут (LE до Oct 18). Back-compat шим `s3ForcePathStyle`→`forcePathStyle` (v2-key ещё читается) — config-правки не требовались. **Rollback = `07db7e3`** (v2, жив в registry). См. [[minio-split-cutover-runbook]] §B3-v3 + [[snolla-live-prod-inplace-image-bump]].
|
|
||||||
|
|
||||||
## Гочи (специфичные)
|
|
||||||
- **snolla `robotsTxt` middleware падает на undefined `app.locals.domain`** — on.snolla.com НЕ имеет row в
|
|
||||||
MoreThenCms `Domains` (served by primaryDomain без per-domain row). `robotsTxt.js` читает
|
|
||||||
`res.app.locals.domain.robotsTxt` без null-guard → GET /robots.txt (и Dockerfile healthcheck) крашится 500.
|
|
||||||
**Фикс в `apps/web/index.js`**: после `initApp`, если `!app.locals.domain`, synthesized `{robotsTxt: site.robotsTxt}`
|
|
||||||
(only robotsTxt.js reads locals.domain; theme resolution uses site.activeThemeId — safe). Без мутации shared DB.
|
|
||||||
**Альтернатива** (не применена): добавить Domains row (Name=on.snolla.com, SiteId=B9ECDB50, Public=1, ThemeId=null)
|
|
||||||
— но это мутация shared prod MoreThenCms; предпочтён app-code fallback.
|
|
||||||
- **Form "Join us" не имеет Liquid form-tag** — snolla не предоставляет form-rendering tag. Форма HTML baked в
|
|
||||||
`layout.liquid` (поля по FormTemplate `join_us`); POST обрабатывает `forms` middleware (мэтч по Path `/`).
|
|
||||||
`form_ajax_response.liquid` — AJAX-ответ (`$form.replaceWith(response)`).
|
|
||||||
- **`/c` в sitemap — dead-link** — snolla SitemapService эмбитит `/c` из sections-source, но страница 404.
|
|
||||||
Prod-оракул идентичен (404) → benign parity, не регрессия.
|
|
||||||
- **hosts override на воркстейшне** — Task A оставил `127.0.0.1 on.snolla.com` в hosts (локальный админ catch-all).
|
|
||||||
External verify прод-on.snolla.com с воркстейшна = только через `curl --resolve on.snolla.com:443:89.253.255.94`
|
|
||||||
(иначе попадёшь в локальный IIS). См. memory `workstation-lan-dns-serves-local-cms-copy`.
|
|
||||||
- **Portainer JWT short-lived** — GET stack + GET file + PUT в одном скрипте: jwt из GET-фазы может протухнуть
|
|
||||||
к PUT. Re-auth (`POST /api/auth`) прямо перед PUT. API-key `ptr_*` даёт 401 (см. [[portainer-stack-management-vds]]).
|
|
||||||
- **Образных rollback-тегов пока нет** — первый deploy, `473923e494db` = `:latest`. ~~Rollback = revert DNS (RUVDS жив).~~ RUVDS DECOMM 2026-07-21 — rollback только образ-тегом.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
- ~~**DNS (предпочт, мгновенный):** revert reg.ru `on.snolla.com` A → `80.64.31.36` (RUVDS IIS жив, не тронут).~~ **НЕАКТУАЛЬНО с 2026-07-21** — [[../entities/ruvds-iis-host]] DECOMM (погашен у провайдера). RUVDS более не rollback-target.
|
|
||||||
- **Образный (рабочий):** PUT стека 22 назад на предыдущий тег (rollback-теги в registry). `473923e494db` = `:latest` — первый deploy, предыдущего тега нет; с этого дня ведём rollback-теги на каждый bump.
|
|
||||||
|
|
||||||
## Связанное
|
|
||||||
- on.snolla.com admin (`/admin`) остаётся на RUVDS .NET — catch-all IIS `snolla` (см. [[snolla-local-admin-and-on-snolla-migration-design]] §Task A).
|
|
||||||
- Тираж snolla на VDS: labtools.ru(17), emspb(18), labtools.pro(19), tandemmebel(20), kupimknigi(21), **on-snolla(22)**.
|
|
||||||
|
|||||||
3
.wiki/concepts/pilorama98-vds-deploy-runbook.md
Normal file
3
.wiki/concepts/pilorama98-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3330]] (concepts/pilorama98-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
@@ -36,19 +36,19 @@ docker run -d --name portainer --network proxy \
|
|||||||
# API admin/init available 5 minutes after start
|
# API admin/init available 5 minutes after start
|
||||||
curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/admin/init \
|
curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/admin/init \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"Username":"vitya","Password":"Pryakhin9-VDS-2026"}'
|
-d '{"Username":"vitya","Password":"<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>"}'
|
||||||
|
|
||||||
# Login → JWT
|
# Login → JWT
|
||||||
JWT=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/auth \
|
JWT=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/auth \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"Username":"vitya","Password":"Pryakhin9-VDS-2026"}' \
|
-d '{"Username":"vitya","Password":"<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>"}' \
|
||||||
| jq -r .jwt)
|
| jq -r .jwt)
|
||||||
|
|
||||||
# Generate API key
|
# Generate API key
|
||||||
APIKEY=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/1/tokens \
|
APIKEY=$(curl -sk -X POST https://portainer.vds.kzntsv.site/api/users/1/tokens \
|
||||||
-H "Authorization: Bearer $JWT" \
|
-H "Authorization: Bearer $JWT" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"description":"automation","password":"Pryakhin9-VDS-2026"}' \
|
-d '{"description":"automation","password":"<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>"}' \
|
||||||
| jq -r .rawAPIKey)
|
| jq -r .rawAPIKey)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ curl -sk -X POST https://portainer.vds.kzntsv.site/api/endpoints \
|
|||||||
|
|
||||||
## Trade-off
|
## Trade-off
|
||||||
|
|
||||||
- ✗ Имя пользователя пароль user'а — приходится менять на «12+ chars» (`Pryakhin9-VDS-2026` вместо `Pryakhin9`).
|
- ✗ Имя пользователя пароль user'а — приходится менять на «12+ chars» (`<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>` вместо `Pryakhin9`).
|
||||||
- ✔ Single source of truth — admin live в DB только через API, всегда последовательное состояние.
|
- ✔ Single source of truth — admin live в DB только через API, всегда последовательное состояние.
|
||||||
- ✔ API token доступен сразу для дальнейшей автоматизации stacks через REST.
|
- ✔ API token доступен сразу для дальнейшей автоматизации stacks через REST.
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Portainer API token из `vds-kzntsv/full-env` `PORTAINER_API_KEY` ранее д
|
|||||||
```bash
|
```bash
|
||||||
JWT=$(curl -ksS -X POST https://portainer.vds.kzntsv.site/api/auth \
|
JWT=$(curl -ksS -X POST https://portainer.vds.kzntsv.site/api/auth \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"username":"vitya","password":"Pryakhin9-VDS-2026"}' | jq -r .jwt)
|
-d '{"username":"vitya","password":"<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>"}' | jq -r .jwt)
|
||||||
```
|
```
|
||||||
|
|
||||||
Pass-store: `pass show vds-kzntsv/full-env` (full env file со всеми creds).
|
Pass-store: `pass show vds-kzntsv/full-env` (full env file со всеми creds).
|
||||||
|
|||||||
@@ -75,3 +75,18 @@ Sub-manifest (и его слои) физически удалены, а **тег
|
|||||||
- [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md) — auth/htpasswd, юзер books-ci, GC через v2 DELETE.
|
- [`registry-kzntsv-auth-model`](registry-kzntsv-auth-model.md) — auth/htpasswd, юзер books-ci, GC через v2 DELETE.
|
||||||
- [`bindmount-config-edit-preserve-mode`](bindmount-config-edit-preserve-mode.md) — как клали кред books-ci в task-runner config (shadow + mode гочи).
|
- [`bindmount-config-edit-preserve-mode`](bindmount-config-edit-preserve-mode.md) — как клали кред books-ci в task-runner config (shadow + mode гочи).
|
||||||
- Хост: [`books-vds`](../entities/books-vds.md) (task-runner), реестр на [`vds-kzntsv`](../entities/vds-kzntsv.md).
|
- Хост: [`books-vds`](../entities/books-vds.md) (task-runner), реестр на [`vds-kzntsv`](../entities/vds-kzntsv.md).
|
||||||
|
|
||||||
|
## Обновление 2026-08-23: 2.8.3 vs OCI-index — docker pull «manifest unknown» + фикс docker-форматом
|
||||||
|
|
||||||
|
**Симптом:** `docker pull registry.kzntsv.site/books-tool-create-picking-list-pdf:master` → `manifest unknown` (и «no basic auth credentials» до починки config.json — см. [[books-vds-no-gitea-actions]]).
|
||||||
|
|
||||||
|
**Диагностика (curl к /v2/<repo>/manifests/<tag>):**
|
||||||
|
- Accept docker-only (`manifest.v2+json`, `manifest.list.v2+json`) → **404**
|
||||||
|
- Accept c `application/vnd.oci.image.index.v1+json` → 200 (Content-Type: OCI index)
|
||||||
|
- child-манифест из index по digest → **404** (удалён прежним host-GC — dangling, как описано выше)
|
||||||
|
|
||||||
|
→ registry 2.8.3 отдаёт OCI-index тег только на точный OCI-Accept; docker daemon 26.1.4 получает «manifest unknown». Запушенные buildx/curl-флоу теги = OCI index с **отсутствующим child** = сломанный pull.
|
||||||
|
|
||||||
|
**Фикс (проверен): перепуш образа как docker schema2-манифест** — те же блобы (content-addressed), обёртка docker: mediaType `application/vnd.docker.distribution.manifest.v2+json`, config `application/vnd.docker.container.image.v1+json` (НЕ `distribution.image.config.v2+json` — docker его не знает, «(unknown)» на pull), layers `application/vnd.docker.image.rootfs.diff.tar.gzip`. Пути: `docker save` → OCI tar → amd64 child → трансформ mediaTypes → `PUT /v2/<repo>/manifests/<tag>` с docker Content-Type. Pull снова работает.
|
||||||
|
|
||||||
|
**Вывод для оператора:** на registry:2.8.3 избегать пуш OCI-формата (buildx по умолчанию), host-GC рвать children нельзя; 2026-08-23 dry-run GC: 67 «eligible», реально существующих файлов ~2.5МБ (остальное — уже удалённые ссылки) → host-GC на живом реестре нецелесообразен, риск > выгоды. Аккуратная чистка buildcache/старых тегов — index-aware инструментом books (`registryGc` в task-runner, v2 DELETE).
|
||||||
|
|||||||
3
.wiki/concepts/runbooks-index.md
Normal file
3
.wiki/concepts/runbooks-index.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3316]] (concepts/runbooks-index). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
3
.wiki/concepts/sched-pipelines-local-stack-runbook.md
Normal file
3
.wiki/concepts/sched-pipelines-local-stack-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3331]] (concepts/sched-pipelines-local-stack-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
3
.wiki/concepts/sched-publish-runbook.md
Normal file
3
.wiki/concepts/sched-publish-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3332]] (concepts/sched-publish-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
3
.wiki/concepts/sched-vds-deploy-runbook.md
Normal file
3
.wiki/concepts/sched-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3333]] (concepts/sched-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
6
.wiki/concepts/sched-workers-vds-mock-runbook.md
Normal file
6
.wiki/concepts/sched-workers-vds-mock-runbook.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# sched-workers-vds — СТУБ
|
||||||
|
|
||||||
|
> ⚙️ **Не читать, не править.** Канон — mappa wiki-сущность `concepts/sched-workers-vds-mock-runbook` (wiki:3339, проект .admin).
|
||||||
|
> Файловый канал ранбуков закрыт (2026-08-29, task:1507). Git-история хранит прежний контент.
|
||||||
|
|
||||||
|
См. `wiki_get(project=".admin", slug="concepts/sched-workers-vds-mock-runbook")`.
|
||||||
3
.wiki/concepts/skills-catalog-deploy-runbook.md
Normal file
3
.wiki/concepts/skills-catalog-deploy-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3315]] (concepts/skills-catalog-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
27
.wiki/concepts/ssh-hardening-vds.md
Normal file
27
.wiki/concepts/ssh-hardening-vds.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: SSH-хардненинг books-vds + аудит vds (2026-08-25)
|
||||||
|
status: live
|
||||||
|
tags: [security, ssh, fail2ban, centos7, books-vds, vds-kzntsv, decision]
|
||||||
|
---
|
||||||
|
|
||||||
|
# SSH-хардненинг books-vds + аудит vds (2026-08-25)
|
||||||
|
|
||||||
|
Решение vitya: «ты за безопасность отвечаешь — и на vds, и на books-vds». Триггер — аудит task:1165.
|
||||||
|
|
||||||
|
## Инцидент-триггер
|
||||||
|
|
||||||
|
Рестарт `books-job-scheduler-mongo` 24.08 22:02Z (инцидент sched, storage-mongo 0.5.1 ветка) — внешнее действие вне агентов (.admin/books/sched сессий нет). Аудит доступа показал: успешных чужих SSH-входов НЕТ (все Accepted = наш ключ), но **постоянный брутфорс**: 8773 Failed за 25.08, 5810 с `210.61.148.174` (HiNet, Тайвань), `PasswordAuthentication=yes`, fail2ban не установлен. Рестарт mongo не через SSH (Accepted в окне пусто) и не через sched-таску — вероятнее Portainer UI (запросы не логируются).
|
||||||
|
|
||||||
|
## Что сделано — books-vds (89.253.255.133, CentOS 7!)
|
||||||
|
|
||||||
|
1. **`PasswordAuthentication no`** в /etc/ssh/sshd_config (бэкап `sshd_config.bak-*`, `sshd -t` перед reload). Доступ — только по ключу `id_ed25519_books_ops` (authorized_keys = 1 запись).
|
||||||
|
2. **fail2ban** (sshd jail: bantime 1h, findtime 10m, maxretry 5, ignoreip 94.19.247.14). **Уже забанил 210.61.148.174**.
|
||||||
|
3. **CentOS 7 EOL-фикс реп**: mirrorlist.centos.org мёртв → vault.centos.org/7.9.2009 (base/extras/updates/centosplus), EPEL 7 → archives.fedoraproject.org (epel-release-7-14), `ip_resolve=4` в /etc/yum.conf (битый IPv6 провайдера ломал yum — dl.fedoraproject.org отдавал «Cannot open»).
|
||||||
|
|
||||||
|
## vds.kzntsv.site (89.253.255.94)
|
||||||
|
|
||||||
|
Парольный SSH **уже отключён** (сервер принимает только publickey — проверено). НО наш SSH-ключ на vds не установлен (ни один из ~/.ssh/* не подошёл) — доступ только через Portainer API (portainer.vds.kzntsv.site). fail2ban на vds не проверен (нет SSH). Хвост: решить, нужен ли SSH-ключ к vds.
|
||||||
|
|
||||||
|
## Portainer books (portainer.kzntsv.site)
|
||||||
|
|
||||||
|
Admin-пароль в pass НЕ хранится (только BOOKS_PORTAINER_API_KEY). Рекомендация: сменить admin-пароль + записать в pass.
|
||||||
@@ -22,7 +22,7 @@ updated: 2026-07-22
|
|||||||
| Параметр | Значение |
|
| Параметр | Значение |
|
||||||
|---|---|
|
|---|---|
|
||||||
| accessKeyId | `stayer_minio` (bucket-user) |
|
| accessKeyId | `stayer_minio` (bucket-user) |
|
||||||
| secretAccessKey | `cXY>AVoWBv#_V-SZn6Vu6Tzx&!M[%UOod` |
|
| secretAccessKey | `secret:stostayer/minio` |
|
||||||
| endpoint | `https://minio-api.stostayer.ru` |
|
| endpoint | `https://minio-api.stostayer.ru` |
|
||||||
| region | `us-west-1` (см. ниже — НЕ `local`) |
|
| region | `us-west-1` (см. ниже — НЕ `local`) |
|
||||||
| pathStyle / ssl | `true` / `true` |
|
| pathStyle / ssl | `true` / `true` |
|
||||||
@@ -31,7 +31,7 @@ imgproxy: `https://imgproxy.stostayer.ru`, basePath `/imgproxy2`, key+salt —
|
|||||||
|
|
||||||
## Три бага в исходном Web.config (почему "смотрит на локальное")
|
## Три бага в исходном Web.config (почему "смотрит на локальное")
|
||||||
|
|
||||||
1. **Чужие креды.** `accessKey=AKIAJ2YJP72W6ZHCRE6Q` + `secretKey=7o0Q4NjE5GL…` — это **books-vds/snolla MinIO root-ключ** (см. [[minio-imgproxy-on-vds]]). На stostayer MinIO не зарегистрирован → `The Access Key Id you provided does not exist in our records`. Видимо, конфиг копипастом из snolla catch-all admin ([[snolla-local-admin-and-on-snolla-migration-design]], тот юзает books-vds `minio.kzntsv.site`), без замены ключей на stostayer-свои.
|
1. **Чужие креды.** `accessKey=secret:pass/minio-vds/full-env` + `secretKey=<root-secret>5GL…` — это **books-vds/snolla MinIO root-ключ** (см. [[minio-imgproxy-on-vds]]). На stostayer MinIO не зарегистрирован → `The Access Key Id you provided does not exist in our records`. Видимо, конфиг копипастом из snolla catch-all admin ([[snolla-local-admin-and-on-snolla-migration-design]], тот юзает books-vds `minio.kzntsv.site`), без замены ключей на stostayer-свои.
|
||||||
2. **Не API-порт.** `serviceURL=https://minio.stostayer.ru` → MinIO отвечает `S3 API Requests must be made to API port`. Прод юзает `https://minio-api.stostayer.ru`. Оба хоста резолвятся в один IP `2a03:6f01:1:2::c66b`, но `minio.stostayer.ru` — не S3 API-эндпоинт.
|
2. **Не API-порт.** `serviceURL=https://minio.stostayer.ru` → MinIO отвечает `S3 API Requests must be made to API port`. Прод юзает `https://minio-api.stostayer.ru`. Оба хоста резолвятся в один IP `2a03:6f01:1:2::c66b`, но `minio.stostayer.ru` — не S3 API-эндпоинт.
|
||||||
3. **Регион.** `authenticationRegion="local"` → SigV4-подпись регионом не совпала → `AmazonS3Exception: The authorization header is malformed; the region is wrong; expecting 'us-west-1'` (HTTP 400) на `GalleriesStorage.EnsureKeyPrefixExists` → `S3DirectoryInfo.ExistsWithBucketCheck` (провайдер галерей падал на ините, в трассе `ToolsController.ImagePreview` → `CreatePreview`). stostayer MinIO настроен на регион `us-west-1`. Прод `default.json` пишет `region:"local"`, но то Node aws-sdk (galleries через imgproxy мимо app-S3; для .NET AWSSDK регион обязан быть `us-west-1`).
|
3. **Регион.** `authenticationRegion="local"` → SigV4-подпись регионом не совпала → `AmazonS3Exception: The authorization header is malformed; the region is wrong; expecting 'us-west-1'` (HTTP 400) на `GalleriesStorage.EnsureKeyPrefixExists` → `S3DirectoryInfo.ExistsWithBucketCheck` (провайдер галерей падал на ините, в трассе `ToolsController.ImagePreview` → `CreatePreview`). stostayer MinIO настроен на регион `us-west-1`. Прод `default.json` пишет `region:"local"`, но то Node aws-sdk (galleries через imgproxy мимо app-S3; для .NET AWSSDK регион обязан быть `us-west-1`).
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ imgproxy: `https://imgproxy.stostayer.ru`, basePath `/imgproxy2`, key+salt —
|
|||||||
9 S3-блоков в `fileStorageClients` (galleries/assets/images/scripts/stylesheets/imageCache/watermarks + inert-настройки contentCache/uploadCache) — 3 глобальные замены в `Web.config`:
|
9 S3-блоков в `fileStorageClients` (galleries/assets/images/scripts/stylesheets/imageCache/watermarks + inert-настройки contentCache/uploadCache) — 3 глобальные замены в `Web.config`:
|
||||||
|
|
||||||
```
|
```
|
||||||
accessKey: AKIAJ2YJP72W6ZHCRE6Q → stayer_minio
|
accessKey: secret:pass/minio-vds/full-env → stayer_minio
|
||||||
secretKey: 7o0Q4NjE5GLkdC48r0oZFEnqddjPNLqtCk+ZEh+S → cXY>AVoWBv#_V-SZn6Vu6Tzx&!M[%UOod (XML-escape: > → >, & → &)
|
secretKey: secret:pass/minio-vds/full-env → cXY>AVoWBv#_V-SZn6Vu6Tzx&!M[%UOod (XML-escape: > → >, & → &)
|
||||||
serviceURL: https://minio.stostayer.ru → https://minio-api.stostayer.ru
|
serviceURL: https://minio.stostayer.ru → https://minio-api.stostayer.ru
|
||||||
authenticationRegion: local → us-west-1
|
authenticationRegion: local → us-west-1
|
||||||
```
|
```
|
||||||
@@ -68,4 +68,4 @@ authenticationRegion: local → us-west-1
|
|||||||
|
|
||||||
- [[galleries-storage-class-local-not-s3]] — snolla-тираж: galleries/assets Local→S3 миграция, snolla catch-all admin на books-vds MinIO.
|
- [[galleries-storage-class-local-not-s3]] — snolla-тираж: galleries/assets Local→S3 миграция, snolla catch-all admin на books-vds MinIO.
|
||||||
- [[snolla-admin-appdata-acl-500-after-scp-migration]] — split-brain админка(Local)↔боевой app(MinIO) на RUVDS; та же проблема «админка не переключена на S3».
|
- [[snolla-admin-appdata-acl-500-after-scp-migration]] — split-brain админка(Local)↔боевой app(MinIO) на RUVDS; та же проблема «админка не переключена на S3».
|
||||||
- [[minio-imgproxy-on-vds]] — books-vds MinIO (root-ключ `AKIAJ2YJP72W6ZHCRE6Q`), который ошибочно попал в stostayer Web.config.
|
- [[minio-imgproxy-on-vds]] — books-vds MinIO (root-ключ `secret:pass/minio-vds/full-env`), который ошибочно попал в stostayer Web.config.
|
||||||
@@ -1,134 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: stostayer-web — деплой легаси web на прод клиента (runbook)
|
|
||||||
status: live
|
|
||||||
tags: [stostayer, docker, portainer, deployment, nuxt2, esm, ops, rollback]
|
|
||||||
related: [[portainer-stack-management-vds]]
|
|
||||||
updated: 2026-06-17
|
|
||||||
---
|
|
||||||
|
|
||||||
# stostayer-web — деплой легаси web на прод клиента
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:207]] (concepts/stostayer-web-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Прод `https://www.stostayer.ru` = легаси `packages/web` (Nuxt 2, CJS, SSR) из монорепо `victor/stostayer.new`. web4 (Nuxt 4) ещё НЕ переключён. Хостится контейнером, управляется Portainer-стеком у клиента.
|
|
||||||
|
|
||||||
> **Машина клиента, настраивает их админ — мы только деплоим свой web-образ, конфиги хоста не трогаем.** Деталь доступов/инфры — в `stostayer.new/.wiki/concepts/client-infra-access.md`. Все креды — `pass stostayer/client`.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Что | Значение |
|
|
||||||
|---|---|
|
|
||||||
| Хост | `91.222.236.225` (SSH `:20435`, юзер `victor` + sudo-с-паролем; парольный SSH) |
|
|
||||||
| Reverse-proxy | **Angie** (форк nginx) на `:443`, навешивает **Basic-auth** на сервисные сабдомены |
|
|
||||||
| Registry | `docker.stostayer.ru` (BA те же, что Angie BA) — образы `stostayer-web:<tag>` |
|
|
||||||
| Стек | Portainer **`stostayer-web` Id 16, EndpointId 3**, compose в Portainer-volume `/data/compose/16/docker-compose.yml` |
|
|
||||||
| Прод-тег (2026-08-13) | `0.3.23`. На хосте лежат старые теги под откат (`0.3.22` и ранее) |
|
|
||||||
| Образ | `packages/web/Dockerfile`: `node:16` → `yarn install` → `yarn build` (nuxt build) → `CMD yarn start`. pm2/`ecosystem.config.js` в проде НЕ используется |
|
|
||||||
|
|
||||||
## Канал деплоя (build здесь → registry → Portainer)
|
|
||||||
|
|
||||||
### 1. Build образа (локально, offline)
|
|
||||||
|
|
||||||
Тег образа независим от `packages/web/package.json` version — это чисто image-тег, инкремент от прода (`0.3.18` → `0.3.19`). Кода/версий не бампать.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# в чекауте нужного коммита, .yarn/cache populated
|
|
||||||
docker build -f packages/web/Dockerfile -t docker.stostayer.ru/stostayer-web:<tag> . # контекст = корень репо
|
|
||||||
```
|
|
||||||
|
|
||||||
Гочи build:
|
|
||||||
- **`.dockerignore` обязателен** (его нет в репо) — иначе `node_modules`/`.git` улетают в build-контекст. Создать временный: `node_modules **/node_modules .git apps docs build *.zip` и т.п. (на явные `COPY` не влияет).
|
|
||||||
- **Локальная `npmAuthToken: "${VERDACCIO_TOKEN}"` в `.yarnrc.yml`** (рабочая модификация, не в коммите) **ломает build**: yarn внутри образа падает `Usage Error: Environment variable not found (VERDACCIO_TOKEN)`. Убрать эту строку из build-копии (потом вернуть).
|
|
||||||
- **Offline против `.yarn/cache`:** добавить `enableNetwork: false` в `.yarnrc.yml` — depы пекутся из cache, verdaccio-токен в образе не нужен. (Cache содержит third-party зипы; их версии ESM-миграция не меняла, так что cache от master-tip обычно покрывает и более старые коммиты.)
|
|
||||||
- **✅ Offline-кэш ПОЧИНЕН (2026-08-13, коммит `5c583f8`):** в `.yarnrc.yml` добавлен `supportedArchitectures: {cpu:[x64], os:[linux,win32,darwin], libc:[glibc,musl]}` — раньше без `libc` yarn пропускал fetch `@rollup/rollup-linux-x64-gnu` (условие libc=glibc). `.yarn/cache` gitignored → один раз на build-машине прогнать сетевой `yarn install` (с `VERDACCIO_TOKEN`) — yarn дольёт `@esbuild/linux-x64`/`@rollup/rollup-linux-x64-gnu` в локальный кэш, дальше offline (`enableNetwork:false`) работает. Сделано 2026-08-13 на этой машине (зипы в кэше).
|
|
||||||
|
|
||||||
### 2. Push в registry
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo '<ba_pass>' | docker login docker.stostayer.ru -u Victor --password-stdin
|
|
||||||
docker push docker.stostayer.ru/stostayer-web:<tag>
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️ **ОДИН push, без retry-циклов.** Инцидент 2026-06-17: retry-шторм (6 попыток, образ 3.29GB) на `docker.stostayer.ru` засветил egress нашего **VPN** → хостинг-провайдер забанил VPN-IP → ВЕСЬ HTTPS дом→хост (`:443`: сайт + registry + portainer) стал TLS-fail (`schannel: failed to receive handshake`), выглядело как «сайт лёг» (а TCP 443/SSH 20435 — открыты). Лечится сменой VPN. **На client-инфру: одна попытка, сбой → стоп и к человеку, не долбить.** (см. [[verify-on-real-client-not-own-curl-tests]])
|
|
||||||
>
|
|
||||||
> ⚠️ **Инцидент 2026-07-21: единичный large-push ТОЖЕ триггерит бан** (не только retry-storm). Первый push `0.3.20` (3.33GB) с VPN → тот же VPN-IP бан → `:443` к хосту TLS-fail с операторского IP (сайт для остальных посетителей работал — Angie access-log это подтвердил; SSH:20435 оставался открыт). **Бан НЕ IP-специфичный для всех — для остальных сайт жив, режется только egress-IP источника push'а.** Митигация: (a) **resume-push после смены egress** — `docker push` resumable, уже залитые слои «Layer already exists» (без re-upload), добивается только остаток + manifest → egress мал → повторный бан не триггерится; (b) **build-on-host + push в localhost-registry** (registry-контейнер на хосте, `127.0.0.1` — вообще без внешнего egress) — самый чистый путь для крупных образов. **Не пытаться долбить упавший push повторно с того же egress.**
|
|
||||||
|
|
||||||
### 3. Передеплой стека — с ХОСТА, мимо Angie BA
|
|
||||||
|
|
||||||
Portainer published **только в docker-сеть** (на host-localhost его НЕТ — там MinIO на `:9000`). Прямой Portainer-API из дома ломает Angie BA (Bearer затирает Basic → 401; auth-cookie не ставится). Решение — бить API по **IP контейнера portainer с самого хоста**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SSH (PuTTY plink; host-key уже известен)
|
|
||||||
plink -ssh -batch -hostkey SHA256:6Zk14J/UakVqBYm/fMPFrL3gosdBdyZOKZnbA0GVnD0 \
|
|
||||||
-P 20435 -pw '<ssh_pass>' victor@91.222.236.225 "bash -s" <<'EOF'
|
|
||||||
base=http://172.18.0.2:9000 # docker inspect portainer → IP в bridge; ПОРТ :9000 обязателен (голый IP → curl на :80 → HTTP=000)
|
|
||||||
JWT=$(curl -s $base/api/auth -H 'Content-Type: application/json' \
|
|
||||||
-d '{"username":"victor","password":"<app_pass>"}' | jq -r .jwt)
|
|
||||||
FILE=$(curl -s -H "Authorization: Bearer $JWT" $base/api/stacks/16/file | jq -r .StackFileContent)
|
|
||||||
NEW=$(printf '%s' "$FILE" | sed 's#stostayer-web:0.3.18#stostayer-web:<NEWTAG>#')
|
|
||||||
PAYLOAD=$(jq -n --arg c "$NEW" '{stackFileContent:$c, env:[], prune:false, pullImage:true}')
|
|
||||||
curl -s -w "\nHTTP=%{http_code}\n" -X PUT -H "Authorization: Bearer $JWT" \
|
|
||||||
-H "Content-Type: application/json" -d "$PAYLOAD" "$base/api/stacks/16?endpointId=3"
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
- `docker` на хосте — через `sudo` (victor не в группе docker): `echo '<ssh_pass>' | sudo -S <cmd>`. Sudo-пароль = SSH-пароль victor.
|
|
||||||
- `jq` на хосте есть. Скрипт через `bash -s`/stdin — чисто с кавычками; UTF-8 round-trip compose не портит (в отличие от PowerShell Invoke-RestMethod, [[portainer-stack-management-vds]] gotcha #9).
|
|
||||||
- `pullImage:true` → Portainer сам тянет новый тег из `docker.stostayer.ru` (хост-docker уже авторизован в registry).
|
|
||||||
|
|
||||||
### 4. Verify (read-only, без нагрузки на публичный сайт)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' | grep stostayer-web # образ = новый тег, Up
|
|
||||||
docker logs --tail 20 stostayer-web # чистый старт Nuxt, без ошибок/краш-лупа
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verify контента (напр. цен) — с хоста, прямым curl в nuxt-контейнер** (Angie даёт 403 на no-UA запрос с самого хоста; обход — `localhost:3000`, host-network):
|
|
||||||
```bash
|
|
||||||
# на хосте (sudo docker + host-network контейнер на :3000)
|
|
||||||
curl -s -H "Host: www.stostayer.ru" -A "Mozilla/5.0" http://localhost:3000/<page> -o /tmp/v.html
|
|
||||||
grep -c "<expected-text>" /tmp/v.html
|
|
||||||
```
|
|
||||||
**Гоча verify-URL:** bare `/remont-kondicionerov` 301-редиректит на `/remont/remont-kondicionerov` (DB-driven `oldPageRedirect`/unit-slug mapping) — это CMS-контент-страница БЕЗ nuxt-компонента. Компоненты `pages/<dir>/*.vue` реально live на своих nuxt-маршрутах (напр. `/remont-kondicionerov/{zapravka,diagnostika}-kondicionera`) — верифицируй по ним, не по bare-URL.
|
|
||||||
|
|
||||||
**Прод-дрейф mount-конфига (2026-08-16, инцидент content-api):** хост монтирует
|
|
||||||
`/var/docker/stostayer-web/config` → `/usr/src/app/packages/web/config` — это **источник истины
|
|
||||||
для прод-конфига**. node-config читает из cwd (`packages/web/config`), т.е. репо-правки
|
|
||||||
`packages/*/config/default.json` (напр. c43d168: `s3.region local → us-west-1`) на прод **не
|
|
||||||
влияют** — образ подхватывает mount. После деплоя, меняющего конфиг: синхронизировать
|
|
||||||
хост-конфиг (или осознанно задокументировать mount как truth). Нюанс node18: `localhost` →
|
|
||||||
`::1` (IPv6-first) — в образе `NODE_OPTIONS=--dns-result-order=ipv4first` (MariaDB/MSSQL на
|
|
||||||
хосте IPv4-only).
|
|
||||||
|
|
||||||
**Гоча локального smoke-контейнера:** `packages/web/config/default.json` содержит **прод-БД** (`www.stostayer.ru:3306` MariaDB + `:1433` MSSQL), и `www.stostayer.ru` резолвится в публичный IP хоста → smoke-контейнер без env-overrides **подключается к prod-БД клиента по интернету** (read-only SSR, без writes, но всё равно нежелательно). Для чистого локального smoke — переопределяй `data.sequelize.*.host` env'ом на localhost/заглушку, либо глуши контейнер сразу после `Server Listening`.
|
|
||||||
|
|
||||||
### Откат
|
|
||||||
|
|
||||||
Тот же PUT с прежним тегом (`<NEWTAG>` → `0.3.18`), `pullImage:true`. 0.3.18 на хосте есть. RTO ~30-60с (пересоздание контейнера).
|
|
||||||
|
|
||||||
## ✅ БЛОКЕР РАЗРЕШЁН 2026-07-21 (retrofit + snolla 0.7.6)
|
|
||||||
|
|
||||||
> **Legacy web снова собирается+запускается с master.** Решение: ретрофит `packages/web` (`require()`→dynamic `import()` в 5 местах, 2 serverMiddleware → CJS) + фикс `@snollajs/snolla@0.7.6` (bare-`get`-баг, см. ниже). Образ `0.3.20` собран, запушен, передеплоен на прод stostayer.ru, verify зелёный. Ретрофит лежит в working-tree stostayer.new (незакоммичен на 2026-07-21) — **stostayer.new должен закоммитить** его в master, иначе следующий legacy-web деплой упрётся в ту же стену. Секция ниже оставлена как история.
|
|
||||||
|
|
||||||
## ⛔ БЛОКЕР (история, 2026-06-17): легаси web НЕ пересобирается с текущего master (ESM-стена)
|
|
||||||
|
|
||||||
> На 2026-06-17 фикс формы жалоб (`120bc07`, задача `stostayer-web-complaint-form-deploy`) **выкатить не удалось** — два ESM-барьера:
|
|
||||||
|
|
||||||
1. **Build-time:** с master-tip `yarn build` (nuxt) падает `SyntaxError: await is only valid in async functions...` — `@stostayer/data` после таски `esm-data-dual` грузит модели через **top-level await**; цепочка `nuxt.config.js → @stostayer/api → @stostayer/data`, Nuxt2 читает конфиг через jiti → top-level await недопустим. Корень — коммит `c805e7e` (ESM-миграция data/api).
|
|
||||||
2. **Runtime:** cherry-pick `120bc07` на `d02f740` (родитель c805e7e, «последний CJS-собираемый master») **собирается**, но контейнер **краш-лупит**: `ERR_REQUIRE_ESM` — `packages/web/server/index.js:6` `require('@snollajs/snolla')`, а версия `@snollajs/snolla` из lockfile d02f740 уже **ESM**. Т.е. d02f740 не runtime-чистая.
|
|
||||||
|
|
||||||
Прод `0.3.18` жив только потому, что собран на ещё более старом стейте (CJS `@snollajs/snolla`, CJS data/api).
|
|
||||||
|
|
||||||
**Вердикт (stostayer.new подтвердил 2026-06-17):** легаси-web `require()`-ит **5** ESM-ставших пакетов — `@snollajs/snolla` (0.7.4, `server/index.js:6`), `@stostayer/api` (`nuxt.config.js:3`), `@snollajs/content-api` (0.8.0, `nuxt.config.js:4`), `@stostayer/data` (`serverMiddleware/oldPagesRedirections.js:14` + `redirections.js:14`). Чтобы найти base где ВСЕ пять ещё CJS — надо к ~0.3.18 и потерять всё с тех пор. **Чейз базы бесполезен. `packages/web` (node16/CJS/Nuxt2) не пересобираем ни с какого свежего дерева — весь dep-граф pure-ESM. `0.3.18` заморожен (последний собираемый артефакт).**
|
|
||||||
|
|
||||||
**Путь:** фикс формы (`120bc07`) едет вместе с **переездом формы в web4** (Opt 3). Ретрофит `require`→dynamic `import()` в 5 местах технически возможен на node16, но это часы на стек, который удаляется web4 — не рекомендуется. До web4-cutover — **остаёмся на 0.3.18**, admin-сессия код не правит ([[verify-on-real-client-not-own-curl-tests]]).
|
|
||||||
|
|
||||||
> Образ `0.3.19` (краш-лупный) лежит в `docker.stostayer.ru` — **НЕ деплоить**. Оставлен в registry по решению vitya (2026-06-17), не удаляем.
|
|
||||||
|
|
||||||
## ✅ Деплой 0.3.23 (2026-08-13)
|
|
||||||
|
|
||||||
Запрос их прогера (письмо `.agents/inbox/2026-08-13T08-43-41Z-stostayer-new.md`): calculator-баннер про цены при покупке запчастей в компании Стайер (exactPrices>0) + фикс краша (snolla `loadCity`/`loadSite` в asyncData) + стек-фикс легаси (явные vue/vuex/vue-router/mariadb deps, `nmHoistingLimits: workspaces`). Коммиты `e5f7bd6`+`7e51cbf`+`d49b4bb` (master-tip). Собран (см. гоча offline выше), запушен **1 попыткой** (слои mostly already-exists → egress мал, бана нет), стек 16 передеплоен 0.3.22→0.3.23 (`pullImage:true`, env re-supply — у стека env пуст). Verify зелёный: Focus II → 200 + баннер, регресс `/`+ремонтная 200, Focus IV (exactPrices=0) без баннера + алерт «уточняйте у мастеров». Прод до этого был на **0.3.22** (0.3.21/0.3.22 собраны 24.07; деплой 0.3.22 шёл через pscp+`sudo docker load` без push в :443).
|
|
||||||
|
|
||||||
## Связи
|
|
||||||
|
|
||||||
- [[portainer-stack-management-vds]] — родственный паттерн (наш VDS), оттуда PowerShell-кодировочные гочи и redeploy-рецепт.
|
|
||||||
- Доступы/инфра клиента (детально): `stostayer.new/.wiki/concepts/client-infra-access.md`.
|
|
||||||
- Задача: `.tasks/stostayer-web-complaint-form-deploy.md` (🔵 blocked на victor/stostayer.new).
|
|
||||||
|
|||||||
@@ -1,161 +1,3 @@
|
|||||||
---
|
# ⛔ Файловый канал закрыт
|
||||||
title: tandemmebel.ru snolla-app — VDS deploy runbook (cutover + 0.42.1 bumps + 0.43.2 v3 cutover)
|
|
||||||
type: concept
|
|
||||||
tags: [tandemmebel, snolla, vds, deploy, docker, portainer, traefik, runbook, migration, blog-portfolio, v3, aws-sdk]
|
|
||||||
related: [[../entities/vds-kzntsv]], [[../entities/ruvds-iis-host]], [[portainer-stack-management-vds]], [[minio-imgproxy-on-vds]], [[snolla-live-prod-inplace-image-bump]], [[labtools-vds-deploy-runbook]], [[labtools.pro-vds-deploy-runbook]], [[emspb-vds-deploy-runbook]], [[minio-split-cutover-runbook]]
|
|
||||||
updated: 2026-07-30
|
|
||||||
---
|
|
||||||
|
|
||||||
# tandemmebel.ru → VDS deploy runbook
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:208]] (concepts/tandemmebel-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
|
|
||||||
Вынос `tandemmebel.ru` (snolla-приложение, `@snollajs/snolla` **0.42.1**, server-side Liquid, **блог-портфолио БЕЗ e-commerce каталога**)
|
|
||||||
с [[../entities/ruvds-iis-host]] (catch-all CMS, 80.64.31.36) в отдельный docker-контейнер на [[../entities/vds-kzntsv]]
|
|
||||||
(89.253.255.94), за traefik. Модель — snolla-app: читает БД-контент MoreThenCms (`mssql.kzntsv.site`)
|
|
||||||
+ ассеты из MinIO (`minio.kzntsv.site`). In-process sharp (resize+data-driven watermark), delivery=serve-bytes,
|
|
||||||
variant-cache бакет в MinIO. imgproxy ИЗ ПУТИ tandem УБРАН (сырой `/imgproxy` → 404 норма).
|
|
||||||
|
|
||||||
Паттерн = [[emspb-vds-deploy-runbook]] / [[labtools.pro-vds-deploy-runbook]]. Отличие: **блог-портфолио** (не каталожный →
|
|
||||||
order-парити секций НЕ применимо — см. 0.42.1 bump ниже). Culture `ru-RU`.
|
|
||||||
|
|
||||||
## Артефакты
|
|
||||||
- **Код:** `victor/tandemmebel.ru` @ `0cd9351` (apps/web, snolla **0.42.1** / core 0.24.1 / liquid 0.10.2 / data 0.14.1).
|
|
||||||
`deploy/Dockerfile` (multi-stage node:22-slim, `yarn install --immutable`, non-root uid `node`, healthcheck `/robots.txt`).
|
|
||||||
- **Образ:** `registry.kzntsv.site/tandemmebel:0cd9351` (+`:latest`). Имя **`tandemmebel`** (= имя стека/контейнера).
|
|
||||||
Собран НА VDS (обход traefik-499). digest `sha256:f29c187fe6114d6ab3926132f4b3eb09df9b606d448cd8fd873780687cb46b8f`.
|
|
||||||
- **Стек Portainer:** `tandemmebel` (**Id 20**, endpoint 1). Source-of-truth compose: `admin/host-stacks/vds-kzntsv/tandemmebel.compose.yml`.
|
|
||||||
- **siteId:** `78080707-F6E0-4330-BA30-7922354C2CEF` (non-secret, в `production.json` — НЕ env).
|
|
||||||
- **siteUrl:** `https://www.tandemmebel.ru` (в `production.json`).
|
|
||||||
- **Runtime env (8 секретов):** `DB_USER DB_PASSWORD IMGPROXY_KEY IMGPROXY_SALT S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY SMTP_USER SMTP_PASSWORD` — Portainer stack env (переиспользованы verbatim из labtools стека 17). НЕ в образе, НЕ в git.
|
|
||||||
- **mem_limit:** `512m` (guardrail; см. [[portainer-stack-management-vds]] § Convention).
|
|
||||||
|
|
||||||
## Cutover 2026-07-12 (первый вынос на VDS — DNS-gated)
|
|
||||||
Последовательность (LE-критичность соблюдена) — см. `NEXT_SESSION.md`:
|
|
||||||
1. **Verify авторит. NS** — `nslookup tandemmebel.ru ns1/ns2.reg.ru` = оба `89.253.255.94` (apex + www), не только резолвер.
|
|
||||||
2. **GET стек 20** — образ `tandemmebel:ed96b18` (0.42.0, staging cutover-подготовлен), env 8/8, rule staging `Host(tandemmebel.vds.kzntsv.site)`.
|
|
||||||
3. **PUT стек 20** — `Host(tandemmebel.ru) || Host(www.tandemmebel.ru)`, env-preserving, `prune:false, pullImage:false`. Python urllib UTF-8 (НЕ PS Invoke-RestMethod — gotcha кириллицы, см. [[portainer-stack-management-vds]]).
|
|
||||||
4. **Poll** — контейнер healthy, LE-серт issued on first hit: CN=tandemmebel.ru, SAN оба, issuer YR2, until 2026-10-10.
|
|
||||||
5. **Live-smoke С VDS** — GREEN.
|
|
||||||
6. Compose source-of-truth + борд обновлены.
|
|
||||||
|
|
||||||
**Латентный прод-баг починен cutover'ом:** `Gotham-Pro.css` был 0B на RUVDS → теперь 200/4436B.
|
|
||||||
|
|
||||||
## In-place bump 2026-07-12: 0.42.0 → 0.42.1 (order-tag Drop-field fix)
|
|
||||||
По рецепту [[snolla-live-prod-inplace-image-bump]] (поверх cutover'а того же дня).
|
|
||||||
|
|
||||||
### Консюмер-бамп (решён оператором, без dev-source)
|
|
||||||
Тот же блокер-паттерн 2026-07-04 (заявленная snolla-версия ≠ консюмер-пин) — на этот раз решён самим оператором:
|
|
||||||
- `apps/web/package.json:12` — `0.42.0` → `0.42.1`.
|
|
||||||
- `yarn install` в корне монорепы → обновил корневой `yarn.lock` (snolla 0.42.1 / core 0.24.1 / liquid 0.10.2 / data 0.14.1). Dockerfile `yarn install --immutable` — lock обязан совпадать с пином.
|
|
||||||
- commit `0cd9351` + push origin (git.kzntsv.site/victor/tandemmebel), подтверждён `ls-remote`.
|
|
||||||
- Тег образа = sha монорепы `0cd9351`.
|
|
||||||
|
|
||||||
### Build → staging → gate → swap
|
|
||||||
1. **Build на VDS** (archive `0cd9351` → `~/build/tandemmebel` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN -t .../tandemmebel:0cd9351 -t .../tandemmebel:latest . && push`). Guard: `config/default.json` ABSENT в архиве (.dockerignore), пин = 0.42.1.
|
|
||||||
2. **Throwaway-staging :5020** из env живого контейнера (`docker inspect tandemmebel` → `.staging.env`, фильтр `^(HOSTNAME|PATH|NODE_VERSION|YARN_VERSION|HOME)=`). `docker run -d --name tandemmebel-staging --env-file .staging.env -p 127.0.0.1:5020:5000 .../tandemmebel:0cd9351` → healthy. robots.txt+`/`+sitemap 200.
|
|
||||||
3. **Completeness-gate С VDS** — sitemapindex разворот (`expand()`), два чека:
|
|
||||||
- **NEW==PROD locs: 184 = 184 IDENTICAL** (вкл. 0.42.x sitemap-реструктуризацию — parity сохранился).
|
|
||||||
- **self-consistency + content-not-lost** (locs идентичны → один проход): 183×2xx + 1×404.
|
|
||||||
- Единственный 404 `/articles` — **идентичен прод-оракулу** (404 и на live `https://www.tandemmebel.ru/articles`) → benign (пустая секция-без-индекса, не регрессия). Tandemmebel НЕ каталожный → order-парити **не применимо** (0.42.1 order-fix инертен на этой теме — как archive-роуты на 0.40→0.42).
|
|
||||||
- **GREEN = 0 регрессий в 404/5xx.**
|
|
||||||
4. **Боевой swap** — operator-gated, node `put-stack.js` (env-preserving: 8/8 name+value, `prune:false, pullImage:true`):
|
|
||||||
`META=st20.json node put-stack.js 20 1 ed96b18 0cd9351` → PUT 200. Контейнер 0cd9351+healthy за ~8s.
|
|
||||||
5. **Live-smoke** — `https://www.tandemmebel.ru/{robots.txt,/,/projects,/sitemap.xml}` = 200. TLS-серт CN=tandemmebel.ru **не дёрнут** (in-place swap серт не трогает). `docker rm -f tandemmebel-staging`.
|
|
||||||
|
|
||||||
### Rollback
|
|
||||||
- Тег `ed96b18` (0.42.0) в registry (+ `b02ca18`, 0.16.2-staging) → PUT стека 20 назад.
|
|
||||||
- ИЛИ revert DNS → 80.64.31.36 (RUVDS IIS жив, не тронут — rollback всех мигрированных).
|
|
||||||
|
|
||||||
## In-place bump 2026-07-13: 0.42.1 → 0.42.1 (sha 0cd9351 → 8df10ee, template-only)
|
|
||||||
|
|
||||||
Ops-handoff от tandemmebel-сессии (inbox-запрос): убрать FB/Twitter/Google+ share-кнопки
|
|
||||||
(экстремистистская символика РФ), оставить ВК+Одноклассники. Коммит `8df10ee` в `victor/tandemmebel.ru`
|
|
||||||
master, меняет только `apps/web/views/social_buttons.liquid` (1 file, 12 deletions), snolla pin 0.42.1
|
|
||||||
НЕ менялся → in-place bump на той же 0.42.1 (не консюмер-бамп).
|
|
||||||
|
|
||||||
### Предсборочная верификация (поймала расхождение)
|
|
||||||
Записка от tandemmebel-сессии утверждала: «сейчас живой на стеке 20 — `ed96b18`/0.42.0». Фактически
|
|
||||||
на проде крутился **`0cd9351`/0.42.1** (in-place bump 0.42.0→0.42.1 был 2026-07-12). Без проверки это
|
|
||||||
не повлияло бы (см. ниже), но вслепую строить нельзя. Проверки С VDS + gitea API (токен `gitea/admin-token`):
|
|
||||||
- `docker inspect tandemmebel` → `registry.kzntsv.site/tandemmebel:0cd9351`, healthy, started 2026-07-12.
|
|
||||||
- gitea `compare/0cd9351...8df10ee` → `total_commits:1`: фикс ровно один коммит поверх 0.42.1 (правильная база).
|
|
||||||
- `git show 8df10ee:apps/web/package.json` → `@snollajs/snolla: 0.42.1` (pin не менялся); yarn.lock snolla 0.42.1 /
|
|
||||||
core 0.24.1 / liquid 0.10.2 / data 0.14.1 — идентично live 0cd9351.
|
|
||||||
- `git show 8df10ee --stat` → только `apps/web/views/social_buttons.liquid` (modified).
|
|
||||||
- 8df10ee full sha `8df10eedcba9979ac91dce2025b2ff827d60e808`, master HEAD = 8df10ee.
|
|
||||||
|
|
||||||
Если бы 8df10ee был на базе 0.42.0 (172 locs) → completeness-gate поймал бы регрессию 184→172. Он на 0.42.1 →
|
|
||||||
parity 184=184.
|
|
||||||
|
|
||||||
### Build → staging → gate → swap
|
|
||||||
1. **Build на VDS** из чистого архива `8df10ee` (gitea API `/archive/<fullsha>.tar.gz`, token- auth `oauth2:`-
|
|
||||||
НЕ работает, `Authorization: token`-header на API endpoints работает; git clone по HTTPS фейлится даже
|
|
||||||
с oauth2:token — gitea admin-token = API-only, не git-transport). Extract `--strip-components=1` в
|
|
||||||
`~/build/tandemmebel-8df10ee`. `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN=<books-ci JWT>
|
|
||||||
-t registry.kzntsv.site/tandemmebel:8df10ee -t .../tandemmebel:latest .` → digest
|
|
||||||
`sha256:30a7e5ab82f2bf371042f1b5fa0cd7ac5ceda71379d1da59e148dee8166bd560`, layer-cache hit (package.json+
|
|
||||||
yarn.lock идентичны 0cd9351), push OK.
|
|
||||||
2. **Throwaway-staging :5020** из env живого контейнера (`docker inspect tandemmebel` → `.staging.env`,
|
|
||||||
фильтр `^(HOSTNAME|PATH|NODE_VERSION|YARN_VERSION|HOME|PORT|NODE_ENV)=`). `docker run -d --name
|
|
||||||
tandemmebel-staging --env-file .staging.env -p 127.0.0.1:5020:5000 .../tandemmebel:8df10ee` → healthy 8s.
|
|
||||||
3. **Completeness-gate С VDS** — sitemapindex разворот. **Важно:** staging sitemapindex содержит АБСОЛЮТНЫЕ
|
|
||||||
prod-URLs (siteUrl=prod в production.json) → sub-sitemaps надо фетчить со staging ПО ПУТЯМ
|
|
||||||
(`http://127.0.0.1:5020/sitemap-pages-1.xml`), а не следовать абсолютным URL (иначе сравнишь PROD==PROD).
|
|
||||||
- NEW==PROD locs: **184 = 184 IDENTICAL** (0 prod-only, 0 new-only).
|
|
||||||
- self-consistency: 183×200 + 1×404 (`/articles` benign parity staging==prod).
|
|
||||||
- share-block: staging vk+ok / fb-tw-gp=0 vs prod-before (0cd915 live ещё имел) vk+ok+fb+tw+gp —
|
|
||||||
фикс убирает ровно лишнее, остальное не трогает.
|
|
||||||
- GREEN = 0 регрессий.
|
|
||||||
4. **Боевой swap** — Portainer JWT auth (`POST /api/auth` vitya/Pryakhin9-VDS-2026; API-key `ptr_*` даёт 401,
|
|
||||||
workaround = JWT). GET `/api/stacks/20` (env 8/8) + `/api/stacks/20/file` (compose). Python urllib UTF-8
|
|
||||||
(НЕ PS Invoke-RestMethod — gotcha кириллицы #9). Замена `tandemmebel:0cd9351`→`8df10ee` в StackFileContent
|
|
||||||
(ровно 1 замена, traefik rule line не тронута). PUT `/api/stacks/20?endpointId=1` `{stackFileContent,
|
|
||||||
env: <8 preserved>, prune:false, pullImage:true}` → HTTP 200. Контейнер 8df10ee+healthy ~8s.
|
|
||||||
5. **Live-smoke С VDS** — robots/`/`/sitemap 200, /articles 404 parity, sitemap 184 locs, 4 share-block
|
|
||||||
страницы (project-post ×2 `/projects/2011/...`, `/furniture/bedrooms`, `/furniture/kitchens/classic`) все
|
|
||||||
200 → **vk+ok на месте, fb/tw/gp=0**. TLS-серт CN=tandemmebel.ru (LE YR2, until 2026-10-10) **не дёрнут**.
|
|
||||||
`docker rm -f tandemmebel-staging`.
|
|
||||||
|
|
||||||
### Rollback (8df10ee)
|
|
||||||
- **Образный (предпочт):** PUT стека 20 назад на `0cd9351` (0.42.1, {% order %} fix) — template-only фикс,
|
|
||||||
откатится чисто. Тег в registry.
|
|
||||||
- `ed96b18` (0.42.0) / `b02ca18` — в registry (ed96b18 стёрт с VDS при disk-cleanup 2026-07-13, `pullImage:true`
|
|
||||||
дотянет из registry).
|
|
||||||
- DNS: revert reg.ru → 80.64.31.36 (RUVDS IIS жив, не тронут).
|
|
||||||
|
|
||||||
### Hygiene-заметка
|
|
||||||
Portainer stack 20 file несёт устаревшие STAGING-комменты (строки 16-17 «Домен: STAGING», 53
|
|
||||||
«STAGING-RULE. Cutover: после flip DNS сменить на Host(...)») — но сама `traefik...rule` строка (55) уже LIVE
|
|
||||||
(`Host(tandemmebel.ru) || Host(www.tandemmebel.ru)`, совпадает с label'ом живого контейнера). Косметика,
|
|
||||||
функционально нейтральна. При деплое 8df10ee обновлён только image-line коммент; шапочные STAGING-комменты
|
|
||||||
не трогались (минимальное изменение). Source-of-truth compose `host-stacks/vds-kzntsv/tandemmebel.compose.yml`
|
|
||||||
актуализирован полностью (8df10ee + LIVE-комменты + история bump'ов). При следующем full-sync можно выровнять
|
|
||||||
Portainer file под git-source.
|
|
||||||
|
|
||||||
## Гочи (специфичные)
|
|
||||||
- **Блокер-паттерн консюмер-пина** — snolla-репо релизит версию, но консюмер-пин в tandemmebel-репо отстаёт. На 0.42.0 (2026-07-04) ждали dev-source; на 0.42.1 (2026-07-12) оператор сделал сам. Всегда byte-verify пин+yarn.lock+`ls-remote` перед build.
|
|
||||||
- **`yarn install --immutable`** в Dockerfile — корневой `yarn.lock` обязан быть снапшотом целевого пина, иначе build падает на checksum-mismatch. Бамп = package.json + `yarn install` (обновляет lock) + commit обоих.
|
|
||||||
- **0.42.x sitemap-реструктуризация** — parity сверять по **page-locs** (развёрнутый sitemapindex), НЕ по именам под-sitemap'ов. 184 тут (было 172 на 0.42.0 — выросло, но NEW==PROD).
|
|
||||||
- **`/articles` 404 benign** — loc из sitemap отдаёт 404, если прод-оракул идентичен → не дефект (см. рецепт § Completeness-gate).
|
|
||||||
- Staging-порт `5020` — проверять `docker ps | grep 127.0.0.1:50` перед bind (stale throwaway от прошлого тиража).
|
|
||||||
- Smoke гнать **С VDS** (воркстейшн ловит LAN-DNS-перехват прод-доменов — см. memory `workstation-lan-dns-serves-local-cms-copy`).
|
|
||||||
- build-secret (VERDACCIO_TOKEN в ARG/ENV build-стадии) — известный follow-up, НЕ блокер (runtime-стадия отдельная, токена в финальном образе нет).
|
|
||||||
|
|
||||||
## Тираж snolla 0.42.1 — статус
|
|
||||||
Все 5 snolla-сайтов на VDS:
|
|
||||||
- labtools.ru (17), emspb.ru (18), labtools.pro (19) — 0.42.1 in-place bump 2026-07-05.
|
|
||||||
- kupimknigi (21), **tandemmebel (20)** — 0.42.0 cutover, tandemmebel in-place bump до 0.42.1 2026-07-12. (kupimknigi остался на 0.42.0 по решению оператора — см. `NEXT_SESSION.md`.)
|
|
||||||
|
|
||||||
## 0.43.2 v3 cutover (aws-sdk v2→v3) — ✅ ВЫПОЛНЕН 2026-07-30
|
|
||||||
6-й сайт тиража v3 (после on-snolla, kupimknigi, labtools.ru, labtools.pro, emspb — 5 GREEN). In-place bump стека 20 (DNS на VDS с 2026-07-12, не трогали). Рецепт — [[minio-split-cutover-runbook]] §B3-v3. **6/6 v3 сайтов GREEN = тираж завершён.**
|
|
||||||
- **Bump:** `@snollajs/snolla 0.42.1→0.43.2` в `apps/web/package.json` (commit `9d51c09`) + `resolutions`-блок в root `package.json` (verbatim labtools.ru: `@aws-sdk/client-s3 3.1005.0` + `@smithy` chain 5.3.11 — optional determinism, live verify доказал 5.6.12 работает vs vds MinIO 2025) + реген `yarn.lock` (single `@smithy/signature-v4@5.3.11` deduped, no dual-instance). Pushed victor/tandemmebel.ru master `f4ddce6..9d51c09`. Endpoint fix `f4ddce6` (s3.endpoint→minio.vds.kzntsv.site) был в master с 2026-07-29, но в бегущем образе `8df10ee` (v2) его не было — v3 cutover заодно перевёл S3-direct read на vds minio.
|
|
||||||
- **Build на VDS:** `git archive 9d51c09` → `~/build/tandemmebel` → `docker build -f deploy/Dockerfile --build-arg VERDACCIO_TOKEN` → `registry.kzntsv.site/tandemmebel:9d51c09`(+latest), digest `sha256:d14982c24dc7c61a334c8973e6f1c48147c7d3cbcc772b28af6c629431947b16`, 476MB (v2 8df10ee был 583MB — v3 leaner), EXIT=0. `yarn install --immutable` в Docker прошёл (yarn.lock matches).
|
|
||||||
- **Staging :5020 parity-gate GREEN** (throwaway контейнер из env живого стека 20, 8 секретов, `--network proxy`):
|
|
||||||
- robots.txt md5 `a01e4c60…` byte-identical (87 B).
|
|
||||||
- sitemap index→5 sub-sitemaps → **184 locs == live 184 IDENTICAL** (0 prod-only / 0 new-only — regression нет).
|
|
||||||
- **self-consistency 183×200 + 1×404** (`/articles` — prod=404 parity benign, не регрессия). 0×301/5xx.
|
|
||||||
- `/` 200 staging 45738B vs prod 45181B (+557B = og:url/og:image request-host context — benign, byte-identical post-cutover).
|
|
||||||
- theme `Gotham-Pro.css` 200 4436B (v3 aws-sdk v3 S3-direct read из vds MinIO 2025 — латентный прод-баг 0B→4436B починен ещё cutover'ом 2026-07-12, сохранён).
|
|
||||||
- **Portainer PUT stack 20 LIVE:** node in-memory `put-stack` (НЕ PS — кириллица compose; Node 22 global fetch, GET `/stacks/20` для Env + `/stacks/20/file` для StackFileContent), env 8/8 preserved, `prune:false pullImage:true mem_limit 512m`. PUT 200. Контейнер `9d51c09` healthy ~9s.
|
|
||||||
- **Live smoke GREEN (via traefik):** `/` 200 45742B, `/projects` 200 90758B, `/sitemap.xml` 200 (184 locs == parity), robots md5 `a01e4c60…`, `/articles` 404 parity, theme `Gotham-Pro.css` 200 4436B (v3 S3-direct). TLS CN=tandemmebel.ru, expire Oct 10 2026 (не тронут — in-place swap). Лог чист (tedious deprecation + `tandemmebel.ru (snolla) listening`).
|
|
||||||
- **Rollback:** `tandemmebel:8df10ee` (0.42.1 v2, template-only share-buttons, жив в registry) — Portainer PUT назад / compose revert. `0cd9351` (0.42.1) + `ed96b18` (0.42.0) — deeper rollback. DNS→RUVDS 80.64.31.36 — крайний.
|
|
||||||
- Compose (admin source-of-truth) синхронно закоммичен `8df10ee`→`9d51c09`. **6/6 v3 сайтов GREEN — тираж v3 завершён.**
|
|
||||||
|
|||||||
3
.wiki/concepts/tg-digest-vds-deploy-runbook.md
Normal file
3
.wiki/concepts/tg-digest-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
|
**Не читать. Не править.** Канон — mappa wiki-сущность [[wiki:3320]] (concepts/tg-digest-vds-deploy-runbook). Скил: `admin-runbooks` / `mappa-knowledge`.
|
||||||
@@ -3,7 +3,7 @@ title: OpenWRT Router (192.168.1.1)
|
|||||||
type: entity
|
type: entity
|
||||||
tags: [hardware, networking, openwrt, nat, dhcp]
|
tags: [hardware, networking, openwrt, nat, dhcp]
|
||||||
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
sources: [../sources/nas-recovery-session-2026-05-18.md]
|
||||||
updated: 2026-05-19
|
updated: 2026-08-24
|
||||||
---
|
---
|
||||||
|
|
||||||
# OpenWRT Router
|
# OpenWRT Router
|
||||||
@@ -55,6 +55,30 @@ updated: 2026-05-19
|
|||||||
|
|
||||||
Дополнительные input rules для wan (стандартные OpenWRT): Allow-DHCP-Renew, Allow-Ping, Allow-IGMP, Allow-DHCPv6, Allow-MLD, Allow-ICMPv6-*, Allow-IPSec-ESP.
|
Дополнительные input rules для wan (стандартные OpenWRT): Allow-DHCP-Renew, Allow-Ping, Allow-IGMP, Allow-DHCPv6, Allow-MLD, Allow-ICMPv6-*, Allow-IPSec-ESP.
|
||||||
|
|
||||||
|
## DNS (https-dns-proxy / DoH)
|
||||||
|
|
||||||
|
- LAN DNS обслуживает dnsmasq (`192.168.1.1:53`), который форвардит на локальные `https-dns-proxy`: `127.0.0.1#5053` (resolver 1) и `#5054` (resolver 2).
|
||||||
|
- Резолверы после фикса 2026-08-24:
|
||||||
|
- `https://1.1.1.1/dns-query` (Cloudflare)
|
||||||
|
- `https://one.one.one.one/dns-query` (Cloudflare)
|
||||||
|
|
||||||
|
### ⚠️ Инцидент 2026-08-24: провайдер режет DoH
|
||||||
|
|
||||||
|
Весь DNS локальной сети умер: dnsmasq логировал `Maximum number of concurrent DNS queries reached (max: 150)`, у https-dns-proxy куча соединений в FIN_WAIT1 к 8.8.4.4:443 и 104.16.249.249:443, клиенты получали таймауты.
|
||||||
|
|
||||||
|
**Побочный эффект:** пока DNS был мёртв, роутер не мог резолвить NTP-пул (`0.openwrt.pool.ntp.org`) — часы стояли на ~2.5 месяца назад (Jun 12 при реальном Aug 24). После фикса DNS NTP доехал и время синхронизировалось. Проверить: `date` на роутере. Если DNS снова ляжет — ждать рассинхрона часов (логи/крона).
|
||||||
|
|
||||||
|
Проверено с роутера: **рабочие** — `https://1.1.1.1/dns-query` (HTTP 200, отвечает), UDP 8.8.8.8:53, UDP 1.1.1.1:53, ICMP. **Нерабочие** — `https://dns.google/dns-query`, `https://cloudflare-dns.com/dns-query` (таймаут TCP 443), `https://dns.quad9.net/dns-query`. Обычный TCP 443 наружу работает (1.1.1.1:443 → 301), т.е. блокировка точечная по DoH-резолверам.
|
||||||
|
|
||||||
|
**Фикс:**
|
||||||
|
```
|
||||||
|
uci set https-dns-proxy.@https-dns-proxy[0].resolver_url='https://1.1.1.1/dns-query'
|
||||||
|
uci set https-dns-proxy.@https-dns-proxy[1].resolver_url='https://one.one.one.one/dns-query'
|
||||||
|
uci commit https-dns-proxy
|
||||||
|
/etc/init.d/https-dns-proxy restart
|
||||||
|
/etc/init.d/dnsmasq restart # при необходимости; dnsmasq_config_update='*' обычно сам
|
||||||
|
```
|
||||||
|
|
||||||
## Config backup (с 2026-06-12)
|
## Config backup (с 2026-06-12)
|
||||||
|
|
||||||
Daily UCI/config backup → [[kreknin-synology]]:
|
Daily UCI/config backup → [[kreknin-synology]]:
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Production CMS (MoreThenCms) **остаётся на** [`windows-recovery-host`]
|
|||||||
|
|
||||||
| Hostname | Service | Auth | Назначение |
|
| Hostname | Service | Auth | Назначение |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `portainer.vds.kzntsv.site` | Portainer | vitya / `Pryakhin9-VDS-2026` (18 chars, см. [`portainer-2.21-admin-password-regression`](../concepts/portainer-2.21-admin-password-regression.md)) | Container management |
|
| `portainer.vds.kzntsv.site` | Portainer | vitya / `<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>` (18 chars, см. [`portainer-2.21-admin-password-regression`](../concepts/portainer-2.21-admin-password-regression.md)) | Container management |
|
||||||
| `traefik.vds.kzntsv.site` | Traefik dashboard | basicAuth vitya / Pryakhin9 | Traefik runtime view |
|
| `traefik.vds.kzntsv.site` | Traefik dashboard | basicAuth vitya / Pryakhin9 | Traefik runtime view |
|
||||||
| `git.kzntsv.site` | Gitea | kreknin users restored | Git hosting |
|
| `git.kzntsv.site` | Gitea | kreknin users restored | Git hosting |
|
||||||
| `verdaccio.kzntsv.site` | Verdaccio | kreknin htpasswd (vitya) | Private npm |
|
| `verdaccio.kzntsv.site` | Verdaccio | kreknin htpasswd (vitya) | Private npm |
|
||||||
@@ -144,3 +144,13 @@ DB TLS: self-signed certs (CN matches hostname), клиент с `verify-none` /
|
|||||||
- ntfy push — TODO ([`vds-ntfy-push`](../../.tasks/vds-ntfy-push.md)).
|
- ntfy push — TODO ([`vds-ntfy-push`](../../.tasks/vds-ntfy-push.md)).
|
||||||
- `iputils-ping` не установлен на host — `apt install iputils-ping` при следующем maintenance, иначе пользоваться `curl` для проверки сети.
|
- `iputils-ping` не установлен на host — `apt install iputils-ping` при следующем maintenance, иначе пользоваться `curl` для проверки сети.
|
||||||
- Hermes — defer, ждёт уточнения user.
|
- Hermes — defer, ждёт уточнения user.
|
||||||
|
|
||||||
|
## SSH-доступ (2026-08-25, критично!)
|
||||||
|
|
||||||
|
**`ssh -i ~/.ssh/id_ed25519 vitya@89.253.255.94`** — штатный вход (НЕ root!). sudo NOPASSWD для vitya.
|
||||||
|
- root login + password auth disabled пост-bootstrap (00-hardening.conf: PermitRootLogin no / PasswordAuthentication no)
|
||||||
|
- Источник: session:462. **Всегда пробовать vitya+id_ed25519 ПЕРВЫМ**, root — только через sudo vitya.
|
||||||
|
- root-пароль: `pass vds-kzntsv/root-pass-2026-08-25` (сменён Rusonyx 25.08, НЕ VDS_ROOT_PASS_INITIAL — неактуален)
|
||||||
|
- Ключ id_ed25519 дополнительно в /root/.ssh/authorized_keys (установлен 25.08 через docker)
|
||||||
|
- Portainer: `portainer.vds.kzntsv.site` + PORTAINER_API_KEY (pass vds-kzntsv/full-env) — полный docker-доступ, fallback без SSH
|
||||||
|
- Диагностика/восстановление через docker (Portainer): privileged-контейнеры с bind-mount/PidMode host (проверено 25.08)
|
||||||
|
|||||||
118
.wiki/index.md
118
.wiki/index.md
@@ -1,117 +1,3 @@
|
|||||||
# Wiki Index
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
Catalog of all wiki pages. One line per page, organized by type. Updated on every ingest / new page.
|
**Не читать. Не править.** Канон — mappa (`mcp__mappa__*`): wiki-сущности проекта, конвенции — AGENTS-сущность. Скил: `mappa-knowledge`.
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
- [overview.md](overview.md) — project overview
|
|
||||||
|
|
||||||
## Entities
|
|
||||||
|
|
||||||
- [books-vds](entities/books-vds.md) — Books VDS (89.253.255.133 — host4g.ru, client server hosting books app + shared mongo/minio/elasticsearch)
|
|
||||||
- [dead-synology-diskstation](entities/dead-synology-diskstation.md) — мёртвая Synology DiskStation (source NAS)
|
|
||||||
- [kreknin-synology](entities/kreknin-synology.md) — Kreknin Synology (backup target + DDNS)
|
|
||||||
- [nl-vds-3xui](entities/nl-vds-3xui.md) — NL VDS 3x-UI VLESS/Reality node (213.176.64.253) — Reality 443 сломан (PQ×intel), plain VLESS 32030 работает
|
|
||||||
- [de-vds-3xui](entities/de-vds-3xui.md) — DE VDS 3x-UI VLESS node (130.17.17.158, Fornex Germany) — plain VLESS 32030 security=none; 3x-ui 3.5.0 gotchas (CSRF на POST, client-tables, binary-only setting)
|
|
||||||
- [openwrt-router](entities/openwrt-router.md) — OpenWRT Router (192.168.1.1)
|
|
||||||
- [ruvds-iis-host](entities/ruvds-iis-host.md) — RUVDS IIS Host (Win Server 2025 Core, 80.64.31.36) — CMS catch-all destination
|
|
||||||
- [snolla-recovery-vm](entities/snolla-recovery-vm.md) — Snolla Recovery VM (VirtualBox, savestate'нута 2026-05-21 после 36h soak)
|
|
||||||
- [vds-kzntsv](entities/vds-kzntsv.md) — VDS kzntsv (Rusonyx 160 NVMe cloud server)
|
|
||||||
- [vdsina-outline-3xui](entities/vdsina-outline-3xui.md) — VDSina Amsterdam VPN (46.151.25.64) — Outline (основной, 11 keys, :443) + 3x-UI/Xray VLESS; основной боевой VPN семьи
|
|
||||||
- [windows-recovery-host](entities/windows-recovery-host.md) — Windows Recovery Host (рабочий PC пользователя)
|
|
||||||
|
|
||||||
## Concepts
|
|
||||||
- [books-vds-no-gitea-actions](concepts/books-vds-no-gitea-actions.md) — решение vitya 2026-08-23: Gitea Actions выпилены везде (books+vds-ops-mcp workflow удалены, раннер books-vds вычищен), сборка только локально (docker build+curl-upload, OCI-index→amd64 child), большие пуши через traefik умирают (Client Closed Request)
|
|
||||||
- [registry-traefik-buffering-gotcha](concepts/registry-traefik-buffering-gotcha.md) — большие (>1.5GB) пуши в registry через traefik buffering middleware (4GB) умирают: traefik буферизует body в RAM (1GB free) + OOM на books-vds; обход — curl монолитными PUT / PATCH стримом + finalize, drop_caches перед большим ридом
|
|
||||||
- [config-custom-env-mapping-in-mounted-dirs](concepts/config-custom-env-mapping-in-mounted-dirs.md) — npm config: env читается только если custom-environment-variables.json лежит в ПРИМОНТИРОВАННОМ конфиг-дире (маунт затеняет образ); mapping не создаёт свойство без env; проверка docker exec с правильным cwd
|
|
||||||
- [sched-runtime-to-file-managed-migration](concepts/sched-runtime-to-file-managed-migration.md) — перевод sched slovo+bookva на file-managed (durable config, #991): почему runtime хрупкий (расписания в памяти демона, сироты без data), рецепт миграции (файл in-place → live-sync НЕ конвертирует runtime → DELETE /tasks+/schedules → verify), gotchas (id=taskName vs UUID, ghost-таски, task-runner config кэш→рестарт, SMTP from==auth, URL-encode)
|
|
||||||
- [mappa-vds-deploy-runbook](concepts/mappa-vds-deploy-runbook.md) — деплой/редеплой mappa на VDS (стек 26, образ registry.kzntsv.site/mappa): БД+role на общем postgres:16 (pass mappa/full-env), сборка образа (schema.sql в dist!), Portainer-реденплой (docker pull до restart), env через Portainer, gotchas
|
|
||||||
- [sched-ozon-creds-stub-contract](concepts/sched-ozon-creds-stub-contract.md) — контракт кредов Ozon slovo/bookva: мисматч-пары (client_id ↔ чужой ключ) — НАМЕРЕННЫЕ заглушки (анти-утечка после tenant-split), матрица 4 sellers, безопасность через break-on-error (2a38fb5), verify по SHA2, НЕ «чинить»
|
|
||||||
- [omniroute-combo-deepseek-web-tuning](concepts/omniroute-combo-deepseek-web-tuning.md) — настройка web-комбо под агента: system_message убивает скилы pi, tool_filter_regex режет тулы, search-режим ломает tool-calling, 503 chat_admission_busy, эмуляция `<tool>` работает, план кастомного провайдера, qwen-web baxia-WAF стена
|
|
||||||
- [forgetmeai-free-web-chat-proxies](concepts/forgetmeai-free-web-chat-proxies.md) — семейство бесплатных web-chat прокси ForgetMeAI (DeepSeek/Qwen/GLM/Kimi): паттерн «обход капчи через реальную браузерную сессию», PoW-солвер DeepSeek, браузерный путь через baxia-WAF (fetch в page.evaluate), CloakBrowser, классификаторы капчи; карта «что берём в lwp»
|
|
||||||
- [sched-admin-client-openapi](concepts/sched-admin-client-openapi.md) — F&F раунды 2026-08-18: Python-клиент одной командой из спеки (openapi-generator 7.24.0); r1 0.1.2 — 24/24+smoke 19/19, находки F-1..F-5 (8 операций на raw-обходе); r2 0.2.0/0.3.5 — все 5 закрыты, raw_ops удалён, 11/11 typed-only, вердикт «claim подтверждён полностью»
|
|
||||||
- [openapi-generator-3-1-spec-gotchas](concepts/openapi-generator-3-1-spec-gotchas.md) — генерация клиентов из 3.1-спеки (openapi-generator 7.24.0, python): wrapped-ответы {} → ValidationError (raw-обход историчен: r2 спека фикснута), const:true → строковый enum, oneOf+discriminator → actual_instance=None (r2: плоская схема), java из PATH а не JAVA_HOME, InlineObjectN-имена, info.version не бьётся
|
|
||||||
- [nvm-junction-ismain-gate-gotcha](concepts/nvm-junction-ismain-gate-gotcha.md) — ESM isMain-гейт молча не выполняется под junction'нутым npm-рутом (nvm/mise/volta); realpath-фикс; диагностика за 30 сек
|
|
||||||
- [nvm-windows-node-switch](concepts/nvm-windows-node-switch.md) — смена версии Node на nvm-windows: junction, per-version глобалы, PATH-кэш, npm 11 allow-scripts; Node 22→24 evidence
|
|
||||||
- [sched-fnf-r8-verification-stand](concepts/sched-fnf-r8-verification-stand.md) — стенд верификации F&F r8 (schedule-as-entity): путь, версии core 0.40.2/ui 0.2.0, карта репро-скриптов (миграции/движок/политики/API 52/UI 13), подтверждённый контракт (ceiling, retryCount, snapshot, pause AND, tz-hoist, PATCH-merge), находки №5 (U1 форма-tz, U2 вёрстка), как закрыть раунд
|
|
||||||
- [sched-fnf-testing-method](concepts/sched-fnf-testing-method.md) — метод F&F-тестирования (урок r10): тест по СКОМПИЛИРОВАННОЙ доке (dev-сервер localhost:5108), не по сырцам; порядок хендоффа 1–6 (код → доки source парсящиеся → generate/dist → push ноль ahead → verdaccio → пинг), битый пример = возврат, schedjs.com = заглушка (деплой не требовать)
|
|
||||||
- [windows-docker-test-harness-gotchas](concepts/windows-docker-test-harness-gotchas.md) — грабли Windows-верификации против docker: rmSync до DatabaseSync (EBUSY), vitest 10s vs ~11s docker-DDL, монотонность фейк-часов, изоляция DB на сценарий, висельники node-процессов
|
|
||||||
- [snolla-smtp-mail-delivery-2026-08](concepts/snolla-smtp-mail-delivery-2026-08.md) — SMTP-инцидент 12–14.08: 525 (Яндекс отключил протоколы) → 554 (холодный ящик, мульти-получатели) → 550 (from not owned); фиксы + монитор на VDS + рецепт деплоя
|
|
||||||
- [keenetic-rci-api](concepts/keenetic-rci-api.md) — KeeneticOS RCI API: auth, чтение конфига, запись через CLI-parser, DNS-override против сломанного hairpin
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- [admin-api-embed-pilonuxt](concepts/admin-api-embed-pilonuxt.md) — admin-api embedded in pilonuxt /admin/api, stack 25 retired (2026-08-05)
|
|
||||||
- [pilonuxt-restart-vs-redeploy-2026-08-16](concepts/pilonuxt-restart-vs-redeploy-2026-08-16.md) — стек 16: рестарт (in-memory viewModel кэш content-api) vs пересборка (код); инцидент 500 на новинках (Content object vs array → core 0.26.7/admin-api 0.11.0); откат-цепочка образов 2026-08-16
|
|
||||||
- [namespace-consolidation-snolla](concepts/namespace-consolidation-snolla.md) — @snollajs → @snolla, 8 sites bumped, 17 packages (2026-08-04/05)
|
|
||||||
- [admin-infra-project](concepts/admin-infra-project.md) — design + migration plan для OpeItcLoc03/admin (canonical)
|
|
||||||
- [backup-inventory-2026-06](concepts/backup-inventory-2026-06.md) — карта всех машин × что реально бэкапится (с доказательством); дыры по приоритету (kreknin SPOF, nl-vds x-ui.db, bookva-*, openwrt) — триггер: MSSQL 3-нед gap
|
|
||||||
- [bindmount-config-edit-preserve-mode](concepts/bindmount-config-edit-preserve-mode.md) — правка bind-mounted config'а через mktemp+mv роняет режим 644→600 → non-root контейнер (uid 1000) EACCES crash-loop; chmod --reference=backup. + гоча: bind-mount затеняет config образа целиком → класть полную секцию, не дельту. Инциденты books-job-scheduler + task-runner 2026-06-18
|
|
||||||
- [vds-kzntsv-ssh-access](concepts/vds-kzntsv-ssh-access.md) — SSH access audit на vds-kzntsv (infra VDS), retained keys + add/revoke processes (was misnamed `books-ssh-access` до 2026-05-25 — content всегда был про vds-kzntsv)
|
|
||||||
- [compose-bcrypt-escape-trap](concepts/compose-bcrypt-escape-trap.md) — Docker Compose ест `$` в bcrypt hashes
|
|
||||||
- [db-tls-self-signed-via-traefik-raw-tcp](concepts/db-tls-self-signed-via-traefik-raw-tcp.md) — DB TLS через traefik raw TCP с self-signed certs
|
|
||||||
- [docker-host-loopback-detect](concepts/docker-host-loopback-detect.md) — Docker host-loopback detection — как доказать что host.docker.internal не петля
|
|
||||||
- [emspb-vds-deploy-runbook](concepts/emspb-vds-deploy-runbook.md) — emspb.ru snolla-app на VDS (стек 18): сборка/env/cutover 07-02 + **0.42.1 in-place bump 07-05** (образ `emspb:95a5c42`)
|
|
||||||
- [es-destructive-delete-incident-2026-05-26](concepts/es-destructive-delete-incident-2026-05-26.md) — ES indices wiped on canonical — **true root cause (2026-05-29): ransom-бот через открытый `0.0.0.0:9200` мимо traefik, free-ES без auth**; гипотеза operator-error опровергнута. Fix = убрать публикацию host-порта (Control #3) + restore. Exposure-audit: mongo/mariadb/minio тоже exposed (credentialed)
|
|
||||||
- [future-resilient-architecture-goals](concepts/future-resilient-architecture-goals.md) — fault-tolerance roadmap placeholder (расширяется через `[resilience-roadmap-design]`)
|
|
||||||
- [hyper-backup-structure-and-recovery](concepts/hyper-backup-structure-and-recovery.md) — Hyper Backup — структура репо и стратегия восстановления
|
|
||||||
- [iis-migration-2026-05-19-postmortem](concepts/iis-migration-2026-05-19-postmortem.md) — post-mortem миграции CMS на нативный IIS, 2026-05-19
|
|
||||||
- [labtools-vds-deploy-runbook](concepts/labtools-vds-deploy-runbook.md) — labtools.ru snolla-app на VDS (стек 17): сборка/env/cutover 07-02 (Yandex DNS!) + **0.42.1 in-place bump 07-05** (образ `labtools:566d41c`, order-фикс + sitemap починен)
|
|
||||||
- [labtools.pro-vds-deploy-runbook](concepts/labtools.pro-vds-deploy-runbook.md) — labtools.pro snolla-каталог на VDS (стек 19): сборка/env/cutover 07-02 + **0.42.1 in-place bump 07-05** (образ `labtools-pro:0610432`, order-парити 3 секции)
|
|
||||||
- [tandemmebel-vds-deploy-runbook](concepts/tandemmebel-vds-deploy-runbook.md) — tandemmebel.ru snolla-блог-портфолио на VDS (стек 20): cutover 07-12 (DNS reg.ru, LE) + **0.42.1 in-place bump 07-12** (образ `tandemmebel:0cd9351`, 184/184 parity, /articles 404 benign)
|
|
||||||
- [maljarka-vds-deploy-runbook](concepts/maljarka-vds-deploy-runbook.md) — maljarka.tandemmebel.ru snolla-лендинг на VDS (стек 23): restore 2026-07-31 (потерян при decomm RUVDS 2026-07-21, не был в v3 тираже). Клон on.snolla.com, siteId A2476738, layout.liquid byte-identical .NET-admin рендеру (43730B). Тираж 7/7
|
|
||||||
- [minio-imgproxy-on-vds](concepts/minio-imgproxy-on-vds.md) — MinIO+imgproxy stack на VDS, миграция с upgrade 2020→2025 + DNS swap pending
|
|
||||||
- [minio-split-cutover-runbook](concepts/minio-split-cutover-runbook.md) — разнос MinIO-контента: Track A books-vds 2020→2025 (shared+bookva) + Track B CMS-медиа → vds 2025. Progress: B1 миррор✅ B2 DNS-flip✅ B3 on-snolla v2-пилот✅ LIVE, B3v3 (snolla 0.43.2 опубликована, live verify ЗЕЛЁНЫЙ, resolutions optional) ⚡READY, B4/B5/Track A TODO. v3 — конечная цель.
|
|
||||||
- [morethencms-null-settingsdata-https-502](concepts/morethencms-null-settingsdata-https-502.md) — MoreThenCms тенант с `Sites.SettingsData=NULL` → 502 только на HTTPS (нет `httpSecure` → KeyNotFound); fix = UPDATE + recycle. maljarka 2026-06-08
|
|
||||||
- [snolla-admin-appdata-acl-500-after-scp-migration](concepts/snolla-admin-appdata-acl-500-after-scp-migration.md) — админский delete/upload ассета → 500: app pool имел на `App_Data` только RX после scp-миграции → `File.Delete` UnauthorizedAccessException. Fix = icacls Modify. + split-brain: админка пишет Local, боевой app читает MinIO
|
|
||||||
- [snolla-local-admin-and-on-snolla-migration-design](concepts/snolla-local-admin-and-on-snolla-migration-design.md) — design: Task A восстановить локальный .NET-админ (catch-all IIS `snolla`, hosts-override, MinIO drop-in) + Task B on.snolla.com → VDS как Node snolla-app
|
|
||||||
- [snolla-local-admin-https-binding](concepts/snolla-local-admin-https-binding.md) — реализация 2026-07-29: HTTPS на локальном IIS-админе `snolla` :443 (self-signed wildcard `*.snolla.com` в Trusted Root, без traefik) + pilorama98.snolla.com в hosts; рабочий entry-point `/admin/account/login` (голый `/admin` → canonical на public-домен, предсуществующее поведение CMS)
|
|
||||||
- [mssql-container-data-restore](concepts/mssql-container-data-restore.md) — MSSQL контейнер с восстановленными production data — паттерн
|
|
||||||
- [mssql-on-vds](concepts/mssql-on-vds.md) — MSSQL Express 2022 Linux на VDS — миграция + login orphan fix + traefik TCP gotchas
|
|
||||||
- [ocis-on-vds-deploy-recipe](concepts/ocis-on-vds-deploy-recipe.md) — oCIS на VDS — deploy recipe + non-obvious gotchas (UID 1001 vs 1000, basic auth, LibreGraph user-create)
|
|
||||||
- [portainer-2.21-admin-password-regression](concepts/portainer-2.21-admin-password-regression.md) — Portainer 2.21 `--admin-password` regression + min 12-char policy
|
|
||||||
- [portainer-stack-management-books-vds](concepts/portainer-stack-management-books-vds.md) — Portainer-managed stacks на books VDS — pattern application + migration log 2026-05-25
|
|
||||||
- [portainer-stack-management-vds](concepts/portainer-stack-management-vds.md) — Portainer-managed stacks на VDS — canonical pattern + migration script + stack-redeploy recipe (новый тег) + gotchas (вкл. PS 5.1 ISO-8859-1 коррапт кириллицы при API round-trip)
|
|
||||||
- [snolla-live-prod-inplace-image-bump](concepts/snolla-live-prod-inplace-image-bump.md) — переиспользуемый рецепт обновления образа на ЖИВОМ snolla-стеке VDS in-place (build→throwaway-staging-acceptance С VDS→env-preserving Portainer PUT→live-smoke); вкл. `put-stack.js`; отработан на тираже 0.42.1 (4 сайта: labtools/emspb/labtools.pro 2026-07-05 + tandemmebel 2026-07-12)
|
|
||||||
- [stostayer-web-deploy-runbook](concepts/stostayer-web-deploy-runbook.md) — деплой легаси web (`www.stostayer.ru`) на прод клиента: build→registry→Portainer-стек 16 через container-IP API с хоста; **деплой 0.3.23 (2026-08-13): calculator-баннер + стек-фикс**; гочи — один push без retry (VPN-IP банится хостером), offline-build не всегда проходит (linux-бинари esbuild/rollup не в cache → временный VERDACCIO_TOKEN ARG/ENV), Portainer API base=http://IP:9000 (порт обязателен)
|
|
||||||
- [stostayer-admin-minio-config](concepts/stostayer-admin-minio-config.md) — локальная .NET-админка stostayer (:8090) на stostayer MinIO: креды НЕ в pass (в stostayer.new config), endpoint `minio-api.stostayer.ru` (minio.stostayer.ru — не API-порт), region `us-west-1` (не local); fix 2026-07-22 — 3 замены в Web.config, админка=прод единое хранилище
|
|
||||||
- [stostayer-markdown-render-404-incident-2026-07](concepts/stostayer-markdown-render-404-incident-2026-07.md) — постмортем 2026-07-24: 404/500 на body_repair_service = двойной корень (snolla `markdown();` без присвоения + exact-pin-staleness — content-api `workspace:*` запёк `@snollajs/snolla: 0.7.4`, фиксовая 0.7.7 легла top-level, content-api юзал nested баговую). Фикс snolla 0.7.7/core 0.26.1 + content-api 0.8.2 `^0.7.7`; деплой 0.3.22 через pscp+load (без :443 push). Pre-build чек yarn.lock на single-snolla ловит класс. Cleanup `workspace-caret-interpackage-deps` (vitya scope)
|
|
||||||
- [proxy-debugging-test-the-real-client](concepts/proxy-debugging-test-the-real-client.md) — анти-паттерн: свои curl/standalone-тесты «работают», а боевой клиент пользователя нет; источник истины — реальный клиент
|
|
||||||
- [reality-pq-mldsa65-dest-incompatibility](concepts/reality-pq-mldsa65-dest-incompatibility.md) — REALITY+ML-DSA-65 (PQ) не работает с не-PQ dest (Akamai/intel шлёт HRR); fix = PQ-совместимый dest или выключить PQ; **caveat: снятие PQ ≠ рабочий Reality у GUI-клиентов**
|
|
||||||
- [recovery-architecture-snapshot](concepts/recovery-architecture-snapshot.md) — текущая recovery architecture (2026-05-19/21, attempt 2)
|
|
||||||
- [registry-gc-mount-and-modify-flag](concepts/registry-gc-mount-and-modify-flag.md) — Docker Registry GC mount layout + `-m` flag
|
|
||||||
- [registry-kzntsv-auth-model](concepts/registry-kzntsv-auth-model.md) — registry.kzntsv.site = standalone registry:2 + htpasswd Basic (бинарный доступ, НЕ Gitea-packages, нет per-repo ACL/robot-токенов); как заводить htpasswd-юзеров (hot-reload), GC через v2 DELETE; юзеры vitya + books-ci
|
|
||||||
- [registry-oci-image-index-gc](concepts/registry-oci-image-index-gc.md) — books-* образы в registry = OCI image-index (buildx multi-manifest): top-level `.config`=null, дата `.created` живёт в платформенном sub-manifest; наивный GC по top-level дате → null у всех → no-op (keepLastN не применяется). DELETE по index-digest, дата из sub. + **dangling-индексы**: 16/19 тегов books-web — тег жив, sub-manifest `MANIFEST_UNKNOWN` (вычищен прежним host-GC); date-GC защищает их как null-dated → не чистит, хотя они и есть мусор. Зафиксировано на books registryGc dryRun 2026-06-18
|
|
||||||
- [rusonyx-vps-onboarding-quirks](concepts/rusonyx-vps-onboarding-quirks.md) — Rusonyx VPS onboarding quirks (Astra Облако / myvm.rusonyx.ru)
|
|
||||||
- [traefik-acme-json-to-iis-cert-import](concepts/traefik-acme-json-to-iis-cert-import.md) — Экспорт LE certs из traefik acme.json в IIS (PFX + SNI bindings)
|
|
||||||
- [traefik-file-watch-wsl2-broken](concepts/traefik-file-watch-wsl2-broken.md) — Traefik file-watch broken под Docker Desktop Windows (WSL2 9p mount)
|
|
||||||
- [traefik-on-windows-docker-desktop](concepts/traefik-on-windows-docker-desktop.md) — Traefik на Windows Docker Desktop — нюансы
|
|
||||||
- [traefik-tcp-passthrough-vs-starttls](concepts/traefik-tcp-passthrough-vs-starttls.md) — Traefik TCP passthrough vs STARTTLS-protocols
|
|
||||||
- [vbox-windows-stability-tuning](concepts/vbox-windows-stability-tuning.md) — VirtualBox + Windows-гость — нюансы стабильности cross-hypervisor миграции
|
|
||||||
- [vds-kzntsv-dhcp-outage-2026-05-28](concepts/vds-kzntsv-dhcp-outage-2026-05-28.md) — DHCP outage 2026-05-28 на vds-kzntsv — диагностика + recovery runbook (link UP но без IPv4 → статика + тикет хостеру)
|
|
||||||
- [verdaccio-prune-semantics](concepts/verdaccio-prune-semantics.md) — Verdaccio prune semantics — proxied vs locally-published
|
|
||||||
- [verdaccio-restore-packument-desync](concepts/verdaccio-restore-packument-desync.md) — disaster-restore: тарболл на диске + древний packument → EEXISTS 409; fix = снять коллизирующий .tgz, не rm -rf каталог
|
|
||||||
- [verdaccio-token-lifecycle](concepts/verdaccio-token-lifecycle.md) — restart trap (ephemeral secret) + max_users:-1 pnpm 409 + JWT fix + yarn clients
|
|
||||||
- [yarn-npm-minimal-age-gate](concepts/yarn-npm-minimal-age-gate.md) — Yarn ≥4.16 client-side age-gate (YN0016 «quarantined») на версиях <24ч; глобальный, не per-scope; fix npmMinimalAgeGate:0
|
|
||||||
- [winacme-iis-owin-catchall-http01](concepts/winacme-iis-owin-catchall-http01.md) — win-acme HTTP-01 авто-renewal LE на IIS под OWIN-catch-all CMS — challenge в отдельном No-Managed-Code приложении + патч шаблона; заменяет ручной PFX-импорт
|
|
||||||
- [wd40efax-smr-cascade](concepts/wd40efax-smr-cascade.md) — WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
|
|
||||||
- [windows-server-2025-core-bootstrap](concepts/windows-server-2025-core-bootstrap.md) — Win Server 2025 Core (RUVDS) — bootstrap для IIS-хоста: default-blockers + transfer-методов матрица
|
|
||||||
- [mcp-init-resilience](concepts/mcp-init-resilience.md) — mcp-init-resilience
|
|
||||||
|
|
||||||
## Packages
|
|
||||||
|
|
||||||
<!-- (none yet) -->
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
- [omniroute-session-2026-08-22](sources/omniroute-session-2026-08-22.md) — хронология сессии настройки omniroute web-комбо (deepseek-web): корни вылетов, фиксы, тесты эмуляции, план кастомного провайдера
|
|
||||||
- [iis-host-migration-2026-05-19](sources/iis-host-migration-2026-05-19.md) — IIS Host Migration Session 2026-05-19 chronology
|
|
||||||
- [iis-migration-to-ruvds-2026-05-23](sources/iis-migration-to-ruvds-2026-05-23.md) — IIS migration to RUVDS — session 2026-05-23/24 (SSH/scp pivot, 25 SNI bindings, partial DNS cutover)
|
|
||||||
- [nas-recovery-session-2026-05-18](sources/nas-recovery-session-2026-05-18.md) — NAS Recovery Session 2026-05-18/19 chronology
|
|
||||||
- [books-vds-backup-daily-kreknin-2026-05-25](sources/books-vds-backup-daily-kreknin-2026-05-25.md) — books VDS daily backup → kreknin pipeline standup 2026-05-25 (ES file-snapshot repo + DB dumps + curl SMTP)
|
|
||||||
- [ruvds-backup-daily-kreknin-2026-05-24](sources/ruvds-backup-daily-kreknin-2026-05-24.md) — RUVDS daily backup → kreknin pipeline standup 2026-05-24 (rclone+SFTP, SYSTEM task)
|
|
||||||
- [nl-vds-3xui-setup-2026-06-05](sources/nl-vds-3xui-setup-2026-06-05.md) — NL VDS 3x-UI setup+troubleshooting session; итог: у реальных клиентов из РФ работает только plain VLESS 32030 (Reality/MTProto/SOCKS не поднялись)
|
|
||||||
- [de-vds-3xui-setup-2026-07-14](sources/de-vds-3xui-setup-2026-07-14.md) — DE VDS (Fornex Germany) setup session; 3x-ui 3.5.0 gotchas: CSRF на panel POST, прямой DB-insert не рендерит (client-tables), wrapper x-ui setting не применяет; plain VLESS 32030 confirmed на real RF-клиенте
|
|
||||||
- [vdsina-outline-3xui-inventory-2026-06-24](sources/vdsina-outline-3xui-inventory-2026-06-24.md) — VDSina Amsterdam (46.151.25.64) inventory: Outline 11 keys + 3x-UI VLESS, секреты в pass vdsina-outline
|
|
||||||
- [vds-kzntsv-bootstrap-2026-05-20](sources/vds-kzntsv-bootstrap-2026-05-20.md) — VDS bootstrap session 2026-05-20 chronology
|
|
||||||
- [vds-kzntsv-incident-2026-05-28](sources/vds-kzntsv-incident-2026-05-28.md) — DHCP outage incident session 2026-05-28 — chronology + ticket text
|
|
||||||
|
|||||||
152
.wiki/log.md
152
.wiki/log.md
@@ -1,151 +1,3 @@
|
|||||||
## [2026-08-23] ingest | NEW concepts/mappa-vds-deploy-runbook.md — ранбук деплоя/редеплоя mappa на VDS (стек 26, образ registry.kzntsv.site/mappa:0.1.0, БД на общем postgres:16, endpoint mappa.vds.kzntsv.site). #1012 закрыта; грабли: schema.sql в dist при tsc-сборке, docker pull до Portainer restart, не-ad-hoc compose, внешний DATABASE_URL для импорта. index.md updated (+1).
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
## [2026-08-19] ingest | NEW concepts/sched-fnf-testing-method.md — метод F&F-тестирования (урок r10): тест по скомпилированной доке (dev-сервер localhost:5108), порядок хендоффа 1–6, битый пример = возврат, schedjs.com заглушка. index.md updated (+1).
|
**Не читать. Не править.** Канон — mappa (`mcp__mappa__*`): wiki-сущности проекта, конвенции — AGENTS-сущность. Скил: `mappa-knowledge`.
|
||||||
## [2026-08-16] ingest | concepts/pilonuxt-restart-vs-redeploy-2026-08-16.md — день деплоев стека 16 (7 операций): рестарт vs пересборка разведены как отдельные операции. Рестарт сбрасывает in-memory viewModel кэш content-api (цена за м³ подтянулась после рестарта без пересборки); пересборка — только для кода. Анти-симптом: 500 на категориях с логом `@snolla/core viewModels/index.js:419 _.find(x=>x.name)` = код/данные (Content object vs array, admin-api create), рестарт не лечит — фикс snolla core 0.26.7 + admin-api 0.11.0 array-canon (1128ddc → 64c2539). Откат-цепочка образов 64c2539→…→8b7e8c4. +index.md. Монорепа-сторона заингестена прогером (lazy-data-not-found-404, admin-api-content-wire array-canon, cms-edits-cache-restart, v-html-on-component-ssr, admin-api-content-edit).
|
|
||||||
## [2026-08-14] decision | FormSubmissions.Content ₽→«?» — координация закрыта. ALTER на клиентской stostayer сделан (.admin); snolla опубликовала `@snolla/data@0.15.1` (STRING→TEXT=NVARCHAR(MAX), тесты 7/7); скан живых БД VDS: MoreThenCms+Staging уже nvarchar (ALTER не нужен), VDS-stostayer varchar — не прод, не трогаем (роль — к оператору); legacy stostayer.web на @snollajs 0.7.x — фикс не едет, ALTER покрывает (backport опция `065bb44`); формы есть у всех сайтов тиража (pilorama98 2516/labtools 916 — вопреки гипотезе snolla). Бонус: email-контракт задокументирован snolla (forms-email-contract.md), warn при пустом `to` — ждёт решения vitya. Таска snolla [formsubmissions-content-nvarchar] закрыта.
|
|
||||||
## [2026-08-14] implement | MSSQL stayer_site: ALTER FormSubmissions.Content varchar(max) cp1251 → NVARCHAR(MAX) (запрос их прогера: ₽→«?» в письмах с калькулятора). Verify до/после (nvarchar max). Live-тест не гонял, тестовые заявки #68485/#68486 не удалял (ждут подтверждения). Соединение: tedious из packages/web node_modules, MSSQL 91.222.236.225:1433 открыт снаружи.
|
|
||||||
## [2026-08-14] lint | inbox-rename cascade audit (idea 13): rg по живых инструкциях .admin — единственный хит log.md (op-log история, ок); stostayer.new — только маркеры «устаревший путь»; ack-письма в .agents/inbox подтверждены (.read); pi-расширение inbox-monitor (деплой+source of truth) на .agents/inbox, .claude-inbox не упоминается. Подтверждение оператору отправлено в .workshop/.agents/inbox.
|
|
||||||
## [2026-08-13] ingest | UPDATE concepts/stostayer-web-deploy-runbook.md — offline-гоча ПОЧИНЕНА их коммитом `5c583f8` (supportedArchitectures libc glibc/musl): сетевой yarn install на build-машине долил linux-зипы в кэш, будущие offline-сборки чисты. Runbook переписан с рецептом. Ack-обмен с stostayer.new (их инбокс `.agents/inbox/`).
|
|
||||||
## [2026-08-13] ingest | UPDATE concepts/stostayer-web-deploy-runbook.md — деплой stostayer-web:0.3.23 на прод www.stostayer.ru (запрос их прогера, письмо inbox). Нюансы зафиксированы: (1) offline-build НЕ проходит — в `.yarn/cache` нет linux-бинарей `@esbuild/linux-x64`/`@rollup/rollup-linux-x64-gnu` → обход через временный `ARG/ENV VERDACCIO_TOKEN` + `--build-arg` (после — revert); (2) Portainer API с хоста — `base=http://<IP>:9000` (порт обязателен); (3) прод-тег в таблице актуализирован 0.3.18→0.3.23; (4) блок деплоя 0.3.23. Task [stostayer-web-deploy-0-3-23] 🟢.
|
|
||||||
## [2026-08-12] implement | UPDATE entities/kreknin-synology.md — автосинк LE-серта: acme-nginx-sync.sh (cron 15 мин) ротирует серт git.kreknin.site из traefik acme.json в nginx DSM автоматически.
|
|
||||||
## [2026-08-12] implement | UPDATE entities/kreknin-synology.md — LE-серт git.kreknin.site (из traefik acme.json) подложен в nginx DSM reverse proxy. Из LAN: валидный LE + Gitea, без DSM-пароля (root SSH).
|
|
||||||
## [2026-08-12] implement | UPDATE entities/kreknin-synology.md — reverse proxy git.kreknin.site:443 → traefik:4443 на DSM nginx (server.ReverseProxy.conf w3conf + reload). Из LAN git.kreknin.site → Gitea (DNS-override + proxy), снаружи LE-серт. Осталось: LE-серт в DSM для git.kreknin.site (из LAN, без предупреждения).
|
|
||||||
## [2026-08-12] implement | NEW concepts/keenetic-rci-api.md — разобрана автоматизация KeeneticOS 5.1.3 (RCI: GET /rci/show/*, POST /rci/ массив parse-команд). Через API добавлены DNS-записи git.kreknin.site/kreknin.site → 192.168.1.43 (решение сломанного hairpin). Проверено с LAN: всё 200. Правила порт-форвардинга в 5.x лежат в ip.static (to-host=MAC).
|
|
||||||
## [2026-08-12] implement | UPDATE entities/kreknin-synology.md — Gitea 1.27.1 + postgres 16 на kreknin (для Алексея), traefik починен (restart policy), LE-серт git.kreknin.site, проброс 80/443→8000/4443 на Keenetic. KeeneticOS Viva API закрыт — правила в GUI. NAT loopback сломан → нужен DNS-override git.kreknin.site→192.168.1.43. Пароли: pass kreknin/keenetic + pass kreknin/gitea.
|
|
||||||
# Wiki op-log
|
|
||||||
## [2026-08-23] ingest | NEW concepts/sched-ozon-creds-stub-contract.md — контракт кредов Ozon slovo/bookva: мисматч-пары в обеих БД — намеренные заглушки (анти-утечка после tenant-split). Урок: .admin «починил» заглушку (slovo seller 1 ← bookva-ключ) → откат 12:07Z по подтверждению владельца; зеркальная заглушка bookva seller 2 (12:15Z). Безопасность: break-on-error 2a38fb5 задеплоен (фейл-фаст без спина). verify по SHA2 (d6227b = slovo-ключ, f700f3 = bookva-ключ). НЕ чинить. Связано: инцидент 2026-08-23, перевод slovo → file-managed (887bbae).
|
|
||||||
## [2026-08-14] ingest | NEW concepts/snolla-smtp-mail-delivery-2026-08.md — postmortem SMTP-инцидента 12–14.08: 525 SMTP disabled (Яндекс 360, доменка) → переезд на e-16513832@yandex.ru (app-password); 554 SPAM на 2+ получателях от холодного ящика → @snolla/mailer@0.9.0 per-recipient (forms-api 0.2.0, web 0.44.0); 550 from not owned → mailSettings.from во всех production.json + шаблон pilorama98checkoutconfirm. Монитор SMTP на VDS (cron 08:00, ntfy) + parity-чек Portainer env. Рецепт деплоя тиража (порядок, гочи: ad-hoc контейнер vs Portainer, regex-парсинг image, yarn up -R data).
|
|
||||||
|
|
||||||
Append-only log of wiki operations (ingests, promotions, lints, migrations).
|
|
||||||
|
|
||||||
## [2026-08-05] ingest | concepts/namespace-consolidation-snolla + concepts/admin-api-embed-pilonuxt — крупнейшие изменения сессии: неймспейс-миграция @snollajs→@snolla (17 пакетов, 8 сайтов бампнуты), admin-api embed в pilonuxt /admin/api, retire стека 25. +index.md строки.
|
|
||||||
|
|
||||||
## [2026-07-30] migrate | RELOCATE runbook `scripts/minio-split-cutover/runbook.md` → `concepts/minio-split-cutover-runbook.md` (дисциплина: runbook'и → `.wiki/concepts/`, не `scripts/`; `scripts/` для executable). +UPDATE content: v3-секция (snolla 0.43.2 опубликована, live MinIO verify ЗЕЛЁНЫЙ оба конфига, resolutions=optional determinism НЕ mandatory — regression-гипотеза v3 SigV4 vs MinIO НЕ воспроизвелась end-to-end, «CONFIRMED» framing откатан), креды нормализованы → `pass show minio-vds/full-env` (были plaintext в bash-примерах — wiki-конвенция «креды в pass, не в git»). +index.md строка. Старый `scripts/minio-split-cutover/` удалён (в нём кроме runbook'а ничего не было). Cross-ref [[on-snolla-vds-deploy-runbook]], [[minio-imgproxy-on-vds]], [[snolla-live-prod-inplace-image-bump]], [[../entities/vds-kzntsv]], [[../entities/books-vds]]. Related memory: admin `aws-sdk-v3-smithy-sigv4-minio-regression`, `aws-sdk-v2-minio-2025-works-no-smithy-pin`.
|
|
||||||
|
|
||||||
## [2026-07-29] implement | NEW concepts/snolla-local-admin-https-binding.md + UPDATE index.md (concepts +2 строки) — локальный .NET-админ snolla (catch-all IIS `snolla`) переведён на HTTPS. Повод: `https://pilorama98.snolla.com/admin` → 404 → ERR_CONNECTION_CLOSED. Корень 404: `pilorama98.snolla.com` не был в hosts-override → реальный DNS → VDS `89.253.255.94` → traefik без роута (системный прокси `127.0.0.1:10808` маскировал «server 127.0.0.1» в curl). pilorama98 — реальный сайт в `dbo.Sites` (Alias=pilorama98, PrimaryDomain=pilorama98.ru, Live), вне тиража-5 из design-дока. Корень ERR_CONNECTION_CLOSED: браузер HSTS-апгрейдил http→https, а локальный IIS без 443. Решение: **IIS-native self-signed wildcard `*.snolla.com` на :443 + Trusted Root**, НЕ traefik (локальный acme.json пуст → LE на snolla.com нет; traefik-as-TLS-terminator = NAT-loopback/docker.sock/URL-Rewrite/SERVER_PORT-leak готчи без выгоды). Скрипт elevated (UAC): `New-SelfSignedCertificate -Subject '*.snolla.com' -DnsName '*.snolla.com','snolla.com' -TextExtension EKU serverAuth` (гоча: `-DnsName` конфликтует с ручным SAN `2.5.29.17` → SAN через `-DnsName`) → копия в `Cert:\LocalMachine\Root` → `New-WebBinding snolla https 443 *` + `AddSslCertificate` + restart. Cert thumb `6E2212B4C13BD28E2FF680322F11741193E8C619`, SAN `*.snolla.com,snolla.com`, 10 лет — покрывает все alias'ы. hosts: добавлен `pilorama98.snolla.com` (elevated, построчно — `Get-Content -Raw` мог опустошить файл). Результат: `https://<alias>.snolla.com/admin/account/login` → 200 форма SNOLLA (pilorama98/kupimknigi/internal/tandemmebel верифицированы). **Голый `/admin` → 302 `/login` → 301 `https://www.<PrimaryDomain>/`** — предсуществующее поведение CMS (оператор подтвердил: «так и было» на RUVDS), не поломка миграции; рабочий entry-point `/admin/account/login`. `on.snolla.com` → 404: hosts стоит `internal.snolla.com` (реальный alias сайта `on` = `internal`), не `on` (design-док писал `on`). Trade-off: self-signed не LE — допустимо (админка локальная, FW закрыт). +index.md (2 строки: design-док ранее не был индексирован + новый page). Cross-ref [[snolla-local-admin-and-on-snolla-migration-design]], [[traefik-on-windows-docker-desktop]], [[stostayer-admin-minio-config]].
|
|
||||||
|
|
||||||
## [2026-07-22] fix | NEW concepts/stostayer-admin-minio-config.md — локальная .NET-админка stostayer (`C:\sites\stostayer\Web.config`, IIS :8090) переведена на рабочий stostayer MinIO. Исходно S3-провайдеры (`MoreThenCms.FileStorage.S3` в `bin/`) уже стояли, но с 3 багами: (1) чужие креды `AKIAJ2YJP72W6ZHCRE6Q` = books-vds/snolla root-ключ (копипаст из snolla catch-all admin, не заменён на stostayer-свои → "Access Key Id does not exist"); (2) `serviceURL=https://minio.stostayer.ru` — НЕ S3 API-порт ("S3 API Requests must be made to API port"), прод юзает `minio-api.stostayer.ru`; (3) `authenticationRegion="local"` → SigV4 регион-мismatch → `AmazonS3Exception: the region is wrong; expecting 'us-west-1'` (400) на `GalleriesStorage.EnsureKeyPrefixExists` (`ToolsController.ImagePreview` → `CreatePreview`). Креды stostayer MinIO **не в pass** — лежат в `~/projects/stostayer.new/packages/web/config/default.json` → `s3`: accessKey `stayer_minio`, endpoint `https://minio-api.stostayer.ru`, region `us-west-1`. Fix: 3 глобальные замены × 9 S3-блоков в Web.config (accessKey/secretKey[XML-escaped `>`/`&`]/serviceURL) + region `local`→`us-west-1`; бэкап `Web.config.bak-2026-07-22`; IIS recycle, лог чист. mc `alias set` + ListBuckets OK (10 бакетов: assets/galleries/themes/…); `assets/9cf0a8e52cf144619fd290606a146d35/` несёт реальные файлы. Acceptance оператором: картинки грузятся. Админка=прод единое хранилище, ручное зеркалирование в `App_Data` упразднено. +index.md concepts-строка. Cross-ref [[galleries-storage-class-local-not-s3]], [[snolla-admin-appdata-acl-500-after-scp-migration]], [[minio-imgproxy-on-vds]].
|
|
||||||
|
|
||||||
## [2026-07-14] ingest | sources/de-vds-3xui-setup-2026-07-14.md (new) + entities/de-vds-3xui.md (sources: привязан) — полноценный ingest DE-сессии: sources-хроника с борьбой против 3x-ui 3.5.0 (CSRF-403 на login → `CSRFMiddleware`/`X-CSRF-Token`/`GET {BP}csrf-token`; прямой DB-insert в `inbounds` не рендерит → client-tables `clients`/`client_inbounds`/`client_traffics` заполняются только `AddInbound` → panel API `POST {BP}panel/api/inbounds/add` с stringified settings/streamSettings/sniffing; wrapper `x-ui setting` не применяет креды → бинарник). Эталон JSON снят с рабочего nl-vds 32030. +index.md sources-строка. Real RF-client confirmed.
|
|
||||||
|
|
||||||
## [2026-07-14] decision | entities/de-vds-3xui.md — real-client из РФ подтверждён (user подключён через 32030, сессия идёт через узел). Plain VLESS = proven-working на обоих NL+DE. Задача [de-vds-3xui-setup] закрыта. Перекрестил `Проверено` блок ✅.
|
|
||||||
|
|
||||||
## [2026-07-14] ingest | entities/de-vds-3xui.md (new) — поднят VPN-VDS в Германии (Fornex `130.17.17.158`, Ubuntu 24.04, 1/2G/20G), аналог [[nl-vds-3xui]]. Повторён **только proven-working plain VLESS 32030** `security=none` (на NL у боевых клиентов из РФ заработал только он); Reality/MTProto/SOCKS5 намеренно НЕ подняты. Креды → `pass de-vds-3xui/full-env`. 3x-ui тут **3.5.0** (на NL была 3.2.7) — вскрыл новые гоча, зафиксированы в entity: (1) **CSRF на всех panel POST** (`CSRFMiddleware` → 403 пустое тело+CSP-nonce без заголовка `X-CSRF-Token`; токен — `GET {BP}csrf-token`; login = `POST {BP}login` JSON); (2) **прямой INSERT в `inbounds` НЕ рендерит инбаунд в 3.5.0** — клиентская модель разнесена по `clients`/`client_inbounds`/`client_traffics`, заполняется только сервисом `AddInbound` → инбаунд надо создавать через panel API `POST {BP}panel/api/inbounds/add` (body=model.Inbound camelCase, settings/streamSettings/sniffing stringified); (3) **wrapper `x-ui setting -username/-password` НЕ применяет** — только бинарник `/usr/local/x-ui/x-ui setting` (после `systemctl stop x-ui`). Server-side e2e подтверждён (xray слушает 32030, exit=сервер); реальный РФ-клиент ждёт проверки user. +index.md.
|
|
||||||
|
|
||||||
## [2026-07-12] deploy | tandemmebel.ru in-place bump 0.42.0→0.42.1 LIVE (стек 20) — поверх cutover'а того же дня (стек уже боевой, образ `ed96b18`/0.42.0). Консюмер-бамп сделан оператором сам (тот же блокер-паттерн 0.42.0 2026-07-04 — теперь решён без запроса dev-source): пин `apps/web/package.json:12` 0.42.0→0.42.1 + `yarn install` (lock snolla 0.42.1/core 0.24.1/liquid 0.10.2/data 0.14.1) + commit `0cd9351` + push origin (ls-remote confirmed). Verdaccio: 0.42.1 published (latest). Build на VDS → `registry.kzntsv.site/tandemmebel:0cd9351` (digest f29c187f). Throwaway-staging :5020 из env живого стека, healthy. Completeness-gate С VDS: **184/184 parity** (NEW==PROD, вкл. 0.42.x sitemap-реструктуризацию), единственный 404 `/articles` идентичен прод-оракулу → benign. Operator-gated PUT стека 20 (node put-stack.js, env 8/8 preserve, prune:false pullImage:true) → контейнер 0cd9351+healthy ~8s. Live-smoke GREEN (robots/home/projects/sitemap 200, TLS-серт CN не дёрнут). 0.42.1 = order-tag Drop-field fix, инертен на блог-портфолио (без e-commerce каталога). NEW concepts/tandemmebel-vds-deploy-runbook.md + UPDATE concepts/snolla-live-prod-inplace-image-bump.md (тираж 0.42.1 = 4 in-place bump-сайта) + host-stacks/vds-kzntsv/tandemmebel.compose.yml (тег+комментарий 0.42.1) + index.md. Rollback = тег ed96b18 в registry (+ b02ca18). **Тираж snolla 0.42.1 закрыт полностью.**
|
|
||||||
|
|
||||||
## [2026-07-12] deploy | tandemmebel.ru cutover LIVE — последний из тиража snolla 0.42.x. DNS reg.ru `tandemmebel.ru`+`www`→89.253.255.94 (verify ns1/ns2.reg.ru оба), traefik Host-rule staging→боевой на стеке 20 (env 8/8 preserve, prune:false, pullImage:false — образ `ed96b18`/0.42.0 не менялся, без пересборки по решению оператора). LE-серт issued on first hit: CN=tandemmebel.ru, SAN оба, until 2026-10-10. Live-smoke С VDS GREEN: key pages 200, sitemap 184 page-locs, sharp media webp 200, robots из БД. Gotham-Pro.css 0B→4436B (латентный прод-баг починен). Rollback = revert DNS→80.64.31.36 ИЛИ PUT staging-host. UPDATE entities/ruvds-iis-host (tandemmebel→🗑️ cutover VDS, биндинги оставлены как rollback) + host-stacks/vds-kzntsv/tandemmebel.compose.yml (rule боевой, заголовок live). **Тираж snolla 0.42.x полностью живой (5/5 стеков на VDS).**
|
|
||||||
|
|
||||||
## [2026-07-03] ingest | NEW concepts/snolla-admin-appdata-acl-500-after-scp-migration — разведка «почему `/admin/assets/<owner>/delete` → 500» на RUVDS IIS. Корень: app pool `IIS AppPool\snolla` (ApplicationPoolIdentity) имел на `C:\sites\snolla\App_Data` только `(RX)` после scp-миграции 23.05 → `File.Delete` в `AssetsService.DeleteAsset` кидает UnauthorizedAccessException → необработанное 500 (event log пуст, IIS substatus 0/win32 0). NRE-версии исключены проверкой строк в БД (Folders root + Files). Fix: `icacls App_Data /grant "IIS AppPool\snolla:(OI)(CI)(M)" /T` (44293 файла, verified `(I)(M)` на таргете). Класс шире delete — все Local-записи из админки (upload/кэши/галереи). Также: заменён `labtools-price.pdf` (owner d375c419) в ОБА хранилища (MinIO `assets/…` ETag→6426ddd0 + локалка IIS, MD5 сверены). Split-brain админка(Local)↔боевой app(MinIO) — cross-ref [[concepts/galleries-storage-class-local-not-s3]].
|
|
||||||
|
|
||||||
## [2026-06-29] deploy | pilorama98.ru (контейнер `pilonuxt`) `cf2bba2` LIVE (стек 16) — ADR-0010: формы переехали с monolith `@snollajs/snolla` на `@snollajs/forms-api`, snolla **дропнут целиком**, core@0.1.1 (Buffer вместо btoa). Тот же build-пайплайн (форс-регенерация клиента + save|ssh load + push с VDS). **Новый обязательный шаг — tree-check `.output/server/node_modules` ПЕРЕД перекатом** (вся сага content-api↔snolla про dual-instance, dev маскирует unmet-deps): `docker run --rm --entrypoint sh <img> -c 'find /app/.output/server/node_modules ...'` → подтвердил 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 (= btoa/snolla-дроп бандл не сломал), robots/yandex из БД (ADR-0009 не регресс), форма `POST /snolla-forms/forms?path=/checkout` пустой→422 JSON (valid НЕ слал — реальное письмо менеджеру), `/nope`→404. **Гоча smoke-харнеса:** forms-api отдаёт 422 только при `Accept: application/json` (иначе `prefersJson(req)` false → ветка `res.status(result.status||404).end()` даёт 404 — это не баг приложения, а отсутствие Accept в curl). Источник: наряд [deploy-pilonuxt-forms-api-drop-snolla]. См. [[concepts/portainer-stack-management-vds]] § Stack redeploy.
|
|
||||||
|
|
||||||
## [2026-06-29] deploy | pilorama98.ru (контейнер `pilonuxt`) `40bb383` LIVE (стек 16) — DB-backed staticPages + robots.txt (ADR-0009). Build из монорепы victor/pilorama98.ru: ключевой шаг — **форс-регенерация gitignored-клиента** `apps/web/src/generated/` (`rm -rf` + `yarn workspace nuxt-app schema-gen` против live MSSQL), иначе ensure-schema skip-if-present собрал бы старый клиент без `getStaticPage`/`getRobotsTxt` → server-500. Push с дома повторил traefik-499 на `.output`-слое даже при образе 425МБ (не multi-GB) → канонический fallback `docker save | ssh vds 'docker load'` + push с VDS (registry локален) сработал. Перекат через Portainer PUT стек 16 (`pullImage:true`, байты+UTF8 gotcha #9). Smoke acceptance 6/6 телами: robots полный из БД (Yandex Clean-param + Sitemap, не статика), yandex/google верификации 200, /nope→Nuxt-404, /+/catalog→200. Источник: наряд [deploy-pilonuxt-static-pages-robots]. См. [[concepts/portainer-stack-management-vds]] § Stack redeploy.
|
|
||||||
|
|
||||||
## [2026-06-24] ingest | entities/vdsina-outline-3xui.md (new) + sources/vdsina-outline-3xui-inventory-2026-06-24.md (new) — задокументирован ранее неизвестный VDS: VDSina Амстердам `46.151.25.64` (`v1621967.hosted-by-vdsina.ru`), «основной боевой VPN» семьи. Два стека на одной машине: Outline/Shadowbox (docker, :443 chacha20, 11 access-keys, mgmt :8080) + 3x-UI/Xray 25.10.15 (панель :50806 `/ecCqrtFVl4zl5kFjdj/`, VLESS xhttp :43666 / grpc :46743, все security=none). Ubuntu 20.04.6 (панель VDSina врёт «22»), 1 vCPU/1 GiB/30 GiB, up 17д, healthy. Секреты → `pass vdsina-outline/full-env` (host-key, root pass, Outline mgmt apiUrl+cert+11 keys, panel user+secret). Открытое: пароль панели — только bcrypt-хеш, plaintext не извлечён. Кросс-линк добавлен в [[nl-vds-3xui]] (другой NL-VDS, Aeza). +index.md (2 строки). Источник: SSH-инвентаризация 2026-06-24.
|
|
||||||
|
|
||||||
## [2026-06-18] ingest | concepts/registry-oci-image-index-gc.md (раздел real-run) — первый боевой registryGc dryRun:false на dangling-фиксе (books master-289c660, под добро юзера): 63 DELETE, 0 ошибок, 0×405 → REGISTRY_STORAGE_DELETE_ENABLED=true подтверждён живьём; 61 dangling + 2 datable снесены, реестр почищен (web 20→4 и т.д.). Побочка-урок: master у books-api/books-ops-mcp был dangling (не пересобирались ~3нед) → снесён → :master 404 (outage нет, контейнеры на локальных образах, но redeploy упрётся). Политика: не удалять named-теги master/latest даже dangling (protectRe → +master/latest); проверить keep/drop digest-коллизию. Источник: real run + post-state probe books-* :master.
|
|
||||||
|
|
||||||
## [2026-06-18] ingest | concepts/registry-oci-image-index-gc.md (раздел dangling-индексы) — перепрогон registryGc dryRun на descent-фиксе (master-dcd7c91) вскрыл 2-й root-cause: реестр засорён dangling image-индексами (тег жив, платформенный sub-manifest отдаёт MANIFEST_UNKNOWN — вычищен прежним host-side `registry garbage-collect`, не следящим index→child для multi-arch). books-web: 19 тегов → 3 датируемых, 16 dangling. date-GC защищает их как null-dated → не удаляет, хотя они и есть мусор (логика задом наперёд). Рекомендация books: различать transient-error (protect) vs MANIFEST_UNKNOWN (eligible). freedBytes от dangling ~0 (слои уже вычищены). +index.md hook. Источник: dryRun #2 + полный manifest-обход books-web 2026-06-18.
|
|
||||||
|
|
||||||
## [2026-06-18] ingest | concepts/registry-oci-image-index-gc.md (new) + concepts/bindmount-config-edit-preserve-mode.md (расширен) — следствие закрытия [books-task-runner-registry-auth-cred]. (1) Новый концепт: books-* образы в registry = OCI image-index (buildx), top-level `.config`=null, дата `.created` в платформенном sub-manifest → наивный GC по top-level дате даёт null у всех → планировщик защищает null-dated группы → drop=0 всегда (keepLastN не применяется). Правила: Accept со всеми media-types, дата из sub, DELETE по index-digest (не sub), 1 версия=1 index+attestation. Зафиксировано на books registryGc dryRun (deleted=0 при 14 tags). (2) bindmount-постмортем дополнен: bind-mount затеняет config-каталог образа целиком → нужна полная кред-секция, не дельта; + worked example task-runner registry-секция (mode не слетел, постмортем сработал). +index.md (2 строки). Источник: dryRun-верификация registryGc + манифест-dump books-web 2026-06-18.
|
|
||||||
|
|
||||||
## [2026-06-18] ingest | concepts/registry-kzntsv-auth-model.md (new) + concepts/bindmount-config-edit-preserve-mode.md (new) — registry.kzntsv.site = standalone registry:2 + htpasswd Basic (бинарный доступ, НЕ Gitea-packages; нет per-repo ACL/robot-токенов; hot-reload htpasswd; GC через v2 DELETE + host garbage-collect; завёл юзера books-ci для books job-scheduler pull, кред в pass vds-kzntsv/registry-books-ci). Постмортем: правка bind-mounted `default.json` через mktemp+mv уронила режим 644→600 → `books-job-scheduler` EACCES crash-loop (прод-даун slovo-cron), fix chmod/chown --reference=backup. +entities/vds-kzntsv.md (registry-строка) +index.md. Источник: консультация+инфра-деплой books registry-auth + инцидент 2026-06-18.
|
|
||||||
|
|
||||||
## [2026-06-17] ingest | concepts/stostayer-web-deploy-runbook.md (new) — ранбук деплоя легаси web на прод stostayer: канал build(offline)→push docker.stostayer.ru→Portainer-стек 16 через container-IP API (172.18.0.2:9000) с хоста мимо Angie BA; rollback; БЛОКЕР ESM-стена (top-level await @stostayer/data на build + ERR_REQUIRE_ESM @snollajs/snolla в рантайме d02f740) → 0.3.18 остаётся; гоча VPN-IP бан хостером при retry-push-шторме. Источник: попытка деплоя `stostayer-web-complaint-form-deploy` 2026-06-17 (откат на 0.3.18). +index.md.
|
|
||||||
|
|
||||||
## [2026-06-17] update | entities/windows-recovery-host.md — уточнён live-state двух stostayer-IIS-сайтов: добавлена таблица порт→connection string→БД (`stostayer` :8090→`www.stostayer.ru,1433` внешний прод + S3 minio.stostayer.ru; `stostayer.old` :8091→`mssql.kzntsv.site,1433` наш VDS), путь админок `/admin`, live-проверка 2026-06-17 (W3SVC Running, :8090/:8091 LISTENING, :80 нет). Пофикшена стейловая строка в «Ключевые папки» (stostayer.old conn localhost→mssql.kzntsv.site). Источник: live-аудит IIS на DESKTOP-NSEF0UK.
|
|
||||||
|
|
||||||
## [2026-06-17] update | concepts/portainer-stack-management-vds.md +раздел «Stack redeploy (новый image tag)» (JWT→GET file байтами→подмена тега→PUT pullImage:true, env=[] плейсхолдером, тело UTF-8) + gotcha #9 (PS 5.1 Invoke-RestMethod декодит /file как ISO-8859-1 → кириллические compose-комментарии mojibake → PUT падает `yaml: could not find expected ':'`; fix = байты+UTF8) + #10 (пустой env схлопывается). Источник: redeploy-pilonuxt-gsc-structured-data (19a4a84 в проде, stack 16).
|
|
||||||
|
|
||||||
## [2026-06-14] update | concepts/minio-imgproxy-on-vds.md +раздел «S3 access для клиентских приложений» (lookup: endpoint `https://minio.kzntsv.site` / сырой `89.253.255.133:9000` / inter `http://minio:9000`, root accessKey `AKIAJ...` + secret в `pass minio-vds/full-env`, форма ключа, ssl/region/pathStyle). Закрывает класс-вопрос «дай MinIO endpoint+креды» БЕЗ SSH. Прецедент: inbox snolla 2026-06-14 (raw-stream content-api), ответ отправлен.
|
|
||||||
## [2026-06-11] ingest | verdaccio-restore-packument-desync — postmortem: disaster-restore вернул тарболлы но древний/пустой packument → publish свежей версии EEXISTS 409. Fix: снять только коллизирующий целевой .tgz (бэкап first), republish; НЕ rm -rf каталог (убьёт locally-published историю). Применено к @snollajs/{data,mailer,numbering,content-api}.
|
|
||||||
## [2026-06-11] ingest | verdaccio-token-lifecycle — restart trap (ephemeral secret → 401), max_users:-1 + pnpm 409 постмортем, JWT config fix, yarn classic vs berry token refresh recipe.
|
|
||||||
## [2026-06-11] ingest | yarn-npm-minimal-age-gate — Yarn ≥4.16 client-side supply-chain gate: версии <24ч → YN0016 «quarantined». Независим от auth (gate #2 после 401). default 1d, читается глобально (per-scope не работает), fix npmMinimalAgeGate:0 / YARN_NPM_MINIMAL_AGE_GATE=0. Диагностировано с pilorama98.ru (контейнер `pilonuxt`); опровергнута первичная гипотеза «серверный карантин verdaccio».
|
|
||||||
## [2026-06-11] update | MSSQL backup gap closed — `/opt/stacks/backup/scripts/run.sh` расширен: 5 prod DBs через `docker exec mssql sqlcmd BACKUP DATABASE ... WITH COPY_ONLY, INIT` (Express, без COMPRESSION); .bak в bind-mount → mv в DUMP_DIR → rsync kreknin. Обновлён [[concepts/mssql-on-vds]] (TODO→реализовано).
|
|
||||||
|
|
||||||
## [2026-06-11] lint | 10 issues found, 8 fixed inline
|
|
||||||
- 🔴 recovery-architecture-snapshot.md: добавлена пометка ИСТОРИЧЕСКАЯ ЗАПИСЬ (рерайт текущей архитектуры отложен — большая задача)
|
|
||||||
- 🔴 snolla-recovery-vm.md: помечена УДАЛЕНА 2026-06-08, updated date исправлен
|
|
||||||
- 🟡 ruvds-iis-host.md: зачёркнуты 2 закрытых риска (imgproxy SPOF снят, LE renewal закрыт); добавлена ссылка на winacme; updated 2026-06-11
|
|
||||||
- 🟡 future-resilient-architecture-goals.md: dead task link cms-stopgap-backup-daily заменён на текстовое примечание
|
|
||||||
- 🟡 mssql-on-vds.md: исправлен frontmatter (status→type, добавлен updated)
|
|
||||||
- 🟡 vds-kzntsv.md: добавлен MSSQL в software stack table
|
|
||||||
- 🟡 orphan winacme-iis-owin-catchall-http01: входящая ссылка добавлена из ruvds-iis-host
|
|
||||||
- ✅ ДОФИКСИРОВАНО: 3 broken links в recovery-architecture-snapshot убиты; TODO mssql backup integration закрыт (реализован)
|
|
||||||
- ⚪ НЕ ФИКСИРОВАНО: orphan mssql-container-data-restore — оставлен
|
|
||||||
|
|
||||||
## [2026-06-08] update | entities/windows-recovery-host — декоммишн после Synology-recovery: снесены traefik/mssql/minio/imgproxy/es + 21 husk-стек + VM snolla-recovery (95ГБ) + C:\nas-recovery (~150ГБ) + IIS-сайт snolla + C:\sites\snolla + локальный MSSQL. Осталось: IIS stostayer/stostayer.old (репойнт на mssql.kzntsv.site, orphan-fix), lightrag, markitdown-MCP, mutable-dev VM. C: free 62→275ГБ (+~176ГБ docker pending vhdx-compact). image-pipeline SPOF снят. Источник: `.tasks/decommission-windows-recovery-host.md`.
|
|
||||||
|
|
||||||
## [2026-06-08] ingest | concepts/morethencms-null-settingsdata-https-502 — RCA+fix: `maljarka.tandemmebel.ru` отдавал 502 только на HTTPS (HTTP=200). Корень = `dbo.Sites.SettingsData=NULL` у тенанта maljarka (нет блока `httpSecure`) → `KeyNotFoundException` в `SnollaMiddleware`/`Owin.ErrorHandler` на HTTPS-ветке. Fix = `UPDATE dbo.Sites SET SettingsData='{httpSecure:...}'` + `Restart-WebAppPool snolla`; verified 443→200 server-local + external. Audit: maljarka — единственный NULL-сайт с :443-биндингом; rimiz degraded по другой причине. Обновлён [[../entities/ruvds-iis-host]]. Источник: live-диагностика 2026-06-08.
|
|
||||||
|
|
||||||
## [2026-06-05] ingest | concepts/winacme-iis-owin-catchall-http01 — постоянный self-renewing LE pipeline на RUVDS IIS (win-acme v2.2.9, 25-SAN cert, SYSTEM scheduled task). Главное: OWIN-catch-all CMS жрёт `/.well-known/acme-challenge/` → решено отдельным IIS-приложением в пуле «No Managed Code» + патч шаблона `Web_Config.xml` (snять Owin-handler). Заменяет ручной [[traefik-acme-json-to-iis-cert-import]] для renewal'а; снимает зависимость RUVDS от домашнего traefik. Закрывает дедлайн cert-expiry 2026-07-22 (новый cert до 2026-09-03). Источник: `.tasks/iis-migration-to-ruvds.md` Decisions log 2026-06-05.
|
|
||||||
|
|
||||||
## [2026-05-28] ingest | concepts/es-destructive-delete-incident-2026-05-26 — RCA для удаления 3 user-индексов на canonical ES `elasticsearch.kzntsv.site` (books VDS stack 33) во время cutover-prep. Caller identity unrecoverable (audit log = X-Pack платный, traefik accessLog был выключен). 2 preventive controls applied + verified: ES env `action.destructive_requires_name=true` + traefik JSON accessLog. Источник: `.tasks/restore-elasticsearch-indices-books-vds.md` § Closure note.
|
|
||||||
|
|
||||||
## 2026-05-21
|
|
||||||
|
|
||||||
- bootstrap: empty wiki skeleton (CLAUDE.md, index.md, log.md, overview.md, raw/README.md) committed
|
|
||||||
|
|
||||||
## [2026-05-21] ingest | concepts/admin-infra-project
|
|
||||||
|
|
||||||
## [2026-05-21] seed | CLAUDE.md Domain conventions — design-context pointers (task: admin-infra-project-pointers)
|
|
||||||
|
|
||||||
## [2026-05-21] migrate | subtree-import 6 entities + 17 concepts + 3 sources from MoreThenCms (history preserved via subtree-split + temp-prefix merge)
|
|
||||||
|
|
||||||
## [2026-05-21] regen | index.md catalog refresh after subtree import
|
|
||||||
|
|
||||||
## [2026-05-21] ingest | concepts/ocis-on-vds-deploy-recipe — oCIS deploy recipe + gotchas (UID mismatch, PROXY_TLS, PROXY_ENABLE_BASIC_AUTH, LibreGraph user-create); updated entities/vds-kzntsv (added owncloud row to stack/hostnames/file-layout); source: .tasks/owncloud-vds-deploy.md
|
|
||||||
|
|
||||||
## [2026-05-22] update | concepts/ocis-on-vds-deploy-recipe — Gotcha 5: 60s HTTP timeout caps slow-uplink uploads at ~200MB on 3 MiB/s link; traefik buffering NOT the fix (500 from oxy buffer); workaround = VDS-side curl PUT loopback via throwaway sftp key. Source: .tasks/owncloud-vds-deploy.md close-note.
|
|
||||||
|
|
||||||
## [2026-05-23] ingest | concepts/windows-server-2025-core-bootstrap — default-blockers (SMB closed, IIS/URL-Rewrite/.NET unverified) + transfer-методов матрица (RDP-redirect / SMB / WinRM / SFTP); recommendation = SMB inbound с source-IP whitelist на время migration. Source: failed-robocopy инцидент 2026-05-23 18:08 (.tasks/iis-migration-to-ruvds.md Decisions log).
|
|
||||||
|
|
||||||
## [2026-05-24] ingest | concepts/books-ssh-access — SSH access audit на shared VDS pre-cutover Фазы 3 tenant-split. Retained keys table (1 key, vitya core dev), removed cosmetic dead root key, sshd hardening verified via `sshd -T` (not raw config), fail2ban active 2670 failed/37 banned, only vitya@94.19.247.14 в access log last 7 days. Source: .tasks/books-ssh-audit-shared-vds.md.
|
|
||||||
|
|
||||||
## [2026-05-24] ingest | RUVDS IIS migration + backup pipeline — entities/ruvds-iis-host (NEW, 80.64.31.36 Win Server 2025 Core, 25 SNI bindings, 2/24 hostnames DNS-flipped) + sources/iis-migration-to-ruvds-2026-05-23 (NEW, SSH/scp pivot после SMB-block by home-ISP) + sources/ruvds-backup-daily-kreknin-2026-05-24 (NEW, rclone+SFTP SYSTEM task daily 04:30) + concepts/traefik-acme-json-to-iis-cert-import (NEW, PFX+SNI recipe) + UPDATE concepts/windows-server-2025-core-bootstrap (SMB deprecate, HTTP middlebox warning, HTTP/2 note, backup-strategy + cert-import закрыты) + UPDATE entities/windows-recovery-host (IIS partial-cutover state, imgproxy SPOF carve-out) + UPDATE overview (RUVDS line). Sources: .tasks/iis-migration-to-ruvds.md, .tasks/ruvds-backup-daily-kreknin.md.
|
|
||||||
|
|
||||||
## [2026-05-28] ingest | vds-kzntsv DHCP outage postmortem — concepts/vds-kzntsv-dhcp-outage-2026-05-28 (NEW, симптомокартина + диагностический алгоритм + recovery runbook + RCA) + sources/vds-kzntsv-incident-2026-05-28 (NEW, timeline 05:25 backup OK → 08:15 detect → 08:43 statics fix → 08:50 disk GC; ticket text) + UPDATE entities/vds-kzntsv (Доступ §+subnet/gw/hypervisor/DNS resolvers; pass-store вместо .common/secrets; Known issues §NEW with 2026-05-28 incident; Open issues bump GC priority + iputils-ping note) + UPDATE concepts/rusonyx-vps-onboarding-quirks (quirk #9 NEW — gw в /18 надсети + recovery commands). Live session, no raw source ingested.
|
|
||||||
|
|
||||||
## [2026-05-28] update | vds-kzntsv DHCP outage post-resolution — после reset хостером в 13:52 MSK выяснилось что root cause — конфликт двух сетевых стеков (netplan+networkd поверх ожидаемого provider's ifupdown). Их `start/ipadd` ожидает чистый ifupdown, не мог auto-recover при разрыве DHCP binding. Resolution: mask netplan+systemd-networkd, reboot, provider положил `/etc/network/interfaces.d/ifcfg-eth0` с /18 netmask. UPDATE concepts/vds-kzntsv-dhcp-outage-2026-05-28 (revised RCA + permanent-fix § + anti-pattern + revised lessons-learned) + UPDATE sources/vds-kzntsv-incident-2026-05-28 (timeline до 14:00 + final config) + UPDATE entities/vds-kzntsv (mask /18, ifupdown stack, kernel cmdline net.ifnames=0) + UPDATE concepts/rusonyx-vps-onboarding-quirks (quirk #9 переписан про /18, quirk #10 NEW про ifupdown vs netplan stack). Memory `vds-kzntsv-rusonyx-network-recovery` переписан с новыми фактами.
|
|
||||||
|
|
||||||
## [2026-05-29] update | ES destructive-delete RECURRENCE — true root cause найден, диагноз сменился. UPDATE concepts/es-destructive-delete-incident-2026-05-26 (correction-блок + Hypothesis помечена ОПРОВЕРГНУТА + новая секция «Рецидив 2026-05-29 — true root cause»: ransom-бот через открытый `0.0.0.0:9200` мимо traefik+basicAuth, ES 7.10 free без auth; `read_me`=BTC-выкуп; снос by-name мимо Control #1; accessLog пуст т.к. бот шёл прямо в :9200; firewalld bypass docker-publish; Control #3 = убрать публикацию host-порта via Portainer PUT stack 33; restore из daily-2026-05-25; exposure-audit таблица — mongo/books-db/bookva-db/minio/bookva-minio тоже exposed но credentialed). bookva не пострадал (bookva-es порт не публикует). Live incident response session.
|
|
||||||
|
|
||||||
## [2026-06-05] ingest | NL VDS 3x-UI node + Reality PQ×dest root-cause
|
|
||||||
- new entities/nl-vds-3xui.md (213.176.64.253, Ubuntu 24.04, 3x-UI 3.2.7 / Xray 26.6.1); creds -> pass nl-vds-3xui/full-env
|
|
||||||
- new concepts/reality-pq-mldsa65-dest-incompatibility.md (root cause: ML-DSA-65 PQ ClientHello X25519MLKEM768 + Akamai dest www.intel.com -> HRR -> handshake fail; fix = PQ-capable dest e.g. www.microsoft.com, verified via isolation matrix)
|
|
||||||
- index.md updated (entity + concept). Fix NOT applied — awaiting user command.
|
|
||||||
|
|
||||||
## [2026-06-05] ingest | NL VDS 3x-UI session — honest final state + lessons
|
|
||||||
- NEW sources/nl-vds-3xui-setup-2026-06-05.md (chronicle: Reality PQ fix, port 443→2053, mtg MTProto, SOCKS5, friend-client instr; HONEST outcome — у реальных клиентов из РФ работает только plain VLESS 32030)
|
|
||||||
- NEW concepts/proxy-debugging-test-the-real-client.md (anti-pattern: свои curl/standalone-тесты ≠ боевой клиент; overclaim «РКН режет 443» / MSS-clamp по MTU-догадке сломал коннект)
|
|
||||||
- REWROTE entities/nl-vds-3xui.md — убраны ложные «Reality verified/fixed», «mtg неотличим»; честная таблица статусов (32030 🟢, Reality 2053 / MTProto 8443 / SOCKS 47020 🔴 у клиентов); MSS-clamp снят
|
|
||||||
- UPDATE concepts/reality-pq-mldsa65-dest-incompatibility.md — caveat «снятие PQ ≠ рабочий Reality у GUI-клиентов» + source link
|
|
||||||
- index.md updated (2 new pages). Креды/ссылки — pass nl-vds-3xui/full-env.
|
|
||||||
|
|
||||||
## [2026-06-06] decision | nl-vds-3xui: +3 classic Shadowsocks inbounds (ports 32031/32/33) for Outline-app, per-key revocable; protocol e2e-tested OK, RF reachability unverified (SS DPI-blocked in RF); creds in pass; discovered undocumented VLESS:13027.
|
|
||||||
|
|
||||||
## [2026-06-10] ingest | concepts/mcp-init-resilience
|
|
||||||
|
|
||||||
## [2026-06-12] migrate | galleries pilorama98 → S3. NEW concepts/galleries-storage-class-local-not-s3.md (root: storageClient `galleries` = Local disk class, never in S3; snolla ждёт s3://galleries/<siteId>/<file> → 404). Путь A: bucket `galleries` создан, 301 файл (77 MiB) с RUVDS IIS залиты verbatim, imgproxy 200 verified с books-vds. UPDATE concepts/minio-imgproxy-on-vds.md (stale-баннер: pipeline на books-vds с 08.06, не windows-host). Task migrate-gallery-originals-to-s3 🟢.
|
|
||||||
|
|
||||||
## [2026-06-27] refactor | books-vds: документирован публичный bookva-db endpoint :33306 (+ slovo/bookva DB-exposure, mongo/ES internal) в § Доступ; закрыт wiki-drift по порту
|
|
||||||
|
|
||||||
## [2026-07-05] decision | тираж snolla 0.42.1 (live-prod in-place bumps). NEW concepts/snolla-live-prod-inplace-image-bump.md (переиспользуемый рецепт build→staging-acceptance-С-VDS→env-preserving Portainer PUT `put-stack.js`→live-smoke). UPDATE 3 рунбука секцией «0.42.1 in-place bump»: labtools-vds-deploy-runbook (стек 17, `labtools:566d41c`), emspb-vds-deploy-runbook (18, `emspb:95a5c42`), labtools.pro-vds-deploy-runbook (19, `labtools-pro:0610432`). FIX orphan: все 3 рунбука добавлены в index.md (не были каталогизированы). Tasks `[labtools-ru|emspb|labtools-pro-deploy-snolla-0-42-1]` 🟢.
|
|
||||||
|
|
||||||
## [2026-07-24] ingest | concepts/stostayer-markdown-render-404-incident-2026-07.md — постмортем инцидента 404/500 на body_repair_service (stostayer.ru). Двойной корень: (1) код-баг snolla `import markdown from 'markdown-it'; markdown();` → инстанс выброшен, `markdown.render` undefined, краш на markdown-поле-шаблонах (латентный ~3 мес, с коммита 1e3f897 2026-04-21); (2) exact-pin-staleness — `@snollajs/content-api@0.8.0` `workspace:*` запёк `@snollajs/snolla: 0.7.4` exact → фиксовая 0.7.7 легла top-level, content-api юзал nested баговую 0.7.4. Фикс: snolla 0.7.7 + core 0.26.1 (код) + content-api 0.8.2 `^0.7.7` (упаковка). Деплой stostayer-web:0.3.22 через pscp SSH:20435 + sudo docker load + Portainer PUT pullImage:false (без push в :443, бана нет). Pre-build чек yarn.lock single-snolla ловит класс. index.md updated (+1). Cleanup `workspace-caret-interpackage-deps` — vitya scope. Memory: stostayer-deploy-image-via-pscp-load-avoid-443-push.md.
|
|
||||||
## [2026-08-13] refactor | concepts/stostayer-web-deploy-runbook — letter path `.claude-inbox` → `.agents/inbox` (rename idea 13 cascade)
|
|
||||||
## [2026-08-18] ingest | NEW concepts/sched-fnf-r8-verification-stand.md — стенд верификации F&F r8 (schedule-as-entity): путь, версии core 0.40.1/storage-* 0.3.1, карта 15 репро-скриптов (миграции/движок/политики), подтверждённый контракт, как продолжить №4/№5. NEW concepts/windows-docker-test-harness-gotchas.md — переиспользуемые грабли Windows-верификации (rmSync-EBUSY, vitest-DDL-таймаут, монотонность фейк-часов, изоляция DB, висельники node). index.md updated (+2).
|
|
||||||
|
|
||||||
## [2026-08-18] ingest | UPDATE concepts/sched-fnf-r8-verification-stand.md — сессия №4/№5: admin-api-test.mjs (52/52, F1 tz-hoist + F3 PATCH-merge подтверждены на core 0.40.2), UI-стенд ui-backend.mjs + sched-ui serve :8081 + браузерный CDP (13/13), находки U1 (форма-tz interval/once → 400) и U2 (таблица шире контейнера ~58px). index.md hook обновлён.
|
|
||||||
## [2026-08-18] ingest | NEW concepts/nvm-junction-ismain-gate-gotcha.md — ESM isMain-гейт молча no-op под junction-префиксом npm-рута (C:\nvm4w\nodejs → realpath …\nvm\v22.22.0): import.meta.url резолвится в realpath, argv[1] держит литеральный → main() не выполняется, exit 0. Бьёт global-install под любым version-manager'ом (nvm/mise/volta). Фикс realpath-сравнения (sched bd38aab, daemon 0.3.2). NEW concepts/nvm-windows-node-switch.md — Node 22→24.19.0 рецепт: junction-свап, per-version глобалы, PATH-кэш shell'а, npm 11 allow-scripts. Оба из сессии CLI-F&F sched 2026-08-18. index.md updated (+2).
|
|
||||||
## [2026-08-18] ingest | NEW concepts/openapi-generator-3-1-spec-gotchas.md — из F&F openapi-client sched (2026-08-18): генерация Python-клиента из 3.1-спеки openapi-generator 7.24.0. Гочи: (1) ответы `{}` в спеке + wrapped-тело ({task/run/schedule:…}) → pydantic ValidationError, фикс — реальные схемы ответов, обход raw-транспорт + by_alias=True; (2) `const:true` → строковый enum ('true') ломает boolean; (3) oneOf+discriminator → actual_instance=None, контент только через raw JSON; Java 11+ обязателен (системная 8 падает), Windows excluded-порты 8090/8091 (EACCES), консоль cp1251 без →/«». Полные находки F-1..F-5: sched/.agents/inbox репорт 19:15Z; пример sched/examples/admin-client-python (24/24). index.md updated.
|
|
||||||
## [2026-08-18] ingest | NEW concepts/sched-admin-client-openapi.md — F&F раунд openapi-claim sched (по предписанию таски): Python-клиент одной командой (openapi-generator 7.24.0 из спеки verdaccio 0.1.2), сьют 24/24 + smoke 19/19 против published daemon 0.3.4 (:8127), вердикт «claim подтверждён с оговорками» (8 операций требуют raw-обхода). Код sched/examples/admin-client-python (e89e868), репорт — sched инбокс 19:15Z, находки F-1..F-5 (спеку не чинил). Таска [sched-admin-client-openapi] 🟢. Гочи генерации — concepts/openapi-generator-3-1-spec-gotchas.md. index.md updated.
|
|
||||||
## [2026-08-18] ingest | UPDATE concepts/openapi-generator-3-1-spec-gotchas.md + concepts/sched-admin-client-openapi.md — round 2 ретест (0.2.0/daemon 0.3.5): все 5 находок F-1..F-5 закрыты, raw_ops.py удалён, 11/11 ранее-сырых операций typed-only, сьют 24/24 + smoke 19/19 (коммит sched d658ff0, репорт sched инбокс 22:55Z). Новые грабли в окружение: CLI спавнит java из PATH а не JAVA_HOME; wrapped-ответы в components.responses → InlineObjectN; info.version не бьётся с версией пакета. index.md hooks обновлены.
|
|
||||||
|
|
||||||
## [2026-08-22] ingest | NEW concepts/omniroute-combo-deepseek-web-tuning.md + sources/omniroute-session-2026-08-22.md — сессия настройки web-комбо omniroute (deepseek-v4-flash/pro-web). Корни вылетов агента: (1) system_message override режет скилы pi, (2) tool_filter_regex ^(bash|computer)$ режет 337/339 тулов, (3) search-модель генерит DEEP_SEARCH-мусор + «Link reading unavailable», (4) quality validation режет стрим на type:error, (5) 503 chat_admission_busy = capacity веб-чата. Фиксы применены: system_message="", regex расширен (базовые+mcp__*), flash think-search→think, maxRetries 2→4, retryDelayMs→4000. Эмуляция tool-calling проверена тестами — исправна. План кастомного провайдера (вытащить executor+translator, без quality validation, persistSession). index.md updated (+2).
|
|
||||||
## [2026-08-22] ingest | UPDATE concepts/omniroute-combo-deepseek-web-tuning.md — qwen-web стена baxia-WAF (подзадача llm-web-proxy-qwen-web): FAIL_SYS_USER_VALIDATE/RGV587 punish-капча на любой не-браузерный запрос (Node-fetch все варианты заголовков + свежий cookie-jar; даже Chrome-CDP chats/new проходит, completion виснет). Капча привязана к device-фингерпринту (bx-umidtoken из sg-wum.alibaba.com), не к cookie. Зацепка: в контейнере omniroute рабочая версия executors/qwen-web.ts ≠ GitHub main. Подзадача 🟡 paused. index.md hook обновлён.
|
|
||||||
## [2026-08-22] ingest | NEW concepts/forgetmeai-free-web-chat-proxies.md + raw/forgetmeai-repo-analysis-2026-08-22.md — анализ аккаунта ForgetMeAI (8 репо, все — бесплатные web-chat прокси, та же ниша lwp). Главное: (1) паттерн «не решать капчу, а обходить реальной браузерной сессией» (Chrome-расширение-экспортёр кук/localStorage, cookie export, persistent-профиль); (2) PoW-солвер DeepSeek (lib/pow.js, WASM compile-cache) — в lwp уже портирован; (3) qwen через baxia-WAF работает ТОЛЬКО браузерным путём: fetch внутри page.evaluate (FreeQwenApi) — настоящий umidtoken+canvas-фингерпринт; наш вывод подтверждён; (4) CloakBrowser-заметки (патченый Chromium, humanize, persistent-профили, geoip), классификаторы капчи (Aliyun WAF/FRONTEND_CAPTCHA_REQUIRED/punish/purecaptcha). Референс-код закоммичен в lwp docs/reference/forgetmeai/. index.md updated (+1).
|
|
||||||
|
|
||||||
## [2026-08-23] ingest | NEW concepts/sched-runtime-to-file-managed-migration.md — sched slovo+bookva переведены на file-managed (durable config, #991 closed): инцидент 2026-08-23 (спин, runtime-расписания в памяти демона) → миграционный рецепт (файл in-place из git, live-sync ≤60с НЕ конвертирует runtime-таски, DELETE /api/tasks+/schedules, verify fileManaged:true/дубли 0/ручной run), gotchas (id=taskName vs UUID, ghost-таски, task-runner config кэш→рестарт, SMTP from==auth Yandex, URL-encode имён). SMTP-уроки линкуются на snolla-smtp-mail-delivery-2026-08 (не дублируются). index.md updated (+1).
|
|
||||||
## [2026-08-23] ingest | NEW 3 concepts (books deploy session): books-vds-no-gitea-actions (решение: Actions выпилены, сборка локально, процедура + gotcha больших пушей), registry-traefik-buffering-gotcha (Client Closed Request на >1.5GB пушах, обход curl-монолитами), config-custom-env-mapping-in-mounted-dirs (mapping-файл в mounted-дире обязателен). index.md updated (+3).
|
|
||||||
|
|||||||
@@ -1,26 +1,3 @@
|
|||||||
# admin — infrastructure agenda
|
# ⛔ Файловый канал закрыт
|
||||||
|
|
||||||
Operational + roadmap workspace for personal-and-client infrastructure stack:
|
**Не читать. Не править.** Канон — mappa (`mcp__mappa__*`): wiki-сущности проекта, конвенции — AGENTS-сущность. Скил: `mappa-knowledge`.
|
||||||
|
|
||||||
- **VDS** (`vds.kzntsv.site`, Rusonyx) — gitea, verdaccio, registry, shared DB park, ntfy, traefik+portainer
|
|
||||||
- **RUVDS IIS host** (`80.64.31.36`, Win Server 2025 Core) — CMS multi-tenant IIS site `snolla` (catch-all для 24+ hostnames), partial-cutover idёт 2026-05-24
|
|
||||||
- **NAS** (`kreknin`, Synology DSM) — backup target, Hyper Backup repo, secondary services
|
|
||||||
- **Windows recovery host** — раньше CMS prod IIS (мигрирует на RUVDS), всё ещё хостит imgproxy + `tandemmebel.ru` + parallel running docker/traefik как rollback
|
|
||||||
- **OpenWRT router** — NAT 80/443 → traefik (на windows-recovery-host)
|
|
||||||
|
|
||||||
## Current state
|
|
||||||
|
|
||||||
See `concepts/recovery-architecture-snapshot.md` (migrated 2026-05-21 from `MoreThenCms`).
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Build fault-tolerant infrastructure so the 2026-05-18 SMR-cascade incident does not repeat. Roadmap design in progress — see `concepts/future-resilient-architecture-goals.md` (migrated 2026-05-21) and active task `[resilience-roadmap-design]`.
|
|
||||||
|
|
||||||
## Related repos (out of admin)
|
|
||||||
|
|
||||||
- `OpeItcLoc03/vds-ops-mcp` — read-only docker ops MCP for VDS
|
|
||||||
- `OpeItcLoc03/synology-ops-mcp` — same for NAS
|
|
||||||
- `OpeItcLoc03/projects-meta-mcp` — cross-project state aggregator
|
|
||||||
- `OpeItcLoc03/agenda` — cross-project task meta-board
|
|
||||||
- `OpeItcLoc03/projects-wiki` — shared knowledge wiki
|
|
||||||
- `OpeItcLoc03/claude-skills` — global skill registry
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ VNC консоль изначально не открывалась. Помог
|
|||||||
- Traefik static config: 6 entrypoints (web/websecure/postgres/mariadb/mongo/redis), HTTP-01 LE challenge (DNS уже указан, проходит за 5 sec), dashboard на `traefik.vds.kzntsv.site` + basicAuth middleware из dynamic file provider.
|
- Traefik static config: 6 entrypoints (web/websecure/postgres/mariadb/mongo/redis), HTTP-01 LE challenge (DNS уже указан, проходит за 5 sec), dashboard на `traefik.vds.kzntsv.site` + basicAuth middleware из dynamic file provider.
|
||||||
- LE issued cert at first request (5 validators с разных AWS regions → 200 OK).
|
- LE issued cert at first request (5 validators с разных AWS regions → 200 OK).
|
||||||
|
|
||||||
**Portainer admin init — 4 итерации.** Hard min 12-char policy в Portainer 2.20+ (regression от 2.20.0+), CLI flag `--admin-password` + bcrypt **не bypass'ит** policy и фактически выходит broken (bcrypt сохраняется, но login fails). После проб с `$2y$`/`$2a$` prefix, single-quote escape, YAML list form, docker run direct — оказалось CLI flag тупо не работает в 2.21.5. Финал: голый `docker run`, без `--admin-password`, потом API admin/init с длинным паролем `Pryakhin9-VDS-2026` (18 chars). Деталь — [`portainer-2.21-admin-password-regression`](../concepts/portainer-2.21-admin-password-regression.md).
|
**Portainer admin init — 4 итерации.** Hard min 12-char policy в Portainer 2.20+ (regression от 2.20.0+), CLI flag `--admin-password` + bcrypt **не bypass'ит** policy и фактически выходит broken (bcrypt сохраняется, но login fails). После проб с `$2y$`/`$2a$` prefix, single-quote escape, YAML list form, docker run direct — оказалось CLI flag тупо не работает в 2.21.5. Финал: голый `docker run`, без `--admin-password`, потом API admin/init с длинным паролем `<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>` (18 chars). Деталь — [`portainer-2.21-admin-password-regression`](../concepts/portainer-2.21-admin-password-regression.md).
|
||||||
|
|
||||||
API key сгенерирован через `/api/users/<id>/tokens`, local docker endpoint создан через `POST /api/endpoints` form-data. Сохранён в `vds-kzntsv.env`.
|
API key сгенерирован через `/api/users/<id>/tokens`, local docker endpoint создан через `POST /api/endpoints` form-data. Сохранён в `vds-kzntsv.env`.
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ Pipeline:
|
|||||||
- **Hermes defer** — user explicit.
|
- **Hermes defer** — user explicit.
|
||||||
- **Registry — fresh install, no migration** — user mid-flight reversal (могу новых наделать).
|
- **Registry — fresh install, no migration** — user mid-flight reversal (могу новых наделать).
|
||||||
- **`HostSNI(*)` + no tls.* для всех DBs** — uniform config работает для всех protocols (STARTTLS + TLS-from-start).
|
- **`HostSNI(*)` + no tls.* для всех DBs** — uniform config работает для всех protocols (STARTTLS + TLS-from-start).
|
||||||
- **Portainer admin pass `Pryakhin9-VDS-2026` (18 chars)** вместо запрошенного `Pryakhin9` (9 chars) — Portainer 2.21+ hard min 12-char policy, `--admin-password` CLI flag broken в 2.20+. Сохранено в `vds-kzntsv.env`.
|
- **Portainer admin pass `<PORTAINER_ADMIN_PASS: pass vds-kzntsv/full-env>` (18 chars)** вместо запрошенного `Pryakhin9` (9 chars) — Portainer 2.21+ hard min 12-char policy, `--admin-password` CLI flag broken в 2.20+. Сохранено в `vds-kzntsv.env`.
|
||||||
|
|
||||||
## Архитектурное замечание
|
## Архитектурное замечание
|
||||||
|
|
||||||
34
AGENTS.md
34
AGENTS.md
@@ -1,4 +1,7 @@
|
|||||||
# AGENTS.md (canon)
|
# AGENTS.md (.admin) — указатель на канон
|
||||||
|
|
||||||
|
**Канон — mappa wiki-сущность `AGENTS` (.admin, wiki:2608).** Читать её через `wiki_get('.admin','AGENTS')`. Этот файл — только триггеры скилов для pi (грузит из cwd) + указатель. Содержимое правил — в wiki:2608, не дублировать здесь.
|
||||||
|
|
||||||
# Agent instructions. Each line is a trigger for an installed skill.
|
# Agent instructions. Each line is a trigger for an installed skill.
|
||||||
|
|
||||||
talk like a caveman
|
talk like a caveman
|
||||||
@@ -7,18 +10,31 @@ use task management system
|
|||||||
check across all projects
|
check across all projects
|
||||||
pull remote before work
|
pull remote before work
|
||||||
session handoff: read on start, write on end
|
session handoff: read on start, write on end
|
||||||
|
inbox monitor: raise on start
|
||||||
follow project discipline
|
follow project discipline
|
||||||
delegate to interns when allowed
|
delegate to interns when allowed
|
||||||
recommend, don't menu
|
recommend, don't menu
|
||||||
|
admin runbooks: use index before any prod op, execute by runbook, write/update runbooks
|
||||||
|
admin artifacts: project skills in .agents/skills/, runbooks in mappa wiki, scripts in .tmp/ or scripts/
|
||||||
we're on Windows
|
we're on Windows
|
||||||
|
|
||||||
# Secrets rule
|
# Session live-ingest
|
||||||
|
|
||||||
Все креды (SSH, БД, panel, BA) лежат в `pass` (password-store). **Перед поиском доступов — `pass ls` / `pass show <path>`, а не grep по вики или `~/.ssh/config`.**
|
`session sync: write to mappa` — opt-in для pi session-sync / CC-хука (w:2604): live-ingest сессий в mappa. Без opt-in сессии не пишутся в mappa (диагноз 2026-08-29: opt-in неполный → мусор mappa.ts:404 + live-ingest не поднят).
|
||||||
Серверы СТО Стайер: `stostayer/client` (`new.stostayer.ru:20435` — машина клиента, файлы 1С в `/var/from_1c/`), `stostayer/rusonyx`, `stostayer/client-wireguard`.
|
|
||||||
|
|
||||||
# VDS ops rule
|
<!-- mappa:canon-block (auto; do not edit) -->
|
||||||
|
## Mappa canon
|
||||||
Все docker-compose stacks на VDS управляются через Portainer (`https://portainer.vds.kzntsv.site`).
|
**Gates (canon/gates — shared):** краткая суть; полный текст — `wiki_get(slug='canon/gates', full=true)`
|
||||||
`ssh + docker compose up -d` на VDS — anti-pattern. См. [`portainer-stack-management-vds`](.wiki/concepts/portainer-stack-management-vds.md) для migration script + gotchas.
|
- Г1. Знание и артефакты → mappa, не файлы — durable-знание и артефакты проекта живут в mappa; файловые каналы (`.brainstorm/`, `.tasks/`, `.wiki/`) и «сохранить рядом с проектом» закрыты.
|
||||||
Исключения: `traefik` + `portainer` (management plane, ad-hoc compose).
|
- Г2. Контракт каналов — письмо (inbox) — носитель вердиктов/указаний/находок, полным телом; комментарии на тасках — короткий след. Адресация `about`/`to`/`thread` (XOR); lifecycle `ack`/`resolve`/`cancel`.
|
||||||
|
- Г3. Поиск до угадывания — не угадывать slug/роут: сначала `search`/`wiki_search`; `wiki_get` без project = shared; общая память — `search(mode='recall')`.
|
||||||
|
- Г4. Адресация и слаги — рефы полными именами (`[[task:N]]`/`[[wiki:slug]]`); слаги kebab-case, латиница; номера `task:N` выдаёт сервер.
|
||||||
|
- Г5. .mappa-гейт — папка участвует в mappa-операциях только с маркером `.mappa`; нет маркера → сказать человеку, мутации — отказ.
|
||||||
|
- Г6. Секреты — в mappa не пишутся (422); только `secret:<path>`-рефы, значения мимо.
|
||||||
|
- Г7. Degraded-режим — mappa недоступна: читать кэш `.mappa/` (canon/methodology/runbooks), мутации → `.mappa/pending/`; нет кэша → стоп, не импровизировать.
|
||||||
|
- Г8. Перед работой с вики/каноном — первым действием прочитать канон-блок AGENTS.md проекта.
|
||||||
|
- Г9. Живое состояние до заявления — статус заявлять только по свежему чтению mappa, не по памяти/кэшу/ответу create.
|
||||||
|
**Entity → runbook (runbooks/index — shared):** task → [[runbooks/tasks]] · wiki → [[runbooks/wiki]] · inbox → [[runbooks/inbox]] · **thread** → [[runbooks/threads]] · session → [[runbooks/session]] · search → [[runbooks/search]] · issue → [[runbooks/issue]] · **intent** → [[runbooks/intent]] · requirements → [[runbooks/requirements]] · plan → [[runbooks/plan]] · comment → [[runbooks/comment]] · tag → [[runbooks/tag]] · attachment → [[runbooks/attachment]] · release → [[runbooks/release]] · brainstorm → [[runbooks/brainstorm]] · agent → [[runbooks/agent-operator]] · repo → [[runbooks/repo-commit]] · project → [[runbooks/project]] · skill → [[runbooks/skill]] · entity-слой → [[runbooks/entity]] · sched → [[runbooks/sched-telemetry]]
|
||||||
|
**Methodology:** `methodology/kzntsv`
|
||||||
|
**Canon version:** 4
|
||||||
|
<!-- /mappa:canon-block -->
|
||||||
|
|||||||
18
daemon3.log
18
daemon3.log
@@ -1,18 +0,0 @@
|
|||||||
node:internal/modules/cjs/loader:1386
|
|
||||||
throw err;
|
|
||||||
^
|
|
||||||
|
|
||||||
Error: Cannot find module 'C:\Users\vitya\projects\.admin\daemon3.js'
|
|
||||||
at Function._resolveFilename (node:internal/modules/cjs/loader:1383:15)
|
|
||||||
at defaultResolveImpl (node:internal/modules/cjs/loader:1025:19)
|
|
||||||
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1030:22)
|
|
||||||
at Function._load (node:internal/modules/cjs/loader:1192:37)
|
|
||||||
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
|
|
||||||
at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
|
|
||||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
|
|
||||||
at node:internal/main/run_main_module:36:49 {
|
|
||||||
code: 'MODULE_NOT_FOUND',
|
|
||||||
requireStack: []
|
|
||||||
}
|
|
||||||
|
|
||||||
Node.js v22.22.0
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# schedd — опубликованный daemon из приватного реестра (verdaccio), как на проде.
|
# schedd — опубликованный daemon из приватного реестра (verdaccio), как на проде.
|
||||||
# @sched/daemon@0.9.0 (latest) + deps (core 0.48.0) — cancel-форвардинг фикс:
|
# @sched/daemon@0.12.1 (latest) + deps (core 0.53.0) — cancel-форвардинг + cancel-on-poll-timeout:
|
||||||
# диспетчер форвардит cancel к per-task раннеру (task.runner), нет хука → no-op.
|
# при poll-timeout sched шлёт POST /cancel воркеру до записи failed (фикс расхождения вердикт↔реальность).
|
||||||
# node >= 24 (engines у daemon; node:sqlite).
|
# node >= 24 (engines у daemon; node:sqlite).
|
||||||
FROM node:24-alpine
|
FROM node:24-alpine
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache git
|
||||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ARG VERDACCIO_TOKEN
|
ARG VERDACCIO_TOKEN
|
||||||
RUN npm config set //verdaccio.kzntsv.site/:_authToken=${VERDACCIO_TOKEN} \
|
RUN npm config set //verdaccio.kzntsv.site/:_authToken=${VERDACCIO_TOKEN} \
|
||||||
&& npm install -g @sched/daemon@0.9.0 --registry=https://verdaccio.kzntsv.site --no-audit --no-fund
|
&& npm install -g @sched/daemon@0.12.1 --registry=https://verdaccio.kzntsv.site --no-audit --no-fund
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
ENTRYPOINT ["schedd"]
|
ENTRYPOINT ["schedd"]
|
||||||
|
|||||||
@@ -7,9 +7,22 @@
|
|||||||
"url": "http://ozon-seller-builder:8080/run?async=1",
|
"url": "http://ozon-seller-builder:8080/run?async=1",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"envelope": true,
|
"envelope": true,
|
||||||
"auth": { "apiKey": "__SCHED_API_KEY__" },
|
"auth": {
|
||||||
|
"apiKey": "__SCHED_API_KEY__"
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"stages": ["hash", "patch", "diff", "classify", "generate", "fix", "build", "postman", "state", "publish"],
|
"stages": [
|
||||||
|
"hash",
|
||||||
|
"patch",
|
||||||
|
"diff",
|
||||||
|
"classify",
|
||||||
|
"generate",
|
||||||
|
"fix",
|
||||||
|
"build",
|
||||||
|
"postman",
|
||||||
|
"state",
|
||||||
|
"publish"
|
||||||
|
],
|
||||||
"dryRun": false,
|
"dryRun": false,
|
||||||
"readmeTemplate": "/app/data/package-readme.md"
|
"readmeTemplate": "/app/data/package-readme.md"
|
||||||
}
|
}
|
||||||
@@ -20,13 +33,28 @@
|
|||||||
"runner": "http",
|
"runner": "http",
|
||||||
"config": {
|
"config": {
|
||||||
"handler": "pipeline",
|
"handler": "pipeline",
|
||||||
"timeoutMs": 120000,
|
|
||||||
"url": "http://ym-client-builder:8080/run",
|
"url": "http://ym-client-builder:8080/run",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"envelope": true,
|
"envelope": true,
|
||||||
"auth": { "apiKey": "__SCHED_API_KEY__" },
|
"auth": {
|
||||||
|
"apiKey": "__SCHED_API_KEY__"
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"stages": ["fetch", "detect", "diff", "classify", "patch", "generate", "fix", "build", "test", "postmanGenerate", "postman", "publish", "githubDistro"],
|
"stages": [
|
||||||
|
"fetch",
|
||||||
|
"detect",
|
||||||
|
"diff",
|
||||||
|
"classify",
|
||||||
|
"patch",
|
||||||
|
"generate",
|
||||||
|
"fix",
|
||||||
|
"build",
|
||||||
|
"test",
|
||||||
|
"postmanGenerate",
|
||||||
|
"postman",
|
||||||
|
"publish",
|
||||||
|
"githubDistro"
|
||||||
|
],
|
||||||
"dryRun": false,
|
"dryRun": false,
|
||||||
"repo": "https://github.com/yandex-market/yandex-market-partner-api",
|
"repo": "https://github.com/yandex-market/yandex-market-partner-api",
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
@@ -39,15 +67,29 @@
|
|||||||
"remoteBase": "__PUBLISH_REMOTE_BASE__",
|
"remoteBase": "__PUBLISH_REMOTE_BASE__",
|
||||||
"githubApiBase": "__PUBLISH_GH_API_BASE__",
|
"githubApiBase": "__PUBLISH_GH_API_BASE__",
|
||||||
"notify": {
|
"notify": {
|
||||||
"ntfy": { "url": "http://ntfy", "topic": "ym-client-builder" },
|
"ntfy": {
|
||||||
"email": { "to": "vitya.kuznetsov@gmail.com", "endpoint": "http://unisender-mock:8080/ru/transactional/api/v1/email/send.json", "on": ["published", "report", "failed"] }
|
"url": "http://ntfy",
|
||||||
|
"topic": "ym-client-builder"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"to": "vitya.kuznetsov@gmail.com",
|
||||||
|
"endpoint": "http://unisender-mock:8080/ru/transactional/api/v1/email/send.json",
|
||||||
|
"on": [
|
||||||
|
"published",
|
||||||
|
"report",
|
||||||
|
"failed"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"timeoutMs": 900000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"on": ["failed"],
|
"on": [
|
||||||
|
"failed"
|
||||||
|
],
|
||||||
"onMissed": true,
|
"onMissed": true,
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"url": "http://alert-bridge:9090/webhook",
|
"url": "http://alert-bridge:9090/webhook",
|
||||||
|
|||||||
@@ -5,16 +5,51 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
mappa:
|
mappa:
|
||||||
image: registry.kzntsv.site/mappa:0.5.1
|
image: registry.kzntsv.site/mappa:v0.64.16
|
||||||
container_name: mappa
|
container_name: mappa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
mem_limit: 512m
|
mem_limit: 1g
|
||||||
networks:
|
networks:
|
||||||
- shared-dbs
|
- shared-dbs
|
||||||
- proxy
|
- proxy
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgres://mappa:${MAPPA_DB_PASS}@postgres:5432/mappa
|
DATABASE_URL: postgres://mappa:${MAPPA_DB_PASS}@postgres:5432/mappa
|
||||||
MAPPA_GITEA_TOKEN: ${MAPPA_GITEA_TOKEN}
|
MAPPA_GITEA_TOKEN: ${MAPPA_GITEA_TOKEN}
|
||||||
|
MAPPA_API_TOKEN: ${MAPPA_API_TOKEN}
|
||||||
|
MAPPA_EXPORT_ENABLED: ${MAPPA_EXPORT_ENABLED}
|
||||||
|
MAPPA_EXPORT_DIR: ${MAPPA_EXPORT_DIR}
|
||||||
|
# embedded sched (task:1651): инфра-джобы vitya (telemetry_aggregate/anomalies); git_index_sync — после task:1691
|
||||||
|
MAPPA_SCHED_RULES: ${MAPPA_SCHED_RULES}
|
||||||
|
MAPPA_TELEMETRY_ANOMALY_RULES: ${MAPPA_TELEMETRY_ANOMALY_RULES}
|
||||||
|
# per-repo таймаут git_index_sync (task:1288/1754): известные таймауты MoreThenCms/O_C-Phazerville
|
||||||
|
MAPPA_GIT_INDEX_REPO_TIMEOUT_MS: ${MAPPA_GIT_INDEX_REPO_TIMEOUT_MS}
|
||||||
|
# клиентский стек версий (task:2075/2084): latest mappa-setup / min-граница (клиент ниже → 426)
|
||||||
|
# env 10/10; согласовано с mappa: latest=0.2.0, min=0.1.0 (inbox:3372/3380)
|
||||||
|
MAPPA_CLIENT_LATEST: ${MAPPA_CLIENT_LATEST}
|
||||||
|
MAPPA_CLIENT_MIN: ${MAPPA_CLIENT_MIN}
|
||||||
|
# S3-бэкенд blob attachment (task:2123, wiki:3435/3309): MinIO mappa-attachments
|
||||||
|
# декларация обязательна — Portainer env-массив транслирует только её (gotcha v0.55.1)
|
||||||
|
MAPPA_ATTACHMENT_BACKEND: ${MAPPA_ATTACHMENT_BACKEND}
|
||||||
|
MAPPA_S3_ENDPOINT: ${MAPPA_S3_ENDPOINT}
|
||||||
|
MAPPA_S3_BUCKET: ${MAPPA_S3_BUCKET}
|
||||||
|
MAPPA_S3_REGION: ${MAPPA_S3_REGION}
|
||||||
|
MAPPA_S3_ACCESS_KEY: ${MAPPA_S3_ACCESS_KEY}
|
||||||
|
MAPPA_S3_SECRET_KEY: ${MAPPA_S3_SECRET_KEY}
|
||||||
|
# per-host креды git-провайдера (issue:30 creds-layer, task:2569): kreknin gitea
|
||||||
|
# JSON {host: {url, token}}; дефолт-креды (kzntsv) — MAPPA_GITEA_TOKEN выше
|
||||||
|
MAPPA_GIT_GITEA_HOSTS: ${MAPPA_GIT_GITEA_HOSTS}
|
||||||
|
# телеметрия → S3 (task:2124): агрегаты jsonl в mappa-telemetry (вместо type=metric)
|
||||||
|
MAPPA_TELEMETRY_BACKEND: ${MAPPA_TELEMETRY_BACKEND}
|
||||||
|
MAPPA_TELEMETRY_S3_BUCKET: ${MAPPA_TELEMETRY_S3_BUCKET}
|
||||||
|
volumes:
|
||||||
|
# git-бэкап (task:1430): каталог экспорта = рабочая копия vds-mappa-backup на хосте
|
||||||
|
- /root/vds-mappa-backup:/backup
|
||||||
|
# MCP-сессии персистентные (task:1680/issue:15, v0.40.1): стор сессий
|
||||||
|
# (/app/data/mcp-sessions.json) переживает редеплой контейнера
|
||||||
|
- /root/vds-mappa-data:/app/data
|
||||||
|
# code-access local-слой (task:2057): bare-репо gitea (git.kzntsv.site, тот же VDS),
|
||||||
|
# ro-маунт на дефолтный MAPPA_GIT_GITEA_ROOT=/data/gitea/repositories (git --git-dir, 0 клонов)
|
||||||
|
- /opt/stacks/gitea/data/git/repositories:/data/gitea/repositories:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8731/health"]
|
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8731/health"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
services:
|
services:
|
||||||
pilonuxt:
|
pilonuxt:
|
||||||
container_name: pilonuxt
|
container_name: pilonuxt
|
||||||
image: registry.kzntsv.site/pilonuxt:64c2539 # git-sha 64c2539 (2026-08-16: @snolla/admin-api 0.11.0 — array-canon Content=массив + D6 dirty-flag; was 1128ddc)
|
image: registry.kzntsv.site/pilonuxt:5a93b20 # git-sha 5a93b20 (task:2678 taxonomy canonical from page_path)
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
@@ -44,6 +44,7 @@ services:
|
|||||||
THEMES_BUCKET: themes
|
THEMES_BUCKET: themes
|
||||||
# CUTOVER 2026-06-15: боевой домен www.pilorama98.ru — аналитика ВКЛ
|
# CUTOVER 2026-06-15: боевой домен www.pilorama98.ru — аналитика ВКЛ
|
||||||
# (дефолт GTM-MNNXFJ6, env не задаём). На smoke-поддомене гасили `""`.
|
# (дефолт GTM-MNNXFJ6, env не задаём). На smoke-поддомене гасили `""`.
|
||||||
|
NODE_CONFIG: '{"s3":{"endpoint":"https://minio.vds.kzntsv.site","region":"us-east-1","forcePathStyle":true,"accessKeyId":"AKIAJ2YJP72W6ZHCRE6Q","secretAccessKey":"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"}}'
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
30
host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom
Normal file
30
host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# sched custom single-binary — embedded engine + admin API + UI morda + MCP + storage (MariaDB).
|
||||||
|
# Built on published @schedjs/* npm packages (artifacts that passed the gates).
|
||||||
|
# Runs custom-entry.mjs — one HTTP server :8080 routing /api, /mcp, / (morda).
|
||||||
|
# Auth at the boundary: everything EXCEPT /api/health + static UI assets requires Bearer SCHED_ADMIN_KEY.
|
||||||
|
#
|
||||||
|
# Build (context = this dir):
|
||||||
|
# docker build -f host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql host-stacks/vds-kzntsv/sched-custom
|
||||||
|
# Run:
|
||||||
|
# docker run -e MYSQL_URL=mysql://sched:pw@mariadb:3306/sched?ssl={"rejectUnauthorized":false} \
|
||||||
|
# -e SCHED_ADMIN_KEY=... -e SCHED_TASKS=/app/config/tasks.json \
|
||||||
|
# registry.kzntsv.site/sched-custom:0.12.1-mysql
|
||||||
|
|
||||||
|
FROM node:24-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN echo '{"name":"schedd","private":true,"packageManager":"yarn@4.18.0"}' > package.json \
|
||||||
|
&& corepack enable \
|
||||||
|
&& printf 'nodeLinker: node-modules\nnpmMinimalAgeGate: 0\n' > .yarnrc.yml \
|
||||||
|
&& CI=1 yarn add @schedjs/daemon@0.12.1 @schedjs/storage-mysql@0.4.0 @schedjs/admin-api@0.3.0 @schedjs/mcp@0.4.1 @schedjs/ui@0.3.0 mysql2@3 \
|
||||||
|
&& rm -f .yarnrc.yml \
|
||||||
|
&& yarn cache clean
|
||||||
|
|
||||||
|
ENV PATH="/app/node_modules/.bin:${PATH}"
|
||||||
|
COPY custom-entry.mjs /app/custom-entry.mjs
|
||||||
|
# Default tasks.json baked into the image (overridden on prod by a bind mount / SCHED_TASKS).
|
||||||
|
RUN echo '{"tasks":[]}' > /app/tasks.json
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME ["/data"]
|
||||||
|
ENTRYPOINT ["node", "/app/custom-entry.mjs"]
|
||||||
128
host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs
Normal file
128
host-stacks/vds-kzntsv/sched-custom/custom-entry.mjs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// sched custom single-binary: embedded engine + admin API + UI morda + MCP (Streamable HTTP) + storage (MariaDB/MySQL).
|
||||||
|
// One HTTP server on :8080, routed:
|
||||||
|
// /api/* → createAdminApi.handleRequest (health open, rest Bearer SCHED_ADMIN_KEY)
|
||||||
|
// /mcp → createMcpHttpHandler (Streamable HTTP MCP, projection of admin API)
|
||||||
|
// /sched-ui.bundle.js → static UI bundle
|
||||||
|
// / → UI morda (html)
|
||||||
|
// Engine via createDaemon (runs ticks + sync; we supply storage+adminApi ourselves on OUR server).
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
import { createDaemon } from '@schedjs/daemon';
|
||||||
|
import { createAdminApi } from '@schedjs/admin-api';
|
||||||
|
import { createMysqlStorage, dbNameFromUrl } from '@schedjs/storage-mysql';
|
||||||
|
import { createPool } from 'mysql2/promise';
|
||||||
|
import { AdminApiClient } from '@schedjs/mcp';
|
||||||
|
|
||||||
|
// Subpath imports are blocked by npm `exports` masks — import the dist modules by
|
||||||
|
// absolute file URL (a direct file reference, not a package specifier).
|
||||||
|
const { createMcpHttpHandler } = await import(pathToFileURL('/app/node_modules/@schedjs/mcp/dist/http.js').href);
|
||||||
|
const { mordaHtml, DEFAULT_UI_BUNDLE } = await import(pathToFileURL('/app/node_modules/@schedjs/ui/dist/morda.js').href);
|
||||||
|
|
||||||
|
const PORT = Number(process.env.SCHED_PORT ?? 8080);
|
||||||
|
const HOST = process.env.SCHED_HOST ?? '0.0.0.0';
|
||||||
|
const ADMIN_KEY = process.env.SCHED_ADMIN_KEY; // Bearer; unset → open (dev)
|
||||||
|
const TASKS = process.env.SCHED_TASKS ?? '/app/tasks.json';
|
||||||
|
|
||||||
|
// --- storage (MariaDB/MySQL) ---
|
||||||
|
const MYSQL_URL = process.env.MYSQL_URL;
|
||||||
|
if (!MYSQL_URL) throw new Error('MYSQL_URL is not set — export MYSQL_URL=<connstring>');
|
||||||
|
const pool = createPool(MYSQL_URL);
|
||||||
|
const storage = await createMysqlStorage(pool); // self-migrates on open
|
||||||
|
const dbName = dbNameFromUrl(MYSQL_URL);
|
||||||
|
|
||||||
|
// --- daemon: engine only (we own the http server) ---
|
||||||
|
// Programmatic alerts (from env), NOT the tasks.json block: the published @schedjs/daemon 0.12.1
|
||||||
|
// does not wire onRunFinal from the file alerts block reliably (books-vds uses a books-built
|
||||||
|
// daemon where it does). Passing `alerts` explicitly guarantees the onRunFinal hook is set.
|
||||||
|
const ALERT_WEBHOOK = process.env.SCHED_ALERTS_WEBHOOK_URL;
|
||||||
|
const ALERT_BASIC = process.env.SCHED_ALERTS_BASIC_AUTH; // base64 `user:pass`, optional
|
||||||
|
const daemon = createDaemon({
|
||||||
|
storage,
|
||||||
|
tasksPath: TASKS,
|
||||||
|
...(ALERT_WEBHOOK
|
||||||
|
? { alerts: {
|
||||||
|
on: ['failed'],
|
||||||
|
onMissed: true,
|
||||||
|
onSyncFailed: true,
|
||||||
|
webhook: {
|
||||||
|
url: ALERT_WEBHOOK,
|
||||||
|
...(ALERT_BASIC ? { headers: { Authorization: `Basic ${ALERT_BASIC}` } } : {}),
|
||||||
|
},
|
||||||
|
} }
|
||||||
|
: {}),
|
||||||
|
// no `admin` here — createDaemon would bind its own /api-only server; we route ourselves
|
||||||
|
});
|
||||||
|
await daemon.start();
|
||||||
|
const engine = daemon.engine;
|
||||||
|
const adminApi = createAdminApi({ engine, storage, auth: ADMIN_KEY ? { apiKey: ADMIN_KEY } : undefined, version: '0.12.1-custom' });
|
||||||
|
|
||||||
|
// --- MCP: projection of admin API via HTTP client to ourselves ---
|
||||||
|
// AdminApiClient appends paths like '/tasks' to baseUrl → must end at the /api mount point.
|
||||||
|
const ADMIN_BASE = `http://127.0.0.1:${PORT}/api`;
|
||||||
|
const mcpClient = new AdminApiClient(ADMIN_BASE, ADMIN_KEY);
|
||||||
|
const mcpHandler = createMcpHttpHandler({ client: mcpClient, readonly: process.env.SCHED_MCP_READONLY === '1' });
|
||||||
|
|
||||||
|
// --- static UI bundle bytes ---
|
||||||
|
let uiBundle = null;
|
||||||
|
try { uiBundle = readFileSync(DEFAULT_UI_BUNDLE); } catch { /* bundle missing → morda 404s its script */ }
|
||||||
|
|
||||||
|
// One HTTP server, auth at the boundary. OPEN (no token): /api/health (probe) + the UI itself
|
||||||
|
// (/ and /sched-ui.bundle.js — static login page/JS bundle, ZERO data; data comes from /api/*).
|
||||||
|
// AUTH required (Bearer SCHED_ADMIN_KEY): /api/* (except health) and /mcp — the actual data plane.
|
||||||
|
// This is the vitya correction: morda stays reachable (it's just the login form), data plane is locked.
|
||||||
|
const AUTH_OPEN_PATHS = ['/api/health', '/sched-ui.bundle.js'];
|
||||||
|
function isOpenPath(p) {
|
||||||
|
return AUTH_OPEN_PATHS.includes(p) || p === '/'; // UI root = static login page, no data
|
||||||
|
}
|
||||||
|
function isAuthorized(req) {
|
||||||
|
if (!ADMIN_KEY) return true; // dev mode, open
|
||||||
|
const h = req.headers.authorization;
|
||||||
|
return h === `Bearer ${ADMIN_KEY}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
||||||
|
const p = url.pathname;
|
||||||
|
try {
|
||||||
|
if (!isOpenPath(p) && !isAuthorized(req)) {
|
||||||
|
res.writeHead(401, { 'content-type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (p === '/api' || p.startsWith('/api/')) {
|
||||||
|
req.url = p.replace(/^\/api/, '') + url.search; // strip /api mount prefix
|
||||||
|
await adminApi.handleRequest(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (p === '/mcp' || p.startsWith('/mcp/')) {
|
||||||
|
await mcpHandler(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (p === '/sched-ui.bundle.js' && uiBundle) {
|
||||||
|
res.writeHead(200, { 'content-type': 'application/javascript' });
|
||||||
|
res.end(uiBundle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// UI morda for everything else (root + non-api paths)
|
||||||
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||||
|
res.end(mordaHtml({ apiBase: '/api', bundleSrc: '/sched-ui.bundle.js', tokenKey: 'sched-token' }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'content-type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, HOST, () => {
|
||||||
|
process.stderr.write(`sched custom up: http://${HOST}:${PORT} (storage=mysql db=${dbName}, admin_auth=${ADMIN_KEY ? 'on' : 'OFF'}, mcp=on)\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful stop on SIGTERM
|
||||||
|
for (const sig of ['SIGTERM', 'SIGINT']) {
|
||||||
|
process.on(sig, () => {
|
||||||
|
daemon.stop();
|
||||||
|
server.close();
|
||||||
|
pool.end();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
96
host-stacks/vds-kzntsv/sched-workers.compose.yml
Normal file
96
host-stacks/vds-kzntsv/sched-workers.compose.yml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# sched-workers — HTTP-воркеры пайплайнов клиентов @apilki (VDS, рядом с ядром sched).
|
||||||
|
# Ядро sched: стек 28 (sched.compose.yml). Воркеры — отдельный стек в той же сети proxy,
|
||||||
|
# sched ходит к ним по имени сервиса из sched.tasks.json.
|
||||||
|
# Моки: verdaccio.kzntsv.site (npm), git.kzntsv.site (Gitea = мок-GitHub, орга apilki),
|
||||||
|
# ntfy (VDS-сервис в proxy), unisender-mock (локальный мок писем в этом стеке).
|
||||||
|
# Deploy: Portainer-managed (см. portainer-stack-management-vds). Env через Portainer (не env_file).
|
||||||
|
# Source-of-truth: host-stacks/vds-kzntsv/sched-workers.compose.yml.
|
||||||
|
|
||||||
|
services:
|
||||||
|
ym-client-builder:
|
||||||
|
image: registry.kzntsv.site/ym-client-builder:0.1.2
|
||||||
|
container_name: ym-client-builder
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 512m
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
environment:
|
||||||
|
PORT: "8080"
|
||||||
|
SCHED_API_KEY: ${SCHED_API_KEY}
|
||||||
|
UNISENDER_API_KEY: mock-key
|
||||||
|
UNISENDER_FROM: noreply@snolla.com
|
||||||
|
NPM_TOKEN: ${NPM_TOKEN}
|
||||||
|
GITHUB_TOKEN: ${GITHUB_TOKEN}
|
||||||
|
# мок-publish: verdaccio (npm) + Gitea (репо/релизы) — без выхода в npmjs/github.com
|
||||||
|
PUBLISH_REGISTRY: ${PUBLISH_REGISTRY}
|
||||||
|
PUBLISH_REMOTE_BASE: ${PUBLISH_REMOTE_BASE}
|
||||||
|
PUBLISH_GH_API_BASE: ${PUBLISH_GH_API_BASE}
|
||||||
|
volumes:
|
||||||
|
- /opt/stacks/sched-workers/data-ym:/app/data
|
||||||
|
|
||||||
|
ozon-seller-builder:
|
||||||
|
image: registry.kzntsv.site/ozon-seller-builder:0.1.0
|
||||||
|
container_name: ozon-seller-builder
|
||||||
|
restart: unless-stopped
|
||||||
|
# tsc-сборка клиента ~700MB пик (openapi-generator + компилятор) — 512m OOM
|
||||||
|
mem_limit: 1g
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
environment:
|
||||||
|
WORKER_PORT: "8080"
|
||||||
|
SCHED_API_KEY: ${SCHED_API_KEY}
|
||||||
|
# внутренний крон не нужен — расписанием рулит sched. 31 февраля не наступает никогда.
|
||||||
|
CRON_SCHEDULE: "0 0 29 2 *"
|
||||||
|
# персистентность state/output на хосте (bind-mount /app/data)
|
||||||
|
STATE_FILE: /app/data/state.json
|
||||||
|
OUTPUT_DIR: /app/data/output/typescript
|
||||||
|
# безопасный дефолт: DRY_RUN=1. Реальный мок-publish — только явным data.dryRun=false в таске.
|
||||||
|
DRY_RUN: "1"
|
||||||
|
BROWSER_CDP_URL: http://browser:9222
|
||||||
|
NTFY_URL: http://ntfy
|
||||||
|
NTFY_TOPIC: ozon-seller-pipeline
|
||||||
|
UNISENDER_API_KEY: mock-key
|
||||||
|
UNISENDER_SENDER_EMAIL: noreply@snolla.com
|
||||||
|
UNISENDER_SENDER_NAME: sched-pipeline
|
||||||
|
UNISENDER_ENDPOINT: http://unisender-mock:8080/ru/transactional/api/v1/email/send.json
|
||||||
|
NOTIFY_EMAIL_TO: ${NOTIFY_EMAIL_TO}
|
||||||
|
GH_TOKEN: ${GITHUB_TOKEN}
|
||||||
|
NPM_TOKEN: ${NPM_TOKEN}
|
||||||
|
# мок-publish: verdaccio (npm) + Gitea (репо/релизы) — без выхода в npmjs/github.com
|
||||||
|
PUBLISH_REGISTRY: ${PUBLISH_REGISTRY}
|
||||||
|
PUBLISH_REMOTE_BASE: ${PUBLISH_REMOTE_BASE}
|
||||||
|
PUBLISH_GH_API_BASE: ${PUBLISH_GH_API_BASE}
|
||||||
|
volumes:
|
||||||
|
- /opt/stacks/sched-workers/data-ozon:/app/data
|
||||||
|
# снапшот спеки из репо (fetch антиботом не ходим — dry-run от ручного снапшота)
|
||||||
|
- /opt/stacks/sched-workers/data-ozon-openapi:/app/openapi
|
||||||
|
depends_on:
|
||||||
|
- browser
|
||||||
|
|
||||||
|
# headless Chrome + CDP :9222 (для антибот-fetch стадий ozon), socat наружу 0.0.0.0:9222
|
||||||
|
browser:
|
||||||
|
image: registry.kzntsv.site/browser-cdp:0.1.0
|
||||||
|
container_name: browser-cdp
|
||||||
|
shm_size: "1gb"
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 512m
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
# мок Unisender Go: письма в /opt/stacks/sched-workers/data-mail, реальных отправок нет
|
||||||
|
unisender-mock:
|
||||||
|
image: registry.kzntsv.site/unisender-mock:0.1.0
|
||||||
|
container_name: unisender-mock
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 128m
|
||||||
|
environment:
|
||||||
|
PORT: "8080"
|
||||||
|
MAIL_DIR: /mail
|
||||||
|
volumes:
|
||||||
|
- /opt/stacks/sched-workers/data-mail:/mail
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
44
host-stacks/vds-kzntsv/sched.compose.yml
Normal file
44
host-stacks/vds-kzntsv/sched.compose.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# sched — custom single-binary (embedded engine + admin API + UI morda + MCP + MariaDB).
|
||||||
|
# Один контейнер, один HTTP-сервер :8080: /api/* (admin API), /mcp (Streamable HTTP MCP),
|
||||||
|
# / (UI morda), /sched-ui.bundle.js (static UI bundle).
|
||||||
|
# Образ registry.kzntsv.site/sched-custom:0.12.1-mysql = published @schedjs/{daemon,storage-mysql,admin-api,mcp,ui}
|
||||||
|
# + собственный custom-entry.mjs (Dockerfile.custom).
|
||||||
|
# Прод: VDS рядом с общей MariaDB (стек 11, сеть shared-dbs, отдельная база sched — свой role).
|
||||||
|
# Endpoint: https://sched.vds.kzntsv.site — /api/* + /mcp, остальное → UI morda.
|
||||||
|
# Deploy: Portainer-managed (см. portainer-stack-management-vds). Env через Portainer (не env_file).
|
||||||
|
|
||||||
|
services:
|
||||||
|
sched:
|
||||||
|
image: registry.kzntsv.site/sched-custom:0.12.1-mysql
|
||||||
|
container_name: sched
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 512m
|
||||||
|
networks:
|
||||||
|
- shared-dbs
|
||||||
|
- proxy
|
||||||
|
environment:
|
||||||
|
SCHED_TASKS: /app/config/tasks.json
|
||||||
|
MYSQL_URL: mysql://sched:${SCHED_DB_PASS}@mariadb:3306/sched?ssl={"rejectUnauthorized":false}
|
||||||
|
SCHED_ADMIN_KEY: ${SCHED_ADMIN_KEY}
|
||||||
|
SCHED_ALERTS_WEBHOOK_URL: ${SCHED_ALERTS_WEBHOOK_URL}
|
||||||
|
SCHED_ALERTS_BASIC_AUTH: ${SCHED_ALERTS_BASIC_AUTH}
|
||||||
|
volumes:
|
||||||
|
- /opt/stacks/sched/tasks.json:/app/config/tasks.json:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8080/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
traefik.enable: "true"
|
||||||
|
traefik.http.routers.sched.entrypoints: websecure
|
||||||
|
traefik.http.routers.sched.rule: Host(`sched.vds.kzntsv.site`)
|
||||||
|
traefik.http.routers.sched.tls.certresolver: letsEncrypt
|
||||||
|
traefik.http.services.sched.loadbalancer.server.port: "8080"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
shared-dbs:
|
||||||
|
external: true
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
102
host-stacks/vds-kzntsv/sched.tasks.json
Normal file
102
host-stacks/vds-kzntsv/sched.tasks.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"name": "ozon-seller-pipeline",
|
||||||
|
"runner": "http",
|
||||||
|
"config": {
|
||||||
|
"url": "http://ozon-seller-builder:8080/run?async=1",
|
||||||
|
"method": "POST",
|
||||||
|
"envelope": true,
|
||||||
|
"auth": {
|
||||||
|
"apiKey": "__SCHED_API_KEY__"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"stages": [
|
||||||
|
"hash",
|
||||||
|
"patch",
|
||||||
|
"diff",
|
||||||
|
"classify",
|
||||||
|
"generate",
|
||||||
|
"fix",
|
||||||
|
"build",
|
||||||
|
"postman",
|
||||||
|
"state",
|
||||||
|
"publish"
|
||||||
|
],
|
||||||
|
"dryRun": false,
|
||||||
|
"readmeTemplate": "/app/data/package-readme.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yandex-market-pipeline",
|
||||||
|
"runner": "http",
|
||||||
|
"config": {
|
||||||
|
"handler": "pipeline",
|
||||||
|
"url": "http://ym-client-builder:8080/run",
|
||||||
|
"method": "POST",
|
||||||
|
"envelope": true,
|
||||||
|
"auth": {
|
||||||
|
"apiKey": "__SCHED_API_KEY__"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"stages": [
|
||||||
|
"fetch",
|
||||||
|
"detect",
|
||||||
|
"diff",
|
||||||
|
"classify",
|
||||||
|
"patch",
|
||||||
|
"generate",
|
||||||
|
"fix",
|
||||||
|
"build",
|
||||||
|
"test",
|
||||||
|
"postmanGenerate",
|
||||||
|
"postman",
|
||||||
|
"publish",
|
||||||
|
"githubDistro"
|
||||||
|
],
|
||||||
|
"dryRun": false,
|
||||||
|
"repo": "https://github.com/yandex-market/yandex-market-partner-api",
|
||||||
|
"branch": "main",
|
||||||
|
"workDir": "/app/data/clone",
|
||||||
|
"snapshotsDir": "data/openapi/snapshots",
|
||||||
|
"statePath": "data/state.json",
|
||||||
|
"outputDir": "data/output/typescript",
|
||||||
|
"testsDir": "tests/client",
|
||||||
|
"registry": "__PUBLISH_REGISTRY__",
|
||||||
|
"remoteBase": "__PUBLISH_REMOTE_BASE__",
|
||||||
|
"githubApiBase": "__PUBLISH_GH_API_BASE__",
|
||||||
|
"notify": {
|
||||||
|
"ntfy": {
|
||||||
|
"url": "http://ntfy",
|
||||||
|
"topic": "ym-client-builder"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"to": "vitya.kuznetsov@gmail.com",
|
||||||
|
"endpoint": "http://unisender-mock:8080/ru/transactional/api/v1/email/send.json",
|
||||||
|
"on": [
|
||||||
|
"published",
|
||||||
|
"report",
|
||||||
|
"failed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeoutMs": 900000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alerts": {
|
||||||
|
"on": [
|
||||||
|
"failed"
|
||||||
|
],
|
||||||
|
"onMissed": true,
|
||||||
|
"onSyncFailed": true,
|
||||||
|
"webhook": {
|
||||||
|
"url": "https://ntfy.vds.kzntsv.site/sched-alerts",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Basic dml0eWE6UHJ5YWtoaW45"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
host-stacks/vds-kzntsv/tg-digest.compose.yml
Normal file
52
host-stacks/vds-kzntsv/tg-digest.compose.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# tg-digest — HTTP-воркер дневного дайджеста телеграм-каналов.
|
||||||
|
# sched cron 0 5 * * * (tz UTC = 08:00 MSK), runner http, simple mode.
|
||||||
|
# Internal: сеть proxy, БЕЗ traefik-labels — наружу не публикуется (см. ранбук tg-digest-vds-deploy-runbook).
|
||||||
|
# Deploy: Portainer-managed (см. portainer-stack-management-vds). Env через Portainer (не env_file).
|
||||||
|
# Image: registry.kzntsv.site/tg-digest-worker:<tag> (python:3.13-alpine, telethon+requests, из main victor/tg-digest).
|
||||||
|
|
||||||
|
services:
|
||||||
|
tg-digest:
|
||||||
|
image: registry.kzntsv.site/tg-digest-worker:<tag>
|
||||||
|
container_name: tg-digest
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 256m
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
environment:
|
||||||
|
# MTProto (оператор; pass telegram/api-id + api-hash)
|
||||||
|
TG_API_ID: ${TG_API_ID}
|
||||||
|
TG_API_HASH: ${TG_API_HASH}
|
||||||
|
TG_SESSION: /data/session.session
|
||||||
|
TG_PHONE: ${TG_PHONE}
|
||||||
|
TG_CACHE: /data/tg-cache
|
||||||
|
TG_WINDOW_HOURS: ${TG_WINDOW_HOURS:-24}
|
||||||
|
# Доставка (pass telegram/full-env)
|
||||||
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||||
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
|
||||||
|
# Стадия-2 LLM (deepseek; ключ — у оператора)
|
||||||
|
LLM_API_KEY: ${LLM_API_KEY}
|
||||||
|
LLM_BASE_URL: ${LLM_BASE_URL:-https://api.deepseek.com}
|
||||||
|
LLM_MODEL: ${LLM_MODEL:-deepseek-chat}
|
||||||
|
# Ингест raw в mappa (pass mappa/full-env)
|
||||||
|
MAPPA_URL: ${MAPPA_URL:-https://mappa.vds.kzntsv.site}
|
||||||
|
MAPPA_API_TOKEN: ${MAPPA_API_TOKEN}
|
||||||
|
MAPPA_PROJECT: ${MAPPA_PROJECT:-tg-digest}
|
||||||
|
# Auth sched → воркер (pass sched/tg-digest-api-key)
|
||||||
|
WORKER_API_KEY: ${WORKER_API_KEY}
|
||||||
|
# 0 — dry-run smoke без отправки в TG
|
||||||
|
TG_SEND: ${TG_SEND:-1}
|
||||||
|
volumes:
|
||||||
|
- tg-digest-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8080/healthz')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tg-digest-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
21
host-stacks/vds-kzntsv/tg-digest/Dockerfile
Normal file
21
host-stacks/vds-kzntsv/tg-digest/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# tg-digest worker — python (telethon + requests) + HTTP-обёртка для sched http-runner.
|
||||||
|
# Файл коммитится в КОРЕНЬ репо victor/tg-digest (рядом http_worker.py).
|
||||||
|
# Build (из чекаута victor/tg-digest, main с d3aefb2):
|
||||||
|
# docker build -t registry.kzntsv.site/tg-digest-worker:<tag> .
|
||||||
|
# Env-контракт — см. ранбук tg-digest-vds-deploy-runbook (все секреты env, pass в контейнере нет).
|
||||||
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY src/ src/
|
||||||
|
COPY http_worker.py ./
|
||||||
|
|
||||||
|
RUN mkdir -p /data
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
CMD ["python", "http_worker.py"]
|
||||||
63
host-stacks/vds-kzntsv/tg-digest/http_worker.py
Normal file
63
host-stacks/vds-kzntsv/tg-digest/http_worker.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""tg-digest HTTP-воркер для sched (http runner, simple mode).
|
||||||
|
|
||||||
|
GET /healthz -> 200 {"ok": true} (healthcheck стека)
|
||||||
|
POST /run -> проверка x-sched-api-key (WORKER_API_KEY); запуск
|
||||||
|
`python -m src.worker`; 200 {ok, runId, summary} | 5xx {error}.
|
||||||
|
|
||||||
|
Simple mode: sched шлёт POST и ждёт ответ до config.timeoutMs; любой 2xx = succeeded.
|
||||||
|
runId приходит в заголовке x-sched-run-id (пишется в ответ, в лог).
|
||||||
|
Заголовки x-sched-api-key от sched — auth per-task (task config.auth.apiKey).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
WORKER_API_KEY = os.environ.get("WORKER_API_KEY", "")
|
||||||
|
WORKER_CMD = [sys.executable, "-m", "src.worker"]
|
||||||
|
WORKER_TIMEOUT = float(os.environ.get("WORKER_TIMEOUT_S") or 3600)
|
||||||
|
PORT = int(os.environ.get("PORT") or 8080)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, *args): # тишина в stdout (логи — по runId)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _send(self, code, obj):
|
||||||
|
body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == "/healthz":
|
||||||
|
self._send(200, {"ok": True})
|
||||||
|
else:
|
||||||
|
self._send(404, {"error": "not found"})
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path != "/run":
|
||||||
|
return self._send(404, {"error": "not found"})
|
||||||
|
if WORKER_API_KEY and self.headers.get("x-sched-api-key") != WORKER_API_KEY:
|
||||||
|
return self._send(401, {"error": "unauthorized"})
|
||||||
|
run_id = self.headers.get("x-sched-run-id", "?")
|
||||||
|
try:
|
||||||
|
r = subprocess.run(WORKER_CMD, capture_output=True, text=True,
|
||||||
|
env=os.environ, timeout=WORKER_TIMEOUT)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return self._send(504, {"error": "worker timeout", "runId": run_id})
|
||||||
|
if r.returncode != 0:
|
||||||
|
return self._send(500, {"error": (r.stderr or r.stdout)[-500:], "runId": run_id})
|
||||||
|
try:
|
||||||
|
summary = json.loads(r.stdout)
|
||||||
|
except ValueError:
|
||||||
|
summary = {"stdout": r.stdout[-500:]}
|
||||||
|
self._send(200, {"ok": True, "runId": run_id, "summary": summary})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
|
||||||
@@ -139,7 +139,7 @@ SNAP_RESULT=$(docker exec elasticsearch curl -sS -X PUT \
|
|||||||
echo "ES snapshot result: $SNAP_RESULT"
|
echo "ES snapshot result: $SNAP_RESULT"
|
||||||
if ! echo "$SNAP_RESULT" | grep -q '"state":"SUCCESS"'; then
|
if ! echo "$SNAP_RESULT" | grep -q '"state":"SUCCESS"'; then
|
||||||
echo "ES snapshot did NOT report SUCCESS state — failing"
|
echo "ES snapshot did NOT report SUCCESS state — failing"
|
||||||
exit 3
|
on_error $LINENO 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "--- prune ES snapshots older than $RETENTION_DAYS ---"
|
echo "--- prune ES snapshots older than $RETENTION_DAYS ---"
|
||||||
@@ -168,7 +168,7 @@ BSNAP_RESULT=$(docker exec bookva-es curl -sS -X PUT \
|
|||||||
echo "bookva-es snapshot result: $BSNAP_RESULT"
|
echo "bookva-es snapshot result: $BSNAP_RESULT"
|
||||||
if ! echo "$BSNAP_RESULT" | grep -q '"state":"SUCCESS"'; then
|
if ! echo "$BSNAP_RESULT" | grep -q '"state":"SUCCESS"'; then
|
||||||
echo "bookva-es snapshot did NOT report SUCCESS state — failing"
|
echo "bookva-es snapshot did NOT report SUCCESS state — failing"
|
||||||
exit 3
|
on_error $LINENO 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "--- prune bookva-es snapshots older than $RETENTION_DAYS ---"
|
echo "--- prune bookva-es snapshots older than $RETENTION_DAYS ---"
|
||||||
|
|||||||
164
scripts/schedjs-npm-oidc-setup.ps1
Normal file
164
scripts/schedjs-npm-oidc-setup.ps1
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# schedjs-npm-oidc-setup.ps1 — разовая настройка OIDC-публикации @schedjs/* (task:3130).
|
||||||
|
#
|
||||||
|
# Зачем: npm не даёт automation-токену публиковать/настраивать без 2FA — версию выкладывает
|
||||||
|
# человек. Trusted publisher (OIDC) снимает это: публикует GitHub Actions, токена нет вовсе.
|
||||||
|
#
|
||||||
|
# Фазы (можно по одной):
|
||||||
|
# -Phase github — создать environment `npm-release` + branch policy «только main» (без 2FA)
|
||||||
|
# -Phase npm — trusted publisher у 9 пакетов scope (НУЖЕН web-логин с 2FA, см. ниже)
|
||||||
|
# -Phase verify — показать, что trust реально встал
|
||||||
|
#
|
||||||
|
# Предусловия для -Phase npm (иначе будет 403 «Two-factor authentication is required»):
|
||||||
|
# 1) npm >= 11.15.0
|
||||||
|
# 2) npm login --auth-type=web (заменить automation-токен npm_… на web-сессию с 2FA)
|
||||||
|
# 3) 2FA на аккаунте включена; режим «Authorization and writes»
|
||||||
|
#
|
||||||
|
# Примеры (на машине только PowerShell 5.1, `pwsh` нет):
|
||||||
|
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase github
|
||||||
|
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase npm
|
||||||
|
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase npm -Otp 123456 # код из аутентификатора
|
||||||
|
# powershell -NoProfile -File .\schedjs-npm-oidc-setup.ps1 -Phase verify
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[ValidateSet('github', 'login', 'npm', 'verify', 'all')]
|
||||||
|
[string]$Phase = 'github',
|
||||||
|
|
||||||
|
[string]$Repo = 'schedjs/schedjs',
|
||||||
|
[string]$WorkflowFile = 'release-npm.yml',
|
||||||
|
[string]$Environment = 'npm-release',
|
||||||
|
|
||||||
|
# 6-значный TOTP, если npm просит его неинтерактивно (или возьми код из аутентификатора на экран)
|
||||||
|
[string]$Otp = ''
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$pkgs = @('core', 'storage-mongo', 'storage-mysql', 'storage-postgres', 'admin-api', 'mcp', 'ui', 'daemon', 'cli')
|
||||||
|
|
||||||
|
function Say([string]$m) { Write-Host $m }
|
||||||
|
function Fail([string]$m) { Write-Host "FAIL: $m" -ForegroundColor Red; exit 1 }
|
||||||
|
|
||||||
|
# npm пишет notice в stderr; при $ErrorActionPreference='Stop' PS 5.1 делает из этого
|
||||||
|
# завершающую ошибку (NativeCommandError) — глушим только на время вызова и смотрим код.
|
||||||
|
function Invoke-Npm([string[]]$NpmArgs) {
|
||||||
|
$old = $ErrorActionPreference
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
try {
|
||||||
|
$text = (& npm @NpmArgs 2>&1 | Out-String)
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
}
|
||||||
|
finally { $ErrorActionPreference = $old }
|
||||||
|
[pscustomobject]@{ Code = $code; Text = $text }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-BranchPolicies {
|
||||||
|
# до включения custom_branch_policies эндпоинт отдаёт 404 — это норма, не ошибка
|
||||||
|
@(gh api "repos/$Repo/environments/$Environment/deployment-branch-policies" --jq '.branch_policies[]?.name' 2>$null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Phase-GitHub {
|
||||||
|
Say "== GitHub environment '$Environment' в $Repo"
|
||||||
|
# 1) создать/обновить environment и включить custom branch policies
|
||||||
|
'{"deployment_branch_policy":{"protected_branches":false,"custom_branch_policies":true}}' |
|
||||||
|
gh api -X PUT "repos/$Repo/environments/$Environment" --input - --jq '{name,deployment_branch_policy}' |
|
||||||
|
ForEach-Object { Say " environment: $_" }
|
||||||
|
|
||||||
|
# 2) policy «только main» (идемпотентно)
|
||||||
|
if ((Get-BranchPolicies) -notcontains 'main') {
|
||||||
|
Say " добавляю policy: branch main"
|
||||||
|
gh api -X POST "repos/$Repo/environments/$Environment/deployment-branch-policies" -f name=main -f type=branch --jq '{name,type}' 2>$null | ForEach-Object { Say " policy: $_" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Say " policy 'main' уже есть — идемпотентно"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3) verify живьём
|
||||||
|
$now = Get-BranchPolicies
|
||||||
|
if ($now -notcontains 'main') { Fail "policy main не встала (получили: $($now -join ', '))" }
|
||||||
|
Say " verify: $(($now | ForEach-Object { "branch:$_" }) -join ', ')"
|
||||||
|
Say "OK: environment готов (секреты не нужны — OIDC)"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-WebLogin {
|
||||||
|
$npmrc = Join-Path $env:USERPROFILE '.npmrc'
|
||||||
|
$content = if (Test-Path $npmrc) { Get-Content $npmrc -Raw } else { '' }
|
||||||
|
if ($content -notmatch 'registry\.npmjs\.org/:_authToken=npm_') { Say '== .npmrc: automation-токена нет — логин уже web-сессия (или токен иной формы)'; return }
|
||||||
|
Say '== .npmrc: automation-токен npm_… — для trust нужна web-сессия. Откроется браузер, введи код 2FA.'
|
||||||
|
$old = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
|
||||||
|
try { & npm login --auth-type=web } finally { $ErrorActionPreference = $old }
|
||||||
|
if ($LASTEXITCODE -ne 0) { Fail "npm login завершился с кодом $LASTEXITCODE — повтори вручную: npm login --auth-type=web" }
|
||||||
|
Say ' логин ок'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-NpmAuth {
|
||||||
|
$npmver = (Invoke-Npm @('-v')).Text.Trim()
|
||||||
|
Say "== npm $npmver (нужно >= 11.15.0)"
|
||||||
|
if ([version]($npmver -replace '[^0-9.].*$', '') -lt [version]'11.15.0') { Fail "npm старый: $npmver" }
|
||||||
|
|
||||||
|
$who = (Invoke-Npm @('whoami')).Text.Trim()
|
||||||
|
if (-not $who -or $who -match 'npm error') { Fail "npm не залогинен. Сделай: npm login --auth-type=web — и повтори -Phase npm" }
|
||||||
|
Say " whoami: $who"
|
||||||
|
|
||||||
|
$npmrc = Join-Path $env:USERPROFILE '.npmrc'
|
||||||
|
if (Test-Path $npmrc) {
|
||||||
|
$content = Get-Content $npmrc -Raw
|
||||||
|
$backup = Join-Path $env:USERPROFILE '.npmrc.bak-npmtrust'
|
||||||
|
if (-not (Test-Path $backup)) {
|
||||||
|
Copy-Item $npmrc $backup
|
||||||
|
Say " бэкап .npmrc -> $backup"
|
||||||
|
}
|
||||||
|
if ($content -match 'registry\.npmjs\.org/:_authToken=npm_') {
|
||||||
|
Fail @"
|
||||||
|
в .npmrc лежит automation-токен npm_… — по нему trust всегда даёт 403 'Two-factor authentication is required'.
|
||||||
|
Лечение (однажды):
|
||||||
|
1) npm login --auth-type=web (браузер + код из аутентификатора; перезапишет токен в .npmrc)
|
||||||
|
2) повторить: -Phase npm
|
||||||
|
3) вернуть старый токен: Copy-Item `$env:USERPROFILE\.npmrc.bak-npmtrust `$env:USERPROFILE\.npmrc -Force
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Phase-Npm {
|
||||||
|
Ensure-WebLogin
|
||||||
|
Assert-NpmAuth
|
||||||
|
Say "== trusted publisher: $($pkgs.Count) пакетов @schedjs/*"
|
||||||
|
$ok = @(); $bad = @()
|
||||||
|
foreach ($p in $pkgs) {
|
||||||
|
$argsList = @('trust', 'github', "@schedjs/$p", '--repo', $Repo, '--file', $WorkflowFile,
|
||||||
|
'--env', $Environment, '--allow-publish', '--yes')
|
||||||
|
if ($Otp) { $argsList += "--otp=$Otp" }
|
||||||
|
Write-Host (" " + "@schedjs/$p … ") -NoNewline
|
||||||
|
$r = Invoke-Npm $argsList
|
||||||
|
if ($r.Code -eq 0) { Write-Host 'OK' -ForegroundColor Green; $ok += $p }
|
||||||
|
else {
|
||||||
|
Write-Host 'FAIL' -ForegroundColor Red
|
||||||
|
$bad += $p
|
||||||
|
($r.Text.Trim() -split "`n" | Select-Object -Last 3) | ForEach-Object { Write-Host " $_" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Say ""
|
||||||
|
Say "итог: ok $($ok.Count)/$($pkgs.Count)$(if ($bad) { ' — упали: ' + ($bad -join ', ') })"
|
||||||
|
if ($bad.Count) { Say "подсказка: 403 → истёк OTP или не web-логин; 404 → пакета нет на npm"; exit 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Phase-Verify {
|
||||||
|
Say "== npm trust list по 9 пакетам (ищем $WorkflowFile / $Environment)"
|
||||||
|
$miss = @()
|
||||||
|
foreach ($p in $pkgs) {
|
||||||
|
$out = (Invoke-Npm @('trust', 'list', "@schedjs/$p")).Text
|
||||||
|
$hit = ($out -match [regex]::Escape($WorkflowFile)) -and ($out -match [regex]::Escape($Environment))
|
||||||
|
if ($hit) { Write-Host (" " + "@schedjs/$p : OK") -ForegroundColor Green }
|
||||||
|
else { Write-Host (" " + "@schedjs/$p : НЕТ trust") -ForegroundColor Yellow; $miss += $p }
|
||||||
|
}
|
||||||
|
if ($miss.Count) { Say "нет trust у: $($miss -join ', ')"; exit 1 }
|
||||||
|
Say "OK: trust у всех 9"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Phase) {
|
||||||
|
'github' { Phase-GitHub }
|
||||||
|
'login' { Ensure-WebLogin }
|
||||||
|
'npm' { Phase-Npm }
|
||||||
|
'verify' { Phase-Verify }
|
||||||
|
'all' { Phase-Npm; Say ''; Phase-Verify }
|
||||||
|
}
|
||||||
365
scripts/sync-sched-publish.mjs
Normal file
365
scripts/sync-sched-publish.mjs
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
// sched → GitHub: полный синк курируемой копии (ранбук wiki:3332, task:1711).
|
||||||
|
//
|
||||||
|
// Алгоритм (один коммит на волну, pre-push CI-эквивалентный гейт):
|
||||||
|
// collect — ls-files dev минус исключения (.wiki/.tasks/.agents/AGENTS/CLAUDE/
|
||||||
|
// branding-pora/pngtree/327274997/cat-clock/MIGRATION)
|
||||||
|
// copy — mkdir -p + cp в pub (untracked dev-мусор не копируется)
|
||||||
|
// bleach — dev-приватное → публичное (yarnrc/Dockerfile/compose/nuxt/
|
||||||
|
// sched-links/publish-check/CHANGELOG/examples)
|
||||||
|
// scrub — dist: rm -rf + cp из dev + скраб git.kzntsv (в т.ч. \u002F и \\u002F)
|
||||||
|
// lock — yarn.lock регенерация против npmjs (gotcha 8): если
|
||||||
|
// `yarn install --immutable` падает YN0028 → `yarn install` → перепроверка
|
||||||
|
// gate — CI-эквивалент ЛОКАЛЬНО до пуша (gotcha 10/11):
|
||||||
|
// tsc --noEmit + vitest (vitest.ci.config.ts) прямыми вызовами
|
||||||
|
// node_modules (обходит Windows yarn-workspace-bin-болезнь)
|
||||||
|
// commit — один чистый коммит (amend при --amend)
|
||||||
|
// push — только если gate зелёный (без --push — стоп перед пушем)
|
||||||
|
//
|
||||||
|
// Windows-заметка: `yarn workspace run` не добавляет root .bin в PATH
|
||||||
|
// (command not found: tsc) — гейт вызывает node node_modules/... напрямую.
|
||||||
|
//
|
||||||
|
// Использование:
|
||||||
|
// node scripts/sync-sched-publish.mjs # полный цикл до пуша (стоп, показать)
|
||||||
|
// node scripts/sync-sched-publish.mjs --push # + push origin main
|
||||||
|
// node scripts/sync-sched-publish.mjs --amend # amend последнего коммита (после фикса dev)
|
||||||
|
// node scripts/sync-sched-publish.mjs --only=lock # только фаза
|
||||||
|
// node scripts/sync-sched-publish.mjs --no-copy # пропустить фазы copy/bleach/scrub
|
||||||
|
//
|
||||||
|
// Возврат: 0 = зелёно, 1 = что-то упало (причины в stdout, мутаций нет после точки отказа).
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
const HOME = os.homedir().replace(/\\/g, "/");
|
||||||
|
const DEV = process.env.SCHED_DEV || `${HOME}/projects/sched`;
|
||||||
|
const PUB = process.env.SCHED_PUB || `${HOME}/projects/sched-upstream`;
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const PUSH = args.includes("--push");
|
||||||
|
const AMEND = args.includes("--amend");
|
||||||
|
const ONLY = args.includes("--only=") ? args.find((a) => a.startsWith("--only=")).slice(7) : null;
|
||||||
|
const NO_COPY = args.includes("--no-copy");
|
||||||
|
|
||||||
|
// ── helpers ────────────────────────────────────────────────────────────────
|
||||||
|
const sh = (cmd, opts = {}) =>
|
||||||
|
execSync(cmd, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", ...opts });
|
||||||
|
const shSilent = (cmd) => {
|
||||||
|
try { return sh(cmd).trim(); } catch { return null; }
|
||||||
|
};
|
||||||
|
const log = (s) => console.log(s);
|
||||||
|
const fail = (s) => { console.error(`\n✗ ${s}`); process.exitCode = 1; };
|
||||||
|
|
||||||
|
function copyTree(srcFile, dstFile) {
|
||||||
|
if (!fs.existsSync(srcFile)) return; // удалён в dev → удалится в pub через git add -A
|
||||||
|
fs.mkdirSync(path.dirname(dstFile), { recursive: true });
|
||||||
|
fs.copyFileSync(srcFile, dstFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── фазы ───────────────────────────────────────────────────────────────────
|
||||||
|
function collect() {
|
||||||
|
const out = shSilent(`git -C "${DEV}" ls-files`);
|
||||||
|
if (out === null) { fail("dev не git-репо"); return null; }
|
||||||
|
const EXCL = /^\.wiki\/|^\.tasks\/|^\.agents\/|^\.mappa\/|^AGENTS\.md$|^CLAUDE\.md$|^branding\/pora-|^pngtree-|^327274997_|^cat-clock-|^MIGRATION\.md$/;
|
||||||
|
return out.split("\n").filter((f) => f && !EXCL.test(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// dev-only артефакты, которые не должны жить в pub НИКОГДА (gotcha 15: .mappa/config.yaml
|
||||||
|
// несёт внутренний реестр — tenant/mappa.vds.kzntsv.site/git.kzntsv.site; в волне 2026-09-19
|
||||||
|
// утёк в паблик-коммит, потому что exclusion-list его не знал)
|
||||||
|
const NEVER = [".mappa"];
|
||||||
|
function purge() {
|
||||||
|
for (const p of NEVER) {
|
||||||
|
const t = `${PUB}/${p}`;
|
||||||
|
if (fs.existsSync(t)) { fs.rmSync(t, { recursive: true, force: true }); log(`purge: удалён ${p} (dev-only)`); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(files) {
|
||||||
|
for (const f of files) copyTree(`${DEV}/${f}`, `${PUB}/${f}`);
|
||||||
|
log(`copy: ${files.length} файлов`);
|
||||||
|
purge();
|
||||||
|
}
|
||||||
|
|
||||||
|
// выбеливание — точечные замены, идемпотентно (эталон: верифицированный b0afd14)
|
||||||
|
function bleach() {
|
||||||
|
const sub = (rel, pairs) => {
|
||||||
|
const p = `${PUB}/${rel}`;
|
||||||
|
if (!fs.existsSync(p)) return;
|
||||||
|
let s = fs.readFileSync(p, "utf8");
|
||||||
|
let dirty = false;
|
||||||
|
for (const [old, nw] of pairs) if (s.includes(old)) { s = s.replaceAll(old, nw); dirty = true; }
|
||||||
|
if (dirty) fs.writeFileSync(p, s);
|
||||||
|
};
|
||||||
|
|
||||||
|
// .yarnrc.yml — канон целиком (3 строки, фиксированный)
|
||||||
|
fs.writeFileSync(`${PUB}/.yarnrc.yml`, `nodeLinker: node-modules\nenableTelemetry: false\nnpmRegistryServer: "https://registry.npmjs.org"\n`);
|
||||||
|
|
||||||
|
sub("docker-compose.dev.yml", [
|
||||||
|
["# npm-based image: pull @schedjs/daemon from the private verdaccio.",
|
||||||
|
"# npm-based image: pull @schedjs/daemon from the public npm registry."],
|
||||||
|
["NPM_REGISTRY: ${NPM_REGISTRY:-https://verdaccio.kzntsv.site}",
|
||||||
|
"NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org}"],
|
||||||
|
["NPM_TOKEN: ${VERDACCIO_TOKEN:-}", "NPM_TOKEN: ${NPM_TOKEN:-}"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("apps/daemon/Dockerfile", [
|
||||||
|
["# Build (public registry):\n# docker build -t schedd:0.10.2 apps/daemon\n# Build (private verdaccio — our internal registry):\n# docker build --secret id=verdaccio_token,env=VERDACCIO_TOKEN \\\n# --build-arg NPM_REGISTRY=https://verdaccio.kzntsv.site \\\n# -t registry.kzntsv.site/sched-daemon:0.10.2 apps/daemon\n#",
|
||||||
|
"# Build:\n# docker build -t schedd:0.12.1 apps/daemon\n#\n# Private-registry mirror (optional, internal use): pass the registry URL as\n# NPM_REGISTRY + a BuildKit secret to install from a mirror instead of npmjs.\n#"],
|
||||||
|
["(verdaccio serves none)", "(a mirror serves none)"],
|
||||||
|
["# VERDACCIO_TOKEN comes from the BuildKit secret (mount type=secret).",
|
||||||
|
"# NPM_TOKEN comes from the BuildKit secret (mount type=secret)."],
|
||||||
|
["id=verdaccio_token", "id=npm_token"],
|
||||||
|
["/run/secrets/verdaccio_token", "/run/secrets/npm_token"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("docs/nuxt.config.ts", [
|
||||||
|
["?? 'https://git.kzntsv.site/victor/sched'", "?? 'https://github.com/schedjs/schedjs'"],
|
||||||
|
["?? 'https://verdaccio.kzntsv.site'", "?? 'https://registry.npmjs.org'"],
|
||||||
|
["?? 'registry.kzntsv.site'", "?? 'ghcr.io/schedjs'"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("docs/modules/sched-links.ts", [
|
||||||
|
["?? 'https://git.kzntsv.site/victor/sched'", "?? 'https://github.com/schedjs/schedjs'"],
|
||||||
|
["?? 'https://verdaccio.kzntsv.site'", "?? 'https://registry.npmjs.org'"],
|
||||||
|
["?? 'registry.kzntsv.site'", "?? 'ghcr.io/schedjs'"],
|
||||||
|
["VERDACCIO_TOKEN", "NPM_TOKEN"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("CHANGELOG.md", [
|
||||||
|
["на verdaccio", "на npm"], ["→ verdaccio", "→ npm"],
|
||||||
|
["registry.kzntsv.site/sched-daemon", "ghcr.io/schedjs/sched-daemon"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const p of ["scripts/publish-check.mjs", "scripts/publish-check.test.mjs"]) {
|
||||||
|
sub(p, [["registry.kzntsv.site/sched-daemon", "ghcr.io/schedjs/sched-daemon"]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of ["examples/admin-client-python/generate.ps1", "examples/admin-client-ts/generate.ps1"]) {
|
||||||
|
sub(f, [[" (verdaccio subpath export);", ";"]]);
|
||||||
|
}
|
||||||
|
for (const f of [
|
||||||
|
"examples/admin-client-python/README.md",
|
||||||
|
"examples/admin-client-ts/README.md",
|
||||||
|
"examples/admin-client-python/tests/test_admin_client.py",
|
||||||
|
]) {
|
||||||
|
sub(f, [["verdaccio", "npm"]]);
|
||||||
|
}
|
||||||
|
log("bleach: выбелено");
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrubDist() {
|
||||||
|
const devDist = `${DEV}/docs/dist`;
|
||||||
|
const pubDist = `${PUB}/docs/dist`;
|
||||||
|
if (!fs.existsSync(devDist)) { log("scrub: нет dev docs/dist — пропуск"); return; }
|
||||||
|
fs.rmSync(pubDist, { recursive: true, force: true });
|
||||||
|
fs.cpSync(devDist, pubDist, { recursive: true });
|
||||||
|
|
||||||
|
// скраб остатков kzntsv (прямые + \u002F + \\u002F в _payload.json)
|
||||||
|
const pairs = [
|
||||||
|
["git.kzntsv.site/victor/sched", "github.com/schedjs/schedjs"],
|
||||||
|
["verdaccio.kzntsv.site/:_authToken=${VERDACCIO_TOKEN}", "registry.npmjs.org/:_authToken=${NPM_TOKEN}"],
|
||||||
|
["verdaccio.kzntsv.site", "registry.npmjs.org"],
|
||||||
|
["registry.kzntsv.site/sched-daemon", "ghcr.io/schedjs/sched-daemon"],
|
||||||
|
["registry.kzntsv.site", "ghcr.io/schedjs"],
|
||||||
|
["VERDACCIO_TOKEN", "NPM_TOKEN"],
|
||||||
|
];
|
||||||
|
const esc1 = "\\u002F", esc2 = "\\\\u002F"; // \u002F и \\u002F (JSON-двойной)
|
||||||
|
const escPairs = [
|
||||||
|
["git.kzntsv.site" + esc2 + "victor" + esc2 + "sched", "github.com" + esc2 + "schedjs" + esc2 + "schedjs"],
|
||||||
|
["git.kzntsv.site" + esc1 + "victor" + esc1 + "sched", "github.com" + esc1 + "schedjs" + esc1 + "schedjs"],
|
||||||
|
["registry.kzntsv.site" + esc2 + "sched-daemon", "ghcr.io/schedjs" + esc2 + "sched-daemon"],
|
||||||
|
["registry.kzntsv.site" + esc1 + "sched-daemon", "ghcr.io/schedjs" + esc1 + "sched-daemon"],
|
||||||
|
["verdaccio.kzntsv.site" + esc2 + ":_authToken=${VERDACCIO_TOKEN}", "registry.npmjs.org" + esc2 + ":_authToken=${NPM_TOKEN}"],
|
||||||
|
["verdaccio.kzntsv.site" + esc1 + ":_authToken=${VERDACCIO_TOKEN}", "registry.npmjs.org" + esc1 + ":_authToken=${NPM_TOKEN}"],
|
||||||
|
["verdaccio.kzntsv.site" + esc2, "registry.npmjs.org" + esc2],
|
||||||
|
["verdaccio.kzntsv.site" + esc1, "registry.npmjs.org" + esc1],
|
||||||
|
["registry.kzntsv.site" + esc2, "ghcr.io/schedjs" + esc2],
|
||||||
|
["registry.kzntsv.site" + esc1, "ghcr.io/schedjs" + esc1],
|
||||||
|
];
|
||||||
|
|
||||||
|
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
||||||
|
const p = `${dir}/${e.name}`;
|
||||||
|
return e.isDirectory() ? walk(p) : [p];
|
||||||
|
});
|
||||||
|
let changed = 0;
|
||||||
|
for (const f of walk(pubDist)) {
|
||||||
|
if (f.endsWith(".svg")) continue;
|
||||||
|
let data;
|
||||||
|
try { data = fs.readFileSync(f, "utf8"); } catch { continue; }
|
||||||
|
let orig = data;
|
||||||
|
for (const [old, nw] of pairs) data = data.split(old).join(nw);
|
||||||
|
for (const [old, nw] of escPairs) data = data.split(old).join(nw);
|
||||||
|
if (data !== orig) { fs.writeFileSync(f, data); changed++; }
|
||||||
|
}
|
||||||
|
log(`scrub: dist скопирован, изменено ${changed} файлов`);
|
||||||
|
|
||||||
|
// logo.svg НЕ коммитить (gotcha 4)
|
||||||
|
if (fs.existsSync(`${pubDist}/logo.svg`)) fs.rmSync(`${pubDist}/logo.svg`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
// сначала immutable; если падает — регенерация и перепроверка
|
||||||
|
const imm = shSilent(`cd "${PUB}" && yarn install --immutable`);
|
||||||
|
if (imm === null || imm.includes("YN0028") || /Failed with errors/.test(imm)) {
|
||||||
|
log("lock: immutable упал — регенерирую против npmjs");
|
||||||
|
const regen = shSilent(`cd "${PUB}" && yarn install`);
|
||||||
|
if (regen === null || /Failed with errors/.test(regen)) { fail("lock: yarn install упал"); return false; }
|
||||||
|
const imm2 = shSilent(`cd "${PUB}" && yarn install --immutable`);
|
||||||
|
if (imm2 === null || /Failed with errors/.test(imm2) || imm2.includes("YN0028")) {
|
||||||
|
fail("lock: immutable после регенерации всё ещё красный"); return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("lock: yarn.lock валиден против npmjs");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gate() {
|
||||||
|
// CI-эквивалент прямыми вызовами node_modules (Windows: yarn workspace run не видит .bin)
|
||||||
|
// gotcha 5: build до tsc (cross-package types через dist) — собрать dist всех пакетов
|
||||||
|
// топологический порядок (как yarn workspaces -pt): пакет раньше своих @schedjs-потребителей
|
||||||
|
const buildCfgs = [];
|
||||||
|
for (const sub of ["packages", "apps"]) {
|
||||||
|
for (const dir of fs.readdirSync(`${PUB}/${sub}`)) {
|
||||||
|
const cfg = `${PUB}/${sub}/${dir}/tsconfig.build.json`;
|
||||||
|
if (fs.existsSync(cfg)) buildCfgs.push(`${sub}/${dir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const order = [];
|
||||||
|
const remaining = new Set(buildCfgs);
|
||||||
|
const depsOf = (p) => {
|
||||||
|
const pj = JSON.parse(fs.readFileSync(`${PUB}/${p}/package.json`, "utf8"));
|
||||||
|
const deps = { ...(pj.dependencies || {}), ...(pj.devDependencies || {}), ...(pj.peerDependencies || {}) };
|
||||||
|
return Object.keys(deps).filter((d) => d.startsWith("@schedjs/"));
|
||||||
|
};
|
||||||
|
const localName = (p) => {
|
||||||
|
const pj = JSON.parse(fs.readFileSync(`${PUB}/${p}/package.json`, "utf8"));
|
||||||
|
return pj.name;
|
||||||
|
};
|
||||||
|
const localNames = new Map(buildCfgs.map((p) => [localName(p), p]));
|
||||||
|
while (remaining.size) {
|
||||||
|
let progress = false;
|
||||||
|
for (const p of [...remaining]) {
|
||||||
|
const unmet = depsOf(p).filter((d) => localNames.has(d) && !order.includes(localNames.get(d)));
|
||||||
|
if (unmet.length === 0) { order.push(p); remaining.delete(p); progress = true; }
|
||||||
|
}
|
||||||
|
if (!progress) { fail("gate: цикл зависимостей build-пакетов"); return false; }
|
||||||
|
}
|
||||||
|
for (const pkg of order) {
|
||||||
|
const clean = `${PUB}/${pkg}/dist`;
|
||||||
|
if (fs.existsSync(clean)) fs.rmSync(clean, { recursive: true, force: true });
|
||||||
|
const b = shSilent(`cd "${PUB}/${pkg}" && node ../../node_modules/typescript/bin/tsc -p tsconfig.build.json`);
|
||||||
|
if (b === null || /error TS/.test(b)) {
|
||||||
|
fail(`gate: build ${pkg} упал`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// полный build-скрипт пакета может включать bundle (esbuild) — повторить
|
||||||
|
const pj = JSON.parse(fs.readFileSync(`${PUB}/${pkg}/package.json`, "utf8"));
|
||||||
|
if ((pj.scripts?.build || "").includes("bundle.mjs")) {
|
||||||
|
const bun = shSilent(`cd "${PUB}/${pkg}" && node scripts/bundle.mjs`);
|
||||||
|
if (bun === null) { fail(`gate: bundle ${pkg} упал`); return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(`gate: build ${order.length} пакетов (dist, топо-порядок)`);
|
||||||
|
|
||||||
|
const tsc = shSilent(`cd "${PUB}" && node node_modules/typescript/bin/tsc --noEmit`);
|
||||||
|
if (tsc === null || /error TS/.test(tsc)) {
|
||||||
|
const errs = (tsc || "").split("\n").filter((l) => l.includes("error TS")).slice(0, 10);
|
||||||
|
fail(`gate: tsc — ${errs.length} ошибок${errs.length ? "\n" + errs.join("\n") : ""}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log("gate: tsc зелёный");
|
||||||
|
|
||||||
|
const vit = shSilent(`cd "${PUB}" && node node_modules/vitest/vitest.mjs run --config vitest.ci.config.ts`);
|
||||||
|
if (vit === null || /failed/.test(vit)) {
|
||||||
|
fail("gate: vitest упал (смотри вывод)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const passLine = (vit.match(/(\d+) passed/) || [])[1] || "?";
|
||||||
|
log(`gate: vitest зелёный (${passLine} passed)`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Портативный скан (gotcha 16): прежний `grep -rnE '…'` через cmd.exe на Windows НЕ работал:
|
||||||
|
// cmd не снимает одинарные кавычки, а `|` для него — оператор pipe → команда падала,
|
||||||
|
// shSilent возвращал null, проверка молча считалась зелёной (0 хитов). Теперь скан — на Node.
|
||||||
|
function grepCheck() {
|
||||||
|
const re = /git\.kzntsv|verdaccio|vds\.kzntsv|registry\.kzntsv|VERDACCIO/i;
|
||||||
|
const SKIP = new Set(["node_modules", ".git", ".yarn"]);
|
||||||
|
const BIN = /\.(svg|png|webp|jpg|jpeg|gif|ico|woff2?|ttf|zip|gz)$/i;
|
||||||
|
const hits = [];
|
||||||
|
const walk = (dir) => {
|
||||||
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const p = `${dir}/${e.name}`;
|
||||||
|
if (e.isDirectory()) { if (!SKIP.has(e.name)) walk(p); continue; }
|
||||||
|
if (BIN.test(p)) continue;
|
||||||
|
let data;
|
||||||
|
try { data = fs.readFileSync(p, "utf8"); } catch { continue; }
|
||||||
|
data.split("\n").forEach((line, i) => {
|
||||||
|
if (re.test(line)) hits.push(`${path.relative(PUB, p)}:${i + 1}: ${line.trim().slice(0, 140)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(PUB);
|
||||||
|
if (hits.length > 0) {
|
||||||
|
fail(`grep: остались ${hits.length} вхождений kzntsv/verdaccio\n` + hits.slice(0, 10).join("\n"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log("grep: 0 вхождений kzntsv/verdaccio (портативный скан)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
const msg = `chore: sync curated copy from dev (${shSilent(`cd "${DEV}" && git log -1 --format=%h`) || "?"})`;
|
||||||
|
sh(`cd "${PUB}" && git add -A`);
|
||||||
|
const staged = shSilent(`cd "${PUB}" && git status -s`);
|
||||||
|
if (!staged || staged.trim() === "") {
|
||||||
|
log("commit: нет изменений — волна уже синхронизирована");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (AMEND) {
|
||||||
|
shSilent(`cd "${PUB}" && git commit --amend --no-edit`);
|
||||||
|
log("commit: amended");
|
||||||
|
} else {
|
||||||
|
sh(`cd "${PUB}" && git commit -m "${msg}"`);
|
||||||
|
log(`commit: ${shSilent(`cd "${PUB}" && git log -1 --format=%s`)}`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── main ────────────────────────────────────────────────────────────────────
|
||||||
|
if (!fs.existsSync(`${PUB}/.git`)) { fail("pub не git-репо"); process.exit(1); }
|
||||||
|
if (!fs.existsSync(`${DEV}/.git`)) { fail("dev не git-репо"); process.exit(1); }
|
||||||
|
|
||||||
|
const files = collect();
|
||||||
|
if (!files) process.exit(1);
|
||||||
|
|
||||||
|
if (ONLY && ONLY !== "lock") { fail("--only поддерживает: lock"); process.exit(1); }
|
||||||
|
|
||||||
|
let ok = true;
|
||||||
|
if (!ONLY) {
|
||||||
|
if (!NO_COPY) { copy(files); bleach(); scrubDist(); }
|
||||||
|
ok = grepCheck() && ok;
|
||||||
|
}
|
||||||
|
if (ONLY === "lock") {
|
||||||
|
ok = lock() && ok;
|
||||||
|
} else if (!NO_COPY) {
|
||||||
|
ok = lock() && ok;
|
||||||
|
}
|
||||||
|
if (ok && !ONLY && !NO_COPY) {
|
||||||
|
ok = gate() && ok;
|
||||||
|
if (ok) ok = commit() && ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
log("\n✗ цикл прерван — мутаций после точки отказа нет. Фикс в dev → повтор.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!PUSH) {
|
||||||
|
log(`\n✓ готово до пуша: ${shSilent(`cd "${PUB}" && git log --oneline -1`)}`);
|
||||||
|
log(" запусти с --push чтобы отправить origin main");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const p = shSilent(`cd "${PUB}" && git push origin main`);
|
||||||
|
if (p === null || /rejected/.test(p)) { fail("push отклонён — pull --ff-only и повтори"); process.exit(1); }
|
||||||
|
log("✓ pushed origin main");
|
||||||
Reference in New Issue
Block a user