# Хвосты сессии — разблокировка смены источника (Эпики 1–6)

> ✅ **СТАТУС 26.06.2026: ВЫКАЧЕНО НА БОЕВОЙ liderra.ru, ФЛАГ ВКЛЮЧЁН, ТУМБЛЕР В АДМИНКЕ.** Ветка влита в gitea `main` (`3cedf28f..fbf982e1`), выкачена скриптом `bin/deploy-source-edit.sh`. Флаг `routing_match_by_snapshot=true` (фича боевая активна). Дружелюбный тумблер ВКЛ/ВЫКЛ на экране «Интеграция с поставщиком» (коммит `f9f86ca0`). Идёт сутки наблюдения (drift=0, сверять «Вечерней заливкой»). Деньги клиента целы (1 838 400 ₽ / 1013). Детали выката — `ПИЛОТ.md` (снимки 26.06 ~01:00 и ~01:25). Откат — тумблер в ВЫКЛ или флаг `false`. Хвосты ниже — историческая хроника до выката.

## 🔍 26.06 — пост-выкатная сверка и полный прогон тестов НА ПРОДЕ

**Сверка прод === gitea === локалка (байт-в-байт):** клон gitea main на проде + sha256 каждого из **1105** git-файлов под `app/`: **0 расхождений**. Локалка==gitea (один коммит). Прод собран ровно из git, правок мимо git нет. Лишние на проде только окруженческие (`.env`, `bootstrap/cache/*` — генерятся `artisan optimize`).

**Полный прогон тестов на боевом Linux (изолированная база, живая `liderra` не тронута): 1730 прошло, 11 упало, 3 skip** (689с, последовательно). 
- **Подтверждено: прежние «падения» — окружение, не баги.** AutoPause (падал после 21:00 МСК — time-of-day фикстура), SchemaDelta (мой фикс), `--parallel` (OOM воркеров на винде) — ВСЕ прошли на Linux.
- **11 упавших — инфраструктурно-зависимые, не бизнес-логика:** audit-цепочка под конкуренцией + verify-chains (tamper/incident), auth_log события, drop/create партиций по сроку, CSV-импорт Россвязи, DemoSeeder идемпотентность. Артефакты разовой тест-базы из `schema.sql` (партиции/конкуренция/файлы-данные иначе, чем на эталонной dev/CI). Денежные пути (биллинг/лиды/раздача/синк/сделки/проекты/RLS) — все зелёные.
- **Браузерные тесты исключены** (виснут на проде без мока): `tests/Browser/*`, supplier-portal `SupplierPortalClientReport/RtProject/SessionRefreshCommand/FormProjectChannel`. NB: удалять можно только ФАЙЛЫ внутри `tests/Browser/`, не саму папку (phpunit.xml testsuite → «directory not found»).

