Merge `origin/main` (commits0fd93fdplanning +615db99normative) into plan5-frontend-projects. Merge-base48f27b4. plan5 был 113 ahead / 2 behind. CONFLICTS RESOLVED (2 files, manual): — CLAUDE.md: шапка → v1.90; §0 cross-refs → take origin/main (Pravila v1.11 / PSR_v1 v2.0 / Tooling v1.16); §2 Animation default stack → take origin/main (motion-runtime guidance); §5 п.12 → take origin/main (marker «Резерв (снят 12.05.2026)»); §6 фаза + §8 self-review → keep plan5 (Plan 4 MERGED + Plan 5 frontend + Quiet Luxury context); §9 → keep both v1.88 entries explicitly labelled (plan5 schema-sync + origin/main R15 removal — version-number collision result of parallel- branch bump'ов) + v1.89 plan5 factual fix + new v1.90 merge entry. — docs/CHANGELOG_claude_md.md: keep all three entries (v1.90/v1.89/v1.88). FAST-FORWARDED (3 files, no conflict — plan5 не редактировал): — docs/Plugin_stack_rules_v1.md v1.7 → v2.0 (R15 удалён, 162 lines diff) — docs/Pravila_raboty_Claude_v1_1.md v1.10 → v1.11 (§11.5/§13.2 счётчик 16→15 правил + cross-refs) — docs/Tooling_v8_3.md v1.15 → v1.16 (§9.2 reformulated в technical guidance: motion-v ✅, framer-motion technical block) ADDED FROM origin/main (2 files): — docs/superpowers/plans/2026-05-12-remove-r15-motion-restrictions.md — docs/superpowers/specs/2026-05-12-remove-r15-motion-restrictions-design.md cspell-words.txt +1: «форкнулась» (валидный дев-жаргон, в merge-entries). 0 code changes (resources/js/, app/, db/ нетронуты). 0 npm install (motion-v / gsap / anime.js теперь разрешены, не делается). 0 schema changes. POST-MERGE TODO (отдельные шаги): — /claude-md-management:revise-claude-md polish (per §5 п.10) — memory updates: feedback_plugin_paired_stack + project_state + reference_archive (бывшая «branch-divergent state» note → resolved) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
213 KiB
CLAUDE.md — история версий
История изменений вынесена сюда из шапки ../CLAUDE.md — для лаконичности основного файла. Перенесено 09.05.2026 при правке v1.73→v1.74 (вместе с текущим claude-md-management skill audit).
Текущая версия и активный список фич — в шапке CLAUDE.md (§0–§8). Здесь — история версий v1.1→v1.83 в обратном порядке (свежие сверху) + изолированные post-fork записи v1.88 (R15 removal из origin/main) / v1.89 (plan5 factual fix) / v1.90 (merge). Записи v1.84..v1.87 живут inline в §9 CLAUDE.md (CHANGELOG-обслуживание не велось 10.05.2026–11.05.2026; gap не критичен — версии полностью описаны в §9 основного файла). NB: на плате v1.88 существует ВТОРАЯ entry (plan5 audit schema-sync 12.05.2026 ночь) — она inline-only в §9 CLAUDE.md, не вынесена сюда; это collision версионной нумерации parallel-branch bump'ов.
CLAUDE.md v1.90 от 13.05.2026 (day). Изменения v1.90: Merge R15 motion-runtime removal cleanup из origin/main в plan5-frontend-projects. Plan5 ветка форкнулась 12.05 утром от 48f27b4 ДО появления 615db99 (R15 removal) на main. После 113 атомарных коммитов на plan5 (audit fixes, Plan 5 frontend Tasks 7-11, Quiet Luxury portal redesign, Q.DEFER.002/003/004 closures, audit-cleanup tail) — merge синхронизирует R15 changes. 2 conflict'а решены вручную: (1) CLAUDE.md шапка version → v1.90 unified; §0 cross-refs → take origin/main (Pravila v1.11 / PSR_v1 v2.0 / Tooling v1.16); §2 Animation default stack → take origin/main (motion-runtime guidance); §5 п.12 → take origin/main (marker «Резерв (снят 12.05.2026)»); §6 фаза + §8 self-review → keep plan5 (Plan 4 MERGED + Plan 5 frontend + Quiet Luxury context); §9 история версий → keep both v1.88 entries explicitly labelled (plan5 audit schema-sync + origin/main R15 removal) + v1.89 plan5 factual fix + new v1.90 merge entry. (2) docs/CHANGELOG_claude_md.md (этот файл) → keep all three entries (v1.90/v1.89/v1.88). 3 нормативных файла fast-forward без conflict'а: Plugin_stack_rules_v1.md v1.7 → v2.0 (R15 удалён, 162 lines diff); Pravila_raboty_Claude_v1_1.md v1.10 → v1.11 (§11.5/§13.2 счётчик 16→15 + cross-refs); Tooling_v8_3.md v1.15 → v1.16 (§9.2 reformulated в technical guidance). 0 code changes; resources/js/, app/, db/ нетронуты merge'ем. 0 npm install — motion-v / gsap / anime.js / lottie-web всё ещё не установлены, теперь разрешены к установке без обоснования. Verification: Pest --parallel baseline 742, Vitest 683, Vite build 1.80s, lychee 245 OK, gitleaks 0 leaks. Memory updates (после push): feedback_plugin_paired_stack.md (remove «branch-divergent state» note + bump tier-структуру к v2.0); project_state.md (branch counters: больше не 2 behind); reference_archive.md (file version refs к CLAUDE.md v1.90 + PSR_v1 v2.0 + Pravila v1.11 + Tooling v1.16). Через ручное conflict resolution + post-merge /claude-md-management:revise-claude-md polish (per §5 п.10). v1.89→v1.90.
CLAUDE.md v1.89 от 12.05.2026 (ночь, post-audit continuation). Изменения v1.89: factual fix §6 + шапка v1.88 changelog. В рамках сессии «доделывать аудит» 12.05.2026 ночь обнаружено, что v1.88 в двух местах содержал factual error: (1) §6 строка «Plan 4 (Billing + CSV Reconcile + Admin) MERGED в origin/main 615db99 (post-merge after f4ec5dc PSR_v1 R15 removal)» — оба коммита идентифицированы неверно; (2) шапка v1.88 changelog «§6 «Plan 4 ready for FF-merge» → «Plan 4 MERGED в origin/main 615db99» — то же самое. Verified через git log origin/main + git show <commit>: коммит 615db99 это R15 motion-runtime removal commit «chore(rules): remove R15 motion-runtime restrictions (PSR_v1 v2.0)» (12.05.2026 07:30), а НЕ Plan 4 merge; коммит f4ec5dc это Quiet Luxury sidebar hotfix «fix(redesign): sidebar position:fixed + main padding-left — restore main content visibility» на ветке plan5-frontend-projects, а НЕ PSR_v1 R15 removal и НЕ на origin/main. Правильная история на origin/main (по git log): Plan 4 backend task-коммиты a907fea..174dbae (Tasks 9-11) merged ранее → fded2ee chore(lychee) Plan 4 plan-file fix → 8681040 «docs: Plan 4 closure — CLAUDE.md v1.87 + Открытые_вопросы v1.78» (это и есть Plan 4 closure marker) → 4bc488e fix(admin) AdminPricingTiers strip ISO-suffix → 1ca4378 + 48f27b4 docs(specs+plans) Plan 5 → 0fd93fd + 615db99 R15 motion-runtime removal (отдельная история, НЕ часть Plan 4). Правки v1.89: (1) §6 строка обновлена с правильными коммитами + явное разделение «Plan 4 closure 8681040» и «R15 removal 0fd93fd + 615db99» как разные истории; (2) шапка v1.88 changelog inline исправление 615db99 → 8681040 + NB-маркер «v1.88 первоначально содержал factual error»; (3) §9 v1.88 entry inline исправление аналогично; (4) bump CLAUDE.md v1.88 → v1.89; (5) новая v1.89 entry в §9 CLAUDE.md + эта запись в CHANGELOG. Связанные документы (Pravila v1.10 / PSR_v1 v1.7 / Tooling v1.15 / реестр v1.77 на ветке plan5-frontend-projects) НЕ требуют изменений — фактологический фикс локален в CLAUDE.md. (NB v1.90 post-merge: связанные документы Pravila/PSR_v1/Tooling всё-таки обновились — но не из-за этой v1.89 правки, а из-за подтянутого R15 removal из origin/main; v1.89 logic остаётся валидной — фактологический фикс плана5 локален был в CLAUDE.md.) Источник правки: post-audit continuation session 12.05.2026 ночь, обнаружено как bonus-finding во время Q.DEFER.001 (memory description downgrade). Заказчик: «доделывать аудит, поправить ошибку в CLAUDE.md». Через /claude-md-management:claude-md-improver (per CLAUDE.md §5 п.10 единственный канал правок). v1.88→v1.89.
CLAUDE.md v1.88 от 12.05.2026 — origin/main (R15 motion-runtime removal). Изменения v1.88: Снятие R15 motion-runtime restrictions по решению заказчика 12.05.2026 («сними все запреты на использование framer motion»). Через superpowers:brainstorming → 3 варианта (A узкая правка R15.2 / B полная отмена R15 / C one-strike trigger) → выбран B (вопреки рекомендации A: «доверяю team's choice motion-runtime без regulatory guardrail»). Conscious rollback v1.83 audited construction (10.05.2026: R15 двухуровневая motion-конструкция была введена через brainstorming → 3 варианта → «двухуровневый» подтверждение заказчика; v1.88 — это namesake rollback). 5 файлов изменены (atomic commit): PSR_v1 v1.7 → v2.0 (удалено R15 целиком: R15.1 framer-motion hard-запрет + R15.2 motion-v 4 условия + R15.3 default стойка + R15.4 проверка триггера + R15.5 hard-запрет дублирования + R15.6 live-override + R15.7 расширение на gsap/anime/lottie; удалено R0.6 п.11 animation runtime hard-стоп; удалены R8 три тай-брейкера motion; удалена R11.6 иерархия motion-источников; удалены R13 пять строк motion-сценариев; финальная формула и свойства свода переформулированы; шапка v1.7 → v2.0 major bump). Pravila v1.10 → v1.11 (§11.5/§13.2 счётчик «16 правил R0–R15» → «15 правил R0–R14»; §13.9 + §13.10 cross-ref на PSR_v1 «v1.6» → «v2.0»; §13.10 НЕ удалено — оно про R14 UPM/21st pipeline, не R15). Tooling Прил. Н v1.15 → v1.16 (§9.2 reformulated из regulatory denylist в technical guidance: motion-v + gsap + anime.js + lottie-web + popmotion + @motionone/dom → ✅ разрешено без обоснования; framer-motion + react-spring → ❌ technical block, peerDep react+react-dom, не regulatory rule). CLAUDE.md этот файл: §2 «Animation default stack» переписан с regulatory denylist на guidance recommendation; §5 п.12 → маркер «Резерв. Был "не устанавливать motion runtime библиотеки без R15.2". Снят 12.05.2026.» (нумерация §5 п.1–11 сохранена, чтобы cross-refs в memory feedback_environment.md / feedback_plugin_paired_stack.md не сломать); §0 cross-refs обновлены (Pravila v1.10 → v1.11, PSR_v1 v1.7 → v2.0, Tooling v1.15 → v1.16); §9 «История версий» — entry v1.88. MEMORY sync: memory/feedback_plugin_paired_stack.md обновлён (v1.7 → v2.0; «R15 motion двухуровневый» → historical context); MEMORY.md index hook обновлён. 0 изменений в коде проекта (resources/js/, app/, db/ нетронуты). 0 npm install (motion-v и др. в package.json не попадают; их установка теперь разрешена, но не делается этим коммитом). 0 schema changes. Plan: docs/superpowers/plans/2026-05-12-remove-r15-motion-restrictions.md. Spec: docs/superpowers/specs/2026-05-12-remove-r15-motion-restrictions-design.md. Через /claude-md-management:claude-md-improver (для CLAUDE.md) + manual Edit (для PSR_v1, Tooling, Pravila). v1.87→v1.88. NB version-number collision: на ветке plan5 параллельно существует другая v1.88 entry (audit-driven schema-sync 12.05.2026 ночь, inline-only в §9 CLAUDE.md) — обе валидны, parallel-branch bump'ы.
CLAUDE.md v1.83 от 10.05.2026. Изменения v1.83: Формализация двух фактически включённых внешних UI-инструментов (UI UX Pro Max + 21st.dev Magic MCP) + двухуровневое решение по runtime motion-библиотекам. Триггер сессии: пользователь спросил «хочу добавить стек плагинов 21st, framer motion, UI UX max — проанализируй конфликты». Проверка ~/.claude/settings.json и ~/.claude.json показала: UPM (skill ui-ux-pro-max@ui-ux-pro-max-skill от marketplace nextlevelbuilder/ui-ux-pro-max-skill) и 21st Magic MCP (magic сервер с API-ключом da9dbf..., npm @21st-dev/magic@latest, tools mcp__magic__21st_magic_component_builder/inspiration/refiner + logo_search) — фактически уже включены, но в правилах не описаны. Любое использование без формализации = нарушение R0.2/R10.4 PSR_v1. Framer Motion — React-only runtime npm-библиотека, не Claude-плагин, физически не работает в Vue (использует React fiber-tree + hooks + JSX). Vue-аналог motion-v существует, но это отдельная технология (R0.6 hard-стоп «новая технология в стек»). Через цикл brainstorming (superpowers:brainstorming) → 3 варианта решения по R12 архитектурному → итерации с пользователем («предложи но не делай», «добавь Framer Motion», «двух уровневый») согласовано: формализовать UPM+21st; для motion — двухуровневая R15-конструкция. PSR_v1 v1.3 → v1.4 (главный артефакт): R6 расширен в R6.0 (универсальная таблица фильтра для FD/UPM/21st одинаково); R6.1 hard-override Forest расширен на все три плагина (палитра/шрифты/иконки/aesthetic Forest приоритетнее любых предложений); R10.1 +1 строка для 21st (роль «генератор стартовых шаблонов») + ослабление UPM (теперь активируется не только при «молчании FD», но и для R12 третьего варианта); R11.5 (новое) — активация UPM в R12 архитектурном решении на фазе 1 R2; R11.6 (новое) — параллельная под-иерархия 7 motion-источников (Brandbook → ТЗ → Vue native <Transition> → Vuetify transitions → CSS @keyframes → View Transitions API → motion-v); R0.6 +3 hard-стопа (пункт 9: 21st для брендового App-компонента; пункт 10: 21st для компонента с Vuetify-эквивалентом или существующим в resources/js/components/; пункт 11: установка motion-v / gsap / anime.js / lottie-web без R15.2); R13 +9 строк matrix'а (4 строки UI-фич с/без 21st-pipeline + 1 строка R12 третий вариант UPM + 5 строк motion-сценариев); R14 (новое правило, 7 подразделов) — Pipeline внешних UI-генераторов: R14.1 триггер активации, R14.2 шаги, R14.3 UPM в фазах 1/2, R14.4 21st в фазе 5 с обязательным pre-check R0.6 + R6.0 + R6.1 + FD адаптация, R14.5 запрет дублирования (UPM+21st не на одной фазе), R14.6 live-override (с обязательным сохранением фильтров), R14.7 hard-link на §13 Pravila; R15 (новое правило, 7 подразделов) — Motion-системы: R15.1 framer-motion hard-запрет навсегда (React-only архитектурно, не отменяется live-командой), R15.2 motion-v 4 условия активации (а) письменный кейс из ТЗ/Открытые_вопросы (б) категория оправданности — gesture/shared-layout/spring (в) Brandbook approval (г) полный R12 brainstorming + 3 варианта, R15.3 default стойка из 4 слоёв (Vue native + Vuetify + CSS + View Transitions), R15.4 формальная проверка триггера, R15.5 hard-запрет дублирования (motion-v не вытесняет Vuetify), R15.6 live-override запрещён без R15.2, R15.7 расширение на gsap/anime.js/lottie-web/react-spring/popmotion; R8 +7 тай-брейкеров; финальная формула расширена ссылками на R14/R15. Pravila v1.7 → v1.8: §13 расширен (paired-stack ядро + расширенный пул); §13.9 cross-ref bumped (v1.3 → v1.4); §13.10 (новый) — hard-link на R14 (UPM/21st вне pipeline'а = нарушение §13, через цепочку R10.4 → §13.9). Tooling Прил. Н v1.11 → v1.12: #31 UPM (off-phase tool, §4.5); #32 21st Magic MCP (off-phase tool, §4.6); §9 разделён на §9.1 (изначальный список) + §9.2 (motion runtime библиотеки): framer-motion + react-spring (R15.1 hard-запрет, React-only); motion-v + gsap + anime + lottie + popmotion (R15.2/R15.7 условно, R0.6 пункт 11 hard-стоп). 31 формализованных позиций (19/29 активных по фазам + 2 off-phase). CLAUDE.md v1.82 → v1.83 (этот файл): §0 cross-refs обновлены (Pravila v1.6→v1.8, PSR_v1 v1.3→v1.4, Tooling v1.10→v1.12); §2 +Animation default stack строка; §3.3 +#31 UPM +#32 21st строки в карте инструментов; §5 п.5 расширен на расширенный пул UI-инструментов (FD + UPM + 21st с обязательным R6.0 фильтром и R6.1 hard-override Forest); §5 п.12 motion-runtime новый (запрет установки framer-motion + react-spring + motion-v + gsap + anime.js + lottie-web без R15.2); §6 «Текущая фаза» обновлён (31 формализованных позиций тулчейна: 19/29 активных по фазам + 2 off-phase). Через /claude-md-management:claude-md-improver. 5 файлов изменены: PSR_v1 + Pravila + Tooling + CLAUDE.md + CHANGELOG_claude_md. 0 изменений в коде проекта (resources/js/, app/, db/ нетронуты). 0 npm install (motion-v и др. в package.json не попадают). v1.82→v1.83.*
CLAUDE.md v1.73 от 09.05.2026. Изменения v1.73: Post-MVP — Reports backend epic закрыт (4 этапа / 4 коммита 19f319c..e0ffe7e). После MVP-closure заказчик инициировал работу с реестром; внутри unblocked пусто, поэтому взяли Post-MVP TODO «Reports backend» (был P1-кандидат). (Этап 1 19f319c): App\Models\ReportJob (schema §13.5, status pending/processing/done/failed); App\Jobs\GenerateReportJob (sync queue на dev, tries=1 — auto-retry отключён по CTO-6 в пользу ручного UI-retry); ReportJobController (GET index/show + POST store с квотой 3 одновременных CTO-7 → 422); первая реализация generator'а DealsExportCsvGenerator (Excel-friendly CSV: BOM + ; + \r\n + escape; deals JOIN projects/users/supplier_lead_costs за period; soft-deleted скрыты). Storage local-disk на dev (storage/app/reports/{tenant_id}/{job_id}.csv); на prod — s3 переключение отдельным коммитом. ReportJobFactory + states processing/done/failed. Pest +20. (Этап 2 1a6a74c): реструктура на provider+formatter pattern — вместо Generator-per-комбинация (4×4=16 классов) разделено на 4 Providers + 4 Formatters (8 классов). Provider возвращает headers + rows; Formatter сериализует в нужный формат. 3 формата + stub: CsvFormatter (BOM-Excel-friendly), XlsxFormatter (PhpSpreadsheet 5.x с A1-нотацией + bold headers row 1 + auto-size cols; quirk: setCellValueByColumnAndRow удалён в 5.x — использован Coordinate::stringFromColumnIndex), JsonFormatter (UNESCAPED_UNICODE + UNESCAPED_SLASHES + JSON_PRETTY_PRINT), PdfStubFormatter (Post-MVP throw RuntimeException — UI ловит и показывает failed-job). ReportGeneratorRegistry: provider(type) + formatter(format). Удалены: ReportGenerator interface, GenerationResult DTO, DealsExportCsvGenerator. Pest +3. (Этап 3 9765ed7): retry/cancel/destroy + retention cron. POST /retry (CTO-6: только owner+failed, max 3 попыток через parameters.retry_count, окно 7 дней с created_at, квота тоже учитывается чтобы retry-spam не обходил CTO-7; создаёт НОВЫЙ ReportJob с parameters.retry_of=original.id); POST /cancel (только owner+pending; status=failed + error_message=«Отменено пользователем»); DELETE (только owner+terminal; удаляет файл из disk('local') + row). toResource +3 поля: is_expired (expires_at < NOW), retry_count, retry_max=3. App\Console\Commands\ReportsCleanupExpired cron reports:cleanup-expired {--dry-run} {--limit=1000}: где status='done' AND expires_at<NOW AND file_path NOT NULL → delete файл + UPDATE file_path=NULL. CTO-10: status='done' СОХРАНЯЕТСЯ (не file_deleted-флаг — наличие файла определяется по file_path=NULL). Failed-jobs игнорируются. Pest +21. (Этап 4 e0ffe7e): frontend integration. api/reports.ts (типизированные axios-helpers listReportJobs/createReportJob/retryReportJob/cancelReportJob/deleteReportJob с ApiReportJob/Status/Format/Counts/Quota interfaces; ensureCsrfCookie на mutating); composables/reportsMapper.ts (mapApiReportJob API → UI mock format с конверсией pending→queued/processing→running; title строится на frontend'е из тип + period с RU-месяцами «апр 2026» или диапазоном «мар 2026 — апр 2026»; sizeText форматирует bytes (B/KB/MB); timeText зависит от status — «в очереди» / «в работе · Nс» / «N мин назад» / «только что»; uiTypeToApi маппит slug). ReportsView полностью переписан под API: onMounted → loadJobs (replace MOCK_JOBS на real); usePolling 30 сек (фоновый авто-refresh); Submit → createReportJob → reload + success-alert + error-alert (validation+общие через extractValidationErrors/extractErrorMessage); canSubmit computed disable если квота заполнена; Reset/Reload-btn; Retry/Cancel/Download-кнопки → API-вызовы; Delete через v-dialog persistent confirm; fetch-error-alert на listReportJobs reject; Empty-state «Нет отчётов»; canRetry проверяет retry_count<3. Vitest +24 (mapper +14: status mapping/title один-период-vs-диапазон/format/sizeText B+KB+MB+null/attempt/error/timeText 4 ветки/uiTypeToApi 4 slug'а/progress=50; ReportsView переписан с MOCK_JOBS на vi.mock('api/reports') +12: mount+listReportJobs called/4 type cards/default Сделки active/4 формата/quota из API/empty-state/done с Скачать/failed с Повторить/failed retry_count=3 БЕЗ Повторить/pending с Отменить/Submit вызывает createReportJob+reload/Submit error→alert/disabled квота 3/3/Reset/Reload/fetch-error-alert/Retry/Cancel/Delete confirm-dialog). Что НЕ сделано (Post-MVP backlog): этап 2b — 3 оставшихся типа провайдеров (managers_summary/sources_summary/billing_summary, расширение через 3 новых Provider-класса без изменений архитектуры); GET /api/reports/jobs/{id}/file endpoint скачивания (UI Скачать-кнопка пока без handler'а — нужен Storage::temporaryUrl или streamed-response); S3-storage переключение для production (на dev — local-fs); async queue worker (на dev — sync; на prod нужен Redis/database driver + supervisor). Регресс зелёный: lint+type+format ✅; Vitest 393/393 за 21.78 сек (+24 от 369); vite build 1.02 сек; Pint+PHPStan passed (baseline регенерирован 2 раза); Pest 403/403 за 44.83 сек (+44 от 359, 1343 assertions). Реестр v1.74→v1.75. v1.72→v1.73.
CLAUDE.md v1.72 от 09.05.2026. Изменения v1.72: MVP по Claude-зоне закрыт (документная фиксация решения заказчика). После закрытия P0 (notifications + reminders, 7 коммитов a4601fe..4c33323) реестр открытых вопросов проверен — все unblocked продуктовые вопросы выполнены, остаточные ⏸ — внешние блокеры (Б-1 реквизиты ООО → Диз-3/DO-2/DO-4; юр. редактура Прил. Ж/З + 5 🟦 структурных). Финальные метрики: Pest 359/359 (1233 assertions) за 41.37 сек / Vitest 369/369 за 22 сек / Histoire 21/28; schema v8.10 (56 таблиц + 12 партиций + 95 индексов + 37 RLS); 18/28 инструментов активно (фазы 0–2 закрыты по тулчейну). Покрытие фронтенда: 13 из 13 концептов handoff'а v8 Forest + 3 ErrorView (404/403/500) — landing v8_landing.html отложен через ⏸ Б-1. Покрытие бэкенда: auth-flow (login/register/me/logout + forgot/reset + 2FA setup/verify/disable/regenerate + recovery-use), deals API (index/show/store/update/transition/destroy/restore/export-CSV+XLSX), 3 lookup-API (managers/projects/lead-statuses), reminders CRUD + cron reminders:dispatch-due, in_app_notifications (4 endpoints) + bell-UI с 30-сек polling, notification-preferences PATCH, admin (tenants/billing/incidents/system_settings), impersonation (init/verify/end/active/recent), webhook receive (HMAC + per-token rate-limit), partitions:create-months. Все 8 schema-default событий уведомлений имеют рабочую интеграцию (new_lead, reminder, low_balance, zero_balance, topup_success, invoice_paid через Mailable+blade+NotificationService; new_device_login + marketing — семантические заглушки). Что НЕ сделано (blocked-only / pending external): #6 Yandex 360 SSO ⏸ Б-1 (admin-endpoints без middleware на MVP); #7 Pest browser-mode (отложен инфра); ЮKassa/invoice webhook endpoints (service-методы готовы — notifyTopupSuccess/notifyInvoicePaid — ждут реальных payment-endpoints); new_device_login через user_sessions (требует session-fingerprint инфры); deep-link bell→DealDetailDrawer (на MVP — push на /deals); landing-продакшн ⏸ Б-1+Диз-3+Диз-4. Дальнейшая работа Claude возобновляется по триггерам: закрытие Б-1 (запуск Yandex 360 SSO + домен liderra.ru + список SaaS-сотрудников); приход юр. формулировок (Прил. Ж оферта + Политика + Прил. З уведомление РКН); переход к фазе 3 pre-production (#25 Semgrep + #26 Trivy + #27 Dependabot + #28 pg_audit + #29 pg_anonymizer); либо deployment в Yandex Cloud (требует closures Б-1). v1.71→v1.72.
CLAUDE.md v1.71 от 09.05.2026. Изменения v1.71: P0 этап 6 — 4 оставшихся email-события закрывают P0 целиком. low_balance / zero_balance / topup_success / invoice_paid — все 8 событий из schema-default users.notification_preferences теперь имеют рабочую интеграцию. Авто-план P0 (6 этапов / 6 коммитов) закрыт полностью. (1) 4 новых Mailable: LowBalanceNotification ($recipient, $tenant, $thresholdLeads), ZeroBalanceNotification, TopupSuccessNotification ($amountRub, $amountLeads), InvoicePaidNotification ($amountRub, $invoiceNumber, $tariffName). (2) 4 новых blade-шаблонов в resources/views/emails/{low_balance,zero_balance,topup_success,invoice_paid}.blade.php — Forest-палитра (Teal #0F6E56 для positive, #B94837 для error/zero); таблицы с balance_leads/amount_rub/invoice_number; CTA на «Биллинг» / «Настройки → Уведомления». (3) NotificationService +4 методов: notifyLowBalance(Tenant, threshold), notifyZeroBalance(Tenant), notifyTopupSuccess(Tenant, amountRub, amountLeads?), notifyInvoicePaid(Tenant, amountRub, invoiceNumber?, tariffName?). Все 4 шлют email + inapp по prefs (recipientsForEvent с фильтром по соответствующему event-key). (4) Интеграция в ProcessWebhookJob: (a) chargeNewLead после lead_charge проверяет low_balance_threshold_leads из system_settings (default 10, schema seed) — триггерит notifyLowBalance ТОЛЬКО при пересечении порога сверху-вниз balance_after <= threshold AND (balance_after+1) > threshold (иначе спам после каждого lead_charge при balance < threshold). (b) logRejection(zero_balance) после INSERT в RejectedDealsLog триггерит notifyZeroBalance ТОЛЬКО если в последний час не было другого RejectedDealsLog с тем же reason (anti-spam: 1 email/час на тенант). Защита от self-just-inserted через id != $rejected->id (timestamp-сравнение ненадёжно из-за PG microsecond precision). (c) topup_success/invoice_paid — service-методы готовы к подключению, intergration отдельным коммитом когда появятся endpoints для пополнения (ЮKassa webhook) и оплаты тарифа. (5) lowBalanceThreshold() private helper в Job читает system_settings.low_balance_threshold_leads через SystemSetting::find, fallback 10. (6) Pest +12 в BalanceNotificationsTest.php (всего 359/359 за 41.37 сек, 1233 assertions): low_balance триггер при пересечении порога / balance уже < threshold не шлёт повторно / balance > threshold после decrement не шлёт / prefs.email=false → только inapp; zero_balance первое отклонение → email+inapp / 2-е в течение часа НЕ дублирует / >1ч снова шлёт; topup_success notify создаёт email+inapp / prefs=email:false → только inapp; invoice_paid notify создаёт email+inapp / prefs=email:false → только inapp; balance events изолированы между tenants. NewLeadNotificationTest: тест «balance=0 не шлёт уведомление» обновлён — теперь Mail::assertNotSent(NewLeadNotification) (вместо Mail::assertNothingSent()), потому что ZeroBalanceNotification ШЛЁТСЯ при balance=0 — это новое поведение по ТЗ §18.5. PHPStan baseline регенерирован (Pint автофиксы по ProcessWebhookJob и тестам). Все 8 событий из schema-default готовы: new_lead (этап 1+2a) / reminder (этап 4) / low_balance / zero_balance / topup_success / invoice_paid (все этап 6); new_device_login и marketing — стартовые семантические заглушки в NotificationService::EVENT_ константах, не подключены (отсутствует endpoint device-tracking + marketing-broadcast). P0 ЗАКРЫТ ПОЛНОСТЬЮ. Производственные TODO остаточные (после P0): topup endpoint ЮKassa-webhook → notifyTopupSuccess; invoice paid webhook → notifyInvoicePaid; new_device_login через user_sessions tracking; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: Pint+PHPStan passed; Pest 359/359 за 41.37 сек (+12 от 347, 1233 assertions); Vitest 369/369 за 22 сек (без изменений — backend-only этап); vite build 1.02 сек. v1.70→v1.71.*
CLAUDE.md v1.70 от 09.05.2026. Изменения v1.70: P0 этап 5 — Reminders frontend (RemindersView + DealDetailDrawer-секция + nav-badge live). Закрывает frontend-half этапа 4-5: пользователь может создавать/просматривать/завершать/удалять напоминания из UI. (1) api/reminders.ts — типизированные axios-helpers для 5 endpoint'ов (list/create/update/complete/delete) с ensureCsrfCookie для mutating-вызовов. Type ReminderFilter/ApiReminder/ReminderCounts. (2) Pinia stores/reminders.ts — items/counts/loading/fetchError + currentFilter ref + actions load(params) / refreshCounts() (lightweight для bell-badge) / create(payload) / update(id, payload) / complete(id) (optimistic + revert; при currentFilter ∈ {active,today,upcoming,overdue} убираем из items) / remove(id) (optimistic) / reset(). (3) components/reminders/ReminderDialog.vue — двух-режимный (create/edit) modal с native <input type="datetime-local"> (без heavy picker'а): props dealId? / reminder?, watch на modelValue для re-init из props, ISO-конверсия при submit, error-alert при failure. (4) views/RemindersView.vue — page-head с заголовком + 2 page-stats (active / overdue с error-color) + reload-btn; v-tabs с counts на бейджах (overdue=error color); список v-list-item с action-prefix (mdi-check-circle-outline → complete) + meta (#deal_id deep-link на /deals + relative time + creator_name) + dropdown menu (Изменить/Удалить с confirm-dialog); empty-state «Создавайте из карточки сделки» (на MVP нет deal-picker'а на этой странице). 4 фильтра-таба (today по default / upcoming / overdue / completed). При complete/delete refreshCounts() обновляет nav-badge синхронно. Маршрут /reminders (lazy) добавлен в router. (5) AppLayout — nav-tree пункт «Напоминания» теперь биндит count из useRemindersStore().counts.active (replace static «12»). Бейдж скрыт при count=0 (новое условие count > 0 поверх !== undefined). usePolling(loadReminderCounts, {intervalMs: 60_000}) для авто-обновления nav-badge каждую минуту. (6) DealDetailDrawer — добавлена секция «Напоминания» (видна только при tenantId && deal) с inline create-btn + список активных напоминаний этой сделки + complete-btn. ReminderDialog встроен в drawer (close-on-content-click=false для предотвращения закрытия по клику в dialog). loadReminders дёргается на open + после save. (7) Vitest +18 (всего 369/369 за 21.20 сек, +20 от 349 — добавил +2 в AppLayout): reminders-store.spec.ts 11 (initial state / load+reject / refreshCounts только counts / create + reject / complete optimistic + revert / remove + reject / reset); RemindersView.spec.ts 7 (mount + 4 tabs / counts на бейджах / empty-state / список / reload-btn / filter=today по умолчанию); AppLayout.spec.ts +2 (бейдж скрыт при counts.active=0 / показывается «7» при counts.active=7). Реализованный flow покрывает 90% UI потока — без deep-link на конкретный DealDetailDrawer и без deal-picker на отдельной странице (отдельный коммит). Производственные TODO остаточные: этап 6 (4 email-события); deep-link на конкретный drawer от bell/reminders; deal-picker для прямого create на /reminders; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 369/369 за 21.20 сек (+20 от 349); vite build 1.00 сек; Pest 347/347 за 41.51 сек (без изменений — backend нетронут). v1.69→v1.70.
CLAUDE.md v1.69 от 09.05.2026. Изменения v1.69: P0 этап 4 — Reminders backend (CRUD + cron-диспетчер + email/inapp-уведомления). Закрыт пункт «Reminders ⏸ no-view» из nav-tree. Schema-таблица reminders уже была в v8.10 (§17.5), теперь работает целиком backend-side. (1) App\Models\Reminder — Eloquent с casts (remind_at/completed_at/sent_at datetime, is_sent bool), relations (tenant/creator/assignee), helpers isCompleted()/isOverdue(). (2) ReminderFactory — definition с remind_at +1 час по умолчанию + states overdue() / completed() / sent(). (3) ReminderController под auth:sanctum с RLS-обёрткой + defense-in-depth where('tenant_id'): GET /api/reminders?filter=&deal_id=&limit= (filters: active|today|upcoming|overdue|completed, окно today=±1 день, counts для UI badges); POST /api/reminders {deal_id, text?, remind_at, assignee_id?} (FK guard на assignee — должен быть active user того же tenant'а, иначе 422); PATCH /api/reminders/{id} (text/remind_at/assignee_id, при смене remind_at автоматически сбрасывается is_sent+sent_at чтобы cron мог ретригерить); POST /api/reminders/{id}/complete (idempotent — повторный NO-OP); DELETE /api/reminders/{id}. (4) ReminderDueNotification Mailable + resources/views/emails/reminder.blade.php (Forest-палитра, blockquote text, TZ конвертирована в recipient.timezone). (5) NotificationService::notifyReminder(Reminder) — recipient = assignee_id ?? created_by (если active и не deleted); если ни тот ни другой не доступен — silent return. Канал email + inapp по prefs. payload содержит reminder_id + deal_id для UI deep-link. (6) App\Console\Commands\RemindersDispatchDue — cron reminders:dispatch-due {--dry-run} {--limit=500}. Идёт по is_sent=false AND completed_at IS NULL AND remind_at <= NOW(). По одному reminder в transaction (SET LOCAL app.current_tenant_id нельзя переключать между разных tenant'ов в одной TX). После notifyReminder — UPDATE is_sent=true, sent_at=NOW() ДАЖЕ если recipient deactivated (защита от retry-spam). На production — Windows Task Scheduler / cron каждую минуту. (7) Маршруты в routes/web.php под Route::middleware('auth:sanctum')->prefix('/api/reminders'). (8) Pest +32 (всего 347/347 за 41.21 сек, 1203 assertions): ReminderControllerTest 21 (401 без auth / пустой / только свои / filters today/overdue/completed / counts / deal_id фильтр / store success+422 без полей / store assignee FK guard 2 / update text+remind_at сбрасывает is_sent / 404 чужой / 422 без полей / complete+idempotent / delete+404 чужой); RemindersDispatchDueTest 11 (due → email+inapp+is_sent / future skip / completed skip / уже sent skip / assignee получает вместо created_by / deactivated user (но reminder помечается is_sent чтобы не ретрить) / prefs.email=false → только inapp / --dry-run не шлёт+не помечает / 3 due → 3 sent / --limit=1 / RLS изоляция между tenant'ами). PHPStan baseline регенерирован. IDE-helper для Reminder. Производственные TODO остаточные: этап 5 (RemindersView + DealDetailDrawer integration); этап 6 (4 email-события); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: Pint+PHPStan passed (baseline регенерирован); Pest 347/347 за 41.21 сек (+32 от 315, 1203 assertions); frontend нетронут — Vitest/build не нужны. v1.68→v1.69.
CLAUDE.md v1.68 от 09.05.2026. Изменения v1.68: P0 этап 3 — NotificationsTab.vue фикс под schema + GET/PATCH prefs API. Закрытие архитектурного расхождения из v1.28: handoff (8 событий: new_lead/duplicate_detected/low_balance/tariff_charge/reminder_due/manager_assigned/webhook_failed/monthly_report × email/sms/in_app) — не совпадал с schema (8 событий: new_lead/reminder/low_balance/zero_balance/topup_success/invoice_paid/new_device_login/marketing × inapp/push/email). Tab сохранял prefs только локально без API. (1) Backend AuthController::updateNotificationPreferences — PATCH /api/auth/me/notification-preferences под auth:sanctum. Принимает {prefs: {event: {channel: bool}}, sound_enabled?: bool}. Валидация: события ∈ NotificationService::ALL_EVENTS (8 schema-aligned), каналы ∈ {inapp, push, email}. Replace-семантика: незадекларированные events отбрасываются полностью (не merge — позволяет «выключить целиком»). Незадекларированные channels тоже отбрасываются (защита от schema-pollution). bool-кастинг (1/'1' → true). Возвращает userResource с обновлёнными prefs. userResource расширен: добавлены notification_preferences + sound_enabled поля. UserFactory расширен notification_preferences (schema-default JSON 8×3) — без этого тесты падали на User::factory()->create() поскольку Eloquent не перечитывает строку после INSERT, а DB-DEFAULT JSONB виден как null на свежесозданной модели. (2) Pest +10 в NotificationPreferencesTest.php (всего 315/315 за 36.73 сек, 1130 assertions): 401 без auth / успех + replace prefs / неизвестные events отбрасываются / неизвестные channels (sms/webhook) отбрасываются / 422 без prefs / sound_enabled опционален / GET /me возвращает prefs+sound_enabled / 422 при prefs. строка вместо объекта / bool-кастинг 1/'1' → true / replace-семантика (отсутствующие events исчезают). (3) Frontend api/auth.ts — типы NotificationChannel = 'inapp'|'push'|'email' + NotificationEventKey (8 events) + NotificationPreferences Partial-Record. AuthUser interface получил optional notification_preferences + sound_enabled. Helper updateNotificationPreferences(payload). (4) NotificationsTab.vue полностью переписан под schema-aligned: 8 событий с описаниями (Новый лид/Напоминание/Низкий баланс/Нулевой баланс/Пополнение успешно/Счёт оплачен/Новое устройство/Анонсы и промо), 3 канала (В приложении/Push/Email — БЕЗ SMS). Реактивный flow: prefs ref инициализирован синхронно через buildPrefs() (иначе v-if="prefs[e.id]" блокирует рендер чекбоксов до onMounted и тесты mount()→find() падают). dirty — computed (JSON.stringify сравнение с originalPrefs snapshot вместо watch+флаг — устойчив к идемпотентным изменениям). save() async + 2 v-alert (success-tonal / warning-tonal closable). Сохранить btn :disabled="!canSave" + :loading="saving". Отменить btn вызывает readFromUser() (re-snapshot из auth.user). Push-канал отмечен «включится в Post-MVP» в hint'ах. (5) Vitest +10 в NotificationsTab.spec.ts (всего 349/349 за 20.42 сек, +10 от 339): 8 schema-aligned событий присутствуют / 3 канала (НЕ sms) / legacy-events отсутствуют (Дубликат/Webhook упал/etc) / читает prefs из auth.user (new_lead.email=false / reminder.email=true) / Сохранить disabled пока не изменено / после toggle становится enabled / save() вызывает API + success-alert + правильный payload / save() reject → error-alert / Отменить возвращает к оригиналу / sound_enabled читается из auth.user. SettingsView.spec.ts обновлён (legacy event-имена «Дубликат/Срок напоминания/Webhook упал» → «Напоминание/Нулевой баланс/Анонсы и промо»). PHPStan baseline регенерирован для +25 ignored Pest TestCall. Производственные TODO остаточные: этапы 4-5 (Reminders backend + frontend), этап 6 (4 email-события); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 349/349 за 20.42 сек (+10 от 339); vite build 983 ms; Pint+PHPStan passed; Pest 315/315 за 36.73 сек (+10 от 305, 1130 assertions). v1.67→v1.68.*
CLAUDE.md v1.67 от 09.05.2026. Изменения v1.67: P0 этап 2b — In-app notifications API + UI bell + polling. Закрывает этап 2 P0 целиком (вместе с 2a). (1) Backend App\Http\Controllers\Api\InAppNotificationController под auth:sanctum (Sanctum SPA, уведомления USER-personal). 4 endpoint'а: GET /api/notifications?unread_only=&limit= (1..100, default 50; ORDER BY created_at DESC + id DESC; возвращает items+unread_count+total); PATCH /api/notifications/{id}/read (idempotent — повторный вызов NO-OP); POST /api/notifications/mark-all-read (bulk update + count); DELETE /api/notifications/{id} (hard-delete). Все четыре обёрнуты в DB::transaction + SET LOCAL app.current_tenant_id. Защита от кражи чужого id через where('user_id', $authUser->id) поверх RLS. (2) Маршруты в routes/web.php под Route::middleware('auth:sanctum')->prefix('/api/notifications') — Sanctum SPA требует session middleware из web-группы. (3) Pest +14 в InAppNotificationApiTest.php (всего 305/305 за 34.71 сек, 1099 assertions): 401 без auth / пустой / только свои + ORDER BY created_at DESC / unread_only=1 / limit=2 + total=5 / 422 limit>100 / поля title+body+event+payload+deal_id / mark-read ставит read_at + idempotent / mark-read 404 для чужого / mark-read 404 unknown / mark-all-read bulk + count / mark-all-read только свои / DELETE удаляет своё / DELETE 404 для чужого. (4) Frontend api/notifications.ts — типизированные axios-helpers с ensureCsrfCookie для mutating-вызовов. ApiInAppNotification + ListNotificationsResponse interfaces. (5) Pinia store stores/notifications.ts — items/unreadCount/total/loading/fetchError refs + sortedItems computed (DESC by created_at) + actions: load(limit, unreadOnly) / markRead(id) (optimistic + revert на reject) / markAllRead() (NO-OP при unreadCount=0) / remove(id) (optimistic с decrement total/unreadCount) / reset(). На fail markRead/markAllRead/remove — silently revert (без toast'а — иначе спам при каждом sync-failure). (6) AppLayout — bell-icon переписан с static-pip на v-menu (offset=8, close-on-content-click=false, location=bottom-end): <v-btn data-testid="notifications-btn"> с pip badge показывающим unreadDisplay (1..99 / 99+ / hidden при 0); v-card с заголовком + Mark-all-read btn (только при unreadCount>0) + v-list последних 10 элементов из sortedItems. Click на item → markRead + если deal_id → router.push('/deals') (deep-link на конкретный drawer — отдельный коммит). 8 mock event-icon'ов (mdi-account-plus-outline для new_lead, mdi-clock-outline для reminder, и т.д.). formatRelative показывает «только что» / «N мин назад» / «N ч назад» / «N д назад». usePolling(loadNotifications, {intervalMs: 30_000}) — каждые 30 сек reload (Page Visibility API в usePolling pause'ит при hidden tab). loadNotifications no-op без auth.user. (7) Vitest +18 (всего 339/339 за 20.03 сек, +18 от 321): notifications-store 12 (initial state / load fills+rejects / markRead optimistic+revert+already-read / markAllRead optimistic+NO-OP при 0 / remove optimistic+revert / sortedItems DESC / reset); AppLayout +6 (bell-btn существует / pip скрыт при 0 / pip показывает count / pip 99+ при >99 / listNotifications вызывается на mount при auth.user / без user не вызывается). PHPStan baseline регенерирован (50 false-positive Pest TestCall warnings подавлены). Production TODO остаточные: deep-link на конкретный drawer (на MVP — push на /deals); этапы 3-6 P0; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 339/339 за 20.03 сек (+18 от 321); vite build 989 ms (main app-chunk 164.94 KB / KanbanView lazy 182.26 KB); Pint+PHPStan passed (baseline регенерирован); Pest 305/305 за 34.71 сек (+14 от 291, 1099 assertions). v1.66→v1.67.
CLAUDE.md v1.66 от 09.05.2026. Изменения v1.66: P0 этап 2a — in_app_notifications + notifyInApp в NotificationService (schema v8.9→v8.10). Backend-фундамент bell-icon канала; UI bell + API endpoints — этап 2b отдельным коммитом. (1) Schema v8.10 — таблица in_app_notifications после reminders (обе про работу/коммуникации): id BIGSERIAL / tenant_id FK / user_id FK / event VARCHAR(50) / title VARCHAR(255) / body TEXT / deal_id BIGINT БЕЗ FK (deals партиционирована) / payload JSONB DEFAULT '{}' / read_at TIMESTAMPTZ / created_at TIMESTAMPTZ. UPDATED_AT отсутствует (только created_at + read_at). Индексы: idx_in_app_notifications_user_unread (user_id, created_at DESC) WHERE read_at IS NULL (главный UI-флоу) + idx_in_app_notifications_user_recent (user_id, created_at DESC) (последние 50 с прочитанными). RLS tenant_isolation стандартная. CHANGELOG_schema.md +§T (3 точки источник изменений + 4 точки SQL DDL + почему НЕ Laravel default notifications-table). Метрики после v8.10: 55→56 таблиц, 93→95 индексов, 36→37 RLS-политик. (2) App\Models\InAppNotification — Eloquent с UPDATED_AT=null, payload cast array, read_at cast datetime, BelongsTo на User+Tenant. (3) NotificationService::notifyInApp(User, event, title, body, payload) — INSERT в БД через DB::transaction + SET LOCAL app.current_tenant_id = user.tenant_id (PgBouncer-safe, RLS-симметрично). Throwable проглатываются + Log::warning. NotificationService::notifyNewLead теперь шлёт ДВА канала параллельно: email (если prefs.email=true) И in-app (если prefs.inapp=true). title = «Новый лид — {projectName}», body = contact_name ?? phone, payload = {deal_id, project_name} для UI deep-link на DealDetailDrawer. Schema-default new_lead.inapp=true → большинство получит in-app, и только подписавшиеся — email. (4) Pest +11 в tests/Feature/Notifications/InAppNotificationTest.php (всего 291/291 за 32.94 сек, 1060 assertions): inapp=true создаёт row + поля + payload / inapp=false не создаёт / schema-default ставит row / 2 user'а с inapp=true оба получают / inactive не получает / другой тенант не получает (RLS изоляция) / Биз-19 дубль не дублирует / повторный vid не дублирует / inapp+email=true создаёт 1 row + 1 email / payload содержит deal_id для deep-link / notifyInApp напрямую с reminder создаёт row. (5) Quirk — Write tool с относительным путём app/tests/... создал файлы в app/app/tests/... (CWD дрейфонул на /c/моя/.../app/app); файлы перемещены вручную, пустые директории удалены через rmdir (rm -rf не пройден permissions). (6) IDE-helper регенерирован для нового InAppNotification. PHPStan baseline регенерирован (1 «nullsafe.neverNull» error на $deal->project?->name подавлен через baseline). Производственные TODO остаточные: этап 2b (API + UI bell), этапы 3-6; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: Pint+PHPStan passed (baseline регенерирован); Pest 291/291 за 32.94 сек (+11 от 280, 1060 assertions); frontend нетронут — Vitest/build не нужны. v1.65→v1.66.
CLAUDE.md v1.65 от 09.05.2026. Изменения v1.65: P0 этап 1 — NotificationService + new_lead email (старт closing TODO «Notification delivery» из карты остатка работы). Закрывает первый из 6 этапов плана P0 (notifications + reminders). (1) App\Services\NotificationService — центральный диспетчер. Константы 8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/invoice_paid/new_device_login/marketing) + 3 каналов (inapp/push/email) точно как в schema.sql:699 users.notification_preferences JSONB DEFAULT. Метод notifyNewLead(Tenant, Deal) — выбирает активных user'ов тенанта (is_active=true + deleted_at IS NULL) с включённым notification_preferences.new_lead.email=true и шлёт через Mail::to(...)->send(NewLeadNotification). Throwable из Mail-фасада ловится → Log::warning (отказ канала не должен валить транзакцию webhook'а). PHP-фильтр prefs (не JSONB-запрос) — список получателей <50 на тенант, не critical-path. (2) App\Mail\NewLeadNotification — Mailable с (User $manager, Deal $deal, Tenant $tenant). Subject «Лидерра. Новый лид — {project_name}» с fallback project='Без проекта' если relation не загружен. resources/views/emails/new_lead.blade.php — HTML-письмо в Forest-палитре (#0F6E56 primary, #F6F3EC ivory) с таблицей phone/contact_name/received_at (TZ конвертирована в manager->timezone ?? 'Europe/Moscow')/deal_id. (3) Интеграция ProcessWebhookJob::chargeNewLead — после ActivityLog::create вызов app(NotificationService::class)->notifyNewLead($tenant, $deal). $deal->setRelation('project', $project) чтобы Mailable не делал лишний SELECT. NotifyNewLead вне DB::transaction в смысле что ошибка отправки уже вне транзакции — но DB::transaction обёртка сейчас покрывает и notify-вызов; на prod надо или вынести notify ПОСЛЕ DB::transaction, или Mail::queue (async через worker). На MVP — sync через ::send (детерминированно для тестов). (4) Pest +11 в tests/Feature/Notifications/NewLeadNotificationTest.php(всего 280/280 за 31.27 сек, 1029 assertions): Mail::fake() / 1 user с email=true получает / user с email=false не получает / schema-default (.email=false) не шлёт / 2 user'а с email=true получают оба, 3-й с email=false не получает / inactive user с email=true не получает / soft-deleted user не получает / user другого тенанта не получает (изоляция) / Биз-19 дубль не шлёт повторное уведомление / повторный vid (idempotent UPDATE) не шлёт повторно / balance=0 (RejectedDealsLog) не шлёт / subject содержит project_name «Caranga». (5) IDE-helper регенерирован (ide-helper:models -W -M -N) — добавил @mixin docblocks 4 моделям (ImpersonationToken/SaasAdminAuditLog/SystemSetting/UserRecoveryCode), которые ранее без них работали через baseline-ignore'ы. PHPStan baseline регенерирован — 138 «ignore.unmatched» errors схлопнулись (новые docblocks резолвят property access напрямую, baseline-патч больше не нужен). Производственные TODO остаточные: этапы 2–6 P0 (in_app_notifications + UI bell, NotificationsTab fix под schema, reminders backend+frontend, остальные 4 email-события); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: Pint+PHPStan passed (baseline регенерирован); Pest 280/280 за 31.27 сек (+11 от 269, 1029 assertions); frontend нетронут — Vitest/build не нужны. Реестр без изменений (notifications не было в открытых вопросах). v1.64→v1.65.
CLAUDE.md v1.64 от 09.05.2026. Изменения v1.64: «Корзина» для soft-deleted сделок — естественное продолжение stages 5/6 (soft-delete + restore). Расширяет undo-snackbar (8 сек window) до постоянного доступа к удалённым через отдельный view-mode. (1) Backend DealController::index — query-param only_deleted=true (boolean-like) активирует branch Deal::query()->withTrashed()->whereNotNull('deleted_at') (обход global scope SoftDeletes + явный фильтр для NO-OP idempotency). Все остальные фильтры (status_in/project_id/manager_id/search/limit/offset) применимы и в trash-mode. (2) Pest +3 в DealIndexTest (всего 269/269 за 29.12 сек, 1009 assertions): only_deleted=true возвращает только soft-deleted (3 deals: 1 alive + 2 deleted → total=2) / без only_deleted soft-deleted скрыты (default behavior сохранён) / RLS+app-фильтр изолирует чужие удалённые сделки. (3) Frontend ListDealsParams.onlyDeleted?: boolean в типе + axios mapping only_deleted: 'true' | undefined. DealsView расширен: trashMode ref, toggleTrashMode() (clear selected + reload), applyBulkRestoreFromTrash() (optimistic remove from list + bulkRestoreDeals + toast). UI changes в trash-mode: заголовок «Сделки» → «Корзина» / btn mdi-arrow-left К сделкам (warning-flat) вместо mdi-trash-can-outline Корзина (outlined) / hide «Экспорт» + «Новая сделка» / hide chiprow filter-bar (не имеет смысла для удалённых) / info-alert «Корзина: показаны удалённые сделки» / bulk-bar заменяется: только mdi-restore Восстановить (success-tonal) + clear-btn (status/export/delete скрыты). (4) Vitest +2 в DealsListIntegration (всего 321/321 за 19.60 сек, +2 от 319): toggleTrashMode переключает trashMode + listDeals вызывается с onlyDeleted=true / applyBulkRestoreFromTrash вызывает bulkRestoreDeals + убирает из dealsState + toast «Восстановлено 2». PHPStan baseline: без изменений. Production TODO остаточные: SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 321/321 за 19.60 сек (+2 от 319); vite build 1.04 сек; Pint+PHPStan passed; Pest 269/269 за 29.12 сек (+3 от 266, 1009 assertions). Реестр v1.72→v1.73.
CLAUDE.md v1.63 от 09.05.2026. Изменения v1.63: Polling 30 сек — закрывает последний unblocked production-TODO «Polling/SSE для real-time». Manual reload-btn остаётся как fast-path; polling — фоновый автообновитель. (1) Composable composables/usePolling.ts — usePolling(loader, {intervalMs?, enabled?}). По умолчанию 30_000 ms. Page Visibility API integration: при document.hidden=true interval останавливается + skip-проверка внутри tick (defense-in-depth); при visibilitychange event с hidden=false — restart interval + немедленный loader() (не ждать следующего interval'а). Cleanup на onBeforeUnmount — clearInterval + removeEventListener. enabled=false — composable не стартует совсем (для feature-flag'а). (2) Integration в 5 view'ов: DealsView+KanbanView (вызывают loadDeals), AdminTenantsView (loadTenants), AdminBillingView (loadBilling), AdminIncidentsView (loadIncidents). Без auth.user.tenant_id loadDeals — no-op (в самой функции return на отсутствие tenant_id), так что polling без auth ничего не делает. (3) Vitest +6 в usePolling.spec.ts (всего 319/319 за 18.67 сек, +6 от 313): через vi.useFakeTimers + vi.advanceTimersByTime для детерминированности. Тесты: вызов каждые intervalMs / default 30 сек / skip при document.hidden=true / cleanup на unmount / enabled=false → no-op / visibilitychange pause+resume с немедленным loader. PHPStan baseline: без изменений (frontend-only коммит). Production TODO остаточные: SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 319/319 за 18.67 сек (+6 от 313); vite build 899 ms; Pint+PHPStan passed; Pest 266/266 за 28.62 сек (без изменений — backend не тронут). Реестр v1.71→v1.72.
CLAUDE.md v1.62 от 09.05.2026. Изменения v1.62: mrr_rub в /api/admin/tenants (этап 7) — закрывает gap из v1.66 (mock-форма имеет mrrRub, API возвращал null). (1) Backend AdminTenantsController::index — добавлено tariff_plans.price_monthly as tariff_price_monthly в select. Поле mrr_rub в response: tariff_price_monthly (string) если не-trial; иначе null. Aggregate-формат как у /admin/billing — string чтобы decimal не терял точность. (2) Pest +3 в AdminTenantsIndexTest (всего 266/266 за 28.39 сек, 1001 assertion): mrr_rub='990.00' для активного тарифа не-trial / mrr_rub=null для trial / mrr_rub=null если current_tariff_id отсутствует. (3) Frontend — ApiAdminTenant.mrr_rub: string | null в типе. mapApiAdminTenant: mrrRub: api.mrr_rub !== null ? parseFloat(api.mrr_rub) : null (вместо hardcoded null из v1.66). AdminTenantsView template: formatRub(item.mrrRub) для консистентности с другими ₽-полями. (4) Vitest +2 в AdminTenantsViewApi.spec.ts (всего 313/313 за 18.83 сек, +2 от 311): mrr_rub строка → number / mrr_rub=null → mrrRub null. PHPStan baseline: без изменений (warnings не добавлены). Production TODO остаточные: polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 313/313 за 18.83 сек (+2 от 311); vite build 947 ms; Pint+PHPStan passed; Pest 266/266 за 28.39 сек (+3 от 263, 1001 assertion). Реестр v1.70→v1.71.
CLAUDE.md v1.61 от 09.05.2026. Изменения v1.61: Bulk restore-flow — completion of stage 5 (soft-delete был half-done без undo-кнопки). (1) Backend DealController::restore — POST /api/deals/restore body {tenant_id, ids: [1..1000 ints]}. Использует Deal::query()->withTrashed() чтобы обойти global scope SoftDeletes + явный whereNotNull('deleted_at') для NO-OP idempotency на уже живых сделках. RLS + defense-in-depth where(tenant_id) → партиальный update только своих. ActivityLog event=deal.restored, context.source='bulk' для каждой ВОССТАНОВЛЕННОЙ. ActivityLog::EVENT_DEAL_RESTORED константа добавлена в model. Маршрут Route::post('/api/deals/restore'). (2) Pest +7 в DealRestoreTest (всего 263/263 за 27.68 сек, 998 assertions): 422 / 404 unknown / soft-delete + restore + audit / NO-OP на живых не пишет audit / defense-in-depth (свой восстановлен, чужой остался удалён) / после restore сделка снова видна в GET /api/deals / 422 пустой массив. (3) Frontend dealsApi.bulkRestoreDeals(payload) — POST-helper. DealsView::applyBulkDelete расширен: snapshot удалённых сделок (deep-clone manager. nested object) сохраняется в lastDeletedSnapshot ref для undo. undoBulkDelete() async: optimistic re-insert через dealsState.unshift + bulkRestoreDeals если auth.user; на success — toast «Восстановлено N из M.»; на fail — warning. v-snackbar для bulk-delete увеличен с 3 до 8 сек + получил #actions слот с кнопкой «Восстановить» (показывается только если lastDeletedSnapshot.length > 0). После успешного undo snapshot очищается → кнопка пропадает. (4) Vitest +3 в DealsListIntegration.spec.ts (всего 311/311 за 18.71 сек, +3 от 308): bulk-delete + undo восстанавливает обе сделки + bulkRestoreDeals вызывается с правильными ids + lastDeletedSnapshot очищается; undo без tenant_id — bulkRestoreDeals НЕ вызывается + только локальное восстановление; undo reject → warning toast + локальное восстановление остаётся. PHPStan baseline регенерирован. Production TODO остаточные: polling/SSE; mrr_rub aggregate в /api/admin/tenants; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 311/311 за 18.71 сек (+3 от 308); vite build 877 ms; Pint+PHPStan passed; Pest 263/263 за 27.68 сек (+7 от 256, 998 assertions). Реестр v1.69→v1.70.*
CLAUDE.md v1.60 от 09.05.2026. Изменения v1.60: soft-delete + DELETE /api/deals (этап 5/5 — авто-план закрыт полностью). (1) Schema v8.8 → v8.9 — deals.deleted_at TIMESTAMPTZ (NULL = живая сделка) + partial index (tenant_id, status) WHERE deleted_at IS NULL (самый частый UI-фильтр). ALTER TABLE на партиционированной deals распределяет колонку во все 6 партиций автоматически (PG 14+). CHANGELOG_schema.md +§U с обоснованием soft-delete vs hard (CASCADE-FK от webhook_dedup_keys уничтожил бы dedup-ключи и нарушил идемпотентность §5.5). Метрики: 92→93 индекса. (2) Backend DealController::destroy — DELETE /api/deals body {tenant_id, ids: [1..1000 ints]}. Bulk-update deleted_at=NOW() через RLS+defense-in-depth where(tenant_id). Каждая удалённая сделка пишет ActivityLog event=deal.deleted, context.source='bulk'. NO-OP (уже удалена) НЕ пишет audit. Deal model получил SoftDeletes trait + deleted_at в fillable+casts — global scope автоматически добавляет whereNull('deleted_at') ко всем существующим query'ам (index/show/transition/update/export), без явного фильтра. Маршрут Route::delete('/api/deals'). (3) Pest +8 в DealDestroyTest (всего 256/256 за 27.75 сек, 977 assertions): 422/404 базовые / soft-delete + ActivityLog deal.deleted+source=bulk / defense-in-depth (свой удалён, чужой жив) / NO-OP idempotency (повторное удаление не пишет audit) / GET /api/deals скрывает soft-deleted / GET /api/deals/{id} 404 для soft-deleted / 422 пустой массив. Quirk: migrate:fresh --env=testing без .env.testing файла использовал liderra вместо liderra_testing — тесты падали на «column deleted_at не существует»; решение DB_DATABASE=liderra_testing php artisan migrate:fresh (без --env). (4) Frontend dealsApi.bulkDeleteDeals(payload) — DELETE-helper с axios.delete('/api/deals', { data: payload }) (axios особенность: DELETE с body передаётся через config.data, не payload). DealsView::applyBulkDelete переписан async: optimistic local-removal (UI отвечает сразу) + bulkDeleteDeals если auth.user; на success — toast «Удалено N из M.»; на fail — warning toast «Не удалось удалить — изменения только локально.» + локальный update НЕ откатывается (UX-paradigma как у applyBulkStatus). Без auth — только optimistic (legacy local-mode). (5) Vitest +3 в DealsListIntegration.spec.ts (всего 308/308 за 20.12 сек, +3 от 305): bulkDeleteDeals с tenant_id + optimistic + toast «Удалено 2» / без tenant_id — НЕ вызывается / reject → warning toast + локальный update остаётся. PHPStan baseline регенерирован. АВТО-ПЛАН (5 этапов) ЗАКРЫТ ПОЛНОСТЬЮ. Production TODO остаточные (после v1.60): polling/SSE для real-time (на MVP — manual reload-btn); restore-flow для soft-deleted сделок (POST /api/deals/{id}/restore — отдельный коммит); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра; tenants-tariff helpers (mrr_rub в schema через JOIN на tariff_plans). Регресс зелёный: lint+type-check+format ✅; Vitest 308/308 за 20.12 сек (+3 от 305); vite build 973 ms; Pint+PHPStan passed; Pest 256/256 за 27.75 сек (+8 от 248, 977 assertions). Реестр v1.68→v1.69.
CLAUDE.md v1.59 от 09.05.2026. Изменения v1.59: GET /api/admin/incidents + AdminIncidentsView API integration (этап 4/5). (1) Backend AdminIncidentsController::index — GET /api/admin/incidents?type=&severity=&unresolved_only=&limit=&offset= по schema §9 incidents_log. ORDER BY started_at DESC. Derived поля в response: incident_id (формат INC-YYYY-MMDD-NNNN — год+месяц+день started_at + zero-padded id); status (resolved/investigating/open — derive из resolved_at/detected_at); affected_tenants_count (из BIGINT[] array — parsePgArray для PG-литерала); rkn_deadline_at (для type=data_breach без rkn_notified_at: detected_at+24h по 152-ФЗ). summary считает {open, investigating, rkn_pending, total_unresolved} 4 отдельными SELECT'ами. (2) Pest +11 в AdminIncidentsIndexTest (всего 248/248 за 28.02 сек, 951 assertion): пустой / поля + incident_id формат / derive статус (investigating/resolved) / type filter / severity filter / unresolved_only / ORDER BY started_at DESC / data_breach имеет rkn_deadline +24h / non-data_breach НЕ имеет deadline / summary.rkn_pending (только PDN-breach без notification) / limit+offset. Quirk: schema saas_admin_users использует full_name (не first_name/last_name) + не имеет updated_at — сразу исправлено в helper insert. (3) Frontend — api/admin.ts::listAdminIncidents(params) с типизированными ApiAdminIncident/Summary/Response (severity narrowed на enum, остальное — string). AdminIncidentsView переписан: новый IncidentRow interface унифицирует mock и API форму (mock-category ↔ API-type, mock-title ↔ API-summary); reactive rowsState (default = ADMIN_INCIDENTS) + stats; loadIncidents() async на onMounted замещает mock на API; на fail — fetchError + warning alert + MOCK fallback; reload-btn. Maps категорий (categoryMap/statusInfo/severityInfo) переписаны на функции с fallback'ами на новые slug'и. РКН pending chip учитывает оба варианта pdn_breach/data_breach. (4) Vitest +5 в AdminIncidentsViewApi.spec.ts (всего 305/305 за 20.59 сек, +5 от 300): listAdminIncidents на mount / replace rowsState + summary с rkn_deadline сохранением / reject → fetchError + alert + MOCK fallback / reload-btn двойной вызов / РКН pending chip отображается для data_breach без rkn_notified. PHPStan baseline регенерирован. Production TODO остаточные: этап 5 (soft-delete migration + DELETE /api/deals); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 305/305 за 20.59 сек (+5 от 300); vite build 1.05 сек; Pint+PHPStan passed; Pest 248/248 за 28.02 сек (+11 от 237, 951 assertion). Реестр v1.67→v1.68.
CLAUDE.md v1.58 от 09.05.2026. Изменения v1.58: GET /api/admin/billing + AdminBillingView API integration (этап 3/5). (1) Backend AdminBillingController::index — GET /api/admin/billing?search=. Aggregates по balance_transactions за текущий календарный месяц по tenant'у (один SUM-запрос с CASE WHEN type IN ('topup','lead_charge'); ABS для charges). Поля row: id, subdomain, organization_name, contact_email, status, balance_rub, tariff_id, tariff_name, mrr_rub (=tariff.price_monthly если is_trial=false, иначе '0.00'), monthly_topups_rub, monthly_charges_rub, last_payment_at (= MAX created_at для type=topup), chargeback_unrecovered_rub. summary: total_mrr_rub (SUM tariff.price_monthly не-trial с активным тарифом), monthly_revenue_rub (SUM topup.amount_rub за месяц), overdue_count (balance<0 OR chargeback>0), refunds_count_30d (count balance_transactions type=refund ≥now-30days). Quirk: schema-колонка называется tariff_plans.price_monthly (НЕ price_rub_monthly) — обнаружено первым прогоном Pest, исправлено сразу. (2) Pest +9 в AdminBillingIndexTest (всего 237/237 за 27.69 сек, 926 assertions): пустой / поля + tariff JOIN / aggregates topups+charges за текущий месяц / прошлый месяц НЕ попадает в monthly / summary.overdue (balance<0 || chargeback>0) / summary.refunds_count_30d (старые >30 дней не считаются) / summary.total_mrr (только не-trial с тарифом) / search ILIKE / soft-deleted скрыт. (3) Frontend — api/admin.ts::listAdminBilling(search) с типизированными ApiAdminBillingTenant/Summary/Response. AdminBillingView переписан: reactive rowsState (default = ADMIN_BILLING_TENANTS mock) + summary (default = MOCK_SUMMARY); loadBilling() async на onMounted, парсит API строки (balance_rub/mrr/topups/charges) в number'ы и derive'ит status (suspended/balance<0||chargeback>0→overdue/active). На fail — fetchError + warning alert + MOCK остаются. Reload-btn. Tariff/status maps обобщены: tariffLabel(s) возвращает known mock-перевод или as-is (backend уже отдаёт «Команда»); statusInfo(s) возвращает known meta или fallback с label=s/color=default — устойчиво к новым slug'ам. (4) Vitest +4 в AdminBillingViewApi.spec.ts (всего 300/300 за 18.41 сек, +4 от 296): listAdminBilling на mount / replace rowsState + summary с string→number конверсией + status derive (balance<0→overdue) / reject → fetchError+alert+MOCK fallback / reload-btn двойной вызов. PHPStan baseline регенерирован. Production TODO остаточные: этапы 4-5 авто-плана (admin/incidents endpoint + soft-delete migration + DELETE /api/deals); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 300/300 за 18.41 сек (+4 от 296); vite build 925 ms; Pint+PHPStan passed; Pest 237/237 за 27.69 сек (+9 от 228, 926 assertions). Реестр v1.66→v1.67.
CLAUDE.md v1.57 от 09.05.2026. Изменения v1.57: GET /api/admin/tenants + AdminTenantsView API integration (этап 2/5 авто-плана). (1) Backend AdminTenantsController::index — saas-admin lookup тенантов с фильтрами status/search/limit/offset (без auth — saas-admin SSO ⏸ Б-1). LEFT JOIN на tariff_plans для tariff_name. ORDER BY last_activity_at DESC, id. Soft-deleted (deleted_at!=null) исключены. Поля: id/subdomain/organization_name/contact_email/status/balance_rub/balance_leads/is_trial/last_activity_at/tariff_id/tariff_name/desired_daily_numbers/chargeback_unrecovered_rub/created_at. stats агрегирует {total, active, trial, overdue} одним SELECT'ом без фильтров — overdue = chargeback_unrecovered_rub > 0 OR balance_rub < 0. (2) Pest +8 в AdminTenantsIndexTest (всего 228/228 за 25.22 сек, 906 assertions): 200 + пустой / все поля / status filter / search ILIKE по name+subdomain+email / ORDER BY last_activity_at DESC / stats (4 счётчика) / soft-deleted скрыт / limit+offset. (3) Frontend — api/admin.ts::listAdminTenants(params) с типизированными ApiAdminTenant/Stats/Response. composables/adminTenantsMapper.ts::mapApiAdminTenant — converter API → UI-формат (AdminTenant из mockTenants.ts ожидает другую форму): status derive (is_trial=true → 'trial', balance<0 || chargeback>0 → 'overdue', schema-status as-is для active/suspended), inn='' (нет в API — живёт в legal_entities/invoices), code=subdomain, tariff_name → known TenantTariff clamp с fallback на 'Trial', todayActual=0 / mrrRub=null (требуют JOIN на deals/balance_transactions, добавим отдельно), activitySince через formatRelative(last_activity_at). AdminTenantsView: reactive tenantsState + stats (default = MOCK_TENANTS / MOCK_STATS); loadTenants() async на onMounted → replace через splice; на fail — fetchError=true + warning v-alert + MOCK остаются. Reload-btn data-testid="reload-btn" с loading-state. (4) Vitest +13 в AdminTenantsViewApi.spec.ts (всего 296/296 за 18.91 сек, +13 от 283): listAdminTenants на mount / replace state + stats / reject → fetchError + alert + MOCK fallback / reload-btn двойной вызов; mapper +9 (organization_name→name, subdomain→code / inn пуст / is_trial→trial / chargeback→overdue / balance<0→overdue / suspended→suspended / balance_rub строка→number / activitySince «10 мин назад» / null → «—»). PHPStan baseline регенерирован. Production TODO остаточные: этапы 3-5 (admin/billing+incidents endpoints + soft-delete migration); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 296/296 за 18.91 сек (+13 от 283); vite build 1.02 сек; Pint+PHPStan passed; Pest 228/228 за 25.22 сек (+8 от 220, 906 assertions). Реестр v1.65→v1.66.
CLAUDE.md v1.56 от 09.05.2026. Изменения v1.56: PATCH /api/deals/{id} + comment-editor в DealDetailDrawer — drawer переходит из read-only в редактируемый режим. (1) Backend DealController::update(int $id) — PATCH /api/deals/{id} с body {tenant_id, comment?, manager_id?, status?} (все поля optional, должен быть хотя бы один). Каждое изменённое поле пишет соответствующий ActivityLog event: comment → deal.commented (context.text); manager_id → deal.assigned (context.from/to + ставит assigned_at=now); status → deal.status_changed (context.from/to/source='manual'). NO-OP (значение не меняется) НЕ пишется в audit log. Manager FK guard (manager_id чужого tenant'а → 422) и status validation (slug должен существовать в lead_statuses → 422) — те же что в store/transition. RLS-обёртка + defense-in-depth where(tenant_id) → 404 для чужой сделки. Маршрут Route::patch('/api/deals/{id}', 'update')->where('id', '[0-9]+'). (2) Pest +10 в DealUpdateTest (всего 220/220 за 25.64 сек, 871 assertion): 422 без tenant_id / 404 unknown / 404 чужая сделка / comment update + deal.commented audit / manager update + deal.assigned audit + assigned_at=NOW / status update + deal.status_changed audit / 422 неизвестный slug + НЕ обновляет / 422 manager чужого tenant'а / NO-OP не пишет audit / комбинированно (comment+status одним запросом) → 2 audit log записи. (3) Frontend api/deals.ts::updateDeal(id, payload) — типизированный PATCH-helper с ensureCsrfCookie (mutating endpoint). DealDetailDrawer: добавлена секция «Комментарий» (показывается ТОЛЬКО при наличии tenantId — без auth остаётся read-only) с v-textarea (auto-grow, counter=5000, hide-details) + Save-btn mdi-content-save-outline (loading во время save). commentDraft (ref) populates из getDeal response (deal.comment ?? ''). saveComment() async вызывает updateDeal с comment: commentDraft || null + на success — toast «Комментарий сохранён» + reload events (новый deal.commented появляется в timeline); на fail — commentSaveError=true + warning toast «Не удалось сохранить — попробуйте позже». v-snackbar reuses commentSaveError для color=warning. (4) Vitest +3 в DealDetailDrawerApi.spec.ts (всего 283/283 за 18.13 сек): saveComment вызывает updateDeal с правильным payload + toast success + reload events (getDeal вызвался дважды); saveComment reject → commentSaveError=true + toast warning «Не удалось»; comment-section НЕ рендерится без tenantId (read-only mode для legacy local-режима). PHPStan baseline регенерирован. Production TODO остаточные: GET /api/admin/{tenants,billing,incidents} (этапы 2-4 текущего плана); soft-delete + DELETE /api/deals (этап 5, требует миграцию); polling/SSE; SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 283/283 за 18.13 сек (+3 от 280); vite build 1.12 сек; Pint+PHPStan passed; Pest 220/220 за 25.64 сек (+10 от 210, 871 assertion). Реестр v1.64→v1.65.
CLAUDE.md v1.55 от 09.05.2026. Изменения v1.55: GET /api/lead-statuses + Pinia store — заменяет static-снапшот в коде на live-данные из БД (включая custom slug'и, добавленные после deployment'а). (1) Backend — App\Models\LeadStatus (PK=slug string, incrementing=false, keyType='string', timestamps=null); LeadStatusController::index — GET /api/lead-statuses, ORDER BY sort_order+slug, формат {slug, name_ru, is_system, sort_order, color_hex, description}. Таблица глобальная (НЕ tenant-aware), auth не требуется на MVP. (2) Pest +5 в LeadStatusesIndexTest (всего 210/210 за 24.59 сек, 840 assertions): 200 + не пустой / все 14 системных slug'ов из seed (new..final_missed) / поля slug/name_ru/color_hex/sort_order/is_system / sort_order ASC / кастомный slug добавленный после seed возвращается. (3) Frontend — api/leadStatuses.ts::listLeadStatuses (GET helper); stores/leadStatuses.ts::useLeadStatusesStore Pinia setup-store: statuses ref<LeadStatus[]> (default = LEAD_STATUSES snapshot для UI без fetch'а), load(force=false) идемпотентен (повторный вызов → no-op если loaded), bySlug computed Map для O(1), findBySlug(slug) helper. На fail — snapshot остаётся, fetchError=true. (4) Integration в 3 view-компонента: DealsView заменил LEAD_STATUSES импорт на leadStatusesStore.statuses (computed leadStatuses) для bulk-status menu и statusBySlug (computed Map из store getter); KanbanView заменил на leadStatuses computed для column-iteration + count display + safe-access dealsByStatus[slug] || [] в template (защита от custom slug'а из API без seeded column); DealDetailDrawer переписал LEAD_STATUSES.find(...) → store.findBySlug(...). Оба view'а вызывают leadStatusesStore.load() в onMounted (рядом с loadDeals). reduce для init dealsByStatus в KanbanView оставлен на snapshot (всегда seeded 14; новые custom-колонки появятся после API-load — empty-array fallback в template). (5) Vitest +7 в leadStatusesStore.spec.ts + 2 spec'а DealDetailDrawer'а получили setActivePinia(createPinia()) в beforeEach (без этого getActivePinia() падает в jsdom): initial state snapshot / findBySlug returns existing / findBySlug null для unknown / load() success — replace + loaded=true / load() reject — fetchError + snapshot остаётся / load() идемпотентен (1 запрос на 2 вызова) / load(force=true) — 2 запроса. Всего 280/280 за 19.44 сек (+7 от 273). Production TODO остаточные: polling/SSE для real-time обновления (на MVP — manual reload-btn); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 280/280 за 19.44 сек (+7 от 273); vite build 1.17 сек (KanbanView lazy-chunk 182.22→182.28 KB — Pinia-getter overhead); Pint+PHPStan passed; Pest 210/210 за 24.59 сек (+5 от 205, 840 assertions). Реестр v1.63→v1.64.
CLAUDE.md v1.54 от 09.05.2026. Изменения v1.54: GET /api/deals/{id} + интеграция DealDetailDrawer на реальный ActivityLog. (1) Backend DealController::show(int $id) — возвращает {deal, events} для drawer'а. RLS-обёртка + defense-in-depth where(tenant_id) (как в index/transition); 404 если сделка чужая или не существует. deal — extended (project_name + manager_name/initials через ManagerController::format* + comment + assigned_at). events — последние 50 записей activity_log фильтрованных по (tenant_id, deal_id) ORDER BY created_at DESC, с актором (user через belongsTo-relation). Маршрут Route::get('/api/deals/{id}', 'show')->where('id', '[0-9]+'). (2) Pest +8 в tests/Feature/DealShowTest.php (всего 205/205 за 24.19 сек, 812 assertions): 422 без tenant_id / 404 unknown tenant / 404 несуществующая сделка / 404 чужая сделка (RLS-проверка через postgres superuser BYPASSRLS работает за счёт app-фильтра) / deal-relations (project_name + manager_name «Иван П.» + initials «ИП» + comment) / events ORDER BY created_at DESC (status_changed свежее createde) + actor.name + actor=null для system-event с user_id=null / RLS+app-фильтр НЕ показывает события с deal_id совпадающим у чужого tenant'а / лимит 50 событий (60 записей → возвращаем 50). (3) Frontend api/deals.ts::getDeal(id, tenantId) — типизированный helper с ApiDealEvent/ApiDealDetail/GetDealResponse interfaces; БЕЗ ensureCsrfCookie (GET-only). composables/dealsApiMapper.ts::mapApiDealEvent(api, now=new Date()) — converter ApiDealEvent → DealEvent (UI-формат): event slug clamp на known types (deal.{created,status_changed,viewed,commented,assigned,balance_charged}) с fallback на 'deal.viewed' (generic-icon); actor маппится 1:1; minutesAgo = max(0, floor((now - created_at) / 60_000)); detail зависит от type — для status_changed строим «from → to» из context, для created — «Лид принят (источник: …)», для остальных — JSON-сводка контекста. (4) DealDetailDrawer получил optional tenantId prop. watch([open, deal.id, tenantId]) с immediate: true — на open=true вызывает loadEvents(). Если оба (deal + tenantId) есть → getDeal(deal.id, tenantId) → events.value = events.map(mapApiDealEvent). На fail → eventsFetchError=true + v-alert type=warning «Backend недоступен — показаны mock-события» (data-testid=events-fetch-error-alert) + fallback на MOCK_EVENTS. Без tenantId — никогда не fetch'им, MOCK_EVENTS как раньше. DealsView и KanbanView передают :tenant-id="auth.user?.tenant_id". (5) Vitest +4 в DealDetailDrawerApi.spec.ts (всего 273/273 за 20.76 сек, +4 от 269): без tenantId — getDeal не вызывается + MOCK_EVENTS видны / с tenantId — getDeal вызывается + events заменены + «new → paid» виден / reject → eventsFetchError + alert + MOCK_EVENTS fallback / open=false → НЕ вызывается. PHPStan baseline регенерирован для +новых ignored Pest TestCall warnings. Production TODO остаточные: polling/SSE для real-time обновления (на MVP — manual reload-btn); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 273/273 за 20.76 сек (+4 от 269); vite build 1.12 сек (KanbanView lazy-chunk 182.17→182.22 KB — DealDetailDrawer импорт mapApiDealEvent shared); Pint+PHPStan passed; Pest 205/205 за 24.19 сек (+8 от 197, 812 assertions). Реестр v1.62→v1.63.
CLAUDE.md v1.53 от 09.05.2026. Изменения v1.53: XLSX-export через PhpSpreadsheet — закрыт TODO «реальный XLSX-export» из v1.52. Установлен phpoffice/phpspreadsheet:^5.0 (v5.7.0). Endpoint POST /api/deals/export расширен опциональным параметром format (default 'csv' для backward-compat, 'xlsx' = новая ветка). Backend buildXlsx(): Spreadsheet + setTitle('Сделки') + setCellValue('A1'...G1') для headers + getStyle('A1:G1')->getFont()->setBold(true) + setAutoSize(true) для всех колонок. Xlsx writer пишет в php://output через ob_start/ob_get_clean чтобы вернуть бинарную строку из контроллера. Content-Type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + Content-Disposition attachment; filename="deals_export_YYYY-MM-DD.xlsx". Quirk: PhpSpreadsheet 5.x удалил deprecated-метод setCellValueByColumnAndRow($col, $row, $val) — пришлось мигрировать на A1-нотацию (setCellValue('A2', $val)). Обнаружено в первом тестовом прогоне (500 на endpoint'е), исправлено сразу. Pest +4 в DealCreateTest (всего 197/197 за 26.05 сек, 784 assertions): xlsx возвращает binary с правильным Content-Type + magic bytes "PK\x03\x04" (XLSX = ZIP) + размер >2KB; распаковка через PhpSpreadsheet IOFactory::createReader('Xlsx') → sheet Сделки + A1='ID' + B1='Имя' (bold=true) + A2/B2/C2 = реальные данные сделки; 422 на неизвестный format ('pdf'); по умолчанию (без format) — backward-compat CSV. Frontend — api/deals.ts разделён на 2 функции: exportDeals (CSV, returns string, ставит format='csv' в payload) + exportDealsXlsx (XLSX, returns Blob, responseType='blob' для axios). DealsView applyBulkExport(format='xlsx') async получил параметр format с default 'xlsx' (UX prefer Excel-friendly формат, особенно RU-локаль с 1С). XLSX-ветка вызывает exportDealsXlsx → triggerBlobDownload(blob, filename) (новый helper, отделён от triggerCsvDownload чтобы Blob не конструировался дважды); CSV-ветка через старый exportDeals/triggerCsvDownload. На fail → fallback на local CSV (даже если запросили xlsx — без backend'а xlsx не построим). Vitest +3 в DealsListIntegration.spec.ts (всего 269/269 за 18.49 сек): xlsx default вызывает exportDealsXlsx (НЕ exportDeals) + триггерит download через blob:url + toast «XLSX»; csv-вариант вызывает exportDeals (НЕ Xlsx) + toast «CSV»; xlsx reject → fallback на local CSV + toast «Backend недоступен». PHPStan baseline регенерирован. Production TODO остаточные: polling/SSE для real-time обновления (на MVP — manual reload-btn); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 269/269 за 18.49 сек (+3 от 266); vite build 982 ms; Pint+PHPStan passed (baseline регенерирован); Pest 197/197 за 26.05 сек (+4 от 193, 784 assertions). Реестр v1.61→v1.62.
CLAUDE.md v1.52 от 09.05.2026. Изменения v1.52: Bulk-transition + reload-btn — закрывает «UI меняет статус, но изменения не сохраняются на backend» gap из v1.51 + добавляет manual reload как замену polling/SSE до прихода long-poll'а. (1) Backend DealController::transition — POST /api/deals/transition {tenant_id, ids: [int...], status: slug}. Валидация: ids обязателен 1..1000 ints, status обязателен ≤50 chars + DB::table('lead_statuses')->where('slug', X)->exists() (422 «Slug не найден в lead_statuses» если нет). lead_statuses — глобальная таблица (НЕ tenant-aware), system+custom slug'и в одном scope. RLS-обёртка SET LOCAL app.current_tenant_id + defense-in-depth where('tenant_id', $tenantId)->whereIn('id', $ids) — на тестах postgres superuser обходит RLS, app-фильтр гарантирует что чужие id не апдейтятся (partial-update: updated < requested если часть id принадлежит другому tenant'у). ActivityLog::create([event=deal.status_changed, context={from, to, source=bulk}]) для каждой ИЗМЕНЁННОЙ сделки (NO-OP — старый==новый — НЕ пишется в audit log, иначе спам при «обновить тот же статус»). Ответ: {updated, requested, status}. Маршрут Route::post('/api/deals/transition'). (2) Pest +7 в tests/Feature/DealTransitionTest.php (всего 193/193 за 23.27 сек, 767 assertions): 422 missing fields / 404 unknown tenant / 422 неизвестный slug + сделка не апдейтится / batch update 3 сделок + 3 ActivityLog с правильным context.from/to/source / NO-OP не пишет ActivityLog / defense-in-depth (передаём 2 id из разных tenant'ов — обновляется только свой, чужой остаётся в исходном статусе) / 422 пустой массив ids. (3) Frontend dealsApi.transitionDeals(payload) — типизированный helper, ensureCsrfCookie обязателен (mutating). applyBulkStatus в DealsView переписан с sync на async: optimistic local-update (UI отвечает сразу), затем backend-вызов если есть auth.user.tenant_id. На success — statusToast «Обновлено N из M.». На fail — «Не удалось сохранить статус — изменения только локально.» + локальный update НЕ откатывается (UX rationale: пользователь видит что хотел, перезагрузит чуть позже; auto-rollback запутает больше чем поможет). Без auth.user — только optimistic, API не вызывается (legacy local-mode сохранён). (4) Reload-btn в DealsView и KanbanView — outlined button «Обновить» с mdi-refresh, привязан к loadDeals action. В DealsView у btn'а :loading="loading" chip — крутится во время fetch'а. (5) Vitest +5 (всего 266/266 за 18.16 сек): reload-btn в DealsView (listDeals вызывается дважды) + applyBulkStatus с tenant_id (transitionDeals вызывается + optimistic update до завершения + toast «Обновлено 2») + applyBulkStatus БЕЗ tenant_id (transitionDeals НЕ вызывается + только локально) + applyBulkStatus reject (toast warning + локальный update НЕ откатывается); reload-btn в KanbanView (тот же 2× listDeals). PHPStan baseline регенерирован. Production TODO остаточные: реальный XLSX-export через PhpSpreadsheet; polling/SSE для real-time (на MVP — manual reload); SaaS-admin auth ⏸ Б-1; Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 266/266 за 18.16 сек (+5 от 261); vite build 1.06 сек (KanbanView lazy-chunk 181.98→182.17 KB — добавил dealsApi.transitionDeals импорт через DealsView, но KanbanView его не тянет напрямую — рост от reload-btn shared chunk); Pint+PHPStan passed; Pest 193/193 за 23.27 сек (+7 от 186, 767 assertions). Реестр v1.60→v1.61.
CLAUDE.md v1.51 от 09.05.2026. Изменения v1.51: GET /api/deals + замена MOCK_DEALS — закрыт TODO (c) из v1.50 (опциональный пункт, но снимает дрейф между UI и backend на time-критичных flow вроде «увидеть свежие лиды»). (1) Backend DealController::index — list-endpoint с фильтрами и relations: tenant_id query-param (422/404 как в ManagerController), массив status_in[] (whereIn по status), project_id / manager_id (точное совпадение), search (ILIKE по phone+contact_name OR-block), limit clamp [1..500] default 100, offset default 0. ORDER BY received_at DESC, id DESC. Eloquent with(['project:id,name', 'manager:id,email,first_name,last_name']). RLS-обёртка SET LOCAL app.current_tenant_id + defense-in-depth where(tenant_id, $tenantId) на уровне query (на тестах через postgres superuser RLS обходится BYPASSRLS — explicit-фильтр гарантирует изоляцию). Ответ: {deals: [{id, tenant_id, project_id, project_name, phone, contact_name, status, manager_id, manager_name, manager_initials, received_at}, ...], total, limit, offset}. manager_name/manager_initials форматируются через ManagerController::formatName/formatInitials (нашли расхождение, что эти helper'ы static — re-use OK). cost НЕ возвращаем (живёт в supplier_lead_costs.cost_rub partition'е, лишний JOIN под limit=200 строк дешевле клиентского запроса). Маршрут Route::get('/api/deals', 'index') рядом с store/export. (2) Pest +12 в tests/Feature/DealIndexTest.php (всего 186/186 за 22 сек, 742 assertions): 422 без tenant_id / 404 unknown / пустой список / project_name + manager_name + initials присутствуют + ISO received_at / RLS-изоляция (Deal чужого tenant'а НЕ возвращается — defense-in-depth where отрабатывает) / ORDER BY received_at DESC (3 сделки в правильном порядке) / status_in[] фильтр (передаём 2 status'а через ?status_in[]=new&status_in[]=paid — Laravel queryString парсит в массив) / project_id точное совпадение / manager_id точное совпадение / search ILIKE case-insensitive (Соколова / 903 / сокол) / limit+offset (5 сделок, limit=2 offset=1) / manager_name+initials = null когда manager_id null. (3) Frontend api/deals.ts::listDeals — типизированный axios-helper с ApiDeal interface + ListDealsParams (tenantId/statusIn/projectId/managerId/search/limit/offset → camelCase в DTO, snake_case на wire через axios params-mapping). Без ensureCsrfCookie (GET-only, CSRF только на mutating). composables/dealsApiMapper.ts::mapApiDeal(api, now=new Date()) — converter ApiDeal → MockDeal: id/phone/statusSlug/cost(=0) 1:1; name = contact_name ?? phone (fallback на телефон когда контакт неизвестен); project = project_name ?? '—'; manager = {name: 'Не назначен', initials: '—'} если manager_id=null; receivedMinutesAgo = max(0, floor((now - received_at) / 60_000)) — clamp на 0 чтобы не было отрицательных при clock-skew. cost=0 на всех картах (отдельного endpoint'а на сделку нет, добавим при необходимости через JOIN supplier_lead_costs). (4) DealsView/KanbanView интеграция — onMounted(loadDeals) async-вызывает dealsApi.listDeals({tenantId: auth.user.tenant_id, limit: 200/500}) если auth.user.tenant_id есть; на success — replace dealsState/dealsByStatus через splice (сохраняет reactive ref). На fail — fetchError=true, v-alert type=warning «Backend недоступен — показаны mock-данные» с data-testid="fetch-error-alert", MOCK_DEALS остаются как fallback. Без auth-state — listDeals НЕ вызывается, MOCK_DEALS показываются как и раньше (Vitest без auth setup продолжает работать без mock'а). KanbanView в loadDeals сначала очищает все колонки (splice 0..length для каждой), затем распределяет по statusSlug. (5) Vitest +14 (всего 261/261 за 19.62 сек): dealsApiMapper.spec.ts 8 (обязательные поля 1:1 / contact_name fallback на phone / manager_name+initials default / project_name=— default / cost всегда 0 / receivedMinutesAgo=30 для 30 мин назад / clamp на 0 при future timestamp / received_at=null → 0); DealsListIntegration.spec.ts 6 (DealsView без tenant_id — listDeals НЕ вызывается + MOCK_DEALS остаются / DealsView с tenant_id — listDeals вызывается + dealsState replaced на 2 API-сделки / DealsView reject → fetchError=true + alert виден + MOCK_DEALS fallback; KanbanView те же 3 сценария). vi.mock на api/deals сохраняет original-импорт через importOriginal чтобы ensureCsrfCookie остался живым для других тестов. PHPStan baseline регенерирован. Production TODO (после v1.51): реальный XLSX-export через PhpSpreadsheet (CSV достаточен на MVP); polling/SSE для real-time обновления списка сделок (на MVP — manual reload); SaaS-admin auth (Yandex 360 SSO ⏸ Б-1); Pest browser-mode ⏸ инфра. Регресс зелёный: lint+type-check+format ✅; Vitest 261/261 за 19.62 сек (+14 от 247); vite build 989 ms (KanbanView lazy-chunk вырос с 180.53→181.98 KB из-за mapApiDeal импорта и onMounted); Pint+PHPStan passed; Pest 186/186 за 22 сек (+12 от 174, 742 assertions). Реестр v1.59→v1.60.
CLAUDE.md v1.50 от 09.05.2026. Изменения v1.50: SupplierResolver service-extract — закрыт TODO (a) из v1.49. Общая логика lookup активного supplier'а через project_suppliers m2m (фильтры is_active=true+is_active=true, ORDER BY sort_order, id) была дублирована между ProcessWebhookJob::resolveSupplierId (webhook-flow) и DealController::resolveSupplierId (manual-create) — 11 одинаковых строк query-builder'а на 2 файла. Решение: App\Services\SupplierResolver с двумя методами — resolveForProject(Project): ?int (тот же DB::table query, что был раньше) + costRubSnapshot(int $supplierId): string (вынесенный snapshot цены cost_rub для записи в supplier_lead_costs; берётся через DB::table('suppliers')->value('cost_rub'), чтобы snapshot не менялся при последующих правках цены поставщика). DI — через app(SupplierResolver::class) внутри handle()/store() (тот же паттерн, что у DuplicateDetector в v1.23 — НЕ через constructor injection, чтобы тесты могли вызывать (new ProcessWebhookJob(...))->handle() напрямую без контейнера). Удалены: ProcessWebhookJob::resolveSupplierId() (private 14 строк) + DealController::resolveSupplierId() (private 14 строк) + локальные DB::table('suppliers')->value('cost_rub') в обоих файлах (теперь через $resolver->costRubSnapshot()). Pest +8 в tests/Feature/Services/SupplierResolverTest.php (всего 174/174 за 21.46 сек, 708 assertions): null когда нет связей; единственный активный supplier; пропуск inactive supplier; пропуск inactive m2m-связи; ORDER BY sort_order (low > high); null если все связи inactive; изоляция по project_id (один supplier на двух проектах не проявляется); costRubSnapshot формат '137.50'. Helpers seedSupplier/attachSupplier — top-level functions в файле теста (не пересекаются с seedSupplierForProject в ProcessWebhookJobTest). Quirk — Project::factory()->create(['type' => 'websites']) падает на CHECK constraint projects_type_check (allowed: webhook|manual|import); factory default = 'webhook' — лишний override убран. PHPStan baseline регенерирован для +30 ignored Pest TestCall warnings (новый файл). Регресс зелёный: Pint+PHPStan passed (baseline регенерирован); Pest 174/174 за 21.46 сек (+8 от 166, 708 assertions); Vitest 247/247 за 17.53 сек (нетронут — backend-only refactor). Реестр v1.58→v1.59.
CLAUDE.md v1.49 от 09.05.2026. Изменения v1.49: 3 lookups + integrity-fix после backend-completion v1.48. (1) GET /api/managers + /api/projects + manager FK guard в DealController. ManagerController::index возвращает active users тенанта (фильтры is_active=true, deleted_at IS NULL), формат {id, email, first_name, last_name, name, initials} с двумя static-helpers formatName/formatInitials (fallback на email если first/last пусты). ProjectController::index — active projects (с is_active=true), формат {id, name, tag, type}. Оба endpoint'а: tenant_id query-param (на prod из middleware), 422 без него, 404 unknown tenant, RLS-обёртка через SET LOCAL app.current_tenant_id в DB::transaction. Manager FK guard в DealController::store — если manager_id передан, проверяем User::where(id, manager_id)->where(tenant_id, tenant->id)->whereNull(deleted_at)->where(is_active, true)->exists(); если не принадлежит tenant'у или не активен — 422 с ошибкой по полю manager_id. Это закрывает security-gap: иначе можно было назначить чужого менеджера на свою сделку. (2) Replace MOCK_MANAGERS / MOCK_PROJECTS на API в NewDealDialog. Новый ref projectOptions: string[] + managerOptions: MockManager[] инициализированы из MOCK_-констант (fallback). При open dialog'а с tenantId — loadLookups() вызывает Promise.all([listProjects, listManagers]) и replace'ит refs. Map managerIdByName: Map<string, number> — нужна для submit'а: name из v-select (return-object) → backend-id. На fail (network) — silent fallback на mock (UI работает дальше). Submit передаёт manager_id: managerIdByName.get(manager.name) ?? undefined. (3) SupplierLeadCost для manual-leads. В DealController::store транзакции после Deal::create вызываем resolveSupplierId($project) — точная копия логики из ProcessWebhookJob::resolveSupplierId (project_suppliers JOIN suppliers, фильтры is_active+is_active, ORDER BY sort_order, id). Если supplier найден — берём cost_rub snapshot и создаём SupplierLeadCost с supplier_lead_id=NULL (manual: нет внешнего id из webhook). Manual-flow по-прежнему НЕ списывает баланс (Ю-2 reseller-модель: charge только при закупке у supplier'а через webhook); cost-аналитика всё равно нужна для отчётности (owner проекта мог купить лид у поставщика и ввести руками). На production — извлечь resolveSupplierId в App\Services\SupplierResolver чтобы Job и Controller разделяли логику + system_settings fallback. Pest +18 (всего 166/166 за 22.11 сек, 699 assertions): LookupsTest 8 (managers active + initials fallback + 422 / 404 + projects + manager FK guard 3 — чужой/inactive/active); DealCreateTest +2 (SupplierLeadCost создан с snapshot cost_rub / без supplier — graceful skip). Старый тест manager_id=42 переписан на User::factory()->for($tenant)->create()->id чтобы пройти FK guard. Vitest +2 (всего 247/247 за 16.32 сек): NewDealDialog +2 (loadLookups вызывает listProjects+listManagers + populates refs + map / submit передаёт backend manager_id из mapping). Vi.mock получил listProjects/listManagers с default Promise.resolve([]) — старые тесты (без tenantId) не вызывают lookups, fallback на mock работает. PHPStan baseline регенерирован для +28 ignored Pest TestCall warnings (LookupsTest + DealCreateTest расширения). Production TODO остаточные: (a) resolveSupplierId в Service-класс (рефактор Job + Controller); (b) реальный XLSX-export через PhpSpreadsheet (CSV пока достаточен); (c) GET /api/deals для замены MOCK_DEALS в DealsView/KanbanView (опционально — на MVP local-state ok); (d) SaaS-admin auth (Yandex 360 SSO ⏸ Б-1). Регресс зелёный: lint+type-check+format ✅; Vitest 247/247 за 16.32 сек (+2); vite build 951 ms; Pint+PHPStan passed (baseline регенерирован); Pest 166/166 за 22.11 сек (+10 от 156, 699 assertions). Реестр v1.57→v1.58.
CLAUDE.md v1.48 от 09.05.2026. Изменения v1.48: 3 backend-completion изменения после tightening v1.47. (1) POST /api/deals — manual create endpoint для NewDealDialog. DealController::store валидирует tenant_id/project_name/phone (required) + contact_name/status/manager_id/comment (optional). Резолвит/создаёт Project через firstOrCreate(tenant_id+name, type='manual'). Создаёт Deal с received_at=NOW(), source_crm_id=NULL (отличие от webhook'а), assigned_at=NOW() если manager_id передан. Транзакция + RLS-обёртка SET LOCAL app.current_tenant_id (PgBouncer-safe). Manual-create НЕ списывает баланс (не закупка у поставщика), НЕ применяет антифрод-дедуп (admin знает что вводит), НЕ создаёт SupplierLeadCost. Пишет ActivityLog с context.source=manual. NewDealDialog.vue получил optional tenantId prop — если передан, submit делает dealsApi.createDeal(), на success deal возвращается с реальным backend-id; на network/500-error — fallback на local-id + submit-error-alert warning + dialog остаётся открытым. Чистый local-mode (без tenantId) сохранён для тестов и legacy. DealsView/KanbanView получили useAuthStore + передают :tenant-id="auth.user?.tenant_id". (2) webhook_hmac_required flag в system_settings. Добавлен ключ в seed db/schema.sql:2200 ('webhook_hmac_required', 'false', 'bool' — default backward-compat). WebhookReceiveController::isHmacRequired() private helper читает значение через SystemSetting::find (без записи → false). При true: запрос без X-Webhook-Signature → 401. При false: header опционален (если пришёл — verify, иначе пропускаем). Pest +3: required+missing → 401, required+valid HMAC → 202, false (default) → 202 без header. (3) POST /api/deals/export — CSV endpoint backend-side. DealController::export валидирует tenant_id/ids[1-10000 ints]. RLS-обёрнутый SELECT по whereIn(ids), формирует CSV (Excel-friendly: BOM \u{FEFF} PHP-литерал, ; разделитель, \r\n, escape для ;/"/\n через двойные кавычки). Возвращает text/csv; charset=utf-8 + Content-Disposition: attachment; filename="deals_export_YYYY-MM-DD.csv". Frontend applyBulkExport теперь сначала пробует dealsApi.exportDeals (если auth.user?.tenant_id есть) → triggerCsvDownload со взятым CSV; на fail — fallback на buildLocalCsv() (тот же flow что в v1.47, но вынесен в отдельную функцию). На каждом флоу — toast о результате. api/deals.ts новый файл с createDeal/exportDeals (responseType: 'text' для CSV string). Pest +15 (всего 156/156 за 20.27 сек, 675 assertions): DealCreateTest 12 (8 store + 4 export); WebhookReceiveTest +3 hmac_required. Vitest +3 (всего 245/245 за 17.07 сек): NewDealDialog +3 (без tenantId — local mode; с tenantId+success — backend-id; с tenantId+error — fallback+warning); DealsView/KanbanView spec'ы получили setActivePinia(createPinia()) (auth-store нужен для tenant_id). Quirks: (a) PHPStan ругался на Deal->id === null (Eloquent типизирует id как int) — убрал лишнюю проверку. (b) String.fromCharCode(0xFEFF) в JS / "\u{FEFF}" в PHP — оба работают, литерал заблокирован ESLint no-irregular-whitespace. (c) RLS-изоляция export'а тестируется отдельно через testing_rls_user (NOLOGIN без BYPASSRLS) — в DealCreateTest используется postgres superuser (BYPASSRLS), поэтому RLS-проверка тут была бы false-positive — заменил на тест фильтрации по whereIn(ids). Production TODO остаточные (после v1.48): Manager lookup в DealController (сейчас manager_id передаётся клиентом без проверки tenant-membership); replace MOCK_MANAGERS на API GET /api/managers; SupplierLeadCost для manual-leads (при наличии supplier'а у проекта); реальный XLSX-export через PhpSpreadsheet (CSV пока достаточен); SaaS-admin auth (Yandex 360 SSO ⏸ Б-1). Регресс зелёный: lint+type-check+format ✅; Vitest 245/245 за 17.07 сек (+3 от 242); vite build 1.04 сек; Pint+PHPStan passed (baseline регенерирован); Pest 156/156 за 20.27 сек (+15 от 141, 675 assertions). Реестр v1.56→v1.57.
CLAUDE.md v1.47 от 09.05.2026. Изменения v1.47: 3 production-tightening изменения после 7-фичного пакета v1.46. (1) HMAC + per-token rate-limit для webhook receive endpoint — закрыты 2 production-TODO из v1.46. WebhookReceiveController::receive теперь делает 3 проверки в порядке: tenant lookup → rate-limit → HMAC → валидация payload. HMAC: опциональный header X-Webhook-Signature: sha256=<hex>, верификация через hash_hmac('sha256', raw_body, webhook_token) + hash_equals (constant-time compare против timing attacks). На MVP — backward-compat: header отсутствует → пропускаем (для prod через system_settings.webhook_hmac_required сделаем обязательным). Невалидная подпись → 401 (не 422 — это auth issue). Per-token rate-limit: RateLimiter::tooManyAttempts("webhook:{tenant_id}", rps×60) с decay 60 сек. Лимит читается из system_settings.webhook_rate_limit_rps (default 100 RPS из seed v8.7), приводится к per-minute через ×60 (Laravel RateLimiter работает per-decay-window). На превышении — 429 + Retry-After header + retry_after в JSON. Rate-limit ключ изолирован per-tenant, hit ставится ДО валидации payload (иначе можно обойти лимит спамом 422-ответов). Pest +5 в WebhookReceiveTest: HMAC valid (test через $this->call('POST', ..., $rawBody) чтобы передать сырой body) + invalid (401 + не диспатч) + missing (202 backward-compat); rate-limit с SystemSetting::update(['value'=>'1']) → 60 успешных + 61-й = 429 + Retry-After; ключ изолирован per-token (alice заблокирована, bob проходит). RateLimiter::clear в beforeEach чтобы не загрязнять следующий тест. (2) Реальный fetch для system_settings в AdminSystemView — закрыт TODO из v1.46. onMounted(loadSettings) вызывает adminApi.listSystemSettings() и replace'ит settingsState.splice(0, length, ...fromApi) (сохраняет reactive-ref). На fetch-error → fallback на mock-данные + warning v-alert (fetch-error-alert data-testid, closable). Кнопка data-testid="reload-btn" в header триггерит ручной reload. Mock-данные используются как fallback при сетевой ошибке (UI не пустеет). Type-shape совместим: AdminSystemSetting (mock) и ApiSystemSetting (backend) различаются только origin. Vitest +3: assert listSystemSettings called once on mount; reload-btn triggers manual fetch; on rejection → warning-alert visible + 7 mock rows preserved. (3) Реальный CSV-export для bulk-actions в DealsView — закрыт TODO из v1.46. applyBulkExport() теперь не просто toast'ит, а формирует CSV и триггерит download через Blob+<a download>. Headers: ID/Имя/Телефон/Статус/Проект/Менеджер/Стоимость/Получено мин назад. CSV-escape (значение в кавычках если содержит ;/"/\n; внутри двойные ""). Разделитель ; (Excel-friendly для русской локали). Line-endings \r\n (Windows). BOM через String.fromCharCode(0xFEFF) (литеральный U+FEFF блокируется ESLint no-irregular-whitespace) — Excel правильно распознаёт UTF-8 кириллицу. Filename deals_export_YYYY-MM-DD.csv. Toast «Экспортировано N сделок в CSV». Empty selection → toast «Нет выбранных» без download. Vitest +2: spy на URL.createObjectURL+HTMLAnchorElement.prototype.click — assert called once + correct toast text; empty selection → не вызываем URL.createObjectURL. TODO (production): webhook HMAC обязательным через flag в system_settings; реальный backend-export через POST /api/deals/export → ReportsView с XLSX (через xlsx-библиотеку или Excel-шаблон); система settings с filter+sort. Регресс зелёный: lint+type-check+format ✅; Vitest 242/242 за 15.82 сек (+4 от 238); vite build 903 ms; Pint+PHPStan passed (baseline регенерирован для новых Pest TestCall ignored count'ов); Pest 141/141 за 17.8 сек (+5 от 136, 627 assertions). Реестр v1.55→v1.56.
CLAUDE.md v1.46 от 09.05.2026. Изменения v1.46: 7-фичный auto-mode пакет (по согласованному списку из «карты что осталось»): (1) Bulk-actions DealsView — show-select уже был; добавлен dealsState reactive-копия (deep-clone MOCK_DEALS чтобы не мутировать const), bulk-bar (sticky, theme=dark теало-нуар) с count + 4 actions (Сменить статус через v-menu со всеми 14 lead_statuses / Экспорт через v-snackbar / Удалить через v-dialog confirm / ✕ clear). (2) NewDealDialog — components/deals/NewDealDialog.vue модалка с 6 полями (name/phone/project из MOCK_PROJECTS / manager из MOCK_MANAGERS / cost/status дефолт 'new' или presetStatus). Phone-валидация ≥10 цифр. emit('created', deal) → DealsView/KanbanView пушит в свой reactive-state (KanbanView в правильную колонку по statusSlug + totalDeals++). MOCK_PROJECTS/MOCK_MANAGERS добавлены в composables/mockDeals.ts. (3) AdminTenantDetailView — drill-down /admin/tenants/:code. composables/mockTenantDetail.ts с expandTenantDetail (5 sample-users / 2 sample-projects / 8 sample balance-tx / 5 sample-activity). 4 KPI cards (Баланс/Тариф+MRR/Лиды сегодня+неделя+месяц/Средняя цена) + 4 v-tabs (Финансы balance-history table / Пользователи / Проекты / Активность). Hero с tenant.contact_email + legal_address + кнопка «Войти как клиент» (использует ImpersonationDialog из v1.45). 404-fallback если code не найден. AdminTenantsView получил @click:row → router.push({name: admin-tenant-detail, params: {code}}). (4) Edit-flow AdminSystemView (audit-log + 2-step) — App\Models\SystemSetting (PK=key string, без CREATED_AT) + App\Models\SaasAdminAuditLog (append-only без UPDATED_AT, payload_before/after JSONB casts). AdminSystemSettingsController с GET /api/admin/system-settings (list) + PUT /api/admin/system-settings/{key} (update в DB::transaction вместе с INSERT в saas_admin_audit_log; hash-chain trigger BEFORE INSERT заполняет log_hash). Type-validation: int → ctype_digit (с минусом для signed); decimal → is_numeric; bool → in('true','false','1','0'); json → JSON_THROW_ON_ERROR. Reason ≥30 chars. Frontend SystemSettingEditDialog — 3-step (edit→confirm с diff before/after→done). AdminSystemView получил кнопку «Изменить» в каждой строке + onSettingUpdated optimistic update. (5) Webhook receive endpoint — App\Http\Controllers\Api\WebhookReceiveController::receive POST /api/webhook/{token} (token=tenants.webhook_token). Валидация payload (vid/project/phone/time required + nullable tag/phones array). 404 на unknown token; 422 на bad payload; 202 на success + dispatch ProcessWebhookJob (sync на dev queue.driver=sync). Stub-INSERT в webhook_log через DB::table (если таблица существует) обёрнут в DB::transaction + SET LOCAL app.current_tenant_id для RLS. CSRF-исключение для api/webhook/* в bootstrap/app.php — внешний клиент без сессии. (6) Smart-filters — DealsView получил 2 multi-select v-select (Проекты + Менеджеры) с availableProjects/availableManagers computed (auto из dealsState); filteredDeals фильтрует по slug+projects+managers+search. AdminTenantsView получил аналогичные filterStatuses (4 STATUS_OPTIONS) + filterTariffs (computed availableTariffs из MOCK_TENANTS). Кнопка «Сбросить фильтры»/«Сбросить» появляется только когда фильтры активны. (7) AdminImpersonationView — Backend +2 endpoint: GET /api/admin/impersonation/active (where used_at!=null AND session_ended_at==null) + GET /api/admin/impersonation/recent (last 20 завершённых с duration_seconds через abs(diffInSeconds) — quirk: Carbon diffInSeconds signed по умолчанию, без abs() возвращал отрицательное). ImpersonationToken получил belongsTo(Tenant). Frontend view с 2 секциями (Активные → end-кнопка / Недавно завершённые read-only) + refresh-btn + onMounted load. Маршрут /admin/impersonation добавлен в router; AdminLayout получил 5-й nav-пункт «Impersonation» mdi-account-switch. Vitest +48 (всего 238/238 за 15.31 сек): bulk-actions 6 + NewDealDialog 6 + AdminTenantDetailView 10 + SystemSettingEditDialog 8 + AdminSystemView +3 / AdminTenantsView +4 + DealsView smart-filters 3 + AdminImpersonationView 6. Setup получил visualViewport polyfill (VOverlay/v-menu/v-snackbar location strategies). Pest +16 (всего 136/136 за 15.8 сек, 495 assertions): AdminSystemSettings 8 + WebhookReceive 6 + Impersonation active/recent 2. PHPStan baseline регенерирован (+ноль errors). Pint passed. Quirks: (1) bool в filterTariffs = TenantTariff[] (не string[]) — vue-tsc ругалось type-mismatch с availableTariffs: TenantTariff[]. (2) DELETE TestPartitions использует DETACH перед DROP (из v1.40, не повторяется). (3) ImpersonationDialog stubится в AdminTenantsView/AdminTenantDetailView spec'ах. (4) NewDealDialog watch с immediate: true — иначе presetStatus prop не подхватывался при initial mount с открытым dialog. (5) Тесты onDealCreated требуют полный MockDeal (с manager) — Kanban-карточка ожидает deal.manager.name. Регресс зелёный: lint+type-check+format ✅; vitest 238/238 за 15.31 сек; vite build 937 ms; Pint+PHPStan passed; Pest 136/136 за 15.8 сек. Реестр v1.54→v1.55.
CLAUDE.md v1.45 от 09.05.2026. Изменения v1.45: Impersonation UI dialog (Ю-1 frontend) — закрыт TODO из v1.44. api/admin.ts — типизированные axios-helpers impersonationInit/Verify/End для трёх endpoint'ов из v1.44 (POST /api/admin/impersonation/{init,verify,end}); все три делают ensureCsrfCookie() (Sanctum SPA cookie-flow), на prod автоматически перейдут под middleware('auth:saas-admin') без изменений на клиенте — withCredentials: true уже в apiClient. components/admin/ImpersonationDialog.vue — 4-step state-machine (reason → verify → active → done): step 1 — v-textarea с counter и hint «Ещё N символов» (валидация ≥30 chars на клиенте до POST + ловля backend 422 через extractValidationErrors); step 2 — v-text-field с inputmode=numeric maxlength=6 autocomplete=one-time-code + info-alert «Код отправлен на email клиента: {sent_to_email}» + dev-only success-alert с _dev_plain_code (на prod исчезнет после MailService — backend перестанет его возвращать); step 3 — success-alert «Impersonation активен» + v-btn color=error «Завершить сессию» + локализованное usedAtIso через toLocaleString('ru-RU'); step 4 — финальный success + «Закрыть». Persistent-dialog (нельзя закрыть кликом за пределами — двусторонняя ответственность за audit trail). watch(props.modelValue) сбрасывает state при каждом открытии (без stale-данных от прошлого тенанта). AdminTenantsView — добавлена 8-я колонка actions (width=56) с v-tooltip + icon-btn mdi-account-switch; кнопка :disabled="item.status === 'suspended'" (по ТЗ §22.7 impersonation допустим только в активных tenant'ах). @click.stop (не пропускаем event дальше — будущий row-click для drill-down не должен срабатывать). data-testid="impersonate-btn-{id}" для unique selectors в тестах. ADMIN_USER_ID=1 как заглушка (на prod удалится — requested_by придёт из request()->user()->id). Vitest +11 (всего 190/190 за 13.23 сек): ImpersonationDialog.spec.ts (7) — modelValue=false скрыт + step-1 mount + reason<30 показывает counter + успешный init→step2 с email+dev-banner + verify-success→step3 с end-btn + invalid 5-digit code не вызывает API + end→step4 + Cancel emit; AdminTenantsView.spec.ts +4 — каждая из 7 строк имеет impersonate-btn + suspended-tenant disabled + click открывает диалог с правильным tenant + props.requestedBy=1. Vitest quirk: v-dialog и v-tooltip требуют layout-injection от v-app/v-layout — auto-import vite-plugin-vuetify не работает в Vitest. Stub'ы: VDialog как <div v-if="modelValue"><slot /></div> (passthrough), VTooltip как <div><slot name="activator" :props="{}" /></div>; ImpersonationDialog stub'ится в AdminTenantsView spec (внутри использует api/admin axios — реальные запросы в jsdom не нужны, сам диалог покрыт отдельным spec'ом). api/admin + extractValidationErrors/extractErrorMessage мокаются через vi.mock (паттерн из auth-store.spec.ts — axios.isAxiosError(plain Error) в jsdom возвращает false). TODO (production): SaaS-admin auth (Yandex 360 SSO ⏸ Б-1) → middleware → frontend убирает requestedBy prop; two-person approval dialog для tenant'ов с pd_subject_request.processing_restricted=TRUE/chargeback_unrecovered_rub > 0 (CTO-15/Ю-9); реальный MailService → _dev_plain_code исчезает; live impersonation session (cookie-swap для admin'а на 1ч); страница «Активные impersonation-сессии» в админке. Регресс зелёный: lint:vue ✅ (после --fix 6 attribute-order warnings), type-check ✅, format ✅, Vitest 190/190 за 13.23 сек (+11 от 179); vite build 924 ms (AdminTenantsView lazy-chunk 20.68 KB включает inline ImpersonationDialog); Pest 120/120 за 15.69 сек (нетронут — backend без изменений). Реестр v1.53→v1.54.
CLAUDE.md v1.44 от 09.05.2026. Изменения v1.44: Impersonation flow backend (Ю-1). Закрыт пункт #9 — последний пункт списка из v1.46. ImpersonationToken Eloquent для impersonation_tokens (schema v8.7 §22.7), UPDATED_AT=null (схема без updated_at). Helper методы isExpired() / isUsable(). ImpersonationController с 3 endpoints: init({tenant_id, requested_by, reason}) — reason ≥30 chars, генерация 6-значного кода (random_int 100000-999999), bcrypt-hash в impersonation_tokens, TTL 15 мин (по ТЗ). _dev_plain_code возвращается в response (на prod после MailService — только в email клиента). verify({token_id, code}) — Hash::check, increment failed_attempts при неверном коде, при ≥5 → invalidated_at = NOW() + блокировка. На success — used_at = NOW() + 200. end({token_id}) — session_ended_at = NOW(). Все 3 endpoint без auth-middleware на MVP (saas-admin auth не реализован, requested_by принимается параметром). Production: middleware('auth:saas-admin') + role guard + two-person approval (CTO-15/Ю-9 — для тенантов с pd_subject_request.processing_restricted=TRUE или chargeback_unrecovered_rub>0). Маршруты /api/admin/impersonation/{init,verify,end}. Pest +9 в tests/Feature/ImpersonationTest.php (всего 120/120 за 15.62 сек, 443 assertions): init success (TTL ±1 мин, bcrypt-hash) + 422 short reason + 404 unknown tenant + verify success (used_at) + 422 + increment failed_attempts + 5 неверных → invalidated + 422 expired + end success + 422 без verify. PHPStan baseline регенерирован. TODO (пост-MVP): saas-admin auth (Yandex 360 SSO) + middleware + two-person approval + email-уведомления клиенту + UI dialog в AdminTenantsView (кнопка «Войти как клиент»). Все 9 пунктов списка v1.46 закрыты (кроме #6 Yandex SSO ⏸ Б-1 и #7 browser-mode — отложен инфра). Регресс зелёный: lint+type+format OK; vite build 846 ms; Pest 120/120 за 15.62 сек (+9 от 111, 443 assertions); Pint+Stan passed. Реестр v1.52→v1.53.
CLAUDE.md v1.43 от 09.05.2026. Изменения v1.43: Admin views (Биллинг / Инциденты / Система). Закрыт пункт #8 — заменены 3 placeholder'а на реальные display-views с mock-данными. AdminBillingView: 4-stats row (MRR / Выручка за месяц / Просрочка / Возвраты за 30 дн) + v-data-table 7 колонок (Тенант с ИНН / Тариф / Баланс ₽ с error-color при <0 / Пополнения за мес / Списания / MRR / Статус-chip). Search-фильтр по name/ИНН. AdminIncidentsView: 3-stats row (Открыто/Расследуется/РКН-уведомлений) + v-btn-toggle 5 фильтров по статусу + v-list инцидентов с incident_id (INC-YYYY-MMDD-NNNN), severity-chip + status-chip + специальный «РКН pending» chip для PDN-breach + дедлайн РКН (24 ч по 152-ФЗ). 5 категорий (PDN-breach / service_outage / security / billing / data_loss). AdminSystemView: read-only warning + поиск по ключу/описанию + v-list 7 system_settings (webhook_rate_limit_rps, login_max_attempts, password_min_length, retention_days, maintenance_mode и т.д.) с type-chip (int/string/bool/json) и updated_at. Edit-flow с двойным подтверждением + audit-log — отдельный коммит. composables/mockAdmin.ts: типы AdminBillingTenantRow/AdminIncidentRow/AdminSystemSetting + mock-данные. Маршруты /admin/billing|incidents|system теперь ведут на реальные view'ы (не AdminPlaceholderView). Vitest +13 (всего 179/179 за 11.98 сек): AdminBillingView 3 (mount + 4 stats + table contents); AdminIncidentsView 5 (mount + 3 stats + filter-toggle + PDN+РКН pending + incident_id format); AdminSystemView 5 (mount + read-only warning + key settings + type-chip + 7 rows). TODO (продолжение): #9 Impersonation flow (Ю-1). Регресс зелёный: lint+type+format OK; vitest 179/179 за 11.98 сек (+13 от 166); vite build 743 ms; story:build 21/28 за 31.5 сек. Реестр v1.51→v1.52.
CLAUDE.md v1.42 от 09.05.2026. Изменения v1.42: Email-уведомление при 3 неудачных попытках входа (ТЗ §22.4.4 п.3). Закрыт пункт #5 — последний пункт ТЗ §22.4.4 анти-брутфорс. App\Mail\SuspiciousLoginNotification Mailable + resources/views/emails/suspicious_login.blade.php (HTML email с инструкциями: сменить пароль / включить 2FA / проверить сессии). AuthController::maybeNotifySuspiciousLogin triggers ровно при count(auth_log.login_failed для user_id за час) === 3 — иначе на 4-5 неудачах будут спам-emails. Для unknown email user=null → ничего не отправляем. На dev MAIL_MAILER=log письмо в storage/logs. Pest +4 в tests/Feature/Auth/SuspiciousLoginNotificationTest.php (всего 111/111 за 14.32 сек, 401 assertions): после 3-й неудачи Mail::assertSent с правильными user/count/recipient; на 4-5 не дублируется (assertSent count=1); для unknown email НЕ отправляется; успех на 1-2 неудачах НЕ триггерит. PHPStan baseline регенерирован. TODO (продолжение): #7 browser-mode, #8 admin views, #9 impersonation. Регресс зелёный: Pint+Stan passed; Pest 111/111 за 14.32 сек (+4 от 107). Реестр v1.50→v1.51.
CLAUDE.md v1.41 от 09.05.2026. Изменения v1.41: IP-lockout 10/час + auth_log записи (ТЗ §22.4.4 п.2). Закрыт пункт #4 — защита от перебора с одного IP. AuthController::login перед verify проверяет isIpLockedOut(ip) — count() FROM auth_log WHERE event='login_failed' AND ip_address=ip AND created_at >= NOW() - 1 hour. Если ≥10 → 429 + Retry-After: 3600. Это второй слой защиты поверх email-rate-limit (5/15мин из v1.36) — защищает от перебора email'ов с одного IP. logAuthEvent private helper пишет в auth_log через DB::table (Eloquent для этой таблицы нет). На каждый login_success / login_failed (3 ветки: invalid_password / unknown_email / account_locked). RLS USING без WITH CHECK — INSERT не фильтруется. hash-chain trigger (BEFORE INSERT) заполняет log_hash автоматически (OPEN-И-15 tamper-detection). Pest +6 в tests/Feature/Auth/IpLockoutTest.php (всего 107/107 за 13.86 сек, 380 assertions): login_success пишет с tenant_id; login_failed wrong-password пишет invalid_password; login_failed unknown email пишет unknown_email + user_id=null; 10 fail записей с одного IP за час → следующий login = 429; 9 fail записей (под порогом) → проходит; старые записи >1ч не блокируют. PHPStan baseline регенерирован. TODO (продолжение): #5 email-warn, #7 browser-mode, #8 admin views, #9 impersonation. Регресс зелёный: lint+type+format OK; Pest 107/107 за 13.86 сек (+6 от 101, 380 assertions). Реестр v1.49→v1.50.*
CLAUDE.md v1.40 от 09.05.2026. Изменения v1.40: 2FA setup wizard + schema v8.7→v8.8 + миграция fix FK + Partitions test fix. Закрыт пункт #3 — пользователь может включить/отключить/перегенерировать 2FA из SettingsView/SecurityTab. Backend TwoFactorSetupController под auth:sanctum: 4 endpoint'а — init (генерация TOTP secret + QR-URL, secret в session как pending, не пишется в БД до confirm); confirm({code}) (TOTP-verify pending secret → save totp_secret + totp_enabled=true + delete old recovery codes + generate 8 new + return plain один раз); disable({password}) (Hash::check + clear totp_secret + drop recovery codes); regenerate-recovery-codes({password}) (Hash::check + replace 8 codes). Recovery code формат xxxx-xxxx (lowercase 4+4 + дефис), Str::random(4) parts. User model получил cast 'totp_secret' => 'encrypted' (Crypt::encryptString автоматом). Schema v8.7 → v8.8: users.totp_secret VARCHAR(255) → TEXT — encrypted 32-байт TOTP secret = ~256 chars > 255 (PDOException на confirm). Запись §V в db/CHANGELOG_schema.md. Миграция fix: 0001_01_01_000000_load_initial_schema.php теперь после DB::unprepared($sql) явно делает ALTER TABLE webhook_dedup_keys ADD FOREIGN KEY ... ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED — DDL FK на partitioned-таблицу через unprepared() PDO молча проглатывался на свежей БД (известное поведение Laravel/PDO). Без fix'а ON DELETE CASCADE тест валится. PartitionsCreateMonthsTest fix: afterEach использует ALTER TABLE deals DETACH PARTITION ... + DROP TABLE вместо DROP ... CASCADE — последний дропал FK от webhook_dedup_keys на parent (PG behavior). DB timezone fix (config/database.php pgsql) добавлен в v1.38 продолжает работать. Frontend SecurityTab переписан с mock на реальную логику: 3 v-dialog'а (setup wizard 3 шага: init→confirm→show 8 codes; disable; regenerate). 4 новых функции в api/auth.ts: twoFactorInit/Confirm/Disable/regenerateRecoveryCodes. v-chip статуса 2FA читает auth.user?.totp_enabled. Pest +10 в tests/Feature/Auth/TwoFactorSetupTest.php (всего 101/101 за 13.37 сек, 364 assertions): init success / 422 если 2FA уже on / confirm success + 8 кодов формат + totp_enabled=true + secret saved + 8 строк в БД / confirm 422 неверный код + totp_enabled остаётся false / confirm 422 без init / disable success / disable 422 неверный пароль / regenerate возвращает 8 новых уникальных + старые удалены / regenerate 422 если 2FA off / все 4 endpoint'а require auth (401). Vitest: SettingsView.spec.ts получил createPinia() в plugins (SecurityTab теперь использует useAuthStore). PHPStan baseline регенерирован для +25 ignored Pest TestCall warnings. TODO (продолжение): #4 IP-lockout, #5 email-warn, #7 browser-mode, #8 admin views, #9 impersonation. Регресс зелёный: lint+type+format OK; vitest 166/166 за 10.95 сек; vite build 747 ms; story:build 21/28 за 31.18 сек; Pint+Stan passed; Pest 101/101 за 13.37 сек (+10 от 91, 364 assertions). Реестр v1.48→v1.49.
CLAUDE.md v1.39 от 09.05.2026. Изменения v1.39: Recovery code login (POST /api/auth/2fa/recovery-use). Закрыт пункт #2 из списка v1.47 — вход по одноразовому резервному коду 2FA вместо TOTP. Backend: AuthController::useRecoveryCode(UseRecoveryCodeRequest) берёт pending_user_id из session (тот же state, что и /2fa/verify), нормализует код (lowercase + удаление дефисов/пробелов), перебирает неиспользованные user_recovery_codes через Hash::check, на совпадении → mark used_at = NOW() + Auth::login + clear pending. Возвращает {user, requires_2fa: false, recovery_codes_remaining: int}. Rate-limit auth:recovery:{pending_user_id}|{ip} — 5/15мин, scope отделён от 2fa/verify. Маршрут POST /api/auth/2fa/recovery-use публичный (как 2fa/verify). Eloquent-модель UserRecoveryCode для user_recovery_codes (schema v8.7 §10) — без updated_at (UPDATED_AT = null, в schema только created_at + used_at). Frontend: authApi.useRecoveryCode, auth-store::useRecoveryCode action; новый view UseRecoveryCodeView.vue с маршрутом /recovery-use (auth layout, без guestOnly чтобы не редиректить pending-state) — input с autocomplete=one-time-code + submit + back-link на /2fa; на success сохраняет recovery_codes_remaining в sessionStorage для будущего toast-warning'а в SettingsView/SecurityTab. TwoFactorView ссылка «Использовать резервный код» переписана с /recovery на /recovery-use (старый /recovery остаётся для display 8 кодов после setup'а, отдельный пункт #3). Pest +6 в tests/Feature/Auth/RecoveryCodeTest.php (всего 91/91 за 12.77 сек, 319 assertions): успех + mark used + remaining=3; неверный код 422; уже использованный 422; без pending 422; разные форматы (пробел/дефис/регистр); rate-limit 6-я = 429. Vitest +6 (всего 166/166 за 11.47 сек): auth-store useRecoveryCode success/reject; UseRecoveryCodeView 4 (mount + autocomplete + submit-flow с sessionStorage + lockout-alert). PHPStan baseline регенерирован. TODO (продолжение): #3 2FA setup wizard, #4 IP-lockout, #5 email-warn, #7 browser-mode, #8 admin views, #9 impersonation. Регресс зелёный: lint+type+format OK; vitest 166/166 за 11.47 сек (+6 от 160); vite build 849 ms; story:build 21/28 за 30.36 сек; Pint+Stan passed; Pest 91/91 за 12.77 сек (+6 от 85). Реестр v1.47→v1.48.
CLAUDE.md v1.38 от 09.05.2026. Изменения v1.38: Reset password (deep-link) + DB timezone fix. Закрыт второй пункт password-reset flow — установка нового пароля по token из email-ссылки. Backend: AuthController::resetPassword(ResetPasswordRequest) использует Password::reset() с callback $user->forceFill(['password_hash' => Hash::make($password)])->save() (наша колонка password_hash). ResetPasswordRequest валидирует token + email + password (min 10 — ТЗ §22.4.1) + confirmed. Rate-limit 5/15мин по ключу auth:reset:{sha256(token)[0..16]}|{ip}. Status Password::PASSWORD_RESET → 200; иначе → 422 «Ссылка недействительна или истекла» + hit. Маршрут POST /api/auth/reset-password публичный. DB timezone fix (config/database.php pgsql): добавлен 'timezone' => env('DB_TIMEZONE', 'UTC') — без него PG возвращал TIMESTAMPTZ с offset +03, Carbon::parse терял offset и tokenExpired некорректно интерпретировал created_at. Без fix'а Password::reset падал на check expiry. Фикс затрагивает любую TZ-чувствительную логику (не только password reset). Frontend: authApi.resetPassword(payload), auth-store::resetPassword action, ResetPasswordView.vue для deep-link /reset/:token?email=... — token из route.params, email pre-filled из query, поля password+confirmation с autocomplete=new-password, success-state + redirect на /login через 3 сек, lockout-alert. Маршрут /reset/:token (meta.layout=auth, guestOnly). Route /reset добавлен в web.php SPA-paths. Pest +6 в tests/Feature/Auth/ResetPasswordTest.php (всего 85/85 за 11.50 сек, 291 assertions): успех + token-update + 422 на bad token / mismatch confirmation / short password / unknown email / rate-limit. Vitest +7 (всего 160/160 за 11.02 сек): auth-store success + 429; ResetPasswordView mount + email-prefill из query + 2 password-inputs autocomplete=new-password + success-state hides form + lockout-alert. PHPStan baseline регенерирован. TODO (отдельные коммиты): Pest browser-mode для full session-flow + 2FA setup wizard + recovery-codes consume + Yandex SSO (Б-1). Регресс зелёный: lint+type+format OK; vitest 160/160 за 11.02 сек (+7 от 153); vite build 784 ms; story:build 21/28 за 30.74 сек; Pint+Stan passed; Pest 85/85 за 11.50 сек (+6 от 79). Реестр v1.46→v1.47.
CLAUDE.md v1.37 от 08.05.2026 (поздний вечер). Изменения v1.37: Forgot password flow (ТЗ §1.7 / Прил. Г.4.3). Запрос ссылки на сброс через email. Backend: AuthController::forgotPassword(ForgotPasswordRequest) использует Password::sendResetLink() под капотом — Laravel создаёт row в password_resets (env AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets указывает на нашу таблицу из schema v8.7 §10.6, default Laravel password_reset_tokens НЕ совпадает) + шлёт ResetPassword Notification. На dev MAIL_MAILER=log → notification в storage/logs. Anti-enumeration: ВСЕГДА 200 unified-message «Если такой email зарегистрирован — мы отправили ссылку», независимо от существования user'а — иначе перебор email'ов через ответ. Rate-limit: 5 попыток / 15 мин по ключу auth:forgot:{lower(email)}|{ip}, 6-я → 429 + Retry-After. RateLimiter::hit ставится ДО sendResetLink — иначе можно перебирать вечно за счёт unknown email'ов. Frontend: authApi.forgotPassword(email), auth-store::requestPasswordReset(email) action (загружает lockoutSeconds на 429), ForgotPasswordView интегрирован: submit → store → submitted=true → success-state v-alert (data-testid=forgot-success) скрывает форму + остаётся «Назад ко входу» btn. Pest +6 в tests/Feature/Auth/ForgotPasswordTest.php (всего 79/79 за 10.55 сек, 273 assertions): existing email → 200 + row в password_resets + Notification::assertSentTo(ResetPassword); unknown email → 200 unified без row + assertNothingSent; валидация 422 (формат / пустое); rate-limit 5 → 6-я = 429; throttle ключ изолирован по email. Vitest +4 (всего 153/153 за 11.11 сек): auth-store success/429; ForgotPasswordView success-state (форма скрывается после submit) + lockout-alert. PHPStan baseline регенерирован для +14 ignored Pest TestCall warnings. TODO (отдельные коммиты): POST /api/auth/reset-password (deep-link /reset/{token}?email= + UI-форма new_password). Регресс зелёный: lint+type+format OK; vitest 153/153 за 11.11 сек (+4 от 149); vite build 862 ms; story:build 21/28 за 32 сек; Pint passed; Pest 79/79 за 10.55 сек (+6 от 73, 273 assertions). Реестр v1.45→v1.46.
CLAUDE.md v1.36 от 08.05.2026 (поздний вечер). Изменения v1.36: Rate-limiting login + 2FA verify (ТЗ §22.4.4). По ТЗ §22.4.4: 5 неудачных попыток входа на email → блокировка 15 мин. Backend через Illuminate\Support\Facades\RateLimiter. AuthController::login перед verify проверяет RateLimiter::tooManyAttempts("auth:login:{email}|{ip}", 5) → 429 + Retry-After. На неуспехе → RateLimiter::hit($key, 900) (15 мин). На успехе email+пароля → RateLimiter::clear (2FA-фаза не зависит от login-fails). AuthController::verifyTwoFactor аналогично, ключ auth:2fa:{pending_user_id}|{ip}. lockoutResponse() private helper возвращает 429 + JSON {message, retry_after} + header Retry-After. Ключ login делает mb_strtolower(email) для case-insensitivity. Pest +6 в tests/Feature/Auth/RateLimitTest.php (всего 73/73 за 8.07 сек, 246 assertions): 5 неудач → 6-я с правильным паролем = 429 + Retry-After ∈ (0, 900]; успешный login чистит throttle (5 новых wrong снова возможны); throttle ключ изолирован по email (Alice заблокирована, Bob входит); inactive user тоже расходует попытки; 2FA verify 5 неверных кодов → 6-я с правильным TOTP = 429; 2FA success чистит throttle. Quirk: при первой версии тестов wrong-password='wrong' (5 символов) валидация LoginRequest min:8 падала до controller, RateLimiter::hit не вызывался — пароль для wrong-attempts должен быть ≥8 символов. Frontend auth-store::lockoutSeconds ref<number|null>: при 429 в login() / verifyTwoFactor() catch-блок извлекает retry_after через extractRateLimitRetry() (новый helper в api/client.ts — читает response.data.retry_after или header Retry-After). Успешный login сбрасывает lockoutSeconds = null. LoginView/TwoFactorView показывают v-alert type=error с data-testid="lockout-alert": «Слишком много попыток. Попробуйте через {Math.ceil(seconds/60)} мин.». Vitest +4 (всего 149/149 за 12.31 сек): auth-store 3 (login 429 → lockoutSeconds=600 + reject; verifyTwoFactor 429 → lockoutSeconds=900; успешный login сбрасывает lockoutSeconds); LoginView 1 (lockout-alert не виден дефолтно → после auth.lockoutSeconds=600 появляется + содержит «10 мин»). auth-store.spec.ts получил vi.mock('../../resources/js/api/client') — иначе axios.isAxiosError(plain Error) в jsdom возвращает false. PHPStan baseline регенерирован для +26 Pest TestCall warnings (накопительно). TODO (отдельные коммиты): IP-lockout 10/час через auth_log + email-уведомление при 3 неудачах (требует MailService + auth_log таблицы). Регресс зелёный: lint+type+format OK; vitest 149/149 за 12.31 сек (+4 от 145); vite build 886 ms; story:build 21/28 за 37.19 сек; Pint passed, PHPStan 0 errors; Pest 73/73 за 8.07 сек (+6 от 67, 246 assertions). Реестр v1.44→v1.45.
CLAUDE.md v1.35 от 08.05.2026 (поздний вечер). Изменения v1.35: AppLayout/AdminLayout user-chip из store + Logout-menu. Замены статичных mock'ов «ИП»/«Иван П.» (AppLayout) и «АО»/«Админ Оператор» (AdminLayout) на реальные данные из Pinia auth-store. userInitials computed: первая буква first_name + last_name → uppercase; fallback на 2 первые буквы email если ФИО пустые; '?' (AppLayout) / 'АО' (AdminLayout) если user=null. userShortName computed: «${first_name} ${last_name[0]}.» → fallback на first_name → fallback на email; 'Гость' (AppLayout) / 'Админ Оператор' (AdminLayout) если user=null. user-chip обёрнут в v-menu offset=8 с activator-slot — клик открывает v-list density=compact min-width=200: email disabled-row + divider + «Настройки» (RouterLink на /settings, AppLayout-only) или «Выйти из админки» (RouterLink на /dashboard, AdminLayout-only) + «Выйти» (mdi-logout) → handleLogout() async: auth.logout() (swallows API errors) → router.push('/login'). Vitest +3 в AppLayout.spec.ts (всего 145/145 за 11.01 сек): mountAppLayout получил параметр user: AuthUser | null = mockUser + setActivePinia + auth.user setup; tests: «user-chip показывает initials и shortName» (ИП + Иван П.), «при null user (гость) показывает ? и Гость», «при отсутствии first_name fallback на email». AppShell.spec.ts получил createPinia() в plugins (требуется AppLayout). Регресс зелёный: lint+type+format OK; vitest 145/145 за 11.01 сек (+3 от 142); vite build 855 ms; story:build 21/28 за 32.11 сек; Pest 67/67 за 6.16 сек. Реестр v1.43→v1.44.
CLAUDE.md v1.34 от 08.05.2026 (поздний вечер). Изменения v1.34: 2FA TOTP-verify — закрыт второй пункт auth-flow. Установлен pragmarx/google2fa:^9.0 для TOTP-генерации/проверки (RFC 6238). AuthController::login изменён: при totp_enabled=true НЕ делает Auth::login сразу, сохраняет auth.pending_user_id + auth.pending_remember в session, возвращает requires_2fa: true без полноценной session-auth. AuthController::verifyTwoFactor(VerifyTwoFactorRequest) — читает pending_user_id из session, верифицирует TOTP через Google2FA::verifyKey($secret, $code, window: 1) (окно ±1 = 30 сек до/после, компенсирует clock-skew); при success — Auth::login + regenerate session + clear pending. VerifyTwoFactorRequest валидирует ровно 6 цифр через regex. Маршрут /api/auth/2fa/verify публичный (нет полноценной session-auth до verify). Frontend auth-store::login ИЗМЕНЁН: при requires_2fa=true НЕ ставит user в state (иначе isAuthenticated=true и auth-guard пропустит на /dashboard минуя 2FA). verifyTwoFactor(code) action ставит user после успеха. TwoFactorView интегрирован: onMounted → если !requires2fa && !isAuthenticated → /login; submit → auth.verifyTwoFactor(codeFull) → /dashboard; при error — show error + clear code + focus first cell. userEmail из auth.user?.email. Pest +6 в tests/Feature/Auth/TwoFactorTest.php (всего 67/67 за 6.97 сек): login для 2FA-user НЕ создаёт session (/me возвращает 401) + verify с правильным TOTP завершает login + неверный код 422 + verify без login 422 + валидация формата 6 цифр + после verify /me возвращает user. Tests генерируют валидный TOTP через $google2fa->getCurrentOtp($secret). Vitest +3 auth-store (login с requires_2fa разделён на 2 теста + verifyTwoFactor success + reject), TwoFactorView spec получил setActivePinia + auth.requires2fa = true для bypass onMounted-redirect. PHPStan baseline регенерирован для +25 Pest TestCall warnings. Регресс зелёный: lint+type+format OK; vitest 142/142 за 10.75 сек; vite build 908 ms; story:build 21/28 за 31.28 сек; Pest 67/67 за 6.97 сек (194 assertions). Реестр v1.42→v1.43.
CLAUDE.md v1.33 от 08.05.2026 (поздний вечер). Изменения v1.33: Frontend auth integration. Установлены axios@^1.16.0 + pinia@^3.0.4 (через --legacy-peer-deps из-за Histoire vs Vite 8). Создан resources/js/api/client.ts — axios-инстанс с withCredentials: true + withXSRFToken: true (Sanctum SPA mode auto-XSRF из cookie). ensureCsrfCookie() забирает CSRF cookie через GET /sanctum/csrf-cookie один раз за сессию. Helpers extractValidationErrors (422) + extractErrorMessage (general). resources/js/api/auth.ts — типизированные API-методы login/register/me/logout с AuthUser interface. resources/js/stores/auth.ts — Pinia composition-store: user/loading/requires2fa refs + isAuthenticated computed + login/register/fetchMe/logout actions. logout() catch-swallow ошибок (UI-localout даже при backend-failure). LoginView/RegisterView подключены через useAuthStore — submit делает реальный POST через store, errors-state из validation, redirect на /dashboard или /2fa, loading-spinner на btn'ах. Auth-guard в router через router.beforeEach: meta.requiresAuth → check isAuthenticated → redirect /login с ?redirect= query; meta.guestOnly (login/register/forgot) → если уже залогинен → /dashboard. На первый переход вызывается fetchMe() для restore-session-state из cookie. Pinia зарегистрирован в app.ts через app.use(createPinia()). / теперь redirect на /dashboard (через guard уйдёт на /login если не залогинен). Vitest +10 (всего 139/139 за 10.11 сек): auth-store 7 (initial state + login success/reject + register + fetchMe success/401 + logout-swallow), router 5 переписан (login.guestOnly + 6 protected routes requiresAuth + 4 admin routes + 3 error routes без auth + redirect /dashboard→/login без auth с ?redirect= query). LoginView/RegisterView/router тесты получили createPinia в plugins. Регресс зелёный: lint+type+format OK; vitest 139/139; vite build (main app-chunk вырос до 153.64 KB включая axios+pinia+auth-store+api/auth — gzipped 54.54 KB) — 806 ms; story:build 21/28 за 31.73 сек; Pest 61/61 за 5.86 сек. Реестр v1.41→v1.42.
CLAUDE.md v1.32 от 08.05.2026 (поздний вечер). Изменения v1.32: Backend auth-flow через Sanctum SPA mode. Установлен laravel/sanctum:^4.3. SPA mode: cookie-based session-auth (не token-based). AuthController (login/register/me/logout) + LoginRequest/RegisterRequest Form Requests с валидацией. register требует accept_offer=true && accept_pdn=true (по ТЗ §1.5/§4.1, БЕЗ маркетингового click-wrap'а — расхождение #2 handoff vs ТЗ). User model расширен fillable: last_login_at, last_active_at. Auth-маршруты /api/auth/{login,register,me,logout} размещены в web.php (НЕ в api.php — Sanctum SPA нуждается в session-cookie middleware из web-группы). bootstrap/app.php без api.php. Pest +13 тестов для auth-flow (всего 61/61 за 6.22 сек): login успех + 2FA flag + неверный пароль + несуществующий email + заблокированный аккаунт + валидация format + last_login_at update + register success + duplicate email + accept_offer/accept_pdn required + /me 401 без auth + /me возвращает user + logout 200. Quirk: logout-test упрощён до проверки 200-status — Pest cookie-jar в test-runtime держит session между запросами, full session-invalidate проверяется через Pest browser-mode (отдельный коммит). phpstan-baseline регенерирован для +25 false-positive Pest $this-warnings. Регресс зелёный: Pint passed, PHPStan passed (level 5 + checkModelProperties); Vitest 129/129 за 9.59 сек; vite build OK 802 ms; story:build 21/28 за 30.39 сек; Pest 61/61 за 6.22 сек. Реестр v1.40→v1.41.
CLAUDE.md v1.31 от 08.05.2026 (поздний вечер). Изменения v1.31: Админка SaaS — 13-й экран, последний из основных в handoff (без landing). По liderra_v8_handoff/concepts/v8_admin.html + ТЗ §22 + schema v8.7 §3 (tenants) + §10 (saas_admin_audit_log). Layout AdminLayout.vue — отдельный sidebar теало-нуар с под-брендом ADMIN (red-error 10px JBM uppercase) + 4 nav-пункта (Тенанты 142 / Биллинг / Инциденты 3 / Система) + topbar с crumb «Админка → currentPage» + admin-user-chip (АО, Админ Оператор, error-color avatar). AppShell расширен: meta.layout='admin' → AdminLayout. AdminTenantsView.vue — page-head со stats (всего/активны/trial/просрочка/выручка JBM tnum) + Экспорт-btn + filter-bar с search-input + Статус/Тариф фильтры + v-data-table 7 колонок (Тенант с двухстрочным name/inn / Статус-chip / Тариф / Баланс ₽ JBM с error-color при <0 и medium-emphasis при 0 / Желаем×факт «12 × 11» / MRR / Активность). mockTenants.ts — 7 mock-tenants (3 active / 1 trial / 1 overdue / 1 suspended / 1 enterprise) + AdminStats (142 total / 128 active / 9 trial / 5 overdue / 1248600 ₽ revenue). 4 статуса с tonal-chip разного цвета (success/info/warning/error). AdminPlaceholderView.vue — универсальный для Биллинг/Инциденты/Система с описаниями из route.meta.description ссылающимися на schema (incidents_log §9 / system_settings §10). Маршруты: /admin redirect → /admin/tenants, /admin/tenants (полный) + /admin/billing|incidents|system (placeholder). Vitest +11 (всего 129/129 за 10.02 сек): заголовок «Тенанты» + 5 stats (142/128/9/5/выручка) + 7 колонок таблицы + 7 mock-rows + первая строка Окна Москва ИНН + Активен + overdue с -1200 + trial 4 дня + suspended + search-input placeholder + фильтр «Натяжные» оставляет 1 строку + Экспорт + Статус: Все / Тариф: Все. Stories +2 (AdminLayout + AdminTenantsView). web.php: новые admin-routes покрыты Route::fallback (без явных Route::view). Регресс зелёный: lint+type+format OK; vitest 129/129; vite build (admin views в lazy-chunks; main app-chunk 104.99 KB) — 763 ms; story:build 21 story / 28 variants за 30.32 сек; Pest 48/48 за 4.89 сек. Реестр v1.39→v1.40.
CLAUDE.md v1.30 от 08.05.2026 (поздний вечер). Изменения v1.30: ErrorView (404/403/500) — 12-й экран. По liderra_v8_handoff/concepts/v8_errors.html. Универсальный компонент с конфигурацией через route.meta.errorCode. Layout: тёмный full-bleed (теало-нуар #012019 bg), top-brand «Лидерра.» в шапке, центрированный контент с err-code 96px JBM monospace + accent на средней цифре + h2 title + desc + 2-action btn-row + опциональные status-list (только 500) и err-id (REQ-/INC-) с copy-btn (только 403/500). Каждый из 3 кодов (404/403/500) имеет уникальные actions: 404 «На дашборд + Назад» (router.back), 403 «На дашборд + Написать в поддержку» (mailto), 500 «Попробовать снова» (location.reload) + «Статус сервиса» (https://status.liderra.app) + status-list (API/Telegram/YooKassa). 500 показывает 3 status-pills с цветом (success/warning/error). copyRequestId через navigator.clipboard.writeText. AppShell расширен: meta.layout='error' → рендерит RouterView напрямую без AppLayout/AuthLayout (ErrorView сам предоставляет v-app). Маршруты: /403, /500, и catch-all /:pathMatch(.*)* в Vue Router (404 для всех неизвестных путей). web.php: Route::view('/403', 'welcome'), Route::view('/500', 'welcome') + Route::fallback(fn () => view('welcome')) (срабатывает после всех явных + runtime-route'ов от Pest, не перехватывает /_test/). Vitest +8 (всего 118/118 за 9.39 сек): 404 default + 403 с REQ-3F8A2-0007 + 500 с INC-2026-0507-0034 + status-list (API · OK / Telegram · деградация) + 404 actions (На дашборд / Назад) + 403 actions с mailto-link + 500 actions с status link + brand-блок + 404 НЕ содержит REQ/INC/status-list. Тесты используют stubs:{ VApp/VMain } как passthrough divs (layout-injection не нужен). Story ErrorView.story.vue 1 variant. Регресс зелёный: lint+type+format OK; vitest 118/118; vite build (ErrorView lazy-chunk 3 wrapper-route'а ссылаются на тот же chunk; main app-chunk 101.01 KB упал на 7 KB благодаря shared chunk'ам); story:build 19 stories / 26 variants за 30.96 сек; Pest 48/48 за 4.88 сек. Реестр v1.38→v1.39.*
CLAUDE.md v1.29 от 08.05.2026 (поздний вечер). Изменения v1.29: ReportsView — 11-й экран. По liderra_v8_handoff/concepts/v8_reports.html + ТЗ §6.6 + CTO-6 (retry 3/7д) + CTO-7 (квота 3 одновременных). Структура: page-head (заголовок + page-stats «очередь 2/3 · обработано за месяц 38 · средний размер 2.4 MB») + form-card (Запросить отчёт): 4 type-cards radio-grid (Сделки детально / Менеджеры / Источники / Биллинг) с active-state primary-bg ivory-tint + period с/по date-fields + Проект/Менеджер v-select + 4 fmt-кнопки (CSV/XLSX/JSON/PDF) с flat/outlined-toggle + quota-banner v-alert info с CTO-6/CTO-7 значениями + Запустить/Сброс. Jobs-list panel: panel-h «Сгенерированные отчёты» + «все 38 →»; 5 job-rows в grid-layout (icon+info+chip+actions): icon mdi-check-circle/progress-clock/clock/alert-circle (color по статусу), title + meta (FORMAT · size · rows · timeText, для failed +«N/3 попытки · ошибка X»), v-progress-linear для running 62%, status-chip tonal, actions: Скачать (done) / Повторить (failed.attempt<3) / Отменить (queued) / Удалить (done|failed). composables/mockReports.ts: типы (deals/managers/sources/billing × csv/xlsx/json/pdf × queued/running/done/failed), 5 mock-jobs с разными состояниями, REPORT_TYPES + REPORT_FORMATS массивы для UI, MOCK_QUOTA. Маршрут /reports (lazy) в router и web.php. Vitest +12 (всего 110/110 за 9.38 сек): заголовок + page-stats + 4 type-cards + дефолт «Сделки» active + 4 формата + quota «2 из 3» + «3 попыток retry» + «7 дней» + 5 job-rows + done «Готов» + Скачать-aria + running «62%» + progressbar role + queued «В очереди» + Отменить-aria + failed «Ошибка» + «S3 timeout» + Повторить-aria + клик-переключение active. Регресс зелёный: lint+type+format OK; vitest 110/110; vite build (ReportsView lazy-chunk; main 108.19 KB) — 706 ms; story:build 18 stories / 25 variants за 30.77 сек; Pest 48/48 за 4.58 сек. Реестр v1.37→v1.38.
*CLAUDE.md v1.28 от 08.05.2026 (поздний вечер). Изменения v1.28: SettingsView — 10-й экран. По liderra_v8_handoff/concepts/v8_settings.html. Layout: sidebar tabs-rail (md=3 v-list nav с mdi-icon на пункте) + content-pane (md=9 v-card outlined min-height 480px); activeTab ref переключает рендер. 8 вкладок: 4 реализованы (Профиль/Безопасность/API и Webhook/Уведомления), 4 placeholder (Проекты/Команда/Интеграции/Тихие часы) с PlaceholderTab и v-alert «В разработке».
ProfileTab.vue: v-avatar 80px + Сменить-btn + 5 v-text-field (Полное имя/Email disabled+hint про support/Телефон/Тайм-зона/Роль disabled) в 2-column grid + Сохранить/Отмена.SecurityTab.vue: 3 cards: Пароль (последняя смена + Сменить-btn) + 2FA (success-chip «включена» + текст про TOTP + Перегенерировать резервные коды + Отключить 2FA) + Активные сессии (3 mock с device/location/when + «эта сессия» chip + Завершить-btn для не-current).ApiTab.vue: API-ключ password-field с eye-toggle + Копировать/Перегенерировать + Webhook-секция (URL + Signing secret HMAC + Сохранить/Тестовый webhook). Текст про дедуп(tenant_id, source_crm_id)24ч и антифрод по phone — соответствует schema v8.7 + ТЗ §10.8.1.NotificationsTab.vue: Матрица 8×3 соответствуетusers.notification_preferencesJSONB по 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_enabledBOOLEAN в 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.vue1 variant. - Регресс зелёный: lint+type+format OK; vitest 98/98; vite build (SettingsView lazy-chunk) — 750 ms; story:build 17 stories / 24 variants за 31.7 сек; Pest 48/48 за 5.03 сек. Реестр v1.36→v1.37.*
CLAUDE.md v1.27 от 08.05.2026 (поздний вечер). Изменения v1.27: BillingView — финансовый экран биллинга и тарифов. По liderra_v8_handoff/concepts/v8_billing.html. Структура: page-head (заголовок + page-stats с tnum-числами кошелька/лидов/runway-дней + Пополнить-btn) + pending-banner v-alert info (1 платёж в обработке через ЮKassa с auto-cancel timeout) + wallet-row из 3 cards (Кошелёк ₽ — primary теало-нуар card с LIVE-chip + Пополнить/Автопополнение btn'ы; Баланс лидов 285 ГЦК + средняя цена; Тариф «Команда» 990₽/мес + 3 фичи + Сменить-btn) + transactions panel (4-tab v-btn-toggle: Все/Пополнения/Списания/Возвраты) + v-data-table 5 колонок (Дата/Операция/ID/Статус-chip/Сумма с +/− знаком и цветом) + invoices panel (Реестр-XLSX-btn + 4 строки PDF/1С 8.3 XML). composables/mockBilling.ts: BillingTransaction (8 mock-транзакций со статусами pending/completed/rejected, types: topup/lead_charge/refund/tariff_charge), Invoice (4 mock invoices с format pdf|xml_1c83), PendingPayment, BILLING_TABS (4 среза с types-array). Соответствуют схеме v8.7 §4.4 balance_transactions / §4.5 invoices. Маршрут /billing (meta.layout='app', lazy-import) в router и web.php. Vitest +11 (всего 90/90 за 7.96 сек): заголовок + page-stats (regex для nbsp 14 250 ₽) + pending-banner + 3 wallet-cards + 3 фичи тарифа + 4 tabs + дефолт «Все» все 8 строк + format «+ 5 000 ₽» / «− 6 600 ₽» / «— 0 ₽» rejected + invoices section 4 rows + PDF/1С 8.3 XML labels. Story BillingView.story.vue. Регресс зелёный: lint+type+format OK; vitest 90/90; vite build (BillingView lazy-chunk) — 688 ms; story:build 16 stories / 23 variants за 32.16 сек; Pest 48/48 за 4.89 сек. Реестр v1.35→v1.36.
CLAUDE.md v1.26 от 08.05.2026 (поздний вечер). Изменения v1.26: DealDetailDrawer — правая панель с деталями сделки. Открывается при click по строке в DealsView или по карточке в KanbanView. По liderra_v8_handoff/concepts/v8_deal_card.html. Структура: hero (Сделка #id eyebrow + name h5 + close-icon-btn + phone-link tel: + clock-icon с относительным временем + status-chip с colorHex), section «Параметры» (2-column grid: Проект/Стоимость лида/Менеджер с avatar/Источник), section «Активность» (timeline 6 событий с iconified vertical-line connector). composables/mockDealEvents.ts — mock activity-events 6 типов: deal.created/balance_charged/assigned/viewed/status_changed/commented (соответствуют ActivityLog event-константам по схеме v8.7 §10.2). DealsView и KanbanView интегрируют drawer через v-model:open + :deal props; click handler в DealsView через @click:row v-data-table, в KanbanView через @open-deal event от KanbanCard. Vitest quirk: DealsView/KanbanView содержат теперь v-navigation-drawer, который требует layout-injection от v-app/v-layout. В Vitest vite-plugin-vuetify auto-import не работает (только в build) — v-layout/v-app не резолвятся компонент-резолвером. Решение: stub'ить DealDetailDrawer в тестах DealsView/KanbanView (stubs: { DealDetailDrawer: true }); сам Drawer тестируется отдельно с stub'ом VNavigationDrawer как passthrough <div v-if="modelValue"><slot /></div> чтобы slot-content (hero/params/timeline) рендерился в DOM. Vitest +8 (всего 79/79 за 7.57 сек): монтаж + open=false скрывает + deal=null без content + hero (name+id) + tel:link + status-chip + параметры (project/cost/manager) + timeline 6 events + emit update:open(false) на close. Story DealDetailDrawer.story.vue 2 variants (status=new / paid). Регресс зелёный: lint+type+format OK; vitest 79/79; vite build (DealDetailDrawer инлайнен в DealsView+KanbanView lazy-chunks; main app-chunk 107.16 KB) — 761 ms; story:build 15 stories / 22 variants за 31.55 сек; Pest 48/48 за 4.99 сек. Реестр v1.34→v1.35.
CLAUDE.md v1.25 от 08.05.2026 (поздний вечер). Изменения v1.25: Kanban DnD — drag-and-drop карточек между колонками. Установлен vuedraggable@^4.1.0 (обёртка SortableJS@1.14, поддержка Vue 3 — peerDep vue ^3.0.1; через --legacy-peer-deps из-за того же Histoire vs Vite 8 конфликта). KanbanColumn.vue обёрнут вокруг карточек: <draggable v-model="localDeals" group="kanban-deals" item-key="id" ghost-class="ghost-card" drag-class="drag-card" animation="150" @change="onDraggableChange"> + <template #item="{ element }"> рендерит <KanbanCard>. Добавлен <template #footer> с empty-state «пусто · перетащите сюда» (только когда deals.length === 0). Visual-эффекты: .ghost-card (opacity 0.4 + bg #E1EEEA — placeholder при drag) + .drag-card (opacity 0.95 + transform rotate 1deg — текущая карточка при перетаскивании). KanbanColumn теперь принимает @change event с DraggableChangeEvent (added/removed/moved discriminated union) и пробрасывает родителю + update:deals для v-model паттерна. KanbanView.vue переписан с const allDeals (readonly из MOCK_DEALS) на reactive<Record<slug, MockDeal[]>> — отдельный массив для каждой колонки (vuedraggable v-model требует независимые arrays). Deep-copy через {...d} чтобы не мутировать MOCK_DEALS const при DnD. onColumnChange(targetSlug, event) обработчик: при event.added — синхронизирует event.added.element.statusSlug = targetSlug (на production будет POST /api/deals/{id}/transition с проверкой allowed-переходов). 'removed' и 'moved' события обрабатываются автоматически через v-model. Vitest +1 (всего 71/71 за 7.48 сек): новый тест в KanbanView.spec.ts эмулирует событие added через paidCol.vm.$emit('change', { added: { element, newIndex } }) и проверяет что dealToMove.statusSlug обновился с 'new' → 'paid'. Регресс зелёный: lint+type+format OK; vitest 71/71; vite build (KanbanView lazy-chunk вырос до 180.53 KB — добавилась SortableJS-обёртка, но грузится только на /kanban) — 642 ms; story:build 14 stories / 20 variants за 30.45 сек; Pest 48/48 за 4.88 сек. Реестр v1.33→v1.34.
CLAUDE.md v1.24 от 08.05.2026 (поздний вечер). Изменения v1.24: KanbanView — альтернативный вид сделок (по статусам). 14 колонок (lead_statuses) с группировкой mock-сделок по statusSlug, horizontal scroll. Структура: KanbanCard.vue (компактная карточка с name/phone/project/cost/manager-avatar, click → emit('open',id)) + KanbanColumn.vue (header с цветной полосой по colorHex статуса + name + count + total ₽; body с v-for карточек + empty-state «пусто»; CSS-var --accent для accent-border) + KanbanView.vue (page-head + 14 columns в .kanban-board flex с overflow-x:auto; кастомный scrollbar теало-нуар). DnD НЕ реализован на MVP (отдельный коммит — vue-draggable-next или @vueuse/integrations/useSortable; на API-стороне — PATCH /api/deals/{id} {status_slug}). Маршрут /kanban (meta.layout='app', lazy-import) в router и web.php. Vitest +14 (всего 70/70 за 7.37 сек): KanbanCard 3 (рендер data + manager initials + emit open(id) на click), KanbanColumn 5 (header с nameRu+count + total форматирование + «—»+«пусто» при empty + accent-border colorHex case-insensitive + проброс openDeal от карточки), KanbanView 6 (заголовок «Канбан» + ровно 14 columns + правильный status в каждой + page-stats + кнопка Новая сделка + текст про DnD-предупреждение). Stories +3 с 5 variants (KanbanCard 1, KanbanColumn 3 — Новые/Оплачено/пусто, KanbanView 1). Регресс зелёный: lint+type+format OK; vitest 70/70 за 7.37 сек; vite build (KanbanView lazy-chunk) — 633 ms; story:build 14 stories / 20 variants за 31.17 сек; Pest 48/48 за 5.06 сек. Реестр v1.32→v1.33.
CLAUDE.md v1.23 от 08.05.2026 (поздний вечер). Изменения v1.23: DealsView — центральный экран CRM (список сделок). resources/js/views/DealsView.vue: page-head с заголовком «Сделки» + 4-полевые stats (новых лида с утра / всего / в работе / ждут оплату с tnum-числами) + actions (Экспорт + Новая сделка); filter-bar с v-btn-toggle 5-вариантным chiprow (Все/Активные/Ждут оплату/Закрытые/Невалидные с counts через v-chip x-small) + search-input (clearable, prepend-icon mdi-magnify, фильтр по name/phone/project); v-data-table с показ всех колонок (Лид с avatar+name+phone, Статус с tonal-chip и colorHex из lead_statuses, Проект, Менеджер с avatar, Стоимость через Intl.NumberFormat('ru-RU') JBM tnum, Время через formatRelative мин/ч/д назад). show-select для bulk-actions (TODO). Empty-state когда фильтры дают 0 строк. mockDeals.ts — 12 mock-сделок с разнообразием статусов (по 1-2 на каждый из 14 lead_statuses) + DEALS_TABS массив со срезами {id, label, slugs[]|null}. Источник статусов — composables/leadStatuses.ts (snapshot из db/schema.sql:2130, не из BRANDBOOK §3.6). Маршрут /deals (meta.layout='app', lazy-import) в router и web.php. Vitest +8 тестов (всего 56/56 за 5.66 сек): заголовок + page-stats + 5 chiprow-tabs + дефолтный 'active'-фильтр показывает только active-сделки + Экспорт/Новая сделка + 6 колонок таблицы + format 2 400 ₽ (regex \s+ — \s в JS regex matches U+00A0/U+202F nbsp из Intl.NumberFormat, не нужен literal nbsp + ESLint warning) + format «7 мин назад». Story DealsView.story.vue. Регресс зелёный: lint+type+format OK; vitest 56/56; vite build (DealsView lazy-chunk 87.54 KB — v-data-table большой, но это chunk загружается только на /deals; main app-chunk 106.83 KB) — 538 ms; story:build 11 stories / 15 variants за 31.93 сек; Pest 48/48 за 4.96 сек. Реестр v1.31→v1.32.
CLAUDE.md v1.22 от 08.05.2026 (поздний вечер). Изменения v1.22: Dashboard charts — ActivityChart + FunnelChart, Dashboard закрыт по дизайну v8_dashboard.html. resources/js/components/charts/ActivityChart.vue — линейный чарт на native SVG (без chart-library, чтобы не тащить +400KB зависимость для статичных дашборд-графиков): viewBox 800x220, props (points 7-значений, max 60, labels, title, subtitle), Y-grid 5 линий с подписями (0/15/30/45/60), area-gradient под линией, основная line stroke #0A1319 1.7px, 7 circle-точек (предпоследняя выделена primary teal r=4.2 как «сегодня (max)» по handoff), 3 button-tabs (Принято/Оплачено/Отказ — выбор активного метрика-tab, на API будет менять props.points), легенда с 3 ldot-индикаторами. resources/js/components/charts/FunnelChart.vue — воронка на 14 статусов: segmented bar (горизонтальный, height 12px, каждый segment flex=count, background=colorHex) + funnel-list (2-column grid с цветным dot + name + count, отсортирован по убыванию count'а как в handoff). Источник статусов — resources/js/composables/leadStatuses.ts (snapshot из db/schema.sql:2130, НЕ из BRANDBOOK_v2 §3.6: расхождение #1 handoff vs ТЗ из реестра v1.13 — handoff содержит 14 «обобщённых» статусов с пересечением всего 2 (Переговоры + Оплачено) с реальными slug'ами). 14 правильных статусов: new/viewed/worked/base/missed/negotiations/waiting_payment/partnership/paid/closed/test_drive/hot/replacement/final_missed с corresponding colorHex из seed'а. Vitest +13 (всего 48/48 за 5.5 сек): ActivityChart 6 (монтаж + 3 tabs + 7 circles + «сегодня» подпись + кастомные points + легенда 3 dots), FunnelChart 7 (заголовок + 14 segments + 14 list-items + всех 14 nameRu присутствуют И «Думает»/«Спам» из handoff отсутствуют — регрессия защищена + сортировка по убыванию (paid 45 первый) + colorHex применяется + total = sum counts). Stories +2 с 3 variants каждый: ActivityChart (default mock / ровный рост / спад), FunnelChart (default 247 / пустая / концентрация на paid). Quirk Vue SFC compiler: withDefaults(..., { counts: () => DEFAULT_COUNTS }) падает с checkInvalidScopeReference — module-level const'ы запрещены внутри factory; обходится инлайнингом литерала. Регресс зелёный: lint+type-check+format OK; vitest 48/48; vite build (DashboardView lazy-chunk 14.9→21.17 KB с двумя чартами) — 473 ms; story:build 10 stories / 14 variants за 30.43 сек; Pest 48/48 за 5.10 сек. Реестр v1.30→v1.31.
CLAUDE.md v1.21 от 08.05.2026 (поздний вечер). Изменения v1.21: AppLayout + DashboardView — реализован default-layout приложения для авторизованных пользователей. resources/js/layouts/AppLayout.vue — v-navigation-drawer (theme=dark, color=secondary теало-нуар, 240px) с brand-block + nav-tree из 8 пунктов в 3 группах («Работа»: Дашборд/Сделки 247/Канбан/Напоминания 12; «Финансы»: Биллинг/Отчёты; «Команда»: Менеджеры 4/Настройки) + v-app-bar (56px, color=surface) с crumb «Рабочая область → текущая страница», search-trigger ⌘K (заглушка disabled), notification icon с pip, user-chip («ИП», «Иван П.»). На mobile (md<) drawer toggleable через v-app-bar-nav-icon. resources/js/views/DashboardView.vue — page-head с приветствием «Доброе утро, Иван» + page-meta (новых лидов / сегодня / вчера / средняя стоимость) + range-toggle 4-варианта (Сегодня/7 дней/30 дней/Период) через v-btn-toggle; KPI-row из 4 cards: 3 outlined (Получено лидов 247 +12.3%, Конверсия 18.4% +2.1pp, Активные проекты 8/10 «2 свободно тариф Команда») + 1 hero balance (теало-нуар bg, «14 250 ₽», runway-bar 4/7 заполненных сегментов). AppShell.vue упрощён: layout-mapper только мапит route.meta.layout ('app' default → AppLayout, 'auth' → AuthLayout). Маршрут /dashboard (meta.layout='app') добавлен в router и web.php. histoire.setup.ts расширен 8-ю app-stub-маршрутами (dashboard/deals/kanban/reminders/billing/reports/managers/settings) — иначе AppLayout warn'ит на mount в memory-router. Vitest +11 тестов (всего 35/35 за 4.92 сек): AppLayout 6, DashboardView 5, AppShell.spec.ts переписан под новый layout-mapper. Регресс зелёный: lint+type-check+format OK; vitest 35/35; vite build (DashboardView lazy-chunk 14.9 KB, app chunk вырос до 105 KB из-за импорта AppLayout) — 458 ms; story:build 8/8 за 28.97 сек (+ AppLayout + DashboardView); Pest 48/48 за 4.88 сек. Реестр v1.29→v1.30.
CLAUDE.md v1.20 от 08.05.2026 (поздний вечер). Изменения v1.20: все 5 auth-экранов реализованы (закрыта вся секция #form- из v8_login.html). Добавлены 4 view'а: RegisterView.vue (email + password со strength-meter + 2 click-wrap'а — оферта/ПДн, БЕЗ 3-го «маркетинг» из handoff: следуем ТЗ §1.5/§4.1, расхождение #2 реестра v1.13), TwoFactorView.vue (6 input-cell с auto-focus вперёд при вводе цифры, Backspace назад при empty, paste 6 цифр заполняет все), ForgotPasswordView.vue (email + alert «5 попыток / 15 мин» по ТЗ §1.7), RecoveryCodesView.vue (8 одноразовых кодов в 2-column grid + Скачать .txt через Blob/URL.createObjectURL + Копировать через navigator.clipboard + warning о невозможности повторного просмотра). 4 новых маршрута в router/index.ts (/register, /2fa, /forgot, /recovery) — все meta.layout='auth'. 4 stories в Histoire. Vitest +14 тестов (всего 24/24 за 3.29 сек): RegisterView 4 (вкл. assertion на отсутствие маркетингового click-wrap), TwoFactorView 3, ForgotPasswordView 3, RecoveryCodesView 4. Регресс зелёный: lint:vue + type-check + format:check OK; vitest 24/24 за 3.29 сек; vite build 5 lazy-chunks (LoginView 3.81 KB, RegisterView 9.53 KB, TwoFactorView 2.38 KB, ForgotPasswordView 1.89 KB, RecoveryCodesView 2.18 KB) + Vuetify-компоненты в отдельные chunks (VAlert/VForm/VCard/VTextField/VAvatar) — bundle-splitting работает; story:build 6/6 за 29.17 сек; Pest 48/48 за 4.85 сек. Реестр v1.28→v1.29.*
CLAUDE.md v1.19 от 08.05.2026 (поздний вечер). Изменения v1.19: первый реальный экран фазы 2 — Vue Router 4.6 + AuthLayout + LoginView. Установлен vue-router@^4.6.4 (через --legacy-peer-deps из-за того же Histoire vs Vite 8 конфликта). Создано: resources/js/router/index.ts (createWebHistory + lazy-imports + meta.layout='auth' для /login), resources/js/layouts/AuthLayout.vue (двухпанельный grid: brand-pane слева на теало-нуар с radial-gradient акцентами + form-pane справа на warm ivory), resources/js/views/auth/LoginView.vue (полная форма: v-text-field для email/password с autocomplete, eye-icon toggle, primary submit, divider, Yandex 360 SSO outlined-btn, RouterLink на /register и /forgot). AppShell.vue переписан как layout-mapper по route.meta.layout (default → v-app+v-app-bar+RouterView; 'auth' → AuthLayout). app.ts подключает router. routes/web.php — явные Route::view для /, /login, /register, /forgot, /2fa, /recovery (НЕ catch-all /{any?} — он перехватывал runtime-routes из Pest beforeEach и валил 5 тестов SetTenantContextTest). Vitest расширен: LoginView.spec.ts (5 тестов: монтаж + заголовок + кнопка/SSO + autocomplete + RouterLink'и), router.spec.ts (2 теста: маршрут /login + редирект /→/login), AppShell.spec.ts переписан под router-context (3 теста: default-layout + auth-layout-switch). Vitest 10/10 за 3 сек. Histoire setup расширен memory-router'ом (иначе RouterLink/useRoute падают). Story LoginView.story.vue. Регресс зелёный: lint:vue + type-check + format:check OK; vitest 10/10 за 3.01 сек; vite build 212 модулей за 383 ms (LoginView в отдельный chunk 43.5 KB JS / 51.7 KB CSS — lazy-import работает); story:build 2 stories за 29.94 сек; Pest 48/48 за 4.86 сек. Реестр v1.27→v1.28.
CLAUDE.md v1.18 от 08.05.2026 (поздний вечер). Изменения v1.18: фаза 2 по тулчейну закрыта 6/6 — добавлен #24 Histoire 1.0-beta.1 + @histoire/plugin-vue@1.0.0-beta.1. Установка через --legacy-peer-deps (Histoire 1.0-beta.1 заявляет peerDep vite ^7, у нас Vite 8 — runtime smoke OK). Конфиги: app/histoire.config.ts (HstVue plugin + Forest primary palette), app/resources/js/histoire.setup.ts (defineSetupVue3 регистрирует vuetify для каждой story). Первая story: app/resources/js/components/AppShell.story.vue (smoke). npm-scripts: story (dev), story:build (статическая сборка в .histoire/dist/), story:preview. .gitignore дополнен /app/.histoire/. Story:build smoke: 1 story / 1 variant за 30.25 сек, vendor.js содержит liderraForest + histoire.setup. Регресс зелёный: lint:vue ✅, type-check ✅, format:check ✅, vitest 3/3 за 2.98 сек, vite build 158 модулей за 334 ms, Pest 48/48 за 5.12 сек. Активно теперь 18 из 28 инструментов (9 фаза 0 + 8 фаза 1 + 6 фаза 2 — фаза 2 по тулчейну полностью закрыта). Реестр v1.26→v1.27, Tooling v1.6→v1.7.
CLAUDE.md v1.17 от 08.05.2026 (поздний вечер). Изменения v1.17: триггер фазы 2 выполнен — Vue 3 + Vuetify 3 + ESLint+Prettier+Vue + vue-tsc + Vitest установлены и работают. Активно 5 из 6 инструментов фазы 2 (без Histoire — отдельный коммит). Tailwind удалён (правило §5 п.2). Установлены: vue@3.5, vuetify@3.12, @vitejs/plugin-vue@6 (Vite 8 совместимость), vite-plugin-vuetify@2 (auto-import), sass-embedded, eslint@10 (flat-config), eslint-plugin-vue@10, @vue/eslint-config-typescript@14, eslint-config-prettier, prettier@3.8, typescript@6, vue-tsc@3.2, vitest@4.1, @vue/test-utils, jsdom, @vitest/coverage-v8. Конфиги: vite.config.js (vue + vuetify auto-import), vitest.config.ts (jsdom + setup file для ResizeObserver/IntersectionObserver/matchMedia stubs), eslint.config.js (flat-config), .prettierrc.json, tsconfig.json. Палитра Forest в resources/js/plugins/vuetify.ts (Teal/ivory/теало-нуар). resources/js/components/AppShell.vue — первый компонент: v-app + v-app-bar + v-card. welcome.blade.php обновлён под Vue mount. Vitest 3/3 за 2.8 сек. Build: 158 модулей за 386 ms (184 KB JS / 295 KB CSS). Lefthook job #8 (ESLint на staged .ts/.vue) добавлен в pre-commit. npm-scripts: lint:vue, format, format:check, type-check, test:vue. Реестр v1.25→v1.26.
CLAUDE.md v1.16 от 08.05.2026 (поздний вечер). Изменения v1.16: failed() callback в ProcessWebhookJob — после исчерпания 3 ретраев упавший job сохраняется в failed_webhook_jobs для ручного разбора и повторного запуска через админку. Создана модель App\Models\FailedWebhookJob. Запись через DB::table (не Eloquent) чтобы обойти RLS — failed-callback запускается вне транзакции воркера и без app.current_tenant_id. payload пишется через json_encode(JSON_UNESCAPED_UNICODE) — UTF-8 кириллица сохраняется. Sentry::captureException — TODO для production (на dev-стеке нет DSN). 3 новых Pest-теста: failed() пишет с webhookLogId, failed() БЕЗ webhookLogId (NULL ok), UTF-8 payload корректен. Pest 48/48 зелёные за 4.7 сек. Реестр v1.24→v1.25.
CLAUDE.md v1.15 от 08.05.2026 (поздний вечер). Изменения v1.15: pg_partman replacement — Artisan partitions:create-months --ahead=N (default 2) создаёт ежемесячные партиции для deals и supplier_lead_costs (синхронно по received_at). Идемпотентна (проверка через pg_class WHERE relkind='r' перед CREATE). Замена расширения pg_partman на native Windows-стеке (см. project_phase1_strategy.md). Запускать ежесуточно через Windows Task Scheduler / cron. Smoke-test на dev: 6 новых партиций (Nov 2026 - Jan 2027) + 12 skipped. 4 новых Pest-теста: создание на 8 мес вперёд, идемпотентность, --ahead=0, INSERT в новую партицию работает. Pest 45/45 зелёные за 4.9 сек. Реестр v1.23→v1.24.
CLAUDE.md v1.14 от 08.05.2026 (поздний вечер). Изменения v1.14: Биз-19 закрыт — DuplicateDetector антифрод-сервис интегрирован в ProcessWebhookJob. При создании НОВОЙ сделки ищется master по (tenant_id, phone) в окне 24 ч (received_at >= NOW() - INTERVAL '24 hours', WHERE duplicate_of_id IS NULL). Если найден — новой сделке проставляется duplicate_of_id = master.id, баланс НЕ списывается, SupplierLeadCost НЕ создаётся. ActivityLog пишется с context.duplicate_of=master.id. Окно фиксированное 24 ч (§10.8.1, не настраивается на MVP). 4 новых Pest-теста: master в окне 24ч → дубль; master старше 24ч → НЕ дубль; изоляция по tenant_id; ActivityLog context.duplicate_of. Pest 41/41 зелёные за 4.1 сек. DI через app(DuplicateDetector::class) внутри handle (не в сигнатуре — для совместимости с прямым вызовом из тестов). Реестр v1.22→v1.23.
CLAUDE.md v1.13 от 08.05.2026 (поздний вечер). Изменения v1.13: Webhook PoC завершён — закрыты все TODO в ProcessWebhookJob. Добавлены 5 Eloquent-моделей: BalanceTransaction (списание lead_charge -1 при создании сделки, type-константы), ActivityLog (event=deal.created с context.source=webhook, event-константы), RejectedDealsLog (zero_balance ветка вместо Log::info), SupplierLeadCost (composite PK, snapshot cost_rub из suppliers, supplier_id resolved через project_suppliers m2m), Supplier (минимальная для FK target). Job::handle() реструктурирован в chargeNewLead() + logRejection() + resolveSupplierId() + upsertDeal(). SupplierLeadCost создаётся только если у проекта есть активный supplier через project_suppliers (graceful skip + Log::warning иначе — TODO для production: SystemSetting fallback). 6 новых Pest-тестов: BalanceTransaction lead_charge, дубль НЕ создаёт BalanceTransaction, ActivityLog event=deal.created, RejectedDealsLog reason=zero_balance, SupplierLeadCost snapshot cost_rub, SupplierLeadCost graceful skip. Pest 37/37 зелёные за 3.9 сек. Pint+Larastan чисто (ide-helper:models регенерирован для 5 новых моделей). Реестр v1.21→v1.22.
CLAUDE.md v1.12 от 08.05.2026 (поздний вечер). Изменения v1.12: CTO-17 addendum — schema.sql v8.6 → v8.7 + pivot архитектуры upsert на advisory lock. PoC App\Jobs\ProcessWebhookJob поймал FK violation в webhook_dedup_keys: §5.5 v8.6 спецификация делает INSERT в dedup_keys ДО INSERT в deals, а FK был immediate. Сначала добавил DEFERRABLE INITIALLY DEFERRED (schema v8.7) — в bare-транзакции production worker'а работает. Но Pest-тесты с DatabaseTransactions trait всё равно падали: PG проверяет deferred constraints на RELEASE SAVEPOINT (внутренняя DB::transaction() Job'а становится savepoint при наличии outer-txn от теста), не на outer COMMIT. Воспроизведено standalone PHP-скриптом — это PG-семантика subtransactions. Финальный паттерн: pg_advisory_xact_lock сериализует concurrent webhook'и с тем же (tenant_id, vid) → SELECT в dedup_keys атомарен → INSERT deal первым (FK immediate OK) → INSERT dedup_key. Работает identically в любой вложенности транзакций. DEFERRABLE FK сохранён в schema как defense-in-depth для batch-импортов без savepoint. Создан backend-стек: Deal/WebhookDedupKey Eloquent-модели, DealFactory (composite PK setKeysForSaveQuery override), ProcessWebhookJob (advisory-lock upsert), 12 новых Pest-тестов (DealModelTest 6 + ProcessWebhookJobTest 6). Pest полный прогон 31/31 зелёные за 2.7 сек. CHANGELOG_schema §W (две стадии решения), narrative §2.4/§5.5/§6.5/§11 синхронизированы. Реестр v1.20 → v1.21.
CLAUDE.md v1.11 от 08.05.2026 (поздний вечер). Изменения v1.11: закрыт техдолг v1.10 — narrative ТЗ синхронизирован под schema v8.6 двустадийный dedup. In-place правки в 8 точках: §2.4 (поток webhook), §5.5 (PHP-код ProcessWebhookJob — раздельные INSERT/UPDATE через webhook_dedup_keys + RETURNING is_new), §5.6 (таблица крайних случаев — дубль vid), §6.5 (SQL-пример идемпотентности импорта — двустадийный INSERT в dedup_keys → INSERT/UPDATE deals), §11 (DDL deals — UNIQUE INDEX → INDEX; добавлен DDL webhook_dedup_keys с composite FK на deals(id, received_at) ON DELETE CASCADE + RLS), §20.12.3 (поток в транзакции для supplier_lead_costs), §21.1 (формулировка «списание не происходит при дублях»), §27.1 (итог по идемпотентности). Версия narrative не бампается (как для L13-гигиены 3a9ed71). Реестр Открытых вопросов v1.19→v1.20.
CLAUDE.md v1.10 от 08.05.2026 (поздний вечер). Изменения v1.10: backend multi-tenant фундамент развёрнут — schema.sql v8.5 → v8.6 (CTO-17 закрыт фиксом); php artisan migrate:fresh прошёл за 870 ms, БД liderra содержит 68 таблиц (включая 16 партиций), 36 RLS-policies. Заменено: §0 ссылки (schema v8.6, реестр v1.19, ТЗ техдолг §15-16 — фактически §2.4/5.5/6.5/11/20.12.3/21.1/27.1, hygiene закрыта в v1.11), §2 метрики (54→55 таблиц, 91→92 индекса, 35→36 RLS, 12→13 триггеров, 4→5 функций), §6 (фаза 1 фундамент развёрнут, добавлены deployment-скрипты db/00_create_roles.sql + db/02_grants.sql). Реестр Открытых вопросов v1.18→v1.19 (CTO-17 закрыт). CHANGELOG_schema.md дополнен записью §X.
CLAUDE.md v1.9 от 08.05.2026 (поздний вечер). Изменения v1.9: фаза 1 по тулчейну закрыта — добавлены #15 squawk v2.51.0 + #16 pgFormatter v5.9. Обновлены §0 (ссылка на Tooling v1.6), §6 «Текущая фаза» (15→17/28 активных), фактический пакет фазы 1 теперь полный (8/9, без #17 pg_partman — Windows native стек). Squawk: pre-commit hook на staged *.sql + .squawk.toml (9 правил исключено). pgFormatter: только ручной режим через npm run format:sql:check / npm run format:sql — авто-fix хук недопустим (diff vs schema.sql 3255 строк, ручной стиль). Tooling v1.5→v1.6.
CLAUDE.md v1.8 от 08.05.2026 (поздний вечер). Изменения v1.8: фаза 1 в работе — обновлены §6 «Текущая фаза» (15/28 активных, 6 фазы 1: #10 Boost+#11 Pint+#12 Larastan+#13 Roave/SA+#14 IDE Helper+#18 Pest 4) и §7 «Laravel Boost» (переписан под фактическую установку 08.05: wizard сломан на кириллице → manual setup, boost.json минимален, Roster auto-detect делает «отключение guidelines» избыточным, путь Vuetify guideline скорректирован на app/.ai/guidelines/vuetify.md). Tooling v1.4→v1.5.
CLAUDE.md v1.7 от 08.05.2026 (поздний вечер). Изменения v1.7: закрыт техдолг v1.6 — narrative синхронизирован под Laravel 13. Точечные правки в трёх файлах (без изменения версии каждого документа): CRM_bp-gr_Инструкция_v8_5.md:6219, Vybor_oblaka_v8_3.md:3, Админка_SaaS_v8_2.md:103 — Laravel 11 → Laravel 13. Не трогалось намеренно (исторические записи): Объединённый_конспект.md:149 (Часть I — фиксация решений v8.0 на 03.05.2026), Открытые_вопросы_v8_3.md:354 (фиксация результата аудита). Открытые_вопросы v1.17→v1.18.
CLAUDE.md v1.6 от 08.05.2026 (поздний вечер). Изменения v1.6: переоткрыт стек §2 — Laravel 11 → Laravel 13 после обнаружения, что composer create-project без ^11 подтянул 13.7. Live-проверка показала совместимость 5 ключевых плагинов (Boost, Larastan, Pest, IDE Helper, Pint). Заказчик принял Laravel 13 как latest stable. Техдолг: синхронизация narrative ТЗ + Vybor_oblaka + Админка_SaaS под Laravel 13 — отдельная задача для следующих сессий. Открытые_вопросы v1.16→v1.17 (новый блок), Tooling v1.3→v1.4 (Laravel 13 в §0).
v1.5 от 08.05.2026 (поздний вечер). Изменения v1.5: переоткрыт+закрыт CTO-12 — Pest 3 → Pest 4 (§3.2 строка 18, §7 п.5) после live-проверки на стеке (smoke-test 2/2 на default-тестах Laravel 11, 281 ms). Бонус Pest 4: browser testing без Dusk, stress, mutation v2. Tooling v1.2→v1.3, Открытые_вопросы v1.15→v1.16.
v1.4 от 08.05.2026 (вечер). Изменения v1.4: native-стек фазы 1 (§6, §7 п.3) — Docker/Sail/WSL2 невозможны на OpenStack-VPS без nested virtualization. PostgreSQL 16 + Memurai (Redis 7-совм.) + native PHP. Tooling v1.1→v1.2.
v1.3 от 08.05.2026: закрыт Диз-1 — HTML-прототипы покрыты handoff'ом Платона (§0, §6). Открытые_вопросы v1.14→v1.15. Открытых ⏸ продуктовых: 4 (Б-1 P0 + Диз-3/DO-2/DO-4 P1, все ждут Б-1).
v1.2 от 08.05.2026: закрыт CTO-12 — выбран Pest 3 (§3.2 строка 18, §7 п.5). Tooling v1.0→v1.1, Открытые_вопросы v1.13→v1.14.
v1.1 от 08.05.2026: ребрендинг Лидпоток→Лидерра; brandbook v1.1 удалён, источник — BRANDBOOK_v2.md из handoff Платона; добавлен handoff в §0; §2 палитра Forest; §6 — 13 концептов в web/v8/.