# CLAUDE.md — история версий

История изменений вынесена сюда из шапки [../CLAUDE.md](../CLAUDE.md) — для лаконичности основного файла. Перенесено 09.05.2026 при правке v1.73→v1.74 (вместе с текущим claude-md-management skill audit).

Текущая версия и активный список фич — в шапке CLAUDE.md (§0–§8). Здесь — история версий v1.1→v1.83 в обратном порядке (свежие сверху) + изолированные post-fork записи v1.88 (R15 removal из origin/main) / v1.89 (plan5 factual fix) / v1.90 (merge). Записи v1.84..v1.87 живут inline в §9 CLAUDE.md (CHANGELOG-обслуживание не велось 10.05.2026–11.05.2026; gap не критичен — версии полностью описаны в §9 основного файла). NB: на плате v1.88 существует ВТОРАЯ entry (plan5 audit schema-sync 12.05.2026 ночь) — она inline-only в §9 CLAUDE.md, не вынесена сюда; это collision версионной нумерации parallel-branch bump'ов.

---
*CLAUDE.md v1.90 от 13.05.2026 (day). Изменения v1.90: **Merge R15 motion-runtime removal cleanup из `origin/main` в `plan5-frontend-projects`**. Plan5 ветка форкнулась 12.05 утром от `48f27b4` ДО появления `615db99` (R15 removal) на main. После 113 атомарных коммитов на plan5 (audit fixes, Plan 5 frontend Tasks 7-11, Quiet Luxury portal redesign, Q.DEFER.002/003/004 closures, audit-cleanup tail) — merge синхронизирует R15 changes. **2 conflict'а решены вручную:** (1) `CLAUDE.md` шапка version → v1.90 unified; §0 cross-refs → take origin/main (Pravila v1.11 / PSR_v1 v2.0 / Tooling v1.16); §2 Animation default stack → take origin/main (motion-runtime guidance); §5 п.12 → take origin/main (marker «Резерв (снят 12.05.2026)»); §6 фаза + §8 self-review → keep plan5 (Plan 4 MERGED + Plan 5 frontend + Quiet Luxury context); §9 история версий → keep both v1.88 entries explicitly labelled (plan5 audit schema-sync + origin/main R15 removal) + v1.89 plan5 factual fix + new v1.90 merge entry. (2) `docs/CHANGELOG_claude_md.md` (этот файл) → keep all three entries (v1.90/v1.89/v1.88). **3 нормативных файла fast-forward без conflict'а:** `Plugin_stack_rules_v1.md` v1.7 → v2.0 (R15 удалён, 162 lines diff); `Pravila_raboty_Claude_v1_1.md` v1.10 → v1.11 (§11.5/§13.2 счётчик 16→15 + cross-refs); `Tooling_v8_3.md` v1.15 → v1.16 (§9.2 reformulated в technical guidance). **0 code changes**; `resources/js/`, `app/`, `db/` нетронуты merge'ем. **0 npm install** — motion-v / gsap / anime.js / lottie-web всё ещё не установлены, теперь разрешены к установке без обоснования. **Verification:** Pest --parallel baseline 742, Vitest 683, Vite build 1.80s, lychee 245 OK, gitleaks 0 leaks. **Memory updates (после push):** `feedback_plugin_paired_stack.md` (remove «branch-divergent state» note + bump tier-структуру к v2.0); `project_state.md` (branch counters: больше не 2 behind); `reference_archive.md` (file version refs к CLAUDE.md v1.90 + PSR_v1 v2.0 + Pravila v1.11 + Tooling v1.16). Через ручное conflict resolution + post-merge `/claude-md-management:revise-claude-md` polish (per §5 п.10). v1.89→v1.90.*

*CLAUDE.md v1.89 от 12.05.2026 (ночь, post-audit continuation). Изменения v1.89: **factual fix §6 + шапка v1.88 changelog.** В рамках сессии «доделывать аудит» 12.05.2026 ночь обнаружено, что v1.88 в двух местах содержал factual error: (1) §6 строка «Plan 4 (Billing + CSV Reconcile + Admin) MERGED в `origin/main` `615db99` (post-merge after `f4ec5dc` PSR_v1 R15 removal)» — оба коммита идентифицированы неверно; (2) шапка v1.88 changelog «§6 «Plan 4 ready for FF-merge» → «Plan 4 MERGED в origin/main `615db99`» — то же самое. **Verified через `git log origin/main` + `git show <commit>`:** коммит `615db99` это R15 motion-runtime removal commit «chore(rules): remove R15 motion-runtime restrictions (PSR_v1 v2.0)» (12.05.2026 07:30), а НЕ Plan 4 merge; коммит `f4ec5dc` это Quiet Luxury sidebar hotfix «fix(redesign): sidebar position:fixed + main padding-left — restore main content visibility» на ветке `plan5-frontend-projects`, а НЕ PSR_v1 R15 removal и НЕ на origin/main. **Правильная история на origin/main (по git log):** Plan 4 backend task-коммиты `a907fea..174dbae` (Tasks 9-11) merged ранее → `fded2ee` chore(lychee) Plan 4 plan-file fix → `8681040` «docs: Plan 4 closure — CLAUDE.md v1.87 + Открытые_вопросы v1.78» (это и есть Plan 4 closure marker) → `4bc488e` fix(admin) AdminPricingTiers strip ISO-suffix → `1ca4378` + `48f27b4` docs(specs+plans) Plan 5 → `0fd93fd` + `615db99` R15 motion-runtime removal (отдельная история, НЕ часть Plan 4). **Правки v1.89:** (1) §6 строка обновлена с правильными коммитами + явное разделение «Plan 4 closure `8681040`» и «R15 removal `0fd93fd` + `615db99`» как разные истории; (2) шапка v1.88 changelog inline исправление `615db99` → `8681040` + NB-маркер «v1.88 первоначально содержал factual error»; (3) §9 v1.88 entry inline исправление аналогично; (4) bump CLAUDE.md v1.88 → v1.89; (5) новая v1.89 entry в §9 CLAUDE.md + эта запись в CHANGELOG. **Связанные документы (Pravila v1.10 / PSR_v1 v1.7 / Tooling v1.15 / реестр v1.77 на ветке `plan5-frontend-projects`) НЕ требуют изменений** — фактологический фикс локален в CLAUDE.md. *(NB v1.90 post-merge: связанные документы Pravila/PSR_v1/Tooling всё-таки обновились — но не из-за этой v1.89 правки, а из-за подтянутого R15 removal из origin/main; v1.89 logic остаётся валидной — фактологический фикс плана5 локален был в CLAUDE.md.)* Источник правки: post-audit continuation session 12.05.2026 ночь, обнаружено как bonus-finding во время Q.DEFER.001 (memory description downgrade). Заказчик: «доделывать аудит, поправить ошибку в CLAUDE.md». Через `/claude-md-management:claude-md-improver` (per CLAUDE.md §5 п.10 единственный канал правок). v1.88→v1.89.*

*CLAUDE.md v1.88 от 12.05.2026 — origin/main (R15 motion-runtime removal). Изменения v1.88: **Снятие R15 motion-runtime restrictions** по решению заказчика 12.05.2026 («сними все запреты на использование framer motion»). Через `superpowers:brainstorming` → 3 варианта (A узкая правка R15.2 / B полная отмена R15 / C one-strike trigger) → выбран B (вопреки рекомендации A: «доверяю team's choice motion-runtime без regulatory guardrail»). **Conscious rollback** v1.83 audited construction (10.05.2026: R15 двухуровневая motion-конструкция была введена через brainstorming → 3 варианта → «двухуровневый» подтверждение заказчика; v1.88 — это namesake rollback). **5 файлов изменены (atomic commit):** PSR_v1 v1.7 → v2.0 (удалено R15 целиком: R15.1 framer-motion hard-запрет + R15.2 motion-v 4 условия + R15.3 default стойка + R15.4 проверка триггера + R15.5 hard-запрет дублирования + R15.6 live-override + R15.7 расширение на gsap/anime/lottie; удалено R0.6 п.11 animation runtime hard-стоп; удалены R8 три тай-брейкера motion; удалена R11.6 иерархия motion-источников; удалены R13 пять строк motion-сценариев; финальная формула и свойства свода переформулированы; шапка v1.7 → v2.0 major bump). Pravila v1.10 → v1.11 (§11.5/§13.2 счётчик «16 правил R0–R15» → «15 правил R0–R14»; §13.9 + §13.10 cross-ref на PSR_v1 «v1.6» → «v2.0»; §13.10 НЕ удалено — оно про R14 UPM/21st pipeline, не R15). Tooling Прил. Н v1.15 → v1.16 (§9.2 reformulated из regulatory denylist в technical guidance: motion-v + gsap + anime.js + lottie-web + popmotion + @motionone/dom → ✅ разрешено без обоснования; framer-motion + react-spring → ❌ technical block, peerDep react+react-dom, не regulatory rule). **CLAUDE.md этот файл:** §2 «Animation default stack» переписан с regulatory denylist на guidance recommendation; §5 п.12 → маркер «Резерв. Был "не устанавливать motion runtime библиотеки без R15.2". Снят 12.05.2026.» (нумерация §5 п.1–11 сохранена, чтобы cross-refs в memory `feedback_environment.md` / `feedback_plugin_paired_stack.md` не сломать); §0 cross-refs обновлены (Pravila v1.10 → v1.11, PSR_v1 v1.7 → v2.0, Tooling v1.15 → v1.16); §9 «История версий» — entry v1.88. **MEMORY sync:** `memory/feedback_plugin_paired_stack.md` обновлён (v1.7 → v2.0; «R15 motion двухуровневый» → historical context); `MEMORY.md` index hook обновлён. **0 изменений в коде проекта** (`resources/js/`, `app/`, `db/` нетронуты). **0 npm install** (motion-v и др. в `package.json` не попадают; их установка теперь разрешена, но не делается этим коммитом). **0 schema changes**. **Plan:** docs/superpowers/plans/2026-05-12-remove-r15-motion-restrictions.md. **Spec:** docs/superpowers/specs/2026-05-12-remove-r15-motion-restrictions-design.md. Через `/claude-md-management:claude-md-improver` (для CLAUDE.md) + manual Edit (для PSR_v1, Tooling, Pravila). v1.87→v1.88. **NB version-number collision:** на ветке plan5 параллельно существует другая v1.88 entry (audit-driven schema-sync 12.05.2026 ночь, inline-only в §9 CLAUDE.md) — обе валидны, parallel-branch bump'ы.*

*CLAUDE.md v1.83 от 10.05.2026. Изменения v1.83: **Формализация двух фактически включённых внешних UI-инструментов (UI UX Pro Max + 21st.dev Magic MCP) + двухуровневое решение по runtime motion-библиотекам.** Триггер сессии: пользователь спросил «хочу добавить стек плагинов 21st, framer motion, UI UX max — проанализируй конфликты». Проверка `~/.claude/settings.json` и `~/.claude.json` показала: **UPM** (skill `ui-ux-pro-max@ui-ux-pro-max-skill` от marketplace `nextlevelbuilder/ui-ux-pro-max-skill`) и **21st Magic MCP** (`magic` сервер с API-ключом `da9dbf...`, npm `@21st-dev/magic@latest`, tools `mcp__magic__21st_magic_component_builder/inspiration/refiner` + `logo_search`) — фактически уже включены, но в правилах не описаны. Любое использование без формализации = нарушение R0.2/R10.4 PSR_v1. **Framer Motion** — React-only runtime npm-библиотека, не Claude-плагин, физически не работает в Vue (использует React fiber-tree + hooks + JSX). Vue-аналог `motion-v` существует, но это отдельная технология (R0.6 hard-стоп «новая технология в стек»). Через цикл brainstorming (`superpowers:brainstorming`) → 3 варианта решения по R12 архитектурному → итерации с пользователем («предложи но не делай», «добавь Framer Motion», «двух уровневый») согласовано: формализовать UPM+21st; для motion — двухуровневая R15-конструкция. **PSR_v1 v1.3 → v1.4** (главный артефакт): R6 расширен в R6.0 (универсальная таблица фильтра для FD/UPM/21st одинаково); R6.1 hard-override Forest расширен на все три плагина (палитра/шрифты/иконки/aesthetic Forest приоритетнее любых предложений); R10.1 +1 строка для 21st (роль «генератор стартовых шаблонов») + ослабление UPM (теперь активируется не только при «молчании FD», но и для R12 третьего варианта); R11.5 (новое) — активация UPM в R12 архитектурном решении на фазе 1 R2; R11.6 (новое) — параллельная под-иерархия 7 motion-источников (Brandbook → ТЗ → Vue native `<Transition>` → Vuetify transitions → CSS @keyframes → View Transitions API → motion-v); R0.6 +3 hard-стопа (пункт 9: 21st для брендового App*-компонента; пункт 10: 21st для компонента с Vuetify-эквивалентом или существующим в `resources/js/components/`; пункт 11: установка motion-v / gsap / anime.js / lottie-web без R15.2); R13 +9 строк matrix'а (4 строки UI-фич с/без 21st-pipeline + 1 строка R12 третий вариант UPM + 5 строк motion-сценариев); R14 (новое правило, 7 подразделов) — Pipeline внешних UI-генераторов: R14.1 триггер активации, R14.2 шаги, R14.3 UPM в фазах 1/2, R14.4 21st в фазе 5 с обязательным pre-check R0.6 + R6.0 + R6.1 + FD адаптация, R14.5 запрет дублирования (UPM+21st не на одной фазе), R14.6 live-override (с обязательным сохранением фильтров), R14.7 hard-link на §13 Pravila; R15 (новое правило, 7 подразделов) — Motion-системы: R15.1 framer-motion hard-запрет навсегда (React-only архитектурно, не отменяется live-командой), R15.2 motion-v 4 условия активации (а) письменный кейс из ТЗ/Открытые_вопросы (б) категория оправданности — gesture/shared-layout/spring (в) Brandbook approval (г) полный R12 brainstorming + 3 варианта, R15.3 default стойка из 4 слоёв (Vue native + Vuetify + CSS + View Transitions), R15.4 формальная проверка триггера, R15.5 hard-запрет дублирования (motion-v не вытесняет Vuetify), R15.6 live-override запрещён без R15.2, R15.7 расширение на gsap/anime.js/lottie-web/react-spring/popmotion; R8 +7 тай-брейкеров; финальная формула расширена ссылками на R14/R15. **Pravila v1.7 → v1.8**: §13 расширен (paired-stack ядро + расширенный пул); §13.9 cross-ref bumped (v1.3 → v1.4); §13.10 (новый) — hard-link на R14 (UPM/21st вне pipeline'а = нарушение §13, через цепочку R10.4 → §13.9). **Tooling Прил. Н v1.11 → v1.12**: #31 UPM (off-phase tool, §4.5); #32 21st Magic MCP (off-phase tool, §4.6); §9 разделён на §9.1 (изначальный список) + §9.2 (motion runtime библиотеки): framer-motion + react-spring (R15.1 hard-запрет, React-only); motion-v + gsap + anime + lottie + popmotion (R15.2/R15.7 условно, R0.6 пункт 11 hard-стоп). 31 формализованных позиций (19/29 активных по фазам + 2 off-phase). **CLAUDE.md v1.82 → v1.83 (этот файл):** §0 cross-refs обновлены (Pravila v1.6→v1.8, PSR_v1 v1.3→v1.4, Tooling v1.10→v1.12); §2 +Animation default stack строка; §3.3 +#31 UPM +#32 21st строки в карте инструментов; §5 п.5 расширен на расширенный пул UI-инструментов (FD + UPM + 21st с обязательным R6.0 фильтром и R6.1 hard-override Forest); §5 п.12 motion-runtime новый (запрет установки framer-motion + react-spring + motion-v + gsap + anime.js + lottie-web без R15.2); §6 «Текущая фаза» обновлён (31 формализованных позиций тулчейна: 19/29 активных по фазам + 2 off-phase). Через `/claude-md-management:claude-md-improver`. **5 файлов изменены:** PSR_v1 + Pravila + Tooling + CLAUDE.md + CHANGELOG_claude_md. **0 изменений в коде проекта** (`resources/js/`, `app/`, `db/` нетронуты). **0 npm install** (motion-v и др. в `package.json` не попадают). v1.82→v1.83.*

*CLAUDE.md v1.73 от 09.05.2026. Изменения v1.73: **Post-MVP — Reports backend epic закрыт** (4 этапа / 4 коммита `19f319c..e0ffe7e`). После MVP-closure заказчик инициировал работу с реестром; внутри unblocked пусто, поэтому взяли Post-MVP TODO «Reports backend» (был P1-кандидат). **(Этап 1 `19f319c`):** `App\Models\ReportJob` (schema §13.5, status pending/processing/done/failed); `App\Jobs\GenerateReportJob` (sync queue на dev, tries=1 — auto-retry отключён по CTO-6 в пользу ручного UI-retry); `ReportJobController` (GET index/show + POST store с квотой 3 одновременных CTO-7 → 422); первая реализация generator'а DealsExportCsvGenerator (Excel-friendly CSV: BOM + ; + \r\n + escape; deals JOIN projects/users/supplier_lead_costs за period; soft-deleted скрыты). Storage local-disk на dev (`storage/app/reports/{tenant_id}/{job_id}.csv`); на prod — s3 переключение отдельным коммитом. ReportJobFactory + states processing/done/failed. Pest +20. **(Этап 2 `1a6a74c`):** реструктура на provider+formatter pattern — вместо Generator-per-комбинация (4×4=16 классов) разделено на 4 Providers + 4 Formatters (8 классов). Provider возвращает headers + rows; Formatter сериализует в нужный формат. **3 формата + stub:** CsvFormatter (BOM-Excel-friendly), XlsxFormatter (PhpSpreadsheet 5.x с A1-нотацией + bold headers row 1 + auto-size cols; quirk: setCellValueByColumnAndRow удалён в 5.x — использован Coordinate::stringFromColumnIndex), JsonFormatter (UNESCAPED_UNICODE + UNESCAPED_SLASHES + JSON_PRETTY_PRINT), PdfStubFormatter (Post-MVP throw RuntimeException — UI ловит и показывает failed-job). ReportGeneratorRegistry: provider(type) + formatter(format). Удалены: ReportGenerator interface, GenerationResult DTO, DealsExportCsvGenerator. Pest +3. **(Этап 3 `9765ed7`):** retry/cancel/destroy + retention cron. POST /retry (CTO-6: только owner+failed, max 3 попыток через `parameters.retry_count`, окно 7 дней с created_at, квота тоже учитывается чтобы retry-spam не обходил CTO-7; создаёт НОВЫЙ ReportJob с `parameters.retry_of=original.id`); POST /cancel (только owner+pending; status=failed + error_message=«Отменено пользователем»); DELETE (только owner+terminal; удаляет файл из disk('local') + row). toResource +3 поля: is_expired (expires_at < NOW), retry_count, retry_max=3. `App\Console\Commands\ReportsCleanupExpired` cron `reports:cleanup-expired {--dry-run} {--limit=1000}`: где status='done' AND expires_at<NOW AND file_path NOT NULL → delete файл + UPDATE file_path=NULL. **CTO-10**: status='done' СОХРАНЯЕТСЯ (не file_deleted-флаг — наличие файла определяется по file_path=NULL). Failed-jobs игнорируются. Pest +21. **(Этап 4 `e0ffe7e`):** frontend integration. `api/reports.ts` (типизированные axios-helpers listReportJobs/createReportJob/retryReportJob/cancelReportJob/deleteReportJob с ApiReportJob/Status/Format/Counts/Quota interfaces; ensureCsrfCookie на mutating); `composables/reportsMapper.ts` (`mapApiReportJob` API → UI mock format с конверсией pending→queued/processing→running; title строится на frontend'е из тип + period с RU-месяцами «апр 2026» или диапазоном «мар 2026 — апр 2026»; sizeText форматирует bytes (B/KB/MB); timeText зависит от status — «в очереди» / «в работе · Nс» / «N мин назад» / «только что»; uiTypeToApi маппит slug). **ReportsView полностью переписан под API:** onMounted → loadJobs (replace MOCK_JOBS на real); usePolling 30 сек (фоновый авто-refresh); Submit → createReportJob → reload + success-alert + error-alert (validation+общие через extractValidationErrors/extractErrorMessage); canSubmit computed disable если квота заполнена; Reset/Reload-btn; Retry/Cancel/Download-кнопки → API-вызовы; Delete через v-dialog persistent confirm; fetch-error-alert на listReportJobs reject; Empty-state «Нет отчётов»; canRetry проверяет retry_count<3. Vitest +24 (mapper +14: status mapping/title один-период-vs-диапазон/format/sizeText B+KB+MB+null/attempt/error/timeText 4 ветки/uiTypeToApi 4 slug'а/progress=50; ReportsView переписан с MOCK_JOBS на vi.mock('api/reports') +12: mount+listReportJobs called/4 type cards/default Сделки active/4 формата/quota из API/empty-state/done с Скачать/failed с Повторить/failed retry_count=3 БЕЗ Повторить/pending с Отменить/Submit вызывает createReportJob+reload/Submit error→alert/disabled квота 3/3/Reset/Reload/fetch-error-alert/Retry/Cancel/Delete confirm-dialog). **Что НЕ сделано (Post-MVP backlog):** этап 2b — 3 оставшихся типа провайдеров (managers_summary/sources_summary/billing_summary, расширение через 3 новых Provider-класса без изменений архитектуры); GET /api/reports/jobs/{id}/file endpoint скачивания (UI Скачать-кнопка пока без handler'а — нужен Storage::temporaryUrl или streamed-response); S3-storage переключение для production (на dev — local-fs); async queue worker (на dev — sync; на prod нужен Redis/database driver + supervisor). **Регресс зелёный:** lint+type+format ✅; **Vitest 393/393 за 21.78 сек** (+24 от 369); vite build 1.02 сек; Pint+PHPStan passed (baseline регенерирован 2 раза); **Pest 403/403 за 44.83 сек** (+44 от 359, 1343 assertions). Реестр v1.74→v1.75. v1.72→v1.73.*

*CLAUDE.md v1.72 от 09.05.2026. Изменения v1.72: **MVP по Claude-зоне закрыт** (документная фиксация решения заказчика). После закрытия P0 (notifications + reminders, 7 коммитов `a4601fe..4c33323`) реестр открытых вопросов проверен — все unblocked продуктовые вопросы выполнены, остаточные ⏸ — внешние блокеры (Б-1 реквизиты ООО → Диз-3/DO-2/DO-4; юр. редактура Прил. Ж/З + 5 🟦 структурных). **Финальные метрики:** Pest 359/359 (1233 assertions) за 41.37 сек / Vitest 369/369 за 22 сек / Histoire 21/28; schema v8.10 (56 таблиц + 12 партиций + 95 индексов + 37 RLS); 18/28 инструментов активно (фазы 0–2 закрыты по тулчейну). **Покрытие фронтенда:** 13 из 13 концептов handoff'а v8 Forest + 3 ErrorView (404/403/500) — landing v8_landing.html отложен через ⏸ Б-1. **Покрытие бэкенда:** 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 + cron `reminders:dispatch-due`, in_app_notifications (4 endpoints) + bell-UI с 30-сек polling, notification-preferences PATCH, admin (tenants/billing/incidents/system_settings), 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 без middleware на MVP); #7 Pest browser-mode (отложен инфра); ЮKassa/invoice webhook endpoints (service-методы готовы — `notifyTopupSuccess`/`notifyInvoicePaid` — ждут реальных payment-endpoints); new_device_login через user_sessions (требует session-fingerprint инфры); deep-link bell→DealDetailDrawer (на MVP — push на /deals); landing-продакшн ⏸ Б-1+Диз-3+Диз-4. **Дальнейшая работа Claude возобновляется по триггерам:** закрытие Б-1 (запуск Yandex 360 SSO + домен `liderra.ru` + список SaaS-сотрудников); приход юр. формулировок (Прил. Ж оферта + Политика + Прил. З уведомление РКН); переход к фазе 3 pre-production (#25 Semgrep + #26 Trivy + #27 Dependabot + #28 pg_audit + #29 pg_anonymizer); либо deployment в Yandex Cloud (требует closures Б-1). v1.71→v1.72.*

