diff --git a/ПИЛОТ.md b/ПИЛОТ.md index ae2afcf7..3efeb1fd 100644 --- a/ПИЛОТ.md +++ b/ПИЛОТ.md @@ -8,6 +8,8 @@ - Волатильную часть (доступ, версии, что развёрнуто) перед рискованными действиями **перепроверять реальной командой по SSH**, не доверять снимку вслепую. - Обновляется по команде заказчика **«обнови пилот»**. +**Снимок снят:** 19.06.2026 (~19:10 МСК) — **🚀 ВЫКАЧЕНЫ на боевой `liderra.ru` все go-live находки G1–G7** (коммиты gitea `01a9029c`, schema до v8.49). **Что теперь живёт:** G1 самозапись клиентов (почта+капча+6-зн.код+реквизиты по ИНН), G2 почтовый дайджест новых сделок (вкл. по умолчанию), G3 удалены «Напоминания» (вкл. таблица `reminders`), G4 убран Push, G6 публичный read-API сделок по ключу, G7-A раздел «Помощь» (`support_requests`), **G7-B дверь «вход под клиентом» + машинный ключ ИИ `lpimp_`** (новая колонка `impersonation_tokens.session_token_hash`). **G5 (пополнение) — НЕ катился** (ждёт ООО/Б-1). **Метод выката (важно для будущего — github мёртв):** `git archive 01a9029c` overlay + фронт-сборка → scp через бастион `ssh liderra-prod` (внутр. `10.128.0.15`, ProxyJump `liderra-bastion`) → **`sudo tar -x`** (код прода `root:root`, папки `www-data` — без sudo не перезаписать) → `migrate --force`. **Грабли миграций:** роль миграций в `.env` = `crm_app_user`, но старые таблицы (`reminders`, `impersonation_tokens`) принадлежат `crm_migrator` → DROP/ALTER падают «must be owner». Применено под `sudo -u postgres psql` (DROP reminders CASCADE + ALTER impersonation_tokens ADD session_token_hash) + ручная запись в `migrations` (batch 17); `support_requests` создалась штатным migrate (CREATE доступен crm_app_user). Затем `optimize:clear`+`optimize` (route/config-кэш!) + restart php8.3-fpm/liderra-queue. **Прод-хвост:** заведена стаб-запись `saas_admin_users` id=1 (для `requested_by` двери). **Сверка перед выкатом (ювелирно):** 436/515 файлов один-в-один; 8/8 денежных файлов (LeadRouter/RouteSupplierLeadJob/LedgerService/BalancePreflight/SupplierWebhook/LeadRegionResolver) — IDENTICAL (не откачены); 56 отличий — все «знакомые нашей истории gitea» (0 чужих прод-правок, проверено `git hash-object`+`cat-file -e`); 18 мёртвых файлов удалены (старые reminders/register/PhoneNormalizer). **Verify после:** liderra.ru→200 (снаружи+изнутри), **client1 (tenant 2 «Компания 1») ЦЕЛ: баланс 1 835 400 ₽ + 1013 сделок не тронуты**, фронт-manifest совпал, дверь за nginx-паролём (/api/admin→401), 0 ошибок в логе. **Бэкапы для отката на сервере:** `/home/ubuntu/preG7B-backup-20260619-160611.sql.gz` (БД, 63М) + `/home/ubuntu/preG7B-code-20260619-161117.tar.gz` (код, 4.9М). **Хвост:** живой прод-тест двери — за владельцем (у него nginx-пароль). Кодовая фраза стены — «роутер-наставник». + **Снимок снят:** 30.05.2026 (~05:30 МСК) — **🚀 ВЫКАЧЕНО на боевой `liderra.ru` Stage 5 findings F1+F2 + точечный quick-fix supplier_projects.** Полный цикл за сессию: pre-deploy 8-check (override NO-GO 2/8 — false positives) → deploy F1+F2 → +60min monitoring → диагностика остаточного storm → quick-fix UPDATE supplier_projects → verify. **Deploy run `26651411374` SUCCESS** (29.05 17:18 UTC = 20:18 МСК) через `gh workflow run deploy.yml --ref main`. F1 advisory-lock миграция `2026_05_30_000001_add_advisory_lock_to_audit_chain_hash` в batch [13] на проде. F2 webhook fast-fail (RouteSupplierLeadJob + storm detection + sql-runner whitelist) — код задеплоен. **Pre-deploy 8 checks NO-GO 2/8** (override после анализа): Check 1 false-positive по quirk 107 (config.php owner `ubuntu:www-data` вместо `www-data:www-data` — self-healing через deploy.yml `php artisan config:cache` после раскатки), Check 2 false-positive в bash regex (`.env CRLF chars: 0` но `[ "$CRLF_COUNT" = "0" ]` ловит multi-line из `grep -c`). Checks 3-8 GREEN (disk 46%, backup 1.5h, queue/nginx/fail2ban active, 0 pending migrations). prod-deploy-validator агент дал false NO-GO из-за YC backbone SSH-фильтра на dev-IP — все 8 проверок шли через Azure runner вместо. **Post-deploy verify (через `stage5-daily-monitor.yml`):** scheduler:check-heartbeats clean, incidents:watch-failures clean, `project_routing_snapshots` 113 проектов за 28.05 и 29.05 (snapshot за 30.05 пустой = НОРМА: суббота, ни один из 113 projects не имеет суббота в `delivery_days_mask`; `SnapshotProjectRoutingJob` last_run_at=29.05 15:02 UTC runtime_ms=0 success), `audit:verify-chains` ✅ chain intact на всех 50+ партиций × 6 audit-таблиц. **F2 эффект:** новые failures упали с ~270/min (pre-deploy 488 325 за 24h) до ~21/min (post-deploy измерение 319 events за 15 мин). Counter скользящего окна 24h: 488 325 → 387 292 за 60 мин (старый пик 14:52 UTC 29.05 выходит из окна, новых добавляется в 13× меньше). **Storm source 30.05 02:02:08-02:02:11 UTC:** 319 событий за 3 секунды от **одного** supplier_lead 1352 / supplier_project (B3, вашиденьги24.рф). Корень — поставщик у себя сменил тип кампании site→sms (webhook payload содержит массив `phones[]` — признак SMS), наш `supplier_projects` 292/293 (B2/B3) остался `signal_type='site'` → resolver выбрасывает `unique_key collision across signal types is not allowed` → поставщик ретраит → F2 fast-fail режет до retry_count=3 (без F2 был бы infinite loop). **Quick fix 30.05 02:30:31 UTC:** `UPDATE supplier_projects SET signal_type='sms' WHERE id IN (292,293) AND unique_key='вашиденьги24.рф'` через расширенный `sql-runner.yml` whitelist (commit `7a1cab6a` добавил `update supplier_projects` в MUTATING_RE). **Первая попытка** с `id IN (291,292,293)` упала на `chk_supplier_projects_b1_not_for_sms` (B1 платформа hard-rule = site всегда); retry с `id IN (292,293)` — `UPDATE 2` SUCCESS. State после fix: 291 B1 site (сохранён), 292 B2 sms ✅, 293 B3 sms ✅. **Verify через 29 мин:** 0 новых failures от вашиденьги24.рф после fix (`COUNT FILTER WHERE failed_at > '2026-05-30 02:30:31+00' AND exception LIKE '%вашиденьги24%'` = 0). **Open хвосты (не блокеры):** (а) **Расширить `SyncSupplierProjectsJob`** — подхватывать смену signal_type у поставщика автоматически, иначе каждая такая смена даст ручной storm (P2 отдельный план); (б) **F2 fast-fail tune** для классов permanent error (collision/validation) — `max_retries=1` вместо 3 (P2 отдельный план, уменьшит volume в 3×); (в) `billing:frozen-reminder` consecutive_failures=1 на 29.05 15:30 UTC — разбор last_error (P3); (г) счётчик 387 292 продолжит снижаться сам — старый пик ~14:52 UTC выходит из 24h окна. **Memory updates:** новая `feedback_supplier_projects_b1_not_sms_constraint.md` (B1 hard-rule) + новая `project_storm_2026_05_30_vashidengi24.md` (хроника + open follow-ups) + UPDATED `MEMORY.md` index (Stage 5 findings 1+2 MERGED → DEPLOYED с verified фактами). **Methodology:** chain через `prod-deploy-validator` агент → workaround через `pre-deploy-checks.yml` workflow (Azure runner обходит YC backbone) → `stage5-daily-monitor.yml` для post-deploy verification → `sql-runner.yml` для диагностики (3 ad-hoc SELECT запроса нашли единый источник storm) → расширение whitelist + quick fix. **Снимок снят:** 29.05.2026 (день+2, ~21:00 МСК) — **🚀 ВЫКАЧЕНО на боевой `liderra.ru` ADR-018 Stage 5 follow-up implementation + cleanup 3 партиций.** Полный цикл за одну сессию: writing-plans → 8 task-коммитов → deploy → 3 prod cleanup runs → verify. **15 коммитов на `origin/main` (`03df0608..c6a47483`):** 8 implementation (Task 1-7 + closure) + 5 cleanup infrastructure (pre-deploy-checks.yml + path fix + dry-run whitelist + sql-rebuild-audit-chain.yml ×2) + 2 docs (ADR enforcement + handoff post-cleanup update). **Deploy run `26646633140` SUCCESS** через `gh workflow run deploy.yml --ref main` (Azure runner, обход YC backbone filter). **Pre-deploy 8-check validation:** новый workflow `.github/workflows/pre-deploy-checks.yml` — 6 PASS (disk 46%, backup 4h, queue/nginx/fail2ban active, 0 pending migrations), 1 false-positive (.env CRLF bash quirk), 1 hygiene (`bootstrap/cache/config.php` owner `ubuntu:www-data` вместо `www-data:www-data` — но НЕ quirk 107 root-owner pattern, через group access работает). User override после анализа — deploy approved. **Cleanup 3 партиций — 18 mismatches → 0:** обнаружилось что race condition бил по 3 tenant-scoped таблицам, не 1 (как думали по Stage 5 findings memory): `activity_log_y2026_m05` (id=599, 6→0, 3 tenants × 216 rows), `balance_transactions_y2026_m05` (id=462, 6→0, 3 tenants × 243 rows), `pd_processing_log_y2026_m05` (id=191, 6→0, 3 tenants × 220 rows). Всего 679 rows / 9 tenant-scopes. `audit:verify-chains` → `All audit chains intact.` ✓ на всех 6 audit-таблицах. **⚠️ Архитектурный gap раскрылся:** Laravel `AuditRebuildChain` не работает на проде с `pgsql_supplier` connection (роль `crm_supplier_worker` BYPASSRLS но НЕ SUPERUSER — не может `SET session_replication_role = 'replica'` который требуется для disable BEFORE-triggers `audit_block_mutation`). Tests проходят потому что test env использует `postgres` superuser → false confidence. **Workaround использованный 29.05:** новый workflow `.github/workflows/sql-rebuild-audit-chain.yml` — параметризованный (partition + from_id + table_kind) PL/pgSQL DO-блок mirror'ящий `AuditRebuildChain::rebuildScope()` PHP логику через `sudo -u postgres psql` (superuser обходит permission). Поддерживает 4 tenant-scoped таблицы (`activity_log` / `balance_transactions` / `pd_processing_log` / `tenant_operations_log`). **Open хвосты (не блокеры):** (а) **Архитектурный fix Laravel AuditRebuildChain** — отдельный план P2: добавить `pgsql_postgres` connection в `config/database.php` + переписать команду использовать его. Workaround workflow доступен для дальнейших cleanup'ов; (б) **Manual `incidents_log` closure** через SaaS-admin UI (`resolved_at = now()`, `root_cause = "cleanup per ADR-018 rebuild fix"`); (в) **Optional:** удалить `sql-rebuild-audit-chain.yml` workflow после архитектурного fix'а (либо оставить как backup-tool). **Memory updates:** новая `feedback_audit_rebuild_laravel_permission_gap.md` + UPDATED `feedback_audit_chain_algorithm_divergence.md` (Stage 5 follow-up CLOSED status) + MEMORY.md index. **Methodology:** subagent-driven-development skill для Task 2 (Sonnet субагент крашнулся API socket после 107 tool_uses → recovery inline), Task 4 deviation from plan SQL (plan'овский CTE+LAG+UPDATE имеет snapshot-isolation bug → использовал PHP loop с partition awareness). **Workflows ran:** deploy.yml ×1, pre-deploy-checks.yml ×2 (после path fix), sql-rebuild-audit-chain.yml ×3, artisan-run.yml ×5. **Прод-код изменён** относительно предыдущего deploy `26634115769`: новый `AuditChainConfig` shared class, refactor `VerifyAuditChains` на shared config, `AuditRebuildChain` переписан под per-tenant LAG (но не работает на проде из-за permission gap → не используется, остаётся ждать архитектурного fix'а).