# Открытые вопросы к заказчику — Лидерра (v8.3)

**Назначение:** единый рабочий список вопросов, требующих решения заказчика для разблокировки разработки. Разбит по адресатам, внутри — по приоритету.

**Версия:** 1.85 от 22.05.2026 — **SEC-1..SEC-7 серверный слой: статусы приведены к факту деплоя на боевой тест-сервер liderra.ru (22.05.2026).** Серверная защита развёрнута до закрытия Б-1 (на тестовой VM Yandex Cloud): SEC-1 WAF ✅ (ModSecurity+CRS, боевой режим), SEC-2 анти-перебор ✅ (fail2ban+throttle), SEC-3 DDoS ⏸ (платный YC отклонён по цене; базовый L3/L4 активен; альтернатива Cloudflare), SEC-4 мониторинг ✅ (отчёт 07:00 + email-алерты; Sentry ⏸ Б-1/RAM), SEC-5 секреты 🟦 (Lockbox+KMS заведены, app-интеграция ⏸ нужен сервис-аккаунт YC), SEC-6 TLS/HSTS/CSP ✅ (Let's Encrypt + CSP боевой), SEC-7 бэкапы ✅ (ежедневные 03:30 + off-site зашифрованная копия на почту). Новый setup-док `docs/security/server-hardening-setup.md`. **§0 без изменений** (инфраструктура, не продуктовые Q-items; статусы — факт деплоя, не формальное закрытие §2.2). Связано: ADR-014 §9, memory `project_server_hardening`. Предыдущая v1.84 от 21.05.2026 — **A8 infosec серверный слой: +7 инфраструктурных security open-items (SEC-1..SEC-7)**. Эпик A8 infosec-tooling (плагины/скилы) формализовал инструменты безопасности портала, но серверный слой защиты боевого портала — не плагины, а инфраструктура — вынесен как открытые вопросы (out of scope A8, ADR-014 §9). SEC-1 WAF (Yandex Smart Web Security либо Coraza/ModSecurity), SEC-2 anti-brute-force / rate-limit (Laravel throttle + серверный), SEC-3 DDoS-защита (Yandex Cloud DDoS Protection), SEC-4 мониторинг вторжений (Sentry #34 pending Б-1 + серверные алерты), SEC-5 хранилище секретов (Yandex Lockbox), SEC-6 TLS/HSTS/CSP на бою, SEC-7 бэкапы + регламент реагирования (IR-runbook, реюз operations:runbook #51). Все 7 — привязка к **Б-1** (provisioning боевой инфраструктуры). **НЕ двигают счётчик продуктовых вопросов** (инфраструктура, не продуктовые Q-items — как DO-*). Связано: ADR-014, план `docs/superpowers/plans/2026-05-21-a8-infosec-tooling.md`. Предыдущая v1.83 от 13.05.2026 (day +1) — **CTO-19 ✅ closed** через Lucide migration: `npm i lucide-vue-next` + custom Vuetify `IconSet` (`liderraLucideSet`) в `app/resources/js/plugins/vuetify.ts` с **103-entry mapping** (78 user-grep'нутых mdi-* + 25 Vuetify-internal defaults: pagination chevrons, v-select dropdown, v-checkbox squares, v-radio circles). 51 Vue/TS файл untouched. Fallback `HelpCircle` для unmapped. CLAUDE.md §2 «Иконки: Lucide» — бренд-spec compliance achieved. **Verification (13.05.2026 day +1):** Pest --parallel 742/739/0/3 ✅, Vitest 88 files / 683 passed + 3 skipped, Vite build 3.52s, axe-core scan `/admin/billing` **0 iconography violations**. Visual smoke 8 views (`/dashboard`, `/projects`, `/settings`, `/admin/billing`, `/no-such-404`, `/kanban`, `/deals`, `/admin/tenants`) — все glyph'ы рендерятся. **Сводка §0 update:** CTO 17✅/1⏸/1 P2 [?] → 18✅/0⏸/0; Итого 70✅/12⏸ → 71✅/11⏸ (без P2 [?] CTO-19). Spec/plan в [docs/superpowers/specs/2026-05-13-cto-19-lucide-icon-migration-design.md](superpowers/specs/2026-05-13-cto-19-lucide-icon-migration-design.md) + [docs/superpowers/plans/2026-05-13-cto-19-lucide-icon-migration.md](superpowers/plans/2026-05-13-cto-19-lucide-icon-migration.md). Через `superpowers:brainstorming` → `superpowers:writing-plans` → `superpowers:subagent-driven-development` (Task 1 install + Task 2 IconSet + Task 2.b extension через subagents; visual smoke + closure controller-driven). Path α (aliases-only, approved заказчиком). Path (i) `npm i @mdi/font` REJECTED (250 KB CSS, против бренда). Path β rename all strings REJECTED (большой diff 51 файл). Pravila §2.2 / §7.1 — явное «закрываем CTO-19 как ✅» получено от заказчика после comprehensive verification (pagination + 7+ views + axe-core a11y). Предыдущая v1.82 — **Catch-up bump v1.77 → v1.82** после обнаружения hidden inconsistency (header заявлял v1.77, но footer-trail накопил v1.78/v1.79/v1.80/v1.81 без обновления шапки + post-Plan-4 work на 12.05–13.05.2026 не отражена в реестре). Catch-up закрывает: **v1.78** Plan 4 (Billing + CSV Reconcile + Admin) closure → +7 Биз-25..31 ⏸ (section ## 13 collision с Аудит C resolved → Plan 4 теперь ## 14); **v1.79** CTO-19 ⏸ open (иконочная система `@mdi/font` не подключена); **v1.80** CTO-20 ⏸ open (cyrillic case-insensitive search broken); **v1.81** CTO-20 ✅ closed (ICU collation fix на dev `liderra`). Plus post-Plan-4 context (12.05–13.05): Plan 5 frontend Tasks 7-11 delivered (schema v8.20: `projects.archived_at` + `tenants.limits`), Quiet Luxury portal redesign 20 commits Direction A, Portal full audit 2026-05-12 ночь (14 audit + Q.DEFER.002 sub-B / Q.DEFER.003 sub-A+B+C / Q.DEFER.004 sub-A+B closures, 10/10 audit-internal closed — tracked в `docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md`), audit-cleanup tail (5 commits `54c69a6..d1b2f5d`), R15 motion-runtime cleanup merge `323957a` (plan5 ← origin/main; PSR_v1 v1.7 → v2.0, Pravila v1.10 → v1.11, Tooling v1.15 → v1.16, CLAUDE.md v1.89 → v1.90; framer-motion: regulatory hard-ban → technical block React-only peerDep). **Регрессия зелёная (на 13.05.2026 day): Pest --parallel 742/739/0/3 ✅ (после `--recreate-databases`), Vitest 88 files / 683 passed + 3 skipped, Vite build 2.67s, lychee 248 OK / 0 errors, gitleaks 0 leaks (367 commits)**. **Сводка §0 counters updated**: 78 → 87 продуктовых / 69 → 70 ✅ / 4 → 12 ⏸ / 1 P0 / 3 → 5 P1 / 0 → 3 P2 / +2 P3 (Биз-29/30). Pravila §2.2 строго соблюдена: ни один новый Q-item не закрыт без явного «закрываем» заказчика (Биз-25..31 + CTO-19 остаются ⏸; Plan 5 / Quiet Luxury / audit / R15 — feature delivery / audit-internal / regulatory, не Q-items registry). Предыдущая v1.81 — см. footer; v1.77 — Sprint 4 Audit tail (см. ниже).

**Что изменилось в v1.85 относительно v1.84:**

- **SEC-1..SEC-7 — статусы приведены к факту.** Серверный слой защиты боевого тест-сервера liderra.ru развёрнут 22.05.2026 (на тестовой VM Yandex Cloud, до закрытия Б-1). Полная процедура и команды управления — новый документ `docs/security/server-hardening-setup.md`.
  - **SEC-1** WAF — ✅ **сделано.** ModSecurity 1.0.3 + OWASP CRS 3.3.5 в боевом режиме (блокировка, порог аномалии 5, 1830 правил). Исключение для вебхука поставщика (приём лидов только наблюдается, не блокируется). Разрешены REST-методы PATCH/DELETE/PUT (иначе ломалось редактирование/удаление в портале).
  - **SEC-2** Anti-brute-force — ✅ **сделано.** Прикладной throttle входов (был в коде) + серверный fail2ban (jails `sshd` + `nginx-http-auth`, bantime 1ч).
  - **SEC-3** DDoS — ⏸ **отложено** (решение заказчика по цене, 22.05). Платная Yandex Cloud DDoS Protection дорога/избыточна; базовая сетевая L3/L4 защита уже активна бесплатно. Альтернатива на будущее — бесплатный Cloudflare перед сайтом (DDoS + WAF + CDN).
  - **SEC-4** Мониторинг — ✅ **сделано (лёгкий).** Ежедневный отчёт безопасности 07:00 (диск/память/TLS/баны/5xx) с email-алертингом на `kdv1@bk.ru`. Sentry ⏸ (мало RAM на тест-VM 1.9 ГБ, pending Б-1).
  - **SEC-5** Хранилище секретов — 🟦 **частично.** Yandex KMS + Lockbox заведены (8 секретов). Интеграция приложения (читать из Lockbox вместо `.env`) ⏸ блокирована — нужен сервис-аккаунт YC (нет доступа к консоли облака).
  - **SEC-6** TLS/HSTS/CSP — ✅ **сделано.** HTTPS (Let's Encrypt, авто-обновление certbot), HSTS + защитные заголовки, CSP в боевом режиме (enforcing, проверен в браузере).
  - **SEC-7** Бэкапы + IR — ✅ **сделано (бэкапы).** Ежедневный `pg_dump` 03:30 (retention 14 дней) + off-site зашифрованная (AES-256) копия на почту. IR-runbook — реюз `operations:runbook` #51 (отдельным документом не формализован).
- **Сводка §0:** без изменений (SEC-*инфраструктурные, не продуктовые Q-items — как DO-*). Эти статусы — фактическое состояние деплоя на тест-сервер, **НЕ** формальное «закрытие» вопроса заказчиком (Pravila §2.2): полный production-hardening пересматривается при реальной инфраструктуре (Б-1).
- Источник: memory `project_server_hardening`, ADR-014 §9, новый `docs/security/server-hardening-setup.md`.

**Что изменилось в v1.84 относительно v1.83:**

- **+7 инфраструктурных security open-items (SEC-1..SEC-7)** — серверный слой защиты боевого портала, вынесенный из A8 infosec-tooling эпика (плагины формализованы, серверная защита — инфраструктура, не плагины):
  - **SEC-1** WAF (веб-фаервол) — Yandex Smart Web Security либо self-hosted Coraza/ModSecurity. Привязка Б-1.
  - **SEC-2** Anti-brute-force / rate-limit — Laravel throttle (готово в коде частично) + серверный слой (fail2ban-style). Привязка Б-1.
  - **SEC-3** DDoS-защита — Yandex Cloud DDoS Protection. Привязка Б-1.
  - **SEC-4** Мониторинг вторжений — Sentry #34 (pending Б-1) + серверные логи/алерты.
  - **SEC-5** Хранилище секретов — Yandex Lockbox (вместо ключей в файлах). Привязка Б-1.
  - **SEC-6** TLS / HSTS / CSP на бою — сертификаты + заголовки (частично проверяется Nuclei #69 / Ward #70).
  - **SEC-7** Бэкапы + регламент реагирования на инцидент (IR-runbook) — реюз `operations:runbook` #51.
- **Сводка §0:** без изменений (счётчик продуктовых вопросов не двигается — SEC-1..SEC-7 инфраструктурные, как DO-*; не продуктовые Q-items). Истинный блокер всех 7 — внешний **Б-1** (provisioning боевой инфраструктуры).
- Источник: эпик A8 infosec-tooling, ADR-014 §9 «Серверный слой (out of scope)». План `docs/superpowers/plans/2026-05-21-a8-infosec-tooling.md`.

**Что изменилось в v1.83 относительно v1.82:**

- **CTO-19 ⏸ → ✅ closed** через Lucide migration. Custom Vuetify `IconSet` (`liderraLucideSet`) в `app/resources/js/plugins/vuetify.ts` с 103-entry mapping (78 user-grep'нутых mdi-* + 25 Vuetify-internal defaults). 51 Vue/TS файл с `icon="mdi-*"` invariant. `npm i lucide-vue-next ^1.0.0` selective tree-shakable imports. Path α (aliases-only) approved заказчиком через brainstorming → writing-plans → subagent-driven-development workflow.
- **Сводка §0 counter update**: CTO 17✅/1⏸/1 P2 [?] → **18 ✅** / 0 ⏸ / 0 P2 [?] (CTO-19 priority TBD убрано); Итого 70✅/12⏸ → **71 ✅** / **11 ⏸** (CTO-19 closed).
- **Регрессия зелёная**: Pest --parallel 742/739/0/3 ✅ (после `--recreate-databases`), Vitest 88 files / 683 passed + 3 skipped, Vite build 3.52s exit 0, axe-core a11y scan `/admin/billing` 0 iconography violations.
- **Visual smoke 8 views** (5 plan-mandated + 3 additional): `/dashboard`, `/projects`, `/settings`, `/admin/billing`, `/no-such-404`, `/kanban`, `/deals`, `/admin/tenants` — все glyph'ы рендерятся правильно (Bell, Search, ChevronLeft/Right, ChevronsLeft/Right, ChevronDown, Square, SquareCheck, CircleDot, CreditCard, AlertTriangle, Users, UserCheck, Settings, RefreshCw, LayoutDashboard, ArrowLeft, Camera, ShieldCheck, Folder, Webhook, Puzzle, Clock, etc.). 0 «?» fallback icons.
- **Brand alignment**: CLAUDE.md §2 «Иконки: Lucide» — теперь visually реализован (был declaratory в spec'е до v1.83).

---

**Что изменилось в v1.82 относительно v1.77 — детально по trail:**

- **v1.78 от 11.05.2026** — **Plan 4 (Billing + CSV Reconcile + Admin) closure** на ветке `plan4-billing` (14 task-коммитов `a907fea..174dbae` + lychee fix `fded2ee` = 15 коммитов). +7 deferred placeholders Биз-25..31 (см. §14). Активирован 7-ступенчатый pricing-tier биллинг (`PricingTierResolver` pure + `LedgerService` dual-balance prepaid→rub bcmath), `CsvReconcileJob` hourly с drift>5% алертом, auto-pause flow `ZeroBalancePausedMail` 1/час/tenant, 3 UI экрана (AdminPricingTiersView + AdminSupplierPricesView + ChargesTab в существующем BillingView). Регрессия: Pest 687/684 passed + 3 skipped/0 failed (2090 assertions), Vitest 49 files / 428 passed, Histoire 24 stories / 31 variants, lychee 0 broken, gitleaks 0 leaks. Plan: `docs/superpowers/plans/2026-05-11-plan4-billing-csv-admin-plan.md` §6 → §14 (post-rename).

- **v1.79 от 12.05.2026** — **CTO-19 ⏸ open**: иконочная система портала не подключена — все `mdi-*` рендерятся пустыми. Vuetify-конфиг создаёт `<i class="mdi-...">` (по умолчанию MDI), но CSS-пакет `@mdi/font` в `package.json` отсутствует. Затронуты все места с `prepend-icon`/`append-icon`/`icon`/`clearable` в Vue-views (mdi-plus, mdi-magnify, mdi-close-circle, notification bell, dropdown chevron'ы). Workaround локально применён в `ProjectsView.vue` (clearable-крестик `::after content '✕'`) — не закрывает общую проблему. **Решение [?]:** либо `npm i @mdi/font` + import CSS в `main.ts` (быстрый workaround), либо переключить Vuetify-config на Lucide-aliases (бренд-соответствующая правка). См. §3 строка CTO-19.

- **v1.80 от 12.05.2026 (заведение)** — **CTO-20 ⏸ open**: регистронезависимый поиск кириллицей не работает на dev `liderra`. Корень: БД создана с `datcollate='C'` / `datctype='C'` — Postgres `lower()/upper()` не делает case-folding для кириллицы. 5 endpoint'ов с `ILIKE` затронуты (`AdminBillingController`, `AdminTenantsController`, `DealController`, `ProjectController`, `ProjectService`). Точечный workaround `COLLATE` применён в `ProjectController::index` для разблокировки Plan 5 frontend.

- **v1.81 от 12.05.2026 (closure)** — **CTO-20 ✅ closed системно**: dev-БД пересоздана с `LOCALE_PROVIDER icu ICU_LOCALE 'und'` (PG 16+ ICU collation, кросс-платформенно). Backup `app/.backups/liderra_pre_icu_2026-05-12.dump` (pg_dump custom format, 394KB). Точечный workaround в `ProjectController::index` откатан. Все 5 endpoint'ов с `ILIKE` работают с кириллицей без правки кода. Регрессионный тест в `ProjectsListShowTest.php:116-141` (10/10 / 37 assertions). **Для prod-deploy:** при создании Managed PG БД использовать ту же команду — `CREATE DATABASE liderra WITH OWNER postgres ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'und' LC_COLLATE 'C' LC_CTYPE 'C' TEMPLATE template0;` (см. `db/00_create_roles.sql` для обновления).

- **v1.82 от 13.05.2026 (catch-up)** — **Это bump**. Section ## 13 collision fix (Plan 4 → ## 14). Сводка §0 counters update (Биз +7 ⏸, CTO +1 ⏸ +1 ✅). Post-Plan-4 context документирован (Plan 5 / Quiet Luxury / Q.DEFER closures / R15 merge — не Q-items, feature delivery / audit-internal / regulatory). CLAUDE.md §0 row «Открытые вопросы» bumped к v1.82 (per CLAUDE.md §5 п.10 через `/claude-md-management:claude-md-improver` или прямой Edit + явное обоснование «registry version sync»). Memory `reference_archive.md` description + `MEMORY.md` index sync. NO code, NO schema, NO migration, NO test changes. Pure doc-hygiene.

---

**Версия:** 1.77 от 10.05.2026 — **Sprint 4 «Audit tail» закрыт** (5 коммитов `f77c91d..b912724`). Закрыты 3 audit O-* пункта: O-perf-04 (keyset pagination в `DealController::index` + 3 Pest-теста), O-refactor-04 хвост (8 Vue-компонентов >300 строк разделены на shell + sub-components; AppLayout R0.6 hard-стоп снят явным запросом заказчика), O-refactor-06 (rollup-plugin-visualizer + knip + cleanup ~165 строк dead-code в composables/). Регрессия зелёная: **Pest 421 / Vitest 416 / Larastan 0 / vue-tsc 0 / ESLint 0 / build OK 1.02 сек**. Также Sprint 0 push выполнен (21 коммит на origin/main). Roadmap до production написан: [docs/superpowers/specs/2026-05-10-roadmap-to-production-design.md](superpowers/specs/2026-05-10-roadmap-to-production-design.md) (3-track A/B/C, 9 sprint'ов: Sprint 0 push + Track A 4-6 не зависят от Б-1 + Track B 7-9 после Б-1 + Track C external).

**Что изменилось в v1.77 относительно v1.76:**

- **Sprint 4 «Audit tail»** (5 фаз / 5 коммитов, ~1 час wall-clock через mix inline + 3 subagent-делегации):

  - **Phase A (backend keyset)** `f77c91d` — `DealController::index` получил опциональный `cursor` query-параметр (base64-encoded JSON `{r:received_at, i:id}`). При cursor — keyset через PG row constructor `(received_at, id) < (?, ?)` с использованием существующего индекса `(received_at DESC, id DESC)`. O(1) на любой глубине, без COUNT(*). Без cursor — backward-compat OFFSET-путь (total + offset для существующего frontend). Оба режима возвращают `next_cursor` (NULL = последняя страница). 3 новых Pest-теста (keyset-навигация, 422 на невалидный cursor, next_cursor flow). Pest +3 от 418 → **421**. Frontend integration не делалась (OFFSET путь жив, не блокирует deploy).

  - **Phase B/1 (3 admin/layout views)** `30ef61d` — AdminTenantsView 377→155 (+TenantsStatsHeader 82 / TenantsFilters 93 / TenantsTable 116), AdminTenantDetailView 436→109 (+TenantDetailHeader 158 / TenantDetailTabs 176 + adminTenantDetailFormatters 43 composable), **AppLayout 466→78** (+AppSidebar 155 / AppTopbar 269; R0.6 layouts hard-стоп снят явным заказчиком в начале сессии). Паттерн: state в parent ради `defineExpose`-контракта Vitest, sub-components читают Pinia stores напрямую (auth/notifications/reminders) — без prop-drilling.

  - **Phase B/2 (3 user views)** `849bc73` — BillingView 416→114 (+BalanceCard 155 / TransactionsTable 113 / InvoicesTable 90 / billingFormatters 51), SecurityTab 354→39 (+ChangePasswordCard 17 / TwoFactorCard 218 / RecoveryCodesCard 104 / SessionsTable 66), RemindersView 345→183 (+RemindersFilters 51 / RemindersList 173). TopupDialog/ReminderForm не созданы — TopupDialog был заглушкой в исходнике (создание = new feature, не split), ReminderForm уже существовал как `ReminderDialog`.

  - **Phase B/3 (2 utility views)** `79ff60f` — ErrorView 320→178 (+ErrorBrand 54 / ErrorIllustration 31 / ErrorActions 55 / ErrorMeta 102), DashboardView 302→84 (+DashboardPageHead 65 / DashboardKpiRow 97 / DashboardBalance 124). DashboardRecentDeals не создан — секция отсутствует в текущем DashboardView (есть KPI-row + ActivityChart/FunnelChart).

  - **Phase C (dead-code)** `b912724` — установлены `rollup-plugin-visualizer ^7.0.1` + `cross-env ^10.1.0` + `knip ^6.12.2`. Vite-plugin visualizer условный (`BUILD_ANALYZE=1`), `npm run build:analyze` script. Knip упал на Windows quirk «oxc-parser ArrayBuffer fail» — конфиг оставлен для CI (Linux). Ручной grep по composables/ нашёл и удалил dead exports: `mockReports.ts:MOCK_JOBS` (62 стр) + `QuotaInfo`/`MOCK_QUOTA`, `reportsMapper.ts:reportTypes`, `mockTenantDetail.ts:expandTenantDetail` + 4 sample-консты, `useCsvDownload.ts:csvEscape` (export убран). ~165 строк dead-code вычищено.

- **Acceptance Sprint 4:**

  - ✅ keyset pagination работает + Pest +3 теста
  - ✅ Audit O-refactor-04 список 12 компонентов >300: 10/12 закрыты в Sprint 3+4 (Top-3 Sprint 3, 8 Sprint 4 B/1+B/2+B/3) + ImpersonationDialog уже <300 органически. **Остаются 2 исключения**: DealsView (560) + DealDetailDrawer (386) — Sprint 3 intentional (defineExpose-контракт Vitest, дальнейшее извлечение требует переписать тестовый контракт; зафиксировано как «правильное инженерное решение» в memory).
  - ✅ Bundle analyzer + knip активны, dead-code удалён
  - ✅ Регрессия зелёная: Pest 421 / Larastan 0 / Pint / ESLint 0 / vue-tsc 0 / Vitest 416/416 / build OK 1.02 сек / Histoire 21/43

- **Что НЕ сделано (вынесено в backlog):**

  - **O-perf-04 frontend-integration** — frontend `useDealsList`/`DealsView` пока использует OFFSET-путь (backward-compat). Включение `cursor` mode на UI = отдельный коммит (не блокирует backend).
  - **DealsView/DealDetailDrawer split** — требует переписывания Vitest defineExpose-контракта. Не блокер.
  - **Knip полный sweep по `api/`/`stores/`/`components/`** — ждёт Linux CI (Windows quirk).
  - **Sprint 5 «Pre-prod tooling local»** — следующий по roadmap'у Track A (#25 Semgrep + #26 Trivy prep + #27 Dependabot).

- **Sprint 0 push выполнен** — 21 коммит ушёл на origin/main `4f36bd3..8c6374d`. PAT permission «Workflows: Read and write» подтверждена через API-диагностику. Различие GitHub UI fine-grained PAT: «Workflows» (запись workflow-файлов) ≠ «Actions» (управление workflow runs).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2). Sprint 4 не был open-вопросом реестра — это закрытие audit O-* хвоста.

**Что изменилось в v1.76 относительно v1.75:**

- **Эпик AdminTenantDetailView** (2 коммита, замена mock на real API):

  - **Этап A (backend)** `f9d8926` — `AdminTenantsController::show($subdomain)` под существующим `/api/admin/tenants/{subdomain}` route'ом. Lookup по subdomain (естественный URL slug) + `whereNull deleted_at`. Без auth-middleware — saas-admin SSO ⏸ Б-1. Возвращает: tenant base (15 полей включая mrr_rub derived) + users (LIMIT 50, ORDER last_active_at DESC) + projects (LEFT JOIN sub-queries для suppliers_count + leads_today) + balance_history (LIMIT 30) + activity (LEFT JOIN users для actor_email, LIMIT 20, json_decode context) + computed metrics (leads_today/week/month через FILTER, AVG(supplier_lead_costs.cost_rub) за 30 дней, runway_days = balance / (month_spend / 30)). 4 private fetcher'а + computeMetrics. Pest +13.

  - **Этап B (frontend)** `cab1f87` — `api/admin.ts +getAdminTenantDetail` + 5 типов; `composables/adminTenantDetailMapper.ts` (API → mockTenantDetail format: code=subdomain, deriveStatus 4 ветки, deriveTariff с Trial-fallback, users.fullName из first+last||email с role='manager' хардкод (schema users role нет — Post-MVP), projects.slug=tag, balance_history с TX- prefix + chargeback_*/trial_bonus/historical_import → ближайший UI-тип, activity.actor=actor_email||system + summary из context.from→to, activitySinceText). AdminTenantDetailView переписан: async loadTenant + 3 ветки template (loading/notFound/fetchError) + watch(code) для реактивной навигации. inn/contact_phone/legal_address скрыты через v-if (нет в schema). Vitest +23.

- **Что НЕ сделано (вынесено в Post-MVP backlog):**
  - **Поля inn/contact_phone/legal_address** требуют расширения tenants schema или JOIN на legal_entities (сейчас живёт только для оператора SaaS).
  - **users.role** требует таблицы user_roles или enum-колонки в users (сейчас на UI хардкод 'manager').
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **416/416 за 21.90 сек** (+23 от 393).
  - `npm run build` — vite OK 1.04 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 416/416 за 45.48 сек** (+13 от 403, 1388 assertions).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2). AdminTenantDetailView не был open-вопросом реестра; это Post-MVP feature расширение (один из P1-кандидатов).

**Что изменилось в v1.75 относительно v1.74:**

- **Эпик Reports backend** (4 коммита, async-генерация отчётов с очередью + квота CTO-7 + retry CTO-6):

  - **Этап 1** `19f319c` — `App\Models\ReportJob` (schema §13.5 report_jobs, status pending/processing/done/failed); `App\Jobs\GenerateReportJob` (sync queue на dev, tries=1 — auto-retry отключён по CTO-6); `ReportJobController` (GET index/show + POST store с квотой 3 одновременных); `App\Services\Reports\DealsExportCsvGenerator` (Excel-friendly CSV: BOM + ; + \r\n). Storage local-disk на dev. Pest +20.

  - **Этап 2** `1a6a74c` — реструктура на provider+formatter (вместо Generator-per-комбинация): 4 + 4 = 8 классов вместо 16. Provider возвращает headers + rows; Formatter сериализует. 4 формата: CsvFormatter, XlsxFormatter (PhpSpreadsheet 5.x с A1-нотацией + bold headers + auto-size), JsonFormatter (UNESCAPED_UNICODE), PdfStubFormatter (Post-MVP throw). Pest +3.

  - **Этап 3** `9765ed7` — retry/cancel/delete + retention cron. POST /retry (CTO-6: 3 попытки за 7 дней, только owner, parameters.retry_count, квота тоже учитывается); POST /cancel (только pending); DELETE (только terminal, удаляет файл + row); `App\Console\Commands\ReportsCleanupExpired` cron `reports:cleanup-expired` (CTO-10: status='done' СОХРАНЯЕТСЯ, file_path=NULL). Pest +21.

  - **Этап 4** `e0ffe7e` — frontend integration. `api/reports.ts` (типизированные axios-helpers); `composables/reportsMapper.ts` (API → UI mock format с конверсией pending→queued/processing→running, title с RU-месяцами, sizeText B/KB/MB, timeText «в очереди»/«в работе · Nс»/«N мин назад»); ReportsView переписан под API (onMounted load + usePolling 30 сек + Submit/Reset/Reload + Retry/Cancel/Delete с v-dialog confirm + fetch/submit-error-alerts + canSubmit disabled при квоте). Vitest +24 (mapper 14 + view 10).

- **Что НЕ сделано (вынесено в Post-MVP backlog):**
  - Этап 2b — 3 оставшихся типа провайдеров (managers_summary, sources_summary, billing_summary) — расширение через добавление 3 Provider-классов без изменений в архитектуре.
  - GET /api/reports/jobs/{id}/file — endpoint скачивания (на этапе 1 не было; UI Скачать-кнопка пока без handler'а).
  - S3-storage переключение для production (на dev — local-fs).
  - Async queue worker (на dev — sync; на prod нужен Redis/database driver + supervisor).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **393/393 за 21.78 сек** (+24 от 369).
  - `npm run build` — vite OK 1.02 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 403/403 за 44.83 сек** (+44 от 359, 1343 assertions).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2). Reports backend не был open-вопросом реестра; это Post-MVP feature расширение.

**Что изменилось в v1.74 относительно v1.73:**

- **MVP-фаза признана завершённой со стороны Claude.** Решение принято заказчиком 09.05.2026 после закрытия P0 (notifications + reminders, 6 этапов / 7 коммитов `a4601fe..4c33323`). Документная фиксация — без дальнейшей разработки до изменения внешних условий.

- **Покрытие фронтенда (Vue 3 + Vuetify 3):** **13 экранов из 13 концептов handoff'а v8 Forest** (+ 3 ErrorView 404/403/500): 5 auth (Login + Register + 2FA + Forgot + Reset + Recovery + UseRecoveryCode + RecoveryCodes) + 8 app (Dashboard, Deals, Kanban с DnD, Reminders, Billing, Reports, Settings 8 tabs, ErrorView) + 6 admin (Tenants, TenantDetail, Billing, Incidents, System, Impersonation). Landing v8_landing.html отложен через ⏸ Б-1 для продакшена.

- **Покрытие бэкенда (Laravel 13):** auth-flow (login/register/me/logout + forgot/reset + 2FA setup/verify/disable/regenerate + recovery-use), deals API (index/show/store/update/transition/destroy/restore/export-CSV+XLSX), 3 lookup-API (managers/projects/lead-statuses), reminders CRUD (5 endpoints) + cron `reminders:dispatch-due`, in_app_notifications (4 endpoints) + bell-UI с 30-сек polling, notification-preferences PATCH, admin (tenants/billing/incidents индексы + system_settings GET+PUT с audit-log), impersonation (init/verify/end/active/recent), webhook receive (HMAC + per-token rate-limit), partitions:create-months. **Все 8 schema-default событий уведомлений** имеют рабочую интеграцию (new_lead + reminder + low_balance + zero_balance + topup_success + invoice_paid через Mailable+blade+NotificationService; new_device_login + marketing — семантические заглушки).

- **Что НЕ сделано (остаточные blocked-only / pending external):**
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1 (admin-endpoints на MVP без middleware).
  - **#7 Pest browser-mode** — отложен (инфра).
  - **ЮKassa/invoice webhook endpoints** — service-методы `notifyTopupSuccess`/`notifyInvoicePaid` готовы, integration ждёт реальных платёжных endpoint'ов.
  - **`new_device_login` через user_sessions tracking** — отложен до session-fingerprint инфраструктуры.
  - **Deep-link bell→DealDetailDrawer** — на MVP push на /deals (UX-граница приемлема).
  - **Реальный landing для продакшена** ⏸ Б-1 + Диз-3 (контакты подвала) + Диз-4 (контент).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2). **Истинные блокеры остались внешние:** Б-1 (реквизиты ООО) → разблокирует Диз-3, DO-2, DO-4, OPEN-З-*; юр. редактура Прил. Ж/З + 5 🟦 структурных вопросов.

- **Дальнейшая работа Claude:** возобновляется по триггерам — закрытие Б-1 (запуск Yandex 360 SSO + домен `liderra.ru` + список SaaS-сотрудников), приход юр. формулировок (Прил. Ж оферта + Политика + Прил. З уведомление РКН), переход к фазе 3 (#25 Semgrep + #26 Trivy + #27 Dependabot + #28 pg_audit + #29 pg_anonymizer — pre-production), либо deployment в Yandex Cloud (требует closures Б-1).

**Что изменилось в v1.73 относительно v1.72:**

- **(1) Backend `DealController::index`**:
  - Новый query-param `only_deleted=true` (boolean).
  - Branch: `Deal::query()->withTrashed()->whereNotNull('deleted_at')` — обход global scope SoftDeletes + явный фильтр для NO-OP idempotency.
  - Все существующие фильтры (status_in/project_id/manager_id/search/limit/offset) применимы и в trash-mode.

- **(2) Pest +3** в DealIndexTest:
  1. only_deleted=true возвращает только soft-deleted (1 alive + 2 deleted → total=2);
  2. Без only_deleted soft-deleted скрыты (default behavior сохранён);
  3. RLS + app-фильтр изолирует чужие удалённые сделки.

- **(3) Frontend integration:**
  - `ListDealsParams.onlyDeleted?: boolean` + axios mapping `only_deleted: 'true' | undefined`.
  - **DealsView**: `trashMode` ref + `toggleTrashMode()` (clear selected + reload) + `applyBulkRestoreFromTrash()` (optimistic remove + bulkRestoreDeals + toast).
  - **UI changes в trash-mode**:
    - Заголовок: «Сделки» → «Корзина».
    - Toggle-btn: `mdi-arrow-left К сделкам` (warning-flat) вместо `mdi-trash-can-outline Корзина` (outlined).
    - Скрыты «Экспорт» + «Новая сделка».
    - Скрыт chiprow filter-bar (не имеет смысла для удалённых).
    - Info-alert «Корзина: показаны удалённые сделки».
    - Bulk-bar: только «Восстановить» (`mdi-restore` success-tonal) + clear-btn; status/export/delete скрыты.

- **(4) Vitest +2** в DealsListIntegration:
  1. toggleTrashMode переключает trashMode + listDeals вызывается с onlyDeleted=true;
  2. applyBulkRestoreFromTrash вызывает bulkRestoreDeals + убирает из dealsState + toast «Восстановлено 2».

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **321/321 за 19.60 сек** (+2 от 319).
  - `npm run build` — vite OK 1.04 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 269/269 за 29.12 сек** (+3 от 266, 1009 assertions).

- **Что НЕ сделано (остаточные blocked-only):**
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.72 относительно v1.71:**

- **(1) Composable `composables/usePolling.ts`**:
  - `usePolling(loader, {intervalMs=30_000, enabled=true})`.
  - **Page Visibility API integration**: при `document.hidden=true` interval останавливается; при `visibilitychange` с возвратом `hidden=false` — restart + немедленный `loader()` (не ждать следующего interval'а).
  - Cleanup на `onBeforeUnmount` — `clearInterval` + `removeEventListener`.
  - `enabled=false` — composable не стартует (для feature-flag).

- **(2) Integration в 5 view'ов:**
  - DealsView, KanbanView → polling `loadDeals`.
  - AdminTenantsView → `loadTenants`.
  - AdminBillingView → `loadBilling`.
  - AdminIncidentsView → `loadIncidents`.
  - Manual reload-btn остаётся как fast-path; polling — фоновый автообновитель.

- **(3) Vitest +6** в usePolling.spec.ts (с `vi.useFakeTimers`):
  1. Вызов каждые intervalMs;
  2. Default = 30 сек;
  3. Skip при document.hidden=true;
  4. Cleanup на unmount — больше не вызывает loader;
  5. enabled=false — loader не вызывается совсем;
  6. visibilitychange — pause+resume с немедленным loader.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **319/319 за 18.67 сек** (+6 от 313).
  - `npm run build` — vite OK 899 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 266/266 за 28.62 сек** (backend не тронут).

- **Что НЕ сделано (остаточные blocked-only):**
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2). **Все unblocked production-TODO закрыты.**

**Что изменилось в v1.71 относительно v1.70:**

- **(1) Backend `AdminTenantsController::index`**:
  - Добавлено `tariff_plans.price_monthly as tariff_price_monthly` в select.
  - Поле `mrr_rub` в response: `tariff_price_monthly` (string) если не-trial; иначе null.
  - Aggregate-формат как у /admin/billing — string чтобы decimal не терял точность.

- **(2) Pest +3** в AdminTenantsIndexTest:
  1. mrr_rub='990.00' для активного тарифа не-trial;
  2. mrr_rub=null для trial-тенанта (даже если есть тариф);
  3. mrr_rub=null если current_tariff_id отсутствует.

- **(3) Frontend integration:**
  - `ApiAdminTenant.mrr_rub: string | null` в типе.
  - **mapApiAdminTenant**: `mrrRub: api.mrr_rub !== null ? parseFloat(api.mrr_rub) : null` (заменяет hardcoded null из v1.66).
  - AdminTenantsView template: `formatRub(item.mrrRub)` для консистентности с другими ₽-полями.

- **(4) Vitest +2** в AdminTenantsViewApi.spec.ts:
  1. mrr_rub строка → number;
  2. mrr_rub=null → mrrRub null.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **313/313 за 18.83 сек** (+2 от 311).
  - `npm run build` — vite OK 947 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 266/266 за 28.39 сек** (+3 от 263, 1001 assertion).

- **Что НЕ сделано (production TODO остаточные после v1.71):**
  - Polling/SSE для real-time (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.70 относительно v1.69:**

- **(1) Backend `DealController::restore`** — POST /api/deals/restore:
  - Body: `{tenant_id, ids: [1..1000 ints]}`.
  - `Deal::query()->withTrashed()` обходит global scope SoftDeletes; явный `whereNotNull('deleted_at')` обеспечивает NO-OP idempotency на уже живых сделках.
  - RLS + defense-in-depth `where(tenant_id)` → партиальный restore только своих.
  - Каждая ВОССТАНОВЛЕННАЯ сделка пишет ActivityLog event=deal.restored, context.source='bulk'.
  - `ActivityLog::EVENT_DEAL_RESTORED` константа добавлена в model.

- **(2) Pest +7** в DealRestoreTest:
  1. 422 без полей; 2. 404 unknown tenant;
  2. soft-delete + restore + ActivityLog deal.restored;
  3. NO-OP idempotency на живых сделках (НЕ пишет audit);
  4. defense-in-depth (свой восстановлен, чужой остался удалён);
  5. После restore сделка снова видна в GET /api/deals;
  6. 422 пустой массив ids.

- **(3) Frontend integration:**
  - `dealsApi.bulkRestoreDeals(payload)` — POST-helper.
  - **DealsView::applyBulkDelete** расширен: snapshot удалённых сделок (deep-clone manager.* nested) сохраняется в `lastDeletedSnapshot` ref.
  - `undoBulkDelete()` async: optimistic re-insert через `dealsState.unshift` + bulkRestoreDeals если auth.user; success → toast «Восстановлено N из M.»; fail → warning.
  - **v-snackbar** для bulk-delete: timeout 3→8 сек + `#actions` слот с кнопкой «Восстановить» (показывается только при `lastDeletedSnapshot.length > 0`).
  - После успешного undo snapshot очищается → кнопка пропадает.

- **(4) Vitest +3** в DealsListIntegration:
  1. bulk-delete + undo восстанавливает обе сделки + bulkRestoreDeals вызывается с правильными ids + lastDeletedSnapshot очищается;
  2. undo без tenant_id — bulkRestoreDeals НЕ вызывается + только локальное восстановление;
  3. undo reject → warning toast + локальное восстановление остаётся.

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **311/311 за 18.71 сек** (+3 от 308).
  - `npm run build` — vite OK 877 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 263/263 за 27.68 сек** (+7 от 256, 998 assertions).

- **Что НЕ сделано (production TODO остаточные после v1.70):**
  - Polling/SSE для real-time (на MVP — manual reload-btn).
  - mrr_rub aggregate в /api/admin/tenants (требует JOIN tariff_plans).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.69 относительно v1.68:**

- **(1) Schema v8.8 → v8.9:**
  - `deals.deleted_at TIMESTAMPTZ` (NULL = живая сделка).
  - Partial index `(tenant_id, status) WHERE deleted_at IS NULL` — самый частый UI-фильтр.
  - ALTER TABLE на партиционированной deals distributes колонку во все 6 партиций (PG 14+).
  - CHANGELOG_schema.md +§U с обоснованием soft vs hard:
    - **CASCADE-FK от webhook_dedup_keys** (через composite `(deal_id, deal_received_at)`) уничтожил бы dedup-ключи при hard-delete → следующий webhook с тем же `vid` создал бы дубль (нарушение §5.5).
    - Soft-delete сохраняет dedup-keys + позволяет restore-flow.
  - Метрики: 92→93 индекса.

- **(2) Backend `DealController::destroy`** — DELETE /api/deals:
  - Body: `{tenant_id, ids: [1..1000 ints]}`.
  - Bulk-update `deleted_at=NOW()` через RLS + defense-in-depth `where(tenant_id)`.
  - Каждая удалённая сделка пишет `ActivityLog event=deal.deleted, context.source='bulk'`. NO-OP (уже удалена) НЕ пишет.
  - **Deal model**: получил `SoftDeletes` trait + `deleted_at` в fillable/casts. Global scope автоматически добавляет `whereNull('deleted_at')` ко всем существующим query (index/show/transition/update/export) — без явного фильтра.

- **(3) Pest +8** в DealDestroyTest:
  1. 422 без tenant_id; 2. 404 unknown tenant;
  2. soft-delete + ActivityLog deal.deleted + source=bulk;
  3. defense-in-depth (свой удалён, чужой жив);
  4. NO-OP idempotency (повторное удаление не пишет audit);
  5. GET /api/deals скрывает soft-deleted;
  6. GET /api/deals/{id} 404 для soft-deleted;
  7. 422 пустой массив ids.
  - **Quirk:** `migrate:fresh --env=testing` без `.env.testing` использует `liderra` вместо `liderra_testing` — решение `DB_DATABASE=liderra_testing php artisan migrate:fresh`.

- **(4) Frontend integration:**
  - `dealsApi.bulkDeleteDeals(payload)` — DELETE-helper с `axios.delete('/api/deals', { data: payload })` (axios особенность: DELETE body через `config.data`).
  - **DealsView::applyBulkDelete** переписан async: optimistic local-removal + bulkDeleteDeals если auth.user; success → toast «Удалено N из M.»; fail → warning toast + локальный update НЕ откатывается. Без auth — только optimistic.

- **(5) Vitest +3** в DealsListIntegration:
  - bulkDeleteDeals с tenant_id + optimistic + toast «Удалено 2».
  - Без tenant_id — НЕ вызывается.
  - Reject → warning toast + локальный update остаётся.

- **АВТО-ПЛАН (5 этапов) ЗАКРЫТ ПОЛНОСТЬЮ.**

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **308/308 за 20.12 сек** (+3 от 305).
  - `npm run build` — vite OK 973 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 256/256 за 27.75 сек** (+8 от 248, 977 assertions).

- **Что НЕ сделано (production TODO остаточные после v1.69):**
  - Polling/SSE для real-time (на MVP — manual reload-btn).
  - Restore-flow для soft-deleted сделок (POST /api/deals/{id}/restore — отдельный коммит).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).
  - tenants.mrr_rub aggregate в admin/tenants endpoint (требует JOIN tariff_plans + balance_transactions).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.68 относительно v1.67:**

- **(1) Backend `AdminIncidentsController::index`** — GET /api/admin/incidents:
  - Query-params: `type` / `severity` / `unresolved_only=true` / `limit` clamp [1..500] / `offset`.
  - ORDER BY `started_at DESC, id`.
  - **Derived в response**:
    - `incident_id` — формат `INC-YYYY-MMDD-NNNN` (год+месяц+день started_at + zero-padded id).
    - `status` — resolved (resolved_at!=null) / investigating (detected_at!=null) / open.
    - `affected_tenants_count` — из BIGINT[] array через parsePgArray (PG-литерал `{1,2,3}`).
    - `rkn_deadline_at` — для type=data_breach без rkn_notified_at: detected_at+24h (152-ФЗ).
  - `summary`: open / investigating / rkn_pending (PDN-breach без notification + не resolved) / total_unresolved.

- **(2) Pest +11** в AdminIncidentsIndexTest:
  1. Пустой; 2. Поля + incident_id формат `/INC-\d{4}-\d{4}-\d{4}/`;
  2. Derive статус (investigating/resolved); 4. Type filter (data_breach);
  3. Severity filter (critical); 6. unresolved_only=true;
  4. ORDER BY started_at DESC; 8. data_breach имеет rkn_deadline +24h;
  5. Non-data_breach НЕ имеет deadline; 10. summary.rkn_pending только PDN-breach без notification;
  6. limit+offset.
  - **Quirk:** schema `saas_admin_users` использует `full_name` (не first/last) + нет updated_at — сразу исправлено в insert helper.

- **(3) Frontend integration:**
  - `api/admin.ts::listAdminIncidents(params)` — типизированный helper с ApiAdminIncident/Summary/Response.
  - **AdminIncidentsView** переписан: унифицированный `IncidentRow` interface (mock-`category` ↔ API-`type`, mock-`title` ↔ API-`summary`); reactive rowsState (default = ADMIN_INCIDENTS) + stats; loadIncidents() async на onMounted; на fail — fetchError + warning alert + MOCK fallback; reload-btn.
  - Maps (categoryMap/statusInfo/severityInfo) → functions с fallback'ами на новые slug'и.
  - РКН pending chip учитывает оба `pdn_breach`/`data_breach`.

- **(4) Vitest +5** в AdminIncidentsViewApi.spec.ts:
  1. listAdminIncidents на mount;
  2. Replace rowsState+summary с rkn_deadline сохранением;
  3. reject → fetchError + alert + MOCK fallback;
  4. reload-btn двойной вызов;
  5. РКН pending chip отображается для data_breach без rkn_notified.

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **305/305 за 20.59 сек** (+5 от 300).
  - `npm run build` — vite OK 1.05 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 248/248 за 28.02 сек** (+11 от 237, 951 assertion).

- **Что НЕ сделано (production TODO остаточные после v1.68):**
  - **Этап 5** авто-плана: soft-delete migration + DELETE /api/deals.
  - Polling/SSE (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.67 относительно v1.66:**

- **(1) Backend `AdminBillingController::index`** — GET /api/admin/billing?search=:
  - Per-tenant aggregates `balance_transactions` за текущий календарный месяц (один SUM с CASE WHEN type IN ('topup','lead_charge') GROUP BY tenant_id; ABS для charges).
  - Поля row: id, subdomain, organization_name, contact_email, status, balance_rub, tariff_id, tariff_name, mrr_rub (=tariff.price_monthly если is_trial=false, иначе '0.00'), monthly_topups_rub, monthly_charges_rub, last_payment_at (=MAX created_at для type=topup), chargeback_unrecovered_rub.
  - `summary`: total_mrr_rub (SUM tariff.price_monthly не-trial), monthly_revenue_rub (SUM topup за месяц), overdue_count (balance<0 OR chargeback>0), refunds_count_30d (count type=refund ≥now-30d).
  - **Quirk**: schema-колонка `tariff_plans.price_monthly` (НЕ `price_rub_monthly`) — обнаружено первым прогоном Pest, исправлено сразу.

- **(2) Pest +9** в AdminBillingIndexTest:
  1. Пустой; 2. Все поля + tariff JOIN; 3. Aggregates topups+charges за месяц;
  2. Прошлый месяц НЕ попадает в monthly_*;
  3. summary.overdue_count (balance<0 || chargeback>0);
  4. summary.refunds_count_30d (старше 30 дней не считаются);
  5. summary.total_mrr_rub (только не-trial с тарифом, trial исключается);
  6. search ILIKE по name+subdomain;
  7. soft-deleted скрыт.

- **(3) Frontend integration:**
  - `api/admin.ts::listAdminBilling(search)` — типизированный helper с ApiAdminBillingTenant/Summary/Response.
  - **AdminBillingView**: reactive `rowsState` (default = ADMIN_BILLING_TENANTS mock) + `summary` (default = MOCK_SUMMARY).
  - `loadBilling()` async на onMounted: парсит API-строки (balance_rub/mrr/topups/charges) → number; derive status: suspended → suspended; balance<0 || chargeback>0 → overdue; иначе active.
  - На fail — fetchError + warning alert + MOCK остаются. Reload-btn.
  - **tariffLabel/statusInfo** обобщены с fallback'ами — устойчиво к новым slug'ам из backend'а.

- **(4) Vitest +4** в AdminBillingViewApi.spec.ts:
  1. listAdminBilling на mount;
  2. Replace rowsState+summary с string→number конверсией + status derive (balance<0→overdue);
  3. reject → fetchError + alert + MOCK fallback;
  4. reload-btn двойной вызов.

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **300/300 за 18.41 сек** (+4 от 296).
  - `npm run build` — vite OK 925 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 237/237 за 27.69 сек** (+9 от 228, 926 assertions).

- **Что НЕ сделано (production TODO остаточные после v1.67):**
  - **Этап 4** авто-плана: GET /api/admin/incidents + AdminIncidentsView.
  - **Этап 5** авто-плана: soft-delete migration + DELETE /api/deals.
  - Polling/SSE (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.66 относительно v1.65:**

- **(1) Backend `AdminTenantsController::index`** — GET /api/admin/tenants:
  - Query-params: `status` / `search` (ILIKE по organization_name+subdomain+contact_email) / `limit` clamp [1..500] default 100 / `offset` default 0.
  - LEFT JOIN на `tariff_plans` для получения `tariff_name`.
  - ORDER BY `last_activity_at DESC, id`. Soft-deleted (deleted_at!=null) исключены.
  - `stats` агрегирует {total, active, trial, overdue} одним SELECT'ом без фильтров. `overdue` = `chargeback_unrecovered_rub > 0 OR balance_rub < 0`.
  - На MVP без auth (saas-admin SSO ⏸ Б-1).

- **(2) Pest +8** в AdminTenantsIndexTest:
  1. 200 + пустой список без данных;
  2. все поля присутствуют (subdomain, organization_name, balance_rub, etc.);
  3. status фильтр (active/suspended);
  4. search ILIKE по name + subdomain + email (case-insensitive);
  5. ORDER BY last_activity_at DESC (3 тенанта в правильном порядке);
  6. stats (total/active/trial/overdue) — overdue считает balance<0 + chargeback>0;
  7. soft-deleted tenant НЕ возвращается;
  8. limit+offset.

- **(3) Frontend integration:**
  - `api/admin.ts::listAdminTenants(params)` — типизированный helper с ApiAdminTenant/Stats/Response.
  - `composables/adminTenantsMapper.ts::mapApiAdminTenant` — converter API → UI:
    - **status derive**: is_trial=true → 'trial'; chargeback>0 || balance<0 → 'overdue'; schema-status as-is для active/suspended.
    - **inn=''** (нет в API — живёт в legal_entities/invoices.payer_inn).
    - **code=subdomain** (slug-эквивалент).
    - **tariff** clamp на known TenantTariff с fallback на 'Trial'.
    - **todayActual=0**, **mrrRub=null** (требуют JOIN на deals/balance_transactions — отдельный endpoint).
    - **activitySince** через formatRelative(last_activity_at).
  - **AdminTenantsView**: reactive `tenantsState` + `stats` (default = MOCK данные); loadTenants() на onMounted → splice replace; на fail — fetchError + warning alert + MOCK остаются.
  - Reload-btn с loading-state.

- **(4) Vitest +13** в AdminTenantsViewApi.spec.ts:
  - View-integration (4): mount → listAdminTenants called / replace state+stats / reject → fetchError+alert+MOCK fallback / reload-btn двойной вызов.
  - Mapper (9): organization_name→name; subdomain→code; inn пуст; is_trial→trial; chargeback>0→overdue; balance<0→overdue; suspended→suspended; balance_rub строка→number; activitySince «10 мин назад» / null → «—».

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **296/296 за 18.91 сек** (+13 от 283).
  - `npm run build` — vite OK 1.02 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 228/228 за 25.22 сек** (+8 от 220, 906 assertions).

- **Что НЕ сделано (production TODO остаточные после v1.66):**
  - **Этапы 3-4** авто-плана: GET /api/admin/{billing,incidents} + интеграция.
  - **Этап 5** авто-плана: soft-delete migration + DELETE /api/deals.
  - Polling/SSE (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.65 относительно v1.64:**

- **(1) Backend `DealController::update(int $id)`** — PATCH /api/deals/{id}:
  - Body: `{tenant_id, comment?, manager_id?, status?}` — все поля optional, должно быть хотя бы одно изменение.
  - Каждое изменённое поле пишет соответствующий ActivityLog event:
    - `comment` → `deal.commented` (context.text);
    - `manager_id` → `deal.assigned` (context.from/to + assigned_at=NOW);
    - `status` → `deal.status_changed` (context.from/to/source='manual').
  - NO-OP (значение не меняется) — НЕ пишется в audit log.
  - Manager FK guard + status slug validation (как в store/transition).
  - RLS + defense-in-depth `where(tenant_id)` → 404 для чужой сделки.

- **(2) Pest +10** в DealUpdateTest:
  1. 422 без tenant_id; 2. 404 unknown tenant; 3. 404 чужая сделка;
  2. comment update + deal.commented audit;
  3. manager update + deal.assigned audit + assigned_at=NOW;
  4. status update + deal.status_changed audit;
  5. 422 неизвестный slug + НЕ обновляет;
  6. 422 manager чужого tenant'а;
  7. NO-OP не пишет audit (status старый==новый, comment старый==новый);
  8. Комбинированно (comment+status одним запросом) → 2 audit log записи.

- **(3) Frontend integration:**
  - `api/deals.ts::updateDeal(id, payload)` — PATCH helper с `ensureCsrfCookie`.
  - **DealDetailDrawer**: новая секция «Комментарий» (показывается только при tenantId — read-only mode для legacy local).
    - `v-textarea` auto-grow + counter=5000 + commentDraft ref populates из getDeal response.
    - Save-btn (loading state) → `updateDeal` с `comment: commentDraft || null`.
    - На success → toast «Комментарий сохранён» + auto-reload events (новый `deal.commented` появляется в timeline).
    - На fail → `commentSaveError=true` + warning toast «Не удалось сохранить».

- **(4) Vitest +3** в DealDetailDrawerApi:
  - saveComment вызывает updateDeal + toast success + reload events (getDeal called twice).
  - saveComment reject → commentSaveError=true + warning toast.
  - comment-section НЕ рендерится без tenantId.

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **283/283 за 18.13 сек** (+3 от 280).
  - `npm run build` — vite OK 1.12 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 220/220 за 25.64 сек** (+10 от 210, 871 assertion).

- **Что НЕ сделано (production TODO остаточные после v1.65):**
  - **Этапы 2-4** текущего авто-плана: GET /api/admin/{tenants,billing,incidents} + интеграция в Admin*View.
  - **Этап 5** авто-плана: soft-delete migration + DELETE /api/deals (требует ALTER TABLE deals + bump schema).
  - Polling/SSE для real-time (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.64 относительно v1.63:**

- **(1) Backend** — `App\Models\LeadStatus` + `LeadStatusController::index`:
  - PK=`slug` string (`incrementing=false`, `keyType='string'`), `timestamps=null`.
  - GET /api/lead-statuses — ORDER BY sort_order, slug; формат `{slug, name_ru, is_system, sort_order, color_hex, description}`.
  - Таблица глобальная (НЕ tenant-aware), auth не требуется на MVP.

- **(2) Pest +5** в LeadStatusesIndexTest:
  1. 200 + не пустой список;
  2. все 14 системных slug'ов из seed (new/viewed/worked/base/missed/negotiations/waiting_payment/partnership/paid/closed/test_drive/hot/replacement/final_missed);
  3. поля slug/name_ru/color_hex/sort_order/is_system;
  4. sort_order ASC;
  5. кастомный slug, добавленный INSERT'ом после seed, возвращается endpoint'ом.

- **(3) Frontend integration:**
  - `api/leadStatuses.ts::listLeadStatuses` — GET helper.
  - `stores/leadStatuses.ts::useLeadStatusesStore` Pinia setup-store:
    - `statuses` default = LEAD_STATUSES snapshot — UI работает без fetch'а.
    - `load(force=false)` идемпотентен (no-op если loaded).
    - `bySlug` computed Map; `findBySlug(slug)` helper.
    - На fail — snapshot остаётся, `fetchError=true`.
  - DealsView и KanbanView: `leadStatuses` computed заменяет `LEAD_STATUSES`-импорт; load() в onMounted.
  - DealDetailDrawer: `store.findBySlug(props.deal.statusSlug)` вместо `LEAD_STATUSES.find(...)`.

- **(4) Vitest +7** в leadStatusesStore.spec.ts (initial snapshot / findBySlug existing & null / load success replace + loaded=true / load reject + fetchError + snapshot fallback / load идемпотентен / load(force=true) refetch). 2 spec'а DealDetailDrawer получили `setActivePinia(createPinia())` в beforeEach.

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **280/280 за 19.44 сек** (+7 от 273).
  - `npm run build` — vite OK 1.17 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 210/210 за 24.59 сек** (+5 от 205, 840 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - Polling/SSE для real-time (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.63 относительно v1.62:**

- **(1) Backend `DealController::show(int $id)`** — GET /api/deals/{id}?tenant_id={id}:
  - Возвращает `{deal, events}` для DealDetailDrawer.
  - `deal` — extended (project_name, manager_name/initials, comment, assigned_at).
  - `events` — последние 50 записей `activity_log` фильтрованных по `(tenant_id, deal_id)` ORDER BY created_at DESC, с актором (user через `belongsTo`).
  - RLS-обёртка + defense-in-depth `where('tenant_id', $tenantId)` (как в index/transition); 404 если сделка чужая или не существует.
  - Маршрут `Route::get('/api/deals/{id}', 'show')->where('id', '[0-9]+')`.

- **(2) Pest +8** в DealShowTest:
  1. 422 без tenant_id; 2. 404 unknown tenant; 3. 404 несуществующая сделка;
  2. 404 чужая сделка (defense-in-depth работает даже с postgres superuser BYPASSRLS);
  3. Deal-relations (project_name, manager_name «Иван П.», initials «ИП», comment);
  4. Events ORDER BY created_at DESC + actor + actor=null для system-event с user_id=null;
  5. RLS+app-фильтр НЕ показывает события чужого tenant'а с тем же deal_id;
  6. Лимит 50 событий (60 записей → 50).

- **(3) Frontend integration:**
  - `api/deals.ts::getDeal(id, tenantId)` — GET-helper с типизированным response (`ApiDealEvent`/`ApiDealDetail`/`GetDealResponse`); без `ensureCsrfCookie` (GET-only).
  - `composables/dealsApiMapper.ts::mapApiDealEvent` — converter `ApiDealEvent → DealEvent` (UI-формат): event-slug clamp на known types (`deal.{created,status_changed,viewed,commented,assigned,balance_charged}`) с fallback на `'deal.viewed'`; `detail` зависит от type — для status_changed строим `«from → to»` из context, для created — `«Лид принят (источник: …)»`, для остальных — JSON-сводка контекста.
  - **DealDetailDrawer** получил optional `tenantId` prop. `watch([open, deal.id, tenantId])` с `immediate: true` — на open=true вызывает `loadEvents()`. Если deal + tenantId есть → getDeal → `events.value = events.map(mapApiDealEvent)`. На fail → `eventsFetchError=true` + `v-alert type=warning` + fallback на MOCK_EVENTS. Без tenantId — fetch'а нет, MOCK_EVENTS как раньше.
  - DealsView и KanbanView передают `:tenant-id="auth.user?.tenant_id"`.

- **(4) Vitest +4** в DealDetailDrawerApi.spec.ts:
  - Без tenantId → getDeal НЕ вызывается + MOCK_EVENTS видны.
  - С tenantId → getDeal вызывается + events заменены на API + «new → paid» виден.
  - reject → eventsFetchError + alert + MOCK_EVENTS fallback.
  - open=false → getDeal НЕ вызывается.

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **273/273 за 20.76 сек** (+4 от 269).
  - `npm run build` — vite OK 1.12 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 205/205 за 24.19 сек** (+8 от 197, 812 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - Polling/SSE для real-time обновления списка сделок и timeline (на MVP — manual reload-btn в DealsView/KanbanView).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.62 относительно v1.61:**

- **(1) Установлен `phpoffice/phpspreadsheet:^5.0`** (v5.7.0) — стандартная PHP-библиотека для генерации XLSX. Heavy dep (~10 MB через ext-zip+ext-xml транзитивы), но ничего легче для production-grade XLSX нет.

- **(2) Backend `DealController::export`** — расширен:
  - Body: `{tenant_id, ids, format?: 'csv' | 'xlsx'}`. Default = 'csv' (backward-compat — старые клиенты v1.51 продолжают получать CSV).
  - При `format=xlsx`: `buildXlsx($rows)` → `Spreadsheet`+`setTitle('Сделки')`+`setCellValue('A1'..'G1')` для headers+`getStyle('A1:G1')->getFont()->setBold(true)`+`setAutoSize` для всех колонок A..G.
  - `Xlsx` writer пишет через `ob_start/php://output/ob_get_clean` для возврата бинарной строки.
  - Content-Type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` + Content-Disposition с `.xlsx`.

- **(3) Quirk PhpSpreadsheet 5.x:** удалён deprecated `setCellValueByColumnAndRow($col, $row, $val)` — пришлось мигрировать на A1-нотацию (`setCellValue('A2', $val)`). Обнаружено в первом тестовом прогоне (Error 500), исправлено сразу.

- **(4) Pest +4** в DealCreateTest:
  1. xlsx возвращает binary с Content-Type `application/vnd.openxmlformats…` + magic bytes `"PK\x03\x04"` (XLSX = ZIP) + размер >2KB.
  2. Распаковка через `IOFactory::createReader('Xlsx')` — sheet «Сделки» + A1='ID' + B1='Имя' (bold=true) + A2/B2/C2 = реальные данные сделки.
  3. 422 на неизвестный format ('pdf' не в whitelist).
  4. Default (без format) — backward-compat CSV.

- **(5) Frontend:**
  - `api/deals.ts` разделён на `exportDeals` (CSV string) + `exportDealsXlsx` (Blob, axios `responseType='blob'`).
  - `applyBulkExport(format='xlsx' | 'csv')` в DealsView — default 'xlsx' (UX-выигрыш: Excel-friendly, особенно RU-локаль с 1С).
  - XLSX-ветка → `exportDealsXlsx` → `triggerBlobDownload` (новый helper); CSV-ветка → старый `exportDeals`/`triggerCsvDownload`.
  - На fail → fallback на local CSV (без backend'а xlsx не построить).

- **(6) Vitest +3** в DealsListIntegration:
  - xlsx default вызывает `exportDealsXlsx` (НЕ `exportDeals`) + Blob-download + toast «XLSX».
  - `applyBulkExport('csv')` вызывает `exportDeals` (НЕ Xlsx) + toast «CSV».
  - xlsx reject → fallback на local CSV + toast «Backend недоступен».

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **269/269 за 18.49 сек** (+3 от 266).
  - `npm run build` — vite OK 982 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 197/197 за 26.05 сек** (+4 от 193, 784 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - Polling/SSE для real-time обновления списка сделок (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.61 относительно v1.60:**

- **(1) Backend `DealController::transition`** — POST /api/deals/transition:
  - Body: `{tenant_id, ids: [1..1000 ints], status: slug}`.
  - Валидация status: `DB::table('lead_statuses')->where('slug', X)->exists()` — 422 если не существует. `lead_statuses` глобальная (НЕ tenant-aware), system+custom slug'и в одном scope.
  - RLS + defense-in-depth `where('tenant_id', $tenantId)->whereIn('id', $ids)` — partial-update: `updated < requested` если часть id из другого tenant'а.
  - `ActivityLog::create(event=deal.status_changed, context={from, to, source: 'bulk'})` для каждой ИЗМЕНЁННОЙ сделки. NO-OP (старый==новый) — НЕ пишется в audit, иначе спам.
  - Ответ: `{updated, requested, status}`.

- **(2) Pest +7** в DealTransitionTest:
  1. 422 missing fields; 2. 404 unknown tenant; 3. 422 неизвестный slug + сделка не апдейтится;
  2. Batch update 3 сделок + 3 ActivityLog с `from='new', to='paid', source='bulk'`;
  3. NO-OP не пишет ActivityLog;
  4. Defense-in-depth: 2 id из разных tenant'ов → updated=1 / requested=2; чужой остаётся в исходном статусе;
  5. 422 пустой массив ids.

- **(3) Frontend integration:**
  - **`dealsApi.transitionDeals`** — типизированный helper с `ensureCsrfCookie` (mutating).
  - **`applyBulkStatus` в DealsView**: переписан с sync на async — optimistic local-update (UI отвечает сразу), затем backend-вызов если auth.user.tenant_id. На success — `statusToast «Обновлено N из M.»`, на fail — `«Не удалось сохранить статус — изменения только локально.»` + локальный update НЕ откатывается (UX rationale: пользователь видит что хотел, перезагрузит позже).
  - Без auth.user — только optimistic, API НЕ вызывается (legacy local-mode сохранён).

- **(4) Reload-btn** в DealsView и KanbanView — outlined button «Обновить» с mdi-refresh, привязан к `loadDeals`. В DealsView `:loading="loading"` chip крутится во время fetch'а. Заменяет polling/SSE до прихода long-poll'а в production.

- **(5) Vitest +5**:
  - DealsView: reload-btn вызывает listDeals дважды; applyBulkStatus с tenant_id вызывает transitionDeals + optimistic update + toast; applyBulkStatus без tenant_id — только локально; applyBulkStatus reject — toast warning + локальный update НЕ откатывается.
  - KanbanView: reload-btn вызывает listDeals дважды.

- **PHPStan baseline регенерирован**.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **266/266 за 18.16 сек** (+5 от 261).
  - `npm run build` — vite OK 1.06 сек.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 193/193 за 23.27 сек** (+7 от 186, 767 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - Реальный XLSX-export через PhpSpreadsheet (CSV достаточен на MVP).
  - Polling/SSE для real-time обновления (на MVP — manual reload-btn).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.60 относительно v1.59:**

- **(1) Backend `DealController::index`** — список сделок с фильтрами:
  - Query-params: `tenant_id` (required, 422/404), `status_in[]` (whereIn по `status`), `project_id` / `manager_id` (точное совпадение), `search` (ILIKE по phone+contact_name), `limit` clamp [1..500] default 100, `offset` default 0.
  - ORDER BY `received_at DESC, id DESC`. Eager-load `project:id,name` + `manager:id,email,first_name,last_name`.
  - RLS-обёртка `SET LOCAL app.current_tenant_id` + **defense-in-depth `where('tenant_id', $tenantId)`** на уровне query — на тестах через postgres superuser RLS обходится BYPASSRLS, app-фильтр гарантирует изоляцию.
  - Ответ: `{deals: [{id, tenant_id, project_id, project_name, phone, contact_name, status, manager_id, manager_name, manager_initials, received_at}, ...], total, limit, offset}`. `manager_name/initials` форматируются через `ManagerController::formatName/formatInitials` (re-use static-helper'ов).
  - `cost` НЕ возвращаем (живёт в `supplier_lead_costs` partition'е; лишний JOIN под limit=200 не нужен на MVP).

- **(2) Pest +12** в `tests/Feature/DealIndexTest.php`:
  1. 422 без tenant_id; 2. 404 unknown tenant; 3. пустой список; 4. relations (project_name + manager_name «Иван П.» + initials «ИП» + ISO received_at);
  2. RLS-изоляция (deal чужого tenant'а НЕ возвращается через `where(tenant_id)`); 6. ORDER BY received_at DESC (3 сделки oldest/middle/newest); 7. status_in[] фильтр (передаём 2 status'а query-string-массивом); 8. project_id точное; 9. manager_id точное;
  3. search ILIKE (Соколова / 903 / `сокол`); 11. limit+offset (5 сделок, limit=2 offset=1); 12. manager_name+initials=null когда manager_id null.

- **(3) Frontend integration:**
  - **`api/deals.ts::listDeals`** — типизированный axios-helper с `ApiDeal`/`ListDealsParams`/`ListDealsResponse`; camelCase в DTO, snake_case на wire через axios `params`-mapping.
  - **`composables/dealsApiMapper.ts::mapApiDeal(api, now=new Date())`** — converter ApiDeal → MockDeal:
    - `name = contact_name ?? phone` (fallback на телефон когда контакт неизвестен).
    - `manager = {name: 'Не назначен', initials: '—'}` если manager_id=null.
    - `project = project_name ?? '—'`.
    - `cost = 0` (отдельного endpoint'а на сделку нет; пустое значение под формат).
    - `receivedMinutesAgo = max(0, floor((now - received_at) / 60_000))` — clamp на 0 от clock-skew.
  - **DealsView/KanbanView** получили `onMounted(loadDeals)`: async-вызов `dealsApi.listDeals({tenantId: auth.user.tenant_id, limit: 200/500})`, на success replace `dealsState`/`dealsByStatus` через splice, на fail — `fetchError=true` + `v-alert type=warning «Backend недоступен — показаны mock-данные»` (data-testid="fetch-error-alert"), MOCK_DEALS остаются. Без auth.user — listDeals НЕ вызывается (Vitest без auth setup продолжает работать без mock'а).
  - KanbanView `loadDeals` сначала очищает все 14 колонок (splice 0..length), затем распределяет по `statusSlug`; обновляет `totalDeals` из API total.

- **(4) Vitest +14** (всего **261/261 за 19.62 сек**): `dealsApiMapper.spec.ts` 8 + `DealsListIntegration.spec.ts` 6 (DealsView без tenant_id — НЕ вызывает API + MOCK_DEALS остаются; с tenant_id — replace dealsState на 2 API-сделки; reject → fetchError=true + alert + fallback; KanbanView те же 3 сценария).

- **PHPStan baseline регенерирован** для +новых ignored Pest TestCall warnings.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **261/261 за 19.62 сек** (+14 от 247).
  - `npm run build` — vite OK 989 ms (KanbanView lazy-chunk 180.53→181.98 KB из-за `mapApiDeal`).
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 186/186 за 22 сек** (+12 от 174, 742 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - Реальный XLSX-export через PhpSpreadsheet (CSV достаточен на MVP).
  - Polling/SSE для real-time обновления списка сделок (на MVP — manual reload через перерефреш страницы; кнопка reload-btn — отдельный коммит).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — backend-frontend интеграция не двигает счётчик продуктовых вопросов.

**Что изменилось в v1.59 относительно v1.58:**

- **(1) SupplierResolver service-extract** — закрыт TODO (a) из v1.58:
  - `App\Services\SupplierResolver::resolveForProject(Project): ?int` — точная копия query, что была в `ProcessWebhookJob::resolveSupplierId` и `DealController::resolveSupplierId` (project_suppliers JOIN suppliers, фильтры is_active+is_active, ORDER BY sort_order, id).
  - `App\Services\SupplierResolver::costRubSnapshot(int): string` — отдельный helper для snapshot цены при записи в `supplier_lead_costs` (вынесенный `DB::table('suppliers')->value('cost_rub')` из обоих мест, чтобы snapshot не менялся при последующих правках цены).
  - **DI**: через `app(SupplierResolver::class)` внутри handle()/store() — тот же паттерн, что у `DuplicateDetector` в v1.23. НЕ через constructor injection — иначе тесты не смогли бы вызывать `(new ProcessWebhookJob(...))->handle()` напрямую без контейнера.
  - **Удалены** в `ProcessWebhookJob` и `DealController`: private `resolveSupplierId()` (по 14 строк) + локальные `DB::table('suppliers')->value('cost_rub')`. Оба файла стали короче, ссылочная логика — в одном месте.

- **(2) Pest +8** в `tests/Feature/Services/SupplierResolverTest.php`:
  1. null когда у проекта нет связей.
  2. Возвращает supplier_id единственного активного поставщика.
  3. Пропускает `suppliers.is_active=false`.
  4. Пропускает `project_suppliers.is_active=false`.
  5. Соблюдает ORDER BY `sort_order, id` (low first).
  6. null если все связи inactive.
  7. Изоляция по project_id (один supplier на двух проектах не проявляется на пустом проекте).
  8. costRubSnapshot возвращает `'137.50'` (как string).

- **Quirk:** `Project::factory()->create(['type' => 'websites'])` падает на CHECK `projects_type_check` (allowed: webhook|manual|import) — factory default `'webhook'` достаточен, лишний override убран.

- **PHPStan baseline регенерирован** для +30 ignored Pest TestCall warnings (новый файл).

- **Регресс зелёный:**
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 174/174 за 21.46 сек** (+8 от 166, 708 assertions).
  - `npm run test:vue` — **247/247 за 17.53 сек** (нетронут — backend-only refactor).

- **Что НЕ сделано (production TODO остаточные):**
  - Реальный XLSX-export через PhpSpreadsheet (CSV достаточен на MVP).
  - GET /api/deals для замены MOCK_DEALS в DealsView/KanbanView (опционально — local-state ok).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — service-extract не двигает счётчик продуктовых вопросов.

**Что изменилось в v1.58 относительно v1.57:**

- **(1) GET /api/managers + /api/projects + manager FK guard в DealController:**
  - `ManagerController::index` — active users тенанта (фильтры `is_active=true + deleted_at IS NULL`). Формат `{id, email, first_name, last_name, name, initials}` с static-helpers `formatName/formatInitials` (fallback на email если first/last пусты).
  - `ProjectController::index` — active projects (фильтр `is_active=true`). Формат `{id, name, tag, type}`.
  - Оба endpoint'а: `tenant_id` query-param, 422 без него, 404 unknown tenant, RLS-обёртка `SET LOCAL app.current_tenant_id` в DB::transaction.
  - **Manager FK guard** в `DealController::store` — если `manager_id` передан, проверяем `User::where(id+tenant_id+is_active=true+deleted_at=null)->exists()`. Не принадлежит/не активен → 422. Закрывает security-gap: иначе можно было назначить чужого менеджера на свою сделку.
  - **Pest +8** в LookupsTest: managers active filter / initials fallback на email / 422 без tenant_id / 404 unknown / projects active filter / manager FK guard 3 кейса (чужой → 422 / inactive → 422 / свой active → 201).

- **(2) Replace MOCK_MANAGERS / MOCK_PROJECTS на API в NewDealDialog:**
  - Новые ref'ы `projectOptions: string[]` + `managerOptions: MockManager[]` инициализированы из MOCK-констант (fallback).
  - При open dialog'а с tenantId — `loadLookups()` через `Promise.all([listProjects, listManagers])` replace'ит refs. Map `managerIdByName: Map<string, number>` для submit'а.
  - На fail (network) — silent fallback на mock.
  - Submit передаёт `manager_id: managerIdByName.get(manager.name) ?? undefined`.
  - **Vitest +2** в NewDealDialog: loadLookups вызывает оба + populates refs+map / submit передаёт backend manager_id из mapping.

- **(3) SupplierLeadCost для manual-leads** — закрыт TODO из v1.57:
  - В `DealController::store` транзакции после Deal::create вызываем `resolveSupplierId($project)` (точная копия логики из `ProcessWebhookJob`: project_suppliers JOIN suppliers, фильтры is_active+is_active, ORDER BY sort_order, id).
  - Если supplier найден — берём `cost_rub` snapshot и создаём `SupplierLeadCost` с `supplier_lead_id=NULL` (manual: нет внешнего id из webhook).
  - Manual-flow по-прежнему НЕ списывает баланс (Ю-2 reseller-модель: charge только при webhook'е); cost-аналитика всё равно нужна (owner мог купить лид и ввести руками).
  - На production: `resolveSupplierId` в `App\Services\SupplierResolver` (Job+Controller разделяют логику + system_settings fallback).
  - **Pest +2** в DealCreateTest: SupplierLeadCost создан с snapshot cost_rub / без supplier — graceful skip.

- **Старый тест из v1.48** (`manager_id=42`) переписан под FK guard: создаётся `User::factory()->for($tenant)` чтобы 422 не сработал.

- **PHPStan baseline регенерирован** для +28 ignored Pest TestCall warnings (расширение DealCreateTest + новый LookupsTest).

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **247/247 за 16.32 сек** (+2 от 245).
  - `npm run build` — vite OK 951 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 166/166 за 22.11 сек** (+10 от 156, 699 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - `resolveSupplierId` рефактор в `App\Services\SupplierResolver` — чтобы Job + Controller разделяли код.
  - Реальный XLSX-export через PhpSpreadsheet (CSV достаточен на MVP).
  - GET /api/deals для замены MOCK_DEALS в DealsView/KanbanView (опционально — local-state ok).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — lookups+integrity не двигают счётчик продуктовых вопросов.

**Что изменилось в v1.57 относительно v1.56:** **3 backend-completion изменения**: POST /api/deals (DealController + manual create + frontend integration) / `webhook_hmac_required` flag в system_settings / POST /api/deals/export (CSV backend-side). **Pest 156/156 (675 assertions) + Vitest 245/245 + Histoire 21/28 зелёные**.

**Что изменилось в v1.57 относительно v1.56:**

- **(1) POST /api/deals — manual create endpoint для NewDealDialog** — закрыт TODO из v1.55:
  - `DealController::store` валидирует `tenant_id/project_name/phone` (required) + optional `contact_name/status/manager_id/comment`. Резолвит `Project` через `firstOrCreate(tenant_id+name, type='manual')`. Создаёт `Deal` с `received_at=NOW()`, `source_crm_id=NULL`, `assigned_at=NOW()` если `manager_id` передан.
  - Транзакция + RLS-обёртка `SET LOCAL app.current_tenant_id` (PgBouncer-safe). Manual-create НЕ списывает баланс (не закупка), НЕ применяет антифрод-дедуп, НЕ создаёт SupplierLeadCost. Пишет ActivityLog с `context.source=manual`.
  - **NewDealDialog.vue** получил optional `tenantId` prop. Если передан — submit делает `dealsApi.createDeal()`, на success deal получает реальный backend-id; на network/500 → fallback на local-id + `submit-error-alert` warning + dialog остаётся открытым. Чистый local-mode (без tenantId) сохранён для тестов.
  - **DealsView/KanbanView** получили `useAuthStore` + передают `:tenant-id="auth.user?.tenant_id"`.
  - **`api/deals.ts`** новый файл с `createDeal/exportDeals`.
  - **Pest +8** в DealCreateTest: store + manual source + project firstOrCreate + используя existing project + ActivityLog manual + 422 без полей + 404 unknown tenant + default status='new' + manager_id→assigned_at + manual НЕ списывает баланс.

- **(2) `webhook_hmac_required` flag в system_settings**:
  - Добавлен ключ в seed `db/schema.sql:2200` (`'webhook_hmac_required', 'false', 'bool'` — backward-compat default).
  - `WebhookReceiveController::isHmacRequired()` private helper читает значение через `SystemSetting::find` (без записи → false).
  - При `true`: запрос без `X-Webhook-Signature` → 401 («header требуется (HMAC обязателен)»).
  - При `false`: header опционален (verify если есть, иначе пропускаем).
  - **Pest +3** в WebhookReceiveTest: required+missing → 401, required+valid HMAC → 202, false+missing → 202.

- **(3) POST /api/deals/export — backend CSV endpoint** — закрыт TODO из v1.56:
  - `DealController::export` валидирует `tenant_id` + `ids[1-10000 ints]`. RLS-обёрнутый SELECT по `whereIn(ids)`, формирует CSV (Excel-friendly: BOM `"\u{FEFF}"` PHP-литерал, `;` разделитель, `\r\n`, escape для `;`/`"`/`\n` через двойные кавычки).
  - Возвращает `text/csv; charset=utf-8` + `Content-Disposition: attachment; filename="deals_export_YYYY-MM-DD.csv"`.
  - **Frontend `applyBulkExport`** теперь сначала пробует `dealsApi.exportDeals` (если `auth.user?.tenant_id` есть) → `triggerCsvDownload`; на fail — fallback на `buildLocalCsv()` (вынесен в отдельную функцию).
  - **Pest +4** в DealCreateTest: CSV с правильными headers+BOM+filename / 422 без ids / 404 unknown tenant / фильтрация по запрошенным ids.

- **Vitest +3** (всего **245/245 за 17.07 сек**):
  - NewDealDialog: без tenantId — local-mode (createDeal не вызывается); с tenantId+success — emit deal с backend-id; с tenantId+error — fallback+warning+dialog open.
  - DealsView/KanbanView spec'ы получили `setActivePinia(createPinia())` для auth-store.

- **Quirks:**
  - PHPStan ругался на `Deal->id === null` (Eloquent типизирует id как int) — убрал лишнюю проверку.
  - `String.fromCharCode(0xFEFF)` в JS / `"\u{FEFF}"` в PHP — оба работают, литерал заблокирован ESLint no-irregular-whitespace.
  - RLS-изоляция export'а тестируется отдельно через testing_rls_user (NOLOGIN без BYPASSRLS) — DealCreateTest использует postgres superuser, RLS-проверка была бы false-positive — заменил на тест фильтрации по `whereIn(ids)`.

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **245/245 за 17.07 сек** (+3 от 242).
  - `npm run build` — vite OK 1.04 сек.
  - `composer pint` + `composer stan` — passed (baseline регенерирован).
  - `composer test` — **Pest 156/156 за 20.27 сек** (+15 от 141, 675 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - Manager lookup в DealController (сейчас manager_id передаётся клиентом без проверки tenant-membership).
  - Replace MOCK_MANAGERS / MOCK_PROJECTS на API GET /api/managers и /api/projects.
  - SupplierLeadCost для manual-leads (при наличии supplier'а у проекта).
  - Реальный XLSX-export через PhpSpreadsheet (CSV пока достаточен).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — backend-completion не двигает счётчик продуктовых вопросов.

**Что изменилось в v1.56 относительно v1.55:** **3 production-tightening изменения** после 7-фичного пакета v1.55: HMAC+rate-limit для webhook / реальный fetch system_settings из backend / CSV-export для bulk-actions. **Pest 141/141 (627 assertions) + Vitest 242/242 + Histoire 21/28 зелёные**.

**Что изменилось в v1.56 относительно v1.55:**

- **(1) HMAC + per-token rate-limit для webhook receive** — закрыты 2 production-TODO из v1.55:
  - `WebhookReceiveController::receive` теперь делает 3 проверки в порядке: tenant lookup (404) → rate-limit (429) → HMAC (401) → валидация payload (422).
  - **HMAC**: опциональный header `X-Webhook-Signature: sha256=<hex>`, верификация через `hash_hmac('sha256', raw_body, webhook_token)` + `hash_equals` (constant-time против timing attacks). На MVP — backward-compat: header отсутствует → пропускаем. На prod через `system_settings.webhook_hmac_required` будет обязательным. Невалидная подпись → 401 (auth issue, не payload).
  - **Per-token rate-limit**: `RateLimiter::tooManyAttempts("webhook:{tenant_id}", rps×60)` с decay 60 сек. Лимит из `system_settings.webhook_rate_limit_rps` (default 100 RPS из seed v8.7) приводится к per-minute через ×60. На превышении — 429 + `Retry-After` header + `retry_after` в JSON.
  - Rate-limit hit ставится **до** валидации payload — иначе можно обойти спамом 422-ответов.
  - **Pest +5** в WebhookReceiveTest: HMAC valid (через `$this->call('POST', ..., $rawBody)` для сырого body) + invalid (401 + assertNothingDispatched) + missing header (202 backward-compat); rate-limit с `SystemSetting::update(['value'=>'1'])` — 60 успешных + 61-й = 429 + Retry-After header; ключ изолирован per-token.
  - `RateLimiter::clear` в `beforeEach` чтобы не загрязнять следующий тест.

- **(2) Реальный fetch system_settings в AdminSystemView** — закрыт TODO из v1.55:
  - `onMounted(loadSettings)` вызывает `adminApi.listSystemSettings()` и replace'ит `settingsState.splice(0, length, ...fromApi)` (сохраняет reactive-ref).
  - На fetch-error → fallback на mock-данные + warning v-alert (closable, `fetch-error-alert` data-testid).
  - Кнопка «Обновить» (`reload-btn`) в header триггерит ручной reload.
  - Type-shape совместим: `AdminSystemSetting` (mock) и `ApiSystemSetting` (backend) различаются только origin (mock vs БД).
  - **Vitest +3**: `listSystemSettings` вызван once на mount; reload-btn триггерит fetch; on rejection → warning-alert visible + 7 mock-rows preserved.

- **(3) Реальный CSV-export для bulk-actions в DealsView** — закрыт TODO из v1.55:
  - `applyBulkExport()` формирует CSV и триггерит download через `Blob` + `<a download>`.
  - 8 колонок: ID/Имя/Телефон/Статус/Проект/Менеджер/Стоимость/Получено мин назад.
  - CSV-escape для значений с `;`/`"`/`\n`. Разделитель `;` (Excel-friendly для русской локали). Line-endings `\r\n` (Windows).
  - **BOM** через `String.fromCharCode(0xFEFF)` (литеральный U+FEFF блокируется ESLint `no-irregular-whitespace`) — Excel правильно распознаёт UTF-8 кириллицу.
  - Filename `deals_export_YYYY-MM-DD.csv`. Toast «Экспортировано N сделок в CSV».
  - Empty selection → toast «Нет выбранных» без download.
  - **Vitest +2**: spy на `URL.createObjectURL` + `HTMLAnchorElement.prototype.click` — assert called once + correct toast; empty selection → НЕ вызывает createObjectURL.

- **PHPStan baseline регенерирован** (новые Pest TestCall ignored count'ы для расширенного WebhookReceiveTest).

- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **242/242 за 15.82 сек** (+4 от 238).
  - `npm run build` — vite OK 903 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 141/141 за 17.8 сек** (+5 от 136, 627 assertions).

- **Что НЕ сделано (production TODO остаточные):**
  - Webhook HMAC обязательным через flag в system_settings (`webhook_hmac_required`).
  - Реальный backend-export через POST /api/deals/export → ReportsView с XLSX (xlsx-библиотека).
  - SaaS-admin auth (Yandex 360 SSO ⏸ Б-1) — все admin-endpoint'ы на MVP без middleware.
  - Two-person approval для system_settings.update суммы > admin_four_eyes_threshold_rub.
  - Live cookie-swap session для impersonation.
  - POST /api/deals для NewDealDialog (сейчас local-state).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).

- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — production-tightening не двигает счётчик продуктовых вопросов.

**Что изменилось в v1.55 относительно v1.54:** **7-фичный auto-mode пакет** (Bulk-actions DealsView / NewDealDialog / AdminTenantDetailView / Edit-flow system_settings / Webhook receive endpoint / Smart-filters / AdminImpersonationView). **Pest 136/136 (495 assertions) + Vitest 238/238 + Histoire 21/28 зелёные**.

**Что изменилось в v1.55 относительно v1.54:**

- **(1) Bulk-actions DealsView** — `dealsState` reactive-копия MOCK_DEALS (deep-clone) для безопасного bulk-edit. Bulk-bar (sticky-top, теало-нуар theme=dark) появляется при `selected.length > 0`: count + 4 actions:
  - **Сменить статус** через v-menu со всеми 14 lead_statuses → batch-update statusSlug.
  - **Экспорт** → v-snackbar «Экспорт N сделок поставлен в очередь» (TODO: POST /api/deals/export).
  - **Удалить** → v-dialog confirm → batch-splice + reset selected (TODO production: soft-delete с архивом 30 дней).
  - **✕** → clear selected.
- **(2) NewDealDialog** — модалка с 6 полями: name/phone/project (v-select из MOCK_PROJECTS) / manager (v-select из MOCK_MANAGERS, return-object) / cost (number) / status (v-select из 14 lead_statuses, default `presetStatus` или 'new'). Phone-валидация ≥10 цифр; client-side валидация перед emit('created'). Используется в DealsView (push в начало dealsState) и KanbanView (push в правильную колонку по statusSlug + totalDeals++). MOCK_PROJECTS/MOCK_MANAGERS добавлены в `composables/mockDeals.ts`.
- **(3) AdminTenantDetailView** — drill-down `/admin/tenants/:code`. Hero (name + ИНН + legal_address + contact_email/phone + status-chip + Войти-как-клиент-кнопка). 4 KPI cards (Баланс с runway / Тариф+MRR/мес / Лиды сегодня+неделя+месяц / Средняя цена). 4 v-tabs (Финансы balance-history table / Пользователи / Проекты / Активность с event-кодами). 404-fallback. `composables/mockTenantDetail.ts::expandTenantDetail(t)` — 5 sample-users (admin/manager×2/reader) + 2 sample-projects + 8 sample-balance-tx + 5 sample-activity-events. AdminTenantsView получил `@click:row` → `router.push({name: admin-tenant-detail})`.
- **(4) Edit-flow AdminSystemView (audit-log + 2-step)** — Backend:
  - `App\Models\SystemSetting` (PK=key string, без CREATED_AT). `App\Models\SaasAdminAuditLog` (append-only без UPDATED_AT, payload_before/after JSONB casts).
  - `AdminSystemSettingsController` с GET (список) + PUT (update). PUT в `DB::transaction`: update setting + INSERT в `saas_admin_audit_log` (action='system_settings.update' + payload_before/after + reason + ip_address + user_agent). Hash-chain trigger BEFORE INSERT заполняет log_hash (OPEN-И-15).
  - Type-validation: int → ctype_digit (с минусом для signed); decimal → is_numeric; bool → in('true','false','1','0'); json → JSON_THROW_ON_ERROR. Reason ≥30 chars. No-op (same value) → 422.
  - Frontend `SystemSettingEditDialog` — 3-step (edit → confirm с diff before/after + reason → done). AdminSystemView получил кнопку «Изменить» в каждой строке + `onSettingUpdated` optimistic update в локальном `settingsState`.
- **(5) Webhook receive endpoint** — `App\Http\Controllers\Api\WebhookReceiveController::receive` POST `/api/webhook/{token}`:
  - Token = `tenants.webhook_token`. Валидация payload (vid integer ≥1, project string, phone string, time integer ≥1, nullable tag/phones).
  - 404 на unknown token; 422 на bad payload; 202 на success + `dispatch(ProcessWebhookJob::class)`.
  - Stub-INSERT в `webhook_log` через DB::table (если таблица есть). Обёрнут в DB::transaction + SET LOCAL app.current_tenant_id для RLS.
  - **CSRF-исключение** для `api/webhook/*` в bootstrap/app.php — внешний клиент без сессии браузера.
  - На prod: + HMAC-валидация X-Webhook-Signature (OPEN-И-15 secret rotation) + per-token rate-limit (по `system_settings.webhook_rate_limit_rps`).
- **(6) Smart-filters** — DealsView получил multi-select v-select (Проект + Менеджер) с `availableProjects`/`availableManagers` computed. AdminTenantsView получил аналогичные filterStatuses (4 STATUS_OPTIONS) + filterTariffs. Кнопка «Сбросить» появляется только когда фильтры активны.
- **(7) AdminImpersonationView** — Backend +2 GET endpoint:
  - `/active` — used_at != null AND session_ended_at == null (с tenant_name eager-load).
  - `/recent` — last 20 завершённых с `duration_seconds` через `abs(session_ended_at->diffInSeconds(used_at))` (Carbon `diffInSeconds` signed по умолчанию — без `abs()` возвращал отрицательное).
  - `ImpersonationToken` получил `belongsTo(Tenant)`.
  - Frontend view с 2 секциями: Активные (table + end-кнопка per row) / Недавно завершённые (read-only table). Refresh-btn + onMounted load.
  - Маршрут `/admin/impersonation` + 5-й nav-пункт «Impersonation» mdi-account-switch в AdminLayout.
- **Vitest +48** (всего **238/238 за 15.31 сек**): bulk-actions 6, NewDealDialog 6, AdminTenantDetailView 10, SystemSettingEditDialog 8, AdminSystemView +3, AdminTenantsView +4, DealsView smart-filters 3, AdminImpersonationView 6, KanbanView +2 (new-deal flow). Setup получил `visualViewport` polyfill (VOverlay/v-menu/v-snackbar location strategies).
- **Pest +16** (всего **136/136 за 15.8 сек**, 495 assertions):
  - AdminSystemSettings 8 (list + update int/bool/string/decimal type-validation + reason<30 + unknown key + same-value no-op + audit-log payload + ip+ua).
  - WebhookReceive 6 (202 + dispatch + 404 unknown token + 422 missing fields + 422 bad types + CSRF excluded + phones array).
  - Impersonation +2 (active filter + recent с duration).
- **PHPStan baseline регенерирован** (0 errors после фикса nullsafe.neverNull в SystemSettingsController).
- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format` — passed.
  - `npm run test:vue` — **238/238 за 15.31 сек** (+48 от 190).
  - `npm run build` — vite OK 937 ms.
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 136/136 за 15.8 сек** (+16 от 120, 495 assertions).
- **Что НЕ сделано (production TODO):**
  - HMAC-валидация webhook (OPEN-И-15 secret rotation).
  - Per-token rate-limit для webhook (по system_settings.webhook_rate_limit_rps).
  - SaaS-admin auth (Yandex 360 SSO ⏸ Б-1) — все admin-endpoint'ы на MVP без middleware.
  - Two-person approval для system_settings.update суммы > admin_four_eyes_threshold_rub.
  - Live cookie-swap session для impersonation (сейчас end лишь ставит флаг в БД).
  - Реальный fetch для system_settings и tenants (сейчас mock-данные).
  - Bulk-export реальный (CSV/XLSX через xlsx-библиотеку).
  - Сделка-сохранение через POST /api/deals (сейчас local-state).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — расширение фич не двигает счётчик продуктовых вопросов.

**Что изменилось в v1.54 относительно v1.53:** **Impersonation UI dialog (Ю-1 frontend)**. Закрыт TODO из v1.53 — кнопка «Войти как клиент» в AdminTenantsView с 4-step state-machine диалогом (reason → verify → active → done). **Pest 120/120 + Vitest 190/190 + Histoire 21/28 зелёные**.

**Что изменилось в v1.54 относительно v1.53:**

- **`api/admin.ts`** — типизированные axios-helpers `impersonationInit/Verify/End` для трёх endpoint'ов из v1.53. Все три — `ensureCsrfCookie()` + `apiClient.post` (Sanctum SPA cookie-flow). На prod автоматически перейдут под `middleware('auth:saas-admin')` без изменений на клиенте — `withCredentials: true` уже в apiClient.
- **`components/admin/ImpersonationDialog.vue`** — 4-step state-machine:
  - **step 1 «reason»** — `v-textarea` rows=3 + counter 200 + hint «Ещё N символов». Валидация ≥30 chars на клиенте до POST + ловля backend 422 через `extractValidationErrors` (если backend ответит 422 reason).
  - **step 2 «verify»** — info-alert «Код отправлен на email клиента: {sent_to_email}» + `v-text-field` `inputmode=numeric maxlength=6 autocomplete=one-time-code`. На dev — success-alert с `_dev_plain_code` (banner с пометкой «DEV-only: на prod исчезнет»). Pre-API regex-валидация `/^\d{6}$/`.
  - **step 3 «active»** — success-alert «Impersonation активен» + локализованное `usedAtIso` через `toLocaleString('ru-RU')` + `v-btn color=error «Завершить сессию»`.
  - **step 4 «done»** — финальный success + «Закрыть».
  - **`persistent`** dialog — нельзя закрыть кликом за пределами (audit trail).
  - `watch(props.modelValue)` сбрасывает state при каждом открытии (без stale-данных от прошлого тенанта).
- **`AdminTenantsView`** — добавлена 8-я колонка `actions` (width=56) с `v-tooltip` + icon-btn `mdi-account-switch`:
  - `:disabled="item.status === 'suspended'"` — по ТЗ §22.7 impersonation допустим только в активных tenant'ах.
  - `@click.stop` — не пропускаем event дальше (будущий row-click для drill-down не должен срабатывать).
  - `data-testid="impersonate-btn-{id}"` — unique selectors для тестов.
  - `ADMIN_USER_ID = 1` как заглушка (на prod удалится — `requested_by` придёт из `request()->user()->id`).
- **Vitest +11** (всего **190/190 за 13.23 сек**):
  - `ImpersonationDialog.spec.ts` (7): modelValue=false скрыт + step-1 mount + reason<30 показывает counter + успешный init→step2 (email + dev-banner + code-input) + verify-success→step3 (end-btn) + invalid 5-digit code не вызывает API + end→step4 (close-btn) + Cancel emit `update:modelValue=false`.
  - `AdminTenantsView.spec.ts` +4: каждая из 7 строк имеет impersonate-btn + suspended-tenant disabled + click открывает диалог с правильным tenant + props.requestedBy=1.
- **Vitest quirk:** `v-dialog`/`v-tooltip` требуют layout-injection от v-app/v-layout, auto-import vite-plugin-vuetify не работает в Vitest. Stub'ы: `VDialog` как passthrough `<div v-if="modelValue"><slot /></div>`, `VTooltip` как `<div><slot name="activator" :props="{}" /></div>`. `ImpersonationDialog` stub'ится в `AdminTenantsView.spec.ts` (внутри использует api/admin axios). `api/admin` + helpers `extractValidationErrors`/`extractErrorMessage` мокаются через `vi.mock` — `axios.isAxiosError(plain Error)` в jsdom возвращает false (паттерн из `auth-store.spec.ts`).
- **Регресс зелёный:**
  - `npm run lint:vue` ✅ (после `--fix` 6 attribute-order warnings auto-fixed).
  - `npm run type-check` + `format:check` ✅.
  - `npm run test:vue` — **190/190 за 13.23 сек** (+11 от 179).
  - `npm run build` — vite OK 924 ms (AdminTenantsView lazy-chunk **20.68 KB** включает inline ImpersonationDialog).
  - `composer pint` + `composer test` — **Pest 120/120** (backend нетронут).
- **Что НЕ сделано (production TODO):**
  - SaaS-admin auth (Yandex 360 SSO ⏸ Б-1) → middleware → frontend убирает `requestedBy` prop.
  - Two-person approval dialog для tenant'ов с `pd_subject_request.processing_restricted=TRUE`/`chargeback_unrecovered_rub > 0` (CTO-15/Ю-9).
  - Реальный MailService → `_dev_plain_code` исчезает (backend перестанет возвращать поле).
  - Live impersonation session (cookie-swap для admin'а на 1ч).
  - Страница «Активные impersonation-сессии» в админке.
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфра, требует Playwright).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — закрытие frontend-TODO не двигает счётчик продуктовых вопросов (Ю-1 уже был ✅ закрыт на backend в v1.53).

**Что изменилось в v1.53 относительно v1.52:** **Impersonation flow backend (Ю-1)**. Закрыт пункт #9 — финал списка из v1.46. **Все 9 пунктов закрыты**, кроме #6 Yandex SSO (⏸ Б-1) и #7 browser-mode (отложен инфра). **Pest 120/120 + Vitest 179/179 + Histoire 21/28 зелёные**.

**Что изменилось в v1.53 относительно v1.52:**

- **`ImpersonationToken` Eloquent** для `impersonation_tokens` (schema v8.7 §22.7).
- **`ImpersonationController`** с 3 endpoint:
  - `init({tenant_id, requested_by, reason})` — reason ≥30 chars, 6-значный код (random_int), bcrypt-hash, TTL 15 мин.
  - `verify({token_id, code})` — Hash::check; failed_attempts increment; ≥5 → invalidated.
  - `end({token_id})` — session_ended_at.
- На MVP без middleware (saas-admin auth не реализован, requested_by параметром). Production: middleware('auth:saas-admin') + role guards + two-person approval (CTO-15/Ю-9).
- **`_dev_plain_code` в response** — на prod заменится отправкой на email клиента.
- Маршруты `/api/admin/impersonation/{init,verify,end}`.
- **Pest +9** в `tests/Feature/ImpersonationTest.php` (всего **120/120 за 15.62 сек**, 443 assertions).
- **PHPStan baseline** регенерирован.
- **Регресс зелёный:**
  - `composer test` — **Pest 120/120 за 15.62 сек** (+9 от 111).
  - `composer pint` + `composer stan` — passed.
  - `npm run lint:vue` + `type-check` + `build` — passed (846 ms).
- **Что НЕ сделано:**
  - Saas-admin auth (Yandex 360 SSO + middleware) — depends on Б-1 + DO-2.
  - Two-person approval (CTO-15/Ю-9) — после saas-admin auth.
  - Email клиенту с кодом impersonation + о завершении — после MailService на prod.
  - UI dialog в AdminTenantsView («Войти как клиент» с reason+code).
  - **#6 Yandex 360 SSO** ⏸ ждёт Б-1.
  - **#7 Pest browser-mode** — отложен (инфраструктурная работа, требует Playwright).

**Итог: 9-пунктовый список из v1.46 (auth/admin extras) реализован end-to-end** на бэкенде, кроме #6 SSO (заблокирован Б-1) и #7 browser-mode (отложен инфра).

**Что изменилось в v1.52 относительно v1.51:** **Admin views (Биллинг / Инциденты / Система)**. Закрыт пункт #8: 3 placeholder'а заменены на реальные display-views с mock-данными по schema v8.7. **Pest 111/111 + Vitest 179/179 + Histoire 21/28 зелёные**.

**Что изменилось в v1.52 относительно v1.51:**

- **`AdminBillingView`** — 4 stats (MRR / Выручка / Просрочка / Возвраты) + v-data-table 7 колонок + поиск.
- **`AdminIncidentsView`** — 3 stats (Открыто / Расследуется / РКН-уведомлений) + 5 фильтров + v-list с incident_id, severity, status, РКН-pending chip для PDN-breach.
- **`AdminSystemView`** — read-only warning + 7 system_settings из mock + type-chip + поиск.
- **`composables/mockAdmin.ts`** — 3 типа + mock-данные.
- Маршруты `/admin/{billing,incidents,system}` ведут на реальные views (не placeholder).
- **Vitest +13** (всего **179/179 за 11.98 сек**).
- **Регресс зелёный:** lint+type+format OK; build 743ms; story:build 21/28 за 31.5с.
- **Что НЕ сделано:** #9 Impersonation flow (Ю-1).

**Что изменилось в v1.51 относительно v1.50:** **Email-уведомление при 3 неудачных попытках входа (ТЗ §22.4.4 п.3)**. Закрыт последний пункт ТЗ §22.4.4 анти-брутфорс. **Pest 111/111 + Vitest 166/166 + Histoire 21/28 зелёные**.

**Что изменилось в v1.51 относительно v1.50:**

- **`App\Mail\SuspiciousLoginNotification`** Mailable + Blade-template `emails.suspicious_login` (HTML с инструкциями).
- **`AuthController::maybeNotifySuspiciousLogin`** срабатывает ровно при count==3 (защита от спама на 4-5 попытках). Для unknown email — skip.
- На dev `MAIL_MAILER=log` → письмо в storage/logs/laravel.log.
- **Pest +4** (всего **111/111 за 14.32 сек**, 401 assertions).
- ТЗ §22.4.4 анти-брутфорс закрыт полностью: email-rate-limit (v1.45) + IP-lockout (v1.50) + email-notify (v1.51).
- **Регресс зелёный:** Pint+Stan passed; **Pest 111/111**.
- **Что НЕ сделано:**
  - **#6 Yandex 360 SSO** (зависит от Б-1 + DO-2).
  - **#7 Pest browser-mode** для full session-flow.
  - **#8 AdminBilling/Incidents/System** views.
  - **#9 Impersonation flow** (Ю-1).

**Что изменилось в v1.50 относительно v1.49:** **IP-lockout 10/час + auth_log записи (ТЗ §22.4.4 п.2)**. Закрыт пункт #4 — второй слой защиты поверх email-rate-limit. **Pest 107/107 + Vitest 166/166 + Histoire 21/28 зелёные**.

**Что изменилось в v1.50 относительно v1.49:**

- **IP-lockout** — `AuthController::login` перед verify считает count login_failed в auth_log с этого IP за последний час. Если ≥10 → 429 + Retry-After: 3600. Защищает от перебора email'ов с одного IP.
- **`logAuthEvent` helper** в AuthController — пишет в `auth_log` через `DB::table` (event=login_success/login_failed, tenant_id/user_id если известны, ip_address, user_agent, failure_reason). hash-chain trigger BEFORE INSERT заполняет log_hash (OPEN-И-15 tamper-detection).
- **3 ветки failure_reason**: `invalid_password` (user найден, пароль неверный), `unknown_email` (user не найден), `account_locked` (is_active=false).
- **Pest +6** в `tests/Feature/Auth/IpLockoutTest.php` (всего **107/107 за 13.86 сек**, 380 assertions).
- **Регресс зелёный:**
  - `composer test` — **Pest 107/107 за 13.86 сек** (+6 от 101).
  - `composer pint` + `composer stan` — passed.
- **Что НЕ сделано (продолжение списка #5-#9):**
  - **#5 Email при 3 неудачах** login (требует MailService).
  - **#6 Yandex 360 SSO** (зависит от Б-1 + DO-2).
  - **#7 Pest browser-mode** для full session-flow.
  - **#8 AdminBilling/Incidents/System** views.
  - **#9 Impersonation flow** (Ю-1).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.49 относительно v1.48:** **2FA setup wizard + schema v8.8**. Backend: 4 endpoint'а `/api/2fa/{init,confirm,disable,regenerate-recovery-codes}`. Frontend: SecurityTab с 3 v-dialog'ами (setup wizard 3 шага + disable + regenerate). Schema v8.7→v8.8 (`users.totp_secret` VARCHAR(255)→TEXT). Migration fix FK на partitioned. **Pest 101/101 + Vitest 166/166 + Histoire 21/28 зелёные**.

**Что изменилось в v1.49 относительно v1.48:**

- **`TwoFactorSetupController`** под `auth:sanctum`:
  - `init`: генерация TOTP secret + QR-URL (otpauth://...), secret в session как pending, не пишется в БД до confirm.
  - `confirm({code})`: TOTP-verify pending → save `users.totp_secret + totp_enabled=true` + replace `user_recovery_codes` (8 plain returned один раз).
  - `disable({password})`: Hash::check → clear secret + delete recovery codes.
  - `regenerate-recovery-codes({password})`: replace 8 codes.
- **Recovery code format** — `xxxx-xxxx` (lowercase, 4+4+дефис), `Str::random(4)` parts. Hash::check сравнивает с нормализованным (без дефиса) кодом из `/recovery-use`.
- **`User::casts`** — добавлен `'totp_secret' => 'encrypted'` (Crypt::encryptString при save, расшифровка при чтении).
- **Schema v8.7 → v8.8** — `users.totp_secret VARCHAR(255)` → `TEXT`. Encrypted secret = ~256 chars (>255 → PDOException). Запись §V в `db/CHANGELOG_schema.md`.
- **Migration fix** — `0001_01_01_000000_load_initial_schema.php` после `DB::unprepared($sql)` делает явный `ALTER TABLE webhook_dedup_keys ADD FK ... DEFERRABLE INITIALLY DEFERRED`. PDO unprepared() тихо проглатывает FK на partitioned-таблицу при initial load. Без fix'а ON DELETE CASCADE тест ломался.
- **PartitionsCreateMonthsTest::afterEach** — `ALTER TABLE ... DETACH PARTITION` + `DROP TABLE` вместо `DROP TABLE ... CASCADE` (последний дропал FK на parent).
- **Frontend `SecurityTab`** переписан: реальный has2fa из `auth.user?.totp_enabled`; 3 v-dialog'а — setup wizard (3 шага: init→confirm→codes), disable, regenerate. 4 новых функции в `api/auth.ts`.
- **`SettingsView.spec.ts`** — добавлен `createPinia()` в plugins (SecurityTab теперь использует useAuthStore).
- **Pest +10** в `tests/Feature/Auth/TwoFactorSetupTest.php` (всего **101/101 за 13.37 сек**, 364 assertions).
- **PHPStan baseline** регенерирован.
- **Регресс зелёный:**
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 101/101 за 13.37 сек** (+10 от 91, 364 assertions).
  - `npm run test:vue` — **Vitest 166/166 за 10.95 сек**.
  - `npm run lint:vue` + `type-check` + `format:check` — passed.
  - `npm run build` — vite OK 747 ms.
  - `npm run story:build` — **21 stories / 28 variants за 31.18 сек**.
- **Что НЕ сделано (продолжение списка #4-#9):**
  - **#4 IP-lockout 10/час** через auth_log.
  - **#5 Email при 3 неудачах** login.
  - **#6 Yandex 360 SSO** (зависит от Б-1 + DO-2).
  - **#7 Pest browser-mode** для full session-flow.
  - **#8 AdminBilling/Incidents/System** views.
  - **#9 Impersonation flow** (Ю-1).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.48 относительно v1.47:** **Recovery code login (POST /api/auth/2fa/recovery-use)**. Закрыт пункт #2 из списка v1.47 — вход через один из 8 резервных кодов 2FA вместо TOTP. **Pest 91/91 + Vitest 166/166 + Histoire 21/28 зелёные**.

**Что изменилось в v1.48 относительно v1.47:**

- **POST /api/auth/2fa/recovery-use** — `AuthController::useRecoveryCode`. Берёт `pending_user_id` из session (тот же state, что и /2fa/verify), перебирает неиспользованные `user_recovery_codes` через `Hash::check`. На совпадении: mark `used_at` + Auth::login + clear pending. Возвращает `{user, requires_2fa: false, recovery_codes_remaining}`.
- **Нормализация кода** — lowercase + удаление дефисов/пробелов (`'ZZZZ-9999'` ≡ `'zzzz 9999'` ≡ `'zzzz9999'`). Сравнение через `Hash::check` со stored bcrypt-хешем.
- **`UseRecoveryCodeRequest`** — валидация `code: string|min:8|max:32`.
- **Rate-limit** `auth:recovery:{pending_user_id}|{ip}` — 5 неудач/15 мин → 429. Scope отделён от `auth:2fa:`.
- **Eloquent `UserRecoveryCode`** — `user_recovery_codes` (schema v8.7 §10), `UPDATED_AT = null` (в schema только created_at + used_at).
- **Frontend:**
  - `authApi.useRecoveryCode(code)` returns `RecoveryCodeResponse extends LoginResponse + recovery_codes_remaining`.
  - `auth-store::useRecoveryCode` ставит user + сбрасывает requires2fa.
  - `UseRecoveryCodeView.vue` — новый view на `/recovery-use`. autocomplete=one-time-code, submit → store → /dashboard. На success сохраняет remaining в `sessionStorage` (для будущего toast в SettingsView/SecurityTab).
  - `TwoFactorView` ссылка «Использовать резервный код»: `/recovery` → `/recovery-use`.
  - Старый `/recovery` остаётся для display 8 кодов (актуализируется при #3 2FA setup wizard).
- **Pest +6** в `tests/Feature/Auth/RecoveryCodeTest.php` (всего **91/91 за 12.77 сек**, 319 assertions):
  - success + mark used_at + remaining=3.
  - 422 на неверный код.
  - 422 на уже использованный.
  - 422 без pending-сессии.
  - Принимает разные форматы (пробел/дефис/регистр).
  - 429 после 5 неверных попыток.
- **Vitest +6** (всего **166/166 за 11.47 сек**): auth-store useRecoveryCode success/reject; UseRecoveryCodeView 4 (mount + autocomplete + submit + sessionStorage + lockout-alert).
- **PHPStan baseline** регенерирован (накопительно).
- **Регресс зелёный:**
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 91/91 за 12.77 сек** (+6 от 85, 319 assertions).
  - `npm run test:vue` — **Vitest 166/166 за 11.47 сек** (+6 от 160).
  - `npm run lint:vue` + `type-check` + `format:check` — passed.
  - `npm run build` — vite OK 849 ms.
  - `npm run story:build` — **21 stories / 28 variants за 30.36 сек**.
- **Что НЕ сделано (отдельные коммиты, продолжение списка #3-#9):**
  - **#3 2FA setup wizard** (QR + verify + 8 кодов) — после этого `/recovery` view получит реальный source данных.
  - **#4 IP-lockout 10/час** через auth_log.
  - **#5 Email при 3 неудачах** login.
  - **#6 Yandex 360 SSO** (зависит от Б-1 + DO-2).
  - **#7 Pest browser-mode** для full session-flow.
  - **#8 AdminBilling/Incidents/System** views.
  - **#9 Impersonation flow** (Ю-1).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.47 относительно v1.46:** **Reset password (deep-link) + DB timezone fix**. Завершён второй пункт password-reset flow: установка нового пароля по token из email + фикс PG timezone (TIMESTAMPTZ → UTC, без него Carbon::parse терял offset и Password::reset падал на token-expiry check). **Pest 85/85 + Vitest 160/160 + Histoire 21/28 зелёные**.

**Что изменилось в v1.47 относительно v1.46:**

- **POST /api/auth/reset-password** — `AuthController::resetPassword` через `Password::reset()`, callback пишет `password_hash = Hash::make($password)`.
- **`ResetPasswordRequest`** — token + email + password (min 10 по ТЗ §22.4.1) + confirmed.
- **Rate-limit** ключ `auth:reset:{sha256(token)[0..16]}|{ip}` — 5 попыток / 15 мин на тот же token.
- **`config/database.php` pgsql** — добавлен `'timezone' => env('DB_TIMEZONE', 'UTC')`. Без этого PG возвращал TIMESTAMPTZ с offset `+03`, Carbon::parse игнорировал zone → `DatabaseTokenRepository::tokenExpired` ошибочно считал token истёкшим. Фикс затрагивает любую TZ-чувствительную логику (audit-логи, retention, scheduled tariffs).
- **Frontend:** `authApi.resetPassword`, `auth-store::resetPassword` action, `ResetPasswordView.vue` (route `/reset/:token`).
- **Маршруты:** `/reset/:token` (meta.layout=auth, guestOnly) в Vue Router; `Route::view('/reset', 'welcome')` в web.php.
- **Pest +6** в `tests/Feature/Auth/ResetPasswordTest.php` (всего **85/85 за 11.50 сек**): успех + token-update + 422 на bad token + 422 на mismatch + 422 short password + 422 unknown email + 429 rate-limit.
- **Vitest +7** (всего **160/160 за 11.02 сек**): auth-store resetPassword success/429; ResetPasswordView mount + email-prefill + 2 password-inputs + success-state + lockout-alert.
- **PHPStan baseline** регенерирован (накопительно).
- **Регресс зелёный:**
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 85/85 за 11.50 сек** (+6 от 79).
  - `npm run test:vue` — **Vitest 160/160 за 11.02 сек** (+7 от 153).
  - `npm run lint:vue` + `type-check` + `format:check` — passed.
  - `npm run build` — vite OK 784 ms.
  - `npm run story:build` — **21 stories / 28 variants за 30.74 сек**.
- **Что НЕ сделано (отдельные коммиты, продолжение списка #2-#9):**
  - **#2 RecoveryCodesView** (POST /api/auth/2fa/recovery-use).
  - **#3 2FA setup wizard** (QR + verify + 8 кодов).
  - **#4 IP-lockout 10/час** через auth_log.
  - **#5 Email при 3 неудачах** login.
  - **#6 Yandex 360 SSO** (зависит от Б-1 + DO-2).
  - **#7 Pest browser-mode** для full session-flow.
  - **#8 AdminBilling/Incidents/System** views (сейчас placeholder).
  - **#9 Impersonation flow** (Ю-1).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.46 относительно v1.45:** **Forgot password flow (ТЗ §1.7 / Прил. Г.4.3)**. Запрос ссылки на сброс пароля через email с anti-enumeration unified-ответом + rate-limit 5/15мин. Backend через `Password::sendResetLink()`. Frontend `requestPasswordReset` в auth-store + ForgotPasswordView success-state. **Pest 79/79 + Vitest 153/153 + Histoire 21/28 зелёные**.

**Что изменилось в v1.46 относительно v1.45:**

- **POST /api/auth/forgot** — новый endpoint. `AuthController::forgotPassword(ForgotPasswordRequest)`.
- **`AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets`** в `.env` + `phpunit.xml` — указывает на нашу таблицу (schema v8.7 §10.6 — `password_resets` с email PRIMARY KEY + token + created_at). Default Laravel — `password_reset_tokens` (НЕ совпадает).
- **Anti-enumeration:** ВСЕГДА 200 unified-message «Если такой email зарегистрирован — мы отправили ссылку для сброса пароля», независимо от существования user'а. Иначе attacker может перебирать емейлы через различие ответов 200/422.
- **Rate-limit:** 5 попыток / 15 мин по ключу `auth:forgot:{lower(email)}|{ip}`. На 6-й → 429 + Retry-After. `RateLimiter::hit` ставится ДО `sendResetLink` — даже unknown email увеличивает счётчик (иначе перебор через unknown emails обходит лимит).
- **Laravel `Password::sendResetLink([email])`** под капотом: ищет user → генерирует token (Str::random(60) + hash) → пишет hashed token в `password_resets` → шлёт `Illuminate\Auth\Notifications\ResetPassword`. Если user нет — silently skip.
- **На dev `MAIL_MAILER=log`** — notification пишется в `storage/logs/laravel.log` (письмо не уходит, но логика flow проверяется).
- **Frontend:**
  - `authApi.forgotPassword(email)` — POST /api/auth/forgot, returns {message}.
  - `auth-store::requestPasswordReset(email)` action — загружает `lockoutSeconds` на 429.
  - `ForgotPasswordView` интегрирован: submit → store → `submitted=true` → success-state `v-alert type=success data-testid=forgot-success` скрывает форму, остаётся «Назад ко входу» btn. На 422 → form-error. На 429 → lockout-alert.
- **Pest +6** в `tests/Feature/Auth/ForgotPasswordTest.php` (всего **79/79 за 10.55 сек**, 273 assertions):
  - Existing email → 200 + row в password_resets + `Notification::assertSentTo(user, ResetPassword::class)`.
  - Unknown email → 200 unified-message + БЕЗ row + `Notification::assertNothingSent()`.
  - Валидация формата email → 422.
  - Валидация: пустой → 422.
  - Rate-limit: 5 успешных → 6-я = 429 + Retry-After.
  - Throttle ключ изолирован по email (Alice locked, Bob проходит).
- **Vitest +4** (всего **153/153 за 11.11 сек**):
  - `auth-store.requestPasswordReset` success: возвращает message + НЕ меняет user-state.
  - `auth-store.requestPasswordReset` 429: lockoutSeconds=900.
  - `ForgotPasswordView` success-state: после submit с моком форма скрывается + появляется success-alert.
  - `ForgotPasswordView` lockout-alert: показывается при `auth.lockoutSeconds=600` с «10 мин».
- **PHPStan baseline регенерирован** — +14 ignored Pest TestCall warnings (накопительно).
- **Регресс зелёный:**
  - `composer pint` + `composer stan` — passed.
  - `composer test` — **Pest 79/79 за 10.55 сек** (+6 от 73, 273 assertions).
  - `npm run test:vue` — **Vitest 153/153 за 11.11 сек** (+4 от 149).
  - `npm run lint:vue` + `type-check` + `format:check` — passed.
  - `npm run build` — vite OK 862 ms.
  - `npm run story:build` — **21 stories / 28 variants за 32 сек**.
- **Что НЕ сделано (отдельные коммиты, по списку из v1.45):**
  - **POST /api/auth/reset-password** + UI-форма для new_password (deep-link `/reset/{token}?email=`).
  - **IP-lockout 10 неудач/час** (ТЗ §22.4.4 п.2) — требует таблицы `auth_log`.
  - **Email-уведомление при 3 неудачах** login (ТЗ §22.4.4 п.3) — требует MailService на проде.
  - **RecoveryCodesView** интеграция (POST /api/auth/2fa/recovery-use).
  - **Yandex 360 SSO** (зависит от Б-1 + DO-2).
  - **2FA setup wizard** — первое включение через QR + verify.
  - **Pest browser-mode тесты** для full session-flow.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.45 относительно v1.44:**

- **Rate-limit на /api/auth/login** (ТЗ §22.4.4: 5 неудачных попыток входа на email → блок 15 мин). Через `Illuminate\Support\Facades\RateLimiter`. Ключ `auth:login:{lower(email)}|{ip}` — защищает email от брутфорса даже за NAT'ом, и IP от перебора емейлов.
- **Rate-limit на /api/auth/2fa/verify** — ключ `auth:2fa:{pending_user_id}|{ip}`, та же логика 5/15.
- **`AuthController::lockoutResponse()`** private helper: возвращает 429 + JSON `{message: "Слишком много попыток. Попробуйте через N сек.", retry_after: N}` + HTTP header `Retry-After: N`. Frontend читает retry_after сначала из JSON, потом из header.
- **На успехе** email+пароля `RateLimiter::clear` сбрасывает счётчик (2FA-фаза не зависит от login-fails). На успехе TOTP — то же.
- **Inactive user** (`is_active=false`) тоже расходует попытки — иначе блок аккаунта обходится через перебор password'ов.
- **`extractRateLimitRetry`** в `api/client.ts` — новый helper для парсинга 429: сначала из `response.data.retry_after`, затем из header.
- **Frontend `auth-store::lockoutSeconds`** ref<number|null>: ставится в catch-блоке login()/verifyTwoFactor() при 429, сбрасывается на successful login.
- **LoginView/TwoFactorView** показывают `v-alert type=error` с `data-testid="lockout-alert"`: «Слишком много попыток. Попробуйте через {минут} мин.».
- **Pest +6** в `tests/Feature/Auth/RateLimitTest.php` (всего **73/73 за 8.07 сек**, 246 assertions):
  - Login: 5 wrong → 6-я с правильным = 429 + Retry-After header.
  - Login success чистит throttle (5 новых wrong снова возможны до 429).
  - Throttle key изолирован по email (Alice locked, Bob входит).
  - Inactive user расходует попытки.
  - 2FA verify: 5 wrong → 6-я с правильным TOTP = 429.
  - 2FA verify success чистит throttle.
- **Vitest +4** (всего **149/149 за 12.31 сек**): auth-store login 429 → lockoutSeconds=600 + reject; verifyTwoFactor 429 → lockoutSeconds=900; successful login сбрасывает lockoutSeconds; LoginView показывает lockout-alert «10 мин» после `auth.lockoutSeconds=600`.
- **Quirk фиксации:** wrong-password в Pest-тестах должен быть ≥8 символов (`LoginRequest::min:8`). Иначе ValidationException срабатывает **до** controller, `RateLimiter::hit` не вызывается. Скорректировано на `'wrong-pass-attempt'`.
- **vi.mock api/client** в auth-store.spec.ts — необходим, иначе `axios.isAxiosError(plain Error)` в jsdom возвращает false для unwrapped reject'ов и `extractRateLimitRetry` не отрабатывает на mock'ах.
- **PHPStan baseline регенерирован** — +26 ignored Pest TestCall warnings (накопительно).
- **Регресс зелёный:**
  - `composer pint` + `composer stan` (level 5 + checkModelProperties) — passed.
  - `composer test` — **Pest 73/73 за 8.07 сек** (+6 от 67, 246 assertions).
  - `npm run test:vue` — **Vitest 149/149 за 12.31 сек** (+4 от 145).
  - `npm run lint:vue` + `type-check` + `format:check` — passed.
  - `npm run build` — vite OK 886 ms.
  - `npm run story:build` — **21 stories / 28 variants за 37.19 сек**.
- **Что НЕ сделано (отдельные коммиты, по списку из v1.44):**
  - **IP-lockout 10 неудач/час** (ТЗ §22.4.4 п.2) — требует таблицы `auth_log` (есть в schema v8.7 §10) + IP-counter через `RateLimiter` или query.
  - **Email-уведомление при 3 неудачах** (ТЗ §22.4.4 п.3) — требует MailService (Unisender Go).
  - **ForgotPasswordView** интеграция (POST /api/auth/forgot).
  - **RecoveryCodesView** интеграция (POST /api/auth/2fa/recovery-use).
  - **Yandex 360 SSO** (зависит от Б-1 + DO-2).
  - **2FA setup wizard** — первое включение через QR + verify.
  - **Pest browser-mode тесты** для full session-flow.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.44 относительно v1.43:**

- **AppLayout user-chip** теперь читает реального user'а из Pinia auth-store вместо хардкода. `userInitials` computed: `first_name[0] + last_name[0]` → uppercase; fallback на 2 буквы email; '?' если null. `userShortName`: `«${first_name} ${last_name[0]}.»` → fallback на first_name → fallback на email; 'Гость' если null.
- **AdminLayout user-chip** — тот же паттерн с админскими defaults: 'АО' / 'Админ Оператор' (когда auth.user=null до полного 2FA-verify).
- **v-menu offset=8** обёрнут вокруг user-chip btn в обоих layout'ах. Activator-slot пробрасывает props через v-bind. Содержимое: email disabled + divider + Настройки (RouterLink на /settings, AppLayout) или «Выйти из админки» (RouterLink на /dashboard, AdminLayout) + «Выйти» с mdi-logout-icon → `handleLogout()`.
- **`handleLogout()`** async: `await auth.logout()` (swallows API-errors per v1.42 design) → `await router.push('/login')`. Работает identically даже если backend logout-endpoint упадёт — UI всегда выходит локально.
- **Vitest +3** (всего **145/145 за 11.01 сек**, +3 от 142):
  - `mountAppLayout` сигнатура изменена: `(path = '/dashboard', user: AuthUser | null = mockUser)` → `setActivePinia(createPinia())` + `auth.user = user`.
  - «user-chip показывает initials и shortName» (ИП + Иван П. для Иван Петров).
  - «при null user (гость) показывает ? и Гость».
  - «при отсутствии first_name fallback на email».
  - `AppShell.spec.ts` получил `createPinia()` в plugins (требуется для AppLayout).
- **Регресс зелёный:**
  - `npm run lint:vue` + `type-check` + `format:check` — passed.
  - `npm run test:vue` — **Vitest 145/145 за 11.01 сек**.
  - `npm run build` — vite OK 855 ms.
  - `npm run story:build` — **21 stories / 28 variants за 32.11 сек**.
  - `composer test` — **Pest 67/67 за 6.16 сек** (backend без изменений).
- **Что НЕ сделано (отдельные коммиты, по списку из v1.43):**
  - **Rate-limiting** verify (5 попыток / 15 мин по ТЗ §1.5) + блок аккаунта + email уведомление.
  - **ForgotPasswordView** интеграция (POST /api/auth/forgot).
  - **RecoveryCodesView** интеграция (POST /api/auth/2fa/recovery-use).
  - **Yandex 360 SSO** OAuth-callback (зависит от Б-1 + DO-2).
  - **2FA setup wizard** — первое включение через QR + verify.
  - **Pest browser-mode тесты** для full session-flow.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.43 относительно v1.42:**

- **2FA TOTP-verify endpoint** — закрыт второй пункт «дальше» из v1.42. Полный auth-flow с 2FA работает end-to-end.
- **Установлен `pragmarx/google2fa:^9.0`** для TOTP-генерации/проверки (RFC 6238). По ТЗ §1.6 / Прил. Г.4.2.
- **`AuthController::login`** изменён: при `totp_enabled=true` НЕ делает `Auth::login`, сохраняет `auth.pending_user_id` + `auth.pending_remember` в session, возвращает `requires_2fa: true` без полноценной session-auth.
- **`AuthController::verifyTwoFactor(VerifyTwoFactorRequest)`:** читает pending_user_id из session → проверяет TOTP через `Google2FA::verifyKey($secret, $code, window: 1)` (окно ±1 = 30 сек до/после) → при success делает `Auth::login + regenerate session + clear pending + last_login_at`.
- **`VerifyTwoFactorRequest`** валидирует ровно 6 цифр через regex `/^\d{6}$/`. Маршрут `/api/auth/2fa/verify` публичный (нет полноценной session-auth до verify).
- **Frontend auth-store::login изменён**: при `requires_2fa=true` user остаётся null (иначе isAuthenticated=true и auth-guard пустит на /dashboard минуя 2FA). `verifyTwoFactor(code)` action вызывает API + ставит user после успеха.
- **TwoFactorView интегрирован:** `onMounted` redirect на /login если нет pending state; submit → `auth.verifyTwoFactor` → /dashboard; на error — clear code + focus first cell для повторного ввода. userEmail из `auth.user?.email`.
- **Pest +6** в `tests/Feature/Auth/TwoFactorTest.php` (всего **67/67 за 6.97 сек**, 194 assertions): login для 2FA-user НЕ создаёт session (/me=401) + verify с правильным TOTP завершает login + неверный код 422 + verify без login 422 + валидация формата + после verify /me=200. Tests генерируют валидный TOTP через `getCurrentOtp($secret)`.
- **Vitest +3** в auth-store: login с requires_2fa разделён на 2 теста (pending vs success); verifyTwoFactor success/reject. TwoFactorView spec получил `setActivePinia` + `auth.requires2fa = true` для bypass onMounted-redirect.
- **PHPStan baseline регенерирован** — +26 ignored Pest TestCall warnings (накопительно).
- **Регресс зелёный:**
  - `composer pint` — passed; `composer stan` — passed.
  - `composer test` — **Pest 67/67 за 6.97 сек** (194 assertions).
  - `npm run test:vue` — **Vitest 142/142 за 10.75 сек**.
  - `npm run lint:vue` + `type-check` + `format:check` — passed.
  - `npm run build` — vite OK 908 ms.
  - `npm run story:build` — **21 stories / 28 variants за 31.28 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Recovery codes** (POST /api/auth/2fa/recovery-use) + RecoveryCodesView интеграция.
  - **2FA setup wizard** — первое включение через QR + verify.
  - **Rate-limiting** verify (5 попыток / 15 мин по ТЗ §1.5) + блок аккаунта.
  - **AppLayout user-chip** реальный user + Logout в menu.
  - **ForgotPasswordView** интеграция.
  - **Yandex 360 SSO**.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.42 относительно v1.41:** Frontend auth integration (axios + Pinia + auth-store + auth-guards + LoginView/RegisterView submit). Pest 61/61 + Vitest 139/139.

**Что изменилось в v1.42 относительно v1.41:**

- **Frontend auth-flow полностью интегрирован.** UI-формы login/register больше не висят на no-op handler'ах — реальный POST через axios, состояние в Pinia, redirect после успеха, validation errors из 422.
- **Установлены `axios@^1.16.0` + `pinia@^3.0.4`** (через `--legacy-peer-deps` — Histoire 1.0-beta.1 vs Vite 8 peerDep).
- **`resources/js/api/client.ts`:** axios-инстанс с `withCredentials: true` + `withXSRFToken: true` (Sanctum SPA: XSRF-TOKEN cookie auto). `ensureCsrfCookie()` забирает CSRF cookie через `GET /sanctum/csrf-cookie` один раз. Helpers `extractValidationErrors` (422) + `extractErrorMessage` (general).
- **`resources/js/api/auth.ts`** — типизированные API-методы login/register/me/logout с `AuthUser` interface.
- **`resources/js/stores/auth.ts`** — Pinia composition-store: state (user/loading/requires2fa) + computed isAuthenticated + actions login/register/fetchMe/logout. logout() catch-swallow для UI-localout даже при backend-failure.
- **LoginView/RegisterView** интегрированы через `useAuthStore`: submit делает реальный POST, errors из validation, redirect на /dashboard или /2fa, :loading на btn'ах.
- **Auth-guard в router** через `router.beforeEach`:
  - `meta.requiresAuth=true` на 6 app-views + 4 admin-views.
  - `meta.guestOnly=true` на login/register/forgot.
  - При первом переходе вызывается `fetchMe()` для restore-session.
  - Redirect-after-login: при unauth-доступе сохраняется `?redirect={original}` query.
- **`/` redirect** изменён с `/login` на `/dashboard` (auth-guard сам перенаправит на /login если не залогинен).
- **Pinia зарегистрирован в `app.ts`** через `app.use(createPinia())`.
- **Vitest +10** (всего **139/139 за 10.11 сек**):
  - `auth-store.spec.ts` (7): initial state + login success/reject + register + fetchMe success/401 + logout-swallow.
  - `router.spec.ts` переписан (5): login.guestOnly + 6 protected routes + admin layout + 3 error routes + unauth-доступ /dashboard → /login с `?redirect`.
  - `vi.mock('../../resources/js/api/auth')` в router/auth-store specs — мокаем API чтобы избежать HTTP-запросов в Vitest.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **139/139 за 10.11 сек**.
  - `npm run build` — main app-chunk **153.64 KB** (с 105 KB — +axios+pinia+auth; gzipped 54.54 KB). 806 ms.
  - `npm run story:build` — **21 stories / 28 variants за 31.73 сек**.
  - `composer test` — **Pest 61/61 за 5.86 сек** (backend без изменений).
- **Что НЕ сделано (отдельные коммиты):**
  - **2FA TOTP-verify endpoint** + интеграция в LoginView/TwoFactorView.
  - **ForgotPasswordView** интеграция (POST /api/auth/forgot).
  - **RecoveryCodesView** интеграция.
  - **Yandex 360 SSO** OAuth-callback + кнопка в LoginView.
  - **Rate-limiting** (5 попыток / 15 мин по ТЗ §1.5) + UI-сообщение.
  - **AppLayout user-chip** показывает реального user'а из store + Logout-action в menu.
  - **Pest browser-mode тесты** для full session-flow.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.41 относительно v1.40:** **Backend auth через Sanctum SPA mode**: AuthController + 13 Pest-тестов. **Pest 61/61 + Vitest 129/129 + Histoire 21/28 зелёные**.

**Что изменилось в v1.41 относительно v1.40:**

- **Backend authentication через Laravel Sanctum SPA mode** — первый коммит после полного покрытия фронта по handoff. UI-формы login/register пока на no-op handler'ах; интеграция в следующем коммите.
- **Установлен `laravel/sanctum:^4.3`**. Опубликована миграция `personal_access_tokens` — **удалена** (для SPA-mode не нужна, используется session-cookie). Опубликован `config/sanctum.php`.
- **SPA mode через session-cookie**, не через bearer-tokens. Frontend (Vue) получает XSRF-TOKEN cookie через `GET /sanctum/csrf-cookie`, отправляет в `X-XSRF-TOKEN` header.
- **`AuthController` (`app/Http/Controllers/Api/AuthController.php`):** login + register + me + logout с подробной валидацией + кастомные русские error-messages.
- **`LoginRequest`** + **`RegisterRequest`** Form Requests. Register требует **`accept_offer:accepted` + `accept_pdn:accepted`** (по ТЗ §1.5/§4.1, БЕЗ маркетингового click-wrap'а — расхождение #2 handoff vs ТЗ из реестра v1.13).
- **`User` model fillable** расширен: `last_login_at`, `last_active_at`.
- **Auth-маршруты в `web.php`** (НЕ в api.php): Sanctum SPA нуждается в session-cookie middleware, который добавляется только к web-группе по умолчанию. См. <https://laravel.com/docs/sanctum#spa-authentication>. `bootstrap/app.php` без api.php — будет добавлен если понадобится token-based mode.
- **Pest +13 тестов** в `tests/Feature/Auth/AuthControllerTest.php` (всего **61/61 за 6.22 сек**, 170 assertions): login success + 2FA-flag + неверный пароль + несуществующий email + заблокированный аккаунт + валидация format/min + last_login_at update + register success/duplicate/без accept_offer/без accept_pdn + me 401/200 + logout 200.
- **Quirk: Pest cookie-jar держит session между запросами теста.** Full session-invalidate проверка (login → me → logout → me=401) не работает — это известная проблема. Logout-test упрощён до 200+flash-message; full flow тестируется через Pest browser-mode (Pest 4 включает без Dusk — отдельный коммит).
- **PHPStan baseline регенерирован** — +25 ignored Pest `$this` warnings (Larastan не понимает `$this` в Pest closure'ах — известная Larastan+Pest static-analysis проблема, паттерн уже использовался для других тестов).
- **Регресс зелёный:**
  - `composer pint` — passed; `composer stan` — passed (level 5 + checkModelProperties).
  - `composer test` — **Pest 61/61 за 6.22 сек**.
  - `npm run test:vue` — **Vitest 129/129 за 9.59 сек** (frontend без изменений).
  - `npm run build` — vite 802 ms.
  - `npm run story:build` — **21 stories / 28 variants за 30.39 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Frontend интеграция:** axios + XSRF-interceptor + Pinia user-store + LoginView/RegisterView отправляют POST вместо no-op.
  - **2FA TOTP-verify endpoint** + LoginView redirects на /2fa если requires_2fa=true.
  - **Rate-limiting** (5 попыток / 15 мин по ТЗ §1.5) + failed_login_attempts счётчик в users.
  - **Yandex 360 SSO** OAuth-callback (зависит от Б-1 + DO-2 для домена).
  - **Password reset flow** (POST /password/forgot + email + reset-form-token).
  - **Auth-guards в Vue Router** (redirect на /login если не залогинен).
  - **Pest browser-mode тесты** для full session-flow.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.40 относительно v1.39:**

- **Реализована админка SaaS** — последний из 12 рабочих концептов handoff (landing v8_landing.html отложен через ⏸ Б-1 для продакшена). По `liderra_v8_handoff/concepts/v8_admin.html` + ТЗ §22 (админка SaaS) + schema v8.7 §3 (tenants) / §9 (incidents_log) / §10 (saas_admin_audit_log + system_settings).
- **`AdminLayout.vue`** (отдельный layout, не AppLayout):
  - **Sidebar:** brand-block «Лидерра.» + brand-sub «ADMIN» (red `#B94837` 10px JBM uppercase letter-spacing 0.16em — визуально отделяет от обычного клиентского интерфейса).
  - **Nav-tree** 4 пункта: Тенанты 142 / Биллинг / Инциденты 3 / Система. Active-state через `route.path.startsWith(item.to)` — это позволяет sub-routes как /admin/tenants/{code} оставаться active.
  - **Topbar:** crumb «Админка → currentPage» (computed по navItems) + admin-user-chip (АО, Админ Оператор) с error-color avatar (визуальный индикатор повышенных привилегий).
- **AppShell** расширен: `meta.layout='admin'` → `<AdminLayout v-else-if="layoutName === 'admin'" />`.
- **`AdminTenantsView.vue`** (`/admin/tenants`):
  - **page-head:** заголовок «Тенанты» + 5-stats line («142 всего · 128 активны · 9 trial · 5 просрочка · выручка месяц 1 248 600 ₽» с tnum-числами и color-accents: success для active, warning для overdue, primary для revenue) + Экспорт-btn.
  - **filter-bar:** search-input («ИНН, юр. лицо, email админа…») + Статус/Тариф `<v-btn append-icon="mdi-chevron-down">` (TODO: реальные фильтры).
  - **`v-data-table`** 7 колонок: Тенант (двухстрочный — name + ИНН caption) / Статус (tonal-chip с цветом по statusColor: success/info/warning/error) / Тариф / Баланс ₽ (JBM tnum с `text-error` при <0 и `text-medium-emphasis` при 0) / Желаем×факт сегодня (`{desired} × {actual}`) / MRR (число или «—» для trial/suspended) / Активность.
- **`composables/mockTenants.ts`:**
  - `AdminTenant` interface с 4 статусами `active`/`trial`/`overdue`/`suspended` + 5 тарифами `Trial`/`Start`/`Команда`/`Pro`/`Enterprise`.
  - `MOCK_TENANTS` — 7 разнообразных tenants:
    - Окна Москва (active, Команда, +14250 ₽, 12×11)
    - Натяжные потолки СПб (active, Команда, +38100 ₽, 8×14)
    - Кухни Екб (active, Pro, +112800 ₽, 5×6, 2990 MRR)
    - Ремонт под ключ (trial 4 дня, +450 ₽, MRR=null)
    - Двери Премиум (overdue 3 дня, **−1200 ₽**, low-color)
    - Оконные системы РФ (suspended, 0 ₽, MRR=null)
    - Кухонная мебель Спб (active, **Enterprise**, +486200 ₽, 28×31, 9990 MRR).
  - `MOCK_STATS` — 142 total / 128 active / 9 trial / 5 overdue / 1 248 600 ₽ monthly revenue.
- **Search-фильтр** через computed `filteredTenants`: case-insensitive matching по name + inn + code.
- **`AdminPlaceholderView.vue`** — универсальный placeholder для трёх admin-разделов (Биллинг/Инциденты/Система). Конфиг через `route.meta.title` + `route.meta.description`. Каждый описание ссылается на конкретные schema-таблицы (incidents_log §9 / system_settings §10).
- **Router:** `/admin` redirect → `/admin/tenants`. 4 admin-route'а с `meta.layout='admin'` (lazy-imports). web.php: НЕ добавляли явные Route::view для admin/* — `Route::fallback` (введённый в v1.39) их покрывает автоматически.
- **Vitest +11 тестов** (всего **129/129 за 10.02 сек**):
  - `AdminTenantsView.spec.ts`: заголовок + 5 page-stats (с regex для nbsp в «1 248 600 ₽») + 7 columns + 7 mock-rows + Окна Москва с ИНН 7724444444 + overdue Двери Премиум с «−1 200» + trial Ремонт под ключ + suspended Оконные системы РФ + search-placeholder содержит «ИНН» + фильтр «Натяжные» оставляет 1 строку + Экспорт/Статус/Тариф кнопки.
- **Stories +2:** `AdminLayout.story.vue` (sidebar + topbar) + `AdminTenantsView.story.vue` (полный экран).
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **129/129 за 10.02 сек**.
  - `npm run build` — admin views в lazy-chunks (AdminLayout импортирован в main app-chunk 104.99 KB); 763 ms.
  - `npm run story:build` — **21 story / 28 variants за 30.32 сек**.
  - `composer test` — **Pest 48/48 за 4.89 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Router-guard на /admin/*** — должен проверять `super_admin` role + 2FA recently-verified (по OPEN-И-13 break-glass).
  - **Карточка тенанта** /admin/tenants/{code} — Edit / Корректировать баланс / **Войти как клиент** (Ю-1 impersonation: одноразовый код email, 15 мин, 5 попыток, 1 час сессия) / Удалить (152-ФЗ).
  - **Impersonation-banner** при активной impersonation-сессии — обязательное предупреждение в шапке (по ТЗ §22).
  - **Admin-биллинг** (сводный по всем тенантам) / **Инциденты-журнал** / **Системные настройки**.
  - **`saas_admin_audit_log`** запись для каждого admin-action'а (по schema v8.7 §10).
  - **Backend** для всех admin-endpoints с проверкой `super_admin` через middleware.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2). 12 из 13 концептов handoff покрыты UI-разводкой.

**Что изменилось в v1.39 относительно v1.38:**

- **Реализован ErrorView** — универсальный экран ошибки 404/403/500. По `liderra_v8_handoff/concepts/v8_errors.html`. Двенадцатый экран фронта.
- **Архитектура:** один `resources/js/views/errors/ErrorView.vue` с конфигурацией через `route.meta.errorCode` ('404'|'403'|'500'). 3 маршрута используют один компонент. Альтернатива (3 отдельных view'а) отброшена — слишком много дублирования CSS.
- **Layout:** новый тип `meta.layout='error'`. AppShell расширен: при error-layout рендерит `<RouterView />` напрямую без AppLayout/AuthLayout. ErrorView сам предоставляет `v-app + v-main` с full-bleed теало-нуар bg `#012019` (брендовый dark-mode для error-pages — отдельно от обычных layout'ов).
- **UI:**
  - **top-brand** (Лидерра. с SVG L-Square) в padding 24px/32px, белый текст с teal-точкой.
  - **err-code** — 96px JBM monospace-цифра, средний символ выделен `--accent` teal `#32C8A9` (по handoff: «4**0**4», «4**0**3», «5**0**0»).
  - **err-title** h2 28px Inter opsz=28.
  - **err-desc** body-text grey #B1C2BD.
  - **err-actions** — 2 v-btn (primary + outlined): 404 «На дашборд (mdi-view-dashboard) + Назад (mdi-arrow-left, router.back)», 403 «На дашборд + Написать в поддержку (mailto:support@liderra.app)», 500 «Попробовать снова (location.reload) + Статус сервиса (https://status.liderra.app)».
  - **status-list** (только 500): 3 pills с цветным dot — API · OK / Telegram · деградация / YooKassa · OK. JBM-monospace text.
  - **err-id** (только 403/500): pill-rounded с label («Запрос» / «Инцидент») + JBM-tnum ID (REQ-3F8A2-0007 / INC-2026-0507-0034) + copy-btn через `navigator.clipboard.writeText`.
  - **err-help** (только 404): `mailto:support@liderra.app` link.
- **AppShell:**
  - `meta.layout = 'error'` → `<RouterView v-else-if="layoutName === 'error'" />` (рендерит ErrorView без обёртки). Auth и app остаются как раньше.
- **Router:**
  - `/403` → ErrorView с `meta.errorCode='403'`.
  - `/500` → ErrorView с `meta.errorCode='500'`.
  - **catch-all `/:pathMatch(.*)*`** → ErrorView с `meta.errorCode='404'` (всегда последний — Vue Router matches in order).
- **`web.php`:**
  - `Route::view('/403', 'welcome')` + `Route::view('/500', 'welcome')` явные.
  - **`Route::fallback(fn () => view('welcome'))`** — Laravel `fallback`-route срабатывает ПОСЛЕ всех остальных + runtime-route'ов от Pest `beforeEach` (которые регистрируются в момент теста, до запроса). Это гарантирует что `/_test/tenant-context` из SetTenantContextTest всё ещё резолвится правильно (Pest-роуты совпадают раньше fallback).
- **Vitest +8** (всего **118/118 за 9.39 сек**):
  - `ErrorView.spec.ts`: default 404 («Страница не найдена» + всех экранов через дашборд) + 403 с REQ-3F8A2-0007 + 500 с INC-2026-0507-0034 + status-list (API · OK / Telegram · деградация) + 404 «На дашборд + Назад» + 403 «На дашборд + Написать в поддержку» с mailto-link + 500 «Попробовать снова + Статус сервиса» с external link + брендовый блок «Лидерра.» в шапке + 404 НЕ содержит REQ/INC/status-list (regression-guard).
  - **stubs:** `VApp` + `VMain` как passthrough `<div><slot /></div>` — обходим Vuetify layout-injection в jsdom. Тесты проверяют DOM-структуру и текст.
- **Story `ErrorView.story.vue`** 1 variant (404 default — без router-context используется fallback в computed).
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **118/118 за 9.39 сек**.
  - `npm run build` — ErrorView в lazy-chunk (3 route'а ссылаются на один chunk). Main app-chunk **101.01 KB упал на 7 KB** благодаря shared chunk'ам и лучшему code-splitting'у. 771 ms.
  - `npm run story:build` — **19 stories / 26 variants за 30.96 сек**.
  - `composer test` — **Pest 48/48 за 4.88 сек** (Laravel `Route::fallback` не сломал runtime-routes от Pest).
- **Что НЕ сделано (отдельные коммиты):**
  - **Sentry capture при mount 500** — `Sentry.captureException(new Error('Server error rendered'))` для метрики error-page показов.
  - **Real requestId/incidentId** — взять из `<meta name="x-request-id">` или из cookie / response-header при backend-генерации error-page.
  - **Status-list из API** `/api/health` — динамический.
  - **400/401/422 страницы** — отдельный коммит при необходимости (обычно 401 = redirect на /login через router-guard).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.38 относительно v1.37:**

- **Реализован ReportsView** (`/reports`, meta.layout='app') — 11-й экран фронта. По `liderra_v8_handoff/concepts/v8_reports.html` + ТЗ §6.6 + CTO-6 (retry-failed 3 попытки / 7 дней / только owner) + CTO-7 (квота 3 одновременных отчёта на тенанта).
- **Структура `resources/js/views/ReportsView.vue`:**
  - **page-head:** заголовок «Отчёты» + page-stats «очередь 2/3 · обработано за месяц 38 · средний размер 2.4 MB» (JBM tnum).
  - **form-card «Запросить отчёт»:**
    - **Тип отчёта** (4 v-card radio-grid): Сделки · детально (default active) / Менеджеры (конверсия + средний чек + время в воронке) / Источники / Биллинг. Active-state: primary-border + ivory-tint bg.
    - **Period с/по** (2 v-text-field type=date с JBM-tnum через :deep(input) override).
    - **Проект / Менеджер** (2 v-select с mock-options).
    - **Формат файла** (4 v-btn toggle: CSV/XLSX/JSON/PDF — flat-primary для активного, outlined для остальных).
    - **quota-banner** v-alert info с CTO-6/CTO-7 значениями: «Квота: 2 из 3 одновременных отчётов · до 3 попыток retry в окне 7 дней».
    - **Запустить + Сброс** btn'ы.
  - **jobs-list panel:** panel-h «Сгенерированные отчёты» + «retry-failed для owner отчёта, до 3 попыток · 7 дней» + «все 38 →». 5 job-rows в grid-layout (auto 1fr auto auto):
    - **icon** (24px mdi-check-circle/progress-clock/clock/alert-circle цвет по статусу).
    - **info:** title (font-weight 500) + meta JBM tnum («FORMAT · size · rows · timeText»; для failed добавляется «N/3 попытки · ошибка X»).
    - **v-progress-linear** для running с progress=62.
    - **status-chip** v-chip tonal с цветом по статусу.
    - **actions:** Скачать (done) / Повторить (failed && attempt<3) / Отменить (queued) / Удалить (done|failed).
- **`resources/js/composables/mockReports.ts`:**
  - Type unions: `ReportType` (deals/managers/sources/billing), `ReportFormat` (csv/xlsx/json/pdf), `ReportStatus` (queued/running/done/failed).
  - `REPORT_TYPES` + `REPORT_FORMATS` массивы для UI-iteration.
  - `ReportJob` interface (id, title, format, status, sizeText/rowsText/timeText/progress/attempt/error).
  - `MOCK_JOBS` — 5 jobs с разными состояниями: 1 done «Сделки апрель» CSV / 1 running «Менеджеры апрель» XLSX 62% / 1 queued «Источники 30 дней» JSON / 1 failed «Биллинг март» XLSX (S3 timeout, 2/3 попытки) / 1 done «Менеджеры март» XLSX (6 дней назад).
  - `MOCK_QUOTA` со значениями activeJobs:2 / maxActiveJobs:3 / retryAttemptsMax:3 / retryWindowDays:7 — соответствуют CTO-6/CTO-7.
- **Маршрут `/reports`** (name='reports', meta.layout='app', lazy-import) в router и web.php (`Route::view`).
- **Vitest +12 тестов** (всего **110/110 за 9.38 сек**):
  - `ReportsView.spec.ts`: заголовок + page-stats (очередь/обработано/средний размер) + 4 type-cards + дефолт «Сделки» active-class + 4 формата + quota-banner («2 из 3» + «3 попыток retry» + «7 дней») + 5 job-rows + done «Готов» + Скачать aria-label + running «В работе · 62%» + progressbar role + queued «В очереди» + Отменить aria + failed «Ошибка» + «S3 timeout» + Повторить aria + клик меняет active-карточку.
- **Story `ReportsView.story.vue`** 1 variant.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed (prettier пере-отформатировал 2 файла).
  - `npm run test:vue` — **110/110 за 9.38 сек**.
  - `npm run build` — ReportsView в lazy-chunk; main app-chunk 108.19 KB. 706 ms.
  - `npm run story:build` — **18 stories / 25 variants за 30.77 сек**.
  - `composer test` — **Pest 48/48 за 4.58 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Backend:** POST /api/reports + GET /api/reports + GET /api/reports/{id}/file (S3 presigned) + POST /api/reports/{id}/retry с проверкой owner+attempt+age.
  - **Realtime-обновление progress** через WebSocket (Echo broadcasting `report.progress` event).
  - **Динамические Project/Manager dropdown'ы** из API.
  - **PDF-preview** перед скачиванием.
  - **«Все 38 →»** click navigation на полный архив (отдельный экран).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.37 относительно v1.36:**

- **Реализован SettingsView** (`/settings`, meta.layout='app') — 10-й экран фронта. По `liderra_v8_handoff/concepts/v8_settings.html`.
- **Layout:** sidebar tabs-rail (md=3 v-list nav с mdi-icon на каждом пункте) + content-pane (md=9 v-card outlined, min-height 480px). `activeTab` ref переключает рендер вкладки через v-if/v-else-if.
- **Реализованы 4 вкладки:**
  - **`ProfileTab.vue`** — v-avatar 80px primary («ИП») + Сменить-btn camera-icon. 5 v-text-field в 2-col grid: Полное имя, Email (disabled + hint про support — связан с авторизацией), Телефон, Тайм-зона (Europe/Moscow + hint про логи и напоминания), Роль (disabled — Владелец). Сохранить/Отмена btn'ы.
  - **`SecurityTab.vue`** — 3 outlined cards:
    - **Пароль:** «Последняя смена 12.04.2026 (26 дней назад)» + Сменить-btn.
    - **2FA:** success-chip «включена» (если активна) + описание (TOTP Google Authenticator/Yandex Key + 8 recovery-codes по ТЗ §1.6/Прил. Г.4.2) + Перегенерировать резервные коды + Отключить 2FA (error-color).
    - **Активные сессии:** 3 mock-сессии (Chrome Windows / Safari iPhone 16 / Firefox Linux) с device, location, when. Текущая помечена «эта сессия» chip; не-current имеют Завершить-btn.
  - **`ApiTab.vue`** — 2 outlined cards:
    - **API-ключ:** password-field с eye-toggle (`tokenVisible` ref) + Копировать-btn + Перегенерировать (warning-color) + описание про немедленный invalidation старого ключа.
    - **Webhook:** Endpoint URL field + Signing secret HMAC password-field с eye-toggle + Сохранить/Тестовый webhook. Текст явно ссылается на schema v8.7: «Дедуп по `(tenant_id, source_crm_id)` в окне 24 ч (антифрод по phone — §10.8.1)».
  - **`NotificationsTab.vue`** — **матрица 8×3** соответствует `users.notification_preferences` JSONB по schema v8.7 §4 (см. также CLAUDE.md §2 user notification matrix):
    - **8 типов событий:** new_lead / duplicate_detected / low_balance (<3 дней) / tariff_charge / reminder_due / manager_assigned / webhook_failed (>3 ретраев) / monthly_report.
    - **3 канала:** email / sms / in_app.
    - reactive `prefs` Record с дефолтами: low_balance везде включён (рабочий + критичный), monthly_report только email, остальные на в-приложении + email где смыслово.
    - **CSS-grid** 1fr 110px 110px 130px для `prefs-table`: head + 8 rows; ru-styled head с JBM-uppercase eyebrow.
    - **`sound_enabled`** v-switch отдельно — соответствует BOOLEAN-полю в schema.
- **Placeholder для 4 вкладок:** `PlaceholderTab.vue` — универсальный с props title/description + v-alert «В разработке». Используется для **Проекты** (макс. 10 на тарифе «Команда»), **Команда** (макс. 4 менеджера + расширение, автораспределение), **Интеграции** (Telegram-бот, экспорт в 1С 8.3, JivoSite, Yandex 360 SSO), **Тихие часы** (например, 22:00-08:00 + выходные).
- **Маршрут `/settings`** (name='settings', meta.layout='app', lazy-import) в router и web.php.
- **Vitest +8 тестов** (всего **98/98 за 8.42 сек**):
  - `SettingsView.spec.ts`: монтаж + ровно 8 nav-tabs + все 8 названий + дефолт «Профиль» (Полное имя/Тайм-зона) + клик «Проекты» → «В разработке» + клик «Уведомления» → «События × каналы» + 5 событий матрицы (Новый лид/Дубликат/Низкий баланс/Срок напоминания/Webhook упал) + клик «Безопасность» → 2FA + Активные сессии + клик «API» → API-ключ + Signing secret HMAC.
- **Story `SettingsView.story.vue`** 1 variant.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **98/98 за 8.42 сек**.
  - `npm run build` — SettingsView в lazy-chunk; main app-chunk 107.85 KB. 750 ms.
  - `npm run story:build` — **17 stories / 24 variants за 31.7 сек**.
  - `composer test` — **Pest 48/48 за 5.03 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **4 placeholder-вкладки**: Проекты/Команда/Интеграции/Тихие часы — отдельные коммиты с full UI.
  - **Save handlers** для всех вкладок (PATCH /api/me, PATCH /api/me/notifications, POST /api/auth/2fa/regenerate-codes, POST /api/api-tokens/regenerate).
  - **2FA setup wizard** при первом включении (QR-code + secret + verify code).
  - **Backend** для всех endpoints.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.36 относительно v1.35:**

- **Реализован BillingView** (`/billing`, meta.layout='app') — финансовый экран по `liderra_v8_handoff/concepts/v8_billing.html`. Девятый экран фронта.
- **Структура `resources/js/views/BillingView.vue`:**
  - **page-head:** заголовок «Биллинг и тарифы» + page-stats («14 250 ₽ кошелёк · 285 лидов запас · хватит на 4 дня» с tnum-числами) + primary-кнопка «Пополнить баланс».
  - **pending-banner** v-alert type=info с детализацией pending-платежа (сумма + метод + время начала + auto-cancel timeout 30 мин). Условный рендер `v-if="MOCK_PENDING"` — на API будет тот же check.
  - **wallet-row** (v-row dense + 3 v-col md=4):
    - **Кошелёк ₽** — primary card теало-нуар с LIVE-chip primary, JBM tnum 32px цифра + ru-suffix, Пополнить/Автопополнение btn-actions, foot-line про мин. пополнение 100 ₽ + округление вниз.
    - **Баланс лидов (ГЦК)** — outlined white card с числом лидов + ру-text «лидов» + foot про среднюю цену 50₽/лид и потрачено за месяц.
    - **Тариф «Команда»** — outlined card с именем + tariff-price JBM teal `· 990 ₽/мес` + 3 фичи в ul (с green-check icons) + Сменить-btn.
  - **transactions panel:** v-btn-toggle (Все/Пополнения/Списания/Возвраты) с computed `filteredTransactions`. v-data-table 5 колонок (Дата JBM, Операция, ID JBM `#TX-N`, Статус-chip с warning/success/error по статусу, Сумма с up/down/neutral классом цвета).
  - **invoices panel:** Реестр-XLSX-btn + ul invoices-list 4 строки (Дата JBM + Title с sub-line + amount JBM + format-btn — PDF (mdi-file-pdf-box) / 1С 8.3 XML (mdi-xml)).
- **`resources/js/composables/mockBilling.ts`** соответствует схеме v8.7:
  - `BillingTransaction` interface: types `topup` / `lead_charge` / `refund` / `tariff_charge`, statuses `pending` / `completed` / `rejected`, signed `amount` (+/− по type).
  - `MOCK_TRANSACTIONS` — 8 транзакций (1 pending pop, 2 completed pop, 2 lead_charge, 1 tariff_charge, 1 rejected pop, 2 refund).
  - `Invoice` interface + `MOCK_INVOICES` (4: 2 PDF tariff-bills + 2 XML 1С 8.3 УПД).
  - `BillingTab` array (4 среза) для transactions-tabs (`Все` → null, `Списания` объединяет lead_charge+tariff_charge).
  - `MOCK_PENDING` — структура pending-платежа.
- **Соответствие схеме v8.7:** balance_transactions (§4.4), invoices (§4.5). При API-интеграции composable заменяется на pinia-store или composable с `GET /api/billing/wallet|transactions|invoices`.
- **Format helpers:** `formatCost(cost)` с знаком `+ N ₽` / `− N ₽` / `— 0 ₽` для rejected; `formatPlain(cost)` без знака; разделители — Intl.NumberFormat ru-RU (nbsp).
- **Маршрут `/billing`** в router (lazy-import) и web.php (`Route::view`).
- **Vitest +11 тестов** (всего **90/90 за 7.96 сек**):
  - `BillingView.spec.ts` (11): заголовок «Биллинг и тарифы» + page-stats (кошелёк regex `/14\s+250\s*₽/`, leads, дни) + pending-banner («1 платёж в обработке» / «5 000 ₽» / «ЮKassa») + 3 wallet-cards (Кошелёк ₽ / LIVE / Баланс лидов / Тариф / Команда / 990₽/мес) + 3 фичи тарифа + 4 tabs (Все/Пополнения/Списания/Возвраты) + дефолт-tab показывает все 8 строк + format `+ 5 000 ₽` / `− 6 600 ₽` через regex с `\s+` (nbsp-нечувствительно) + rejected `— 0 ₽` + invoices section 4 строки + PDF/1С 8.3 XML format-labels.
- **Story `BillingView.story.vue`** 1 variant.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **90/90 за 7.96 сек**.
  - `npm run build` — BillingView в lazy-chunk (Vuetify VDataTable вынесен в общий chunk 79.84 KB — переиспользуется DealsView и BillingView; main app-chunk 107.56 KB). 688 ms.
  - `npm run story:build` — **16 stories / 23 variants за 32.16 сек**.
  - `composer test` — **Pest 48/48 за 4.89 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Auto-topup wizard:** диалог настройки автопополнения через ЮKassa (Биз-* в реестре).
  - **Tariff change wizard** + tariff comparison table (Solo/Команда/Бизнес/Корпоративный).
  - **Refund-request dialog** (заявка на возврат при поступлении spam/duplicate).
  - **Backend `/api/billing/{wallet,transactions,invoices}`** + tenant-RLS isolation.
  - **POST `/api/billing/topup` → ЮKassa-checkout** redirect-flow.
  - **Pagination на transactions** (sticky-footer + per-page select).
  - **Filter-popover** для transactions (date-range + статус + сумма).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.35 относительно v1.34:**

- **Реализован DealDetailDrawer** — правая панель с деталями сделки. По `liderra_v8_handoff/concepts/v8_deal_card.html`. Открывается при click на строку в DealsView или на карточку в KanbanView.
- **Структура `resources/js/components/deals/DealDetailDrawer.vue`:**
  - `v-navigation-drawer` location="right" temporary width=480px, v-model:open для двусторонней привязки.
  - **Hero-секция:** «Сделка #N» eyebrow (JBM uppercase) + h5 имя клиента + close-icon-btn (mdi-close) + phone как `<a href="tel:...">` (с очисткой phone от non-digit для href) + clock-icon с относительным временем + status-chip tonal с colorHex и status-dot.
  - **Section «Параметры»:** dl с 2-column grid: Проект, Стоимость лида (Intl.NumberFormat ru-RU + JBM tnum), Менеджер (с v-avatar 20px), Источник.
  - **Section «Активность»:** ul timeline с 6 событиями. Каждый item: iconified circle (24px, ivory-tint bg, primary-color icon) + vertical line connector (через `::before`) + body (timeline-type uppercase + timeline-time JBM + timeline-detail + timeline-actor с avatar 16px).
- **`resources/js/composables/mockDealEvents.ts`:**
  - `DealEvent` interface с 6 типами событий (соответствуют ActivityLog event-константам по схеме v8.7 §10.2): `deal.created`, `deal.status_changed`, `deal.viewed`, `deal.commented`, `deal.assigned`, `deal.balance_charged`.
  - 6 mock-events для timeline (хронологически: created → balance_charged → assigned → viewed → status_changed → commented; minutesAgo от 28 до 8).
  - `eventTypeLabel(type)` + `eventTypeIcon(type)` — мапы для UI.
  - На API будет `GET /api/deals/{id}/events` — выборка из `activity_log` с tenant-isolation через RLS.
- **Интеграция в DealsView:**
  - Локальные `drawerOpen` ref + `selectedDeal` ref.
  - `@click:row="(_e, { item }) => openDeal(item)"` на v-data-table (типизация через MockDeal type-import).
  - Рендер `<DealDetailDrawer v-model:open="drawerOpen" :deal="selectedDeal" />` после v-card таблицы.
- **Интеграция в KanbanView:**
  - Локальные `drawerOpen` ref + `selectedDeal` ref.
  - `onOpenDeal(id)` ищет deal по id среди всех reactive-массивов `dealsByStatus` (across колонок).
  - Рендер `<DealDetailDrawer v-model:open="drawerOpen" :deal="selectedDeal" />` после kanban-board.
- **Vitest quirk: layout-injection в JSDOM.** DealsView и KanbanView теперь содержат `v-navigation-drawer`, который требует layout-context (provided by `v-app` или `v-layout`). В Vitest:
  - **vite-plugin-vuetify auto-import** не работает — `v-layout`/`v-app` не резолвятся компонент-резолвером Vue ("Failed to resolve component: v-layout").
  - **Wrap в template `<v-app><DealsView /></v-app>`** не помогает — Vuetify внутренний layout-injection не активируется в test-env.
  - **Решение:** stub'ить `DealDetailDrawer` в тестах DealsView/KanbanView через `stubs: { DealDetailDrawer: true }` — drawer заменяется на `<deal-detail-drawer-stub />` без mount его внутренностей. Это правильно: тесты DealsView/KanbanView не должны тестировать drawer (он покрыт отдельно).
  - **Сам Drawer тестируется** в `DealDetailDrawer.spec.ts` со stub'ом `VNavigationDrawer` как passthrough `<div class="drawer-stub" v-if="modelValue"><slot /></div>` — slot-content (hero/params/timeline) рендерится в DOM, доступен для assertion.
- **Vitest +8 тестов** (всего **79/79 за 7.57 сек**):
  - `DealDetailDrawer.spec.ts` (8): монтаж + open=false скрывает контент + deal=null без content + hero (имя+id) + phone tel:link с tel:+ префиксом + status-chip с nameRu + параметры (project/cost regex/manager) + timeline ровно 6 items + emit update:open(false) на close.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **79/79 за 7.57 сек**.
  - `npm run build` — DealDetailDrawer инлайнен в DealsView+KanbanView lazy-chunks (общий код через Vite chunking — drawer импортируется в обе view'ы; main app-chunk 107.16 KB). 761 ms.
  - `npm run story:build` — **15 stories / 22 variants за 31.55 сек**.
  - `composer test` — **Pest 48/48 за 4.99 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Редактирование параметров inline** (Проект/Менеджер/Источник): input-fields + save-trigger + PATCH /api/deals/{id}.
  - **Смена статуса** через dropdown в hero (на Канбане — через DnD).
  - **Tag management** — chips с inline добавление/удаление + PATCH /api/deals/{id}/tags.
  - **Manager reassignment** через dropdown.
  - **Reminder создание** (Биз-* в реестре, требует v-date-picker + v-time-picker).
  - **Comment** + templates (Биз-* — quick-reply шаблоны).
  - **Backend `GET /api/deals/{id}/events`** + tenant-RLS isolation.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.34 относительно v1.33:**

- **Реализован DnD на Канбан-доске** — карточки можно перетаскивать между 14 колонками (статусами). При drop в новую колонку — `statusSlug` сделки автоматически синхронизируется.
- **Установлен `vuedraggable@^4.1.0`** + транзитивно `sortablejs@1.14.0` (обёртка SortableJS для Vue 3, peerDep `vue ^3.0.1`). Через `--legacy-peer-deps` (тот же Histoire vs Vite 8 конфликт). Альтернативы рассмотрены и отброшены: vuedraggable@2.24 (Vue 2 only), sortablejs-vue3@1.3 (менее популярна, меньше документации), @formkit/drag-and-drop (новая, без широкой адаптации). Выбор vuedraggable@4 — стандарт для Vue 3 + SortableJS под капотом (популярная DnD-библиотека).
- **`KanbanColumn.vue` переписан:**
  - Карточки обёрнуты в `<draggable v-model="localDeals" group="kanban-deals" item-key="id" ghost-class="ghost-card" drag-class="drag-card" animation="150" @change="onDraggableChange">`.
  - `group="kanban-deals"` — общий пул для всех 14 колонок (карточки можно тянуть между колонками; если group не совпадает — нельзя бросить в чужую колонку).
  - `<template #item="{ element }">` — рендерит `<KanbanCard>` через slot-API vuedraggable (не v-for).
  - `<template #footer>` — empty-state «пусто · перетащите сюда» (показывается только когда колонка пустая, как drop-target).
  - Visual-эффекты: `.ghost-card` (opacity 0.4 + bg ivory-tint — placeholder в новой позиции при drag) + `.drag-card` (opacity 0.95 + transform rotate 1deg — внешний вид перетаскиваемой карточки).
  - Props: `status`, `deals` + emit `update:deals` (v-model паттерн), `change` (с типизированным `DraggableChangeEvent`), `openDeal`.
- **Тип `DraggableChangeEvent`** (discriminated union):
  - `added: { element, newIndex }` — карточка пришла в эту колонку.
  - `removed: { element, oldIndex }` — карточка ушла из этой колонки.
  - `moved: { element, oldIndex, newIndex }` — порядок внутри колонки изменился.
- **`KanbanView.vue` переписан** с `const allDeals` (readonly) на `reactive<Record<slug, MockDeal[]>>` через `LEAD_STATUSES.reduce()`. Каждая колонка получает собственный reactive-array. Сделки клонируются shallow `{...d}` чтобы не мутировать `MOCK_DEALS` const при DnD.
- **`onColumnChange(targetSlug, event)`:**
  - При `event.added` — `event.added.element.statusSlug = targetSlug` (мутация уже клонированного объекта).
  - 'removed' и 'moved' — обрабатываются автоматически через v-model (vuedraggable мутирует array, reactive triggers re-render).
  - **TODO для production:** `POST /api/deals/{id}/transition` с проверкой allowed-переходов по матрице (например, нельзя из 'paid' обратно в 'new').
- **Vitest +1 тест** в `KanbanView.spec.ts` (всего **71/71 за 7.48 сек**):
  - «обновляет statusSlug сделки при drop в новую колонку» — эмулирует событие через `paidCol.vm.$emit('change', { added: { element, newIndex: 0 } })`, проверяет что у переехавшей сделки `statusSlug === 'paid'`. Полный DnD (mouse-events) не симулируется — JSDOM не умеет drag-events корректно; тест на event-handler логику достаточен.
- **`KanbanView.spec.ts`** обновлён старый тест: «предупреждает о DnD-в следующем коммите» → «содержит подсказку про перетаскивание (DnD активен)» (DnD теперь работает, текст в page-stats изменён с «Drag-and-drop в следующем коммите» на «Перетаскивание между колонками»).
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **71/71 за 7.48 сек**.
  - `npm run build` — **KanbanView lazy-chunk: 180.53 KB** (вырос с маленького за счёт SortableJS-обёртки vuedraggable; ~62.59 KB gzipped). Этот chunk грузится **только на /kanban** — главный app-chunk и другие views не задеты. 642 ms.
  - `npm run story:build` — **14 stories / 20 variants за 30.45 сек**.
  - `composer test` — **Pest 48/48 за 4.88 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Backend `PATCH /api/deals/{id}/transition`** с проверкой allowed-переходов (по matrix `lead_status_transitions` если будет создана) + tenant-isolation через RLS.
  - **Optimistic update + rollback на ошибку API**: если backend отклонил переход — откатить визуальное состояние.
  - **Audit-log при transition** — запись в activity_log с `event=deal.status_changed` (по схеме v8.7).
  - **Drag-handle** на карточке (сейчас вся карточка — drag-trigger; мешает click для open-drawer когда drawer будет реализован).
  - **`moved` event handler** — переупорядочивание внутри колонки (на API будет `?sort_order=N`).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.33 относительно v1.32:**

- **Реализован KanbanView** — альтернативный вид сделок (по статусам). Маршрут `/kanban` (meta.layout='app'). По `liderra_v8_handoff/concepts/v8_kanban.html`. **DnD НЕ реализован на MVP** — отдельный коммит после выбора библиотеки.
- **Структура:**
  - **`resources/js/components/kanban/KanbanCard.vue`** — компактная карточка: name + phone (JBM tnum 11px) + project + cost (Intl.NumberFormat ru-RU JBM tnum) + manager-avatar 20px с initials. `@click` → emit('open', id) для будущего DealDetailDrawer.
  - **`resources/js/components/kanban/KanbanColumn.vue`** — колонка статуса. Header: `border-top: 3px solid var(--accent)` где `--accent` инлайн-css-var из status.colorHex; внутри name + count (JBM tnum chip) + total ₽ под name (`formatTotal`: «—» при 0, иначе `Intl.NumberFormat ru-RU`). Body: vertical-scroll v-for KanbanCard + empty-state «пусто» с dashed-border. Props: `status: LeadStatus`, `deals: MockDeal[]`. Emit: `openDeal: id`.
  - **`resources/js/views/KanbanView.vue`** — orchestrator. Group MOCK_DEALS по statusSlug в Record-словарь, передаёт в каждую колонку. Page-head с stats (14 статусов · 12 сделок · «Drag-and-drop в следующем коммите»). `.kanban-board` — flex с `overflow-x: auto`, gap 12px, custom-scrollbar (height 8px, thumb #d9d5cd → #92907b на hover). 14 columns по `LEAD_STATUSES`.
- **Маршрут `/kanban`** (name='kanban', meta.layout='app', lazy-import) в router и web.php (`Route::view('/kanban', 'welcome')`).
- **Vitest +14 тестов** (всего **70/70 за 7.37 сек**):
  - **`KanbanCard.spec.ts`** (3): рендер name/phone/project/cost (regex `/1\s+850\s*₽/` для nbsp-разделителя) + manager initials + emit('open', id) на click по `.kanban-card`.
  - **`KanbanColumn.spec.ts`** (5): header с nameRu+count + total форматирование (4250 ₽ = 1850+2400) + «—» и «пусто» при empty + colorHex как inline-style CSS-var (case-insensitive matching: Vue 3 сохраняет hex case-preserve) + проброс `open` от карточки → `openDeal` с id.
  - **`KanbanView.spec.ts`** (6): заголовок «Канбан» + ровно 14 KanbanColumn-инстансов + каждая колонка получает соответствующий статус по индексу + page-stats содержит 14/статусов/сделок + кнопка «Новая сделка» + текст про DnD-предупреждение.
- **Stories +3** (4 variants):
  - `KanbanCard.story.vue` — 1 variant с MOCK_DEALS[0].
  - `KanbanColumn.story.vue` — 3 variants: «Новые» (1 deal), «Оплачено» (2 deals), пустая.
  - `KanbanView.story.vue` — 1 variant.
- **Quirk: ESLint `no-unused-vars`** на `function onOpenDeal(_id: number)` — даже underscore-prefix не работает в TS strict с ESLint config. Решение: убрать `_` и добавить `void id` внутри функции — или явно использовать аргумент в TODO-комменте; выбран первый вариант для лаконичности.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed (prettier пере-отформатировал 4 файла).
  - `npm run test:vue` — **70/70 за 7.37 сек**.
  - `npm run build` — KanbanView в lazy-chunk (отдельный chunk наряду с DealsView 84.78 KB; main app-chunk 107.18 KB). 633 ms.
  - `npm run story:build` — **14 stories / 20 variants за 31.17 сек**.
  - `composer test` — **Pest 48/48 за 5.06 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **DnD (drag-and-drop):** библиотека (vue-draggable-next или @vueuse/integrations/useSortable), хук `@end` → mutation в локальном dealsByStatus + PATCH /api/deals/{id}.
  - **Filters в Канбане:** Проект/Менеджер — общий filter-bar с DealsView (выделить в composable).
  - **DealDetailDrawer:** click по карточке → правая панель со всеми деталями (timeline, audit-log, контактами).
  - **Виртуализация:** при 100+ сделках в колонке нужен v-virtual-scroll внутри KanbanColumn.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.32 относительно v1.31:**

- **Реализован DealsView** — центральный экран CRM (`/deals`, meta.layout='app'), используется менеджерами ежедневно. По `liderra_v8_handoff/concepts/v8_deals.html` секции page-head + filter-bar + table.
- **Структура `resources/js/views/DealsView.vue`:**
  - **page-head:** `<h1>` «Сделки» + `page-stats` (4 поля JBM tnum: «+3 новых лида с утра», «12 всего», «5 в работе», «1 ждут оплату») + actions (Экспорт outlined + Новая сделка primary).
  - **filter-bar:** `v-btn-toggle` mandatory color=primary с 5 chiprow-вариантами (Все/Активные/Ждут оплату/Закрытые/Невалидные) — каждый кнопка-tab имеет внутри `v-chip x-small variant=tonal` со счётчиком; реактивный counts через computed `Record<tabId, number>`. Дальше в той же строке `v-text-field` density=compact с placeholder «Поиск: имя, телефон, проект…» + prepend mdi-magnify + clearable.
  - **`v-data-table`** show-select, item-value=id, hide-default-footer (pagination на API), hover, density=comfortable. 6 колонок: Лид (cell-deal с v-avatar 32px + name + phone caption), Статус (v-chip tonal с inline-style colorHex и border-color из lead_statuses + status-dot 6px), Проект, Менеджер (cell-manager с v-avatar 22px + name), Стоимость (`Intl.NumberFormat('ru-RU')` JBM tnum), Время (`formatRelative`: <60 → «N мин назад», <24*60 → «N ч назад», иначе «N д назад»).
  - **Empty-state** под таблицей: «Нет сделок по выбранным фильтрам» при `filteredDeals.length === 0`.
- **`resources/js/composables/mockDeals.ts`:**
  - `MockDeal` interface (id, name, phone, statusSlug из LEAD_STATUSES, project, manager {initials, name}, cost, receivedMinutesAgo).
  - `MOCK_DEALS` — 12 разнообразных сделок (новые/проработан/переговоры/оплачено/недозвон/ожидаем оплаты/просмотрено/горячий/закрыто/партнёрка/конечный недозвон/оплачено) для UI-разводки. Заменяется на API-fetch при backend-интеграции.
  - `DealsTab` interface + `DEALS_TABS` array — 5 срезов фильтра. `slugs: null` для «Все» (не фильтруем); slug-arrays для остальных. На API-стороне будут как `?status_in=...`.
- **Маршрут `/deals`** (name='deals', meta.layout='app', lazy-import) в router и `web.php` (`Route::view('/deals', 'welcome')`).
- **Vitest +8 тестов** (всего **56/56 за 5.66 сек**):
  - `DealsView.spec.ts`: монтаж + заголовок «Сделки» + 4 page-stats (новых/всего/в работе/ждут оплату) + 5 chiprow-tabs + дефолтный `active`-фильтр показывает только сделки с активными статусами (5 из 12) + Экспорт/Новая сделка кнопки + 6 колонок таблицы + формат стоимости с разделителем тысяч (regex `/2\s+400\s*₽/` — `\s` в JS уже matches U+00A0/U+202F nbsp из `Intl.NumberFormat`, не нужны literal nbsp символы — иначе ESLint `no-irregular-whitespace`) + формат «7 мин назад» для свежих сделок.
- **Quirk: ESLint `no-irregular-whitespace`** в regex с literal-nbsp. Решение: `\s+` достаточен — в JavaScript regex `\s` уже включает U+00A0 (NBSP), U+202F (narrow nbsp), U+2000-U+200A и др. Никакой literal-nbsp в regex не нужен.
- **Story `DealsView.story.vue`** (12 mock-сделок).
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed.
  - `npm run test:vue` — **56/56 за 5.66 сек**.
  - `npm run build` — DealsView в lazy-chunk **87.54 KB** (v-data-table — крупный компонент Vuetify, но загружается только на /deals; main app-chunk без изменений 106.83 KB). 538 ms.
  - `npm run story:build` — **11 stories / 15 variants за 31.93 сек**.
  - `composer test` — **Pest 48/48 за 4.96 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Drawer с деталями сделки** при клике на строку (правая панель `v-navigation-drawer right` с timeline / audit-log / контактами).
  - **Filter-popover'ы** для Проект/Менеджер/Ещё (полный multi-select со списком из API).
  - **Bulk-actions** (массовые операции при нескольких выбранных строках: смена статуса / удаление / экспорт выборки).
  - **Sort** через v-data-table sort-by-headers по cost/receivedMinutesAgo (сейчас sortable: true в headers, но без реактивного применения — на API будет server-side).
  - **Экспорт XLSX** — требует backend endpoint (php-spreadsheet) + frontend trigger.
  - **Pagination** (sticky-footer с per-page select + page navigation на server-side через ?page&per_page).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.31 относительно v1.30:**

- **Закрыт раздел charts на дашборде** — последняя крупная UI-секция из `liderra_v8_handoff/concepts/v8_dashboard.html`. Теперь Dashboard содержит весь набор: page-head + KPI-row (4 cards) + charts-row (Activity + Funnel).
- **Native SVG (без chart-library):** решено НЕ вводить ApexCharts/Chart.js/vue3-charts (+200..400 KB JS-зависимостей) — для статичных дашборд-графиков native SVG достаточно (как в handoff). Интерактив (tooltip on hover) добавим позже как Vuetify-overlay над SVG-точками.
- **`resources/js/components/charts/ActivityChart.vue`** — линейный чарт «Активность по дням»:
  - Props: `points` (7 значений по умолчанию `[16, 31, 27, 47, 39, 56, 50]`), `max` (60), `labels` (default `пн..сб + сегодня`), `title`, `subtitle`.
  - Структура: `v-card` outlined → panel-head с заголовком + subtitle + `v-btn-toggle` 3 tabs (Принято/Оплачено/Отказ) → SVG (viewBox 800×220) → легенда 3 ldot.
  - SVG-внутренности: Y-grid 5 линий (0/15/30/45/60) + area-gradient под линией (`#0A1319` opacity 0.10 → 0) + main line stroke `#0A1319` 1.7px round-join + 7 circle-точек (предпоследняя выделена primary teal `#0F6E56` r=4.2 как «сегодня (max)» по handoff) + X-подписи внизу.
  - Computed `coords` маппит points в SVG-координаты с учётом max и аспекта (X_LEFT=36, X_RIGHT=790, Y_TOP=20, Y_BOTTOM=200).
- **`resources/js/components/charts/FunnelChart.vue`** — воронка распределения по 14 статусам:
  - Props: `counts` (`Record<slug, number>`, default — mock с paid=45 как самым большим), `title`.
  - Структура: `v-card` outlined → panel-head с заголовком + `{статусов} статусов · {total} лидов · сейчас` → segmented bar (height 12px, border-radius 6px) → funnel-list (2-column grid).
  - Segmented bar: 14 div'ов с `flex: count`, `background: colorHex`, `title="{nameRu} · {count}"` для browser-tooltip.
  - List отсортирован по убыванию count'а (как в handoff — наглядность).
- **`resources/js/composables/leadStatuses.ts`** — snapshot 14 статусов из `db/schema.sql:2130` (lead_statuses seed). Объявлен **отдельным файлом-композаблом** (не inline в FunnelChart) для переиспользования в будущих view'ах (Сделки/Канбан) и для удобной замены на API-fetch при backend-интеграции. Источник истины — schema, **НЕ BRANDBOOK_v2 §3.6** (расхождение #1 handoff vs ТЗ из реестра v1.13: handoff содержит 14 «обобщённых» статусов с пересечением всего 2 (Переговоры + Оплачено)). 14 правильных slug'ов: new/viewed/worked/base/missed/negotiations/waiting_payment/partnership/paid/closed/test_drive/hot/replacement/final_missed.
- **Quirk Vue SFC compiler:** `withDefaults(..., { counts: () => DEFAULT_COUNTS })` где `DEFAULT_COUNTS` объявлен на module-level → ошибка `[plugin:vite:vue] checkInvalidScopeReference`. Vue compiler требует чтобы factory-функция в `withDefaults` не реферировала module-level const'ы. Обходится инлайнингом литерала-объекта прямо в factory. Зафиксировано комментарием в FunnelChart.vue:27.
- **Vitest +13 тестов** (всего **48/48 за 5.5 сек**):
  - `ActivityChart.spec.ts` — 6 тестов: монтаж + заголовок + 3 tabs + 7 circles в SVG + «сегодня» подпись на последней x-метке + кастомные points через props + легенда 3 dots.
  - `FunnelChart.spec.ts` — 7 тестов: заголовок + ровно 14 segments в bar + ровно 14 list-items + **assertion на наличие всех 14 nameRu из lead_statuses + отсутствие имён из BRANDBOOK ('Думает', 'Спам' — handoff-only)** — регрессия защищена + сортировка списка (paid 45 первый) + colorHex применяется к segments + total = sum counts.
- **Stories +2:**
  - `ActivityChart.story.vue` — 3 variants: default mock / ровный рост / спад.
  - `FunnelChart.story.vue` — 3 variants: default 247 лидов / пустая воронка / концентрация на paid (100/5/5/5).
  - **Histoire 10 stories / 14 variants за 30.43 сек.**
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed (prettier пере-отформатировал 2 файла).
  - `npm run test:vue` — **48/48 за 5.5 сек**.
  - `npm run build` — **DashboardView chunk вырос 14.9→21.17 KB** с двумя чартами; общий app-chunk без изменений (charts инлайнены в DashboardView lazy-chunk). 473 ms.
  - `npm run story:build` — **10 stories / 14 variants за 30.43 сек**.
  - `composer test` — **Pest 48/48 за 5.10 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - Интерактив на ActivityChart: tooltip-on-hover у точек через Vuetify overlay/menu, переключение метрики при клике на tab — сейчас активный tab меняет state но props.points не меняются.
  - Интерактив на FunnelChart: клик по сегменту → drill-down на /deals?status={slug}.
  - **Виды Сделки/Канбан/Напоминания/Биллинг/Отчёты/Менеджеры/Настройки** — отдельные коммиты по концептам v8_*.html.
  - **Backend `/api/dashboard/summary`** — отдельный коммит (вместе с auth-flow).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.30 относительно v1.29:**

- **Реализован AppLayout** — default-layout приложения для авторизованных страниц по `liderra_v8_handoff/concepts/v8_dashboard.html`. Vuetify-компоненты: `v-navigation-drawer` (theme=dark, color=secondary теало-нуар `#012019`, width=240px) + `v-app-bar` (56px, color=surface, border-bottom hairline) + `v-main` для `<RouterView />`.
  - **Sidebar:** brand-block с SVG L-Square mark + «Лидерра.» с teal-точкой; nav-tree из 8 пунктов в 3 группах через массив-конфиг `navGroups`:
    - **Работа** (4): Дашборд (mdi-view-dashboard-outline) → /dashboard, Сделки (mdi-format-list-bulleted, count=247) → /deals, Канбан (mdi-view-column-outline) → /kanban, Напоминания (mdi-clock-outline, count=12) → /reminders.
    - **Финансы** (2): Биллинг (mdi-credit-card-outline) → /billing, Отчёты (mdi-chart-box-outline) → /reports.
    - **Команда** (2): Менеджеры (mdi-account-group-outline, count=4) → /managers, Настройки (mdi-cog-outline) → /settings.
  - **Active state:** через `:active="route.path === item.to"` в `v-list-item`.
  - **Topbar:** `v-app-bar-nav-icon` (mobile-only) + crumb «Рабочая область → currentPageTitle (computed)» + spacer + search-trigger `v-btn` outlined small с ⌘K kbd-tag (`disabled` пока — TODO: command palette) + notification icon-btn с red-pip + user-chip (avatar «ИП» + «Иван П.»).
  - **Mobile (md<):** drawer toggleable через `drawerOpen ref` + nav-icon в app-bar.
- **Реализован DashboardView** (`resources/js/views/DashboardView.vue`):
  - **page-head:** заголовок «Доброе утро, Иван» (Inter opsz=28, em для имени с primary-цветом) + page-meta (`+3 новых лида с утра`, `сегодня 11 · вчера 38`, `средняя стоимость 2 248 ₽` — JBM tnum для всех чисел) + `v-btn-toggle` 4-вариантный (Сегодня/7 дней/30 дней/Период…) с `range` ref'ом.
  - **KPI-row:** `v-row dense` + 4 `v-col` (md=3 каждый):
    - 3 outlined `v-card` с `kpi-label` (text-medium-emphasis 13px) + `kpi-value` (JBM tnum 32px 600) + опционально `kpi-unit` + `kpi-foot` с delta (icon mdi-arrow-up/down + text + sub-line).
    - 1 hero `v-card` flat color=secondary с `balance-row1` (label + LIVE chip primary), `balance-amount` (JBM 32px), `runway-bar` (7 сегментов через `v-for="i in balance.runwayMax"` — заполненные `i <= balance.runwayDays` кладут teal `#32C8A9`, остальные — белые с opacity 0.08), `runway-foot` («≈ 285 лидов · хватит на 4 дня» + «пополнить →» link).
  - Все числа — mock'и. **TODO:** GET `/api/dashboard/summary` с tenant-context'ом через `SetTenantContext` middleware.
- **`AppShell.vue` упрощён** — теперь только layout-mapper: `route.meta.layout ?? 'app'` → `'app'` рендерит `<AppLayout />`, `'auth'` → `<AuthLayout />`. Раньше default-ветка inline-рендерила `v-app + v-app-bar + RouterView` — это переехало в AppLayout.
- **Router:** маршрут `/dashboard` (name='dashboard', meta.layout='app', lazy-import).
- **`web.php`:** добавлен `Route::view('/dashboard', 'welcome')`.
- **`histoire.setup.ts`:** memory-router расширен 8 app-stub-маршрутами (dashboard/deals/kanban/reminders/billing/reports/managers/settings) — иначе AppLayout warn'ит «No match found for location» на mount внутри Histoire iframe.
- **Stories +2:** `AppLayout.story.vue` (просто `<AppLayout />` — RouterView в memory-router stub'ах рендерит пустой `<div />`, sidebar/topbar видны), `DashboardView.story.vue` (DashboardView внутри `v-app + v-main` с warm ivory bg).
- **Vitest +11 тестов:**
  - `AppLayout.spec.ts` — 6 тестов: монтаж + brand-блок «Лидерра» + 3 nav-группы + 8 nav-пунктов + счётчики у Сделки/Напоминания/Менеджеры + breadcrumb с currentPageTitle.
  - `DashboardView.spec.ts` — 5 тестов: приветствие + range-toggle 4 опции + 3 KPI-cards (label) + balance-card («Баланс»/«14 250»/«LIVE»/«хватит на») + runway-bar содержит 7 сегментов.
  - `AppShell.spec.ts` переписан — 3 теста под новый layout-mapper (default рендерит AppLayout с sidebar; auth скрывает sidebar).
  - **Vitest 35/35 за 4.92 сек**.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — passed (prettier пере-отформатировал 2 файла).
  - `npm run test:vue` — **35/35 за 4.92 сек**.
  - `npm run build` — DashboardView в lazy-chunk **14.9 KB**; app-chunk вырос **78→105 KB** из-за импорта AppLayout (он не lazy — компонент уровня AppShell, нужен сразу). 458 ms.
  - `npm run story:build` — **8 stories / 8 variants за 28.97 сек**.
  - `composer test` — **Pest 48/48 за 4.88 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - **Графики на дашборде:** «Активность по дням» (line chart) + «Воронка» из 14 статусов (segmented bar + 14 list-items с цветами по lead_statuses из schema). Нужен chart-library — Vuetify 3 не имеет встроенных графиков; кандидаты: ApexCharts, Chart.js, vue3-charts. Решение — отдельный коммит после выбора.
  - **Виды Сделки/Канбан/Напоминания/Биллинг/Отчёты/Менеджеры/Настройки** — отдельные коммиты по концептам v8_*.html.
  - **Backend `/api/dashboard/summary`** — отдельный коммит вместе с auth-flow.
  - **Command palette ⌘K** — отдельный коммит.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.29 относительно v1.28:**

- **Закрыты 4 оставшихся auth-экрана** из `liderra_v8_handoff/concepts/v8_login.html` — все 5 секций (#form-login/#form-register/#form-2fa/#form-forgot/#form-recovery) теперь реализованы как Vue-компоненты с Vuetify-паттернами.
- **`RegisterView.vue`** — email + password (strength-meter 0..4 через computed: длина 8+ / mixed-case / digit / special-char) + **2 обязательных click-wrap'а**: оферта (§1.5) + согласие на ПДн (§4.1). **3-й «маркетинг»-click-wrap из handoff НЕ реализован** — handoff противоречит ТЗ (расхождение #2 реестра v1.13: handoff §form-register строка 401-403 содержит `«Согласен на получение информационных сообщений от Лидерра»`, ТЗ §1.5/§4.1 такого не предусматривает). Vitest содержит явный assertion на отсутствие строк `«информационных сообщений»` и `«маркетинг»` в DOM — регрессия защищена.
- **`TwoFactorView.vue`** — 6 input-cell, auto-focus вперёд при вводе цифры (`nextTick + inputs[i+1].focus()`), Backspace назад при empty cell, paste 6 цифр заполняет все одной операцией. UX по ТЗ §1.6 (TOTP 6 цифр, 30-сек окно). Таймер обратного отсчёта статичен (02:34) — динамика будет с backend.
- **`ForgotPasswordView.vue`** — email + `v-alert type="info"` с текстом «5 попыток в 15 минут» (по ТЗ §1.7). Кнопка «Назад ко входу» как `<v-btn :to="{ name: 'login' }">` через RouterLink.
- **`RecoveryCodesView.vue`** — 8 кодов (XXXX-XXXX, monospace) в 2-column grid. Действия: «Скачать .txt» через `Blob`/`URL.createObjectURL`/`<a download>` (legacy execCommand fallback не нужен — у нас HTTPS-only); «Копировать» через `navigator.clipboard.writeText`. Warning по ТЗ §1.6: «После закрытия страницы коды нельзя посмотреть снова».
- **Router:** добавлены 4 маршрута в `router/index.ts` — `/register`, `/2fa`, `/forgot`, `/recovery`, все meta.layout='auth'. Lazy-imports для каждого view'а (`() => import(...)`).
- **Stories:** 4 новых `*.story.vue` для Histoire (по тому же паттерну: v-app + v-main с warm ivory bg + центрирование).
- **Vitest +14 тестов** (всего 24/24 за 3.29 сек):
  - `RegisterView.spec.ts` — 4 теста: монтаж + autocomplete email/new-password + **assertion на ровно 2 click-wrap'а БЕЗ маркетингового** + RouterLink на /login.
  - `TwoFactorView.spec.ts` — 3 теста: заголовок + ровно 6 input-cell с numeric inputmode + RouterLink на recovery.
  - `ForgotPasswordView.spec.ts` — 3 теста: заголовок + текст rate-limit alert + email-input/«Отправить ссылку».
  - `RecoveryCodesView.spec.ts` — 4 теста: заголовок + ровно 8 кодов в формате XXXX-XXXX + warning о невозможности повторного просмотра + кнопки Скачать/Копировать.
- **Регресс зелёный:**
  - `npm run lint:vue` — passed; `npm run type-check` — passed; `npm run format:check` — All matched files use Prettier code style.
  - `npm run test:vue` — **24/24 за 3.29 сек**.
  - `npm run build` — **5 lazy-chunks для views** (LoginView 3.81 KB, RegisterView 9.53 KB, TwoFactorView 2.38 KB, ForgotPasswordView 1.89 KB, RecoveryCodesView 2.18 KB) + **Vuetify-компоненты в отдельные chunks** (VAlert 3.14 KB, VForm 3.61 KB, VCard 18.34 KB, VTextField 18.69 KB, VAvatar 124.08 KB) — code-splitting через lazy-imports работает: главный `app` chunk теперь 78.38 KB вместо 198.40 KB до lazy split'а. 468 ms.
  - `npm run story:build` — **6 stories / 6 variants за 29.17 сек**.
  - `composer test` — **Pest 48/48 за 4.85 сек**.
- **Что НЕ сделано (отдельные коммиты):**
  - Backend: POST /login (Sanctum), POST /register, POST /2fa/verify, POST /password/forgot, POST /2fa/recovery-codes — отдельный коммит.
  - Yandex 360 SSO — отдельный коммит (зависит от Б-1 для регистрации OAuth-приложения).
  - Динамический таймер 02:34 в TwoFactorView — отдельный коммит с `ref<number>` + `setInterval` + `onUnmounted`.
  - zxcvbn для production-grade strength-meter в RegisterView (сейчас простая эвристика).
  - Реальные коды в RecoveryCodesView из API.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — реализация UI-экранов из дизайн-handoff не двигает счётчик продуктовых вопросов.

**Что изменилось в v1.28 относительно v1.27:**

- **Реализован первый реальный экран фазы 2 — LoginView** на маршруте `/login` (5 экранов из v8_login.html — login/register/2fa/forgot/recovery — пока сделан только LoginView, остальные 4 — в отдельные коммиты).
- **Установлен `vue-router@^4.6.4`** (через `--legacy-peer-deps` — тот же Histoire 1.0-beta.1 vs Vite 8 peerDep конфликт).
- **Создано:**
  - `app/resources/js/router/index.ts` — `createWebHistory()` + lazy-imports для views; маршруты `/`→редирект на `/login`, `/login` → `LoginView` с `meta.layout='auth'`.
  - `app/resources/js/layouts/AuthLayout.vue` — двухпанельный grid: brand-pane слева (теало-нуар `#012019` + radial-gradient акценты Teal/dim-Teal по углам, brand mark с SVG L-Square + цитата с акцентом + footer с оферты/политики), form-pane справа (warm ivory `#F6F3EC`, `<RouterView />` для конкретного экрана). На mobile (md<) brand-pane скрыт через `d-none d-md-flex`.
  - `app/resources/js/views/auth/LoginView.vue` — полная форма: `v-text-field` для email/password с autocomplete (email/current-password), eye-icon toggle для password, primary submit `<v-btn>`, divider «или», Yandex 360 SSO outlined-btn, RouterLink на /register и /forgot.
- **Переписан `AppShell.vue`** как layout-mapper: `useRoute().meta.layout` → `default` (v-app + v-app-bar + RouterView для авторизованных страниц) или `auth` (AuthLayout).
- **`app.ts`** подключает router: `app.use(router)` после `app.use(vuetify)`.
- **`routes/web.php`** — **явные** `Route::view('/path', 'welcome')` для /, /login, /register, /forgot, /2fa, /recovery. **Catch-all `/{any?}` отброшен** — он перехватывал runtime-routes из Pest `beforeEach` (например `/_test/tenant-context` в `SetTenantContextTest`) и валил 5 backend-тестов.
- **Vitest расширен:**
  - `tests/Frontend/LoginView.spec.ts` — 5 тестов (монтаж + заголовок «Вход в Лидерру» + кнопка «Войти»/Yandex 360 + autocomplete email/current-password + RouterLink'и на регистрацию/сброс пароля).
  - `tests/Frontend/router.spec.ts` — 2 теста (маршрут /login с meta.layout='auth' + редирект /→/login).
  - `tests/Frontend/AppShell.spec.ts` переписан под router-context — 3 теста (default-layout рендерит брендинг + chip; auth-layout-switch скрывает chip).
  - **Vitest 10/10 за 3.01 сек**.
- **Histoire setup расширен** memory-router'ом — иначе RouterLink/useRoute в LoginView/AppShell падают на mount. Памяти-router содержит stub-маршруты для всех 6 SPA-путей (рендерят `<div />`).
- **Story `LoginView.story.vue`** — Variant «default» рендерит LoginView внутри `v-main` с warm ivory bg (имитация .form-pane без двухпанельной обвязки — RouterView в memory-router stub'ах не рендерит реальные views).
- **Регресс зелёный:**
  - `npm run lint:vue` — passed.
  - `npm run type-check` — passed.
  - `npm run format:check` — All matched files use Prettier code style.
  - `npm run test:vue` — **10/10 за 3.01 сек**.
  - `npm run build` — 212 модулей за 383 ms; **LoginView lazy-chunk: 43.5 KB JS / 51.7 KB CSS** (раздельный chunk через `import()` подтверждает работу lazy-loading).
  - `npm run story:build` — **2 stories / 2 variants за 29.94 сек**.
  - `composer test` — **Pest 48/48 за 4.86 сек** (после фикса web.php).
- **Что НЕ сделано (отдельные коммиты):**
  - 4 оставшихся auth-экрана: RegisterView, TwoFactorView, ForgotPasswordView, RecoveryCodesView (по v8_login.html секциям #form-register/#form-2fa/#form-forgot/#form-recovery).
  - **3-й click-wrap «маркетинг»** в register — НЕ реализуется в handoff-версии. По ТЗ §1.5/§4.1 только обязательные «оферта» и «согласие на ПДн» (расхождение #2 из v1.13 реестра — handoff противоречит ТЗ, источник — ТЗ).
  - Backend POST /login (Sanctum + 2FA) — отдельный коммит.
  - Yandex 360 SSO — отдельный коммит (зависит от Б-1 для регистрации OAuth-приложения).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — реализация экрана из дизайн-handoff не двигает счётчик продуктовых вопросов.

**Что изменилось в v1.27 относительно v1.26:**

- **Закрыт #24 Histoire** — последний инструмент фазы 2. Установлены `histoire@1.0.0-beta.1` + `@histoire/plugin-vue@1.0.0-beta.1`.
- **Vite 8 несовместимость:** Histoire 1.0-beta.1 заявляет peerDep `vite ^7` (как и alpha.5; для Vite 5 нужен legacy `histoire@0.17.17`). У нас Vite 8 ради `@vitejs/plugin-vue@6` — поставлено через `npm install --legacy-peer-deps`. Runtime smoke (build) пройден: vendor.js содержит `liderraForest` + `histoire.setup`, Vuetify-плагин регистрируется через `setupFile` mechanism в Histoire (второй из двух code-path'ов в `MountStory.ts`; первый — `setupCode` — даёт tree-shaking warnings про `setupVue3 will always be undefined`, безвредны).
- **Конфиги:**
  - `app/histoire.config.ts` — `HstVue` plugin + `setupFile: 'resources/js/histoire.setup.ts'` + Forest primary palette (Teal `#0F6E56` 50…900) + `storyMatch: ['resources/js/**/*.story.vue']`.
  - `app/resources/js/histoire.setup.ts` — `defineSetupVue3(({ app }) => app.use(vuetify))`. Без него `v-app/v-card/v-btn` в story не рендерятся.
- **Первая story:** `app/resources/js/components/AppShell.story.vue` — smoke с `<Story title="Layout / AppShell">` + `<Variant title="default">`.
- **npm-scripts:** добавлены `story` (dev), `story:build` (статическая сборка в `app/.histoire/dist/`), `story:preview`.
- **`.gitignore`:** добавлен `/app/.histoire/` (статическая сборка не в репо).
- **Story:build smoke:** 1 story / 1 variant за **30.25 сек**, 4 tree-shaking warnings (не блокируют).
- **Регресс зелёный:**
  - `npm run lint:vue` — passed.
  - `npm run type-check` — passed (после удаления двойного `import 'vuetify/styles'` в setup-файле — TS 6 strict не видит side-effect d.ts).
  - `npm run format:check` — All matched files use Prettier code style.
  - `npm run test:vue` — **3/3 за 2.98 сек**.
  - `npm run build` — 158 модулей за 334 ms (184 KB JS / 295 KB CSS gzipped).
  - `composer test` — **Pest 48/48 за 5.12 сек** (130 assertions).
- **Активно теперь 18 из 28** инструментов: 9 фазы 0 + 8 фазы 1 + 6 фазы 2 (фаза 2 по тулчейну закрыта). Триггер фазы 3 — ~спринт 12 (`Semgrep`, `Trivy`, `Dependabot`, `pg_audit`, `pg_anonymizer`).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — закрытие тулчейна не двигало счётчик продуктовых вопросов.

**Что изменилось в v1.26 относительно v1.25:**

- **Триггер фазы 2 выполнен** — первый коммит в `app/resources/js/`. Активирует 5 из 6 инструментов фазы 2 (CLAUDE.md §3.3) — без Histoire (отдельный коммит).
- **Стек установлен:**
  - `vue@3.5`, `vuetify@3.12`, `@vitejs/plugin-vue@6` (для Vite 8 совместимости — `@vitejs/plugin-vue@5` поддерживает только Vite 5/6), `vite-plugin-vuetify@2` (auto-import — tree-shaking без ручных import'ов в каждом .vue), `sass-embedded` (для SCSS Vuetify).
  - `eslint@10` (flat-config), `eslint-plugin-vue@10`, `@vue/eslint-config-typescript@14`, `eslint-config-prettier`, `prettier@3.8`.
  - `typescript@6`, `vue-tsc@3.2` для type-check.
  - `vitest@4.1`, `@vue/test-utils`, `jsdom`, `@vitest/coverage-v8`.
  - **Tailwind удалён** (правило CLAUDE.md §5 п.2).
- **Конфиги:**
  - `app/vite.config.js` — vue + vuetify-плагины + laravel-vite-plugin.
  - `app/vitest.config.ts` — jsdom + `setupFiles: ['./tests/Frontend/setup.ts']` (полифиллы ResizeObserver/IntersectionObserver/matchMedia/CSS.supports — JSDOM не имеет браузерных API, без них Vuetify-компоненты падают).
  - `app/eslint.config.js` — flat-config с `multi-word-component-names: 'off'` (для AppShell.vue).
  - `app/.prettierrc.json` — singleQuote, tabWidth 4, printWidth 120, vueIndentScriptAndStyle false.
  - `app/tsconfig.json` — strict TS6 + Bundler module resolution + verbatimModuleSyntax.
- **Frontend-структура:**
  - `resources/js/app.ts` — точка входа, createApp + Vuetify плагин.
  - `resources/js/plugins/vuetify.ts` — палитра Forest (Teal `#0F6E56` primary, warm ivory `#F6F3EC` background, теало-нуар `#012019` secondary, success/warning/error/info из BRANDBOOK_v2 §3).
  - `resources/js/components/AppShell.vue` — первый компонент с v-app + v-app-bar + v-card.
  - `resources/css/app.css` — глобальные стили: Inter (UI, opsz axis), JetBrains Mono (.numeric для финансов с tnum); Tailwind-импорты убраны.
  - `resources/views/welcome.blade.php` упрощён под Vue mount (`<div id="app"></div>`).
- **Vitest полифиллы JSDOM:** ResizeObserver, IntersectionObserver, matchMedia, CSS.supports — иначе Vuetify-компоненты падают (`useResizeObserver`, `useLayout` в `VApp`). Smoke-тесты `tests/Frontend/AppShell.spec.ts`: монтируется без ошибок, содержит «Лидерра.», рендерит chip с фазой 2.
- **Build smoke:** `npm run build` за 386 ms — 158 модулей, 184 KB JS / 295 KB CSS gzipped.
- **npm-scripts:** `build`, `dev`, `lint:vue`, `format`, `format:check`, `type-check`, `test:vue`.
- **lefthook job #8** (ESLint на staged `resources/js/**/*.{ts,vue}`) добавлен в pre-commit. Полный type-check (vue-tsc) и Vitest — отдельно вручную (медленно для pre-commit: ~3 сек на каждый).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — триггер фазы инфраструктурный, не открытый вопрос.

**Что изменилось в v1.25 относительно v1.24:**

- **Закрыт failed() callback** в `ProcessWebhookJob` (третий и последний кусок §5.5 narrative ТЗ, не считая Sentry/Telegram). После исчерпания всех 3 ретраев Laravel вызывает `failed(\Throwable $e)` — упавший job сохраняется в `failed_webhook_jobs` для ручного разбора и повторного запуска через админку.
- **`app/app/Models/FailedWebhookJob.php`** — Eloquent-модель для таблицы `failed_webhook_jobs` (схема v8.7 §6).
- **Реализация `failed()`** — запись через `DB::table('failed_webhook_jobs')->insert()` (не через Eloquent::create) чтобы обойти RLS: failed-callback запускается вне транзакции воркера, `SET LOCAL app.current_tenant_id` не выставлен, политика отвергла бы INSERT. Запись должна попасть в БД даже в катастрофическом сценарии. payload через `json_encode(JSON_UNESCAPED_UNICODE)` — UTF-8 кириллица сохраняется.
- **Sentry::captureException** оставлен как TODO для production (на dev-стеке нет DSN).
- **3 новых Pest-теста** в `ProcessWebhookJobTest`:
  - failed() пишет упавший job с `webhookLogId` (создаётся через `DB::table('webhook_log')` для FK).
  - failed() работает БЕЗ `webhookLogId` (NULL ok — soft FK).
  - failed() записывает payload с UTF-8 кириллицей корректно.
- **Pest полный прогон 48/48 зелёные** за 4.7 сек. Pint + Larastan чисто (ide-helper:models регенерирован для FailedWebhookJob; Pint удалил unused-import `FailedWebhookJob` из теста — DB::table используется, не Eloquent).
- **Webhook-flow покрыт полностью** на dev-стеке (за исключением Sentry::captureException и SendNewLeadNotificationJob — Биз-20 Telegram-нотификации, ждёт настоящего бота с токеном).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2).

**Что изменилось в v1.24 относительно v1.23:**

- **Закрыт пункт «pg_partman replacement» из стратегии фазы 1** (см. `project_phase1_strategy.md` — pg_partman недоступен на native Windows-стеке без сборки из исходников).
- **`app/app/Console/Commands/PartitionsCreateMonths.php`** — Artisan-команда `partitions:create-months`. Создаёт ежемесячные партиции для двух партиционированных таблиц (`deals` + `supplier_lead_costs` — обе по `received_at`). По дефолту `--ahead=2` (текущий месяц + 2 вперёд = 3 партиции на каждую таблицу). Идемпотентна (проверка через `pg_class WHERE relkind='r'` перед CREATE). Запускать через Windows Task Scheduler / cron ежесуточно.
- **Smoke-test на dev:** при `--ahead=8` создались 6 партиций (Nov 2026 - Jan 2027) + 12 skipped. После migrate:fresh партиции возвращаются к initial 6.
- **4 новых Pest-теста** в `PartitionsCreateMonthsTest`:
  - создание партиций на 8 месяцев вперёд для обеих таблиц
  - идемпотентность (повторный `--ahead=5` → 0 created, 12 skipped)
  - `--ahead=0` создаёт только текущий месяц
  - INSERT в deals с `received_at` в новой партиции корректно роутится планировщиком
  - Тесты используют beforeEach/afterEach для cleanup'а через `DROP TABLE ... CASCADE` (FK от webhook_dedup_keys на партицию propagates через partitioned-FK).
- **Pest полный прогон 45/45 зелёные** за 4.9 сек. Pint + Larastan чисто (phpstan-baseline регенерирован для динамических свойств `$this` в Pest closure'ах).
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — pg_partman replacement не открытый вопрос, а инфраструктурный TODO.

**Что изменилось в v1.23 относительно v1.22:**

- **Закрыт Биз-19** (антифрод-дедуп по phone в окне 24 ч, §10.8.1). Реализация:
  - `app/app/Services/DuplicateDetector.php` — отдельный сервис: `findMaster(tenantId, phone, ?Carbon $now): ?Deal`. Ищет в `deals` master-сделку (`duplicate_of_id IS NULL`) с тем же `(tenant_id, phone)` и `received_at >= now - 24h`. Возвращает первую по `received_at ASC` или null. Окно `WINDOW_HOURS = 24` — константа класса.
  - `App\Jobs\ProcessWebhookJob::handle()` — после `upsertDeal()` для новой сделки вызывает `DuplicateDetector::findMaster()`. Если master найден И не равен самой только что созданной сделке (она уже в БД, попадает в выборку) — `markAsDuplicate(deal, master)`: проставляет `duplicate_of_id = master.id`, пишет ActivityLog с `context.duplicate_of = master.id`. Списания НЕ происходит (BalanceTransaction + SupplierLeadCost пропускаются).
  - DI через `app(DuplicateDetector::class)` внутри `handle()` (не в сигнатуре — для совместимости с прямыми вызовами из Pest без `Bus::dispatchSync`).
- **4 новых Pest-теста** в `ProcessWebhookJobTest`:
  - master в окне 24 ч → дубль с `duplicate_of_id`, баланс не списан, BalanceTransaction только для master.
  - master старше 24 ч → НЕ дубль, баланс списан дважды.
  - дубли изолированы по tenant_id (тот же phone у разных тенантов = НЕ дубль).
  - ActivityLog для дубля содержит `context = {source: webhook, duplicate_of: master.id}`.
- **Pest полный прогон 41/41 зелёные** за 4.1 сек. Pint + Larastan чисто.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — Биз-19 уже был ✅ в v1.12 (закрытие на ТЗ-уровне), сейчас закрыт на код-уровне.

**Что изменилось в v1.22 относительно v1.21:**

- **Закрыты 4 TODO в `App\Jobs\ProcessWebhookJob`** — теперь Job полностью реализует §5.5 narrative ТЗ (за исключением `DuplicateDetector` Биз-19 и `SendNewLeadNotificationJob` Биз-20 — отдельные ветви).
- **Новые модели:**
  - `BalanceTransaction` — списание `lead_charge -1` при создании новой сделки. `balance_leads_after` фиксируется после `decrement`. `related_type`/`related_id` указывают на Deal. Type-константы (`TYPE_LEAD_CHARGE`, `TYPE_TOPUP` и т.д.) — strict CHECK constraint в schema.
  - `ActivityLog` — событие `deal.created` с `context = {source: webhook}`, `user_id = NULL` (системное). Event-константы.
  - `RejectedDealsLog` — записывается при `balance_leads <= 0` (раньше был только `Log::info`). `payload` сохраняет полный webhook для возможного восстановления при пополнении баланса. `reason = zero_balance`.
  - `SupplierLeadCost` (Ю-2) — composite PK (id, received_at), партиционирована синхронно с `deals`. `cost_rub` — snapshot `suppliers.cost_rub` на момент приёма. `supplier_id` resolves через `project_suppliers` m2m (первый активный по sort_order). Graceful skip + `Log::warning` если у проекта нет активного supplier.
  - `Supplier` — минимальная модель (FK target для SupplierLeadCost).
- **Job-структура:** `handle()` оркестрирует, делегирует в `logRejection()` / `chargeNewLead()` / `resolveSupplierId()` / `upsertDeal()`. Все INSERT'ы в одной `DB::transaction` — атомарность Ю-2 (deal + balance_transaction + supplier_lead_cost появляются вместе).
- **Не входит в PoC (отдельные ветви фазы 1):**
  - `DuplicateDetector` (Биз-19, окно 24 ч по `phone`).
  - `SendNewLeadNotificationJob` (Биз-20, Telegram).
  - `Sentry::captureException` + `FailedWebhookJob` в `failed()` callback.
  - `SystemSetting` fallback для `supplier_id` (сейчас лукап только через `project_suppliers`).
- **6 новых Pest-тестов** в `ProcessWebhookJobTest`:
  - BalanceTransaction `lead_charge -1` создаётся для новой сделки.
  - Дубль vid НЕ создаёт BalanceTransaction.
  - ActivityLog `event=deal.created` с `context.source=webhook`.
  - RejectedDealsLog `reason=zero_balance` записан при `balance_leads=0`.
  - SupplierLeadCost создаётся со snapshot `cost_rub` (helper `seedSupplierForProject()`).
  - SupplierLeadCost graceful skip если у проекта нет активного supplier.
- **Pest полный прогон 37/37 зелёные** за 3.9 сек. Pint + Larastan чисто.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2) — закрытие TODO в job не двигало счётчик продуктовых вопросов.

**Что изменилось в v1.21 относительно v1.20:**

- **CTO-17 addendum (фаза 1, Webhook PoC) — закрыт фиксом, schema.sql v8.6 → v8.7.** При имплементации `App\Jobs\ProcessWebhookJob` строго по §5.5 narrative и запуске Pest-тестов поймана FK violation в `webhook_dedup_keys`:
  - `SQLSTATE[23503]: webhook_dedup_keys_deal_id_deal_received_at_fkey … Ключ (deal_id, deal_received_at)=(N, …) отсутствует в таблице "deals".`
  - Спецификация §5.5: INSERT в dedup_keys через `nextval('deals_id_seq')` ДО INSERT в deals (атомарный захват ключа через ON CONFLICT). FK был immediate → constraint check падает на стадии 1.
- **Решение в две стадии (заказчик 08.05 поздний вечер: «решай сам, минимизируй риск ошибок в будущем»):**
  - **Стадия 1 — `DEFERRABLE INITIALLY DEFERRED` FK** на [(deal_id, deal_received_at) REFERENCES deals](../db/schema.sql#L1315) (попало в schema v8.7). В bare-транзакции production worker'а решает проблему: constraint проверяется на COMMIT.
  - **Стадия 2 — pivot на `pg_advisory_xact_lock`** (попало в production-код Job'а). Pest-тесты с `DatabaseTransactions` trait всё равно падали: outer-txn от теста + inner `DB::transaction()` Job'а = savepoint, и PG проверяет deferred constraints на RELEASE SAVEPOINT (не на outer COMMIT). Воспроизведено standalone PHP-скриптом — это PG-семантика subtransactions. Advisory lock работает identically в любой вложенности транзакций.
- **Финальный паттерн упсёрта (`ProcessWebhookJob::upsertDeal()`):**
  1. `pg_advisory_xact_lock(((tenant_id << 32) | (vid & 0xFFFFFFFF)))` — сериализует concurrent webhook'и с одинаковым (tenant_id, vid). Lock авто-освобождается на COMMIT/ROLLBACK.
  2. `SELECT deal_id, deal_received_at FROM webhook_dedup_keys WHERE tenant_id=? AND source_crm_id=?` — атомарно из-за lock.
  3. Если найдено — UPDATE deal по composite-ключу (id, received_at) с partition pruning.
  4. Иначе — INSERT deal первым (FK immediate OK), затем INSERT dedup_key с deal.id.
- **DEFERRABLE FK сохранён в schema** как defense-in-depth — для альтернативных production-паттернов без savepoint (например, batch-импорт CSV в одной транзакции).
- **Альтернативы отброшены** как orphan-prone:
  - Reverse INSERT order (deals → dedup_keys) **без advisory lock** — race condition между двумя webhook'ами с одинаковым vid создаёт orphan-deals.
  - SELECT FOR UPDATE на dedup_keys — race condition (FOR UPDATE на несуществующей строке не блокирует).
  - INSERT-в-dedup-keys-первым с DEFERRABLE FK — работает в production, но падает в тестах (savepoint quirk).
- **Backend-стек создан:**
  - `app/app/Models/Deal.php` — composite PK через override `setKeysForSaveQuery` (PG требует id+received_at для partition pruning).
  - `app/app/Models/WebhookDedupKey.php` — мини-модель для тестов и debug.
  - `app/database/factories/DealFactory.php` — fake данные с received_at в текущей партиции.
  - `app/app/Jobs/ProcessWebhookJob.php` — advisory-lock-based upsert по §5.5 v8.7 (RLS через `SET LOCAL app.current_tenant_id`, `lockForUpdate` на tenants, advisory lock + SELECT в dedup_keys + INSERT/UPDATE deals). PoC scope: dedup + balance check + project findOrCreate. TODO для следующих ветвей: BalanceTransaction, SupplierLeadCost, ActivityLog, RejectedDealsLog, DuplicateDetector (Биз-19).
  - `app/tests/Feature/DealModelTest.php` — 6 тестов composite PK + связи + casts.
  - `app/tests/Feature/ProcessWebhookJobTest.php` — 6 тестов: новая сделка, дубль vid, balance=0, изоляция тенантов, findOrCreate проекта, ON DELETE CASCADE.
- **Pest 31/31 зелёные** (вкл. 19 предыдущих + 12 новых) за 2.7 сек.
- **Метрики schema.sql v8.6 → v8.7:** без изменений (одна DDL-правка в существующем CREATE TABLE). 55 таблиц + 12 партиций, 92 индекса, 36 RLS, 5 функций, 13 триггеров.
- **Импакт:** [CLAUDE.md §0](../CLAUDE.md) v1.11 → v1.12 (schema v8.7, реестр v1.21). [db/CHANGELOG_schema.md](../db/CHANGELOG_schema.md) — добавлена запись §W. Narrative ТЗ §11 DDL `webhook_dedup_keys` синхронизирован с DEFERRABLE; §5.5 PHP-код получил комментарий-обоснование.
- **Сводка §0:** **70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1 + 0 P2** — без изменений (CTO-17 addendum — фикс, не отдельный вопрос).

**Что изменилось в v1.20 относительно v1.19:**

- **Закрыт техдолг v1.19** — narrative ТЗ ↔ schema v8.6 двустадийный dedup. Точечные in-place правки в 8 разделах [CRM_bp-gr_Инструкция_v8_5.md](CRM_bp-gr_Инструкция_v8_5.md):
  - **§2.4 Поток данных от Webhook до сделки** — поток воркера переписан: вместо `UPSERT по (tenant_id, source_crm_id)` теперь двустадийная операция INSERT в `webhook_dedup_keys` ON CONFLICT → INSERT/UPDATE deals по `(deal_id, deal_received_at)`.
  - **§5.5 ProcessWebhookJob (PHP-код)** — `Deal::updateOrCreate()` заменён на сырой `DB::selectOne` с `INSERT INTO webhook_dedup_keys ... ON CONFLICT DO UPDATE RETURNING (xmax = 0) AS is_new, deal_id, deal_received_at` + раздельные ветки `Deal::create()` и `Deal::where(...)->update(...)`. `wasRecentlyCreated` → `$isNew`.
  - **§5.6 Таблица крайних случаев** — строка «дубль vid» теперь упоминает 2-стадийную dedup-логику через `webhook_dedup_keys`.
  - **§6.5 Идемпотентность импорта** — SQL-пример `INSERT INTO deals ... ON CONFLICT (tenant_id, source_crm_id) DO UPDATE` заменён на двустадийный паттерн (INSERT в dedup_keys → INSERT в deals или UPDATE по composite-ключу). Добавлено объяснение, почему UNIQUE на партиционированной таблице невозможен.
  - **§11 DDL deals** — `CREATE UNIQUE INDEX ON deals (tenant_id, source_crm_id) WHERE source_crm_id IS NOT NULL` → `CREATE INDEX` (без UNIQUE). Добавлен полный DDL `webhook_dedup_keys` (PK, composite FK, INDEX, ENABLE RLS, POLICY tenant_isolation USING + WITH CHECK).
  - **§20.12.3 Связь с биллингом тенанта** — поток в транзакции расширен с 4 шагов до 5: добавлены шаги `INSERT INTO webhook_dedup_keys` + ветка is_new для `INSERT INTO deals`/UPDATE.
  - **§21.1 Списание баланса** — фраза «при дублях (UPSERT по `vid`)» → «двустадийная dedup-логика через `webhook_dedup_keys` по `vid`, v8.6 §5.5».
  - **§27.1 Что зафиксировано** — итоговая строка про идемпотентность переписана под `webhook_dedup_keys.PRIMARY KEY (tenant_id, source_crm_id)` с composite FK на `deals(id, received_at)`.
- **Не трогалось** (намеренно): line 302 (глоссарий «Idempotency» — общая концепция UPSERT, остаётся корректной); line 1238 (диаграмма CSV-импорта «делает UPSERT» — обобщённый текст, не SQL); line 1180 (маппинг колонки `id → deals.source_crm_id` — не про идемпотентность); line 4467 (формулировка списания, исправлена в §21.1 уже).
- **Импакт:** [CLAUDE.md §0](../CLAUDE.md) обновляется до v1.11 (Открытые_вопросы v1.19→v1.20). Версия narrative ТЗ файла **не меняется** (как для L13-гигиены `3a9ed71`) — путь архива остаётся `docs/CRM_bp-gr_Инструкция_v8_5.md`.
- **Сводка §0:** без изменений (70 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1) — закрытие техдолга связности не двигало счётчик продуктовых вопросов.

**Что изменилось в v1.19 относительно v1.18:**

- **CTO-17 (фаза 1, 08.05.2026 поздний вечер) — закрыт фиксом, schema.sql v8.5 → v8.6.** При первом реальном `php artisan migrate:fresh` PostgreSQL 16 отклонил 2 DDL-конструкции v8.5:
  1. `CREATE UNIQUE INDEX ON deals (tenant_id, source_crm_id) WHERE source_crm_id IS NOT NULL` ([db/schema.sql:1263](../db/schema.sql)) — UNIQUE на партиционированной таблице должен включать partition key (`received_at`). Включить его нельзя — ломает идемпотентность webhook'ов (тот же `vid` при retry с разным timestamp создаст дубль вместо UPDATE).
  2. `deadline_at TIMESTAMPTZ GENERATED ALWAYS AS (received_at + INTERVAL '30 days') STORED` ([db/schema.sql:1999](../db/schema.sql)) — `+ INTERVAL` не immutable (зависит от timezone/DST), отклонено как «генерирующее выражение не является постоянным».
- **Решение (заказчик 08.05 поздний вечер: «архитектурный фикс»):**
  1. Идемпотентность webhook'ов вынесена в новую таблицу `webhook_dedup_keys` (не партиционированная, PRIMARY KEY (tenant_id, source_crm_id), composite FK на deals(id, received_at) ON DELETE CASCADE, RLS tenant_isolation USING + WITH CHECK). UNIQUE INDEX на `deals` заменён на обычный INDEX. Webhook handler — двустадийная операция в одной транзакции (INSERT ON CONFLICT в dedup_keys → INSERT/UPDATE в deals).
  2. `deadline_at` стал обычной `TIMESTAMPTZ NOT NULL` колонкой, заполняемой триггером `trg_pd_subject_requests_deadline` (BEFORE INSERT OR UPDATE OF received_at) + функцией `set_pd_subject_request_deadline()`.
- **Метрики schema.sql v8.5 → v8.6:** таблиц 54→55, индексов 91→92, RLS-политик 35→36, ENABLE RLS 35→36, функций 4→5, триггеров 12→13. Партиции/роли — без изменений.
- **Техдолг для narrative ТЗ:** [CRM_bp-gr_Инструкция_v8_5.md §15-16](CRM_bp-gr_Инструкция_v8_5.md) содержит `INSERT ... ON CONFLICT (tenant_id, source_crm_id) DO UPDATE` в SQL-примерах webhook handler'а (строки ~518, 1218-1220). Эту логику нужно обновить на двустадийную (через `webhook_dedup_keys`). **Не блокер для phase 2** — Laravel webhook handler пишется по новой схеме сразу. Но связность архива требует hygiene-прохода в следующих сессиях.
- **Импакт:** [CLAUDE.md §0](../CLAUDE.md) обновляется до v1.10 (schema v8.5→v8.6, Открытые_вопросы v1.18→v1.19). [db/CHANGELOG_schema.md](../db/CHANGELOG_schema.md) — добавлена запись §X.
- **Сводка §0:** **70 ✅ / 5 🟦 / 4 ⏸** / 1 P0 / 3 P1 / 0 P2 — CTO-17 добавлен и сразу закрыт (+1 решённый, +1 в счёт). Был 69 ✅.

**Что изменилось в v1.18 относительно v1.17:**

- **Закрыт техдолг v1.17** — синхронизация narrative под Laravel 13. Точечные правки в трёх файлах (один токен на файл, без изменения версии каждого документа): [CRM_bp-gr_Инструкция_v8_5.md:6219](CRM_bp-gr_Инструкция_v8_5.md), [Vybor_oblaka_v8_3.md:3](Vybor_oblaka_v8_3.md), [Админка_SaaS_v8_2.md:103](Админка_SaaS_v8_2.md) — `Laravel 11` → `Laravel 13`.
- **Не трогалось** (намеренно, исторические записи): [Объединённый_конспект.md:149](Объединённый_конспект.md) — таблица «Финальная фиксация всех решений» внутри Части I «Создание документации v8.0 (03.05.2026)»; [Открытые_вопросы_v8_3.md:354](Открытые_вопросы_v8_3.md) — фиксация результата аудита (на момент аудита стек был L11, аудит подтвердил правильность выбора над «лоскутным» Vue 2 + Yii2; смысл утверждения сохраняется при апдейте до L13).
- **Импакт:** [CLAUDE.md §0](../CLAUDE.md) обновляется до v1.7 (Открытые_вопросы v1.17→v1.18). Triggers/реестр/Tooling/Pest/брендбук — без изменений.
- **Сводка §0:** без изменений (69 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1) — закрытие техдолга связности не двигало счётчик продуктовых вопросов.

**Что изменилось в v1.17 относительно v1.16:**

- **Переоткрыт стек §2 CLAUDE.md** — backend-фреймворк уточнён. Решение 08.05.2026 (поздний вечер): **Laravel 13** (не Laravel 11, как было в CLAUDE.md v1.0–v1.5). Произошло так: при `composer create-project laravel/laravel app` я не зафиксировал `^11` — Composer подтянул свежайшую `laravel/framework: ^13.7` (Laravel 13.7, релиз ~Feb 2026). Smoke-test'ы (Laravel migrate 3/3, Pest 2/2, Pint --test passed, PHPStan analyse passed, ide-helper:generate OK) подтвердили что Laravel 13 функционирует. Live-проверка совместимости 5 ключевых плагинов: Boost v2.4.6 (composer dry-run резолвит lock без conflict), Larastan v3.9.6, Pest v4.7.0, IDE Helper v3.7.0, Pint v1.29 — всё работает с Laravel 13. Заказчик 08.05 (поздний вечер) принял Laravel 13 как latest stable. Откат дороговат — `rm -rf app/ && composer create-project laravel/laravel:^11 app + повторить predis/Pest/Pint/Larastan/IDE Helper`.
- **Импакт:** [CLAUDE.md §2](../CLAUDE.md) обновляется до v1.6 (Laravel 11 → Laravel 13); [Tooling_v8_3.md](Tooling_v8_3.md) v1.3→v1.4 (стек §0 — Laravel 13). composer.json в `app/`: `laravel/framework: ^13.7`.
- **Техдолг (для следующих сессий):** синхронизация narrative ТЗ ([CRM_bp-gr_Инструкция_v8_5.md:6219](CRM_bp-gr_Инструкция_v8_5.md), [Vybor_oblaka_v8_3.md:3](Vybor_oblaka_v8_3.md), [Админка_SaaS_v8_2.md:103](Админка_SaaS_v8_2.md)) под Laravel 13 — пока эти документы упоминают Laravel 11. Не блокер для разработки, но нарушает связность архива. Включить в hygiene-проход при следующем апдейте narrative до v8.6. **(Закрыт в v1.18.)**
- **Сводка §0:** без изменений (69 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1) — переоткрытие стека не двигало счётчик.

**Что изменилось в v1.16 относительно v1.15:**

- **Переоткрыт+закрыт CTO-12** — выбор тест-фреймворка PHP уточнён до мажорной версии. Решение 08.05.2026 (поздний вечер): **Pest 4** (не Pest 3, как было в v1.14). Произошло так: при `composer require pestphp/pest pestphp/pest-plugin-laravel --dev --with-all-dependencies` я не зафиксировал `^3` — composer подтянул свежайшую Pest v4.7.0 + pest-plugin-laravel v4.1.0. Smoke-test (`./vendor/bin/pest`) на default-тестах Laravel 11 прошёл 2/2 за 281 ms — backward-compat с Pest 3 syntax подтверждён. Заказчик 08.05 (поздний вечер) принял Pest 4: бонус — browser testing (без Dusk), stress testing, mutation testing v2. Откат дёшев (`composer require pestphp/pest:^3`).
- **Импакт:** [Tooling_v8_3.md §3.3](Tooling_v8_3.md) (фаза 2 — было ошибочно «фаза 1»; уточняется в Tooling v1.3): **Pest 4** в строке 18; [CLAUDE.md §3.2](../CLAUDE.md) обновляется до v1.5 (упоминание «Pest 3» → «Pest 4»). На `boost:install` (триггер фазы 1 интерактивный) — по-прежнему отключить guideline PHPUnit, оставить Pest. Composer.json в `app/` уже фиксирует `pestphp/pest: ^4.7`.
- **Сводка §0:** без изменений (69 ✅ / 5 🟦 / 4 ⏸ / 1 P0 + 3 P1) — переоткрытие CTO-12 не двигало счётчик, остался ✅.

**Что изменилось в v1.15 относительно v1.14:**

- **Закрыт Диз-1** (P1) — HTML-прототипы. Решение заказчика 08.05.2026: фактически покрыт handoff Платона v8 Forest на 100%+ (13 экранов в [web/v8/](../web/v8/) против 8 запланированных + 3 экрана ошибок). Прил. Л не выпускается (избыточно). Точечные расхождения handoff vs ТЗ — реализовывать по ТЗ/schema (см. шапку v1.13). axe: 81 violation на 10/13 — исправлять при реализации в Vue+Vuetify.
- **Сводка §0:** «Дизайнер/маркетинг» 2→3 закрыто, итого продуктовых 68→69 ✅, 5→4 ⏸, 4→3 P1.
- **Истинно открыто на 08.05.2026 (после v1.15):** **4 ⏸** — **Б-1 (P0)** + 3 P1, все ждут Б-1: Диз-3 (контакты подвала), DO-2 (домен `liderra.*` + SSL), DO-4 (список SaaS-сотрудников). **Один настоящий блокер** — Б-1.

**Что изменилось в v1.14 относительно v1.13:**

- **Закрыт CTO-12** — выбор тест-фреймворка PHP. Решение заказчика 08.05.2026: **Pest 3** (не PHPUnit). Обоснование: datasets для 14 статусов воронки / 34 RLS / 4 ролей БД; architecture testing закрепляет правила Claude на код-уровне; родной для Laravel и Boost (один guideline вместо разрыва); на PHPUnit под капотом → откат дёшев. На `boost:install` (триггер фазы 1) — отключить guideline PHPUnit, оставить Pest.
- **Сводка §0:** CTO 15→16 закрыто, итого продуктовых 77→78 / 67→68 ✅. Открытых вопросов осталось **5 ⏸** (Б-1 P0 + 4 P1: Диз-1, Диз-3, DO-2, DO-4) — без изменений.
- **Импакт на код:** нет (фаза 0). При триггере фазы 1: `composer require pestphp/pest --dev` вместо встроенного PHPUnit; в [Tooling_v8_3.md §3.4](Tooling_v8_3.md) и [CLAUDE.md §3.2](../CLAUDE.md) — фиксированный выбор.

**Что изменилось в v1.13 относительно v1.12:**

- **Ребрендинг 08.05.2026** — продукт переименован в «Лидерра.» (с точкой). Решение принято заказчиком после получения handoff-пакета [liderra_v8_handoff/](../liderra_v8_handoff/) от дизайнера Платона (kpd9363@gmail.com) от 07.05.2026. Старое имя «Лидпоток» (v1.1 brandbook) — deprecated. Массовая замена выполнена в 33 файлах (449 вхождений) — все .md/.sql/.json/.toml/.yml/.txt/.html, кроме исторических упоминаний внутри `liderra_v8_handoff/`.
- **Удалён** старый `docs/brandbook.md` v1.1. Источник истины бренда — `liderra_v8_handoff/docs/BRANDBOOK_v2.md` (v8 Forest).
- **Добавлен дизайн-handoff** в [CLAUDE.md §0](../CLAUDE.md): `liderra_v8_handoff/docs/DEVELOPER_HANDOFF.md` — токены, лого, типографика, компоненты. **Применяется только к дизайну/имени/логотипу.** Функционал, состав страниц, правила (CTO-11, click-wrap, SSO break-glass, 14 статусов воронки) — **остаются по ТЗ v8.5/schema v8.5**, расхождения handoff с ТЗ зафиксированы ниже.
- **Скопированы 13 концептов** из `liderra_v8_handoff/concepts/v8_*.html` в `web/v8/`. Старые `web/01-login.html`, `02-dashboard.html`, `03-deals.html`, `index.html` (v1.1 палитра) удалены.
- **Точечные расхождения handoff vs ТЗ**, которые НЕ применены (зафиксированы для возможной обратной связи Платону):
  - 14 «обобщённых» статусов в [BRANDBOOK_v2 §3.6](../liderra_v8_handoff/docs/BRANDBOOK_v2.md) ≠ 14 slug'ов в [schema.sql:2076](../db/schema.sql#L2076) (совпадает только 2 из 14: «Переговоры», «Оплачено»). **Источник — schema/ТЗ §6.4.**
  - 3-й click-wrap чекбокс в `v8_login.html` («маркетинг-опционально») ≠ ТЗ §1.5/§4.1 («согласие на ПДн», обязательное). **Источник — ТЗ.**
  - SSO в `v8_admin.html` («локальный 2FA fallback») ≠ ТЗ OPEN-И-13 (break-glass `super_admin`, локальный 2FA выключен). **Источник — ТЗ.**
  - Заявление «axe-core 4.10.2 — 0 violations на всех макетах» в [README handoff](../liderra_v8_handoff/README.md) — локальная проверка Pa11y 9.1.1 + axe нашла 81 violation на 10 из 13 HTML (преимущественно `color-contrast` на декоративных separator'ах с `--ink-disabled`). Чисто: settings/errors/palette_options.
- **Сводка §0:** без изменений (67 ✅ / 5 🟦 / 5 ⏸). Диз-1 (HTML-прототипы у Claude) — формально остаётся ⏸ до явного «закрываем» от заказчика, но фактически покрыт handoff Платона на 100%+ (13 экранов > 8 запланированных).

> **⚠ Состояние на 06.05.2026 (v8.3.3):** все упоминания файлов `audit-batch-12..15-2026-05-05.md` в этом документе следует читать как ссылки на соответствующие **части 12, 13, 14, 15** объединённого документа `Аудит_partii_12_15_originala_v8_3.md` (объединение проведено 05.05.2026 в рамках оптимизации архива). Содержимое исходных партий полностью сохранено. Аналогично: ссылки на `CHANGELOG-v8_3.md` теперь — это **запись A** в объединённом `CHANGELOG_schema.md`; ссылки на `Konspekt_sessii_05_05_2026_obrabotka_*.md` теперь — это соответствующие **части V и VI** в `Объединённый_конспект.md` (`Konspekt_sessii_05_05_2026.md` удалён в шаге «v8.3++ optimized + правила Claude», информация перенесена в `Объединённый_конспект.md`).

**Что изменилось в v1.12 относительно v1.11:**

- **Закрыты все 27 вопросов аудита C** (раздел 13.2–13.5) — решение заказчика 07.05.2026 «A везде, рекомендованные варианты». Выбор обоснован тем, что все рекомендации в реестре проходили предварительную фильтрацию агентами C-1/C-2 как минимально-достаточный baseline для pay-per-lead-сегмента + 152-ФЗ.
- **Сводный список закрытий с импактом на schema/narrative** — новый §13.10 «Закрытия аудита C — 27 решений (07.05.2026)».
- **8 P0 разблокированы** для триггера фазы 1 (`composer create-project`). Архитектурные следствия: `projects.assignment_strategy='manual'` (Биз-17), `deals.duplicate_of_id` + 24ч-дедуп (Биз-19), e2e-тест `SET LOCAL`+PgBouncer в спринте 1 (CTO-13), `WITH CHECK` + REVOKE на saas-таблицах (OPEN-И-14), append-only triggers + hash-chain + роль `crm_audit_writer` на 5 audit-таблицах (OPEN-И-15), Sentry whitelist+regex для 6 паттернов ПДн (OPEN-И-16), OIDC+JIT+break-glass для admin SSO (OPEN-И-13), TTFR-15-мин default (Биз-18).
- **12 P1 закрытий** — все попадают в фазы 1–2: UTM-поля (CTO-14), `project_user_assignments` (CTO-16), TTL ключей 365 дней (OPEN-И-17), DNS-rebinding pin-IP (OPEN-И-18), hard-limit `api_keys` ≤10 (OPEN-И-19), presigned URL 24ч + триггер audit (OPEN-И-20), Nginx rate-limit + Yandex SmartCaptcha + disposable-blacklist (OPEN-И-21), two-person impersonation для `compliance` (CTO-15, Ю-9), Telegram-бот в спринте 9 (Биз-20), generic outbound `marketing.conversion` (Биз-21), простой scoring (Биз-22).
- **7 P2 закрытий** — все либо мелкие schema-добавки (`deals.region_code/city` Биз-23, закомментированный DDL `call_recordings` OPEN-И-26), либо infra-задачи (Yandex KMS per-tenant DEK OPEN-И-22, `pg_anonymizer` процедура OPEN-И-24), либо cron-регламенты (Биз-24, OPEN-И-25), либо документная фиксация роли `crm_audit_writer` (OPEN-И-23, уже взято в P0 OPEN-И-15).
- **Сводка §0:** 67 ✅ / 5 🟦 / 5 ⏸ (1 P0 Б-1, 4 P1 Диз-1/Диз-3/DO-2/DO-4 — все ждут Б-1 или у Claude). **Все P2 продуктовые закрыты повторно.**
- **Импакт на schema/narrative:** schema.sql v8.4 → **v8.5** (12 column-add + 2 новых таблицы + 5 audit-триггеров + 1 новая роль + WITH CHECK на 2 политики + REVOKE на 5 таблиц), narrative v8.4 → **v8.5** (правки §10 дедуп, §12.5.5 TTFR, §22.7 SSO, §22.X Sentry PII, §23.10 admin flow). Реализация — отдельный коммит после фиксации решений.

**Что изменилось в v1.11 относительно v1.10:**

- **Аудит C проведён 07.05.2026** (read-only general-purpose агенты, без правки кода). Найдено **27 новых открытых вопросов** — раздел 13:
  - **Бизнес/продукт (8):** Биз-17 (автораспределение), Биз-18 (TTFR-SLA), Биз-19 (антифрод дублей) — все P0; Биз-20 (Telegram), Биз-21 (Я.Директ/VK), Биз-22 (lead scoring) — P1; Биз-23 (гео-таргетинг сделок), Биз-24 (нотификация просрочки) — P2.
  - **CTO (4):** CTO-13 (SET LOCAL+PgBouncer тест) P0; CTO-14 (UTM в deals), CTO-15 (impersonation two-person), CTO-16 (skill-based routing) — P1.
  - **OPEN-И (14):** OPEN-И-13 (Yandex 360 SSO), OPEN-И-14 (RLS WITH CHECK), OPEN-И-15 (append-only audit), OPEN-И-16 (Sentry PII) — P0; OPEN-И-17 (ротация секретов), OPEN-И-18 (DNS-rebinding), OPEN-И-19 (api_keys лимит), OPEN-И-20 (signed URL), OPEN-И-21 (anti-DDoS) — P1; OPEN-И-22 (crypto-shred), OPEN-И-23 (audit role), OPEN-И-24 (pg_anonymizer), OPEN-И-25 (эскалация лидов), OPEN-И-26 (call_recordings задел) — P2.
  - **Юрист (1):** Ю-9 (152-ФЗ запрет impersonation в processing_restricted) — P1.
- **Закрытий нет** — правило §2.2 (без явного «закрываем» от заказчика). Все 5 ⏸ из v1.10 (Б-1, Диз-1, Диз-3, DO-2, DO-4) остаются в работе.
- Подробное описание + варианты A/B/C по каждому ID — раздел 13.

**Что изменилось в v1.10 относительно v1.9:**

- **Закрыто 11 вопросов** одним проходом по дефолтам Claude, утверждены заказчиком 06.05.2026:
  - **Биз-6** (P1) — скриншоты crm.bp-gr.ru через Claude → переведено в режим «по запросу» (15 партий аудита уже сделано 04–05.05).
  - **Биз-10..13** (4× P2) — модель напоминаний (отдельная таблица `reminders`), мульти-кошельковый биллинг (одна модель в рублях), телефония (Post-MVP), magic-link 24ч (делаем).
  - **Биз-14..16** (3× P2) — TTL soft-deleted projects (180 дней + cron disabled), OSINT wizard (не делаем на MVP), `tenants.desired_daily_numbers` (добавляем).
  - **OPEN-К-1, К-7** (2× P2) — грант Cloud Boost и резервирование CVoS — отложены до стабилизации.
  - **OPEN-И-12** (P1) — контакты эскалации временно в Notion private / 1Password DevOps до закрытия DO-4.
- **Сводка §0:** ✅ 33 → 40, ⏸ 12 → 5, P1 4 → 4, P2 7 → 0 (закрыли 7×P2 — Биз-10..16; Биз-6 P1 закрыт, OPEN-И-12 P1 → P1 разница 0; OPEN-К-1/К-7 учитываются в Прил. К, не в продуктовых).
- **Истинно открытые продуктовые после v1.10:** 5 — Б-1 (P0), Диз-1 (P1, у Claude), Диз-3 (P1 ждёт Б-1), DO-2 (P1 ждёт Б-1), DO-4 (P1 ждёт Б-1). **Все P2 закрыты.**

**Что изменилось в v1.9 относительно v1.8:**

- В архив добавлено **Прил. Н** — `Tooling_v8_3.md` v1.0 (реестр 28 инструментов разработки в 4 фазах). Создан корневой `CLAUDE.md` — оперативная карта для Claude Code. Утверждено заказчиком 06.05.2026.
- **Продуктовых вопросов не добавлено** — Прил. Н фиксирует техническое решение Claude по выбору tooling, не требующее закрытия со стороны заказчика, бухгалтера, юриста, дизайнера или DevOps.
- **Сводка §0 не изменилась** (50 продуктовых вопросов: ✅ 33 / 🟦 5 / ⏸ 12 / P0 1 / P1 4 / P2 7).
- Технически появился новый ID на будущее: **CTO-12** — выбор Pest или PHPUnit. Решается до триггера фазы 1 (`composer create-project`); явного дефолта в Прил. Н §3.4 нет — там только требование выбрать ОДИН до установки Boost (см. также §6 п. 2 «конфликты»). **Не блокер** для текущей фазы 0.
- В §10 «Что осталось мне (Claude)» добавлена явная задача: при триггере фазы 1 — пройти процедуру `boost:install` по Прил. Н §10.1 (отключение нерелевантных guidelines, замена PG MCP, создание Vuetify guideline override).

**Что изменилось в v1.8 относительно v1.7:**
(см. v1.7 ниже)

**Что изменилось в v1.7 относительно v1.6:**

- Проведён **аудит связности архива v8.3++ optimized** (отчёт в чате 05.05). Найдено: 4 🔴 ошибки + 11 🟡 расхождений; ✅ метрики schema.sql сходятся (51 таблица + 12 партиций, 81 индекс, 31 RLS, 0 orphan-FK).
- **Разрешена коллизия `OPEN-И-2`.** В v8.1 этот ID означал «контакты эскалации» (P0), в v8.2 был переиспользован для «стратегия CRM-интеграций» (✅ закрыт 04.05). Параллельно в narrative §28 был заведён `OPEN-И-11` для миграций `crm_connections`/`crm_field_mappings` (✅ закрыт в составе OPEN-И-2). По решению заказчика 05.05 (вариант B′): «контакты эскалации» переименованы в **`OPEN-И-12` (P1)** — см. секцию 8.4. ID `OPEN-И-2` и `OPEN-И-11` оставлены в текущем смысле.
- **Синхронизирован** `Runbook_ekspluatatsii_v8_2.md` Часть В: запись `OPEN-И-1` (incidents_log) помечена как ✅ закрытая (раньше шапка говорила ✅, тело таблицы — ⏸).
- Исправлены 4 ссылки на удалённый `Konspekt_sessii_05_05_2026.md` → `Объединённый_конспект.md` (Части V–VI). Файл удалён в шаге «v8.3++ optimized + правила Claude»; информация перенесена.
- Добавлены явные пометки 📜 «историческая запись» к таблице приложений §28 narrative (срез на 04.05) для прослеживаемости решений.
- Уточнён счётчик OPEN-вопросов в приложениях: было «64» (v1.6), фактически было 65 (с учётом OPEN-И-11), стало 66 (+OPEN-И-12).

**Что изменилось в v1.6 относительно v1.5:**

- Получены результаты **параллельного аудита партий 12–15** (4 файла `audit-batch-12..15-2026-05-05.md`, read-only).
- **Прил. М** обновлён до v1.1 — добавлены раздел 9 «Результаты партий 12–15», расширен §3 (DDL для schema.sql v8.3), уточнены §2.5/2.13/2.15.
- Добавлен **раздел 12** с **3 новыми продуктовыми вопросами**: **Биз-14, Биз-15, Биз-16**. Все имеют разумные дефолты.
- **Биз-10 переоткрыт** — гипотеза «1 напоминание = 1 deal» опровергнута. В оригинале `histories[]` — массив. Решение в форме отдельной таблицы `reminders` (минимальный паритет).
- **Окончательный вердикт по outbound webhook'ам** (партия 15): нет, 7 линий доказательств. С уточнением о hardcoded `prostats.info/bitrix24/webhook.php` (inbound с т. з. CRM).
- **Подтверждение модели B1/B2/B3** (партия 13.3.5) — цитата из UI о capabilities. Расширение `suppliers` 5 полями.
- Новый антипаттерн оригинала: **API credentials в `<input type="text">`** (партия 15) → у нас обязательно `password` + toggle.
- Подтверждение prompt injection в DOM усилено новыми элементами (`claude-agent-glow-border`, `@keyframes claude-pulse`) — гипотеза «расширение браузера» наиболее вероятна.
- Обновлены задачи Claude в разделе 9: добавлены патчи schema.sql v8.3 (`reminders` v8.3, `suppliers` capabilities, `tenants.desired_daily_numbers`, `system_settings` cron purge-deleted).

**Что изменилось в v1.5 относительно v1.4:**

- Получены результаты **аудита оригинала crm.bp-gr.ru** (`crm-bp-gr-audit-2026-05-04.md`, 11 партий, read-only).
- Создано **Прил. М** «Анализ оригинала и архитектурные следствия» — `Analiz_originala_v8_3.md`.
- Добавлен **раздел 11** с **4 новыми продуктовыми вопросами** из аудита: **Биз-10, Биз-11, Биз-12, Биз-13**. Все имеют разумные дефолты, не блокеры разработки.
- Обновлены задачи Claude в разделе 9: добавлены патчи schema.sql под `suppliers`, `project_suppliers`, расширение `projects`, `project_limit_adjustments`. Список разделов narrative для v8.4 расширен до 10.
- Подтверждена prompt injection-атака в DOM crm.bp-gr.ru (раздел 11.5) — действия Claude в Chrome корректные.

**Что изменилось в v1.4 относительно v1.3:**

- Зафиксировано **30 продуктовых решений** в сессии 04.05.2026 (полный конспект — `Konspekt_sessii_04_05_2026_intervyu.md`).
- Закрыты: DO-1 (Yandex Cloud), Б-2..6 (кроме Б-1), CTO-6..11, Биз-5/7/9, Ю-6/7, Диз-2/4, DO-3/5, OPEN-К-2..8, OPEN-Д-1/5, OPEN-Ж-3, OPEN-И-1/2.
- В работе у Claude: Диз-1 (HTML-прототипы → Прил. Л).
- Истинных P0-блокеров от заказчика осталось: **1 (Б-1)**.

---

## 0. Сводка

| Адресат | Всего | ✅ закрыто | 🟦 структурно | Открыто | P0 | P1 | P2 |
|---------|-------|------------|---------------|---------|----|----|-----|
| Юрист | 9 | 4 (Ю-1, Ю-6, Ю-7, Ю-9 v1.12) | 5 (Ю-2-доп, Ю-3-юр, Ю-4, Ю-5, Ю-8) | 0 | 0 | 0 | 0 |
| Бухгалтер / финансы | 6 | 5 (Б-2..6) | 0 | 1 (Б-1) | **1** | 0 | 0 |
| CTO / архитектор | **18** | **18** (CTO-1..16 + CTO-19 v1.83 Lucide migration + CTO-20 v1.81 ICU collation) | 0 | 0 | 0 | 0 | 0 |
| Бизнес / продакт | **24** | 17 (6 + Биз-3/4/8 авто + Биз-10..16 v1.10 + Биз-17..24 v1.12) | 0 | **7** (Биз-25..31 v1.78 Plan 4 deferred placeholders, см. §14) | 0 | **2** (Биз-25, Биз-28) | **3** (Биз-26, Биз-27, Биз-31) |
| Дизайнер / маркетинг | 4 | 3 (Диз-1 закрыт 08.05 handoff'ом, Диз-2, Диз-4) | 0 | 1 (Диз-3 ждёт Б-1) | 0 | 1 | 0 |
| DevOps / эксплуатация | 5 | 3 (DO-1, DO-3, DO-5) | 0 | 2 (DO-2, DO-4 ждут Б-1) | 0 | 2 | 0 |
| OPEN-К (облако) | 8 | 8 | 0 | 0 | 0 | 0 | 0 |
| **Аудит партий 1–11** | **4** | **4** (Биз-10..13 закрыты v1.10) | 0 | 0 | 0 | 0 | 0 |
| **Аудит партий 12–15** | **3** | **3** (Биз-14..16 закрыты v1.10) | 0 | 0 | 0 | 0 | 0 |
| **Аудит C — арх+sec (v1.12)** | **14** | **14** (OPEN-И-13..26 закрыты v1.12) | 0 | 0 | 0 | 0 | 0 |
| **Plan 4 (Billing+CSV+Admin, v1.78)** | **7** | 0 | 0 | **7** (Биз-25..31 ⏸ deferred placeholders, см. §14) | 0 | 2 | 3 |
| **Итого продуктовых** | **87** | **71 ✅** | **5 🟦** | **11 ⏸** | **1 P0** | **5 P1** | **3 P2** + 2 P3 (Биз-29, Биз-30) |

Плюс 65 OPEN-вопросов в приложениях Д/Ж/З/И — большинство ждут юридической редактуры (см. раздел 8). *В v1.10 закрыт OPEN-И-12 (контакты эскалации) → 66 → 65.*

**Истинные P0-блокеры на 13.05.2026 (после v1.83):**

- **Б-1** (реквизиты юр. лица) — единственный продуктовый P0 от заказчика. От него зависят Диз-3, DO-2, DO-4. **Все 8 P0 аудита C закрыты в v1.12** (см. §13.10), триггер фазы 1 (`composer create-project laravel/laravel app`) разблокирован архитектурно. Plan 4 +7 Биз-25..31 — все ⏸ deferred placeholders за дефолтами в коде, не блокеры старта, но **требуются до production** (по spec §7.6 + §14).

**P2 после v1.82:** 3 open Биз — Биз-26 (email rate-limit zero_balance 1/час/tenant), Биз-27 (tenant видит все 7 ступеней vs только текущую), Биз-31 (pricing-tier-change при повышении цены — единая логика effective 1-е след. мес.). Все — Plan 4 deferred placeholders за дефолтами в коде, нужны до production. *Старая формулировка «все P2 закрыты» — устарела с v1.78.*

**P1 после v1.82 (5):** Диз-3 (ждёт Б-1), DO-2 (ждёт Б-1), DO-4 (ждёт Б-1), **Биз-25** (дефолтные tier-цены, ждёт первого платящего tenant), **Биз-28** (CSV-схема `/admin/report/index?type=49` поставщика, ждёт Plan 3 Tasks 1-2 BLOCKED credentials). Диз-1 закрыт 08.05.2026 через handoff Платона.

**P3 после v1.82 (2):** Биз-29 (CSV-window 25h — норма?), Биз-30 (drift threshold 5% — норма?) — оба ждут 1-2 мес. эксплуатации для empirical-based решения.

---

## 1. Юристу

| ID | Вопрос | Статус | Где |
|----|--------|--------|-----|
| ✅ Ю-1 | Impersonation через одноразовый код email тенанту, 15 минут, 5 попыток, 1 час сессия | Закрыто 03.05 | Прил. Г §4.2.1 |
| 🟦 Ю-2-доп | Согласие физлица на цепочку 8.2 — формулировка для договора с Поставщиком | Структурно (Прил. Ж §Б.7.3) | ждёт юриста |
| 🟦 Ю-3-юр | Юридическая сторона chargeback (24-месячное окно, четыре итога) | Структурно (Прил. Ж §А.9.4–9.5) | ждёт юриста |
| 🟦 Ю-4 | Уведомление РКН (форма 180) с матрицей 7 целей обработки | Структурно (Прил. З) | ждёт Б-1 → юриста → подачи. **DO-1 закрыт** (адрес ЦОД Yandex Cloud Москва известен) |
| 🟦 Ю-5 | Структурный шаблон публичной оферты + Политики конфиденциальности | Структурно (Прил. Ж) | ждёт юриста |
| ✅ **Ю-6** | **Cloudflare → Yandex CDN** (закрывается через DO-1) | **Закрыто 04.05** | narrative §22.9.4 |
| ✅ **Ю-7** | **Sentry → self-hosted в Yandex; Mailgun → Unisender Go (SMTP-relay)** | **Закрыто 04.05** | narrative §22 |
| 🟦 Ю-8 | Зеркальная гарантия в договоре с Поставщиком (надлежащее согласие физлиц) | Структурно (Прил. Ж §А.9.6) | юр. проверка действующего договора → допсоглашение |

**Доп. задача 04.05:** **OPEN-К-6** — DPA (Data Processing Agreement) с Yandex Cloud по 152-ФЗ ст.6 ч.3. Шаблон есть на странице соответствия Yandex Cloud — юристу проверить и подписать.

**Текущий блокер юриста:** ничего архитектурного, всё ждёт человеческой работы.

---

## 2. Бухгалтеру / финансам

| ID | Вопрос | Статус |
|----|--------|--------|
| **⏸ Б-1** | **Реквизиты юр. лица: ИНН, КПП, ОГРН, банковские реквизиты, адреса, ФИО директора, основание полномочий, система налогообложения, сканы печати/подписи** | **P0, ждёт регистрации ООО** |
| ✅ Б-2 | Формат выгрузки в 1С — **1С:Предприятие 8.3 + XML** | Закрыто 04.05 |
| ✅ Б-3 | Минимальная сумма пополнения — **100 ₽** | Закрыто 04.05 |
| ✅ Б-4 | Округление при ₽→лиды — **вниз** (5050₽ → 100 лидов + 50₽ остаток) | Закрыто 04.05 |
| ✅ Б-5 | Реестр УПД бухгалтеру — **XLSX по email раз в месяц** | Закрыто 04.05 |
| ✅ Б-6 | Промокоды и реферальная программа — **нет на MVP**, Post-MVP | Закрыто 04.05 |

---

## 3. CTO / архитектору

12 базовых вопросов закрыты (5 в сессии 03.05, 6 в сессии 04.05, CTO-12 в сессии 08.05). **CTO-18 открыт 09.05.2026** по аудиту 2026-05-09 P1-10 — auth+tenant middleware на `/api/deals` на pre-prod миграции (см. ниже под таблицей).

| ID | Решение | Где |
|----|---------|-----|
| ✅ CTO-1 | Смена тарифа в 00:00 МСК + cron `tariffs:apply-scheduled` | КС §3.4 |
| ✅ CTO-2 | RLS на MVP, 29 политик на 30 защищённых таблиц, 3 роли БД, `TenantAwareJob` | schema.sql §12 |
| ✅ CTO-3 | Pending транзакции: 30 мин soft + polling, 24ч hard timeout | state_machines §1 |
| ✅ CTO-4 | `users.notification_preferences` JSONB матрица 8×3 + `sound_enabled` BOOLEAN | schema.sql §4 |
| ✅ CTO-5 | RLS включён сразу, не отложен | schema.sql §12, §13 |
| ✅ **CTO-6** | **Retry отчётов из UI**: кнопка ↻ на failed, 3 попытки на исходную задачу, окно 7 дней, только владелец | Закрыто 04.05 |
| ✅ **CTO-7** | **Лимит одновременных отчётов на тенанта — 3** (DoS-защита) | Закрыто 04.05 |
| ✅ **CTO-8** | **Партиционирование журналов на пороге 1М строк** (триггер по миграции, не сразу) | Закрыто 04.05 |
| ✅ **CTO-9** | **PgBouncer на MVP сразу** (transaction pooling) | Закрыто 04.05 |
| ✅ **CTO-10** | **`report_jobs.file_deleted` не вводим**, статус `done` остаётся, наличие файла по `expires_at` | Закрыто 04.05 |
| ✅ **CTO-11** | **Кнопка «Отменить» pending-платежа в UI клиента — нет**, cron самовосстанавливается через 30 мин | Закрыто 04.05 |
| ✅ **CTO-12** | **Pest 4** (не PHPUnit). Обоснование: datasets лаконично покрывают 14 статусов воронки и 34 RLS-политики; architecture testing закрепляет правила («integration тесты не должны мокировать БД» — линт вместо памяти); родной для Laravel/Boost (один guideline вместо разрыва); Pest 4 backward-compat с Pest 3 syntax + бонус: browser testing без Dusk, stress, mutation v2 | Закрыт 08.05 (Pest 3), переоткрыт+закрыт 08.05 поздний вечер (Pest 4 — проверка на live-стеке). Импакт: [Tooling_v8_3.md §3.3](Tooling_v8_3.md), [CLAUDE.md §3.2](../CLAUDE.md). composer.json в app/: `pestphp/pest: ^4.7`. На `boost:install` отключить guideline PHPUnit, оставить Pest |
| ⏸ **CTO-18** | **auth+tenant middleware на `/api/deals` на pre-prod миграции.** На MVP `/api/deals` использует `tenant_id` query-param для tenant-resolution (`app/routes/web.php:103-110`). Sprint 1 Phase B (`56195c8`) применил `tenant` middleware к `/api/notifications`, `/api/reminders`, `/api/reports/jobs` (auth:sanctum-группы). `/api/deals` остаётся без auth для MVP-flow. На prod-миграции **обязательно** перейти на `auth:sanctum + tenant` middleware с резолюцией через subdomain или header `X-Tenant-Id`. Trigger закрытия: prod-миграция (после Б-1, регистрации ООО). Связанная находка: [docs/audit_2026-05-09.md](audit_2026-05-09.md) P1-10 | Открыт 09.05.2026 |
| ✅ **CTO-20** | **Регистронезависимый поиск кириллицей — починен на уровне БД.** Корень: dev-БД `liderra` была создана с `datcollate='C'` / `datctype='C'` — Postgres `lower()/upper()` не делает case-folding для кириллицы. Закрытие 12.05.2026: dev-БД пересоздана с `LOCALE_PROVIDER icu ICU_LOCALE 'und'` (PG 16+ feature, кросс-платформенно). Backup `app/.backups/liderra_pre_icu_2026-05-12.dump` (pg_dump custom format, 394KB). Восстановление через `php artisan migrate:fresh && php artisan db:seed --class=DemoSeeder`. После: `lower('СПб')='спб'`, `'Окна СПб' ILIKE '%сп%'` → true. **Точечный workaround в `ProjectController::index` откатан** в той же сессии — больше не нужен. Все 5 endpoint'ов с `ILIKE` (`AdminBillingController`, `AdminTenantsController`, `DealController`, `ProjectController`, `ProjectService`) теперь работают с кириллицей без правки кода. **Регрессионный тест** в [ProjectsListShowTest.php:116-141](../app/tests/Feature/Plan5/Projects/ProjectsListShowTest.php#L116-L141) (10/10 / 37 assertions). **Для prod-deploy:** при создании Managed PG БД использовать ту же команду — `CREATE DATABASE liderra WITH OWNER postgres ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'und' LC_COLLATE 'C' LC_CTYPE 'C' TEMPLATE template0;` (см. `db/00_create_roles.sql` для обновления). | Закрыт 12.05.2026 |
| ✅ **CTO-19** | **Иконочная система — Lucide migration через Vuetify custom IconSet (закрыто 13.05.2026 day +1).** В `app/resources/js/plugins/vuetify.ts` добавлен custom `IconSet` (`liderraLucideSet`) с **103-entry mapping `mdi-*` → Lucide components** (78 user-grep'нутых + 25 Vuetify-internal defaults: pagination chevrons, v-select dropdown arrows, v-checkbox squares, v-radio circles). Все 51 Vue/TS файл с `icon="mdi-*"` остаются untouched — mdi-имена работают как semantic-ID для Lucide рендеринга. `npm i lucide-vue-next ^1.0.0` selective tree-shakable imports. Fallback `HelpCircle` для unmapped. CLAUDE.md §2 «Иконки: Lucide» — бренд-spec compliance achieved. **Verification (13.05.2026 day +1):** Pest --parallel 742/739/0/3 ✅, Vitest 88 files / 683 passed + 3 skipped, Vite build 3.52s, axe-core a11y scan на `/admin/billing` 0 iconography violations. Visual smoke на 8 views: `/dashboard`, `/projects`, `/settings`, `/admin/billing`, `/no-such-404`, `/kanban`, `/deals`, `/admin/tenants` — все glyph'ы рендерятся (Bell, Search, ChevronLeft/Right, ChevronsLeft/Right, CreditCard, AlertTriangle, Users, UserCheck, Settings, RefreshCw, LayoutDashboard, ArrowLeft, Camera, ShieldCheck, Folder, Webhook, Puzzle, Clock, Square, etc.). **Spec:** docs/superpowers/specs/2026-05-13-cto-19-lucide-icon-migration-design.md. **Plan:** docs/superpowers/plans/2026-05-13-cto-19-lucide-icon-migration.md. **Через:** `superpowers:brainstorming` → `superpowers:writing-plans` → `superpowers:subagent-driven-development` (subagent для Task 1 install + Task 2 IconSet + Task 2.b extension; controller для visual smoke + closure). **Прежний контент:** Vuetify-конфиг создавал `<i class="mdi-...">` (default MDI), но CSS-пакет `@mdi/font` в `package.json` отсутствовал → `document.fonts` не содержит `Material Design Icons`, `::before content: none`. Затронуты все места с `prepend-icon`/`append-icon`/`icon`/`clearable` в Vue-views. Локальный workaround клиентский для clearable-крестика в `ProjectsView.vue` (`::after content '✕'`) применён 12.05.2026 — может быть убран в отдельной post-CTO-19 hygiene-task. | Закрыт 13.05.2026 |

---

## 4. Бизнесу / продакту

| ID | Вопрос | Статус |
|----|--------|--------|
| ✅ Биз-1 | 4 тарифа-заглушки (`start`/`basic`/`pro`/`enterprise`), цены = 1.00 ₽ | Закрыто 03.05 |
| ✅ Биз-2 | Кнопки «Отменить подписку» нет, уход через смену на `start` или удаление | Закрыто 03.05 |
| ✅ авто Биз-3 | Закрыт CTO-3 (pending → failed через 30 мин/24ч) | — |
| ✅ авто Биз-4 | Закрыт CTO-2 (RLS гарантирует изоляцию) | — |
| ✅ **Биз-5** | **Helpdesk — JivoSite** (внешний JS-snippet, ~990₽/мес/оператор) | Закрыто 04.05 |
| ✅ **Биз-6** | **Сбор скриншотов crm.bp-gr.ru — режим «по запросу»** (15 партий аудита уже сделано 04–05.05, отчёты в `Analiz_originala_v8_3.md` v1.1 и `Аудит_partii_12_15_originala_v8_3.md`). Открывать конкретный неисследованный раздел при необходимости — биллинг, КЦ-диалоги, Голосовой Робот | Закрыто 06.05 (v1.10) |
| ✅ **Биз-7** | **Восстановление отвергнутых лидов — лайт-версия**: автоматическое при пополнении, без UI лога в кабинете клиента (только в админке SaaS). +0.5–0.7 спринта | Закрыто 04.05 |
| ✅ авто Биз-8 | Закрыт через Ю-1 (impersonation 15 мин код) | — |
| ✅ **Биз-9** | **2FA доступна на всех тарифах** (снято с фичи `pro+`) | Закрыто 04.05 |

---

## 5. Дизайнеру / маркетингу

| ID | Вопрос | Статус |
|----|--------|--------|
| ✅ **Диз-1** | **HTML-прототипы покрыты handoff Платона v8 Forest** (13 экранов в [web/v8/](../web/v8/) > 8 запланированных + 3 экрана ошибок: login/dashboard/deals/deal_card/kanban/billing/settings/reports/errors/admin/landing/brand/palette_options). Прил. Л — НЕ выпускается (избыточно после handoff'а). Точечные расхождения handoff vs ТЗ зафиксированы в шапке v1.13: статусы воронки, click-wrap, SSO — реализовывать по ТЗ/schema. axe: 81 violation на 10/13 (color-contrast на декоративных separator'ах), исправлять при реализации в Vue+Vuetify | Закрыто 08.05.2026 |
| ✅ **Диз-2** | **Название: Лидерра. Логотип «Поток» (3 круга, бирюзовая палитра). Брендбук v1.0** | Закрыто 04.05 (см. brandbook.md) |
| ⏸ Диз-3 | Контакты подвала (email, телефон, соцсети) | P1, ждёт Б-1 |
| ✅ **Диз-4** | **Контент лендинга, FAQ — заглушки «Lorem ipsum»** на MVP, реальный контент к запуску от маркетолога | Закрыто 04.05 |

---

## 6. DevOps / эксплуатации

| ID | Вопрос | Статус |
|----|--------|--------|
| ✅ **DO-1** | **Облачный провайдер — Yandex Cloud, регион ru-central1 (Москва)** | Закрыто 04.05, см. Прил. К |
| ⏸ DO-2 | Основной домен `liderra.ru` + wildcard SSL | P1, регистрация после Б-1 (~200–500₽/год) |
| ✅ **DO-3** | **IP allow-list для админки SaaS — НЕТ. Вместо: 5 компенсирующих мер** (rate-limit /admin/login 5 попыток/15 мин с IP, captcha после 2 неудач, email-уведомление при новом устройстве/IP, `auth_log` со всеми попытками, Sentry/Slack-алерт >20 неудач/час с разных IP) | Закрыто 04.05 |
| ⏸ DO-4 | Список сотрудников SaaS с ролями (`super_admin`, `finance`, `support`, `compliance`, `dev_oncall`) | P1, ждёт Б-1 |
| ✅ **DO-5** | **SSO для админов SaaS — Yandex 360** (нативно к Yandex Cloud) | Закрыто 04.05 |

---

## 7. OPEN-К — облачная инфраструктура (новый раздел из Прил. К)

| ID | Вопрос | Статус |
|----|--------|--------|
| ✅ **OPEN-К-1** | **Грант Cloud Boost — отложено до стабилизации** (после первых клиентов оценим, есть ли смысл подавать заявку до 1 млн ₽) | Закрыто 06.05 (v1.10) |
| ✅ OPEN-К-2 | **План A — Yandex Cloud** | Закрыто = DO-1 |
| ✅ OPEN-К-3 | **Single-AZ + multi-AZ-ready фундамент**: 3 подсети, параметризованный Terraform, бэкапы в Object Storage. Миграция на multi-AZ при росте — 3–5 дней без даунтайма | Закрыто 04.05 |
| ✅ OPEN-К-4 | **Backup-стратегия**: PITR + Object Storage внутри Yandex (без cross-cloud) | Закрыто 04.05 |
| — OPEN-К-5 | Внешний CDN — **отпадает**, т.к. Yandex CDN нативный | Снято автоматически |
| ✅ OPEN-К-6 | **DPA с Yandex Cloud — задача юристу** (152-ФЗ ст.6 ч.3) | Закрыто как задача → юристу |
| ✅ **OPEN-К-7** | **CVoS (Yandex commitment, скидка до 22%) — отложено до стабилизации нагрузки.** До прогноза стабильного MAU/load — pay-as-you-go | Закрыто 06.05 (v1.10) |
| ✅ OPEN-К-8 | **RPO=1ч, RTO=4ч** (single-AZ + хорошие бэкапы укладывается с запасом) | Закрыто 04.05 |

---

## 8. OPEN-вопросы внутри приложений

### 8.1. Прил. Д (Workflow_pd_subject_requests)

| ID | Вопрос | Статус |
|----|--------|--------|
| ✅ **OPEN-Д-1** | **Ограничение обработки ПДн — BOOLEAN флаг `processing_restricted` в `pd_subject_requests`** (ст.21 152-ФЗ) | Закрыто 04.05 |
| ✅ **OPEN-Д-5** | **Создаём таблицу `incidents_log`** для журнала инцидентов | Закрыто 04.05 (см. OPEN-И-1) |
| OPEN-Д-2..4, 6..20 | 18 вопросов юр. редактуры | Ждут юриста |

### 8.2. Прил. Ж (Oferta_i_Politika)

| ID | Вопрос | Статус |
|----|--------|--------|
| ✅ **OPEN-Ж-3** | **Способ принятия оферты — Click-wrap** (3 чекбокса при регистрации: оферта / Политика / согласие на ПДн) | Закрыто 04.05 |
| OPEN-Ж-1, 2, 4..20 | 19 вопросов юр. редактуры (формулировки, сроки, реквизиты) | Ждут юриста |

### 8.3. Прил. З (Uvedomlenie_RKN)

| ID | Вопрос | Статус |
|----|--------|--------|
| OPEN-З-1..14 | 14 вопросов: ИНН, ОГРН, КПП, юр. адрес, ФИО руководителя, дата начала обработки и др. | Ждут Б-1 + юриста, **DO-1 уже закрыт** (адрес ЦОД Yandex Cloud Москва известен) |

### 8.4. Прил. И (Runbook_ekspluatatsii)

| ID | Вопрос | Статус |
|----|--------|--------|
| ✅ **OPEN-И-1** | **Создать таблицу `incidents_log`** | Закрыто 04.05 (вместе с OPEN-Д-5) |
| ✅ **OPEN-И-2** | **Внешние CRM — Уровень 1 (webhook outbound) на MVP + архитектурный задел + amoCRM-коннектор в спринте 14–15** (Bitrix24, RetailCRM — Post-MVP) | Закрыто 04.05 |
| OPEN-И-3..10 | 8 вопросов эксплуатационных деталей (периодичность ревью, game days, шаблон уведомления РКН, резервный шлюз, status page, регламент пост-мортемов) | Ждут Б-1 + DO-4 |
| ✅ **OPEN-И-11** | **Миграции `crm_connections` / `crm_field_mappings`** (DDL-часть OPEN-И-2) | Закрыто 04.05 в составе schema.sql v8.2 |
| ✅ **OPEN-И-12** | **Контакты эскалации — Notion private / 1Password DevOps** до закрытия DO-4. После DO-4 (список сотрудников SaaS) — миграция в общий 1Password vault. *Был `OPEN-И-2` в v8.1, переименован 05.05 (v8.3.1).* | Закрыто 06.05 (v1.10), будет ревизировано после DO-4 |

### 8.5. Frontend tooling (новый раздел, Sprint 1 hygiene)

| ID | Вопрос | Статус |
|----|--------|--------|
| ⏸ **OPEN-FE-1** | **Histoire ↔ Vite 8 миграционный долг.** Histoire 1.0-beta.1 заявляет peerDep `vite ^7`, у нас Vite 8. Установлен через `npm install --legacy-peer-deps` в `app/`. Smoke-test (`histoire build`) пройден; Vuetify auto-import регистрируется через `setupFile`. Не блокирует разработку. Trigger закрытия: релиз Histoire с peerDep `vite ^8` (отслеживать [github.com/histoire-dev/histoire/releases](https://github.com/histoire-dev/histoire/releases)). Связанная находка: [docs/audit_2026-05-09.md](audit_2026-05-09.md) P1-09 | Открыт 09.05.2026 |

---

## 9. Что я (Claude) должен сделать дальше

| Задача | Файл | Приор. | Сложность |
|--------|------|--------|-----------|
| ✅ **Прил. М v1.0 — Анализ оригинала и архитектурные следствия (партии 1–11)** | `Analiz_originala_v8_3.md` | — | Сделано 04.05 (~40 мин) |
| ✅ **Прил. М v1.1 — расширение по партиям 12–15** | `Analiz_originala_v8_3.md` | — | Сделано 05.05 (~25 мин) |
| ✅ **Открытые_вопросы v1.6** — раздел 12 с Биз-14/15/16 | `Открытые_вопросы_v8_3.md` | — | Сделано 05.05 (~15 мин) |
| ✅ **Патч schema.sql v8.2** — `processing_restricted` (OPEN-Д-1) + `incidents_log` (OPEN-И-1) + `suppliers` + `project_suppliers` + миграция `supplier_lead_costs` (Прил. М §3.1) + 6 новых полей в `projects` + `project_limit_adjustments` (Прил. М §3.2) | schema.sql v8.1 → v8.2 | — | Сделано 04.05 (~50 мин) |
| **Патч schema.sql v8.3** — перепись `reminders` (минимальный паритет с `histories[]`), удаление `deals.reminder_text/reminder_at`, расширение `suppliers` capabilities, `tenants.desired_daily_numbers`, 3 ключа в `system_settings` (Прил. М §3.5) | schema.sql v8.2 → v8.3 | **P0** | средняя (~30 мин) |
| **Прил. Л — HTML/CSS/JS прототипы 8 экранов + 3 ошибок** | новый | P1 | большая (~30 мин) |
| Полный апдейт narrative по 30 решениям + выводам аудита (партии 1–15) → v8.4. Разделов: §1, §5, §7, §8, §9, §12, §14, §17, §18.5, §19, §22, §23.10, §26 (детали в Прил. М §4 + §9.6) | Инструкция_v8_3 → Инструкция_v8_4 | P1 | очень большая (несколько сессий) |

---

## 10. Что осталось от заказчика

**Истинный P0 — один:** **Б-1** (реквизиты юр. лица). Когда зарегистрируется ООО → автоматически разблокируются Диз-3, DO-2, DO-4, OPEN-З-*.

> **Связанная находка аудита 2026-05-09 (P1-11):** `/api/admin/*` маршруты в `app/routes/web.php:74-99` без auth — тот же блокер. Saas-admin SSO Yandex 360 ⏸ ждёт Б-1; до этого момента admin-endpoints на MVP без middleware. Закрывается одновременно с Б-1.

**Параллельно (юристу — независимо от Б-1):**

- **Прил. Ж** — редактура оферты + Политики (19 OPEN-Ж-* без формулировок).
- **Прил. З** — финализация уведомления РКН (14 OPEN-З-*, ждут Б-1 + юриста).
- **OPEN-К-6** — DPA с Yandex Cloud по 152-ФЗ ст.6 ч.3.
- **Ю-8** — зеркальная гарантия Поставщика (допсоглашение к договору с crm.bp-gr.ru).

**Что Claude может делать сейчас (не блокировано):**

- ✅ Диз-1 закрыт 08.05.2026 — handoff Платона покрыл HTML-прототипы (13/8). Прил. Л не выпускается.
- ✅ Narrative v8.4 завершён 06.05.2026 (13/13). Архив v8.4 финал.
- ✅ Аудит C закрыт 07.05.2026 — все 27 решений зафиксированы в §13.10 реестра v1.12.
- ✅ CTO-12 закрыт 08.05.2026 (поздний вечер) — **Pest 4** (переоткрыт с Pest 3 после live-проверки на стеке, backward-compat подтверждён). См. §3.
- Гигиена v8.5: запись v8.5 в `Объединённый_конспект.md` (отложена), ER-диаграммы Mermaid под schema v8.5 (отложены).
- Обратная связь Платону по 4 расхождениям handoff vs ТЗ (статусы / click-wrap / SSO / axe).
- При триггере фазы 1 (`composer create-project`) — `boost:install` по Прил. Н §10.1 (теперь с Pest 3).

**Все P2 закрыты:** 7 P2 из v1.10 (Биз-10..16, OPEN-К-1, К-7) + 7 P2 из v1.12 (Биз-23/24, OPEN-И-22..26). Из 78 продуктовых: **69 ✅ + 5 🟦 структурно + 4 ⏸** (Б-1 P0; Диз-3, DO-2, DO-4 — все P1 ждут Б-1).

---

## 11. Аудит crm.bp-gr.ru — новые продуктовые вопросы (04.05.2026)

**Контекст:** 04.05.2026 проведён read-only аудит crm.bp-gr.ru (11 партий, 2 повторных захода). Полные выводы — в **Прил. М** (`Analiz_originala_v8_3.md`). Здесь — только новые продуктовые вопросы, требующие решения заказчика.

**Все 4 вопроса имеют разумные дефолты — разработка не блокирована.**

| ID | Вопрос | Решение | Спринт | Статус |
|----|--------|---------|--------|--------|
| ✅ **Биз-10** | **Модель «задач» сделки** — отдельная таблица `reminders` с минимальным паритетом (`text + remind_at + created_by + assignee_id NULL + completed_at + is_sent + sent_at`). Без приоритетов / каналов / recurrence. Расширения — Post-MVP. *⚠ Был переоткрыт в v1.6 после партии 12 — гипотеза «1 напоминание = 1 deal» опровергнута, `histories[]` массив.* DDL — Прил. М v1.1 §3.5.1. | Закрыто 06.05 (v1.10) | спринт 5 | ✅ |
| ✅ **Биз-11** | **Один кошелёк в рублях** — наша упрощённая модель vs 4 счётчика оригинала (ГЦК / Диалоги КЦ / Минуты / Автозвонок). 4 кошелька — наследие телекома, у нас одна операция (приём лида) | Закрыто 06.05 (v1.10) | спринт 7 | ✅ |
| ✅ **Биз-12** | **Телефонная интеграция — Post-MVP**, на MVP не делаем. На MVP достаточно outbound webhook (OPEN-И-2). Добавляем при первом запросе клиента после запуска. | Закрыто 06.05 (v1.10) | Post-MVP | ✅ |
| ✅ **Биз-13** | **Magic-link 24ч для менеджеров — делаем на MVP** (~0.3 спринта). Админ тенанта генерирует одноразовую ссылку для менеджера-подрядчика, тот заходит без пароля. Явное преимущество для подрядных колл-центров. | Закрыто 06.05 (v1.10) | спринт 11 | ✅ |

### 11.1. Что подтвердил аудит (наши решения корректны)

- ✅ 14 фиксированных статусов, 8 переименовываются → совпадает с нашей моделью.
- ✅ Outbound webhook = конкурентное преимущество (в оригинале нет, окончательно подтверждено партией 10).
- ✅ Реселлерская модель Ю-2 (мы покупаем у crm.bp-gr.ru) — корректна.
- ✅ Стек Vue 3 + Vuetify 3 + Laravel 11 — правильное решение, оригинал использует «лоскутный» Vue 2 + Vuetify 1.5 + Element UI + jQuery + Yii2.

### 11.2. Что меняется в нашей модели данных (через schema.sql v8.2)

Подробно — в Прил. М §3. Кратко:

- Новая сущность **«Поставщик»** (`suppliers` + `project_suppliers`) — раскрытая в партии 10 архитектура B1/B2/B3 как уровень над проектом.
- **Динамические лимиты проектов** (`projects.daily_limit_target`, `effective_daily_limit_today` + сервис `EffectiveLimitCalculator`) — автоматическое снижение лимита при нехватке баланса (партия 10.6).
- **5 новых полей в `projects`**: `daily_limit_target`, `effective_daily_limit_today`, `effective_limit_calculated_at`, `region_mask` + `region_mode`, `delivery_days_mask` — паритет с карточкой проекта оригинала (партия 10.3).
- Новая таблица **`project_limit_adjustments`** — лог автокоррекций лимитов.

### 11.3. Что меняется в narrative (через v8.4)

10 разделов с правками: §1 (преимущества), §5 (формат ingress), §7 (новые сущности), §8 (free state machine), §9 (карточка проекта), §12 (Конверсия проектов), §14 (полный аудит мутаций), §17 (тихие часы), §22 (защита от prompt injection), §26 (стек). Подробно — Прил. М §4.

### 11.4. Что фиксируется как наше конкурентное преимущество

В маркетинговых материалах и шапке narrative:

1. **Outbound webhook к тенанту** — оригинал только принимает.
2. **Полный аудит мутаций сделки** — оригинал не логирует смену ответственного / проекта / телефона / суммы.
3. **Kanban-доска с drag-and-drop** — у оригинала только табличный вид.
4. **Real-time push-уведомления** — у оригинала обновления только перезагрузкой страницы.
5. **Дашборд с настраиваемыми виджетами** — у оригинала фиксированный дашборд с 5 KPI.
6. **API для CRM-интеграций (amoCRM, Bitrix24)** — у оригинала нет.

### 11.5. Безопасность — prompt injection в DOM CRM (важная находка)

В DOM каждой страницы crm.bp-gr.ru обнаружен элемент с явным таргетингом на агентов-Claude:

```html
<div id="claude-agent-stop-container">
  <button id="claude-agent-stop-button"><span>Stop Claude</span></button>
</div>
```

**Источник не установлен.** Возможные версии: расширение браузера заказчика / тестовая разметка владельцев CRM / враждебная XSS-инъекция / скомпрометированный сторонний скрипт. Версия про HelpDeskEddy отвергнута.

**Действия Claude в Chrome:** ✅ корректные — кнопку не нажимал, текстовую команду не выполнял, доложил пользователю. Все системные правила защиты от prompt injection соблюдены.

**Что нужно сделать заказчику (необязательное, но желательное):**

1. Проверить расширения браузера в том профиле, где открывается crm.bp-gr.ru. Версия «расширение» — наиболее вероятная.
2. Если расширение не нашлось — открыть DevTools → Sources → найти, в каком файле создаётся `claude-agent-stop-container`.
3. При враждебной инъекции в коде CRM — сообщить владельцам crm.bp-gr.ru.

**Что мы делаем у себя:** narrative v8.4 §22 пополняется разделом про CSP и санитизацию пользовательского ввода (DOMPurify), запрет любых `claude-*` / `gpt-*` / `agent-*` маркеров в нашем DOM.

---

## 12. Аудит crm.bp-gr.ru — параллельные партии 12–15 (05.05.2026)

Раздел добавлен в v1.6. Источники: `audit-batch-12-2026-05-05.md` … `audit-batch-15-2026-05-05.md` + Прил. М v1.1 (раздел 9).

### 12.1. Сводка партий 12–15

| Партия | Что исследовано | Ключевая находка |
|---|---|---|
| 12 | Конверсия проектов + Напоминание + Досье | **Биз-10 переоткрыт** — `histories[]` массив, нужна отдельная таблица |
| 13 | Рекомендации источников + Настройки + Просмотреть + Звонки | **B1/B2/B3 capabilities жёстко подтверждены** цитатой UI; новое поле `desired_daily_numbers` |
| 14 | Редкие статусы + KB + Безопасность профиля | Оригинал не имеет 2FA / journal / sessions — **наше преимущество**; антипаттерн plaintext-пароль |
| 15 | Карточки 12 интеграций «API ▾» | **Outbound webhook'ов нет** — окончательно (7 линий доказательств); антипаттерн API-key в `<input type="text">` |

### 12.2. Биз-10 переоткрыт — модель напоминаний

**Исходная гипотеза (v1.5):** одно напоминание на сделку — поля `deals.reminder_text` + `deals.reminder_at`.

**Опровергнуто партией 12.2:** в оригинале `model.histories[]` содержит записи `type='reminder'` — массив. Технически множество поддержано, фактически в UI обычно одно (UX-конвенция).

**Новая формулировка:** какую модель напоминаний делаем — отдельную таблицу с минимальным паритетом или сохранить простую модель?

**Рекомендация Claude:** **отдельная таблица `reminders`** с полями минимального паритета:

```
reminders {
  id, tenant_id, deal_id,
  text VARCHAR(255),
  remind_at TIMESTAMPTZ,
  created_by FK→users (= histories[].from в оригинале),
  assignee_id FK→users NULL (= histories[].to, на UI не правится),
  completed_at TIMESTAMPTZ NULL (наше расширение для дашборда),
  is_sent / sent_at (для cron нотификатора)
}
```

Поля паритета: `text + remind_at + created_by + created_at`. Без приоритетов, каналов, ассайнмента, recurrence. Дашборд задач достигается фильтрами `?reminders=today|last|future|none` (как в оригинале).

**Дефолт:** реализуем как описано выше — см. Прил. М v1.1 §3.5.1 для полного DDL и §9.1.2/9.1.5 для обоснования. На MVP UI — без расширений сверх паритета, потом по запросу.

### 12.3. Биз-14 — TTL для soft-deleted проектов ✅ закрыто

**Контекст:** в оригинале (партия 13.2.3) экран «Удалённые проекты ГЦК» показывает проекты, удалённые **минимум 96 дней назад**. У нас в schema v8.2 поле `projects.deleted_at` есть, но retention policy не определена.

**Решение (закрыто 06.05 в v1.10):** **180 дней (6 месяцев) + cron `projects:purge-deleted`, выключен по умолчанию** до Б-1 (реквизиты ООО). Включается админом через `system_settings.projects_purge_deleted_enabled = true` после юридического согласования. До включения cron — все «удалённые» живут в БД бессрочно.

См. Прил. М §3.5.4. Возможна корректировка срока до 90/120/360 дней при юридической редактуре (152-ФЗ) — отдельный пересмотр после Б-1.

**Статус:** ✅ закрыто 06.05.2026 (v1.10).

### 12.4. Биз-15 — Wizard «Рекомендации источников» (OSINT) ✅ закрыто

**Контекст:** в оригинале (партия 13.1) есть полноценная фича `/admin/gck/dop-sources` — 3-шаговый wizard для самостоятельного поиска новых источников трафика клиентом. **OSINT-инструмент**, не управление поставщиками.

**Решение (закрыто 06.05 в v1.10):** **не делаем на MVP**, в Post-MVP при запросе клиента. Аргументы:

- Спорная бизнес-ценность для арбитражной модели «получи лиды от B1/B2/B3».
- Реализация требует внешнего OSINT API (~3 спринта).
- Если делать — отдельные таблицы: `source_recommendation_searches`, `source_recommendation_results`, `source_recommendation_blacklist`.

**Статус:** ✅ закрыто 06.05.2026 (v1.10).

### 12.5. Биз-16 — Поле `tenants.desired_daily_numbers` ✅ закрыто

**Контекст:** в оригинале (партия 13.2.2) есть аккаунтная (не проектная!) модалка «Желаемое кол-во номеров в день». Это ориентир для бэк-офиса ГЦК, не лимит.

**Решение (закрыто 06.05 в v1.10):** **добавляем** как `tenants.desired_daily_numbers INT NULL`. Полезный сигнал саппорту Лидерры для проактивных предложений. Затраты ~0.1 спринта.

См. Прил. М §3.5.3. В админке `/admin/tenants` появляется колонка «Желаемое × факт сегодня».

**Статус:** ✅ закрыто 06.05.2026 (v1.10), реализуется в спринте 1 вместе с базовой схемой.

### 12.6. Изменения в schema.sql v8.3 по партиям 12–15

| Изменение | Тип | Источник |
|---|---|---|
| `reminders` — новая модель (text, remind_at, created_by, assignee_id, completed_at, is_sent, sent_at) | DROP+CREATE | Партия 12.2, Биз-10 |
| `deals.reminder_text`, `deals.reminder_at`, `idx_deals_reminder` | DROP COLUMN/INDEX | Партия 12.2, Биз-10 |
| `suppliers` +5 полей capabilities (channel, supports_sender_name, supports_keyword, supports_csv_upload, supports_domains_list) | ADD COLUMN | Партия 13.3.5 |
| Seed `suppliers` для B1/B2/B3 | UPDATE | Партия 13.3.5 |
| `tenants.desired_daily_numbers INT NULL` | ADD COLUMN | Партия 13.2.2, Биз-16 |
| `system_settings` +3 ключа cron purge-deleted | INSERT | Партия 13.2.3, Биз-14 |
| `system_settings.schema_version` → '8.3' | UPDATE | — |

Полный DDL — Прил. М v1.1 §3.5.

### 12.7. Дополнения в narrative для v8.4 (по партиям 12–15)

- **§1 (Конкурентные преимущества):** добавить «у оригинала нет 2FA, нет журнала входов, нет списка сессий» (партия 14) — security как differentiator.
- **§7 (Сущности):** обновить `reminders` (новая форма), `suppliers` (capabilities), `tenants.desired_daily_numbers`.
- **§9 (Карточка проекта):** условные поля по supplier capabilities (sender_name только для B2/B3, keyword только для B2).
- **§12 (Дашборд и аналитика):** уточнить экран «Конверсия проектов» — 17 колонок, monotone per column, формат `N (XX.XX%)`, нет drill-down/экспорта.
- **§17 (Напоминания и тихие часы):** уточнить формат «Тихих часов» (start_hour/end_hour 0..23 + общий timezone + минимум 3 часа).
- **§18 (Профиль):** новая вкладка «Безопасность» (Пароль / 2FA / История входов / Активные сессии / Подозрительная активность).
- **§19 (REST API):** outbound webhook как наше уникальное преимущество (7 линий доказательств).
- **§22 (Безопасность):** расширение про prompt injection. Plus антипаттерны оригинала: пароль plaintext, API-key в `<input type="text">`.
- **§23 (Админка SaaS):** колонка «Желаемое × факт сегодня» в `/admin/tenants` (Биз-16).
- **§26 (Дизайн-система):** маскирование credentials в формах интеграций (input type="password" + toggle).

### 12.8. Безопасность — антипаттерны оригинала, найденные в партиях 12–15

**Партия 14 — пароль plaintext в HTML-форме:**

- На странице `/admin/user/account` поле «Текущий пароль» — `<input type="text">` с заполненным реальным значением. XSS-уязвимость на странице профиля → угон пароля без подтверждения.
- **У нас:** отдельная защищённая форма «Старый/Новый/Подтверждение» с rate-limit, поля `type="password"`. См. Прил. М §6.6.

**Партия 15 — API credentials в `<input type="text">`:**

- Во всех 5 открытых карточках интеграций (Bitrix, amoCRM, Скорозвон, Unisender, Мои Звонки) поля `api_key`, `client_secret`, `token` — `<input type="text">`. При просмотре заполненной карточки админом другие люди в комнате видят токены на экране.
- **У нас:** все credentials — `type="password"` + кнопка-toggle «Показать» (eye-icon). См. Прил. М §6.6.

**Партии 12–15 — повторное подтверждение prompt injection:**

- В каждой партии найдены DOM-маркеры `claude-agent-stop-container`, `claude-agent-stop-button`, `claude-agent-glow-border`, `claude-agent-animation-styles` (с `@keyframes claude-pulse`).
- В партии 15 особо отмечено наличие `claude-agent-glow-border` (наклеивающий рамку вокруг элементов) — гипотеза «расширение браузера» усилилась (это паттерн UI-extension'ов, не XSS-инъекции).
- **Действия Claude во всех 4 партиях:** ✅ корректные — кнопку не нажимал, инструкцию не выполнял, доложил.

---

## 13. Аудит C — архитектура + security (07.05.2026)

**Что:** после закрытия аудитов D (P0-блокеры миграции, коммит `7619ac0`) и B (~85 правок P0+P1, 5 коммитов `f1ea6a1` → `574154b`) проведён глубокий аудит **C** — архитектурно-функциональный обзор + STRIDE threat modeling. Запускались 2 независимых read-only general-purpose агента: **C-1** (CRM-best-practices vs narrative v8.4 для pay-per-lead-сегмента РФ) и **C-2** (security: 4 потока × STRIDE + 5 осей: RLS-дыры, secret rotation, audit-логи, DDoS, PII).

**Результат:** **27 новых открытых вопросов** (8 P0, 12 P1, 7 P2). Закрытий нет — правило §2.2 («не закрывать без явного «закрываем» от заказчика»). Сводка обновлена в §0.

### 13.1. Сводка по адресатам

| Адресат | Всего | P0 | P1 | P2 |
|---------|-------|----|-----|-----|
| Бизнес / продукт | 8 | 3 | 3 | 2 |
| CTO / архитектор | 4 | 1 | 3 | 0 |
| OPEN-И (DevOps + security) | 14 | 4 | 5 | 5 |
| Юрист | 1 | 0 | 1 | 0 |
| **Итого новых** | **27** | **8** | **12** | **7** |

### 13.2. Бизнес-владельцу (продуктовые)

| ID | Вопрос | Приор. | Вариант A (рекомендуем) | Вариант B | Вариант C |
|---|---|---|---|---|---|
| **Биз-17** | Стратегия автораспределения лидов (`projects.assignment_strategy`) | **P0** | `manual` (ручное) — паритет с оригиналом, минимум логики | `round_robin` по `users.is_active=TRUE` в проекте | `least_loaded` по числу открытых сделок у менеджера |
| **Биз-18** | Time To First Response (TTFR) SLA на проект | **P0** | 15 мин default + alert менеджеру/админу | Жёсткий SLA = 5 мин с эскалацией супервайзеру | Без SLA в MVP, метрика только на дашборде §12.5.5 |
| **Биз-19** | Антифрод-политика дублей по `phone` | **P0** | Окно 24 ч, дубль помечается `duplicate_of_id` + НЕ списывает лид | Окно 7 дней, дубль возвращается на баланс | Без дедупликации в MVP — клиент платит за каждый webhook |
| **Биз-20** | Telegram-бот для нотификаций менеджерам | P1 | Делаем в спринте 9 (после биллинга) | Только email + push в MVP, Telegram Post-MVP | SMS через Unisender Go в MVP вместо Telegram |
| **Биз-21** | Маркетинг-конверсии Я.Директ Offline + VK Ads Goals | P1 | Шаблон события `marketing.conversion` через outbound webhook §19.10 | Нативный коннектор Я.Директ + VK (как amoCRM в спринте 14–15) | Только generic outbound — клиент сам пишет адаптер |
| **Биз-22** | Lead scoring модель | P1 | Простая: `score = supplier_quality × time_in_form` (3 поля, без ML) | Полная скоринг-модель с обучением на исторических данных (Post-MVP) | Без скоринга в MVP — менеджер сам приоритизирует |
| **Биз-23** | Гео-таргетинг сделок (`deals.region_code`, `deals.city`) | P2 | Поля + автоопределение по `phone` префиксу + filter в §10.3 | Поля только если приходят в webhook (без enrichment) | Не делаем — оставляем `projects.region_mask` как сейчас |
| **Биз-24** | Нотификация саппорту о просрочке `waiting_payment` → `paid` | P2 | Алерт через 48 ч в админке + email finance-роли | Алерт через 7 дней | Без алерта — менеджер сам отслеживает |

### 13.3. CTO / архитектору

| ID | Вопрос | Приор. | Вариант A (рекомендуем) | Вариант B | Вариант C |
|---|---|---|---|---|---|
| **CTO-13** | Тест поведения `SET LOCAL app.current_tenant_id` через PgBouncer transaction-pooling | **P0** | Обязательный e2e-тест в спринте 1: auto-commit, reuse соединения, job retry — каждый кейс должен либо успешно установить tenant_id, либо явно упасть с exception (не молча → utечь) | Перевести pooler в session-mode для tenant-сессий (пожертвовать производительностью) | Использовать `SET ROLE` вместо `SET LOCAL` (требует CREATE ROLE per tenant — не масштабируется) |
| **CTO-14** | UTM-поля в `deals` для когортной аналитики (`utm_source/medium/campaign/content`) | P1 | 4 × VARCHAR(100) NULL в `deals` + индекс по `(tenant_id, utm_source)` (с осторожностью — partitioned) | Отдельная таблица `deal_attributions` (1:1 с deals, не партиционированная) | Не добавляем — UTM не приходят в webhook от bp-gr.ru |
| **CTO-15** | Two-person approval для impersonation в тенант с активным `pd_subject_request.processing_restricted=TRUE` или `chargeback_unrecovered_rub > 0` | P1 | Требовать второго админа уровня `compliance` для подтверждения по email | Запретить полностью impersonation для таких тенантов | Оставить как сейчас — single admin click, audit постфактум |
| **CTO-16** | Skill-based / regional routing — связь `users` ↔ `projects` | P1 | Новая таблица `project_user_assignments(project_id, user_id, skills JSONB)` для round-robin pool в проекте | `users.skills JSONB` + `projects.required_skills JSONB`, выбор по intersection | Не делаем — все менеджеры тенанта могут видеть все проекты (как сейчас) |

### 13.4. OPEN-И — DevOps / security

| ID | Вопрос | Приор. | Вариант A (рекомендуем) | Вариант B | Вариант C |
|---|---|---|---|---|---|
| **OPEN-И-13** | Yandex 360 SSO для админов: SAML/OIDC flow, JIT-provisioning vs ручной CRUD `saas_admin_users`, выключение локального 2FA при SSO, fallback при недоступности IDP | **P0** | OIDC + JIT-provisioning + локальный 2FA выключен, fallback — break-glass-аккаунт `super_admin` с force-2FA | SAML + ручной CRUD + локальный 2FA остаётся всегда | Без SSO в MVP, только email/password + 2FA TOTP (отказ от DO-5) |
| **OPEN-И-14** | RLS `WITH CHECK` на INSERT для `deal_tag_pivot`, `saas_invoice_items` + явный `REVOKE` saas-уровневых таблиц от `crm_app_user` | **P0** | Добавить `WITH CHECK` к существующим политикам + REVOKE на `saas_admin_*`, `incidents_log`, `pd_subject_requests`, `impersonation_tokens`, `auth_log`-admin-строки | Только `WITH CHECK`, GRANT'ы оставить | Не делаем — полагаемся на application-уровень |
| **OPEN-И-15** | Append-only constraint + hash chain для `auth_log`, `activity_log`, `pd_processing_log`, `saas_admin_audit_log`, `balance_transactions` | **P0** | Триггер `BEFORE UPDATE/DELETE → RAISE EXCEPTION` + `log_hash = sha256(prev.log_hash \|\| current_row)`. Отдельная роль `crm_audit_writer` (только INSERT) | Только триггер без hash chain | Полагаться на pg_audit (фаза 3) — отложить до prod |
| **OPEN-И-16** | PII-scrubbing в Sentry (`before_send`/event_processor) для phone/email/totp_secret/api_key, regex + whitelist полей | **P0** | Whitelist + regex маска для `phone`, `email`, `password`, `secret`, `token`, `api_key` во всех контекстах (request, breadcrumbs, exception args) | Только regex без whitelist (риск: новые поля попадут в Sentry) | Sentry self-hosted = «свои данные», не маскируем |
| **OPEN-И-17** | Автоматическая ротация секретов: cron + email-уведомления за 30 дней до expiry | P1 | TTL 365 дней: `webhook_token`, `api_keys`, `outbound.secret_hash`. Cron `secrets:notify-expiring` + UI «продлить» | TTL только для `api_keys`, `webhook_token` остаётся бессрочным | Без TTL — как сейчас, ротация только вручную при компрометации |
| **OPEN-И-18** | DNS-rebinding защита для outbound webhook (resolve→pin→connect) + лимит 3 redirect, max body 1 MB | P1 | resolve once, pin IP, blacklist RFC1918 на pinned IP, connect через pinned. Доверенный CA-bundle | Использовать pre-validated DNS-сервис (например, Yandex DNS) с whitelisting | Не делаем — Laravel HTTP client достаточно безопасен |
| **OPEN-И-19** | Лимит количества `api_keys` per tenant (≤10) + обязательный `expires_at` (max 365 дней) | P1 | Hard limit 10 активных ключей в `tenants.api_key_limit`, default 5; обязательный `expires_at NOT NULL` с дефолтом NOW() + 365d | Soft limit с предупреждением, но не блокировкой | Без лимита — как сейчас |
| **OPEN-И-20** | Signed URL с TTL ≤ 24 ч для `report_jobs.file_path` + триггер записи в `pd_processing_log` | P1 | S3 presigned URL TTL 24 ч + триггер ON INSERT `report_jobs` → INSERT в `pd_processing_log` action='exported' | Presigned без триггера (запись через app-код, риск пропустить) | Permanent URL за auth (как сейчас в narrative) |
| **OPEN-И-21** | Anti-DDoS: глобальный cap на webhook RPS (Nginx-уровень), CAPTCHA на /register, email-blacklist disposable доменов, fingerprinting | P1 | Nginx `limit_req_zone` 1000 RPS глобально + CAPTCHA через Yandex SmartCaptcha + disposable-blacklist (~50К доменов) | Только Nginx limit, без CAPTCHA | Полагаться на Yandex Cloud Application Load Balancer DDoS-protection |
| **OPEN-И-22** | Crypto-shredding для S3-бэкапов (per-tenant DEK, при удалении tenant — destroy DEK) | P2 | Per-tenant DEK хранится в Yandex KMS, encryption envelope каждого бэкапа использует tenant DEK | Один DEK на бэкап, при удалении тенанта — пересоздание бэкапов без него | Не делаем — soft-delete + retention 30 дней достаточно |
| **OPEN-И-23** | Отдельная роль `crm_audit_writer` (write-only на audit-таблицы) + REVOKE DELETE/UPDATE даже у `super_admin` через триггеры | P2 | Создать роль, дать INSERT, REVOKE остальное, app использует эту роль для audit-write | Без отдельной роли, только триггер защиты | Не делаем — `crm_app_user` пишет audit как сейчас |
| **OPEN-И-24** | Dev/staging: `pg_anonymizer` процедура с маскированием phone/email/totp_secret при создании дампа из prod | P2 | Документированная процедура `pg_dump prod → pg_anonymize → restore staging`. Автоматизация в CI | Manual masking-скрипт без расширения | Dev = synthetic data только, prod-дампы запрещены |
| **OPEN-И-25** | Регламент эскалации просроченных лидов (auto-reassign после N часов, запись в `activity_log`) | P2 | Cron `leads:escalate-stale` каждые 30 мин. Если `deals.assigned_at + 4h < NOW()` и статус ≠ closed → reassign + email | Только notification без reassign | Не делаем — менеджер сам обновляет |
| **OPEN-И-26** | Архитектурный задел под call-recording / транскрипцию (Биз-12 Post-MVP): закомментированный DDL `call_recordings` | P2 | Добавить закомментированный DDL в `db/schema.sql` (10 строк) + ссылка из §10 narrative | Без DDL, только TODO-комментарий | Не закладываем — добавим при ретрофите Биз-12 |

### 13.5. Юристу

| ID | Вопрос | Приор. | Вариант A (рекомендуем) | Вариант B | Вариант C |
|---|---|---|---|---|---|
| **Ю-9** | 152-ФЗ ст.21 ч.5: при impersonation в тенант с `pd_subject_request.processing_restricted=TRUE` — запрет любого доступа кроме `compliance`-роли с обоснованием | P1 | Hard-блокировка в `SaasAdminAuthService` для всех ролей кроме `compliance` + UI-заглушка с указанием 152-ФЗ ст.21 | Soft-warning при попытке + audit, но действие разрешено | Не блокируем — полагаемся на дисциплину админов |

### 13.6. 5 P0-находок до старта фазы 1 (`composer create-project`)

1. **CTO-13 + OPEN-И-14** — без проверки `SET LOCAL` через PgBouncer и без `WITH CHECK` все 34 RLS-политики могут оказаться декоративными. Это must-test до первого PR с tenant-моделью.
2. **OPEN-И-13** — Yandex 360 SSO flow не описан полностью (только «решено в DO-5»). Без него нельзя писать `SaasAdminAuthController`.
3. **OPEN-И-15** — Append-only audit hash-chain. Без этого все доказательства в `saas_admin_audit_log` юридически слабы (admin сам пишет — может и стереть). Нужно до первого `manual_credit`.
4. **OPEN-И-16** — PII-scrubbing Sentry. ПДн в Sentry = breach по 152-ФЗ. До первого деплоя в prod.
5. **Биз-17/18/19** — без автораспределения, TTFR-SLA и антифрода дублей продукт не отличим от голого webhook'а. Это base-line для pay-per-lead-сегмента.

### 13.7. Что хорошо покрыто (из C-2)

- **34 RLS-политики на 34 защищённых таблицах** (1:1, JOIN-политики для pivot-таблиц) — корректно.
- **CSP без `unsafe-inline`**, `frame-ancestors 'none'`, report-uri в Sentry — методология сильнее большинства SaaS.
- **HMAC SHA-256 + timestamp + replay-защита** на outbound webhook — константно-временная проверка, retry с backoff 30s→24h.
- **SSRF-защита** на outbound — RFC1918 + metadata-эндпоинты + только https (плюс OPEN-И-18 для DNS-rebinding).
- **Impersonation Ю-1** — email-код 15 мин + audit + post-сессия email + запрет critical write — лучшая практика индустрии.
- **§22.12 антипаттерны оригинала** — явная защита от DOM-plaintext credentials.
- **Atomic billing** — `gateway_idempotence_key UNIQUE` + `lockForUpdate` + `late_webhook_ignored` — двойного списания не будет.
- **`processing_restricted` BOOLEAN** (152-ФЗ ст.21 ч.5) — реализовано через guard-исключение.
- **Four-eyes** > 50 000 ₽ + лимиты `support` 1000 ₽/24ч — anti-fraud для админов.
- **Изоляция тенантов 4 уровня** — сильнее, чем у HubSpot multi-tenant.
- **Kanban+DnD виртуализация** (§11) — на уровне Pipedrive.
- **Outbound webhook** — лучше, чем AmoCRM.
- **Аудит мутаций** (§14) — глубже оригинала.
- **152-ФЗ workflow** (§22.9 + Прил. Д) — превосходит индустрию РФ.
- **Тихие часы** (§17.8), **2FA на всех тарифах** (§18.4.1) — security-differentiator.

### 13.8. Что НЕ нашли пробелов (важно — уже хорошо в narrative)

- **Распределение лидов в проекте**: `manager_id` есть, RLS работает, нужна только стратегия (Биз-17).
- **Биллинг integrity**: `balance_transactions` + `late_webhook_ignored` + `gateway_idempotence_key` — bullet-proof.
- **Outbound webhook MVP**: 7 линий доказательств отсутствия в оригинале + полная реализация в §19.10.
- **Email-провайдер**: Unisender Go (Ю-7 ✅), маршрутизация через Laravel Mail::send.
- **Compliance**: tenant_consents append-only flag, pd_processing_log, processing_restricted.

### 13.9. Источники аудита C

- C-1 архитектурный агент (general-purpose, 100K токенов, 22 tool uses): чтение narrative + schema + brandbook + Открытые_вопросы. Сравнение с лучшими практиками HubSpot/Pipedrive/AmoCRM/Kommo/RetailCRM для pay-per-lead-РФ-сегмента.
- C-2 security агент (general-purpose, 149K токенов, 21 tool use): STRIDE для 4 потоков (логин админа, inbound webhook, билинг, экспорт) + 5 осей (RLS-дыры, secret rotation, audit-логи, DDoS, PII).
- Дата проведения: 07.05.2026.

### 13.10. Закрытия аудита C — 27 решений (07.05.2026, v1.12)

**Решение заказчика 07.05.2026 (вечер):** «A везде, рекомендованные варианты». Все 27 вопросов из §13.2–13.5 закрыты выбором A. Обоснование выбора зафиксировано в комментариях рекомендаций самого реестра (агенты C-1/C-2 ранжировали варианты так, чтобы A был минимально-достаточным baseline для pay-per-lead-сегмента + 152-ФЗ).

**Закрытия в этом разделе НЕ затирают таблицы §13.2–13.5** — те остаются как audit history (правило §4.4 — прослеживаемость). Для каждого ID ниже указан выбранный вариант + куда дальше пойдёт реализация.

#### 13.10.1. P0 — 8 закрытий (разблокированы для триггера фазы 1)

| ID | Решение (вариант A) | Импакт schema/narrative |
|---|---|---|
| ✅ **Биз-17** | `manual` (паритет с оригиналом, минимум логики) | schema: `projects.assignment_strategy VARCHAR(32) NOT NULL DEFAULT 'manual'` + CHECK `IN ('manual','round_robin','least_loaded')`. Narrative: §10 routing — описать, что MVP = manual, остальное под флагом Post-MVP |
| ✅ **Биз-18** | TTFR-SLA = 15 мин default + alert менеджеру/админу при просрочке | schema: `projects.ttfr_target_minutes INT DEFAULT 15`. Narrative: §12.5.5 — добавить SLA + alert через event bus + UI badge на просрочке |
| ✅ **Биз-19** | Окно 24 ч, дубль помечается `duplicate_of_id`, **НЕ списывает лид** | schema: `deals.duplicate_of_id BIGINT NULL REFERENCES deals(id) ON DELETE SET NULL` + index `(tenant_id, phone, created_at)` для O(log n) lookup в окне. Narrative: §10 — секция «Антифрод дублей» |
| ✅ **CTO-13** | Обязательный e2e-тест в спринте 1: auto-commit, reuse соединения, job retry — каждый кейс либо успешно ставит `tenant_id`, либо падает с exception | Narrative: §22 + Прил. И «Спринт 1 — TenantAwareJob test plan». Schema: без изменений (уже есть `app.current_tenant_id` GUC). Триггер фазы 1 — после прохождения этого теста |
| ✅ **OPEN-И-13** | OIDC + JIT-provisioning, локальный 2FA выключен, fallback — break-glass `super_admin` с force-2FA | Narrative: §22.7 (auth admin) + §23.10 (admin SaaS) — полный flow OIDC + ManageSAML/Yandex 360 endpoint + JIT user create + break-glass procedure. Schema: `saas_admin_users.sso_provider VARCHAR(32) DEFAULT 'yandex360'`, `saas_admin_users.is_break_glass BOOLEAN DEFAULT FALSE` |
| ✅ **OPEN-И-14** | `WITH CHECK` на INSERT для `deal_tag_pivot`, `saas_invoice_items` + явный REVOKE saas-таблиц от `crm_app_user` | Schema: ALTER POLICY ... WITH CHECK + `REVOKE ALL ON saas_admin_users, saas_admin_sessions, saas_admin_audit_log, incidents_log, pd_subject_requests, impersonation_tokens FROM crm_app_user` |
| ✅ **OPEN-И-15** | Триггер `BEFORE UPDATE/DELETE → RAISE EXCEPTION` + `log_hash = sha256(prev.log_hash \|\| row)`. Отдельная роль `crm_audit_writer` (только INSERT) | Schema: `+log_hash BYTEA NOT NULL` на 5 audit-таблицах (`auth_log`, `activity_log`, `pd_processing_log`, `saas_admin_audit_log`, `balance_transactions`) + 5 BEFORE-триггеров + функция `audit_chain_hash()` + `CREATE ROLE crm_audit_writer` + GRANT INSERT |
| ✅ **OPEN-И-16** | Whitelist + regex маска для phone/email/password/secret/token/api_key во всех контекстах (request, breadcrumbs, exception args) | Narrative: §22 — новый подраздел «Sentry PII-scrubbing» с `before_send` псевдокодом. Schema: без изменений |

#### 13.10.2. P1 — 12 закрытий (фазы 1–2)

| ID | Решение (вариант A) | Импакт |
|---|---|---|
| ✅ **Биз-20** | Telegram-бот для нотификаций — спринт 9 (после биллинга) | Narrative: §17 (нотификации) + roadmap. Schema (фаза 2): `users.telegram_user_id BIGINT NULL`, `tenants.telegram_bot_token TEXT NULL` (зашифровано) |
| ✅ **Биз-21** | Шаблон события `marketing.conversion` через outbound webhook §19.10 | Narrative: §19.10 — добавить event type `marketing.conversion` с payload-схемой (utm_*, deal_id, conversion_value). Schema: без новых таблиц |
| ✅ **Биз-22** | Простой scoring `score = supplier_quality × time_in_form` (3 поля, без ML) | Schema: `suppliers.quality_score NUMERIC(3,2) DEFAULT 1.00`, `deals.time_in_form_seconds INT NULL`, `deals.lead_score NUMERIC(5,2) GENERATED ALWAYS AS (...) STORED` |
| ✅ **CTO-14** | 4 × VARCHAR(100) NULL в `deals` + индекс `(tenant_id, utm_source)` (партиционируется PG 16) | Schema: `+utm_source/utm_medium/utm_campaign/utm_content` в `deals` + partition-wise index |
| ✅ **CTO-15** | Two-person approval для impersonation в `processing_restricted=TRUE` ИЛИ `chargeback_unrecovered_rub > 0` тенанты — требуется второй админ роли `compliance` через email | Narrative: §22.7.X (impersonation) — секция «Two-person для restricted tenants». Schema: `+impersonation_tokens.second_approver_id BIGINT NULL REFERENCES saas_admin_users(id)`, `+impersonation_tokens.second_approval_at TIMESTAMPTZ NULL` |
| ✅ **CTO-16** | Новая таблица `project_user_assignments(project_id, user_id, skills JSONB)` для round-robin pool | Schema: новая таблица + RLS-политика `project_user_assignments_tenant_iso` + 1 индекс. Narrative: §10 (routing) — описать как «расширение для Биз-17 round_robin/least_loaded» |
| ✅ **OPEN-И-17** | TTL 365 дней: `webhook_token`, `api_keys`, `outbound.secret_hash`. Cron `secrets:notify-expiring` + UI «продлить» | Schema: `+expires_at TIMESTAMPTZ DEFAULT NOW() + INTERVAL '365 days'` на 3 объектах (либо вынос в `secret_rotations` log-таблицу). Narrative: §22 — секция «TTL secrets + ротация» |
| ✅ **OPEN-И-18** | resolve once → pin IP → blacklist RFC1918 на pinned → connect через pinned. Доверенный CA-bundle. Лимит 3 redirect, max body 1 MB | Narrative: §19.10 (outbound webhook) — расширить «SSRF-защита» секцию подразделом «DNS-rebinding pin-IP». Schema: без изменений |
| ✅ **OPEN-И-19** | Hard limit 10 ключей (`tenants.api_key_limit DEFAULT 5`), обязательный `expires_at NOT NULL` (default NOW + 365d) | Schema: `+tenants.api_key_limit INT DEFAULT 5 NOT NULL CHECK (api_key_limit BETWEEN 1 AND 10)` + `ALTER api_keys.expires_at DROP DEFAULT, SET NOT NULL` (миграция: backfill всем NOW + 365d) |
| ✅ **OPEN-И-20** | S3 presigned URL TTL 24 ч + триггер ON INSERT `report_jobs` → INSERT в `pd_processing_log` action='exported' | Narrative: §14 (отчёты) + §22.9 (152-ФЗ ст.18 экспорт). Schema: новый триггер `report_jobs_export_log` |
| ✅ **OPEN-И-21** | Nginx `limit_req_zone` 1000 RPS глобально + Yandex SmartCaptcha на /register + disposable-blacklist (~50К доменов) | Narrative: §22 (security) + Прил. И (Runbook) — секция «Anti-DDoS». Schema: без изменений. Зависимость на сервис Yandex SmartCaptcha — учесть в смете |
| ✅ **Ю-9** | Hard-блокировка в `SaasAdminAuthService` для всех ролей кроме `compliance` + UI-заглушка с указанием 152-ФЗ ст.21 | Narrative: §22.7.X + §23.10.9 (workflow ПДн). Реализация совместно с CTO-15 — единый guard |

#### 13.10.3. P2 — 7 закрытий (фазы 1–3)

| ID | Решение (вариант A) | Импакт |
|---|---|---|
| ✅ **Биз-23** | `deals.region_code VARCHAR(8)` + `deals.city VARCHAR(100)` + автоопределение по prefix phone + filter в §10.3 | Schema: 2 поля + индекс `(tenant_id, region_code)`. Narrative: §10.3 (фильтры). Service: `PhonePrefixService` (Минсвязи API или offline CSV) |
| ✅ **Биз-24** | Алерт через 48 ч в админке + email finance-роли при `waiting_payment` → `paid` просрочке | Narrative: §17 (нотификации) + Прил. Г (админка). Schema: cron `payments:notify-stale` + конфигурация `system_settings` |
| ✅ **OPEN-И-22** | Per-tenant DEK хранится в Yandex KMS; encryption envelope каждого бэкапа использует tenant DEK | Narrative: §22.9 + Прил. И — секция «Crypto-shred per-tenant». Schema: без изменений. Зависимость на Yandex KMS API + IAM-роль |
| ✅ **OPEN-И-23** | Создать роль `crm_audit_writer` (только INSERT) + REVOKE остальное; app использует эту роль для audit-write | **Уже взято в P0 OPEN-И-15** — фиксируем как отдельный пункт реестра для прослеживаемости |
| ✅ **OPEN-И-24** | Документированная процедура `pg_dump prod → pg_anonymize → restore staging` + автоматизация в CI | Прил. И (Runbook) — новая процедура. Schema: без изменений (расширение `pg_anonymizer` ставится в фазе 3 по Прил. Н) |
| ✅ **OPEN-И-25** | Cron `leads:escalate-stale` каждые 30 мин: если `deals.assigned_at + 4h < NOW()` и статус ≠ closed → reassign + email | Narrative: §10 (routing) + §17 (нотификации). Schema: `+deals.assigned_at TIMESTAMPTZ NULL` + `+deals.escalated_count INT DEFAULT 0`. Дополняет Биз-18 TTFR |
| ✅ **OPEN-И-26** | Закомментированный DDL `call_recordings` (10 строк) в `db/schema.sql` + ссылка из §10 narrative | Schema: `-- Post-MVP Биз-12: call_recordings(...)` блок. Narrative: §10 — упоминание задела |

#### 13.10.4. Итоговый импакт на schema.sql v8.4 → v8.5 и narrative v8.4 → v8.5

**Schema.sql v8.5 (после реализации):**

- **Новых таблиц:** **+1** (`project_user_assignments`, CTO-16). Закомментированный задел `call_recordings` не учитывается.
- **Новых колонок:** ~16 (`projects.assignment_strategy/ttfr_target_minutes`, `deals.duplicate_of_id/utm_*×4/region_code/city/lead_score/time_in_form_seconds/assigned_at/escalated_count`, `tenants.api_key_limit`, `saas_admin_users.sso_provider/is_break_glass`, `impersonation_tokens.second_approver_id/second_approval_at`, `suppliers.quality_score`, `+log_hash×5`).
- **Новых триггеров:** **+6** (5 audit-append-only + 1 report_jobs_export_log).
- **Новых ролей:** **+1** (`crm_audit_writer`).
- **Изменённых RLS-политик:** **+2 WITH CHECK** (`deal_tag_pivot`, `saas_invoice_items`).
- **REVOKE statements:** **+6 таблиц** от `crm_app_user`.
- **Новых функций:** **+1** (`audit_chain_hash()`).
- **Новых индексов:** ~5 (`(tenant_id, phone, created_at)`, `(tenant_id, utm_source)`, `(tenant_id, region_code)`, `project_user_assignments` 1, прочие).

**Прогноз метрик schema.sql v8.5:** 53 → **54** таблицы (+1) + 12 партиций; 86 → **~91** индекса (+5); 33 → **35** RLS-политик (+2 WITH CHECK переписаны или +1 для project_user_assignments); 34 → **35** ENABLE RLS (+1); **3 → 4 роли** (+crm_audit_writer); **+6 триггеров**; **+1 функция**.

**Narrative v8.5 — затрагиваемые разделы:**

- §10 (routing/dedup) — Биз-17/19/23, CTO-16, OPEN-И-25, OPEN-И-26.
- §12.5.5 (TTFR) — Биз-18.
- §14 (отчёты/экспорт) — OPEN-И-20.
- §17 (нотификации) — Биз-20/24, OPEN-И-25.
- §19.10 (outbound webhook) — Биз-21, OPEN-И-18.
- §22 (security/auth/SSO/Sentry) — OPEN-И-13/14/15/16/17/21/22, Ю-9, CTO-15.
- §23.10 (админка SaaS) — OPEN-И-13, CTO-15.
- Прил. И (Runbook) — OPEN-И-21/24/25, CTO-13.

**Реализация — отдельный коммит после фиксации настоящего реестра.** Если schema/narrative v8.5 не будут выпущены сразу, текущая v8.4 остаётся точкой истины, а §13.10 этого реестра — единственным источником решений до следующей версии.

---

*Версия: 1.7 от 05.05.2026 (поздний вечер).*
*Изменения v1.6 → v1.7: аудит связности архива (4 🔴 + 11 🟡 находок, 0 ошибок в DDL); разрешена коллизия OPEN-И-2 (вариант B′: «контакты эскалации» → OPEN-И-12 P1); синхронизирована таблица OPEN-И-* в Runbook v8.2 Часть В с шапкой; исправлены 4 ссылки на удалённый Konspekt_sessii_05_05_2026.md → Объединённый_конспект.md; добавлены пометки 📜 ИСТОРИЧЕСКАЯ ЗАПИСЬ к таблице приложений §28 narrative; уточнён счётчик OPEN-*в приложениях 64→66.*
*Изменения v1.5 → v1.6: добавлен раздел 12 «Аудит партий 12–15»; 3 новых вопроса (Биз-14/15/16); Биз-10 переоткрыт; Прил. М обновлён до v1.1 (новый раздел 9 + §3.5 + §6.6); запланирован schema.sql v8.3 + CHANGELOG-v8_3.md; общее число продуктовых вопросов 47→50, открытых 10→13.* 📜 *Исторический срез на момент v1.6 (05.05.2026 день). На 05.05.2026 (поздний вечер) `schema.sql` v8.3 уже выпущен; `CHANGELOG-v8_3.md` объединён в `CHANGELOG_schema.md` (запись A) — см. ⚠-сноску в начале документа.*
*Изменения v1.4 → v1.5: получены результаты аудита crm.bp-gr.ru (11 партий); создано Прил. М v1.0; добавлен раздел 11 с 4 новыми вопросами (Биз-10..13, все с дефолтами); расширен раздел 9 — патч schema.sql v8.2 теперь включает suppliers/project_suppliers + лимиты проектов; зафиксирована prompt injection-атака в DOM оригинала.*
*Изменения v1.3 → v1.4: фиксация 30 решений интервью; счётчик ✅ 8→30; счётчик 🟦 5; открытых полностью 30→6; истинных P0 4→1.*

---

## 14. Plan 4 (Billing + CSV Reconcile + Admin) — 7 новых открытых вопросов

**Контекст:** Plan 4 завершён на ветке `plan4-billing` (14 task-коммитов `a907fea..174dbae` + lychee fix `fded2ee` = 15 коммитов). Spec — [docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md](superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md) §7.6. 7 placeholder'ов закрыты дефолтами для запуска impl; требуют явного согласования заказчика **до production**.

| ID | Вопрос | Адресат | Приоритет | Дефолт (применён в коде) | Когда нужно решение |
|---|---|---|---|---|---|
| **Биз-25** | Дефолтные tier-цены (50000/45000/40000/35000/30000/27000/25000 коп = 500/450/400/350/300/270/250 руб) | Заказчик | P1 | placeholder в `database/seeders/PricingTierSeeder.php` (effective_from='1970-01-01') | До production (Б-1 + первый продакт-tenant) |
| **Биз-26** | Email rate-limit zero_balance 1/час/tenant — норма? Альтернативы: 1/день / token-bucket / 1/15мин | Заказчик | P2 | 1/час через Redis SETNX (`RouteSupplierLeadJob::handleInsufficientBalance` Cache::store('redis')->add(..., now()->addHour())) | До production |
| **Биз-27** | Tenant видит ВСЕ ступени или только текущую (transparency vs обфускация)? | Заказчик | P2 | transparent — все 7 ступеней доступны через `TenantChargesController::index` | До UI release (post-Б-1 SSO) |
| **Биз-28** | CSV-схема `/admin/report/index?type=49` поставщика — точные имена/порядок столбцов | Discovery после credentials | P1 | placeholder `vid;project;tag;phone;phones;time` (6 columns) в `SupplierCsvParser::EXPECTED_COLUMNS` | После Plan 3 Tasks 1-2 (BLOCKED credentials) |
| **Биз-29** | CSV-window 25h — норма для catch-up? Альтернативы 1h / 6h / 48h | Заказчик/опыт | P3 | 25h (`CsvReconcileJob::WINDOW_HOURS`) — 1ч запас над hourly cron'ом | После 1-2 мес. эксплуатации |
| **Биз-30** | Drift threshold 5% — норма для алерта? | Заказчик/опыт | P3 | 5% (`CsvReconcileJob::DRIFT_THRESHOLD = 0.05`) — из spec §5.2 | После 1-2 мес. эксплуатации |
| **Биз-31** | Pricing-tier-change при повышении цены — единая логика с понижением (effective 1-е след. месяца)? Или требует отдельного approval / уведомления tenant'ов заранее? | Заказчик | P2 | единая логика — `AdminPricingTiersController::store` всегда ставит `effective_from = DATE_TRUNC('month', NOW() + INTERVAL '1 month')` | До production |

**Что значит «дефолт применён в коде»:** Plan 4 impl-tasks использовали placeholder-значения по spec §7.6. Каждый дефолт работает корректно для текущих интеграционных тестов (Pest 687 / Vitest 428 — все PASS); но **семантически требует подтверждения заказчиком** для production-traffic.

**Когда закрываем эти 7 вопросов:**

- Биз-25, Биз-26, Биз-27, Биз-31 — перед открытием production (зависит от Б-1 + первый платящий tenant).
- Биз-28 — после получения credentials поставщика (BLOCKED на Plan 3 Tasks 1-2 параллельно).
- Биз-29, Биз-30 — по эмпирическим данным после 1-2 месяцев эксплуатации.

---

*Версия: 1.83 от 13.05.2026 (day +1) — **CTO-19 ✅ closed** через Lucide migration. Custom Vuetify `IconSet` (`liderraLucideSet`) в `app/resources/js/plugins/vuetify.ts` с 103-entry mapping (78 user-grep'нутых mdi-* + 25 Vuetify-internal defaults: pagination chevrons, v-select dropdown, v-checkbox/radio). 51 Vue/TS файл untouched. `npm i lucide-vue-next ^1.0.0` tree-shakable selective imports. Fallback HelpCircle. Path α (aliases-only) approved заказчиком через brainstorming → writing-plans → subagent-driven-development workflow (Task 1 install + Task 2 IconSet + Task 2.b Vuetify-internal extension через subagents; visual smoke + closure controller-driven). Pravila §2.2 / §7.1 — явное «закрываем CTO-19 как ✅» получено после comprehensive verification (pagination + 7+ views + axe-core a11y 0 iconography violations). CLAUDE.md §2 «Иконки: Lucide» brand-spec compliance achieved. **Сводка §0 update:** CTO 17✅/1⏸/1 P2 [?] → 18 ✅/0⏸/0; Итого 70✅/12⏸ → 71 ✅/11 ⏸. Регрессия: Pest 742/739/0/3 / Vitest 88 files / 683 / 3 skipped / Vite 3.52s / axe-core 0 iconography. v1.82 → v1.83. Предыдущая v1.82 — Catch-up bump v1.77 → v1.82 после обнаружения hidden inconsistency header vs footer-trail. v1.81 CTO-20 ✅ closed (ICU collation); v1.80 CTO-20 заведение; v1.79 CTO-19 (иконочная система: `@mdi/font`); v1.78 Plan 4 (Billing+CSV+Admin) +7 Биз-25..31.*