*CLAUDE.md v1.71 от 09.05.2026. Изменения v1.71: **P0 этап 6 — 4 оставшихся email-события закрывают P0 целиком**. low_balance / zero_balance / topup_success / invoice_paid — все 8 событий из schema-default `users.notification_preferences` теперь имеют рабочую интеграцию. **Авто-план P0 (6 этапов / 6 коммитов) закрыт полностью.** **(1) 4 новых Mailable**: `LowBalanceNotification` ($recipient, $tenant, $thresholdLeads), `ZeroBalanceNotification`, `TopupSuccessNotification` ($amountRub, $amountLeads), `InvoicePaidNotification` ($amountRub, $invoiceNumber, $tariffName). **(2) 4 новых blade-шаблонов** в `resources/views/emails/{low_balance,zero_balance,topup_success,invoice_paid}.blade.php` — Forest-палитра (Teal #0F6E56 для positive, #B94837 для error/zero); таблицы с balance_leads/amount_rub/invoice_number; CTA на «Биллинг» / «Настройки → Уведомления». **(3) `NotificationService` +4 методов**: `notifyLowBalance(Tenant, threshold)`, `notifyZeroBalance(Tenant)`, `notifyTopupSuccess(Tenant, amountRub, amountLeads?)`, `notifyInvoicePaid(Tenant, amountRub, invoiceNumber?, tariffName?)`. Все 4 шлют email + inapp по prefs (recipientsForEvent с фильтром по соответствующему event-key). **(4) Интеграция в `ProcessWebhookJob`**: (a) `chargeNewLead` после lead_charge проверяет `low_balance_threshold_leads` из `system_settings` (default 10, schema seed) — триггерит `notifyLowBalance` ТОЛЬКО при пересечении порога сверху-вниз `balance_after <= threshold AND (balance_after+1) > threshold` (иначе спам после каждого lead_charge при balance < threshold). (b) `logRejection(zero_balance)` после INSERT в `RejectedDealsLog` триггерит `notifyZeroBalance` ТОЛЬКО если в последний час не было другого RejectedDealsLog с тем же reason (anti-spam: 1 email/час на тенант). Защита от self-just-inserted через `id != $rejected->id` (timestamp-сравнение ненадёжно из-за PG microsecond precision). (c) topup_success/invoice_paid — service-методы готовы к подключению, intergration отдельным коммитом когда появятся endpoints для пополнения (ЮKassa webhook) и оплаты тарифа. **(5) `lowBalanceThreshold()`** private helper в Job читает `system_settings.low_balance_threshold_leads` через SystemSetting::find, fallback 10. **(6) Pest +12** в `BalanceNotificationsTest.php` (всего **359/359 за 41.37 сек**, 1233 assertions): low_balance триггер при пересечении порога / balance уже < threshold не шлёт повторно / balance > threshold после decrement не шлёт / prefs.email=false → только inapp; zero_balance первое отклонение → email+inapp / 2-е в течение часа НЕ дублирует / >1ч снова шлёт; topup_success notify создаёт email+inapp / prefs=email:false → только inapp; invoice_paid notify создаёт email+inapp / prefs=email:false → только inapp; balance events изолированы между tenants. **`NewLeadNotificationTest`**: тест «balance=0 не шлёт уведомление» обновлён — теперь `Mail::assertNotSent(NewLeadNotification)` (вместо `Mail::assertNothingSent()`), потому что ZeroBalanceNotification ШЛЁТСЯ при balance=0 — это новое поведение по ТЗ §18.5. **PHPStan baseline регенерирован** (Pint автофиксы по ProcessWebhookJob и тестам). **Все 8 событий из schema-default готовы:** new_lead (этап 1+2a) / reminder (этап 4) / low_balance / zero_balance / topup_success / invoice_paid (все этап 6); new_device_login и marketing — стартовые семантические заглушки в NotificationService::EVENT_* константах, не подключены (отсутствует endpoint device-tracking + marketing-broadcast). **P0 ЗАКРЫТ ПОЛНОСТЬЮ.** **Производственные TODO остаточные (после P0):** topup endpoint ЮKassa-webhook → notifyTopupSuccess; invoice paid webhook → notifyInvoicePaid; new_device_login через user_sessions tracking; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** Pint+PHPStan passed; **Pest 359/359 за 41.37 сек** (+12 от 347, 1233 assertions); **Vitest 369/369 за 22 сек** (без изменений — backend-only этап); vite build 1.02 сек. v1.70→v1.71.*

*CLAUDE.md v1.70 от 09.05.2026. Изменения v1.70: **P0 этап 5 — Reminders frontend** (RemindersView + DealDetailDrawer-секция + nav-badge live). Закрывает frontend-half этапа 4-5: пользователь может создавать/просматривать/завершать/удалять напоминания из UI. **(1) `api/reminders.ts`** — типизированные axios-helpers для 5 endpoint'ов (list/create/update/complete/delete) с `ensureCsrfCookie` для mutating-вызовов. Type `ReminderFilter`/`ApiReminder`/`ReminderCounts`. **(2) Pinia `stores/reminders.ts`** — items/counts/loading/fetchError + `currentFilter` ref + actions `load(params)` / `refreshCounts()` (lightweight для bell-badge) / `create(payload)` / `update(id, payload)` / `complete(id)` (optimistic + revert; при currentFilter ∈ {active,today,upcoming,overdue} убираем из items) / `remove(id)` (optimistic) / `reset()`. **(3) `components/reminders/ReminderDialog.vue`** — двух-режимный (create/edit) modal с native `<input type="datetime-local">` (без heavy picker'а): props `dealId?` / `reminder?`, watch на modelValue для re-init из props, ISO-конверсия при submit, error-alert при failure. **(4) `views/RemindersView.vue`** — page-head с заголовком + 2 page-stats (active / overdue с error-color) + reload-btn; v-tabs с counts на бейджах (overdue=error color); список v-list-item с action-prefix (mdi-check-circle-outline → complete) + meta (#deal_id deep-link на /deals + relative time + creator_name) + dropdown menu (Изменить/Удалить с confirm-dialog); empty-state «Создавайте из карточки сделки» (на MVP нет deal-picker'а на этой странице). 4 фильтра-таба (today по default / upcoming / overdue / completed). При complete/delete refreshCounts() обновляет nav-badge синхронно. Маршрут `/reminders` (lazy) добавлен в router. **(5) `AppLayout`** — nav-tree пункт «Напоминания» теперь биндит count из `useRemindersStore().counts.active` (replace static «12»). Бейдж скрыт при count=0 (новое условие `count > 0` поверх `!== undefined`). `usePolling(loadReminderCounts, {intervalMs: 60_000})` для авто-обновления nav-badge каждую минуту. **(6) `DealDetailDrawer`** — добавлена секция «Напоминания» (видна только при `tenantId && deal`) с inline create-btn + список активных напоминаний этой сделки + complete-btn. ReminderDialog встроен в drawer (close-on-content-click=false для предотвращения закрытия по клику в dialog). `loadReminders` дёргается на open + после save. **(7) Vitest +18** (всего **369/369 за 21.20 сек**, +20 от 349 — добавил +2 в AppLayout): `reminders-store.spec.ts` 11 (initial state / load+reject / refreshCounts только counts / create + reject / complete optimistic + revert / remove + reject / reset); `RemindersView.spec.ts` 7 (mount + 4 tabs / counts на бейджах / empty-state / список / reload-btn / filter=today по умолчанию); `AppLayout.spec.ts` +2 (бейдж скрыт при counts.active=0 / показывается «7» при counts.active=7). Реализованный flow покрывает 90% UI потока — без deep-link на конкретный DealDetailDrawer и без deal-picker на отдельной странице (отдельный коммит). **Производственные TODO остаточные:** этап 6 (4 email-события); deep-link на конкретный drawer от bell/reminders; deal-picker для прямого create на /reminders; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 369/369 за 21.20 сек** (+20 от 349); vite build 1.00 сек; **Pest 347/347 за 41.51 сек** (без изменений — backend нетронут). v1.69→v1.70.*

*CLAUDE.md v1.69 от 09.05.2026. Изменения v1.69: **P0 этап 4 — Reminders backend (CRUD + cron-диспетчер + email/inapp-уведомления)**. Закрыт пункт «Reminders ⏸ no-view» из nav-tree. Schema-таблица `reminders` уже была в v8.10 (§17.5), теперь работает целиком backend-side. **(1) `App\Models\Reminder`** — Eloquent с casts (remind_at/completed_at/sent_at datetime, is_sent bool), relations (tenant/creator/assignee), helpers `isCompleted()`/`isOverdue()`. **(2) `ReminderFactory`** — definition с remind_at +1 час по умолчанию + states `overdue()` / `completed()` / `sent()`. **(3) `ReminderController`** под `auth:sanctum` с RLS-обёрткой + defense-in-depth `where('tenant_id')`: GET /api/reminders?filter=&deal_id=&limit= (filters: active|today|upcoming|overdue|completed, окно today=±1 день, counts для UI badges); POST /api/reminders {deal_id, text?, remind_at, assignee_id?} (FK guard на assignee — должен быть active user того же tenant'а, иначе 422); PATCH /api/reminders/{id} (text/remind_at/assignee_id, при смене remind_at автоматически сбрасывается is_sent+sent_at чтобы cron мог ретригерить); POST /api/reminders/{id}/complete (idempotent — повторный NO-OP); DELETE /api/reminders/{id}. **(4) `ReminderDueNotification`** Mailable + `resources/views/emails/reminder.blade.php` (Forest-палитра, blockquote text, TZ конвертирована в recipient.timezone). **(5) `NotificationService::notifyReminder(Reminder)`** — recipient = assignee_id ?? created_by (если active и не deleted); если ни тот ни другой не доступен — silent return. Канал email + inapp по prefs. payload содержит `reminder_id` + `deal_id` для UI deep-link. **(6) `App\Console\Commands\RemindersDispatchDue`** — cron `reminders:dispatch-due {--dry-run} {--limit=500}`. Идёт по `is_sent=false AND completed_at IS NULL AND remind_at <= NOW()`. По одному reminder в transaction (`SET LOCAL app.current_tenant_id` нельзя переключать между разных tenant'ов в одной TX). После notifyReminder — `UPDATE is_sent=true, sent_at=NOW()` ДАЖЕ если recipient deactivated (защита от retry-spam). На production — Windows Task Scheduler / cron каждую минуту. **(7) Маршруты** в `routes/web.php` под `Route::middleware('auth:sanctum')->prefix('/api/reminders')`. **(8) Pest +32** (всего **347/347 за 41.21 сек**, 1203 assertions): `ReminderControllerTest` 21 (401 без auth / пустой / только свои / filters today/overdue/completed / counts / deal_id фильтр / store success+422 без полей / store assignee FK guard 2 / update text+remind_at сбрасывает is_sent / 404 чужой / 422 без полей / complete+idempotent / delete+404 чужой); `RemindersDispatchDueTest` 11 (due → email+inapp+is_sent / future skip / completed skip / уже sent skip / assignee получает вместо created_by / deactivated user (но reminder помечается is_sent чтобы не ретрить) / prefs.email=false → только inapp / --dry-run не шлёт+не помечает / 3 due → 3 sent / --limit=1 / RLS изоляция между tenant'ами). PHPStan baseline регенерирован. IDE-helper для Reminder. **Производственные TODO остаточные:** этап 5 (RemindersView + DealDetailDrawer integration); этап 6 (4 email-события); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** Pint+PHPStan passed (baseline регенерирован); **Pest 347/347 за 41.21 сек** (+32 от 315, 1203 assertions); frontend нетронут — Vitest/build не нужны. v1.68→v1.69.*

*CLAUDE.md v1.68 от 09.05.2026. Изменения v1.68: **P0 этап 3 — NotificationsTab.vue фикс под schema + GET/PATCH prefs API**. Закрытие архитектурного расхождения из v1.28: handoff (8 событий: new_lead/duplicate_detected/low_balance/tariff_charge/reminder_due/manager_assigned/webhook_failed/monthly_report × email/sms/in_app) — **не совпадал** с schema (8 событий: new_lead/reminder/low_balance/zero_balance/topup_success/invoice_paid/new_device_login/marketing × inapp/push/email). Tab сохранял prefs только локально без API. **(1) Backend `AuthController::updateNotificationPreferences`** — PATCH /api/auth/me/notification-preferences под `auth:sanctum`. Принимает `{prefs: {event: {channel: bool}}, sound_enabled?: bool}`. Валидация: события ∈ `NotificationService::ALL_EVENTS` (8 schema-aligned), каналы ∈ `{inapp, push, email}`. **Replace-семантика**: незадекларированные events отбрасываются полностью (не merge — позволяет «выключить целиком»). Незадекларированные channels тоже отбрасываются (защита от schema-pollution). bool-кастинг (`1`/`'1'` → `true`). Возвращает `userResource` с обновлёнными prefs. **`userResource`** расширен: добавлены `notification_preferences` + `sound_enabled` поля. **`UserFactory`** расширен `notification_preferences` (schema-default JSON 8×3) — без этого тесты падали на `User::factory()->create()` поскольку Eloquent не перечитывает строку после INSERT, а DB-DEFAULT JSONB виден как null на свежесозданной модели. **(2) Pest +10** в `NotificationPreferencesTest.php` (всего **315/315 за 36.73 сек**, 1130 assertions): 401 без auth / успех + replace prefs / неизвестные events отбрасываются / неизвестные channels (sms/webhook) отбрасываются / 422 без prefs / sound_enabled опционален / GET /me возвращает prefs+sound_enabled / 422 при prefs.* строка вместо объекта / bool-кастинг 1/'1' → true / replace-семантика (отсутствующие events исчезают). **(3) Frontend `api/auth.ts`** — типы `NotificationChannel = 'inapp'|'push'|'email'` + `NotificationEventKey` (8 events) + `NotificationPreferences` Partial-Record. `AuthUser` interface получил optional `notification_preferences` + `sound_enabled`. Helper `updateNotificationPreferences(payload)`. **(4) `NotificationsTab.vue`** полностью переписан под schema-aligned: 8 событий с описаниями (Новый лид/Напоминание/Низкий баланс/Нулевой баланс/Пополнение успешно/Счёт оплачен/Новое устройство/Анонсы и промо), 3 канала (В приложении/Push/Email — БЕЗ SMS). Реактивный flow: `prefs` ref инициализирован синхронно через `buildPrefs()` (иначе `v-if="prefs[e.id]"` блокирует рендер чекбоксов до onMounted и тесты `mount()→find()` падают). `dirty` — computed (JSON.stringify сравнение с `originalPrefs` snapshot вместо watch+флаг — устойчив к идемпотентным изменениям). `save()` async + 2 v-alert (success-tonal / warning-tonal closable). `Сохранить` btn `:disabled="!canSave"` + `:loading="saving"`. `Отменить` btn вызывает `readFromUser()` (re-snapshot из auth.user). Push-канал отмечен «включится в Post-MVP» в hint'ах. **(5) Vitest +10** в `NotificationsTab.spec.ts` (всего **349/349 за 20.42 сек**, +10 от 339): 8 schema-aligned событий присутствуют / 3 канала (НЕ sms) / legacy-events отсутствуют (Дубликат/Webhook упал/etc) / читает prefs из auth.user (new_lead.email=false / reminder.email=true) / Сохранить disabled пока не изменено / после toggle становится enabled / save() вызывает API + success-alert + правильный payload / save() reject → error-alert / Отменить возвращает к оригиналу / sound_enabled читается из auth.user. SettingsView.spec.ts обновлён (legacy event-имена «Дубликат/Срок напоминания/Webhook упал» → «Напоминание/Нулевой баланс/Анонсы и промо»). **PHPStan baseline** регенерирован для +25 ignored Pest TestCall. **Производственные TODO остаточные:** этапы 4-5 (Reminders backend + frontend), этап 6 (4 email-события); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 349/349 за 20.42 сек** (+10 от 339); vite build 983 ms; Pint+PHPStan passed; **Pest 315/315 за 36.73 сек** (+10 от 305, 1130 assertions). v1.67→v1.68.*

*CLAUDE.md v1.67 от 09.05.2026. Изменения v1.67: **P0 этап 2b — In-app notifications API + UI bell + polling**. Закрывает этап 2 P0 целиком (вместе с 2a). **(1) Backend `App\Http\Controllers\Api\InAppNotificationController`** под `auth:sanctum` (Sanctum SPA, уведомления USER-personal). 4 endpoint'а: `GET /api/notifications?unread_only=&limit=` (1..100, default 50; ORDER BY created_at DESC + id DESC; возвращает items+unread_count+total); `PATCH /api/notifications/{id}/read` (idempotent — повторный вызов NO-OP); `POST /api/notifications/mark-all-read` (bulk update + count); `DELETE /api/notifications/{id}` (hard-delete). Все четыре обёрнуты в DB::transaction + SET LOCAL app.current_tenant_id. Защита от кражи чужого id через `where('user_id', $authUser->id)` поверх RLS. **(2) Маршруты** в `routes/web.php` под `Route::middleware('auth:sanctum')->prefix('/api/notifications')` — Sanctum SPA требует session middleware из web-группы. **(3) Pest +14** в `InAppNotificationApiTest.php` (всего **305/305 за 34.71 сек**, 1099 assertions): 401 без auth / пустой / только свои + ORDER BY created_at DESC / unread_only=1 / limit=2 + total=5 / 422 limit>100 / поля title+body+event+payload+deal_id / mark-read ставит read_at + idempotent / mark-read 404 для чужого / mark-read 404 unknown / mark-all-read bulk + count / mark-all-read только свои / DELETE удаляет своё / DELETE 404 для чужого. **(4) Frontend `api/notifications.ts`** — типизированные axios-helpers с `ensureCsrfCookie` для mutating-вызовов. ApiInAppNotification + ListNotificationsResponse interfaces. **(5) Pinia store `stores/notifications.ts`** — items/unreadCount/total/loading/fetchError refs + sortedItems computed (DESC by created_at) + actions: `load(limit, unreadOnly)` / `markRead(id)` (optimistic + revert на reject) / `markAllRead()` (NO-OP при unreadCount=0) / `remove(id)` (optimistic с decrement total/unreadCount) / `reset()`. На fail markRead/markAllRead/remove — silently revert (без toast'а — иначе спам при каждом sync-failure). **(6) AppLayout** — bell-icon переписан с static-pip на v-menu (offset=8, close-on-content-click=false, location=bottom-end): `<v-btn data-testid="notifications-btn">` с pip badge показывающим `unreadDisplay` (1..99 / `99+` / hidden при 0); v-card с заголовком + Mark-all-read btn (только при unreadCount>0) + v-list последних 10 элементов из sortedItems. Click на item → `markRead` + если `deal_id` → `router.push('/deals')` (deep-link на конкретный drawer — отдельный коммит). 8 mock event-icon'ов (mdi-account-plus-outline для new_lead, mdi-clock-outline для reminder, и т.д.). **`formatRelative`** показывает «только что» / «N мин назад» / «N ч назад» / «N д назад». `usePolling(loadNotifications, {intervalMs: 30_000})` — каждые 30 сек reload (Page Visibility API в usePolling pause'ит при hidden tab). `loadNotifications` no-op без auth.user. **(7) Vitest +18** (всего **339/339 за 20.03 сек**, +18 от 321): notifications-store 12 (initial state / load fills+rejects / markRead optimistic+revert+already-read / markAllRead optimistic+NO-OP при 0 / remove optimistic+revert / sortedItems DESC / reset); AppLayout +6 (bell-btn существует / pip скрыт при 0 / pip показывает count / pip 99+ при >99 / listNotifications вызывается на mount при auth.user / без user не вызывается). **PHPStan baseline регенерирован** (50 false-positive Pest TestCall warnings подавлены). Production TODO остаточные: deep-link на конкретный drawer (на MVP — push на /deals); этапы 3-6 P0; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 339/339 за 20.03 сек** (+18 от 321); vite build 989 ms (main app-chunk 164.94 KB / KanbanView lazy 182.26 KB); Pint+PHPStan passed (baseline регенерирован); **Pest 305/305 за 34.71 сек** (+14 от 291, 1099 assertions). v1.66→v1.67.*

*CLAUDE.md v1.66 от 09.05.2026. Изменения v1.66: **P0 этап 2a — in_app_notifications + notifyInApp в NotificationService** (schema v8.9→v8.10). Backend-фундамент bell-icon канала; UI bell + API endpoints — этап 2b отдельным коммитом. **(1) Schema v8.10** — таблица `in_app_notifications` после `reminders` (обе про работу/коммуникации): id BIGSERIAL / tenant_id FK / user_id FK / event VARCHAR(50) / title VARCHAR(255) / body TEXT / deal_id BIGINT БЕЗ FK (deals партиционирована) / payload JSONB DEFAULT '{}' / read_at TIMESTAMPTZ / created_at TIMESTAMPTZ. UPDATED_AT отсутствует (только created_at + read_at). Индексы: `idx_in_app_notifications_user_unread (user_id, created_at DESC) WHERE read_at IS NULL` (главный UI-флоу) + `idx_in_app_notifications_user_recent (user_id, created_at DESC)` (последние 50 с прочитанными). RLS `tenant_isolation` стандартная. CHANGELOG_schema.md +§T (3 точки источник изменений + 4 точки SQL DDL + почему НЕ Laravel default `notifications`-table). **Метрики после v8.10:** 55→56 таблиц, 93→95 индексов, 36→37 RLS-политик. **(2) `App\Models\InAppNotification`** — Eloquent с `UPDATED_AT=null`, payload cast `array`, read_at cast `datetime`, BelongsTo на User+Tenant. **(3) `NotificationService::notifyInApp(User, event, title, body, payload)`** — INSERT в БД через `DB::transaction` + `SET LOCAL app.current_tenant_id = user.tenant_id` (PgBouncer-safe, RLS-симметрично). Throwable проглатываются + Log::warning. **`NotificationService::notifyNewLead`** теперь шлёт ДВА канала параллельно: email (если prefs.email=true) И in-app (если prefs.inapp=true). `title` = `«Новый лид — {projectName}»`, `body` = `contact_name ?? phone`, `payload` = `{deal_id, project_name}` для UI deep-link на DealDetailDrawer. Schema-default `new_lead.inapp=true` → большинство получит in-app, и только подписавшиеся — email. **(4) Pest +11** в `tests/Feature/Notifications/InAppNotificationTest.php` (всего **291/291 за 32.94 сек**, 1060 assertions): inapp=true создаёт row + поля + payload / inapp=false не создаёт / schema-default ставит row / 2 user'а с inapp=true оба получают / inactive не получает / другой тенант не получает (RLS изоляция) / Биз-19 дубль не дублирует / повторный vid не дублирует / inapp+email=true создаёт 1 row + 1 email / payload содержит deal_id для deep-link / `notifyInApp` напрямую с reminder создаёт row. **(5) Quirk** — Write tool с относительным путём `app/tests/...` создал файлы в `app/app/tests/...` (CWD дрейфонул на `/c/моя/.../app/app`); файлы перемещены вручную, пустые директории удалены через rmdir (rm -rf не пройден permissions). **(6) IDE-helper регенерирован** для нового InAppNotification. **PHPStan baseline регенерирован** (1 «nullsafe.neverNull» error на `$deal->project?->name` подавлен через baseline). **Производственные TODO остаточные:** этап 2b (API + UI bell), этапы 3-6; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** Pint+PHPStan passed (baseline регенерирован); **Pest 291/291 за 32.94 сек** (+11 от 280, 1060 assertions); frontend нетронут — Vitest/build не нужны. v1.65→v1.66.*