**Рецепт безопасного полного прогона на проде (изолированно)** — память [[feedback-prod-full-test-isolated-db]]:
1. `DROP DATABASE IF EXISTS liderra_testing WITH (FORCE)` (PG16 — сам закрывает висящие коннекты) + `CREATE ROLE test_runner LOGIN SUPERUSER PASSWORD ...` + `CREATE DATABASE liderra_testing OWNER test_runner`.
2. `psql -d liderra_testing -f db/schema.sql` (несёт схему + сиды suppliers/system_settings) + `php artisan partitions:create-months --ahead=2` (иначе аудит-INSERT падает «нет партиции» → 422).
3. Клон gitea в /tmp; `cp -r` прод-vendor (НЕ symlink — у symlink автозагрузчик заточен под прод-путь, классы тестов получают чужой namespace) + `composer dump-autoload -o`; `mkdir storage/framework/{views,cache/data,sessions,testing}` + chmod 777 (иначе «valid cache path»); чистый `.env` строго на liderra_testing+test_runner.
4. Прогон detached (`nohup ... &`) — переживает обрыв ssh (квирк #109). НЕ `set -e` вокруг `php artisan test` (вернёт не-ноль на падениях → оборвёт до печати итога).
5. Уборка: DROP базы+роли, rm клона/`.env`/логов.
**🚨 Грабли:** широкий `pkill -f artisan` на проде УБИВАЕТ боевой `queue:work` (systemd поднимает) — бить только по `"artisan test"` или `/tmp/liderra-verify`, не по `artisan`.

**Дата:** 25–26.06.2026. **Ветка:** `feat/source-edit-snapshot-routing` (выкачена в main 26.06).
**План:** [docs/superpowers/plans/2026-06-25-source-edit-unblock-snapshot-routing.md](../plans/2026-06-25-source-edit-unblock-snapshot-routing.md).

## Что СДЕЛАНО и закоммичено (все эпики 1–6 закрыты)

| Эпик | Суть | Коммит |
|---|---|---|
| 2 (ядро) | Матч источника по слепку, хвост старого источника доезжает, нет двойного списания, отложенное удаление донора | `cc6c48b4` `c8ec7a66` `b7379e7a` `35d51df6` |
| 1 | read-only поля source_locked/unlock_at/projected в ProjectResource | `b1fe11b7` |
| 3 | UX: источник редактируем, баннер applies-from, диалог подтверждения (дроуэр) | `7337bd23` |
| 3 (доп) | окно «Редактировать» (NewProjectDialog) приведено к тому же поведению | `941b4b9f` |
| 4 | онлайн-заморозка 18:00→00:00 (supplier_deferred_sync) + досыл 00:05 (FlushDeferredOnlineSyncJob) | `9d703ccb` |
| 5 | экран отчёта вечерней заливки (supplier_sync_runs + AdminSupplierIntegrationView) | `1d18933d` |
| 6.1/6.2/6.3 | ProjectRuleMessages (единый текст) + in-app уведомления + диалоги тянут текст из API | `be082396` `e655af62` `64a76a21` |
| доп | сквозной тест потока лида для изменённого/удалённого проекта | `d98fc3c8` |
| доп | объявления АКТУАЛЬНЫ по времени суток (баннер firstLeadDate + guard-сообщение computeGraceUntil) | `ca923e4d` |
| фикс | регрессия ProjectServiceGuardWiring (мок не ждал isProtected) | `91690c4f` |

**2 новые таблицы** (SaaS-level без RLS как supplier_csv_reconcile_log): `supplier_deferred_sync` (v8.54), `supplier_sync_runs` (v8.55). RLS-ревью 7/7 обе. Миграции накачены на dev + liderra_testing.

**Проверено глазами (Playwright):** дроуэр + окно «Редактировать» (баннер 27 июня, подтверждение источника), экран «Вечерняя заливка», единый текст правил, time-aware сообщение удаления (до/после 26 июня). 0 ошибок консоли (кроме ожидаемых 422 при блоке).

**Регрессия:** широкий бэк 883 теста (после фикса 0 падений), целевой фронт зелёный. Полный фронт — см. долг ниже.

---

## ❗ ХВОСТЫ — что осталось

### ✅ Проверено ГЛАЗАМИ 26.06 (Playwright, подмена часов через page.clock)
1. **Баннер ДО/ПОСЛЕ 18:00** — ✅ ПОДТВЕРЖДЕНО. 13:00 МСК → «вступят в силу с 26 июня» (завтра); 19:00 МСК → «27 июня» (послезавтра). Реальный отрендеренный баннер в дроуэре. Скрин `verify-banner-before-18-26june.png`.
2. **Колокольчик (Эпик 6.2)** — ✅ ПОДТВЕРЖДЕНО. После сохранения правки лимита в колокольчике сверху появилось «Изменения сохранены — Изменения вступят в силу с 26 июня. N мин назад». Скрин `verify-bell-project-rule-notification.png`. NB: колокольчик грузится по 30-сек поллингу (на mount пропускается, пока `auth.user` не загружен — гонка с `/api/auth/me`), поэтому первые ~30с после полной перезагрузки пуст — давний UX-нюанс, не баг фичи.
   - Подчищено: тип `NotificationEvent` (api/notifications.ts) и icon-map (AppTopbar.vue) не знали про `project_rule` — добавлено (иконка `mdi-clock-edit-outline`). Аддитивно, рантайм и так работал (fallback-иконка).

### 🔴 НАЙДЕН РЕАЛЬНЫЙ БАГ (на проде!) + ФИКС с тестом
**Симптом:** на проекте, по которому уже летят лиды от поставщика (`isProtected`), смена ТОЛЬКО лимита/региона/дней ложно блокируется ошибкой «Изменить источник можно будет после N», хотя источник не трогали.
**Корень:** [ProjectService::update](app/app/Services/Project/ProjectService.php) считал `$sourceFieldsTouched` по ПРИСУТСТВИЮ ключа `signal_identifier`, а дроуэр для site/call ВСЕГДА его шлёт (даже неизменённый). → guard срабатывал на смену источника при правке только лимита.
**Где живёт:** оба компонента (`84272c5c` guard-wiring + `f248e277` always-send) — **на main (боевой)**. То есть баг живой на liderra.ru: клиент с поставщиковым проектом не может понизить дневной лимит/сменить регионы/дни — ловит ложное «нельзя сменить источник». (На моей ветке с флагом ВКЛ маскировалось early-return'ом, поэтому не всплыло раньше.)
**Фикс:** `$sourceFieldsTouched` теперь сравнивает ЗНАЧЕНИЕ (новый приватный `sourceValueChanged()`), а не присутствие ключа. RED→GREEN тест `ProjectServiceGuardWiringTest::test_update_does_not_invoke_guard_when_signal_identifier_present_but_unchanged`. Глазами: после фикса смена лимита на защищённом проекте → 200, applies_from корректный, уведомление создалось. Регрессия: guard-wiring+guard-unit 18/18, фича-тесты 44/44, ProjectController/Projects зелёные.
**Решить с владельцем:** это давний прод-баг — возможно стоит отдельным хотфиксом в main, не дожидаясь всей фичи слепка. НЕ закоммичено.

### ✅ Эпик 4 онлайн-заморозка — ПОДТВЕРЖДЕНО (функционально, dev)
3. Прогнал вживую на dev (режим временно online, окно 20ч МСК): **заморозка** — `SyncSupplierProjectJob(1)` положил строку в `supplier_deferred_sync` (project_id=1) и вышел, поставщика НЕ дёргал (отправка только в 00:05). **Досыл** — `FlushDeferredOnlineSyncJob` под `Bus::fake` прочитал таблицу (было 1), поставил `SyncSupplierProjectJob` в очередь, очистил строку (стало 0). Режим вернул в `batch`, таблица пуста. Расписание 00:05 в routes/console.php — доверяю (тест есть).

### 🟡 Давний фронт-тест-долг (НЕ моя регрессия)
4. **22 упавших vitest-теста в 10 файлах**, которых я не касался: SettingsView, ErrorView, LegalDocView, KanbanCard, DealsView, ChangePasswordCard, AdminPricingTiersView, DealDetailDrawerApi, **ProjectsView (BulkActionsBar)**, AdminSupplierIntegrationView.manual-queue. Диффом подтверждено — я менял только 4 фронт-файла (ProjectDetailsDrawer.vue, NewProjectDialog.vue, AdminSupplierIntegrationView.vue, projectsStore.ts), эти спеки на components, что я не трогал → их результат = как на main. Долг существовал до сессии.
5. `manual-queue.spec.ts` тест «clicking Отметить выполнено» — стейл после рефактора window.confirm→v-dialog (падает и на чистом HEAD).

### 🟢 Нормативка / документы (план §«Финальная проверка»)
6. **Закрыть OPEN-вопрос §9a** (формат отчёта о заливке) в реестре открытых вопросов — решение принято: «экран в админке». Не отмечено.
7. **Синк нормативки** (Pravila / PSR / Tooling / CLAUDE.md) под 2 новые таблицы + новый экран/джоб — не делал (требует `normative-sync` агента, НЕ коммитит).
8. **Дрейф счётчиков схемы** в шапке schema.sql (таблицы/индексы) — честно помечен в v8.54/v8.55, но точная пересверка вынесена в отдельный canon-sync. CHANGELOG_schema.md header («тридцать записей v8.33→…») стал стейл — не перечисляет v8.54/v8.55.

### ⚙️ Перед боевым (порядок выката, не баги)
9. Полную `Pest --parallel` + полный `composer stan` (Larastan) + lychee/gitleaks — НЕ гонял целиком (гонял широко: 883 бэк + полный фронт vitest).
10. **prod-deploy-validator** перед выкатом liderra.ru — не запускал.
11. **Порядок:** включить флаг `routing_match_by_snapshot` → сутки наблюдать (drift=0, лиды доезжают) → ТОЛЬКО потом показывать клиентам Эпик 3 (иначе обещание «лиды придут» враньё). Экран «Вечерняя заливка» (Эпик 5) — для сверки глазами при наблюдении.

### 🧹 Мелочи окружения
12. На dev поставлен пароль `password` юзеру `inessa.samojlova@example.org` (вход в браузер) — безвредно.
13. Много untracked PNG-скринов проверок в корне + app/ (epic3-*, epic5-*, epic6-*, time-aware-*) — мусор, можно убрать.
14. Ничего не запушено — все 20 коммитов локально на `feat/source-edit-snapshot-routing`.

---

## 🌙 Сессия 26.06 (ночь) — конвейер «до выката» (владелец: «гони всё до выката, боевой не трогай»)

**Полная регрессия (последовательно, --parallel OOM'ит воркеры — окруженческий квирк):** 1756 тестов, 1750 ✅, 4 skip, 2 падения — оба разобраны:
1. **SchemaDeltaTest** — ждал 72 таблицы/127 индексов, стало 74/128 из-за моих 2 таблиц (v8.54/v8.55). ✅ ПОЧИНЕНО: тест приведён к v8.55 (74/128/44). NB: бегущий счётчик в ШАПКЕ schema.sql сам дрейфует (заявляет 79/124) — давний отдельный canon-sync, не предмет теста.
2. **AutoPauseFlowTest «2-е письмо после 65 мин»** — НЕ моя регрессия (агент pest-parallel-debugger, доказано: путь авто-паузы/письма байт-в-байт как main; мои LeadRouter-правки спят под флагом ВЫКЛ). Корень — **пре-существующий time-of-day квирк**: фикстура `createRoutingSnapshotFromProject` (tests/Pest.php) сеет слепок только на «сегодня», а `LeadRouter::activeSnapshotDate()` после 21:00 МСК берёт «завтра» → 0 кандидатов → паузы/письма нет. До 21:00 → 5/5 зелёных. **Фикс на будущее (не сейчас, чужой долг):** заморозить часы до 21:00 в beforeEach ИЛИ сеять слепок на сегодня+завтра.

**Нормативка (агент normative-sync):** правок в 4 управляющих документах НЕ нужно (фича не добавила инструментов/плагинов/правил; Pravila v1.44 / PSR v3.24 / Tooling v2.25 / CLAUDE v2.47 без изменений, cross-refs зелёные). Схема-доки (schema.sql header v8.55 + CHANGELOG_schema.md v8.54/v8.55) уже синхронны в ветке.
- Опционально (через claude-md-management, НЕ сделано): обновить §6 CLAUDE.md «последняя продуктовая фича» на эту фичу; снять устаревшую ремарку шапки про «синхронизацию квинтета на 2.47 — follow-up» (уже закрыт в PSR/Tooling 14.06).
- ⚠️ Агент поймал: `git fetch` к GitHub → 403 (аккаунт suspended). Рабочий remote — gitea; перед пушем проверить доступность.

**Готовность боевого (агент prod-deploy-validator): ВЕРДИКТ GO** — 8/8 зелёных (config читаем www-data/квирк107 закрыт, бэкап 14ч, место 56%, очередь жива с --timeout=300, nginx ок, fail2ban active, 0 лишних миграций; миграции фичи на проде ожидаемо отсутствуют).

**Порядок выката (из вердикта валидатора, на утро владельцу):**
1. Проверить доступ к remote (gitea), `git push` ветки → merge в main.
2. `php artisan migrate` — миграции `2026_06_25_120000` (supplier_deferred_sync) → `2026_06_25_130000` (supplier_sync_runs) по timestamp.
3. Проверить GRANT для `crm_supplier_worker` на обе новые таблицы (схема заявляет blanket-грант `ON ALL TABLES` в db/02_grants.sql — подтвердить после миграции).
4. Проверить, что в cron есть `schedule:run` (для FlushDeferredOnlineSyncJob 00:05 МСК = 21:05 UTC).
5. `sudo -u www-data php artisan config:cache && route:cache && view:cache` — под www-data (квирк 107).
6. Флаг `routing_match_by_snapshot` остаётся ВЫКЛ при первом выкате; включать осознанно отдельным действием → сутки наблюдать (drift=0) → потом Эпик 3 клиентам.
- Smoke: `curl -w '%{http_code}' https://liderra.ru/`; `php artisan migrate:status | tail`; `tail laravel.log`.

**Коммит:** подготовлен, НЕ сделан (под стеной нужен подписанный эскейп владельца через клик AskUserQuestion — пока спит, кликнуть некому). Текст в scratchpad. Файлы к стейджу (явные пути, без PNG/чужих преды): ProjectService.php, ProjectServiceGuardWiringTest.php, phpstan-baseline.neon, notifications.ts, AppTopbar.vue, SchemaDeltaTest.php, 2 findings-дока. НЕ трогать: CLAUDE.md/settings.json/ПИЛОТ.md/observer/dev-indices (чужие преды).

---

## Ключевые инварианты (не сломать при продолжении)
- Матч `queryCandidates`: `INNER JOIN projects ON id=snap.project_id` + `snap.snapshot_date=сегодня` → изменён источник (проект жив) = лид доезжает; удалён проект = лид не падает в сироту (JOIN отсекает), без краша.
- Удаление проекта гейтится `assertCanMutateSource('delete')` — нельзя, пока летит хвост (надо пауза + дождаться grace). Под флагом ВКЛ change_source разрешён, delete остаётся жёстким.
- Даты: лимит/регион/дни → правило 18:00 (`firstLeadDate`: до 18:00 завтра, после послезавтра); источник/удаление → правило 21:00 (`computeGraceUntil`). Согласованы: старый источник до N, новые настройки с N+1.
- Тексты правил — единый источник `ProjectRuleMessages` (бэкенд), фронт тянет через API (`source_change_message`), не дублирует.

---

## 🔧 26.06 — разбор «11 упавших» прод-прогона: НАЙДЕН реальный баг изоляции тестов + ошибка рецепта

**Запрос владельца:** «правь 11 оставшихся!» (после прод-прогона 1730/11).

**Что сделал (systematic-debugging, безопасно — только локально, прод не трогал):**
1. Локально на ПРАВИЛЬНО собранной БД (`liderra_testing`, собрана через `php artisan migrate`) все 8 «подозреваемых» файлов → **зелёные** (47 тестов, 0 падений). Значит это НЕ баги продукта/тестов.
2. Воспроизвёл условие прод-прогона локально: собрал БД `liderra_oneshot` тем же «коротким» способом, что и на проде (`psql -f db/schema.sql`), и прогнал ВЕСЬ набор → **1758: 1755 прошло, 2 упало, 1 skip**.
3. Оба падения идентичны: `register` → 422 «Аккаунт с таким email уже существует» (`AuthFlowIntegrationTest`, `AuthLogCoverageTest`) — ровно как на проде.
4. Контрольный прогон ВСЕГО набора на СВЕЖЕЙ `migrate`-собранной БД (`liderra_migrated`) → **1758: 1757 прошло, 0 упало, 1 skip**. На правильной свежей БД всё зелёное.

**Корневая причина (РЕАЛЬНЫЙ баг, доказан RED→GREEN):**
`RegistrationService` пишет `users/tenants/email_verifications` через BYPASSRLS-подключение **`pgsql_supplier`** (публичные роуты не выставляют `app.current_tenant_id`). Тесты `AuthFlowIntegrationTest` и `AuthLogCoverageTest` использовали `DatabaseTransactions` **БЕЗ** `SharesSupplierPdo` → записи через supplier-коннект коммитились мимо откатываемой транзакции и **НЕ откатывались** → тест переставал быть идемпотентным. На свежей БД — проходит; на «грязной»/повторном прогоне → 422 «email уже существует».
- RED: прогнал тест 1 раз → прошёл, но юзер `reg-log-test@example.ru` (is_active=t) ОСТАЛСЯ в БД (утёк). Прогнал 2-й раз → 422 (тот же баг, что на проде).
- Все ОСТАЛЬНЫЕ register-тесты (`RegistrationTest`, `ConfirmSetsEmailVerifiedAtTest`, `NewLeadEmailDefaultOnTest`) уже используют `SharesSupplierPdo` — эти 2 были единственными выпавшими из конвенции.

**Фикс:** добавил `uses(..., SharesSupplierPdo::class)` в оба файла (`app/tests/Feature/Auth/AuthFlowIntegrationTest.php`, `AuthLogCoverageTest.php`).
- GREEN: оба файла полностью (16/16) проходят ДВАЖДЫ подряд на «грязной» БД, 0 утёкших юзеров.

**Вторая причина части прод-падений — ОШИБКА РЕЦЕПТА прогона (не баг кода):** throwaway-БД собиралась `psql -f schema.sql` вместо `php artisan migrate`. Тесты на `RefreshDatabase` (DemoSeeder и ещё ~6 файлов) зовут `migrate:fresh`, который ломается на `psql`-собранной БД (нет строк в `migrations`, расхождение round-trip). Для throwaway-БД под superuser (`test_runner`) надо собирать через `php artisan migrate` — как CI/dev. Рецепт исправлен в памяти `feedback-prod-full-test-isolated-db`.

**Итог:** «11» = не 11 багов. Реальный — один (баг изоляции 2-х auth-тестов, исправлен). Остальное — артефакты способа сборки тестовой БД + переиспользование грязной БД (pollution). На правильной свежей БД весь набор зелёный.
