From ed9bade863dd2be4361d4961188f435aba7f6a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 11 May 2026 00:46:51 +0300 Subject: [PATCH] feat: extract brain artifacts from Liderra + ~/.claude/ project-files/: - CLAUDE.md.template (266 lines) - docs/Pravila_raboty_Claude.template.md (720 lines) - docs/Plugin_stack_rules.template.md (916 lines) - docs/Tooling.template.md (613 lines) - docs/CHANGELOG_claude_md.template.md - docs/visualizations/hooks-skills-plugins-map.html (3122 lines) - .mcp.json.template (universal: playwright/github/semgrep; laravel-boost dropped) user-level-files/: - hooks/ (10 Python files: skill-marker, skill-check, economy-* x8) - settings-fragment.json (enabledPlugins + permissions + hooks only) - marketplaces.json (3 sources) - plugins-manifest.json (4 plugins pinned with gitCommitSha) - mcp-user.template.json (magic with <> placeholder) Gitleaks scan: 0 findings. Co-Authored-By: Claude Opus 4.7 (1M context) --- project-files/.mcp.json.template | 29 + project-files/CLAUDE.md.template | 266 ++ .../docs/CHANGELOG_claude_md.template.md | 163 + .../docs/Plugin_stack_rules.template.md | 916 +++++ .../docs/Pravila_raboty_Claude.template.md | 720 ++++ project-files/docs/Tooling.template.md | 613 ++++ .../hooks-skills-plugins-map.html | 3122 +++++++++++++++++ user-level-files/hooks/economy-mode-test.py | 157 + user-level-files/hooks/economy-mode.py | 307 ++ user-level-files/hooks/economy-postcompact.py | 66 + .../hooks/economy-self-check-test.py | 116 + user-level-files/hooks/economy-self-check.py | 73 + .../hooks/economy-state-guard-test.py | 93 + user-level-files/hooks/economy-state-guard.py | 117 + user-level-files/hooks/economy-verifier.py | 49 + user-level-files/hooks/skill-check.py | 59 + user-level-files/hooks/skill-marker.py | 25 + user-level-files/marketplaces.json | 26 + user-level-files/mcp-user.template.json | 16 + user-level-files/plugins-manifest.json | 45 + user-level-files/settings-fragment.json | 125 + 21 files changed, 7103 insertions(+) create mode 100644 project-files/.mcp.json.template create mode 100644 project-files/CLAUDE.md.template create mode 100644 project-files/docs/CHANGELOG_claude_md.template.md create mode 100644 project-files/docs/Plugin_stack_rules.template.md create mode 100644 project-files/docs/Pravila_raboty_Claude.template.md create mode 100644 project-files/docs/Tooling.template.md create mode 100644 project-files/docs/visualizations/hooks-skills-plugins-map.html create mode 100644 user-level-files/hooks/economy-mode-test.py create mode 100644 user-level-files/hooks/economy-mode.py create mode 100644 user-level-files/hooks/economy-postcompact.py create mode 100644 user-level-files/hooks/economy-self-check-test.py create mode 100644 user-level-files/hooks/economy-self-check.py create mode 100644 user-level-files/hooks/economy-state-guard-test.py create mode 100644 user-level-files/hooks/economy-state-guard.py create mode 100644 user-level-files/hooks/economy-verifier.py create mode 100644 user-level-files/hooks/skill-check.py create mode 100644 user-level-files/hooks/skill-marker.py create mode 100644 user-level-files/marketplaces.json create mode 100644 user-level-files/mcp-user.template.json create mode 100644 user-level-files/plugins-manifest.json create mode 100644 user-level-files/settings-fragment.json diff --git a/project-files/.mcp.json.template b/project-files/.mcp.json.template new file mode 100644 index 0000000..444cc6e --- /dev/null +++ b/project-files/.mcp.json.template @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/anthropics/claude-code/main/schemas/mcp.json", + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "-y", + "@playwright/mcp@latest" + ], + "comment": "Фаза 0 #2 — открыть web/*.html, screenshot, проверка интерактива" + }, + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp", + "headers": { + "Authorization": "Bearer ${GITHUB_TOKEN}" + }, + "comment": "Фаза 0 #3 — официальный hosted GitHub MCP (https://github.com/github/github-mcp-server). Требует env GITHUB_TOKEN с PAT (scopes: repo, read:org, не давать admin/delete). Раньше использовали deprecated @modelcontextprotocol/server-github — заменён 06.05.2026." + }, + "semgrep": { + "command": "npx", + "args": [ + "-y", + "semgrep-mcp" + ], + "comment": "Фаза 3 #25 — Semgrep MCP (SAST). Семантический поиск/анализ кода через Semgrep rules в Claude Code. Пакет: npmjs.com/package/semgrep-mcp — если 404, запустить 'npm search semgrep mcp' для актуального имени." + } + } +} diff --git a/project-files/CLAUDE.md.template b/project-files/CLAUDE.md.template new file mode 100644 index 0000000..d6b18db --- /dev/null +++ b/project-files/CLAUDE.md.template @@ -0,0 +1,266 @@ +# CLAUDE.md — техконтекст Лидерры + +**Версия:** 1.86 от 10.05.2026 (поздний вечер) — закрытие 13 находок третьего аудита правил использования плагинов и скилов (4 P0 + 5 P1 + 2 P2 + 2 sync-правки в README/README_АРХИВ). Через `/claude-md-management:claude-md-improver`. Ключевые правки: **P0-01 §3 header «Карта 28 инструментов» → «33 инструментов»** (header застрял с pre-FD эпохи, в то время как контент включает #1–#33); **P0-02 §3.4 header «(+5, итого 28)» → «итого 29»** (после добавления #30 в фазу 2 фаза 3 cumulative должна быть 29, не 28); **P0-03 §3.3 footer «из 30 номеров минус #1 = 29 active» → «из 33 номеров (29 phase-slot + 3 off-phase + 1 historic)»** (формулировка предшествовала формализации #31/#32/#33); **P0-04 §6 «Активно: 19 инструментов из 29» + «(19/29 активны по фазам)» → «24 / (24/29)»** (внутренний арифметический конфликт: тут же раскладка 9+8+7=24, но числовая метка застряла на 19 с эпохи когда фаза 2 имела ~4 активных); **P1-06 §5 п.5 «PSR_v1 v1.5+» → «v1.7+»** (sync после bump'а PSR_v1); **P2-02 §3.3 #33 «вне Pravila §13» → «вне UI-пула §13»** (Pravila §13.2 v1.10 включает claude-md-management как infrastructure subsection — текущая формулировка вводила в заблуждение). Связанные обновления: **PSR_v1 v1.6 → v1.7** (sync cross-refs шапки на v1.86/v1.10/v1.15; description-fix описки «slot уровня 2.5» → «2b» внутри changelog'а v1.6, фактическое R0.1 всегда содержало «2b»). **Tooling v1.14 → v1.15** (sync cross-refs шапки на v1.86/v1.10/v1.7; §11.5/§12 «28 инструментов» → «33 формализованные позиции»). Pravila v1.10 — без изменений. Предыдущая v1.85 — закрытие 15 находок второго аудита (детали в [docs/CHANGELOG_claude_md.md](docs/CHANGELOG_claude_md.md)). +**Назначение:** оперативная карта для Claude Code. Не первоисточник — первоисточники указаны в §0. +**Владелец и режим правок:** все изменения этого файла — **только** через плагин `claude-md-management` (skills `/claude-md-management:claude-md-improver` для audit/targeted-updates и `/claude-md-management:revise-claude-md` для capture session-learnings). Прямые правки запрещены — см. §5 п.11. + +> **Ребрендинг 08.05.2026:** «Лидпоток» → **«Лидерра.»** (с точкой). Палитра, лого и шрифты — из handoff Платона (v8 Forest). Применяется только к дизайну/имени/логотипу; функционал, состав страниц и правила — без изменений (источник — ТЗ v8.5/schema v8.5). + +--- + +## 0. Источник истины + +| Тема | Документ | +|---|---| +| Продуктовые правила работы Claude | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (**v1.10 от 10.05.2026 вечер** — §0 +note про §11 локальное override-исключение над §2.2/§4.5/§8.4; §11.5 «10 правил» → «v1.6, 16»; §13.2 «v1.4 (15 правил)» → «v1.6 (16)»; §13.9/§13.10 PSR_v1 v1.4 → v1.6; v1.9 наследие — §12.3 SoT, §13.2 +claude-md-management off-pool, §13.6 hard-rule tier-структура) | +| **Правила совместного использования плагинов Claude** | [docs/Plugin_stack_rules_v1.md](docs/Plugin_stack_rules_v1.md) (**v1.7 от 10.05.2026 поздний вечер** — sync cross-refs шапки на актуальные версии связанных документов после bump'ов CLAUDE.md v1.85 → v1.86 и Tooling v1.14 → v1.15; description-fix описки «slot уровня 2.5» → «slot уровня 2b» внутри changelog'а v1.6 (фактическое R0.1 line 33 всегда содержало «2b»); v1.6 наследие — R0.4.A свёрнут до cross-ref на Pravila §12.3 SoT, R0.6 пронумерован 1–11; v1.5 наследие — R10.1 разбит на 3 блока (enabledPlugins/built-in/MCP), R10.4/R14.7 tier-метки, R8 +тай-брейкер FD↔21st) | +| Полный реестр 33 формализованных позиций тулчейна (29 active + 3 off-phase + 1 historic) | [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) (**Прил. Н v1.15 от 10.05.2026 поздний вечер** — sync cross-refs шапки на актуальные версии связанных документов после bump'ов CLAUDE.md v1.85 → v1.86 и PSR_v1 v1.6 → v1.7 («Pravila v1.9+» → «v1.10+», «PSR_v1 v1.5+» → «v1.7+», «CLAUDE.md v1.84+» → «v1.86+»); §11.5/§12 «28 инструментов» → «33 формализованные позиции» (DevOps-раздел застрял с эпохи v1.0 — фразы «не входят в 28», «вне 28»); v1.14 наследие — §10.3 шаг 2 «3 skills» → «14», §13 +v1.13 +v1.14 entries, §7 +Tooling explicit slot 2b alongside CLAUDE.md; v1.13 наследие — §7 +PSR_v1 уровнем 3, §4.7 +#33 claude-md-management, §6 +5 конфликтов v1.4, §4.6 settings → .claude.json, §0 счётчик 33) | +| Главное ТЗ | [docs/CRM_bp-gr_Инструкция_v8_5.md](docs/CRM_bp-gr_Инструкция_v8_5.md) (v8.5 от 07.05.2026 — реализация 27 решений аудита C; in-place hygiene v1.20 от 08.05.2026 поздний вечер: §2.4/§5.5/§5.6/§6.5/§11/§20.12.3/§21.1/§27.1 синхронизированы под schema v8.6 двустадийный dedup) | +| Схема БД | [db/schema.sql](db/schema.sql) (**v8.11 от 09.05.2026** — Sprint 1 Phase A: RLS на `impersonation_tokens` + 2 missing FK indices (audit P0-02 + O-perf-02/03). Метрики: 56 базовых таблиц + 12 партиций + 97 индексов + 38 RLS + 5 функций + 13 триггеров) | +| Открытые вопросы | [docs/Открытые_вопросы_v8_3.md](docs/Открытые_вопросы_v8_3.md) (**v1.75 от 09.05.2026 — Post-MVP Reports backend закрыт** (4 этапа); MVP по Claude-зоне закрыт в v1.74; финал-метрики Pest 403/403 + Vitest 393/393 + Histoire 21/43) | +| **Брендбук** | [liderra_v8_handoff/docs/BRANDBOOK_v2.md](liderra_v8_handoff/docs/BRANDBOOK_v2.md) **(v2 Forest от 07.05.2026)** — старый `docs/brandbook.md` v1.1 удалён 08.05.2026 | +| **Дизайн-handoff (токены, компоненты, 25 экранов)** | [liderra_v8_handoff/docs/DEVELOPER_HANDOFF.md](liderra_v8_handoff/docs/DEVELOPER_HANDOFF.md) (v8 Forest от 07.05.2026) — **только дизайн/токены/компоненты**; функционал и состав экранов — по ТЗ v8.5 | +| Анализ оригинала | [docs/Analiz_originala_v8_3.md](docs/Analiz_originala_v8_3.md) (Прил. М v1.1) | +| ТЗ рекламного лендинга | [лендинг/TZ_landing_v1_0.md](лендинг/TZ_landing_v1_0.md) (v1.0 от 08.05.2026, ⏸ Б-1 для продакшена) | +| Состав архива | [docs/README_АРХИВ_v8_5.md](docs/README_АРХИВ_v8_5.md) (v8.5 от 07.05.2026) | + +Этот файл — **оперативная карта**. При противоречии — приоритет у источников выше. + +--- + +## 1. Приоритет правил при конфликте + +``` +0. Pravila §12 — Superpowers hard rule (инвокация skills первой) ← неотменяемо §9 + ↓ +1. docs/Pravila_raboty_Claude_v1_1.md (продуктовые правила, утверждены заказчиком) + ↓ +2a. CLAUDE.md (общая оперативная карта) +2b. docs/Tooling_v8_3.md Прил. Н (детальный реестр 33 инструментов) + ↑ оба operational maps уровня 2; при конфликте между ними — приоритет CLAUDE.md + ↓ +3. docs/Plugin_stack_rules_v1.md (координация Superpowers + Frontend Design — gate, фазы, разделения) + ↓ +4. .claude/settings.json (хуки, permissions — исполняется средой) + ↓ +5. memory/*.md (динамическая память между сессиями) + ↓ +6. Прочие плагины (claude-md-management, ui-ux-pro-max и т.п.) — поведенческие подсказки +``` + +При любом противоречии — выбирается верхний уровень. **§12 правил Claude — единственное explicit hard-правило в Pravila; §9 «Отступления» к нему не применяется** (см. Pravila §12.4). Дополнительно §13.9 и §13.10 Pravila — **transitive hard-rule** через hard-link на нарушения PSR_v1 R10/R14 (см. Pravila §13.6 tier-таблицу). Plugin_stack_rules_v1 (уровень 3) — координирующий слой между двумя плагинами Claude; ниже Pravila/CLAUDE.md/Tooling, выше settings.json. **Tooling Прил. Н** (уровень 2b, добавлен в v1.85) — детальный реестр инструментов; alongside CLAUDE.md (оба operational maps), но при прямом конфликте между ними побеждает CLAUDE.md как корневая карта Claude Code. + +**Scope этой цепочки (v1.85+):** общая 7-уровневая **файловая/слоевая** иерархия (уровень 2 разделён на 2a CLAUDE.md + 2b Tooling — оба operational maps). Не дублирует: + +- **Pravila §0** — внутрипараграфный приоритет внутри Pravila (§1–§13). +- **PSR_v1 R0.1** — scope головенства stack'а внутри уровней 4–6 этой цепочки. +- **Tooling §7** — синхронная копия этой цепочки для Tooling-читателей. + +--- + +## 2. Стек проекта + +| Слой | Что | +|---|---| +| Backend | PHP 8.3 + **Laravel 13** (мажор переоткрыт 08.05.2026 поздний вечер: при `composer create-project laravel/laravel` без `^11` Composer подтянул свежайшую 13.7; live-проверка совместимости — Boost v2.4.6, Larastan v3.9.6, Pest v4.7.0, IDE Helper v3.7.0, Pint v1.29 работают; принято заказчиком) | +| Frontend | Vue 3 + **Vuetify 3** (НЕ Tailwind, НЕ Inertia, НЕ Livewire, НЕ Filament) | +| БД | PostgreSQL 16 (**56 базовых таблиц + 12 партиций, 97 индексов, 38 RLS-политик, 4 роли БД, 13 триггеров, 5 функций** — schema v8.11 от 09.05.2026; backend multi-tenant фундамент развернут на dev `liderra` через `php artisan migrate:fresh`) | +| Кэш / очереди | Redis 7 | +| Pooler | PgBouncer (transaction pooling) | +| Облако | Yandex Cloud, регион `ru-central1` (Москва) | +| SSO админов | Yandex 360 | +| Email | Unisender Go (SMTP-relay) | +| Sentry | self-hosted в Yandex Cloud | +| Helpdesk | JivoSite | + +**Шрифты:** Inter (UI, axis `opsz` 14..32), JetBrains Mono (numerics с `tnum`, код). **Иконки:** Lucide. **Палитра v8 Forest:** Teal `#0F6E56` (primary, неоспариваемый), `#F6F3EC` warm ivory (page bg), `#012019` теало-нуар (sidebar). 14 OKLCH-статусов в [BRANDBOOK_v2 §3.6](liderra_v8_handoff/docs/BRANDBOOK_v2.md) — **палитра используется**, но мапить на 14 slug'ов из [db/schema.sql:2076](db/schema.sql#L2076) (источник истины для статусов воронки — schema/ТЗ §6.4, не handoff). **A11y:** WCAG 2.1 AA. **Animation default stack** (R11.6 + R15 PSR_v1): Vue native `` / `` + Vuetify transitions (`v-fade`, `v-slide-y`, `v-scale`, `v-expand`, `v-dialog-transition`) + CSS `@keyframes` + `prefers-reduced-motion` + View Transitions API (Chrome 111+ / Safari 18+). motion-v / framer-motion / gsap / anime.js / lottie-web — **не установлены** и условно разрешены только по R15.2 (4 триггера), см. §5 п.12. + +--- + +## 3. Карта 33 инструментов — «когда что использовать» + +Полный реестр с командами установки и конфликтами — [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md). Здесь — оперативная карта для частых задач. + +### 3.1. Фаза 0 — документация (9 активных) + +| # | Задача | Инструмент | Команда | +|---|---|---|---| +| 1 | ~~SQL-запрос к dev-БД~~ | ~~PostgreSQL MCP~~ | заменён #10 Boost (см. §3.2) | +| 2 | Открыть/проверить `web/*.html` | Playwright MCP | через MCP | +| 3 | Issues / PR | GitHub MCP | через MCP | +| 4 | Линт `.md` | markdownlint-cli2 | `npm run lint:md` | +| 5 | Орфография `.md` | cspell | `npm run spell` | +| 6 | Проверка ссылок в `.md` | lychee | `npm run links` | +| 7 | Линт CSS в прототипах | Stylelint | `npm run lint:css` | +| 8 | Поиск секретов в diff | gitleaks | pre-commit hook | +| 9 | A11y проверка прототипов | Pa11y | `npm run a11y` | + +### 3.2. Фаза 1 — старт Laravel (+8 нетто, итого 17) + +| # | Задача | Инструмент | Команда | +|---|---|---|---| +| 10 | SQL / Eloquent / docs Laravel | **Laravel Boost** (заменяет PG MCP) | через MCP-tools Boost'а | +| 11 | Code style PHP | Laravel Pint | `composer pint` | +| 12 | Статанализ PHP | Larastan | `composer stan` (CI) | +| 13 | CVE на install | Roave/SecurityAdvisories | автоматически на `composer install` | +| 14 | IDE-stubs PHP | Laravel IDE Helper | `php artisan ide-helper:generate` | +| 15 | Линт миграций PostgreSQL | squawk | в pre-commit для `database/migrations/*.php` | +| 16 | Форматирование SQL | pgFormatter | хук на правке `db/schema.sql` | +| 17 | Партиционирование PG | pg_partman | расширение БД | +| 18 | Тесты PHP | **Pest 4** (CTO-12 переоткрыт+закрыт 08.05.2026 поздний вечер; backward-compat с 3, бонус: browser/stress/mutation v2) | `composer test` | + +### 3.3. Фаза 2 — старт frontend (+7, итого 24) + +| # | Задача | Инструмент | Команда | +|---|---|---|---| +| 19 | Поведение Claude (TDD/debug/review/plans/parallel) | Superpowers v5.1.0 — все 14 skills | автоматически (override §2.2/§4.5/§8.4 разрешён, см. Pravila §11) + **§12 hard rule: skill инвокируется ПЕРВЫМ** для подходящих задач (карта §12.2 правил Claude); координация с #30 Frontend Design — см. [docs/Plugin_stack_rules_v1.md](docs/Plugin_stack_rules_v1.md) | +| 20 | Vue language server | Volar | VSCode-расширение | +| 21 | Type-check Vue | vue-tsc | `npm run type-check` (CI only) | +| 22 | Линт + форматтер JS/Vue (связка) | ESLint + Prettier + config-prettier + plugin-vue | `npm run lint:vue`, `npm run format` | +| 23 | Тесты Vue | Vitest | `npm run test:vue` | +| 24 | Каталог компонентов | Histoire (НЕ Storybook) | `npm run story` | +| 30 | Доменная база UI (компоненты, паттерны, состояния, a11y-принципы) | **Frontend Design plugin** (Anthropic, paired со Superpowers) | автоматически через `~/.claude/settings.json`; **обязательный стек-фильтр** Vue+Vuetify (см. [Plugin_stack_rules_v1.md](docs/Plugin_stack_rules_v1.md) Правило 6) | +| 31 | Резерв-библиотека UI (50+ стилей, 161 палитра, 99 UX-гайдлайнов, 25 типов графиков) — *off-phase tool* | **UI UX Pro Max** (skill `ui-ux-pro-max@ui-ux-pro-max-skill`) | автоматически через `~/.claude/settings.json`; активация — только через PSR_v1 v1.6 R14.3 pipeline (фаза 2 R2 fallback к FD ИЛИ фаза 1 R2 «третий вариант» в R12 архитектурном); R6.0 фильтр + R6.1 hard-override Forest обязательны | +| 32 | Генератор стартовых шаблонов для UI-компонентов (LLM-based) — *off-phase tool* | **21st.dev Magic MCP** (`magic` сервер в `~/.claude.json`, tools `mcp__magic__21st_magic_component_*` + `logo_search`) | активация — только через PSR_v1 v1.6 R14.4 pipeline (pre-check R0.6 пунктов 9–10: брендовый App*? Vuetify-эквивалент? существующий компонент? — все «нет» обязательно) → R6.0 фильтр (JSX→Vue, Tailwind→utility, shadcn→Vuetify) → R6.1 hard-override → FD адаптация; Pa11y обязателен на deployable | +| 33 | Инфраструктурный плагин для CLAUDE.md edits — *off-phase tool, инфраструктурная категория* | **claude-md-management** (skills `claude-md-improver` + `revise-claude-md`, marketplace `anthropics/claude-plugins-official`) | автоматически через `~/.claude/settings.json`; **обязательный канал** правок CLAUDE.md (§5 п.10). Категория **инфраструктурная** (не UI), **вне UI-пула §13** Pravila (но в §13.2 как infrastructure subsection v1.9+) — поэтому не проходит R6.0/R6.1 фильтр и R14 pipeline. Регулируется PSR_v1 R10.1 блок 1 | + +### 3.4. Фаза 3 — pre-production (+5, итого 29) + +| # | Задача | Инструмент | Команда | +|---|---|---|---| +| 25 | SAST | Semgrep + Semgrep MCP | `npm run sast` (CI) | +| 26 | Скан Docker-образов | Trivy | в CI перед push в Yandex Container Registry | +| 27 | CVE-PR авто | GitHub Dependabot | `.github/dependabot.yml` | +| 28 | Audit-логи PostgreSQL | pg_audit | расширение БД | +| 29 | Маскирование ПДн в дампах | pg_anonymizer | расширение БД | + +(Нумерация: #1–#29 — phase-slot (фазы 0–3) в порядке введения; #30 Frontend Design plugin добавлен post-MVP в фазу 2 после закрытия фазы 3 в реестре; #31 UPM, #32 21st Magic MCP, #33 claude-md-management — off-phase tools (формализованы post-факт, см. §6). **Phase-slot активных: 29** — из 30 phase-номеров минус #1 PostgreSQL MCP (заменён #10 Boost). Off-phase tools — отдельная категория, не входят в фазовую раскладку. **Total формализованных позиций: 33** = 29 phase-active + 3 off-phase + 1 historic (#1).) + +### 3.5. Заметки к `.claude/settings.json` + +- **Permissions**: список allow/deny — для фазы 0 (документация + HTML-прототипы). При переходе в фазу 1 добавить `Bash(composer:*)`, `Bash(php artisan:*)`. +- **Hooks**: один хук на авто-fix Markdown через `markdownlint-cli2` (исключая корневой `CLAUDE.md`, чтобы не зацикливаться). Pre-commit (gitleaks, link-check) — **не здесь**, отдельно через git hooks (lefthook). +- **Источник истины**: см. [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) Прил. Н §8 — source of truth для всех конфигов проекта. +- **`$schema`**: канонический URL — `https://json.schemastore.org/claude-code-settings.json` (только этот валиден для Claude Code; кастомные `_comments` / `comment` в файле — нельзя, схема их отвергает). + +--- + +## 4. Команды быстрого доступа + +```bash +# Документация (фаза 0, активно сейчас) +npm run lint:md # markdownlint +npm run spell # cspell +npm run links # lychee +npm run a11y # Pa11y +npm run check:docs # все 4 выше параллельно + +# Безопасность (Windows: бинарь в bin/, не в PATH; Linux/Mac CI ставят gitleaks через brew/apt) +./bin/gitleaks.exe detect # ручной запуск +./bin/gitleaks.exe protect --staged # в pre-commit (через lefthook) + +# Backend (фаза 1+) +composer pint # форматтер +composer stan # Larastan +composer test # Pest или PHPUnit +php artisan boost:mcp # запуск MCP-сервера Boost вручную + +# Frontend (фаза 2+) +npm run lint:vue +npm run type-check +npm run test:vue +npm run story # Histoire + +# Pre-prod (фаза 3+) +npm run sast # Semgrep +trivy image liderra:latest +``` + +--- + +## 5. Что НЕ делать + +1. **Не подключать Boost к production DB.** `.env.production` не должен попадать в локальный Boost-конфиг. +2. **Не использовать Inertia / Livewire / Tailwind / Filament / Flux UI / Nova / Folio / Volt / Wayfinder guidelines** Boost'а — у нас Vue + Vuetify. +3. **Не запускать a11y через Lighthouse** — единственный источник истины Pa11y. +4. **Не помещать ПДн / токены / API-ключи в коммиты.** Правило §5.2 правил Claude. Защита — gitleaks в pre-commit. +5. **Расширенный пул UI-инструментов — координируется через [PSR_v1](docs/Plugin_stack_rules_v1.md) v1.7+.** Кратко: paired-stack ядро (**Superpowers** = процесс / **Frontend Design** = решатель UI), плюс два инструмента **в роли материала, не решателя**: UPM (резерв-библиотека, R10.1/R11.5/R14.3) и 21st Magic MCP (генератор шаблонов, R10.1/R14.4). Все четыре проходят **R6.0 фильтр стека** (срезать React/Tailwind/shadcn/JSX → Vue 3 + Vuetify 3) и **R6.1 hard-override Forest** (палитра/шрифты/иконки/aesthetic — Brandbook, не плагины). UPM и 21st **не параллельно** с FD и друг с другом (R14.5). **A11y технический** — за Pa11y (п.3); плагины покрывают только a11y-принципы. **Детали — PSR_v1 R6/R10/R11/R14** (не копировать сюда — оперативная карта остаётся компактной). +6. **Не ставить два инструмента на одну задачу** — список 10+ запрещённых дублей в [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) §9. +7. **Не редактировать этот `CLAUDE.md` без обновления** [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) и [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) — иначе три источника разойдутся (применяется ВНУТРИ flow п.10; пропуск синхронизации — отдельная ошибка даже при работающем плагине). +8. **Не править `db/schema.sql`** без записи в [db/CHANGELOG_schema.md](db/CHANGELOG_schema.md) — правило §4.2 правил Claude. +9. **Не закрывать открытые вопросы** (`Биз-*`, `CTO-*`, `Ю-*`, `Диз-*`, `DO-*`, `OPEN-*`) без явного «закрываем» от заказчика — §2.2 правил Claude. +10. **Не править этот `CLAUDE.md` напрямую** — только через плагин **`claude-md-management`** (`anthropics/claude-plugins-official` marketplace). Два входа: + - `/claude-md-management:claude-md-improver` — audit + targeted updates (структурные изменения, добавление/удаление секций, правки версии в шапке, правки правил §5). + - `/claude-md-management:revise-claude-md` — захват learnings из текущей сессии (новые quirks, команды, паттерны → CLAUDE.md). + + Плагин — **единственный** интерфейс ведения файла; он отвечает за содержание и качество (по `references/quality-criteria.md` плагина: commands/architecture/non-obvious patterns/conciseness/currency/actionability). Прямые `Edit`/`Write` по `CLAUDE.md` без вызова skill'а — нарушение, фиксировать в feedback. Внутри flow плагина продолжают действовать пп.7 (синхронизация Pravila + Tooling) и общие §4 правил Claude. +11. **Не пропускать инвокацию Superpowers skill'а** для задачи, попадающей под карту §12.2 правил Claude (TDD, debug, plan, parallel, review, verify, brainstorm, worktree, finishing PR, subagent, writing-skills). Это **hard rule** (§12 правил Claude), §9 «Отступления» к нему **не применяется**. Рационализация типа «эта задача проще, чем требует skill» / «сейчас быстрее без skill'а» — нарушение того же уровня, что игнорирование §5 ПДн. **Список exclusions — Pravila §12.3 (Single Source of Truth, v1.9+)**: при расширении правок здесь — править только Pravila §12.3, не дублировать список текстом сюда. Запрос заказчика «не используй superpowers сейчас» — единственная отмена, и **только** на текущее действие. См. Pravila §12.4. + +12. **Не устанавливать motion runtime библиотеки без прохождения R15.2 PSR_v1** (4 условия триггера) — `framer-motion` (R15.1 hard-запрет навсегда: React-only архитектурно, не работает в Vue), `react-spring` (R15.1-аналог: тоже React-only), `motion-v` (R15.2: Vue 3 порт framer-motion, условно по 4 триггерам), `gsap`, `anime.js`, `react-spring`, `lottie-web`, `popmotion`, `@motionone/dom` (R15.7: аналогично). **Default motion stack** — Vue native `` / `` + Vuetify transitions + CSS `@keyframes` + View Transitions API (R11.6 уровни 3–6). Большинство задач закрываются на уровнях 1–2. Установка любой animation runtime библиотеки в `package.json` — **R0.6 пункт 11 hard-стоп**, Auto mode не отменяет. Подробности — PSR_v1 v1.6 R15 + Tooling Прил. Н v1.14 §9.2. + +--- + +## 6. Текущая фаза проекта + +**Post-MVP (10.05.2026).** Фазы 0/1/2 по тулчейну закрыты (24/29 активны по фазам после установки Frontend Design plugin v1.78). **+3 off-phase tools формализованы 10.05.2026:** #31 UI UX Pro Max (skill, резерв-библиотека UI, формализован v1.83), #32 21st.dev Magic MCP (генератор шаблонов UI, формализован v1.83), **#33 claude-md-management** (skills, инфраструктура CLAUDE.md edits — формализован в v1.84 после audit находки «5-й включённый плагин без номера в реестре»). UPM + 21st активируются только через PSR_v1 v1.6 R14 pipeline; claude-md-management — обязательный канал правок CLAUDE.md (§5 п.10), регулируется PSR_v1 R10.1 блок 1. Итого формализованных позиций тулчейна: **33** (19/29 активных по фазам + 3 off-phase + 1 заменённый PG MCP исторически — слот #1, заменён #10 Boost в фазе 1, формально остаётся в реестре). Без «+1 historic» арифметика «33» не сходится — это правка v1.85. MVP Claude-зоны закрыт в v1.72; затем закрыт Reports backend epic (v1.73, 4 этапа `19f319c..e0ffe7e`). 13/13 экранов handoff покрыты UI + 3 ErrorView (404/403/500); landing ⏸ Б-1. Backend: auth (login/register/2FA/recovery/forgot/reset) + deals API (index/show/store/update/transition/destroy/restore/export-CSV+XLSX) + 3 lookup-API + reminders CRUD + cron + in_app_notifications + bell-UI polling + notification-preferences PATCH + admin (tenants/billing/incidents/system) + impersonation + webhook receive (HMAC + per-token rate-limit). Все 8 schema-default событий уведомлений интегрированы. **Pest 403/403, Vitest 393/393, Histoire 21/43.** + +- Активно: **24 инструмента из 29 phase-slot** — 9 из фазы 0 (см. §3.1) + 8 из фазы 1: **#10 Boost v2.4.6**, **#11 Pint v1.29**, **#12 Larastan v3.9.6**, **#13 Roave/SecurityAdvisories**, **#14 IDE Helper v3.7.0**, **#15 squawk v2.51.0**, **#16 pgFormatter v5.9**, **#18 Pest v4.7.0** + 7 из фазы 2 (закрыта по тулчейну, см. ниже): #19 Superpowers + #20–24 + **#30 Frontend Design plugin** (paired stack). 9+8+7=24. Off-phase tools (#31 UPM + #32 21st + #33 claude-md-management) — также активны в `~/.claude/settings.json`/`~/.claude.json`, но регулируются отдельной механикой R10/R14 PSR_v1 / §5 п.10 (не входят в фазовую раскладку). +- Готово в фазе 1: Laravel 13.7 в `app/`, predis 3.4.2, **schema.sql v8.6 развёрнута через `migrate:fresh` (870 ms, 1 миграция `load_initial_schema.php` — raw SQL через `DB::unprepared(file_get_contents(...))`)**, 3 default Laravel-миграции удалены (users/cache/jobs дублировались с нашей schema), smoke-test'ы (**Pest 19/19 за 1711 ms** — 4 RLS smoke + 8 model smoke + 5 middleware + 2 default; Pint passed, PHPStan analyse passed с baseline, ide-helper:generate OK + ide-helper:models -W -M -N для @mixin IdeHelper*, squawk 0 issues с конфигом, pgFormatter dry-run OK), MCP-сервер `boost:mcp` через Roster auto-detect (9 tools, JSON-RPC 2024-11-05). **Eloquent-модели**: `Tenant`, `User`, `Project` (+ factories) — `User` переписан под нашу схему (`password_hash` вместо `password`, override `getAuthPassword()`), Soft Deletes на Tenant + User. **Middleware `SetTenantContext`** (alias `tenant`): резолюция tenant_id из `auth()->user()`, subdomain или `X-Tenant-Id` header → `SET LOCAL app.current_tenant_id` в обёртке транзакции (PgBouncer-safe). **Deployment-скрипты ролей БД** для production: `db/00_create_roles.sql`, `db/02_grants.sql`. На dev — `postgres` superuser. **CTO-13 RLS smoke-test реализован**: `tests/Feature/RlsSmokeTest.php` + `TenantModelsTest.php` + `SetTenantContextTest.php`. +- Артефакты фазы 0 без изменений: 17 файлов архива (narrative v8.5 финал 07.05.2026), **13 концептов v8 Forest в [web/v8/](web/v8/)**. +- **Стек dev**: native Windows. PostgreSQL 16 (Chocolatey, Windows-сервис) + Memurai Developer (Redis 7-совместимый, Windows-сервис) + native PHP 8.3 + Composer. **Без Docker, без WSL2** — машина OpenStack-VPS не пробрасывает nested virtualization. Подробности — `memory/project_phase1_strategy.md`. +- **Не применимо**: **#17 pg_partman** заменён ручным cron'ом — на native Windows-PG расширение недоступно (см. project_phase1_strategy). Pre-commit хуки для Pint/Larastan/squawk — в `lefthook.yml` (jobs 5/6/7). +- Активно фаза 2 (**6/6 инструментов — фаза 2 по тулчейну закрыта**): **#20 Volar** (VSCode-only), **#21 vue-tsc 3.2**, **#22 ESLint+Prettier+Vue** (eslint 10 flat-config + plugin-vue 10 + @vue/eslint-config-typescript 14 + config-prettier), **#23 Vitest 4.1** + @vue/test-utils + jsdom (**139/139 тестов**, +10 за auth-store + router-guard, за 10.11 сек), **#24 Histoire 1.0-beta.1** (21 story / **28 variants** за 31.73 сек). **Runtime-deps:** vuedraggable@4 + axios@1.16 + pinia@3.0. Frontend-стек: Vue 3.5 + Vuetify 3.12 + @vitejs/plugin-vue 6 + vite-plugin-vuetify (auto-import) + **vue-router 4.6** (createWebHistory + lazy-imports). Палитра Forest в `resources/js/plugins/vuetify.ts` (Teal `#0F6E56` primary, ivory `#F6F3EC` bg, теало-нуар `#012019` secondary). Pre-commit lefthook-job #8 ESLint на staged `resources/js/**/*.{ts,vue}`. Tailwind удалён. **Histoire vs Vite 8 несовместимость:** Histoire 1.0-beta.1 заявляет peerDep `vite ^7`, установлен через `--legacy-peer-deps`; smoke-test (build) пройден, Vuetify-плагин регистрируется через `setupFile`. При выходе совместимой с Vite 8 версии — обновить. +- Frontend-структура: `resources/js/router/index.ts` (6 маршрутов + meta.layout 'auth'/'app'), `layouts/AuthLayout.vue` (двухпанельный для auth-экранов), `layouts/AppLayout.vue` (sidebar nav-tree + topbar + RouterView для авторизованных), `views/auth/{Login,Register,TwoFactor,ForgotPassword,RecoveryCodes}View.vue` (5 auth-view'ов), `views/DashboardView.vue` (KPI-row + balance), `components/AppShell.vue` (layout-mapper по `route.meta.layout`: 'app' default → AppLayout, 'auth' → AuthLayout). Backend SPA-маршруты: `routes/web.php` явные `Route::view('/...', 'welcome')` для /, /login, /register, /forgot, /2fa, /recovery, /dashboard (явные, не catch-all — иначе перехватывал бы `_test/*` runtime-routes из Pest beforeEach). +- Триггер фазы 3: ~спринт 12. + +**P0-блокер** один: **Б-1** (реквизиты юр. лица, ждут регистрации ООО). От него зависят также Диз-3, DO-2, DO-4. Диз-1 закрыт 08.05.2026 (handoff Платона покрыл 13/8). + +--- + +## 7. Laravel Boost — фактическая установка 08.05.2026 (✅ применено) + +**Wizard `php artisan boost:install` сломан в обоих режимах на этой машине** — кириллица в пути крашит интерактив `laravel/prompts`, `--no-interaction` падает в баге L13 `ConfiguresPrompts::multiselectFallback`. Установка выполнена **вручную** (коммит `e04f53b`). Подробности — memory `feedback_environment.md` п.26. На Linux/macOS-машинах с ASCII-путями wizard работает нормально. + +**Что сделано вручную (вместо wizard'а):** + +1. **`composer require laravel/boost --dev`** — установлен Boost v2.4.6 + транзитивы (laravel/mcp v0.7.0, laravel/roster v0.5.1, symfony/yaml v7.4). +2. **Этот `CLAUDE.md` не тронут** — Boost при manual setup ничего не пишет в `app/CLAUDE.md` (и тем более в корневой). Корневой остаётся источником истины. +3. **`.mcp.json` (корень)** — добавлен блок `laravel-boost` (command=`php`, args=[`app/artisan`, `boost:mcp`]) рядом с `playwright`/`github`. PostgreSQL MCP убран ещё в фазе 0 (`_comment_postgres`). +4. **«Отключение guidelines»** — **избыточно**. Boost через `laravel/roster` auto-detect видит установленные пакеты в composer.lock и серверит только релевантные. Inertia, Livewire, Tailwind, Filament, Flux UI, Nova, Folio, Volt, Wayfinder, Sail, PHPUnit — у нас не установлены, Roster их не серверит. `boost.json` минимален: 3 ключа (`agents=[claude_code]`, `guidelines=true`, `mcp=true`). +5. **Кастомный Vuetify 3 guideline** — `app/.ai/guidelines/vuetify.md` (путь по `Laravel\Boost\Install\GuidelineComposer::userGuidelineDir = '.ai/guidelines'`; в Tooling §10.2 был указан устаревший `resources/boost/guidelines/...` — скорректирован в Tooling v1.5). +6. **`.mcp.json` и `app/boost.json`** — оба в репозитории. +7. **Pest 4** активен — Roster видит его, серверит Pest guidelines. PHPUnit убран из direct deps в коммите `30f0335` — Roster не серверит PHPUnit guideline. +8. **Production DB** — не подключать к Boost. `app/.env.production` не должен попадать в локальный конфиг. + +--- + +## 8. Self-review триггеры + +После массивных правок (≥3 групп патчей) — обязательно (§4.6 правил Claude): + +| Файл | Что проверять | +|---|---| +| `db/schema.sql` | 0 orphan-FK, целостность RLS, метрики сверять с текущей версией (v8.11 = 56 базовых таблиц + 12 партиций + 97 индексов + 38 RLS-политик + 5 функций + 13 триггеров), 0 дубликатов `CREATE TABLE` | +| narrative `.md` | Версии в шапке/колонтитуле, 0 «готовится»/«TBD», кросс-ссылки на актуальные имена файлов | +| Прил. А–Н | Версия совпадает с narrative; все упомянутые подразделы существуют | +| Прил. Н (этот реестр инструментов) | Ровно 29 в активном наборе; 0 дублей; синхронность с этим CLAUDE.md | + +Результат — кратким блоком в конце ответа. + +--- + +## 9. История версий + +Полная история — [docs/CHANGELOG_claude_md.md](docs/CHANGELOG_claude_md.md) (вынесена 09.05.2026 при правке v1.73→v1.74 ради лаконичности шапки). Здесь — последние правки: + +- **v1.86 от 10.05.2026 (поздний вечер)** — закрытие 13 находок третьего аудита правил использования плагинов и скилов (4 P0 + 5 P1 + 2 P2 + 2 sync-правки в README/README_АРХИВ). Заказчик: «проведи аудит правил использования плагинов и скилов на предмет конфликта и запутаностей» → Claude через `/claude-md-management:claude-md-improver` нашёл 12 формальных находок + 4 sync-побочки, представил quality report, получил «исправь все, только при выполнении руководствуйся правилом, прежде чем вносить изменения тебе надо проанализировать как оно влияет на другие правила, что исправляю одно не делать других ошибок», применил с cross-impact-анализом перед каждой группой. **P0 (4 — реальные арифметические конфликты в CLAUDE.md, прошли мимо второго аудита):** §3 header «Карта 28 инструментов» → «33» (header застрял с pre-FD эпохи); §3.4 header «(+5, итого 28)» → «итого 29» (после добавления #30 в фазу 2 cumulative должна быть 29); §3.3 footer «из 30 номеров минус #1 = 29 active» → расширенная формулировка «33 номеров: 29 phase-active + 3 off-phase + 1 historic»; §6 «Активно: 19 инструментов из 29» + «(19/29 активны)» → «24» в обоих местах (внутренний арифметический конфликт: тут же раскладка 9+8+7=24, но числовая метка застряла на 19 с эпохи когда фаза 2 имела ~4 активных). **P1 (5 — обновление stale `+`-refs на актуальные версии):** PSR_v1 шапка cross-refs «CLAUDE.md v1.84+/Pravila v1.9+» → «v1.86+/v1.10+»; Tooling шапка cross-refs «Pravila v1.9+/PSR_v1 v1.5+/CLAUDE.md v1.84+» → «v1.10+/v1.7+/v1.86+»; CLAUDE.md §5 п.5 «PSR_v1 v1.5+» → «v1.7+». **P2 (2 — внутренние несогласованности формулировок):** PSR_v1 line 4 «slot уровня 2.5» → «уровня 2b» (описка внутри changelog'а v1.6, фактическое R0.1 line 33 всегда содержало «2b»); CLAUDE.md §3.3 #33 «вне Pravila §13» → «вне UI-пула §13» (Pravila §13.2 v1.10 включает claude-md-management как infrastructure subsection; «вне §13» вводило в заблуждение). **Побочки sync:** README.md и README_АРХИВ_v8_5.md «карта 28 инструментов» → «33 инструмента»; Tooling §11.5/§12 «не входят в 28» → «33 формализованные позиции». Связано: **PSR_v1 v1.6→v1.7**, **Tooling v1.14→v1.15**. Pravila v1.10 — без изменений. Через `/claude-md-management:claude-md-improver`. + +- **v1.85 от 10.05.2026 (вечер)** — закрытие 15 находок аудита правил использования плагинов и скилов (4 P0 + 7 P1 + 4 P2). Заказчик: «проведи аудит правил использования плагинов и скилов на предмет конфликта и запутанностей» → Claude через `/claude-md-management:claude-md-improver` нашёл 16 находок, представил quality report, получил «все 15» (P0+P1+P2), применил батчем. **P0:** §6 арифметика «33» исправлена (+1 historic PG MCP); Tooling §10.3 шаг 2 sync с §4.1 (3→14 skills); Pravila §13.2 «(15 правил)»→«(16)»; Tooling §13 +v1.13 +v1.14 entries. **P1:** массовый stale-refs дрейф v1.4→v1.6 + v1.12→v1.14 в 7 местах (CLAUDE.md #31/#32/§5п.12, Pravila §11.5/§13.2/§13.9/§13.10). **P2:** Tooling Прил. Н добавлен explicit-слотом уровня 2b (раньше PSR_v1 R0.1 говорил «stack ниже Tooling», но Tooling не было в priority chain ни одного из 4 файлов); PSR_v1 R0.4.A свёрнут до cross-ref на Pravila §12.3 SoT (раньше параллелил список разной формулировкой — риск дрейфа); Pravila §0 +note про §11 override-приоритет (раньше §11 формально стоял ниже §9 в цепочке вопреки фактическому override §2.2/§4.5/§8.4); PSR_v1 R0.6 пронумерован 1–11 для надёжности cross-refs. Связано: **Pravila v1.9→v1.10**, **PSR_v1 v1.5→v1.6**, **Tooling v1.13→v1.14**. Через `/claude-md-management:claude-md-improver`. + +- **v1.84 от 10.05.2026** — закрытие 14 находок аудита нормативной документации (конфликты и запутанности между CLAUDE.md / Pravila / PSR_v1 / Tooling). **#33 claude-md-management** формализован в реестре Tooling §3.3 как 5-й включённый плагин (инфраструктурная категория, off-phase). §5 п.5 свёрнут до 2 строк (была копия PSR_v1 R14 целиком). §5 п.11 — cross-ref на Pravila §12.3 SoT. §1 — scope-метка «общая 7-уровневая файловая иерархия». §6 — счётчик 31→33. Связано: Pravila v1.8→v1.9, PSR_v1 v1.4→v1.5, Tooling v1.12→v1.13. + +- **v1.83 от 10.05.2026** — **формализация двух фактически включённых внешних UI-инструментов + двухуровневое решение по runtime motion-библиотекам.** Триггер: пользователь спросил «хочу добавить стек плагинов 21st, framer motion, UI UX max — проанализируй конфликты». Проверка показала: 21st (MCP `magic`) и UI UX Pro Max (skill) уже включены в `~/.claude.json` и `~/.claude/settings.json` соответственно, но в правилах не описаны (любое использование = нарушение R0.2/R10.4 PSR_v1). Framer Motion — React-only runtime-библиотека, не Claude-плагин, физически не работает в Vue. Через цикл brainstorming → 3 варианта → итерации согласовано: формализовать UPM + 21st; для motion — двухуровневая R15-конструкция (framer-motion hard-запрет + motion-v узкое окно по 4 условиям). **PSR_v1 v1.3 → v1.4** (R6/R6.1 расширены на FD/UPM/21st, R10.1 +21st row, R11.5 + R11.6, R0.6 +3 hard-стопа, R13 +9 строк matrix'а, R14 (новое) pipeline UI-генераторов с R14.4 21st pre-check, R15 (новое) motion-системы — R15.1 framer-motion hard-запрет + R15.2 motion-v 4 условия + R15.3 default стойка + R15.7 расширение на gsap/anime/lottie). **Pravila v1.7 → v1.8** (§13 расширен, §13.10 hard-link на R14: использование UPM/21st вне pipeline'а = нарушение §13). **Tooling Прил. Н v1.11 → v1.12** (#31 UPM + #32 21st как off-phase tools; §9.2 motion-runtime denylist). **CLAUDE.md изменения:** §0 cross-refs обновлены (Pravila v1.8, PSR_v1 v1.4, Tooling v1.12); §2 +Animation default stack; §3.3 +#31 UPM +#32 21st строки; §5 п.5 расширен на расширенный пул UI-инструментов; §5 п.12 motion-runtime новый; §6 обновлён (31 формализованных позиций: 19/29 по фазам + 2 off-phase). Через `/claude-md-management:claude-md-improver`. + +- **v1.82 от 09.05.2026** — Sprint 1 «Hygiene» Phase D: sync метрик schema v8.10 → v8.11 (97 индексов, 38 RLS после Sprint 1 Phase A `e01caa3`: RLS на impersonation_tokens + 2 FK indices) + Histoire 21/28 → 21/43 (фактическое значение из stage1 аудита) + cross-link на детали F–K патчей PSR_v1 в [Plugin_stack_rules_v1.md История версий](docs/Plugin_stack_rules_v1.md#история-версий). Закрывает audit P1-03 (Histoire) + P2-03 (F-K детализация) + post-A метрики. Через `/claude-md-management:claude-md-improver`. +- **v1.81 от 09.05.2026** — Plugin_stack_rules_v1 v1.2 → v1.3 (6 трений второго порядка F–K) + Pravila v1.5 → v1.6 (§13.9 hard-link на R10). **F**: R12 архитектурное → override §4.5 через явный `brainstorming` skill или просьбу «свободно/без вариантов» (Pravila §11.1). **G**: R12 тактическое разделено на «с альтернативами» (A/B/C формат разрешён под user-стиль «а/б») и «без альтернатив» (одна BOLD от FD). **H**: R13 строка про новую UI-фичу разделена — «вне ТЗ И не в Открытые_вопросы» = hard-стоп (Pravila §7), «в рамках MVP-skopa без детализации» = средняя+предположение. **I**: R11.4 «Fallback при технической недоступности уровня» — таблица 6 уровней с маршрутами; недоступность 1–2 = hard-стоп, 3–6 = мягкий fallback. **J**: R10.4 смягчение формулировки + hard-link через Pravila §13.9 (нарушение R10 = нарушение §13). **K**: R0.1 точный scope «головенства» через таблицу priority chain — Stack головной над уровнями 4–6 (settings.json, memory, прочие плагины), не над 0–2 (Pravila §12, Pravila, CLAUDE.md). Через `/claude-md-management:claude-md-improver`. +- **v1.80 от 09.05.2026** — Plugin_stack_rules_v1 v1.1 → v1.2: закрытие 9 проектных перекрытий + принцип-аксиома «stack — головной». **R10** (новое): внешние плагины как инструменты — реестр 11 плагинов с явными ролями (ui-ux-pro-max = резерв-библиотека, claude-md-management = инструмент CLAUDE.md edits, review/security-review/init/simplify = только по явному `/имя`, Boost = служебный слой ниже). **R11** (новое): иерархия 6 источников истины UI/UX — Brandbook → ТЗ+schema → FD → Boost guidelines → ui-ux-pro-max → Vue/Vuetify docs. **R12** (новое): три паттерна дизайн-решений (архитектурное §4.5, тактическое brainstorm/BOLD, стилевое одна идея, тривиальное прямое). **R13** (новое): decision matrix Auto+§12+R0.6 на 14 типов задач × confidence × действие. Только §0 строка PSR_v1; в составе инструментов — без изменений. Через `/claude-md-management:claude-md-improver`. diff --git a/project-files/docs/CHANGELOG_claude_md.template.md b/project-files/docs/CHANGELOG_claude_md.template.md new file mode 100644 index 0000000..8f39020 --- /dev/null +++ b/project-files/docs/CHANGELOG_claude_md.template.md @@ -0,0 +1,163 @@ +# 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 в обратном порядке (свежие сверху). + +--- +*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 `` → 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 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 `` (без 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): `` с 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 (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` — нужна для 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=`, верификация через `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+``. 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` как `
` (passthrough), `VTooltip` как `
`; `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: при 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 `
` чтобы 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` обёрнут вокруг карточек: `` + `