*CLAUDE.md v1.65 от 09.05.2026. Изменения v1.65: **P0 этап 1 — NotificationService + new_lead email** (старт closing TODO «Notification delivery» из карты остатка работы). Закрывает первый из 6 этапов плана P0 (notifications + reminders). **(1) `App\Services\NotificationService`** — центральный диспетчер. Константы 8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/invoice_paid/new_device_login/marketing) + 3 каналов (inapp/push/email) точно как в schema.sql:699 `users.notification_preferences` JSONB DEFAULT. Метод `notifyNewLead(Tenant, Deal)` — выбирает активных user'ов тенанта (is_active=true + deleted_at IS NULL) с включённым `notification_preferences.new_lead.email=true` и шлёт через `Mail::to(...)->send(NewLeadNotification)`. Throwable из Mail-фасада ловится → Log::warning (отказ канала не должен валить транзакцию webhook'а). **PHP-фильтр** prefs (не JSONB-запрос) — список получателей <50 на тенант, не critical-path. **(2) `App\Mail\NewLeadNotification`** — Mailable с (User $manager, Deal $deal, Tenant $tenant). Subject `«Лидерра. Новый лид — {project_name}»` с fallback project=`'Без проекта'` если relation не загружен. **`resources/views/emails/new_lead.blade.php`** — HTML-письмо в Forest-палитре (#0F6E56 primary, #F6F3EC ivory) с таблицей phone/contact_name/received_at (TZ конвертирована в `manager->timezone ?? 'Europe/Moscow'`)/deal_id. **(3) Интеграция `ProcessWebhookJob::chargeNewLead`** — после ActivityLog::create вызов `app(NotificationService::class)->notifyNewLead($tenant, $deal)`. `$deal->setRelation('project', $project)` чтобы Mailable не делал лишний SELECT. NotifyNewLead вне DB::transaction в смысле что ошибка отправки уже вне транзакции — но DB::transaction обёртка сейчас покрывает и notify-вызов; на prod надо или вынести notify ПОСЛЕ DB::transaction, или `Mail::queue` (async через worker). На MVP — sync через ::send (детерминированно для тестов). **(4) Pest +11** в `tests/Feature/Notifications/NewLeadNotificationTest.php`(всего **280/280 за 31.27 сек**, 1029 assertions): Mail::fake() / 1 user с email=true получает / user с email=false не получает / schema-default (.email=false) не шлёт / 2 user'а с email=true получают оба, 3-й с email=false не получает / inactive user с email=true не получает / soft-deleted user не получает / user другого тенанта не получает (изоляция) / Биз-19 дубль не шлёт повторное уведомление / повторный vid (idempotent UPDATE) не шлёт повторно / balance=0 (RejectedDealsLog) не шлёт / subject содержит project_name «Caranga». **(5) IDE-helper** регенерирован (`ide-helper:models -W -M -N`) — добавил @mixin docblocks 4 моделям (ImpersonationToken/SaasAdminAuditLog/SystemSetting/UserRecoveryCode), которые ранее без них работали через baseline-ignore'ы. **PHPStan baseline регенерирован** — 138 «ignore.unmatched» errors схлопнулись (новые docblocks резолвят property access напрямую, baseline-патч больше не нужен). **Производственные TODO остаточные:** этапы 2–6 P0 (in_app_notifications + UI bell, NotificationsTab fix под schema, reminders backend+frontend, остальные 4 email-события); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** Pint+PHPStan passed (baseline регенерирован); **Pest 280/280 за 31.27 сек** (+11 от 269, 1029 assertions); frontend нетронут — Vitest/build не нужны. Реестр без изменений (notifications не было в открытых вопросах). v1.64→v1.65.*

*CLAUDE.md v1.64 от 09.05.2026. Изменения v1.64: **«Корзина» для soft-deleted сделок** — естественное продолжение stages 5/6 (soft-delete + restore). Расширяет undo-snackbar (8 сек window) до постоянного доступа к удалённым через отдельный view-mode. **(1) Backend `DealController::index`** — query-param `only_deleted=true` (boolean-like) активирует 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` (всего **269/269 за 29.12 сек**, 1009 assertions): only_deleted=true возвращает только soft-deleted (3 deals: 1 alive + 2 deleted → total=2) / без only_deleted soft-deleted скрыты (default behavior сохранён) / RLS+app-фильтр изолирует чужие удалённые сделки. **(3) Frontend `ListDealsParams.onlyDeleted?: boolean`** в типе + axios mapping `only_deleted: 'true' | undefined`. **DealsView расширен:** `trashMode` ref, `toggleTrashMode()` (clear selected + reload), `applyBulkRestoreFromTrash()` (optimistic remove from list + bulkRestoreDeals + toast). **UI changes в trash-mode:** заголовок «Сделки» → «Корзина» / btn `mdi-arrow-left К сделкам` (warning-flat) вместо `mdi-trash-can-outline Корзина` (outlined) / hide «Экспорт» + «Новая сделка» / hide chiprow filter-bar (не имеет смысла для удалённых) / info-alert «Корзина: показаны удалённые сделки» / bulk-bar заменяется: только `mdi-restore Восстановить` (success-tonal) + clear-btn (status/export/delete скрыты). **(4) Vitest +2** в DealsListIntegration (всего **321/321 за 19.60 сек**, +2 от 319): toggleTrashMode переключает trashMode + listDeals вызывается с onlyDeleted=true / applyBulkRestoreFromTrash вызывает bulkRestoreDeals + убирает из dealsState + toast «Восстановлено 2». **PHPStan baseline**: без изменений. **Production TODO остаточные:** SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 321/321 за 19.60 сек** (+2 от 319); vite build 1.04 сек; Pint+PHPStan passed; **Pest 269/269 за 29.12 сек** (+3 от 266, 1009 assertions). Реестр v1.72→v1.73.*

*CLAUDE.md v1.63 от 09.05.2026. Изменения v1.63: **Polling 30 сек** — закрывает последний unblocked production-TODO «Polling/SSE для real-time». Manual reload-btn остаётся как fast-path; polling — фоновый автообновитель. **(1) Composable `composables/usePolling.ts`** — `usePolling(loader, {intervalMs?, enabled?})`. По умолчанию 30_000 ms. **Page Visibility API integration**: при `document.hidden=true` interval останавливается + skip-проверка внутри tick (defense-in-depth); при `visibilitychange` event с `hidden=false` — restart interval + немедленный `loader()` (не ждать следующего interval'а). Cleanup на `onBeforeUnmount` — clearInterval + removeEventListener. `enabled=false` — composable не стартует совсем (для feature-flag'а). **(2) Integration в 5 view'ов:** DealsView+KanbanView (вызывают `loadDeals`), AdminTenantsView (`loadTenants`), AdminBillingView (`loadBilling`), AdminIncidentsView (`loadIncidents`). Без auth.user.tenant_id loadDeals — no-op (в самой функции return на отсутствие tenant_id), так что polling без auth ничего не делает. **(3) Vitest +6** в `usePolling.spec.ts` (всего **319/319 за 18.67 сек**, +6 от 313): через `vi.useFakeTimers` + `vi.advanceTimersByTime` для детерминированности. Тесты: вызов каждые intervalMs / default 30 сек / skip при document.hidden=true / cleanup на unmount / enabled=false → no-op / visibilitychange pause+resume с немедленным loader. **PHPStan baseline**: без изменений (frontend-only коммит). **Production TODO остаточные:** SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 319/319 за 18.67 сек** (+6 от 313); vite build 899 ms; Pint+PHPStan passed; **Pest 266/266 за 28.62 сек** (без изменений — backend не тронут). Реестр v1.71→v1.72.*

*CLAUDE.md v1.62 от 09.05.2026. Изменения v1.62: **mrr_rub в /api/admin/tenants** (этап 7) — закрывает gap из v1.66 (mock-форма имеет mrrRub, API возвращал null). **(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` (всего **266/266 за 28.39 сек**, 1001 assertion): mrr_rub='990.00' для активного тарифа не-trial / mrr_rub=null для trial / mrr_rub=null если current_tariff_id отсутствует. **(3) Frontend** — `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` (всего **313/313 за 18.83 сек**, +2 от 311): mrr_rub строка → number / mrr_rub=null → mrrRub null. **PHPStan baseline**: без изменений (warnings не добавлены). **Production TODO остаточные:** polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 313/313 за 18.83 сек** (+2 от 311); vite build 947 ms; Pint+PHPStan passed; **Pest 266/266 за 28.39 сек** (+3 от 263, 1001 assertion). Реестр v1.70→v1.71.*

*CLAUDE.md v1.61 от 09.05.2026. Изменения v1.61: **Bulk restore-flow** — completion of stage 5 (soft-delete был half-done без undo-кнопки). **(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)` → партиальный update только своих. ActivityLog event=deal.restored, context.source='bulk' для каждой ВОССТАНОВЛЕННОЙ. **`ActivityLog::EVENT_DEAL_RESTORED`** константа добавлена в model. Маршрут `Route::post('/api/deals/restore')`. **(2) Pest +7** в `DealRestoreTest` (всего **263/263 за 27.68 сек**, 998 assertions): 422 / 404 unknown / soft-delete + restore + audit / NO-OP на живых не пишет audit / defense-in-depth (свой восстановлен, чужой остался удалён) / после restore сделка снова видна в GET /api/deals / 422 пустой массив. **(3) Frontend `dealsApi.bulkRestoreDeals(payload)`** — POST-helper. **DealsView::applyBulkDelete** расширен: snapshot удалённых сделок (deep-clone manager.* nested object) сохраняется в `lastDeletedSnapshot` ref для undo. `undoBulkDelete()` async: optimistic re-insert через `dealsState.unshift` + `bulkRestoreDeals` если auth.user; на success — toast «Восстановлено N из M.»; на fail — warning. **v-snackbar** для bulk-delete увеличен с 3 до 8 сек + получил `#actions` слот с кнопкой «Восстановить» (показывается только если `lastDeletedSnapshot.length > 0`). После успешного undo snapshot очищается → кнопка пропадает. **(4) Vitest +3** в `DealsListIntegration.spec.ts` (всего **311/311 за 18.71 сек**, +3 от 308): bulk-delete + undo восстанавливает обе сделки + bulkRestoreDeals вызывается с правильными ids + lastDeletedSnapshot очищается; undo без tenant_id — bulkRestoreDeals НЕ вызывается + только локальное восстановление; undo reject → warning toast + локальное восстановление остаётся. **PHPStan baseline регенерирован**. **Production TODO остаточные:** polling/SSE; mrr_rub aggregate в /api/admin/tenants; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 311/311 за 18.71 сек** (+3 от 308); vite build 877 ms; Pint+PHPStan passed; **Pest 263/263 за 27.68 сек** (+7 от 256, 998 assertions). Реестр v1.69→v1.70.*

*CLAUDE.md v1.60 от 09.05.2026. Изменения v1.60: **soft-delete + DELETE /api/deals** (этап 5/5 — авто-план **закрыт полностью**). **(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` распределяет колонку во все 6 партиций автоматически (PG 14+). CHANGELOG_schema.md +§U с обоснованием soft-delete vs hard (CASCADE-FK от webhook_dedup_keys уничтожил бы dedup-ключи и нарушил идемпотентность §5.5). Метрики: 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 (уже удалена) НЕ пишет audit. `Deal` model получил `SoftDeletes` trait + `deleted_at` в fillable+casts — global scope автоматически добавляет `whereNull('deleted_at')` ко всем существующим query'ам (index/show/transition/update/export), без явного фильтра. Маршрут `Route::delete('/api/deals')`. **(3) Pest +8** в `DealDestroyTest` (всего **256/256 за 27.75 сек**, 977 assertions): 422/404 базовые / soft-delete + ActivityLog deal.deleted+source=bulk / defense-in-depth (свой удалён, чужой жив) / NO-OP idempotency (повторное удаление не пишет audit) / GET /api/deals скрывает soft-deleted / GET /api/deals/{id} 404 для soft-deleted / 422 пустой массив. **Quirk:** `migrate:fresh --env=testing` без `.env.testing` файла использовал `liderra` вместо `liderra_testing` — тесты падали на «column deleted_at не существует»; решение `DB_DATABASE=liderra_testing php artisan migrate:fresh` (без --env). **(4) Frontend `dealsApi.bulkDeleteDeals(payload)`** — DELETE-helper с `axios.delete('/api/deals', { data: payload })` (axios особенность: DELETE с body передаётся через `config.data`, не `payload`). **DealsView::applyBulkDelete** переписан async: optimistic local-removal (UI отвечает сразу) + `bulkDeleteDeals` если auth.user; на success — toast «Удалено N из M.»; на fail — warning toast «Не удалось удалить — изменения только локально.» + локальный update НЕ откатывается (UX-paradigma как у applyBulkStatus). Без auth — только optimistic (legacy local-mode). **(5) Vitest +3** в `DealsListIntegration.spec.ts` (всего **308/308 за 20.12 сек**, +3 от 305): bulkDeleteDeals с tenant_id + optimistic + toast «Удалено 2» / без tenant_id — НЕ вызывается / reject → warning toast + локальный update остаётся. **PHPStan baseline регенерирован**. **АВТО-ПЛАН (5 этапов) ЗАКРЫТ ПОЛНОСТЬЮ.** **Production TODO остаточные (после v1.60):** polling/SSE для real-time (на MVP — manual reload-btn); restore-flow для soft-deleted сделок (POST /api/deals/{id}/restore — отдельный коммит); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра; tenants-tariff helpers (mrr_rub в schema через JOIN на tariff_plans). **Регресс зелёный:** lint+type-check+format ✅; **Vitest 308/308 за 20.12 сек** (+3 от 305); vite build 973 ms; Pint+PHPStan passed; **Pest 256/256 за 27.75 сек** (+8 от 248, 977 assertions). Реестр v1.68→v1.69.*

*CLAUDE.md v1.59 от 09.05.2026. Изменения v1.59: **GET /api/admin/incidents + AdminIncidentsView API integration** (этап 4/5). **(1) Backend `AdminIncidentsController::index`** — GET /api/admin/incidents?type=&severity=&unresolved_only=&limit=&offset= по schema §9 `incidents_log`. ORDER BY started_at DESC. **Derived поля** в response: `incident_id` (формат `INC-YYYY-MMDD-NNNN` — год+месяц+день started_at + zero-padded id); `status` (resolved/investigating/open — derive из resolved_at/detected_at); `affected_tenants_count` (из BIGINT[] array — parsePgArray для PG-литерала); `rkn_deadline_at` (для type=data_breach без rkn_notified_at: detected_at+24h по 152-ФЗ). **`summary`** считает {open, investigating, rkn_pending, total_unresolved} 4 отдельными SELECT'ами. **(2) Pest +11** в `AdminIncidentsIndexTest` (всего **248/248 за 28.02 сек**, 951 assertion): пустой / поля + incident_id формат / derive статус (investigating/resolved) / type filter / severity filter / unresolved_only / ORDER BY started_at DESC / data_breach имеет rkn_deadline +24h / non-data_breach НЕ имеет deadline / summary.rkn_pending (только PDN-breach без notification) / limit+offset. **Quirk:** schema saas_admin_users использует `full_name` (не `first_name`/`last_name`) + не имеет updated_at — сразу исправлено в helper insert. **(3) Frontend** — `api/admin.ts::listAdminIncidents(params)` с типизированными ApiAdminIncident/Summary/Response (severity narrowed на enum, остальное — string). **AdminIncidentsView** переписан: новый `IncidentRow` interface унифицирует mock и API форму (mock-`category` ↔ API-`type`, mock-`title` ↔ API-`summary`); reactive `rowsState` (default = ADMIN_INCIDENTS) + `stats`; loadIncidents() async на onMounted замещает mock на API; на fail — fetchError + warning alert + MOCK fallback; reload-btn. Maps категорий (categoryMap/statusInfo/severityInfo) переписаны на функции с fallback'ами на новые slug'и. РКН pending chip учитывает оба варианта `pdn_breach`/`data_breach`. **(4) Vitest +5** в `AdminIncidentsViewApi.spec.ts` (всего **305/305 за 20.59 сек**, +5 от 300): listAdminIncidents на mount / replace rowsState + summary с rkn_deadline сохранением / reject → fetchError + alert + MOCK fallback / reload-btn двойной вызов / РКН pending chip отображается для data_breach без rkn_notified. **PHPStan baseline регенерирован**. **Production TODO остаточные:** этап 5 (soft-delete migration + DELETE /api/deals); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 305/305 за 20.59 сек** (+5 от 300); vite build 1.05 сек; Pint+PHPStan passed; **Pest 248/248 за 28.02 сек** (+11 от 237, 951 assertion). Реестр v1.67→v1.68.*

*CLAUDE.md v1.58 от 09.05.2026. Изменения v1.58: **GET /api/admin/billing + AdminBillingView API integration** (этап 3/5). **(1) Backend `AdminBillingController::index`** — GET /api/admin/billing?search=. Aggregates по `balance_transactions` за текущий календарный месяц по tenant'у (один SUM-запрос с CASE WHEN type IN ('topup','lead_charge'); 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.amount_rub за месяц), overdue_count (balance<0 OR chargeback>0), refunds_count_30d (count balance_transactions type=refund ≥now-30days). **Quirk:** schema-колонка называется `tariff_plans.price_monthly` (НЕ `price_rub_monthly`) — обнаружено первым прогоном Pest, исправлено сразу. **(2) Pest +9** в `AdminBillingIndexTest` (всего **237/237 за 27.69 сек**, 926 assertions): пустой / поля + tariff JOIN / aggregates topups+charges за текущий месяц / прошлый месяц НЕ попадает в monthly / summary.overdue (balance<0 || chargeback>0) / summary.refunds_count_30d (старые >30 дней не считаются) / summary.total_mrr (только не-trial с тарифом) / search ILIKE / soft-deleted скрыт. **(3) Frontend** — `api/admin.ts::listAdminBilling(search)` с типизированными 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/balance<0||chargeback>0→overdue/active). На fail — fetchError + warning alert + MOCK остаются. Reload-btn. **Tariff/status maps** обобщены: `tariffLabel(s)` возвращает known mock-перевод или as-is (backend уже отдаёт «Команда»); `statusInfo(s)` возвращает known meta или fallback с label=s/color=default — устойчиво к новым slug'ам. **(4) Vitest +4** в `AdminBillingViewApi.spec.ts` (всего **300/300 за 18.41 сек**, +4 от 296): listAdminBilling на mount / replace rowsState + summary с string→number конверсией + status derive (balance<0→overdue) / reject → fetchError+alert+MOCK fallback / reload-btn двойной вызов. **PHPStan baseline регенерирован**. **Production TODO остаточные:** этапы 4-5 авто-плана (admin/incidents endpoint + soft-delete migration + DELETE /api/deals); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 300/300 за 18.41 сек** (+4 от 296); vite build 925 ms; Pint+PHPStan passed; **Pest 237/237 за 27.69 сек** (+9 от 228, 926 assertions). Реестр v1.66→v1.67.*

*CLAUDE.md v1.57 от 09.05.2026. Изменения v1.57: **GET /api/admin/tenants + AdminTenantsView API integration** (этап 2/5 авто-плана). **(1) Backend `AdminTenantsController::index`** — saas-admin lookup тенантов с фильтрами `status`/`search`/`limit`/`offset` (без auth — saas-admin SSO ⏸ Б-1). LEFT JOIN на `tariff_plans` для `tariff_name`. ORDER BY `last_activity_at DESC, id`. Soft-deleted (deleted_at!=null) исключены. Поля: id/subdomain/organization_name/contact_email/status/balance_rub/balance_leads/is_trial/last_activity_at/tariff_id/tariff_name/desired_daily_numbers/chargeback_unrecovered_rub/created_at. **`stats`** агрегирует {total, active, trial, overdue} одним SELECT'ом без фильтров — `overdue` = `chargeback_unrecovered_rub > 0 OR balance_rub < 0`. **(2) Pest +8** в `AdminTenantsIndexTest` (всего **228/228 за 25.22 сек**, 906 assertions): 200 + пустой / все поля / status filter / search ILIKE по name+subdomain+email / ORDER BY last_activity_at DESC / stats (4 счётчика) / soft-deleted скрыт / limit+offset. **(3) Frontend** — `api/admin.ts::listAdminTenants(params)` с типизированными ApiAdminTenant/Stats/Response. **`composables/adminTenantsMapper.ts::mapApiAdminTenant`** — converter API → UI-формат (`AdminTenant` из `mockTenants.ts` ожидает другую форму): status derive (is_trial=true → 'trial', balance<0 || chargeback>0 → 'overdue', schema-status as-is для active/suspended), `inn=''` (нет в API — живёт в legal_entities/invoices), `code=subdomain`, tariff_name → known TenantTariff clamp с fallback на 'Trial', `todayActual=0` / `mrrRub=null` (требуют JOIN на deals/balance_transactions, добавим отдельно), activitySince через formatRelative(last_activity_at). **AdminTenantsView**: reactive `tenantsState` + `stats` (default = MOCK_TENANTS / MOCK_STATS); `loadTenants()` async на onMounted → replace через splice; на fail — `fetchError=true` + warning v-alert + MOCK остаются. Reload-btn `data-testid="reload-btn"` с loading-state. **(4) Vitest +13** в `AdminTenantsViewApi.spec.ts` (всего **296/296 за 18.91 сек**, +13 от 283): listAdminTenants на mount / replace state + stats / reject → fetchError + alert + MOCK fallback / reload-btn двойной вызов; mapper +9 (organization_name→name, subdomain→code / inn пуст / is_trial→trial / chargeback→overdue / balance<0→overdue / suspended→suspended / balance_rub строка→number / activitySince «10 мин назад» / null → «—»). **PHPStan baseline регенерирован**. **Production TODO остаточные:** этапы 3-5 (admin/billing+incidents endpoints + soft-delete migration); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 296/296 за 18.91 сек** (+13 от 283); vite build 1.02 сек; Pint+PHPStan passed; **Pest 228/228 за 25.22 сек** (+8 от 220, 906 assertions). Реестр v1.65→v1.66.*

*CLAUDE.md v1.56 от 09.05.2026. Изменения v1.56: **PATCH /api/deals/{id} + comment-editor в DealDetailDrawer** — drawer переходит из read-only в редактируемый режим. **(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 (manager_id чужого tenant'а → 422) и status validation (slug должен существовать в lead_statuses → 422) — те же что в store/transition. RLS-обёртка + defense-in-depth `where(tenant_id)` → 404 для чужой сделки. Маршрут `Route::patch('/api/deals/{id}', 'update')->where('id', '[0-9]+')`. **(2) Pest +10** в `DealUpdateTest` (всего **220/220 за 25.64 сек**, 871 assertion): 422 без tenant_id / 404 unknown / 404 чужая сделка / comment update + deal.commented audit / manager update + deal.assigned audit + assigned_at=NOW / status update + deal.status_changed audit / 422 неизвестный slug + НЕ обновляет / 422 manager чужого tenant'а / NO-OP не пишет audit / комбинированно (comment+status одним запросом) → 2 audit log записи. **(3) Frontend `api/deals.ts::updateDeal(id, payload)`** — типизированный PATCH-helper с `ensureCsrfCookie` (mutating endpoint). **DealDetailDrawer:** добавлена секция «Комментарий» (показывается ТОЛЬКО при наличии tenantId — без auth остаётся read-only) с `v-textarea` (auto-grow, counter=5000, hide-details) + Save-btn `mdi-content-save-outline` (loading во время save). `commentDraft` (ref) populates из `getDeal` response (`deal.comment ?? ''`). `saveComment()` async вызывает `updateDeal` с `comment: commentDraft || null` + на success — toast «Комментарий сохранён» + reload events (новый `deal.commented` появляется в timeline); на fail — `commentSaveError=true` + warning toast «Не удалось сохранить — попробуйте позже». `v-snackbar` reuses `commentSaveError` для color=warning. **(4) Vitest +3** в `DealDetailDrawerApi.spec.ts` (всего **283/283 за 18.13 сек**): saveComment вызывает updateDeal с правильным payload + toast success + reload events (getDeal вызвался дважды); saveComment reject → commentSaveError=true + toast warning «Не удалось»; comment-section НЕ рендерится без tenantId (read-only mode для legacy local-режима). **PHPStan baseline регенерирован**. **Production TODO остаточные:** GET /api/admin/{tenants,billing,incidents} (этапы 2-4 текущего плана); soft-delete + DELETE /api/deals (этап 5, требует миграцию); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 283/283 за 18.13 сек** (+3 от 280); vite build 1.12 сек; Pint+PHPStan passed; **Pest 220/220 за 25.64 сек** (+10 от 210, 871 assertion). Реестр v1.64→v1.65.*

*CLAUDE.md v1.55 от 09.05.2026. Изменения v1.55: **GET /api/lead-statuses + Pinia store** — заменяет static-снапшот в коде на live-данные из БД (включая custom slug'и, добавленные после deployment'а). **(1) Backend** — `App\Models\LeadStatus` (PK=`slug` string, `incrementing=false`, `keyType='string'`, `timestamps=null`); `LeadStatusController::index` — 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` (всего **210/210 за 24.59 сек**, 840 assertions): 200 + не пустой / все 14 системных slug'ов из seed (new..final_missed) / поля slug/name_ru/color_hex/sort_order/is_system / sort_order ASC / кастомный slug добавленный после seed возвращается. **(3) Frontend** — `api/leadStatuses.ts::listLeadStatuses` (GET helper); `stores/leadStatuses.ts::useLeadStatusesStore` Pinia setup-store: `statuses` ref<LeadStatus[]> (default = `LEAD_STATUSES` snapshot для UI без fetch'а), `load(force=false)` идемпотентен (повторный вызов → no-op если loaded), `bySlug` computed Map для O(1), `findBySlug(slug)` helper. На fail — snapshot остаётся, `fetchError=true`. **(4) Integration в 3 view-компонента:** DealsView заменил `LEAD_STATUSES` импорт на `leadStatusesStore.statuses` (computed `leadStatuses`) для bulk-status menu и `statusBySlug` (computed Map из store getter); KanbanView заменил на `leadStatuses` computed для column-iteration + count display + safe-access `dealsByStatus[slug] || []` в template (защита от custom slug'а из API без seeded column); DealDetailDrawer переписал `LEAD_STATUSES.find(...)` → `store.findBySlug(...)`. Оба view'а вызывают `leadStatusesStore.load()` в `onMounted` (рядом с loadDeals). **`reduce` для init `dealsByStatus`** в KanbanView оставлен на snapshot (всегда seeded 14; новые custom-колонки появятся после API-load — empty-array fallback в template). **(5) Vitest +7** в `leadStatusesStore.spec.ts` + 2 spec'а DealDetailDrawer'а получили `setActivePinia(createPinia())` в beforeEach (без этого `getActivePinia()` падает в jsdom): initial state snapshot / findBySlug returns existing / findBySlug null для unknown / load() success — replace + loaded=true / load() reject — fetchError + snapshot остаётся / load() идемпотентен (1 запрос на 2 вызова) / load(force=true) — 2 запроса. Всего **280/280 за 19.44 сек** (+7 от 273). **Production TODO остаточные:** polling/SSE для real-time обновления (на MVP — manual reload-btn); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 280/280 за 19.44 сек** (+7 от 273); vite build 1.17 сек (KanbanView lazy-chunk 182.22→182.28 KB — Pinia-getter overhead); Pint+PHPStan passed; **Pest 210/210 за 24.59 сек** (+5 от 205, 840 assertions). Реестр v1.63→v1.64.*

*CLAUDE.md v1.54 от 09.05.2026. Изменения v1.54: **GET /api/deals/{id} + интеграция DealDetailDrawer на реальный ActivityLog**. **(1) Backend `DealController::show(int $id)`** — возвращает `{deal, events}` для drawer'а. RLS-обёртка + defense-in-depth `where(tenant_id)` (как в index/transition); 404 если сделка чужая или не существует. `deal` — extended (project_name + manager_name/initials через `ManagerController::format*` + comment + assigned_at). `events` — последние **50** записей `activity_log` фильтрованных по `(tenant_id, deal_id)` ORDER BY created_at DESC, с актором (user через `belongsTo`-relation). Маршрут `Route::get('/api/deals/{id}', 'show')->where('id', '[0-9]+')`. **(2) Pest +8** в `tests/Feature/DealShowTest.php` (всего **205/205 за 24.19 сек**, 812 assertions): 422 без tenant_id / 404 unknown tenant / 404 несуществующая сделка / 404 чужая сделка (RLS-проверка через postgres superuser BYPASSRLS работает за счёт app-фильтра) / deal-relations (project_name + manager_name «Иван П.» + initials «ИП» + comment) / events ORDER BY created_at DESC (status_changed свежее createde) + actor.name + actor=null для system-event с user_id=null / RLS+app-фильтр НЕ показывает события с `deal_id` совпадающим у чужого tenant'а / лимит 50 событий (60 записей → возвращаем 50). **(3) Frontend `api/deals.ts::getDeal(id, tenantId)`** — типизированный helper с `ApiDealEvent`/`ApiDealDetail`/`GetDealResponse` interfaces; БЕЗ ensureCsrfCookie (GET-only). **`composables/dealsApiMapper.ts::mapApiDealEvent(api, now=new Date())`** — converter ApiDealEvent → DealEvent (UI-формат): `event` slug clamp на known types (`deal.{created,status_changed,viewed,commented,assigned,balance_charged}`) с fallback на `'deal.viewed'` (generic-icon); `actor` маппится 1:1; `minutesAgo = max(0, floor((now - created_at) / 60_000))`; `detail` зависит от type — для `status_changed` строим `«from → to»` из context, для `created` — `«Лид принят (источник: …)»`, для остальных — JSON-сводка контекста. **(4) DealDetailDrawer** получил optional `tenantId` prop. `watch([open, deal.id, tenantId])` с `immediate: true` — на open=true вызывает `loadEvents()`. Если оба (deal + tenantId) есть → `getDeal(deal.id, tenantId)` → `events.value = events.map(mapApiDealEvent)`. На fail → `eventsFetchError=true` + `v-alert type=warning «Backend недоступен — показаны mock-события»` (data-testid=`events-fetch-error-alert`) + fallback на `MOCK_EVENTS`. Без tenantId — никогда не fetch'им, MOCK_EVENTS как раньше. DealsView и KanbanView передают `:tenant-id="auth.user?.tenant_id"`. **(5) Vitest +4** в `DealDetailDrawerApi.spec.ts` (всего **273/273 за 20.76 сек**, +4 от 269): без tenantId — getDeal не вызывается + MOCK_EVENTS видны / с tenantId — getDeal вызывается + events заменены + «new → paid» виден / reject → eventsFetchError + alert + MOCK_EVENTS fallback / open=false → НЕ вызывается. PHPStan baseline регенерирован для +новых ignored Pest TestCall warnings. **Production TODO остаточные:** polling/SSE для real-time обновления (на MVP — manual reload-btn); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 273/273 за 20.76 сек** (+4 от 269); vite build 1.12 сек (KanbanView lazy-chunk 182.17→182.22 KB — DealDetailDrawer импорт `mapApiDealEvent` shared); Pint+PHPStan passed; **Pest 205/205 за 24.19 сек** (+8 от 197, 812 assertions). Реестр v1.62→v1.63.*

*CLAUDE.md v1.53 от 09.05.2026. Изменения v1.53: **XLSX-export через PhpSpreadsheet** — закрыт TODO «реальный XLSX-export» из v1.52. Установлен `phpoffice/phpspreadsheet:^5.0` (v5.7.0). Endpoint POST /api/deals/export расширен опциональным параметром `format` (default 'csv' для backward-compat, 'xlsx' = новая ветка). Backend `buildXlsx()`: `Spreadsheet` + `setTitle('Сделки')` + `setCellValue('A1'...G1')` для headers + `getStyle('A1:G1')->getFont()->setBold(true)` + `setAutoSize(true)` для всех колонок. `Xlsx` writer пишет в `php://output` через `ob_start/ob_get_clean` чтобы вернуть бинарную строку из контроллера. Content-Type `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` + Content-Disposition `attachment; filename="deals_export_YYYY-MM-DD.xlsx"`. **Quirk:** PhpSpreadsheet 5.x удалил deprecated-метод `setCellValueByColumnAndRow($col, $row, $val)` — пришлось мигрировать на A1-нотацию (`setCellValue('A2', $val)`). Обнаружено в первом тестовом прогоне (500 на endpoint'е), исправлено сразу. **Pest +4** в `DealCreateTest` (всего **197/197 за 26.05 сек**, 784 assertions): xlsx возвращает binary с правильным Content-Type + magic bytes "PK\x03\x04" (XLSX = ZIP) + размер >2KB; распаковка через PhpSpreadsheet IOFactory::createReader('Xlsx') → sheet `Сделки` + A1='ID' + B1='Имя' (bold=true) + A2/B2/C2 = реальные данные сделки; 422 на неизвестный format ('pdf'); по умолчанию (без format) — backward-compat CSV. **Frontend** — `api/deals.ts` разделён на 2 функции: `exportDeals` (CSV, returns string, ставит format='csv' в payload) + `exportDealsXlsx` (XLSX, returns Blob, responseType='blob' для axios). DealsView `applyBulkExport(format='xlsx')` async получил параметр format с default 'xlsx' (UX prefer Excel-friendly формат, особенно RU-локаль с 1С). XLSX-ветка вызывает `exportDealsXlsx` → `triggerBlobDownload(blob, filename)` (новый helper, отделён от `triggerCsvDownload` чтобы Blob не конструировался дважды); CSV-ветка через старый `exportDeals`/`triggerCsvDownload`. На fail → fallback на local CSV (даже если запросили xlsx — без backend'а xlsx не построим). **Vitest +3** в `DealsListIntegration.spec.ts` (всего **269/269 за 18.49 сек**): xlsx default вызывает `exportDealsXlsx` (НЕ `exportDeals`) + триггерит download через blob:url + toast «XLSX»; csv-вариант вызывает `exportDeals` (НЕ Xlsx) + toast «CSV»; xlsx reject → fallback на local CSV + toast «Backend недоступен». PHPStan baseline регенерирован. **Production TODO остаточные:** polling/SSE для real-time обновления (на MVP — manual reload-btn); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 269/269 за 18.49 сек** (+3 от 266); vite build 982 ms; Pint+PHPStan passed (baseline регенерирован); **Pest 197/197 за 26.05 сек** (+4 от 193, 784 assertions). Реестр v1.61→v1.62.*

*CLAUDE.md v1.52 от 09.05.2026. Изменения v1.52: **Bulk-transition + reload-btn** — закрывает «UI меняет статус, но изменения не сохраняются на backend» gap из v1.51 + добавляет manual reload как замену polling/SSE до прихода long-poll'а. **(1) Backend `DealController::transition`** — POST /api/deals/transition `{tenant_id, ids: [int...], status: slug}`. Валидация: `ids` обязателен 1..1000 ints, `status` обязателен ≤50 chars + `DB::table('lead_statuses')->where('slug', X)->exists()` (422 «Slug не найден в lead_statuses» если нет). `lead_statuses` — глобальная таблица (НЕ tenant-aware), system+custom slug'и в одном scope. RLS-обёртка `SET LOCAL app.current_tenant_id` + defense-in-depth `where('tenant_id', $tenantId)->whereIn('id', $ids)` — на тестах postgres superuser обходит RLS, app-фильтр гарантирует что чужие id не апдейтятся (partial-update: `updated < requested` если часть id принадлежит другому tenant'у). `ActivityLog::create([event=deal.status_changed, context={from, to, source=bulk}])` для каждой ИЗМЕНЁННОЙ сделки (NO-OP — старый==новый — НЕ пишется в audit log, иначе спам при «обновить тот же статус»). Ответ: `{updated, requested, status}`. Маршрут `Route::post('/api/deals/transition')`. **(2) Pest +7** в `tests/Feature/DealTransitionTest.php` (всего **193/193 за 23.27 сек**, 767 assertions): 422 missing fields / 404 unknown tenant / 422 неизвестный slug + сделка не апдейтится / batch update 3 сделок + 3 ActivityLog с правильным context.from/to/source / NO-OP не пишет ActivityLog / defense-in-depth (передаём 2 id из разных tenant'ов — обновляется только свой, чужой остаётся в исходном статусе) / 422 пустой массив ids. **(3) Frontend `dealsApi.transitionDeals(payload)`** — типизированный helper, `ensureCsrfCookie` обязателен (mutating). **`applyBulkStatus` в DealsView** переписан с sync на async: optimistic local-update (UI отвечает сразу), затем backend-вызов если есть auth.user.tenant_id. На success — `statusToast «Обновлено N из M.»`. На fail — `«Не удалось сохранить статус — изменения только локально.»` + локальный update НЕ откатывается (UX rationale: пользователь видит что хотел, перезагрузит чуть позже; auto-rollback запутает больше чем поможет). Без auth.user — только optimistic, API не вызывается (legacy local-mode сохранён). **(4) Reload-btn** в DealsView и KanbanView — outlined button «Обновить» с mdi-refresh, привязан к `loadDeals` action. В DealsView у btn'а `:loading="loading"` chip — крутится во время fetch'а. **(5) Vitest +5** (всего **266/266 за 18.16 сек**): reload-btn в DealsView (listDeals вызывается дважды) + applyBulkStatus с tenant_id (transitionDeals вызывается + optimistic update до завершения + toast «Обновлено 2») + applyBulkStatus БЕЗ tenant_id (transitionDeals НЕ вызывается + только локально) + applyBulkStatus reject (toast warning + локальный update НЕ откатывается); reload-btn в KanbanView (тот же 2× listDeals). PHPStan baseline регенерирован. **Production TODO остаточные:** реальный XLSX-export через PhpSpreadsheet; polling/SSE для real-time (на MVP — manual reload); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 266/266 за 18.16 сек** (+5 от 261); vite build 1.06 сек (KanbanView lazy-chunk 181.98→182.17 KB — добавил `dealsApi.transitionDeals` импорт через DealsView, но KanbanView его не тянет напрямую — рост от reload-btn shared chunk); Pint+PHPStan passed; **Pest 193/193 за 23.27 сек** (+7 от 186, 767 assertions). Реестр v1.60→v1.61.*

*CLAUDE.md v1.51 от 09.05.2026. Изменения v1.51: **GET /api/deals + замена MOCK_DEALS** — закрыт TODO (c) из v1.50 (опциональный пункт, но снимает дрейф между UI и backend на time-критичных flow вроде «увидеть свежие лиды»). **(1) Backend `DealController::index`** — list-endpoint с фильтрами и relations: `tenant_id` query-param (422/404 как в `ManagerController`), массив `status_in[]` (whereIn по `status`), `project_id` / `manager_id` (точное совпадение), `search` (ILIKE по phone+contact_name OR-block), `limit` clamp [1..500] default 100, `offset` default 0. ORDER BY `received_at DESC, id DESC`. Eloquent `with(['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 — explicit-фильтр гарантирует изоляцию). Ответ: `{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`/`manager_initials` форматируются через `ManagerController::formatName/formatInitials` (нашли расхождение, что эти helper'ы static — re-use OK). `cost` НЕ возвращаем (живёт в `supplier_lead_costs.cost_rub` partition'е, лишний JOIN под limit=200 строк дешевле клиентского запроса). Маршрут `Route::get('/api/deals', 'index')` рядом с `store/export`. **(2) Pest +12** в `tests/Feature/DealIndexTest.php` (всего **186/186 за 22 сек**, 742 assertions): 422 без tenant_id / 404 unknown / пустой список / project_name + manager_name + initials присутствуют + ISO received_at / RLS-изоляция (Deal чужого tenant'а НЕ возвращается — defense-in-depth where отрабатывает) / ORDER BY received_at DESC (3 сделки в правильном порядке) / status_in[] фильтр (передаём 2 status'а через `?status_in[]=new&status_in[]=paid` — Laravel queryString парсит в массив) / project_id точное совпадение / manager_id точное совпадение / search ILIKE case-insensitive (Соколова / 903 / `сокол`) / limit+offset (5 сделок, limit=2 offset=1) / manager_name+initials = null когда manager_id null. **(3) Frontend `api/deals.ts::listDeals`** — типизированный axios-helper с `ApiDeal` interface + `ListDealsParams` (tenantId/statusIn/projectId/managerId/search/limit/offset → camelCase в DTO, snake_case на wire через axios `params`-mapping). Без `ensureCsrfCookie` (GET-only, CSRF только на mutating). **`composables/dealsApiMapper.ts::mapApiDeal(api, now=new Date())`** — converter ApiDeal → MockDeal: `id/phone/statusSlug/cost(=0)` 1:1; `name = contact_name ?? phone` (fallback на телефон когда контакт неизвестен); `project = project_name ?? '—'`; `manager = {name: 'Не назначен', initials: '—'}` если `manager_id=null`; `receivedMinutesAgo = max(0, floor((now - received_at) / 60_000))` — clamp на 0 чтобы не было отрицательных при clock-skew. **`cost`=0** на всех картах (отдельного endpoint'а на сделку нет, добавим при необходимости через JOIN supplier_lead_costs). **(4) DealsView/KanbanView интеграция** — `onMounted(loadDeals)` async-вызывает `dealsApi.listDeals({tenantId: auth.user.tenant_id, limit: 200/500})` если auth.user.tenant_id есть; на success — replace `dealsState`/`dealsByStatus` через splice (сохраняет reactive ref). На fail — `fetchError=true`, `v-alert type=warning «Backend недоступен — показаны mock-данные»` с `data-testid="fetch-error-alert"`, MOCK_DEALS остаются как fallback. Без auth-state — listDeals НЕ вызывается, MOCK_DEALS показываются как и раньше (Vitest без auth setup продолжает работать без mock'а). KanbanView в loadDeals сначала очищает все колонки (splice 0..length для каждой), затем распределяет по `statusSlug`. **(5) Vitest +14** (всего **261/261 за 19.62 сек**): `dealsApiMapper.spec.ts` 8 (обязательные поля 1:1 / contact_name fallback на phone / manager_name+initials default / project_name=— default / cost всегда 0 / receivedMinutesAgo=30 для 30 мин назад / clamp на 0 при future timestamp / received_at=null → 0); `DealsListIntegration.spec.ts` 6 (DealsView без tenant_id — listDeals НЕ вызывается + MOCK_DEALS остаются / DealsView с tenant_id — listDeals вызывается + dealsState replaced на 2 API-сделки / DealsView reject → fetchError=true + alert виден + MOCK_DEALS fallback; KanbanView те же 3 сценария). vi.mock на `api/deals` сохраняет original-импорт через `importOriginal` чтобы `ensureCsrfCookie` остался живым для других тестов. PHPStan baseline регенерирован. **Production TODO (после v1.51):** реальный XLSX-export через PhpSpreadsheet (CSV достаточен на MVP); polling/SSE для real-time обновления списка сделок (на MVP — manual reload); SaaS-admin auth (Yandex 360 SSO ⏸ Б-1); Pest browser-mode ⏸ инфра. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 261/261 за 19.62 сек** (+14 от 247); vite build 989 ms (KanbanView lazy-chunk вырос с 180.53→181.98 KB из-за `mapApiDeal` импорта и onMounted); Pint+PHPStan passed; **Pest 186/186 за 22 сек** (+12 от 174, 742 assertions). Реестр v1.59→v1.60.*

*CLAUDE.md v1.50 от 09.05.2026. Изменения v1.50: **SupplierResolver service-extract** — закрыт TODO (a) из v1.49. Общая логика lookup активного supplier'а через `project_suppliers` m2m (фильтры `is_active=true`+`is_active=true`, ORDER BY `sort_order, id`) была дублирована между `ProcessWebhookJob::resolveSupplierId` (webhook-flow) и `DealController::resolveSupplierId` (manual-create) — 11 одинаковых строк query-builder'а на 2 файла. Решение: `App\Services\SupplierResolver` с двумя методами — `resolveForProject(Project): ?int` (тот же DB::table query, что был раньше) + `costRubSnapshot(int $supplierId): string` (вынесенный snapshot цены `cost_rub` для записи в `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::resolveSupplierId()` (private 14 строк) + `DealController::resolveSupplierId()` (private 14 строк) + локальные `DB::table('suppliers')->value('cost_rub')` в обоих файлах (теперь через `$resolver->costRubSnapshot()`). **Pest +8** в `tests/Feature/Services/SupplierResolverTest.php` (всего **174/174 за 21.46 сек**, 708 assertions): null когда нет связей; единственный активный supplier; пропуск inactive supplier; пропуск inactive m2m-связи; ORDER BY sort_order (low > high); null если все связи inactive; изоляция по project_id (один supplier на двух проектах не проявляется); costRubSnapshot формат '137.50'. Helpers `seedSupplier`/`attachSupplier` — top-level functions в файле теста (не пересекаются с `seedSupplierForProject` в ProcessWebhookJobTest). **Quirk** — `Project::factory()->create(['type' => 'websites'])` падает на CHECK constraint `projects_type_check` (allowed: webhook|manual|import); factory default = 'webhook' — лишний override убран. **PHPStan baseline** регенерирован для +30 ignored Pest TestCall warnings (новый файл). **Регресс зелёный:** Pint+PHPStan passed (baseline регенерирован); **Pest 174/174 за 21.46 сек** (+8 от 166, 708 assertions); **Vitest 247/247 за 17.53 сек** (нетронут — backend-only refactor). Реестр v1.58→v1.59.*

*CLAUDE.md v1.49 от 09.05.2026. Изменения v1.49: **3 lookups + integrity-fix** после backend-completion v1.48. **(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 (на prod из middleware), 422 без него, 404 unknown tenant, RLS-обёртка через `SET LOCAL app.current_tenant_id` в DB::transaction. **Manager FK guard** в `DealController::store` — если `manager_id` передан, проверяем `User::where(id, manager_id)->where(tenant_id, tenant->id)->whereNull(deleted_at)->where(is_active, true)->exists()`; если не принадлежит tenant'у или не активен — 422 с ошибкой по полю `manager_id`. Это закрывает security-gap: иначе можно было назначить чужого менеджера на свою сделку. **(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'а: name из v-select (return-object) → backend-id. На fail (network) — silent fallback на mock (UI работает дальше). Submit передаёт `manager_id: managerIdByName.get(manager.name) ?? undefined`. **(3) SupplierLeadCost для manual-leads.** В `DealController::store` транзакции после Deal::create вызываем `resolveSupplierId($project)` — точная копия логики из `ProcessWebhookJob::resolveSupplierId` (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 только при закупке у supplier'а через webhook); cost-аналитика всё равно нужна для отчётности (owner проекта мог купить лид у поставщика и ввести руками). На production — извлечь `resolveSupplierId` в `App\Services\SupplierResolver` чтобы Job и Controller разделяли логику + system_settings fallback. **Pest +18** (всего **166/166 за 22.11 сек**, 699 assertions): LookupsTest 8 (managers active + initials fallback + 422 / 404 + projects + manager FK guard 3 — чужой/inactive/active); DealCreateTest +2 (SupplierLeadCost создан с snapshot cost_rub / без supplier — graceful skip). Старый тест manager_id=42 переписан на User::factory()->for($tenant)->create()->id чтобы пройти FK guard. **Vitest +2** (всего **247/247 за 16.32 сек**): NewDealDialog +2 (loadLookups вызывает listProjects+listManagers + populates refs + map / submit передаёт backend manager_id из mapping). Vi.mock получил listProjects/listManagers с default `Promise.resolve([])` — старые тесты (без tenantId) не вызывают lookups, fallback на mock работает. **PHPStan baseline** регенерирован для +28 ignored Pest TestCall warnings (LookupsTest + DealCreateTest расширения). **Production TODO остаточные:** (a) `resolveSupplierId` в Service-класс (рефактор Job + Controller); (b) реальный XLSX-export через PhpSpreadsheet (CSV пока достаточен); (c) GET /api/deals для замены MOCK_DEALS в DealsView/KanbanView (опционально — на MVP local-state ok); (d) SaaS-admin auth (Yandex 360 SSO ⏸ Б-1). **Регресс зелёный:** lint+type-check+format ✅; **Vitest 247/247 за 16.32 сек** (+2); vite build 951 ms; Pint+PHPStan passed (baseline регенерирован); **Pest 166/166 за 22.11 сек** (+10 от 156, 699 assertions). Реестр v1.57→v1.58.*

*CLAUDE.md v1.48 от 09.05.2026. Изменения v1.48: **3 backend-completion изменения** после tightening v1.47. **(1) POST /api/deals — manual create endpoint для NewDealDialog.** `DealController::store` валидирует `tenant_id/project_name/phone` (required) + `contact_name/status/manager_id/comment` (optional). Резолвит/создаёт `Project` через `firstOrCreate(tenant_id+name, type='manual')`. Создаёт `Deal` с `received_at=NOW()`, `source_crm_id=NULL` (отличие от webhook'а), `assigned_at=NOW()` если `manager_id` передан. Транзакция + RLS-обёртка `SET LOCAL app.current_tenant_id` (PgBouncer-safe). Manual-create НЕ списывает баланс (не закупка у поставщика), НЕ применяет антифрод-дедуп (admin знает что вводит), НЕ создаёт SupplierLeadCost. Пишет ActivityLog с `context.source=manual`. **NewDealDialog.vue** получил optional `tenantId` prop — если передан, submit делает `dealsApi.createDeal()`, на success deal возвращается с реальным backend-id; на network/500-error — fallback на local-id + `submit-error-alert` warning + dialog остаётся открытым. Чистый local-mode (без tenantId) сохранён для тестов и legacy. DealsView/KanbanView получили `useAuthStore` + передают `:tenant-id="auth.user?.tenant_id"`. **(2) `webhook_hmac_required` flag в system_settings.** Добавлен ключ в seed `db/schema.sql:2200` (`'webhook_hmac_required', 'false', 'bool'` — default backward-compat). `WebhookReceiveController::isHmacRequired()` private helper читает значение через `SystemSetting::find` (без записи → false). При `true`: запрос без `X-Webhook-Signature` → 401. При `false`: header опционален (если пришёл — verify, иначе пропускаем). Pest +3: required+missing → 401, required+valid HMAC → 202, false (default) → 202 без header. **(3) POST /api/deals/export — CSV endpoint backend-side.** `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` со взятым CSV; на fail — fallback на `buildLocalCsv()` (тот же flow что в v1.47, но вынесен в отдельную функцию). На каждом флоу — toast о результате. **api/deals.ts** новый файл с `createDeal`/`exportDeals` (responseType: 'text' для CSV string). **Pest +15** (всего **156/156 за 20.27 сек**, 675 assertions): DealCreateTest 12 (8 store + 4 export); WebhookReceiveTest +3 hmac_required. **Vitest +3** (всего **245/245 за 17.07 сек**): NewDealDialog +3 (без tenantId — local mode; с tenantId+success — backend-id; с tenantId+error — fallback+warning); DealsView/KanbanView spec'ы получили `setActivePinia(createPinia())` (auth-store нужен для tenant_id). **Quirks:** (a) PHPStan ругался на `Deal->id === null` (Eloquent типизирует id как int) — убрал лишнюю проверку. (b) `String.fromCharCode(0xFEFF)` в JS / `"\u{FEFF}"` в PHP — оба работают, литерал заблокирован ESLint no-irregular-whitespace. (c) RLS-изоляция export'а тестируется отдельно через testing_rls_user (NOLOGIN без BYPASSRLS) — в DealCreateTest используется postgres superuser (BYPASSRLS), поэтому RLS-проверка тут была бы false-positive — заменил на тест фильтрации по `whereIn(ids)`. **Production TODO остаточные (после v1.48):** Manager lookup в DealController (сейчас manager_id передаётся клиентом без проверки tenant-membership); replace MOCK_MANAGERS на API GET /api/managers; SupplierLeadCost для manual-leads (при наличии supplier'а у проекта); реальный XLSX-export через PhpSpreadsheet (CSV пока достаточен); SaaS-admin auth (Yandex 360 SSO ⏸ Б-1). **Регресс зелёный:** lint+type-check+format ✅; **Vitest 245/245 за 17.07 сек** (+3 от 242); vite build 1.04 сек; Pint+PHPStan passed (baseline регенерирован); **Pest 156/156 за 20.27 сек** (+15 от 141, 675 assertions). Реестр v1.56→v1.57.*

*CLAUDE.md v1.47 от 09.05.2026. Изменения v1.47: **3 production-tightening изменения** после 7-фичного пакета v1.46. **(1) HMAC + per-token rate-limit для webhook receive endpoint** — закрыты 2 production-TODO из v1.46. `WebhookReceiveController::receive` теперь делает 3 проверки в порядке: tenant lookup → rate-limit → HMAC → валидация payload. **HMAC**: опциональный header `X-Webhook-Signature: sha256=<hex>`, верификация через `hash_hmac('sha256', raw_body, webhook_token)` + `hash_equals` (constant-time compare против timing attacks). На MVP — backward-compat: header отсутствует → пропускаем (для prod через `system_settings.webhook_hmac_required` сделаем обязательным). Невалидная подпись → 401 (не 422 — это auth issue). **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 (Laravel RateLimiter работает per-decay-window). На превышении — 429 + `Retry-After` header + `retry_after` в JSON. Rate-limit ключ изолирован per-tenant, hit ставится ДО валидации payload (иначе можно обойти лимит спамом 422-ответов). Pest +5 в `WebhookReceiveTest`: HMAC valid (test через `$this->call('POST', ..., $rawBody)` чтобы передать сырой body) + invalid (401 + не диспатч) + missing (202 backward-compat); rate-limit с `SystemSetting::update(['value'=>'1'])` → 60 успешных + 61-й = 429 + `Retry-After`; ключ изолирован per-token (alice заблокирована, bob проходит). `RateLimiter::clear` в `beforeEach` чтобы не загрязнять следующий тест. **(2) Реальный fetch для system_settings** в AdminSystemView — закрыт TODO из v1.46. `onMounted(loadSettings)` вызывает `adminApi.listSystemSettings()` и replace'ит `settingsState.splice(0, length, ...fromApi)` (сохраняет reactive-ref). На fetch-error → fallback на mock-данные + warning v-alert (`fetch-error-alert` data-testid, closable). Кнопка `data-testid="reload-btn"` в header триггерит ручной reload. Mock-данные используются как fallback при сетевой ошибке (UI не пустеет). Type-shape совместим: `AdminSystemSetting` (mock) и `ApiSystemSetting` (backend) различаются только origin. Vitest +3: assert `listSystemSettings` called once on mount; reload-btn triggers manual fetch; on rejection → warning-alert visible + 7 mock rows preserved. **(3) Реальный CSV-export для bulk-actions** в DealsView — закрыт TODO из v1.46. `applyBulkExport()` теперь не просто toast'ит, а формирует CSV и триггерит download через Blob+`<a download>`. Headers: 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 text; empty selection → не вызываем URL.createObjectURL. **TODO (production):** webhook HMAC обязательным через flag в system_settings; реальный backend-export через POST /api/deals/export → ReportsView с XLSX (через xlsx-библиотеку или Excel-шаблон); система settings с filter+sort. **Регресс зелёный:** lint+type-check+format ✅; **Vitest 242/242 за 15.82 сек** (+4 от 238); vite build 903 ms; Pint+PHPStan passed (baseline регенерирован для новых Pest TestCall ignored count'ов); **Pest 141/141 за 17.8 сек** (+5 от 136, 627 assertions). Реестр v1.55→v1.56.*

*CLAUDE.md v1.46 от 09.05.2026. Изменения v1.46: **7-фичный auto-mode пакет** (по согласованному списку из «карты что осталось»): **(1) Bulk-actions DealsView** — show-select уже был; добавлен `dealsState` reactive-копия (deep-clone MOCK_DEALS чтобы не мутировать const), bulk-bar (sticky, theme=dark теало-нуар) с count + 4 actions (Сменить статус через v-menu со всеми 14 lead_statuses / Экспорт через v-snackbar / Удалить через v-dialog confirm / ✕ clear). **(2) NewDealDialog** — `components/deals/NewDealDialog.vue` модалка с 6 полями (name/phone/project из MOCK_PROJECTS / manager из MOCK_MANAGERS / cost/status дефолт 'new' или presetStatus). Phone-валидация ≥10 цифр. emit('created', deal) → DealsView/KanbanView пушит в свой reactive-state (KanbanView в правильную колонку по statusSlug + totalDeals++). `MOCK_PROJECTS`/`MOCK_MANAGERS` добавлены в `composables/mockDeals.ts`. **(3) AdminTenantDetailView** — drill-down `/admin/tenants/:code`. `composables/mockTenantDetail.ts` с `expandTenantDetail` (5 sample-users / 2 sample-projects / 8 sample balance-tx / 5 sample-activity). 4 KPI cards (Баланс/Тариф+MRR/Лиды сегодня+неделя+месяц/Средняя цена) + 4 v-tabs (Финансы balance-history table / Пользователи / Проекты / Активность). Hero с tenant.contact_email + legal_address + кнопка «Войти как клиент» (использует ImpersonationDialog из v1.45). 404-fallback если code не найден. AdminTenantsView получил `@click:row` → `router.push({name: admin-tenant-detail, params: {code}})`. **(4) Edit-flow AdminSystemView (audit-log + 2-step)** — `App\Models\SystemSetting` (PK=key string, без CREATED_AT) + `App\Models\SaasAdminAuditLog` (append-only без UPDATED_AT, payload_before/after JSONB casts). `AdminSystemSettingsController` с GET /api/admin/system-settings (list) + PUT /api/admin/system-settings/{key} (update в DB::transaction вместе с INSERT в saas_admin_audit_log; hash-chain trigger BEFORE INSERT заполняет log_hash). Type-validation: int → ctype_digit (с минусом для signed); decimal → is_numeric; bool → in('true','false','1','0'); json → JSON_THROW_ON_ERROR. Reason ≥30 chars. Frontend `SystemSettingEditDialog` — 3-step (edit→confirm с diff before/after→done). AdminSystemView получил кнопку «Изменить» в каждой строке + onSettingUpdated optimistic update. **(5) Webhook receive endpoint** — `App\Http\Controllers\Api\WebhookReceiveController::receive` POST /api/webhook/{token} (token=`tenants.webhook_token`). Валидация payload (vid/project/phone/time required + nullable tag/phones array). 404 на unknown token; 422 на bad payload; 202 на success + dispatch `ProcessWebhookJob` (sync на dev queue.driver=sync). Stub-INSERT в `webhook_log` через DB::table (если таблица существует) обёрнут в DB::transaction + SET LOCAL app.current_tenant_id для RLS. CSRF-исключение для `api/webhook/*` в bootstrap/app.php — внешний клиент без сессии. **(6) Smart-filters** — DealsView получил 2 multi-select v-select (Проекты + Менеджеры) с `availableProjects`/`availableManagers` computed (auto из dealsState); `filteredDeals` фильтрует по slug+projects+managers+search. AdminTenantsView получил аналогичные filterStatuses (4 STATUS_OPTIONS) + filterTariffs (computed availableTariffs из MOCK_TENANTS). Кнопка «Сбросить фильтры»/«Сбросить» появляется только когда фильтры активны. **(7) AdminImpersonationView** — Backend +2 endpoint: GET /api/admin/impersonation/active (where used_at!=null AND session_ended_at==null) + GET /api/admin/impersonation/recent (last 20 завершённых с duration_seconds через abs(diffInSeconds) — quirk: Carbon diffInSeconds signed по умолчанию, без abs() возвращал отрицательное). `ImpersonationToken` получил belongsTo(Tenant). Frontend view с 2 секциями (Активные → end-кнопка / Недавно завершённые read-only) + refresh-btn + onMounted load. Маршрут `/admin/impersonation` добавлен в router; AdminLayout получил 5-й nav-пункт «Impersonation» mdi-account-switch. **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. Setup получил `visualViewport` polyfill (VOverlay/v-menu/v-snackbar location strategies). **Pest +16** (всего **136/136 за 15.8 сек**, 495 assertions): AdminSystemSettings 8 + WebhookReceive 6 + Impersonation active/recent 2. PHPStan baseline регенерирован (+ноль errors). Pint passed. **Quirks:** (1) `bool` в filterTariffs = `TenantTariff[]` (не `string[]`) — vue-tsc ругалось type-mismatch с `availableTariffs: TenantTariff[]`. (2) DELETE TestPartitions использует DETACH перед DROP (из v1.40, не повторяется). (3) ImpersonationDialog stubится в AdminTenantsView/AdminTenantDetailView spec'ах. (4) NewDealDialog watch с `immediate: true` — иначе presetStatus prop не подхватывался при initial mount с открытым dialog. (5) Тесты onDealCreated требуют полный MockDeal (с manager) — Kanban-карточка ожидает `deal.manager.name`. **Регресс зелёный:** lint+type-check+format ✅; vitest 238/238 за 15.31 сек; vite build 937 ms; Pint+PHPStan passed; **Pest 136/136 за 15.8 сек**. Реестр v1.54→v1.55.*

*CLAUDE.md v1.45 от 09.05.2026. Изменения v1.45: **Impersonation UI dialog (Ю-1 frontend)** — закрыт TODO из v1.44. **`api/admin.ts`** — типизированные axios-helpers `impersonationInit/Verify/End` для трёх endpoint'ов из v1.44 (`POST /api/admin/impersonation/{init,verify,end}`); все три делают `ensureCsrfCookie()` (Sanctum SPA cookie-flow), на prod автоматически перейдут под middleware('auth:saas-admin') без изменений на клиенте — `withCredentials: true` уже в apiClient. **`components/admin/ImpersonationDialog.vue`** — 4-step state-machine (`reason → verify → active → done`): step 1 — `v-textarea` с counter и hint «Ещё N символов» (валидация ≥30 chars на клиенте до POST + ловля backend 422 через `extractValidationErrors`); step 2 — `v-text-field` с `inputmode=numeric maxlength=6 autocomplete=one-time-code` + info-alert «Код отправлен на email клиента: {sent_to_email}» + dev-only success-alert с `_dev_plain_code` (на prod исчезнет после MailService — backend перестанет его возвращать); step 3 — success-alert «Impersonation активен» + `v-btn color=error «Завершить сессию»` + локализованное `usedAtIso` через `toLocaleString('ru-RU')`; step 4 — финальный 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 + verify-success→step3 с end-btn + invalid 5-digit code не вызывает API + end→step4 + Cancel emit; `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` как `<div v-if="modelValue"><slot /></div>` (passthrough), `VTooltip` как `<div><slot name="activator" :props="{}" /></div>`; `ImpersonationDialog` stub'ится в AdminTenantsView spec (внутри использует api/admin axios — реальные запросы в jsdom не нужны, сам диалог покрыт отдельным spec'ом). **api/admin** + `extractValidationErrors`/`extractErrorMessage` мокаются через `vi.mock` (паттерн из auth-store.spec.ts — `axios.isAxiosError(plain Error)` в jsdom возвращает false). **TODO (production):** 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` исчезает; live impersonation session (cookie-swap для admin'а на 1ч); страница «Активные impersonation-сессии» в админке. **Регресс зелёный:** lint:vue ✅ (после `--fix` 6 attribute-order warnings), type-check ✅, format ✅, **Vitest 190/190 за 13.23 сек** (+11 от 179); vite build 924 ms (AdminTenantsView lazy-chunk **20.68 KB** включает inline ImpersonationDialog); **Pest 120/120 за 15.69 сек** (нетронут — backend без изменений). Реестр v1.53→v1.54.*

*CLAUDE.md v1.44 от 09.05.2026. Изменения v1.44: **Impersonation flow backend (Ю-1)**. Закрыт пункт #9 — последний пункт списка из v1.46. **`ImpersonationToken` Eloquent** для `impersonation_tokens` (schema v8.7 §22.7), `UPDATED_AT=null` (схема без updated_at). Helper методы `isExpired()` / `isUsable()`. **`ImpersonationController`** с 3 endpoints: `init({tenant_id, requested_by, reason})` — reason ≥30 chars, генерация 6-значного кода (random_int 100000-999999), bcrypt-hash в `impersonation_tokens`, TTL 15 мин (по ТЗ). `_dev_plain_code` возвращается в response (на prod после MailService — только в email клиента). `verify({token_id, code})` — Hash::check, increment failed_attempts при неверном коде, при ≥5 → `invalidated_at = NOW()` + блокировка. На success — `used_at = NOW()` + 200. `end({token_id})` — `session_ended_at = NOW()`. Все 3 endpoint без auth-middleware на MVP (saas-admin auth не реализован, `requested_by` принимается параметром). Production: middleware('auth:saas-admin') + role guard + two-person approval (CTO-15/Ю-9 — для тенантов с pd_subject_request.processing_restricted=TRUE или chargeback_unrecovered_rub>0). Маршруты `/api/admin/impersonation/{init,verify,end}`. **Pest +9** в `tests/Feature/ImpersonationTest.php` (всего **120/120 за 15.62 сек**, 443 assertions): init success (TTL ±1 мин, bcrypt-hash) + 422 short reason + 404 unknown tenant + verify success (used_at) + 422 + increment failed_attempts + 5 неверных → invalidated + 422 expired + end success + 422 без verify. PHPStan baseline регенерирован. **TODO** (пост-MVP): saas-admin auth (Yandex 360 SSO) + middleware + two-person approval + email-уведомления клиенту + UI dialog в AdminTenantsView (кнопка «Войти как клиент»). **Все 9 пунктов списка v1.46 закрыты** (кроме #6 Yandex SSO ⏸ Б-1 и #7 browser-mode — отложен инфра). **Регресс зелёный:** lint+type+format OK; vite build 846 ms; **Pest 120/120 за 15.62 сек** (+9 от 111, 443 assertions); Pint+Stan passed. Реестр v1.52→v1.53.*

*CLAUDE.md v1.43 от 09.05.2026. Изменения v1.43: **Admin views (Биллинг / Инциденты / Система)**. Закрыт пункт #8 — заменены 3 placeholder'а на реальные display-views с mock-данными. **`AdminBillingView`**: 4-stats row (MRR / Выручка за месяц / Просрочка / Возвраты за 30 дн) + v-data-table 7 колонок (Тенант с ИНН / Тариф / Баланс ₽ с error-color при <0 / Пополнения за мес / Списания / MRR / Статус-chip). Search-фильтр по name/ИНН. **`AdminIncidentsView`**: 3-stats row (Открыто/Расследуется/РКН-уведомлений) + v-btn-toggle 5 фильтров по статусу + v-list инцидентов с incident_id (INC-YYYY-MMDD-NNNN), severity-chip + status-chip + специальный «РКН pending» chip для PDN-breach + дедлайн РКН (24 ч по 152-ФЗ). 5 категорий (PDN-breach / service_outage / security / billing / data_loss). **`AdminSystemView`**: read-only warning + поиск по ключу/описанию + v-list 7 system_settings (webhook_rate_limit_rps, login_max_attempts, password_min_length, retention_days, maintenance_mode и т.д.) с type-chip (int/string/bool/json) и updated_at. Edit-flow с двойным подтверждением + audit-log — отдельный коммит. **`composables/mockAdmin.ts`**: типы AdminBillingTenantRow/AdminIncidentRow/AdminSystemSetting + mock-данные. Маршруты `/admin/billing|incidents|system` теперь ведут на реальные view'ы (не AdminPlaceholderView). **Vitest +13** (всего **179/179 за 11.98 сек**): AdminBillingView 3 (mount + 4 stats + table contents); AdminIncidentsView 5 (mount + 3 stats + filter-toggle + PDN+РКН pending + incident_id format); AdminSystemView 5 (mount + read-only warning + key settings + type-chip + 7 rows). **TODO** (продолжение): #9 Impersonation flow (Ю-1). **Регресс зелёный:** lint+type+format OK; **vitest 179/179 за 11.98 сек** (+13 от 166); vite build 743 ms; story:build 21/28 за 31.5 сек. Реестр v1.51→v1.52.*

*CLAUDE.md v1.42 от 09.05.2026. Изменения v1.42: **Email-уведомление при 3 неудачных попытках входа (ТЗ §22.4.4 п.3)**. Закрыт пункт #5 — последний пункт ТЗ §22.4.4 анти-брутфорс. **`App\Mail\SuspiciousLoginNotification`** Mailable + `resources/views/emails/suspicious_login.blade.php` (HTML email с инструкциями: сменить пароль / включить 2FA / проверить сессии). **`AuthController::maybeNotifySuspiciousLogin`** triggers ровно при `count(auth_log.login_failed для user_id за час) === 3` — иначе на 4-5 неудачах будут спам-emails. Для unknown email user=null → ничего не отправляем. На dev `MAIL_MAILER=log` письмо в storage/logs. **Pest +4** в `tests/Feature/Auth/SuspiciousLoginNotificationTest.php` (всего **111/111 за 14.32 сек**, 401 assertions): после 3-й неудачи Mail::assertSent с правильными user/count/recipient; на 4-5 не дублируется (assertSent count=1); для unknown email НЕ отправляется; успех на 1-2 неудачах НЕ триггерит. PHPStan baseline регенерирован. **TODO** (продолжение): #7 browser-mode, #8 admin views, #9 impersonation. **Регресс зелёный:** Pint+Stan passed; **Pest 111/111 за 14.32 сек** (+4 от 107). Реестр v1.50→v1.51.*

*CLAUDE.md v1.41 от 09.05.2026. Изменения v1.41: **IP-lockout 10/час + auth_log записи (ТЗ §22.4.4 п.2)**. Закрыт пункт #4 — защита от перебора с одного IP. **AuthController::login** перед verify проверяет `isIpLockedOut(ip)` — count(*) FROM auth_log WHERE event='login_failed' AND ip_address=ip AND created_at >= NOW() - 1 hour. Если ≥10 → 429 + Retry-After: 3600. Это второй слой защиты поверх email-rate-limit (5/15мин из v1.36) — защищает от перебора email'ов с одного IP. **`logAuthEvent`** private helper пишет в auth_log через DB::table (Eloquent для этой таблицы нет). На каждый login_success / login_failed (3 ветки: invalid_password / unknown_email / account_locked). RLS USING без WITH CHECK — INSERT не фильтруется. hash-chain trigger (BEFORE INSERT) заполняет log_hash автоматически (OPEN-И-15 tamper-detection). **Pest +6** в `tests/Feature/Auth/IpLockoutTest.php` (всего **107/107 за 13.86 сек**, 380 assertions): login_success пишет с tenant_id; login_failed wrong-password пишет invalid_password; login_failed unknown email пишет unknown_email + user_id=null; 10 fail записей с одного IP за час → следующий login = 429; 9 fail записей (под порогом) → проходит; старые записи >1ч не блокируют. PHPStan baseline регенерирован. **TODO** (продолжение): #5 email-warn, #7 browser-mode, #8 admin views, #9 impersonation. **Регресс зелёный:** lint+type+format OK; **Pest 107/107 за 13.86 сек** (+6 от 101, 380 assertions). Реестр v1.49→v1.50.*

*CLAUDE.md v1.40 от 09.05.2026. Изменения v1.40: **2FA setup wizard + schema v8.7→v8.8 + миграция fix FK + Partitions test fix**. Закрыт пункт #3 — пользователь может включить/отключить/перегенерировать 2FA из SettingsView/SecurityTab. **Backend `TwoFactorSetupController`** под `auth:sanctum`: 4 endpoint'а — `init` (генерация TOTP secret + QR-URL, secret в session как pending, не пишется в БД до confirm); `confirm({code})` (TOTP-verify pending secret → save totp_secret + totp_enabled=true + delete old recovery codes + generate 8 new + return plain один раз); `disable({password})` (Hash::check + clear totp_secret + drop recovery codes); `regenerate-recovery-codes({password})` (Hash::check + replace 8 codes). Recovery code формат `xxxx-xxxx` (lowercase 4+4 + дефис), `Str::random(4)` parts. `User` model получил cast `'totp_secret' => 'encrypted'` (Crypt::encryptString автоматом). **Schema v8.7 → v8.8:** `users.totp_secret VARCHAR(255)` → `TEXT` — encrypted 32-байт TOTP secret = ~256 chars > 255 (PDOException на confirm). Запись §V в `db/CHANGELOG_schema.md`. **Миграция fix:** `0001_01_01_000000_load_initial_schema.php` теперь после `DB::unprepared($sql)` явно делает `ALTER TABLE webhook_dedup_keys ADD FOREIGN KEY ... ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED` — DDL FK на partitioned-таблицу через unprepared() PDO молча проглатывался на свежей БД (известное поведение Laravel/PDO). Без fix'а ON DELETE CASCADE тест валится. **PartitionsCreateMonthsTest fix:** afterEach использует `ALTER TABLE deals DETACH PARTITION ...` + `DROP TABLE` вместо `DROP ... CASCADE` — последний дропал FK от webhook_dedup_keys на parent (PG behavior). **DB timezone fix** (config/database.php pgsql) добавлен в v1.38 продолжает работать. **Frontend `SecurityTab`** переписан с mock на реальную логику: 3 v-dialog'а (setup wizard 3 шага: init→confirm→show 8 codes; disable; regenerate). 4 новых функции в `api/auth.ts`: `twoFactorInit/Confirm/Disable/regenerateRecoveryCodes`. v-chip статуса 2FA читает `auth.user?.totp_enabled`. **Pest +10** в `tests/Feature/Auth/TwoFactorSetupTest.php` (всего **101/101 за 13.37 сек**, 364 assertions): init success / 422 если 2FA уже on / confirm success + 8 кодов формат + totp_enabled=true + secret saved + 8 строк в БД / confirm 422 неверный код + totp_enabled остаётся false / confirm 422 без init / disable success / disable 422 неверный пароль / regenerate возвращает 8 новых уникальных + старые удалены / regenerate 422 если 2FA off / все 4 endpoint'а require auth (401). **Vitest:** SettingsView.spec.ts получил createPinia() в plugins (SecurityTab теперь использует useAuthStore). PHPStan baseline регенерирован для +25 ignored Pest TestCall warnings. **TODO** (продолжение): #4 IP-lockout, #5 email-warn, #7 browser-mode, #8 admin views, #9 impersonation. **Регресс зелёный:** lint+type+format OK; **vitest 166/166 за 10.95 сек**; vite build 747 ms; story:build 21/28 за 31.18 сек; Pint+Stan passed; **Pest 101/101 за 13.37 сек** (+10 от 91, 364 assertions). Реестр v1.48→v1.49.*

*CLAUDE.md v1.39 от 09.05.2026. Изменения v1.39: **Recovery code login (POST /api/auth/2fa/recovery-use)**. Закрыт пункт #2 из списка v1.47 — вход по одноразовому резервному коду 2FA вместо TOTP. Backend: `AuthController::useRecoveryCode(UseRecoveryCodeRequest)` берёт `pending_user_id` из session (тот же state, что и /2fa/verify), нормализует код (lowercase + удаление дефисов/пробелов), перебирает неиспользованные `user_recovery_codes` через `Hash::check`, на совпадении → mark `used_at = NOW()` + `Auth::login` + clear pending. Возвращает `{user, requires_2fa: false, recovery_codes_remaining: int}`. Rate-limit `auth:recovery:{pending_user_id}|{ip}` — 5/15мин, scope отделён от 2fa/verify. Маршрут `POST /api/auth/2fa/recovery-use` публичный (как 2fa/verify). **Eloquent-модель `UserRecoveryCode`** для `user_recovery_codes` (schema v8.7 §10) — без `updated_at` (`UPDATED_AT = null`, в schema только `created_at` + `used_at`). **Frontend:** `authApi.useRecoveryCode`, `auth-store::useRecoveryCode` action; новый view `UseRecoveryCodeView.vue` с маршрутом `/recovery-use` (auth layout, без guestOnly чтобы не редиректить pending-state) — input с autocomplete=one-time-code + submit + back-link на /2fa; на success сохраняет `recovery_codes_remaining` в `sessionStorage` для будущего toast-warning'а в SettingsView/SecurityTab. **TwoFactorView** ссылка «Использовать резервный код» переписана с `/recovery` на `/recovery-use` (старый /recovery остаётся для display 8 кодов после setup'а, отдельный пункт #3). **Pest +6** в `tests/Feature/Auth/RecoveryCodeTest.php` (всего **91/91 за 12.77 сек**, 319 assertions): успех + mark used + remaining=3; неверный код 422; уже использованный 422; без pending 422; разные форматы (пробел/дефис/регистр); rate-limit 6-я = 429. **Vitest +6** (всего **166/166 за 11.47 сек**): auth-store useRecoveryCode success/reject; UseRecoveryCodeView 4 (mount + autocomplete + submit-flow с sessionStorage + lockout-alert). PHPStan baseline регенерирован. **TODO** (продолжение): #3 2FA setup wizard, #4 IP-lockout, #5 email-warn, #7 browser-mode, #8 admin views, #9 impersonation. **Регресс зелёный:** lint+type+format OK; **vitest 166/166 за 11.47 сек** (+6 от 160); vite build 849 ms; story:build 21/28 за 30.36 сек; Pint+Stan passed; **Pest 91/91 за 12.77 сек** (+6 от 85). Реестр v1.47→v1.48.*

*CLAUDE.md v1.38 от 09.05.2026. Изменения v1.38: **Reset password (deep-link) + DB timezone fix**. Закрыт второй пункт password-reset flow — установка нового пароля по token из email-ссылки. Backend: `AuthController::resetPassword(ResetPasswordRequest)` использует `Password::reset()` с callback `$user->forceFill(['password_hash' => Hash::make($password)])->save()` (наша колонка password_hash). `ResetPasswordRequest` валидирует token + email + password (min 10 — ТЗ §22.4.1) + confirmed. Rate-limit 5/15мин по ключу `auth:reset:{sha256(token)[0..16]}|{ip}`. Status `Password::PASSWORD_RESET` → 200; иначе → 422 «Ссылка недействительна или истекла» + hit. Маршрут `POST /api/auth/reset-password` публичный. **DB timezone fix (config/database.php pgsql):** добавлен `'timezone' => env('DB_TIMEZONE', 'UTC')` — без него PG возвращал TIMESTAMPTZ с offset `+03`, Carbon::parse терял offset и `tokenExpired` некорректно интерпретировал created_at. Без fix'а Password::reset падал на check expiry. Фикс затрагивает любую TZ-чувствительную логику (не только password reset). **Frontend:** `authApi.resetPassword(payload)`, `auth-store::resetPassword` action, `ResetPasswordView.vue` для deep-link `/reset/:token?email=...` — token из route.params, email pre-filled из query, поля password+confirmation с autocomplete=new-password, success-state + redirect на /login через 3 сек, lockout-alert. Маршрут `/reset/:token` (meta.layout=auth, guestOnly). Route `/reset` добавлен в web.php SPA-paths. **Pest +6** в `tests/Feature/Auth/ResetPasswordTest.php` (всего **85/85 за 11.50 сек**, 291 assertions): успех + token-update + 422 на bad token / mismatch confirmation / short password / unknown email / rate-limit. **Vitest +7** (всего **160/160 за 11.02 сек**): auth-store success + 429; ResetPasswordView mount + email-prefill из query + 2 password-inputs autocomplete=new-password + success-state hides form + lockout-alert. PHPStan baseline регенерирован. **TODO** (отдельные коммиты): Pest browser-mode для full session-flow + 2FA setup wizard + recovery-codes consume + Yandex SSO (Б-1). **Регресс зелёный:** lint+type+format OK; **vitest 160/160 за 11.02 сек** (+7 от 153); vite build 784 ms; story:build 21/28 за 30.74 сек; Pint+Stan passed; **Pest 85/85 за 11.50 сек** (+6 от 79). Реестр v1.46→v1.47.*

*CLAUDE.md v1.37 от 08.05.2026 (поздний вечер). Изменения v1.37: **Forgot password flow (ТЗ §1.7 / Прил. Г.4.3)**. Запрос ссылки на сброс через email. Backend: `AuthController::forgotPassword(ForgotPasswordRequest)` использует `Password::sendResetLink()` под капотом — Laravel создаёт row в `password_resets` (env `AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets` указывает на нашу таблицу из schema v8.7 §10.6, default Laravel `password_reset_tokens` НЕ совпадает) + шлёт ResetPassword Notification. На dev `MAIL_MAILER=log` → notification в storage/logs. **Anti-enumeration:** ВСЕГДА 200 unified-message «Если такой email зарегистрирован — мы отправили ссылку», независимо от существования user'а — иначе перебор email'ов через ответ. **Rate-limit:** 5 попыток / 15 мин по ключу `auth:forgot:{lower(email)}|{ip}`, 6-я → 429 + Retry-After. `RateLimiter::hit` ставится ДО `sendResetLink` — иначе можно перебирать вечно за счёт unknown email'ов. **Frontend:** `authApi.forgotPassword(email)`, `auth-store::requestPasswordReset(email)` action (загружает lockoutSeconds на 429), `ForgotPasswordView` интегрирован: submit → store → `submitted=true` → success-state v-alert (data-testid=forgot-success) скрывает форму + остаётся «Назад ко входу» btn. **Pest +6** в `tests/Feature/Auth/ForgotPasswordTest.php` (всего **79/79 за 10.55 сек**, 273 assertions): existing email → 200 + row в password_resets + Notification::assertSentTo(ResetPassword); unknown email → 200 unified без row + assertNothingSent; валидация 422 (формат / пустое); rate-limit 5 → 6-я = 429; throttle ключ изолирован по email. **Vitest +4** (всего **153/153 за 11.11 сек**): auth-store success/429; ForgotPasswordView success-state (форма скрывается после submit) + lockout-alert. PHPStan baseline регенерирован для +14 ignored Pest TestCall warnings. **TODO** (отдельные коммиты): POST /api/auth/reset-password (deep-link `/reset/{token}?email=` + UI-форма new_password). **Регресс зелёный:** lint+type+format OK; **vitest 153/153 за 11.11 сек** (+4 от 149); vite build 862 ms; story:build 21/28 за 32 сек; Pint passed; **Pest 79/79 за 10.55 сек** (+6 от 73, 273 assertions). Реестр v1.45→v1.46.*

*CLAUDE.md v1.36 от 08.05.2026 (поздний вечер). Изменения v1.36: **Rate-limiting login + 2FA verify (ТЗ §22.4.4)**. По ТЗ §22.4.4: 5 неудачных попыток входа на email → блокировка 15 мин. Backend через `Illuminate\Support\Facades\RateLimiter`. **AuthController::login** перед verify проверяет `RateLimiter::tooManyAttempts("auth:login:{email}|{ip}", 5)` → 429 + `Retry-After`. На неуспехе → `RateLimiter::hit($key, 900)` (15 мин). На успехе email+пароля → `RateLimiter::clear` (2FA-фаза не зависит от login-fails). **AuthController::verifyTwoFactor** аналогично, ключ `auth:2fa:{pending_user_id}|{ip}`. `lockoutResponse()` private helper возвращает 429 + JSON `{message, retry_after}` + header `Retry-After`. Ключ login делает `mb_strtolower(email)` для case-insensitivity. Pest +6 в `tests/Feature/Auth/RateLimitTest.php` (всего **73/73 за 8.07 сек**, 246 assertions): 5 неудач → 6-я с правильным паролем = 429 + `Retry-After ∈ (0, 900]`; успешный login чистит throttle (5 новых wrong снова возможны); throttle ключ изолирован по email (Alice заблокирована, Bob входит); inactive user тоже расходует попытки; 2FA verify 5 неверных кодов → 6-я с правильным TOTP = 429; 2FA success чистит throttle. **Quirk:** при первой версии тестов wrong-password='wrong' (5 символов) валидация LoginRequest `min:8` падала **до** controller, RateLimiter::hit не вызывался — пароль для wrong-attempts должен быть ≥8 символов. **Frontend `auth-store::lockoutSeconds`** ref<number|null>: при 429 в login() / verifyTwoFactor() catch-блок извлекает `retry_after` через `extractRateLimitRetry()` (новый helper в `api/client.ts` — читает `response.data.retry_after` или header `Retry-After`). Успешный login сбрасывает `lockoutSeconds = null`. **LoginView/TwoFactorView** показывают `v-alert type=error` с `data-testid="lockout-alert"`: «Слишком много попыток. Попробуйте через {Math.ceil(seconds/60)} мин.». **Vitest +4** (всего **149/149 за 12.31 сек**): auth-store 3 (login 429 → lockoutSeconds=600 + reject; verifyTwoFactor 429 → lockoutSeconds=900; успешный login сбрасывает lockoutSeconds); LoginView 1 (lockout-alert не виден дефолтно → после `auth.lockoutSeconds=600` появляется + содержит «10 мин»). `auth-store.spec.ts` получил vi.mock('../../resources/js/api/client') — иначе axios.isAxiosError(plain Error) в jsdom возвращает false. PHPStan baseline регенерирован для +26 Pest TestCall warnings (накопительно). **TODO** (отдельные коммиты): IP-lockout 10/час через auth_log + email-уведомление при 3 неудачах (требует MailService + auth_log таблицы). **Регресс зелёный:** lint+type+format OK; **vitest 149/149 за 12.31 сек** (+4 от 145); vite build 886 ms; story:build 21/28 за 37.19 сек; Pint passed, PHPStan 0 errors; **Pest 73/73 за 8.07 сек** (+6 от 67, 246 assertions). Реестр v1.44→v1.45.*

*CLAUDE.md v1.35 от 08.05.2026 (поздний вечер). Изменения v1.35: **AppLayout/AdminLayout user-chip из store + Logout-menu**. Замены статичных mock'ов «ИП»/«Иван П.» (AppLayout) и «АО»/«Админ Оператор» (AdminLayout) на реальные данные из Pinia auth-store. `userInitials` computed: первая буква `first_name` + `last_name` → uppercase; fallback на 2 первые буквы `email` если ФИО пустые; '?' (AppLayout) / 'АО' (AdminLayout) если user=null. `userShortName` computed: `«${first_name} ${last_name[0]}.»` → fallback на `first_name` → fallback на `email`; 'Гость' (AppLayout) / 'Админ Оператор' (AdminLayout) если user=null. user-chip обёрнут в `v-menu offset=8` с activator-slot — клик открывает `v-list density=compact min-width=200`: email disabled-row + divider + «Настройки» (RouterLink на /settings, AppLayout-only) или «Выйти из админки» (RouterLink на /dashboard, AdminLayout-only) + «Выйти» (mdi-logout) → `handleLogout()` async: `auth.logout()` (swallows API errors) → `router.push('/login')`. Vitest +3 в `AppLayout.spec.ts` (всего **145/145 за 11.01 сек**): mountAppLayout получил параметр `user: AuthUser | null = mockUser` + setActivePinia + auth.user setup; tests: «user-chip показывает initials и shortName» (ИП + Иван П.), «при null user (гость) показывает ? и Гость», «при отсутствии first_name fallback на email». `AppShell.spec.ts` получил `createPinia()` в plugins (требуется AppLayout). **Регресс зелёный:** lint+type+format OK; **vitest 145/145 за 11.01 сек** (+3 от 142); vite build 855 ms; story:build 21/28 за 32.11 сек; **Pest 67/67 за 6.16 сек**. Реестр v1.43→v1.44.*

*CLAUDE.md v1.34 от 08.05.2026 (поздний вечер). Изменения v1.34: **2FA TOTP-verify** — закрыт второй пункт auth-flow. Установлен `pragmarx/google2fa:^9.0` для TOTP-генерации/проверки (RFC 6238). **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 сек до/после, компенсирует clock-skew); при success — Auth::login + regenerate session + clear pending. `VerifyTwoFactorRequest` валидирует ровно 6 цифр через regex. Маршрут `/api/auth/2fa/verify` публичный (нет полноценной session-auth до verify). **Frontend `auth-store::login` ИЗМЕНЁН**: при `requires_2fa=true` НЕ ставит user в state (иначе isAuthenticated=true и auth-guard пропустит на /dashboard минуя 2FA). `verifyTwoFactor(code)` action ставит user после успеха. **TwoFactorView интегрирован**: `onMounted` → если !requires2fa && !isAuthenticated → /login; submit → `auth.verifyTwoFactor(codeFull)` → /dashboard; при error — show error + clear code + focus first cell. userEmail из `auth.user?.email`. **Pest +6** в `tests/Feature/Auth/TwoFactorTest.php` (всего **67/67 за 6.97 сек**): login для 2FA-user НЕ создаёт session (/me возвращает 401) + verify с правильным TOTP завершает login + неверный код 422 + verify без login 422 + валидация формата 6 цифр + после verify /me возвращает user. Tests генерируют валидный TOTP через `$google2fa->getCurrentOtp($secret)`. **Vitest +3** auth-store (login с requires_2fa разделён на 2 теста + verifyTwoFactor success + reject), TwoFactorView spec получил `setActivePinia` + `auth.requires2fa = true` для bypass onMounted-redirect. PHPStan baseline регенерирован для +25 Pest TestCall warnings. **Регресс зелёный:** lint+type+format OK; vitest **142/142 за 10.75 сек**; vite build 908 ms; story:build 21/28 за 31.28 сек; **Pest 67/67 за 6.97 сек** (194 assertions). Реестр v1.42→v1.43.*

*CLAUDE.md v1.33 от 08.05.2026 (поздний вечер). Изменения v1.33: **Frontend auth integration**. Установлены `axios@^1.16.0` + `pinia@^3.0.4` (через `--legacy-peer-deps` из-за Histoire vs Vite 8). Создан `resources/js/api/client.ts` — axios-инстанс с `withCredentials: true` + `withXSRFToken: true` (Sanctum SPA mode auto-XSRF из cookie). `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: `user/loading/requires2fa` refs + `isAuthenticated` computed + `login/register/fetchMe/logout` actions. logout() catch-swallow ошибок (UI-localout даже при backend-failure). LoginView/RegisterView подключены через `useAuthStore` — submit делает реальный POST через store, errors-state из validation, redirect на /dashboard или /2fa, loading-spinner на btn'ах. **Auth-guard в router** через `router.beforeEach`: meta.requiresAuth → check isAuthenticated → redirect /login с `?redirect=` query; meta.guestOnly (login/register/forgot) → если уже залогинен → /dashboard. На первый переход вызывается `fetchMe()` для restore-session-state из cookie. Pinia зарегистрирован в app.ts через `app.use(createPinia())`. / теперь redirect на /dashboard (через guard уйдёт на /login если не залогинен). Vitest +10 (всего **139/139 за 10.11 сек**): auth-store 7 (initial state + login success/reject + register + fetchMe success/401 + logout-swallow), router 5 переписан (login.guestOnly + 6 protected routes requiresAuth + 4 admin routes + 3 error routes без auth + redirect /dashboard→/login без auth с `?redirect=` query). LoginView/RegisterView/router тесты получили createPinia в plugins. **Регресс зелёный:** lint+type+format OK; vitest 139/139; vite build (main app-chunk вырос до **153.64 KB** включая axios+pinia+auth-store+api/auth — gzipped 54.54 KB) — 806 ms; story:build 21/28 за 31.73 сек; **Pest 61/61 за 5.86 сек**. Реестр v1.41→v1.42.*

*CLAUDE.md v1.32 от 08.05.2026 (поздний вечер). Изменения v1.32: **Backend auth-flow через Sanctum SPA mode**. Установлен `laravel/sanctum:^4.3`. SPA mode: cookie-based session-auth (не token-based). `AuthController` (login/register/me/logout) + `LoginRequest`/`RegisterRequest` Form Requests с валидацией. `register` требует `accept_offer=true && accept_pdn=true` (по ТЗ §1.5/§4.1, БЕЗ маркетингового click-wrap'а — расхождение #2 handoff vs ТЗ). User model расширен fillable: `last_login_at`, `last_active_at`. Auth-маршруты `/api/auth/{login,register,me,logout}` размещены в `web.php` (НЕ в api.php — Sanctum SPA нуждается в session-cookie middleware из web-группы). `bootstrap/app.php` без api.php. Pest +13 тестов для auth-flow (всего **61/61 за 6.22 сек**): login успех + 2FA flag + неверный пароль + несуществующий email + заблокированный аккаунт + валидация format + last_login_at update + register success + duplicate email + accept_offer/accept_pdn required + /me 401 без auth + /me возвращает user + logout 200. **Quirk:** logout-test упрощён до проверки 200-status — Pest cookie-jar в test-runtime держит session между запросами, full session-invalidate проверяется через Pest browser-mode (отдельный коммит). phpstan-baseline регенерирован для +25 false-positive Pest `$this`-warnings. **Регресс зелёный:** Pint passed, PHPStan passed (level 5 + checkModelProperties); Vitest **129/129 за 9.59 сек**; vite build OK 802 ms; story:build **21/28 за 30.39 сек**; **Pest 61/61 за 6.22 сек**. Реестр v1.40→v1.41.*

*CLAUDE.md v1.31 от 08.05.2026 (поздний вечер). Изменения v1.31: **Админка SaaS** — 13-й экран, последний из основных в handoff (без landing). По `liderra_v8_handoff/concepts/v8_admin.html` + ТЗ §22 + schema v8.7 §3 (tenants) + §10 (saas_admin_audit_log). Layout `AdminLayout.vue` — отдельный sidebar теало-нуар с под-брендом ADMIN (red-error 10px JBM uppercase) + 4 nav-пункта (Тенанты 142 / Биллинг / Инциденты 3 / Система) + topbar с crumb «Админка → currentPage» + admin-user-chip (АО, Админ Оператор, error-color avatar). AppShell расширен: meta.layout='admin' → AdminLayout. `AdminTenantsView.vue` — page-head со stats (всего/активны/trial/просрочка/выручка JBM tnum) + Экспорт-btn + filter-bar с search-input + Статус/Тариф фильтры + v-data-table 7 колонок (Тенант с двухстрочным name/inn / Статус-chip / Тариф / Баланс ₽ JBM с error-color при <0 и medium-emphasis при 0 / Желаем×факт «12 × 11» / MRR / Активность). `mockTenants.ts` — 7 mock-tenants (3 active / 1 trial / 1 overdue / 1 suspended / 1 enterprise) + AdminStats (142 total / 128 active / 9 trial / 5 overdue / 1248600 ₽ revenue). 4 статуса с tonal-chip разного цвета (success/info/warning/error). `AdminPlaceholderView.vue` — универсальный для Биллинг/Инциденты/Система с описаниями из route.meta.description ссылающимися на schema (incidents_log §9 / system_settings §10). Маршруты: `/admin` redirect → `/admin/tenants`, `/admin/tenants` (полный) + `/admin/billing|incidents|system` (placeholder). Vitest +11 (всего **129/129 за 10.02 сек**): заголовок «Тенанты» + 5 stats (142/128/9/5/выручка) + 7 колонок таблицы + 7 mock-rows + первая строка Окна Москва ИНН + Активен + overdue с -1200 + trial 4 дня + suspended + search-input placeholder + фильтр «Натяжные» оставляет 1 строку + Экспорт + Статус: Все / Тариф: Все. Stories +2 (AdminLayout + AdminTenantsView). web.php: новые admin-routes покрыты `Route::fallback` (без явных Route::view). **Регресс зелёный:** lint+type+format OK; vitest 129/129; vite build (admin views в lazy-chunks; main app-chunk 104.99 KB) — 763 ms; story:build **21 story / 28 variants за 30.32 сек**; **Pest 48/48 за 4.89 сек**. Реестр v1.39→v1.40.*

*CLAUDE.md v1.30 от 08.05.2026 (поздний вечер). Изменения v1.30: **ErrorView** (404/403/500) — 12-й экран. По `liderra_v8_handoff/concepts/v8_errors.html`. Универсальный компонент с конфигурацией через `route.meta.errorCode`. Layout: тёмный full-bleed (теало-нуар `#012019` bg), top-brand «Лидерра.» в шапке, центрированный контент с err-code 96px JBM monospace + accent на средней цифре + h2 title + desc + 2-action btn-row + опциональные status-list (только 500) и err-id (REQ-/INC-) с copy-btn (только 403/500). Каждый из 3 кодов (404/403/500) имеет уникальные actions: 404 «На дашборд + Назад» (router.back), 403 «На дашборд + Написать в поддержку» (mailto), 500 «Попробовать снова» (location.reload) + «Статус сервиса» (https://status.liderra.app) + status-list (API/Telegram/YooKassa). 500 показывает 3 status-pills с цветом (success/warning/error). copyRequestId через navigator.clipboard.writeText. AppShell расширен: `meta.layout='error'` → рендерит RouterView напрямую без AppLayout/AuthLayout (ErrorView сам предоставляет v-app). Маршруты: `/403`, `/500`, и **catch-all `/:pathMatch(.*)*`** в Vue Router (404 для всех неизвестных путей). web.php: `Route::view('/403', 'welcome')`, `Route::view('/500', 'welcome')` + **`Route::fallback(fn () => view('welcome'))`** (срабатывает после всех явных + runtime-route'ов от Pest, не перехватывает /_test/*). Vitest +8 (всего **118/118 за 9.39 сек**): 404 default + 403 с REQ-3F8A2-0007 + 500 с INC-2026-0507-0034 + status-list (API · OK / Telegram · деградация) + 404 actions (На дашборд / Назад) + 403 actions с mailto-link + 500 actions с status link + brand-блок + 404 НЕ содержит REQ/INC/status-list. Тесты используют stubs:`{ VApp/VMain }` как passthrough divs (layout-injection не нужен). Story `ErrorView.story.vue` 1 variant. **Регресс зелёный:** lint+type+format OK; vitest 118/118; vite build (ErrorView lazy-chunk 3 wrapper-route'а ссылаются на тот же chunk; main app-chunk 101.01 KB упал на 7 KB благодаря shared chunk'ам); story:build **19 stories / 26 variants за 30.96 сек**; **Pest 48/48 за 4.88 сек**. Реестр v1.38→v1.39.*

*CLAUDE.md v1.29 от 08.05.2026 (поздний вечер). Изменения v1.29: **ReportsView** — 11-й экран. По `liderra_v8_handoff/concepts/v8_reports.html` + ТЗ §6.6 + CTO-6 (retry 3/7д) + CTO-7 (квота 3 одновременных). Структура: page-head (заголовок + page-stats «очередь 2/3 · обработано за месяц 38 · средний размер 2.4 MB») + form-card (Запросить отчёт): 4 type-cards radio-grid (Сделки детально / Менеджеры / Источники / Биллинг) с active-state primary-bg ivory-tint + period с/по date-fields + Проект/Менеджер v-select + 4 fmt-кнопки (CSV/XLSX/JSON/PDF) с flat/outlined-toggle + quota-banner v-alert info с CTO-6/CTO-7 значениями + Запустить/Сброс. Jobs-list panel: panel-h «Сгенерированные отчёты» + «все 38 →»; 5 job-rows в grid-layout (icon+info+chip+actions): icon mdi-check-circle/progress-clock/clock/alert-circle (color по статусу), title + meta (FORMAT · size · rows · timeText, для failed +«N/3 попытки · ошибка X»), v-progress-linear для running 62%, status-chip tonal, actions: Скачать (done) / Повторить (failed.attempt<3) / Отменить (queued) / Удалить (done|failed). `composables/mockReports.ts`: типы (deals/managers/sources/billing × csv/xlsx/json/pdf × queued/running/done/failed), 5 mock-jobs с разными состояниями, REPORT_TYPES + REPORT_FORMATS массивы для UI, MOCK_QUOTA. Маршрут `/reports` (lazy) в router и web.php. Vitest +12 (всего **110/110 за 9.38 сек**): заголовок + page-stats + 4 type-cards + дефолт «Сделки» active + 4 формата + quota «2 из 3» + «3 попыток retry» + «7 дней» + 5 job-rows + done «Готов» + Скачать-aria + running «62%» + progressbar role + queued «В очереди» + Отменить-aria + failed «Ошибка» + «S3 timeout» + Повторить-aria + клик-переключение active. **Регресс зелёный:** lint+type+format OK; vitest 110/110; vite build (ReportsView lazy-chunk; main 108.19 KB) — 706 ms; story:build **18 stories / 25 variants за 30.77 сек**; **Pest 48/48 за 4.58 сек**. Реестр v1.37→v1.38.*

*CLAUDE.md v1.28 от 08.05.2026 (поздний вечер). Изменения v1.28: **SettingsView** — 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 переключает рендер. **8 вкладок**: 4 реализованы (Профиль/Безопасность/API и Webhook/Уведомления), 4 placeholder (Проекты/Команда/Интеграции/Тихие часы) с `PlaceholderTab` и v-alert «В разработке».

- **`ProfileTab.vue`:** v-avatar 80px + Сменить-btn + 5 v-text-field (Полное имя/Email disabled+hint про support/Телефон/Тайм-зона/Роль disabled) в 2-column grid + Сохранить/Отмена.
- **`SecurityTab.vue`:** 3 cards: Пароль (последняя смена + Сменить-btn) + 2FA (success-chip «включена» + текст про TOTP + Перегенерировать резервные коды + Отключить 2FA) + Активные сессии (3 mock с device/location/when + «эта сессия» chip + Завершить-btn для не-current).
- **`ApiTab.vue`:** API-ключ password-field с eye-toggle + Копировать/Перегенерировать + Webhook-секция (URL + Signing secret HMAC + Сохранить/Тестовый webhook). Текст про дедуп `(tenant_id, source_crm_id)` 24ч и антифрод по phone — соответствует schema v8.7 + ТЗ §10.8.1.
- **`NotificationsTab.vue`:** **Матрица 8×3** соответствует `users.notification_preferences` JSONB по schema v8.7 §4. 8 событий (new_lead/duplicate_detected/low_balance/tariff_charge/reminder_due/manager_assigned/webhook_failed/monthly_report) × 3 канала (email/sms/in_app) с v-checkbox; reactive prefs Record. Дополнительно sound-switch (соответствует `sound_enabled` BOOLEAN в schema). CSS-grid 1fr 110px 110px 130px для prefs-table (head + 8 rows).
- **`PlaceholderTab.vue`:** универсальный stub с props title/description + v-alert «В разработке».
- Маршрут `/settings` (lazy) в router и web.php.
- Vitest +8 (всего **98/98 за 8.42 сек**): монтаж + ровно 8 nav-tabs + все 8 названий + дефолт «Профиль» (Полное имя/Тайм-зона) + переключение на Проекты → «В разработке» + переключение на Уведомления показывает «События × каналы» + 5 событий из матрицы (Новый лид/Дубликат/Низкий баланс/Срок напоминания/Webhook упал) + Безопасность: 2FA + Активные сессии + API: API-ключ + Signing secret HMAC. Story `SettingsView.story.vue` 1 variant.
- **Регресс зелёный:** lint+type+format OK; vitest 98/98; vite build (SettingsView lazy-chunk) — 750 ms; story:build **17 stories / 24 variants за 31.7 сек**; **Pest 48/48 за 5.03 сек**. Реестр v1.36→v1.37.*

*CLAUDE.md v1.27 от 08.05.2026 (поздний вечер). Изменения v1.27: **BillingView** — финансовый экран биллинга и тарифов. По `liderra_v8_handoff/concepts/v8_billing.html`. Структура: page-head (заголовок + page-stats с tnum-числами кошелька/лидов/runway-дней + Пополнить-btn) + pending-banner v-alert info (1 платёж в обработке через ЮKassa с auto-cancel timeout) + wallet-row из 3 cards (Кошелёк ₽ — primary теало-нуар card с LIVE-chip + Пополнить/Автопополнение btn'ы; Баланс лидов 285 ГЦК + средняя цена; Тариф «Команда» 990₽/мес + 3 фичи + Сменить-btn) + transactions panel (4-tab v-btn-toggle: Все/Пополнения/Списания/Возвраты) + v-data-table 5 колонок (Дата/Операция/ID/Статус-chip/Сумма с +/− знаком и цветом) + invoices panel (Реестр-XLSX-btn + 4 строки PDF/1С 8.3 XML). `composables/mockBilling.ts`: `BillingTransaction` (8 mock-транзакций со статусами pending/completed/rejected, types: topup/lead_charge/refund/tariff_charge), `Invoice` (4 mock invoices с format pdf|xml_1c83), `PendingPayment`, `BILLING_TABS` (4 среза с types-array). Соответствуют схеме v8.7 §4.4 balance_transactions / §4.5 invoices. Маршрут `/billing` (meta.layout='app', lazy-import) в router и web.php. Vitest +11 (всего **90/90 за 7.96 сек**): заголовок + page-stats (regex для nbsp `14 250 ₽`) + pending-banner + 3 wallet-cards + 3 фичи тарифа + 4 tabs + дефолт «Все» все 8 строк + format «+ 5 000 ₽» / «− 6 600 ₽» / «— 0 ₽» rejected + invoices section 4 rows + PDF/1С 8.3 XML labels. Story `BillingView.story.vue`. **Регресс зелёный:** lint+type+format OK; vitest 90/90; vite build (BillingView lazy-chunk) — 688 ms; story:build **16 stories / 23 variants за 32.16 сек**; **Pest 48/48 за 4.89 сек**. Реестр v1.35→v1.36.*

*CLAUDE.md v1.26 от 08.05.2026 (поздний вечер). Изменения v1.26: **DealDetailDrawer** — правая панель с деталями сделки. Открывается при click по строке в DealsView или по карточке в KanbanView. По `liderra_v8_handoff/concepts/v8_deal_card.html`. Структура: hero (Сделка #id eyebrow + name h5 + close-icon-btn + phone-link tel: + clock-icon с относительным временем + status-chip с colorHex), section «Параметры» (2-column grid: Проект/Стоимость лида/Менеджер с avatar/Источник), section «Активность» (timeline 6 событий с iconified vertical-line connector). `composables/mockDealEvents.ts` — mock activity-events 6 типов: deal.created/balance_charged/assigned/viewed/status_changed/commented (соответствуют ActivityLog event-константам по схеме v8.7 §10.2). DealsView и KanbanView интегрируют drawer через `v-model:open` + `:deal` props; click handler в DealsView через `@click:row` v-data-table, в KanbanView через `@open-deal` event от KanbanCard. **Vitest quirk:** DealsView/KanbanView содержат теперь v-navigation-drawer, который требует layout-injection от v-app/v-layout. В Vitest `vite-plugin-vuetify` auto-import не работает (только в build) — `v-layout`/`v-app` не резолвятся компонент-резолвером. Решение: stub'ить `DealDetailDrawer` в тестах DealsView/KanbanView (`stubs: { DealDetailDrawer: true }`); сам Drawer тестируется отдельно с stub'ом `VNavigationDrawer` как passthrough `<div v-if="modelValue"><slot /></div>` чтобы slot-content (hero/params/timeline) рендерился в DOM. Vitest +8 (всего **79/79 за 7.57 сек**): монтаж + open=false скрывает + deal=null без content + hero (name+id) + tel:link + status-chip + параметры (project/cost/manager) + timeline 6 events + emit update:open(false) на close. Story `DealDetailDrawer.story.vue` 2 variants (status=new / paid). **Регресс зелёный:** lint+type+format OK; vitest 79/79; vite build (DealDetailDrawer инлайнен в DealsView+KanbanView lazy-chunks; main app-chunk 107.16 KB) — 761 ms; story:build **15 stories / 22 variants за 31.55 сек**; **Pest 48/48 за 4.99 сек**. Реестр v1.34→v1.35.*

*CLAUDE.md v1.25 от 08.05.2026 (поздний вечер). Изменения v1.25: **Kanban DnD** — drag-and-drop карточек между колонками. Установлен `vuedraggable@^4.1.0` (обёртка SortableJS@1.14, поддержка Vue 3 — peerDep `vue ^3.0.1`; через `--legacy-peer-deps` из-за того же Histoire vs Vite 8 конфликта). `KanbanColumn.vue` обёрнут вокруг карточек: `<draggable v-model="localDeals" group="kanban-deals" item-key="id" ghost-class="ghost-card" drag-class="drag-card" animation="150" @change="onDraggableChange">` + `<template #item="{ element }">` рендерит `<KanbanCard>`. Добавлен `<template #footer>` с empty-state «пусто · перетащите сюда» (только когда `deals.length === 0`). Visual-эффекты: `.ghost-card` (opacity 0.4 + bg #E1EEEA — placeholder при drag) + `.drag-card` (opacity 0.95 + transform rotate 1deg — текущая карточка при перетаскивании). `KanbanColumn` теперь принимает `@change` event с `DraggableChangeEvent` (added/removed/moved discriminated union) и пробрасывает родителю + `update:deals` для v-model паттерна. `KanbanView.vue` переписан с `const allDeals` (readonly из MOCK_DEALS) на `reactive<Record<slug, MockDeal[]>>` — отдельный массив для каждой колонки (vuedraggable v-model требует независимые arrays). Deep-copy через `{...d}` чтобы не мутировать MOCK_DEALS const при DnD. `onColumnChange(targetSlug, event)` обработчик: при `event.added` — синхронизирует `event.added.element.statusSlug = targetSlug` (на production будет POST /api/deals/{id}/transition с проверкой allowed-переходов). 'removed' и 'moved' события обрабатываются автоматически через v-model. Vitest +1 (всего **71/71 за 7.48 сек**): новый тест в KanbanView.spec.ts эмулирует событие `added` через `paidCol.vm.$emit('change', { added: { element, newIndex } })` и проверяет что `dealToMove.statusSlug` обновился с `'new'` → `'paid'`. **Регресс зелёный:** lint+type+format OK; vitest 71/71; vite build (KanbanView lazy-chunk вырос **до 180.53 KB** — добавилась SortableJS-обёртка, но грузится только на /kanban) — 642 ms; story:build **14 stories / 20 variants за 30.45 сек**; **Pest 48/48 за 4.88 сек**. Реестр v1.33→v1.34.*

*CLAUDE.md v1.24 от 08.05.2026 (поздний вечер). Изменения v1.24: **KanbanView** — альтернативный вид сделок (по статусам). 14 колонок (lead_statuses) с группировкой mock-сделок по statusSlug, horizontal scroll. Структура: `KanbanCard.vue` (компактная карточка с name/phone/project/cost/manager-avatar, click → emit('open',id)) + `KanbanColumn.vue` (header с цветной полосой по colorHex статуса + name + count + total ₽; body с v-for карточек + empty-state «пусто»; CSS-var --accent для accent-border) + `KanbanView.vue` (page-head + 14 columns в .kanban-board flex с overflow-x:auto; кастомный scrollbar теало-нуар). DnD НЕ реализован на MVP (отдельный коммит — vue-draggable-next или @vueuse/integrations/useSortable; на API-стороне — PATCH /api/deals/{id} {status_slug}). Маршрут `/kanban` (meta.layout='app', lazy-import) в router и web.php. Vitest +14 (всего **70/70 за 7.37 сек**): KanbanCard 3 (рендер data + manager initials + emit open(id) на click), KanbanColumn 5 (header с nameRu+count + total форматирование + «—»+«пусто» при empty + accent-border colorHex case-insensitive + проброс openDeal от карточки), KanbanView 6 (заголовок «Канбан» + ровно 14 columns + правильный status в каждой + page-stats + кнопка Новая сделка + текст про DnD-предупреждение). Stories +3 с 5 variants (KanbanCard 1, KanbanColumn 3 — Новые/Оплачено/пусто, KanbanView 1). **Регресс зелёный:** lint+type+format OK; vitest 70/70 за 7.37 сек; vite build (KanbanView lazy-chunk) — 633 ms; story:build **14 stories / 20 variants за 31.17 сек**; **Pest 48/48 за 5.06 сек**. Реестр v1.32→v1.33.*

*CLAUDE.md v1.23 от 08.05.2026 (поздний вечер). Изменения v1.23: **DealsView** — центральный экран CRM (список сделок). `resources/js/views/DealsView.vue`: page-head с заголовком «Сделки» + 4-полевые stats (новых лида с утра / всего / в работе / ждут оплату с tnum-числами) + actions (Экспорт + Новая сделка); filter-bar с `v-btn-toggle` 5-вариантным chiprow (Все/Активные/Ждут оплату/Закрытые/Невалидные с counts через v-chip x-small) + search-input (clearable, prepend-icon mdi-magnify, фильтр по name/phone/project); `v-data-table` с показ всех колонок (Лид с avatar+name+phone, Статус с tonal-chip и colorHex из lead_statuses, Проект, Менеджер с avatar, Стоимость через `Intl.NumberFormat('ru-RU')` JBM tnum, Время через `formatRelative` мин/ч/д назад). show-select для bulk-actions (TODO). Empty-state когда фильтры дают 0 строк. **mockDeals.ts** — 12 mock-сделок с разнообразием статусов (по 1-2 на каждый из 14 lead_statuses) + `DEALS_TABS` массив со срезами {id, label, slugs[]|null}. Источник статусов — composables/leadStatuses.ts (snapshot из db/schema.sql:2130, не из BRANDBOOK §3.6). Маршрут `/deals` (meta.layout='app', lazy-import) в router и web.php. Vitest +8 тестов (всего **56/56 за 5.66 сек**): заголовок + page-stats + 5 chiprow-tabs + дефолтный 'active'-фильтр показывает только active-сделки + Экспорт/Новая сделка + 6 колонок таблицы + format `2 400 ₽` (regex `\s+` — `\s` в JS regex matches U+00A0/U+202F nbsp из `Intl.NumberFormat`, не нужен literal nbsp + ESLint warning) + format «7 мин назад». Story `DealsView.story.vue`. **Регресс зелёный:** lint+type+format OK; vitest 56/56; vite build (DealsView lazy-chunk **87.54 KB** — v-data-table большой, но это chunk загружается только на /deals; main app-chunk 106.83 KB) — 538 ms; story:build **11 stories / 15 variants за 31.93 сек**; **Pest 48/48 за 4.96 сек**. Реестр v1.31→v1.32.*

*CLAUDE.md v1.22 от 08.05.2026 (поздний вечер). Изменения v1.22: **Dashboard charts** — ActivityChart + FunnelChart, Dashboard закрыт по дизайну v8_dashboard.html. `resources/js/components/charts/ActivityChart.vue` — линейный чарт на native SVG (без chart-library, чтобы не тащить +400KB зависимость для статичных дашборд-графиков): viewBox 800x220, props (points 7-значений, max 60, labels, title, subtitle), Y-grid 5 линий с подписями (0/15/30/45/60), area-gradient под линией, основная line stroke #0A1319 1.7px, 7 circle-точек (предпоследняя выделена primary teal r=4.2 как «сегодня (max)» по handoff), 3 button-tabs (Принято/Оплачено/Отказ — выбор активного метрика-tab, на API будет менять props.points), легенда с 3 ldot-индикаторами. `resources/js/components/charts/FunnelChart.vue` — воронка на 14 статусов: segmented bar (горизонтальный, height 12px, каждый segment flex=count, background=colorHex) + funnel-list (2-column grid с цветным dot + name + count, отсортирован по убыванию count'а как в handoff). **Источник статусов** — `resources/js/composables/leadStatuses.ts` (snapshot из `db/schema.sql:2130`, **НЕ из BRANDBOOK_v2 §3.6**: расхождение #1 handoff vs ТЗ из реестра v1.13 — handoff содержит 14 «обобщённых» статусов с пересечением всего 2 (Переговоры + Оплачено) с реальными slug'ами). 14 правильных статусов: new/viewed/worked/base/missed/negotiations/waiting_payment/partnership/paid/closed/test_drive/hot/replacement/final_missed с corresponding colorHex из seed'а. Vitest +13 (всего **48/48 за 5.5 сек**): ActivityChart 6 (монтаж + 3 tabs + 7 circles + «сегодня» подпись + кастомные points + легенда 3 dots), FunnelChart 7 (заголовок + 14 segments + 14 list-items + всех 14 nameRu присутствуют И «Думает»/«Спам» из handoff отсутствуют — регрессия защищена + сортировка по убыванию (paid 45 первый) + colorHex применяется + total = sum counts). Stories +2 с 3 variants каждый: ActivityChart (default mock / ровный рост / спад), FunnelChart (default 247 / пустая / концентрация на paid). Quirk Vue SFC compiler: `withDefaults(..., { counts: () => DEFAULT_COUNTS })` падает с `checkInvalidScopeReference` — module-level const'ы запрещены внутри factory; обходится инлайнингом литерала. **Регресс зелёный:** lint+type-check+format OK; vitest 48/48; vite build (DashboardView lazy-chunk **14.9→21.17 KB** с двумя чартами) — 473 ms; story:build **10 stories / 14 variants за 30.43 сек**; **Pest 48/48 за 5.10 сек**. Реестр v1.30→v1.31.*

*CLAUDE.md v1.21 от 08.05.2026 (поздний вечер). Изменения v1.21: **AppLayout + DashboardView** — реализован default-layout приложения для авторизованных пользователей. `resources/js/layouts/AppLayout.vue` — `v-navigation-drawer` (theme=dark, color=secondary теало-нуар, 240px) с brand-block + nav-tree из 8 пунктов в 3 группах («Работа»: Дашборд/Сделки 247/Канбан/Напоминания 12; «Финансы»: Биллинг/Отчёты; «Команда»: Менеджеры 4/Настройки) + `v-app-bar` (56px, color=surface) с crumb «Рабочая область → текущая страница», search-trigger ⌘K (заглушка disabled), notification icon с pip, user-chip («ИП», «Иван П.»). На mobile (md<) drawer toggleable через `v-app-bar-nav-icon`. `resources/js/views/DashboardView.vue` — page-head с приветствием «Доброе утро, Иван» + page-meta (новых лидов / сегодня / вчера / средняя стоимость) + range-toggle 4-варианта (Сегодня/7 дней/30 дней/Период) через `v-btn-toggle`; KPI-row из 4 cards: 3 outlined (Получено лидов 247 +12.3%, Конверсия 18.4% +2.1pp, Активные проекты 8/10 «2 свободно тариф Команда») + 1 hero balance (теало-нуар bg, «14 250 ₽», runway-bar 4/7 заполненных сегментов). `AppShell.vue` упрощён: layout-mapper только мапит `route.meta.layout` ('app' default → AppLayout, 'auth' → AuthLayout). Маршрут `/dashboard` (meta.layout='app') добавлен в router и `web.php`. `histoire.setup.ts` расширен 8-ю app-stub-маршрутами (dashboard/deals/kanban/reminders/billing/reports/managers/settings) — иначе AppLayout warn'ит на mount в memory-router. Vitest +11 тестов (всего **35/35 за 4.92 сек**): AppLayout 6, DashboardView 5, AppShell.spec.ts переписан под новый layout-mapper. **Регресс зелёный:** lint+type-check+format OK; vitest 35/35; vite build (DashboardView lazy-chunk 14.9 KB, app chunk вырос до 105 KB из-за импорта AppLayout) — 458 ms; story:build **8/8 за 28.97 сек** (+ AppLayout + DashboardView); **Pest 48/48 за 4.88 сек**. Реестр v1.29→v1.30.*

*CLAUDE.md v1.20 от 08.05.2026 (поздний вечер). Изменения v1.20: **все 5 auth-экранов реализованы** (закрыта вся секция #form-* из v8_login.html). Добавлены 4 view'а: `RegisterView.vue` (email + password со strength-meter + 2 click-wrap'а — оферта/ПДн, БЕЗ 3-го «маркетинг» из handoff: следуем ТЗ §1.5/§4.1, расхождение #2 реестра v1.13), `TwoFactorView.vue` (6 input-cell с auto-focus вперёд при вводе цифры, Backspace назад при empty, paste 6 цифр заполняет все), `ForgotPasswordView.vue` (email + alert «5 попыток / 15 мин» по ТЗ §1.7), `RecoveryCodesView.vue` (8 одноразовых кодов в 2-column grid + Скачать .txt через Blob/URL.createObjectURL + Копировать через navigator.clipboard + warning о невозможности повторного просмотра). 4 новых маршрута в `router/index.ts` (`/register`, `/2fa`, `/forgot`, `/recovery`) — все meta.layout='auth'. 4 stories в Histoire. **Vitest +14 тестов** (всего 24/24 за 3.29 сек): RegisterView 4 (вкл. assertion на отсутствие маркетингового click-wrap), TwoFactorView 3, ForgotPasswordView 3, RecoveryCodesView 4. **Регресс зелёный:** lint:vue + type-check + format:check OK; vitest **24/24 за 3.29 сек**; vite build 5 lazy-chunks (LoginView 3.81 KB, RegisterView 9.53 KB, TwoFactorView 2.38 KB, ForgotPasswordView 1.89 KB, RecoveryCodesView 2.18 KB) + Vuetify-компоненты в отдельные chunks (VAlert/VForm/VCard/VTextField/VAvatar) — bundle-splitting работает; story:build **6/6 за 29.17 сек**; **Pest 48/48 за 4.85 сек**. Реестр v1.28→v1.29.*

*CLAUDE.md v1.19 от 08.05.2026 (поздний вечер). Изменения v1.19: **первый реальный экран фазы 2** — Vue Router 4.6 + AuthLayout + LoginView. Установлен `vue-router@^4.6.4` (через `--legacy-peer-deps` из-за того же Histoire vs Vite 8 конфликта). Создано: `resources/js/router/index.ts` (createWebHistory + lazy-imports + meta.layout='auth' для /login), `resources/js/layouts/AuthLayout.vue` (двухпанельный grid: brand-pane слева на теало-нуар с radial-gradient акцентами + form-pane справа на warm ivory), `resources/js/views/auth/LoginView.vue` (полная форма: v-text-field для email/password с autocomplete, eye-icon toggle, primary submit, divider, Yandex 360 SSO outlined-btn, RouterLink на /register и /forgot). `AppShell.vue` переписан как layout-mapper по `route.meta.layout` (default → v-app+v-app-bar+RouterView; 'auth' → AuthLayout). `app.ts` подключает router. `routes/web.php` — явные `Route::view` для /, /login, /register, /forgot, /2fa, /recovery (НЕ catch-all `/{any?}` — он перехватывал runtime-routes из Pest `beforeEach` и валил 5 тестов SetTenantContextTest). Vitest расширен: `LoginView.spec.ts` (5 тестов: монтаж + заголовок + кнопка/SSO + autocomplete + RouterLink'и), `router.spec.ts` (2 теста: маршрут /login + редирект /→/login), `AppShell.spec.ts` переписан под router-context (3 теста: default-layout + auth-layout-switch). **Vitest 10/10 за 3 сек**. Histoire setup расширен memory-router'ом (иначе RouterLink/useRoute падают). Story `LoginView.story.vue`. **Регресс зелёный:** lint:vue + type-check + format:check OK; vitest 10/10 за 3.01 сек; vite build 212 модулей за 383 ms (LoginView в отдельный chunk 43.5 KB JS / 51.7 KB CSS — lazy-import работает); story:build 2 stories за 29.94 сек; **Pest 48/48 за 4.86 сек**. Реестр v1.27→v1.28.*

*CLAUDE.md v1.18 от 08.05.2026 (поздний вечер). Изменения v1.18: **фаза 2 по тулчейну закрыта 6/6** — добавлен **#24 Histoire 1.0-beta.1** + `@histoire/plugin-vue@1.0.0-beta.1`. Установка через `--legacy-peer-deps` (Histoire 1.0-beta.1 заявляет peerDep `vite ^7`, у нас Vite 8 — runtime smoke OK). Конфиги: `app/histoire.config.ts` (HstVue plugin + Forest primary palette), `app/resources/js/histoire.setup.ts` (`defineSetupVue3` регистрирует `vuetify` для каждой story). Первая story: `app/resources/js/components/AppShell.story.vue` (smoke). npm-scripts: `story` (dev), `story:build` (статическая сборка в `.histoire/dist/`), `story:preview`. `.gitignore` дополнен `/app/.histoire/`. Story:build smoke: 1 story / 1 variant за 30.25 сек, vendor.js содержит `liderraForest` + `histoire.setup`. Регресс зелёный: lint:vue ✅, type-check ✅, format:check ✅, vitest 3/3 за 2.98 сек, vite build 158 модулей за 334 ms, **Pest 48/48** за 5.12 сек. Активно теперь **18 из 28** инструментов (9 фаза 0 + 8 фаза 1 + 6 фаза 2 — фаза 2 по тулчейну полностью закрыта). Реестр v1.26→v1.27, Tooling v1.6→v1.7.*

*CLAUDE.md v1.17 от 08.05.2026 (поздний вечер). Изменения v1.17: **триггер фазы 2 выполнен** — Vue 3 + Vuetify 3 + ESLint+Prettier+Vue + vue-tsc + Vitest установлены и работают. Активно 5 из 6 инструментов фазы 2 (без Histoire — отдельный коммит). Tailwind удалён (правило §5 п.2). Установлены: vue@3.5, vuetify@3.12, @vitejs/plugin-vue@6 (Vite 8 совместимость), vite-plugin-vuetify@2 (auto-import), sass-embedded, 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, vitest@4.1, @vue/test-utils, jsdom, @vitest/coverage-v8. Конфиги: `vite.config.js` (vue + vuetify auto-import), `vitest.config.ts` (jsdom + setup file для ResizeObserver/IntersectionObserver/matchMedia stubs), `eslint.config.js` (flat-config), `.prettierrc.json`, `tsconfig.json`. Палитра Forest в `resources/js/plugins/vuetify.ts` (Teal/ivory/теало-нуар). `resources/js/components/AppShell.vue` — первый компонент: v-app + v-app-bar + v-card. `welcome.blade.php` обновлён под Vue mount. Vitest 3/3 за 2.8 сек. Build: 158 модулей за 386 ms (184 KB JS / 295 KB CSS). Lefthook job #8 (ESLint на staged .ts/.vue) добавлен в pre-commit. npm-scripts: lint:vue, format, format:check, type-check, test:vue. Реестр v1.25→v1.26.*

*CLAUDE.md v1.16 от 08.05.2026 (поздний вечер). Изменения v1.16: **failed() callback** в `ProcessWebhookJob` — после исчерпания 3 ретраев упавший job сохраняется в `failed_webhook_jobs` для ручного разбора и повторного запуска через админку. Создана модель `App\Models\FailedWebhookJob`. Запись через `DB::table` (не Eloquent) чтобы обойти RLS — failed-callback запускается вне транзакции воркера и без `app.current_tenant_id`. payload пишется через `json_encode(JSON_UNESCAPED_UNICODE)` — UTF-8 кириллица сохраняется. Sentry::captureException — TODO для production (на dev-стеке нет DSN). 3 новых Pest-теста: failed() пишет с webhookLogId, failed() БЕЗ webhookLogId (NULL ok), UTF-8 payload корректен. **Pest 48/48 зелёные** за 4.7 сек. Реестр v1.24→v1.25.*

*CLAUDE.md v1.15 от 08.05.2026 (поздний вечер). Изменения v1.15: **pg_partman replacement** — Artisan `partitions:create-months --ahead=N` (default 2) создаёт ежемесячные партиции для `deals` и `supplier_lead_costs` (синхронно по received_at). Идемпотентна (проверка через `pg_class WHERE relkind='r'` перед CREATE). Замена расширения pg_partman на native Windows-стеке (см. project_phase1_strategy.md). Запускать ежесуточно через Windows Task Scheduler / cron. Smoke-test на dev: 6 новых партиций (Nov 2026 - Jan 2027) + 12 skipped. 4 новых Pest-теста: создание на 8 мес вперёд, идемпотентность, --ahead=0, INSERT в новую партицию работает. **Pest 45/45 зелёные** за 4.9 сек. Реестр v1.23→v1.24.*

*CLAUDE.md v1.14 от 08.05.2026 (поздний вечер). Изменения v1.14: **Биз-19 закрыт** — `DuplicateDetector` антифрод-сервис интегрирован в `ProcessWebhookJob`. При создании НОВОЙ сделки ищется master по `(tenant_id, phone)` в окне 24 ч (`received_at >= NOW() - INTERVAL '24 hours'`, `WHERE duplicate_of_id IS NULL`). Если найден — новой сделке проставляется `duplicate_of_id = master.id`, баланс НЕ списывается, SupplierLeadCost НЕ создаётся. ActivityLog пишется с `context.duplicate_of=master.id`. Окно фиксированное 24 ч (§10.8.1, не настраивается на MVP). 4 новых Pest-теста: master в окне 24ч → дубль; master старше 24ч → НЕ дубль; изоляция по tenant_id; ActivityLog context.duplicate_of. **Pest 41/41 зелёные** за 4.1 сек. DI через `app(DuplicateDetector::class)` внутри handle (не в сигнатуре — для совместимости с прямым вызовом из тестов). Реестр v1.22→v1.23.*

*CLAUDE.md v1.13 от 08.05.2026 (поздний вечер). Изменения v1.13: **Webhook PoC завершён** — закрыты все TODO в `ProcessWebhookJob`. Добавлены 5 Eloquent-моделей: `BalanceTransaction` (списание lead_charge -1 при создании сделки, type-константы), `ActivityLog` (event=deal.created с context.source=webhook, event-константы), `RejectedDealsLog` (zero_balance ветка вместо Log::info), `SupplierLeadCost` (composite PK, snapshot cost_rub из suppliers, supplier_id resolved через project_suppliers m2m), `Supplier` (минимальная для FK target). Job::handle() реструктурирован в `chargeNewLead()` + `logRejection()` + `resolveSupplierId()` + `upsertDeal()`. SupplierLeadCost создаётся только если у проекта есть активный supplier через project_suppliers (graceful skip + Log::warning иначе — TODO для production: SystemSetting fallback). 6 новых Pest-тестов: BalanceTransaction lead_charge, дубль НЕ создаёт BalanceTransaction, ActivityLog event=deal.created, RejectedDealsLog reason=zero_balance, SupplierLeadCost snapshot cost_rub, SupplierLeadCost graceful skip. **Pest 37/37 зелёные** за 3.9 сек. Pint+Larastan чисто (ide-helper:models регенерирован для 5 новых моделей). Реестр v1.21→v1.22.*

*CLAUDE.md v1.12 от 08.05.2026 (поздний вечер). Изменения v1.12: **CTO-17 addendum** — schema.sql v8.6 → v8.7 + pivot архитектуры upsert на advisory lock. PoC `App\Jobs\ProcessWebhookJob` поймал FK violation в `webhook_dedup_keys`: §5.5 v8.6 спецификация делает INSERT в dedup_keys ДО INSERT в deals, а FK был immediate. Сначала добавил `DEFERRABLE INITIALLY DEFERRED` (schema v8.7) — в bare-транзакции production worker'а работает. Но Pest-тесты с `DatabaseTransactions` trait всё равно падали: PG проверяет deferred constraints на RELEASE SAVEPOINT (внутренняя `DB::transaction()` Job'а становится savepoint при наличии outer-txn от теста), не на outer COMMIT. Воспроизведено standalone PHP-скриптом — это PG-семантика subtransactions. Финальный паттерн: `pg_advisory_xact_lock` сериализует concurrent webhook'и с тем же (tenant_id, vid) → SELECT в dedup_keys атомарен → INSERT deal первым (FK immediate OK) → INSERT dedup_key. Работает identically в любой вложенности транзакций. DEFERRABLE FK сохранён в schema как defense-in-depth для batch-импортов без savepoint. Создан backend-стек: Deal/WebhookDedupKey Eloquent-модели, DealFactory (composite PK setKeysForSaveQuery override), ProcessWebhookJob (advisory-lock upsert), 12 новых Pest-тестов (DealModelTest 6 + ProcessWebhookJobTest 6). **Pest полный прогон 31/31 зелёные** за 2.7 сек. CHANGELOG_schema §W (две стадии решения), narrative §2.4/§5.5/§6.5/§11 синхронизированы. Реестр v1.20 → v1.21.*

*CLAUDE.md v1.11 от 08.05.2026 (поздний вечер). Изменения v1.11: **закрыт техдолг v1.10** — narrative ТЗ синхронизирован под schema v8.6 двустадийный dedup. In-place правки в 8 точках: §2.4 (поток webhook), §5.5 (PHP-код ProcessWebhookJob — раздельные INSERT/UPDATE через `webhook_dedup_keys` + RETURNING is_new), §5.6 (таблица крайних случаев — дубль vid), §6.5 (SQL-пример идемпотентности импорта — двустадийный INSERT в dedup_keys → INSERT/UPDATE deals), §11 (DDL deals — `UNIQUE INDEX` → `INDEX`; добавлен DDL `webhook_dedup_keys` с composite FK на `deals(id, received_at)` ON DELETE CASCADE + RLS), §20.12.3 (поток в транзакции для supplier_lead_costs), §21.1 (формулировка «списание не происходит при дублях»), §27.1 (итог по идемпотентности). Версия narrative не бампается (как для L13-гигиены `3a9ed71`). Реестр Открытых вопросов v1.19→v1.20.*

*CLAUDE.md v1.10 от 08.05.2026 (поздний вечер). Изменения v1.10: **backend multi-tenant фундамент развёрнут** — schema.sql **v8.5 → v8.6** (CTO-17 закрыт фиксом); `php artisan migrate:fresh` прошёл за 870 ms, БД `liderra` содержит 68 таблиц (включая 16 партиций), 36 RLS-policies. Заменено: §0 ссылки (schema v8.6, реестр v1.19, ТЗ техдолг §15-16 — фактически §2.4/5.5/6.5/11/20.12.3/21.1/27.1, hygiene закрыта в v1.11), §2 метрики (54→55 таблиц, 91→92 индекса, 35→36 RLS, 12→13 триггеров, 4→5 функций), §6 (фаза 1 фундамент развёрнут, добавлены deployment-скрипты `db/00_create_roles.sql` + `db/02_grants.sql`). Реестр Открытых вопросов v1.18→v1.19 (CTO-17 закрыт). CHANGELOG_schema.md дополнен записью §X.*

*CLAUDE.md v1.9 от 08.05.2026 (поздний вечер). Изменения v1.9: фаза 1 по тулчейну **закрыта** — добавлены **#15 squawk v2.51.0** + **#16 pgFormatter v5.9**. Обновлены §0 (ссылка на Tooling v1.6), §6 «Текущая фаза» (15→17/28 активных), фактический пакет фазы 1 теперь полный (8/9, без #17 pg_partman — Windows native стек). Squawk: pre-commit hook на staged `*.sql` + `.squawk.toml` (9 правил исключено). pgFormatter: только ручной режим через `npm run format:sql:check` / `npm run format:sql` — авто-fix хук недопустим (diff vs schema.sql 3255 строк, ручной стиль). Tooling v1.5→v1.6.*

*CLAUDE.md v1.8 от 08.05.2026 (поздний вечер). Изменения v1.8: фаза 1 в работе — обновлены §6 «Текущая фаза» (15/28 активных, 6 фазы 1: #10 Boost+#11 Pint+#12 Larastan+#13 Roave/SA+#14 IDE Helper+#18 Pest 4) и §7 «Laravel Boost» (переписан под фактическую установку 08.05: wizard сломан на кириллице → manual setup, `boost.json` минимален, Roster auto-detect делает «отключение guidelines» избыточным, путь Vuetify guideline скорректирован на `app/.ai/guidelines/vuetify.md`). Tooling v1.4→v1.5.*

*CLAUDE.md v1.7 от 08.05.2026 (поздний вечер). Изменения v1.7: закрыт техдолг v1.6 — narrative синхронизирован под Laravel 13. Точечные правки в трёх файлах (без изменения версии каждого документа): `CRM_bp-gr_Инструкция_v8_5.md:6219`, `Vybor_oblaka_v8_3.md:3`, `Админка_SaaS_v8_2.md:103` — `Laravel 11` → `Laravel 13`. Не трогалось намеренно (исторические записи): `Объединённый_конспект.md:149` (Часть I — фиксация решений v8.0 на 03.05.2026), `Открытые_вопросы_v8_3.md:354` (фиксация результата аудита). Открытые_вопросы v1.17→v1.18.*

*CLAUDE.md v1.6 от 08.05.2026 (поздний вечер). Изменения v1.6: переоткрыт стек §2 — **Laravel 11 → Laravel 13** после обнаружения, что `composer create-project` без `^11` подтянул 13.7. Live-проверка показала совместимость 5 ключевых плагинов (Boost, Larastan, Pest, IDE Helper, Pint). Заказчик принял Laravel 13 как latest stable. Техдолг: синхронизация narrative ТЗ + Vybor_oblaka + Админка_SaaS под Laravel 13 — отдельная задача для следующих сессий. Открытые_вопросы v1.16→v1.17 (новый блок), Tooling v1.3→v1.4 (Laravel 13 в §0).*

*v1.5 от 08.05.2026 (поздний вечер). Изменения v1.5: переоткрыт+закрыт CTO-12 — **Pest 3 → Pest 4** (§3.2 строка 18, §7 п.5) после live-проверки на стеке (smoke-test 2/2 на default-тестах Laravel 11, 281 ms). Бонус Pest 4: browser testing без Dusk, stress, mutation v2. Tooling v1.2→v1.3, Открытые_вопросы v1.15→v1.16.*

*v1.4 от 08.05.2026 (вечер). Изменения v1.4: native-стек фазы 1 (§6, §7 п.3) — Docker/Sail/WSL2 невозможны на OpenStack-VPS без nested virtualization. PostgreSQL 16 + Memurai (Redis 7-совм.) + native PHP. Tooling v1.1→v1.2.*

*v1.3 от 08.05.2026: закрыт Диз-1 — HTML-прототипы покрыты handoff'ом Платона (§0, §6). Открытые_вопросы v1.14→v1.15. Открытых ⏸ продуктовых: 4 (Б-1 P0 + Диз-3/DO-2/DO-4 P1, все ждут Б-1).*

*v1.2 от 08.05.2026: закрыт CTO-12 — выбран Pest 3 (§3.2 строка 18, §7 п.5). Tooling v1.0→v1.1, Открытые_вопросы v1.13→v1.14.*

*v1.1 от 08.05.2026: ребрендинг Лидпоток→Лидерра; brandbook v1.1 удалён, источник — BRANDBOOK_v2.md из handoff Платона; добавлен handoff в §0; §2 палитра Forest; §6 — 13 концептов в web/v8/.*
