Commit Graph

261 Commits

Author SHA1 Message Date
Дмитрий 73e64128dc phase2(2fa-setup): wizard init+confirm+disable+regenerate в SettingsView/SecurityTab
- TwoFactorSetupController (auth:sanctum): /api/2fa/{init,confirm,disable,regenerate-recovery-codes}
- init секрет в session (не в БД), QR-URL otpauth://; confirm активирует 2FA + 8 recovery codes
- disable/regenerate требуют password-confirmation
- User.casts: totp_secret => encrypted

Schema v8.7→v8.8: users.totp_secret VARCHAR(255) → TEXT (encrypted ~256 chars)
Migration fix: explicit ALTER TABLE webhook_dedup_keys ADD FK после DB::unprepared (PDO глотал FK на partitioned)
PartitionsCreateMonthsTest fix: DETACH PARTITION + DROP вместо DROP CASCADE

Frontend: SecurityTab реальная логика (setup wizard 3 шага, disable, regenerate dialogs)

- Pest +10 (101/101 за 13.37с, 364 assertions)
- Vitest 166/166
- CLAUDE.md v1.39→v1.40, реестр v1.48→v1.49, schema v8.7→v8.8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:03:02 +03:00
Дмитрий c39d555e6f phase2(recovery-code): POST /api/auth/2fa/recovery-use + UseRecoveryCodeView
- AuthController::useRecoveryCode перебирает unused codes через Hash::check, нормализация (lowercase + remove dash/space)
- UserRecoveryCode Eloquent (UPDATED_AT=null — schema без updated_at)
- Rate-limit auth:recovery:{pending_user_id}|{ip} (5/15мин)
- Returns recovery_codes_remaining для UI-warning'а (sessionStorage на frontend)
- UseRecoveryCodeView.vue → POST /api/auth/2fa/recovery-use, /recovery-use route, autocomplete=one-time-code
- TwoFactorView "резервный код" ссылка /recovery → /recovery-use
- Pest +6 RecoveryCodeTest (91/91 за 12.77с, 319 assertions)
- Vitest +6 (166/166 за 11.47с)
- TODO: #3 2FA setup wizard (после этого /recovery view получит реальный source данных)
- Регресс: lint+type+format OK; build 849ms; story:build 21/28 за 30.36с; Pint+Stan passed
- CLAUDE.md v1.38→v1.39, реестр v1.47→v1.48

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:43:58 +03:00
Дмитрий 9c488122a1 phase2(reset-password): POST /api/auth/reset-password + ResetPasswordView + DB timezone fix
- AuthController::resetPassword через Password::reset() (callback пишет password_hash)
- ResetPasswordRequest: token + email + password (min 10 по ТЗ §22.4.1) + confirmed
- Rate-limit auth:reset:{sha256(token)[0..16]}|{ip} (5/15мин)
- ResetPasswordView для deep-link /reset/:token?email=...; pre-fill email из query; success → redirect /login через 3 сек
- Vue Router /reset/:token (guestOnly); web.php /reset SPA-path
- DB FIX: config/database.php pgsql.timezone=UTC — без него PG TIMESTAMPTZ +03 терялся при Carbon::parse и tokenExpired ошибочно срабатывал
- Pest +6 ResetPasswordTest (85/85 за 11.50с, 291 assertions)
- Vitest +7 (160/160 за 11.02с)
- Регресс: lint+type+format OK; build 784ms; story:build 21/28 за 30.74с; Pint+Stan passed
- CLAUDE.md v1.37→v1.38, реестр v1.46→v1.47

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:36:27 +03:00
Дмитрий 170382878b phase2(forgot-password): POST /api/auth/forgot + ForgotPasswordView интеграция
- AuthController::forgotPassword использует Password::sendResetLink (anti-enumeration: всегда 200)
- AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets — указывает на нашу таблицу из schema v8.7
- Rate-limit 5/15мин по auth:forgot:{email}|{ip} — hit ставится ДО sendResetLink (защита перебора через unknown email)
- Frontend: authApi.forgotPassword, auth-store.requestPasswordReset, ForgotPasswordView success-state
- Pest +6 в ForgotPasswordTest (79/79 за 10.55с, 273 assertions)
- Vitest +4 (153/153 за 11.11с)
- TODO: POST /api/auth/reset-password + UI-форма new_password (deep-link)
- Регресс: lint+type+format OK; build 862ms; story:build 21/28 за 32с; Pint+Stan passed
- CLAUDE.md v1.36→v1.37, реестр v1.45→v1.46

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:10:28 +03:00
Дмитрий 75897b1636 phase2(rate-limit): login + 2FA verify (5/15min) + frontend lockout
- AuthController: RateLimiter::hit/clear на login + verifyTwoFactor по ключу email|ip / pending_user_id|ip
- 429 + Retry-After header + JSON retry_after (lockoutResponse helper)
- ТЗ §22.4.4: 5 попыток / 15 мин; success чистит throttle; inactive user тоже расходует попытки
- extractRateLimitRetry в api/client.ts; auth-store.lockoutSeconds; v-alert в LoginView/TwoFactorView
- Pest +6 в RateLimitTest.php (73/73 за 8.07с, 246 assertions)
- Vitest +4 в auth-store + LoginView (149/149 за 12.31с)
- Quirk: wrong-password в тестах ≥8 символов (LoginRequest::min:8) — иначе валидация падает до controller
- Quirk: vi.mock api/client в auth-store.spec — иначе axios.isAxiosError в jsdom возвращает false для plain Error
- TODO (отдельные коммиты): IP-lockout 10/час через auth_log + email при 3 неудачах
- Регресс: lint+type+format OK; build 886ms; story:build 21/28 за 37.19с; Pint+Stan passed
- CLAUDE.md v1.35→v1.36, реестр v1.44→v1.45

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:49:47 +03:00
Дмитрий 1d1353931d phase2(user-chip): реальный user в AppLayout/AdminLayout + Logout-menu
- AppLayout: userInitials/userShortName computed из auth-store, fallback цепочка → email → '?' / 'Гость'
- AdminLayout: тот же паттерн с админ-defaults 'АО' / 'Админ Оператор'
- v-menu offset=8 на user-chip: email + Настройки/Выйти из админки + Выйти
- handleLogout async: auth.logout() (swallows API errors) → router.push('/login')
- Vitest +3 в AppLayout.spec.ts (всего 145/145): store-mock + null-user + email-fallback
- AppShell.spec.ts получил createPinia в plugins
- Регресс: lint+type+format OK, vitest 145/145 за 11.01с, build 855ms, story:build 21/28 за 32.11с, Pest 67/67 за 6.16с
- CLAUDE.md v1.34→v1.35, реестр v1.43→v1.44

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:29:05 +03:00
Дмитрий 374724a7a3 phase2(auth-2fa): TOTP-verify endpoint + TwoFactorView интеграция
- pragmarx/google2fa@^9.0 для TOTP RFC 6238.
- AuthController::login изменён: при totp_enabled=true НЕ делает Auth::login,
  сохраняет auth.pending_user_id+pending_remember в session, возвращает
  requires_2fa=true. /me=401 пока 2FA не пройдена.
- AuthController::verifyTwoFactor: читает pending_user_id, верифицирует TOTP
  через Google2FA::verifyKey($secret, $code, window: 1) (окно ±1 = 30s).
  Success → Auth::login + regenerate + clear pending + last_login_at.
- VerifyTwoFactorRequest: regex /^\d{6}$/.
- /api/auth/2fa/verify публичный (нет session-auth до verify).

Frontend:
- auth-store::login: при requires_2fa=true user остаётся null (иначе
  isAuthenticated=true и guard пустит на /dashboard минуя 2FA).
- auth-store::verifyTwoFactor action.
- api/auth.ts::verifyTwoFactor(code).
- TwoFactorView: onMounted redirect на /login если нет pending state;
  submit → verify → /dashboard; на error - clear code + focus first cell.
  userEmail из auth.user?.email.

Pest +6 (всего 67/67 за 6.97s, 194 assertions): login для 2FA НЕ создаёт
session + verify success/неверный код/без login/валидация формата +
после verify /me=200.

Vitest +3 (всего 142/142 за 10.75s): login pending vs success state +
verifyTwoFactor success/reject. TwoFactorView spec получил setActivePinia
+ requires2fa=true для bypass onMounted-redirect.

PHPStan baseline +26 Pest TestCall warnings (накопительно).

Регресс: pint+stan passed; vitest 142/142; vite build 908ms;
story:build 21/28 за 31.28s; Pest 67/67 за 6.97s.

CLAUDE.md v1.33->v1.34, реестр Открытых_вопросов v1.42->v1.43.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:14:33 +03:00
Дмитрий 59299d3c2b phase2(auth-frontend): axios + Pinia + auth-store + auth-guards + form integration
- axios@^1.16 + pinia@^3.0 (--legacy-peer-deps).
- api/client.ts: axios с withCredentials+withXSRFToken (Sanctum SPA auto-XSRF).
  ensureCsrfCookie() + extractValidationErrors/Message helpers.
- api/auth.ts: типизированные login/register/me/logout с AuthUser interface.
- stores/auth.ts: Pinia composition-store (user/loading/requires2fa +
  isAuthenticated computed + login/register/fetchMe/logout actions).
  logout() catch-swallow - UI всегда выходит локально.
- LoginView/RegisterView: useAuthStore интеграция, real POST через store,
  errors из 422 на v-text-fields, redirect на /dashboard или /2fa,
  :loading на btn'ах.
- Auth-guard в router.beforeEach: meta.requiresAuth на 10 routes
  (6 app + 4 admin), meta.guestOnly на login/register/forgot. При первом
  переходе fetchMe() restore-session. Unauth → /login?redirect=<original>.
- / redirect → /dashboard (auth-guard перехватит если не залогинен).
- Pinia в app.ts через app.use(createPinia()).
- cspell-words.txt: мокаем.

Vitest +10 (всего 139/139 за 10.11s):
- auth-store 7 (initial state + login success/reject + register + fetchMe
  success/401 + logout swallow).
- router 5 переписан (login.guestOnly + 6 protected + admin layout +
  3 error без auth + unauth /dashboard → /login?redirect).
- LoginView/RegisterView/router тесты получили createPinia в plugins.
- vi.mock api/auth в router+auth-store specs.

Регресс: lint+type+format OK; vitest 139/139; vite build (main app-chunk
105→153.64 KB +axios+pinia+auth gzipped 54.54 KB) 806ms; story:build 21/28
за 31.73s; Pest 61/61 за 5.86s.

CLAUDE.md v1.32->v1.33, реестр Открытых_вопросов v1.41->v1.42.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:59:43 +03:00
Дмитрий 04b90afda4 phase2(auth-backend): Sanctum SPA mode + AuthController + 13 Pest tests
- laravel/sanctum@^4.3 install. SPA mode (cookie-based session, не tokens).
  personal_access_tokens migration удалена (для SPA не нужна).
- AuthController (Api/): login + register + me + logout с детальной валидацией
  + кастомные русские error-messages.
- LoginRequest + RegisterRequest Form Requests. Register требует
  accept_offer:accepted + accept_pdn:accepted (по ТЗ §1.5/§4.1, БЕЗ
  маркетингового click-wrap'а - расхождение #2 handoff vs ТЗ).
- User::fillable += last_login_at, last_active_at.
- Auth-routes в web.php (НЕ api.php): Sanctum SPA нуждается в session-cookie
  middleware из web-группы (laravel.com/docs/sanctum#spa-authentication).
- cspell-words.txt: pdn, залогинен.

Pest +13 (всего 61/61 за 6.22s):
- login success + 2FA-flag + invalid pass + missing email + blocked + format
  validation + last_login_at update + register success/duplicate/без accept +
  me 401/200 + logout 200.
- Logout-test упрощён до 200+message - Pest cookie-jar держит session между
  запросами теста, full flow через browser-mode (отдельный коммит).
- phpstan-baseline: +25 ignored Pest TestCall warnings (Larastan+Pest quirk).

Регресс: pint+stan passed; vitest 129/129 за 9.59s; vite build 802ms;
story:build 21/28 за 30.39s; Pest 61/61 за 6.22s.

CLAUDE.md v1.31->v1.32, реестр Открытых_вопросов v1.40->v1.41.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:41:35 +03:00
Дмитрий da65cf4bf7 phase2(admin): AdminLayout + AdminTenantsView - админка SaaS (12/13 концептов)
- AdminLayout: отдельный sidebar теало-нуар с под-брендом ADMIN (red error
  10px JBM uppercase) + 4 nav (Тенанты 142 / Биллинг / Инциденты 3 / Система) +
  topbar с crumb «Админка → currentPage» + admin-user-chip с error-color avatar.
- AdminTenantsView (/admin/tenants): page-head + 5-stats + Экспорт +
  search/Статус/Тариф фильтры + v-data-table 7 колонок (Тенант с двухстрочным
  name+inn / Статус-chip 4 цвета / Тариф / Баланс ₽ с error-color при <0 /
  Желаем×факт / MRR с «—» / Активность).
- mockTenants.ts соответствует schema v8.7 §3: 4 статуса × 5 тарифов, 7 mock
  с разнообразием (active/trial/overdue/suspended) + AdminStats (142/128/9/5/
  1 248 600 ₽).
- AdminPlaceholderView универсальный для Биллинг/Инциденты/Система с
  описаниями ссылающимися на schema v8.7 (incidents_log §9 / system_settings §10).
- AppShell расширен meta.layout='admin'. Router: /admin redirect на /tenants +
  4 admin-route'а с lazy-imports. Web.php fallback покрывает /admin/*.
- cspell-words.txt: Екб.

Vitest +11 (всего 129/129 за 10.02s):
- заголовок + 5 stats (regex nbsp в 1 248 600 ₽) + 7 columns + 7 rows +
  Окна Москва ИНН + overdue −1 200 + trial 4 дня + suspended + search filter
  «Натяжные» → 1 row + Экспорт/Статус/Тариф кнопки.

Регресс: lint+type+format OK; vitest 129/129; vite build (admin views
в lazy-chunks; main 104.99 KB); story:build 21/28 за 30.32s; Pest 48/48 за 4.89s.

CLAUDE.md v1.30->v1.31, реестр Открытых_вопросов v1.39->v1.40.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:23:28 +03:00
Дмитрий 034657788d phase2(errors): ErrorView 404/403/500 + Laravel fallback
- ErrorView универсальный с конфигурацией через route.meta.errorCode
  (404/403/500). По v8_errors.html: full-bleed теало-нуар bg, top-brand,
  err-code 96px JBM с accent на средней цифре, title/desc, 2 actions,
  опциональные status-list (500) и err-id с copy-btn (403/500).
- AppShell: meta.layout='error' → RouterView напрямую (ErrorView сам
  предоставляет v-app).
- Router: /403, /500, catch-all /:pathMatch(.*)*  → ErrorView с meta.errorCode.
- web.php: явные Route::view + Route::fallback (срабатывает после Pest
  runtime-routes, не ломает SetTenantContextTest).
- cspell-words.txt: резолвится, роуты.

Vitest +8 (всего 118/118 за 9.39s):
- 404 default + 403 с REQ-ID + 500 с INC-ID + status-list (API/Telegram/YooKassa) +
  404 actions (На дашборд + Назад) + 403 mailto-link + 500 status-link +
  brand-блок + 404 НЕ содержит REQ/INC/status-list (regression-guard).
- stubs:{VApp/VMain} как passthrough — обходим Vuetify layout-injection в jsdom.

Регресс: lint+type+format OK; vitest 118/118; vite build (ErrorView lazy-chunk;
main app-chunk 101.01KB упал на 7KB благодаря shared chunk'ам); story:build
19/26 за 30.96s; Pest 48/48 за 4.88s (fallback не сломал runtime-routes).

CLAUDE.md v1.29->v1.30, реестр Открытых_вопросов v1.38->v1.39.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:11:09 +03:00
Дмитрий 4de39e70b2 phase2(reports): ReportsView - асинхронная генерация отчётов с очередью
- ReportsView (/reports): form-card (Запросить) + jobs-list panel.
  Form: 4 type-cards radio-grid (Сделки/Менеджеры/Источники/Биллинг) +
  date-range + Проект/Менеджер v-select + 4 fmt-кнопки (CSV/XLSX/JSON/PDF) +
  quota-banner alert (CTO-7: 2/3 одновременных + CTO-6: 3 попытки/7 дней) +
  Запустить/Сброс.
- Jobs-list: 5 mock-rows × 4 статуса (done/running/queued/failed) с icon +
  meta JBM (FORMAT · size · rows · timeText) + status-chip + actions
  (Скачать done / Повторить failed && attempt<3 / Отменить queued /
  Удалить done|failed). v-progress-linear для running 62%.
- composables/mockReports.ts: type unions (4×4×4) + 5 mock-jobs + MOCK_QUOTA
  (CTO-6/7 значения).
- Маршрут /reports (meta.layout=app, lazy-import) в router + web.php.

Vitest +12 (всего 110/110 за 9.38s):
- заголовок + page-stats + 4 type-cards + дефолт active + 4 формата +
  quota-banner («2 из 3» / «3 попыток retry» / «7 дней») + 5 job-rows +
  done-«Готов»+Скачать-aria + running-«62%»+progressbar role + queued-Отменить +
  failed-«Ошибка»+«S3 timeout»+Повторить-aria + клик-переключение active.

Регресс: lint+type+format OK; vitest 110/110; vite build (ReportsView lazy-chunk;
main 108.19 KB); story:build 18/25 за 30.77s; Pest 48/48 за 4.58s.

CLAUDE.md v1.28->v1.29, реестр Открытых_вопросов v1.37->v1.38.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:01:10 +03:00
Дмитрий 394663597f phase2(settings): SettingsView - 8 вкладок (4 реализованы, 4 placeholder)
- SettingsView (/settings): sidebar tabs-rail (md=3, 8 v-list-item с mdi-icon)
  + content-pane (md=9 v-card outlined min-height 480px). activeTab ref
  переключает рендер вкладки.

Реализованы:
- ProfileTab: avatar 80px + 5 form-fields (имя/email disabled/телефон/TZ/роль).
- SecurityTab: 3 cards (Пароль / 2FA включена + recovery codes + Отключить /
  Активные сессии 3 mock с Завершить-btn).
- ApiTab: API-ключ password+eye-toggle + Webhook (URL + signing secret HMAC).
  Текст про дедуп (tenant_id, source_crm_id) 24ч и антифрод по phone (§10.8.1).
- NotificationsTab: матрица 8x3 (events × channels) соответствует schema v8.7
  §4 users.notification_preferences JSONB. 8 событий (new_lead, duplicate_detected,
  low_balance, tariff_charge, reminder_due, manager_assigned, webhook_failed,
  monthly_report) × 3 канала (email/sms/in_app). + sound_enabled switch.

Placeholder:
- PlaceholderTab универсальный с props title/description + v-alert «В разработке».
- Используется для Проекты / Команда / Интеграции / Тихие часы.

Маршрут /settings (meta.layout=app, lazy-import) в router + web.php.
.gitleaks.toml: settings/*.vue в allowlist (фиктивный профиль).
cspell-words.txt: смыслово.

Vitest +8 (всего 98/98 за 8.42s):
- 8 nav-tabs + все названия + дефолт «Профиль» + Проекты → «В разработке» +
  Уведомления показывает «События × каналы» + 5 событий матрицы +
  Безопасность: 2FA + сессии + API: API-ключ + Signing secret HMAC.

Регресс: lint+type+format OK; vitest 98/98; vite build (SettingsView lazy-chunk;
main app-chunk 107.85KB); story:build 17/24 за 31.7s; Pest 48/48 за 5.03s.

CLAUDE.md v1.27->v1.28, реестр Открытых_вопросов v1.36->v1.37.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:51:41 +03:00
Дмитрий c8012896e3 phase2(billing): BillingView - финансовый экран биллинга и тарифов
- BillingView (/billing): page-head со stats (кошелёк/лиды/runway-дни) + pending
  banner v-alert info («1 платёж в обработке через ЮKassa, auto-cancel 30 мин»)
  + 3 wallet-cards (Кошелёк ₽ primary card теало-нуар + LIVE; Баланс лидов
  ГЦК; Тариф «Команда» 990₽/мес + 3 фичи) + transactions panel (4 tabs +
  v-data-table 5 колонок: Дата/Операция/ID/Статус-chip/Сумма ± JBM tnum) +
  invoices list (PDF + 1С 8.3 XML).
- composables/mockBilling.ts соответствует схеме v8.7 §4.4-4.5: 8 mock
  транзакций (types: topup/lead_charge/refund/tariff_charge; statuses:
  pending/completed/rejected) + 4 invoices (pdf/xml_1c83) + pending payment.
- Маршрут /billing (meta.layout=app) в router + web.php.

Format helpers: «+ N ₽» / «− N ₽» / «— 0 ₽» rejected; Intl.NumberFormat ru-RU.

Vitest +11 (всего 90/90 за 7.96s):
- заголовок + page-stats nbsp regex + pending banner + 3 wallet-cards + 3 фичи
  тарифа + 4 tabs + дефолт «Все» 8 строк + format «+/−» + rejected «— 0 ₽» +
  4 invoice rows + PDF/1С 8.3 XML labels.

Регресс: lint+type+format OK; vitest 90/90; vite build (BillingView lazy-chunk;
VDataTable вынесен в общий chunk 79.84KB - shared с DealsView); story:build
16/23 за 32.16s; Pest 48/48 за 4.89s.

CLAUDE.md v1.26->v1.27, реестр Открытых_вопросов v1.35->v1.36.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:39:49 +03:00
Дмитрий 45239f6602 phase2(deal-drawer): DealDetailDrawer - правая панель с деталями сделки
- DealDetailDrawer (v-navigation-drawer right temporary 480px):
  - hero (#id eyebrow + name h5 + close + tel:link + clock + status-chip)
  - section Параметры (2-col grid: Проект/Стоимость/Менеджер/Источник)
  - section Активность (timeline 6 events с iconified vertical-line)
- mockDealEvents.ts: 6 mock-events (created/balance_charged/assigned/viewed/
  status_changed/commented) - соответствуют ActivityLog event-константам v8.7.
- Интеграция в DealsView (@click:row) и KanbanView (через @open-deal от карточки).
- cspell-words.txt: iconified, мапы, резолвятся, резолвером, stub'ить, инлайнен.

Vue3 quirk: v-navigation-drawer требует layout-injection от v-app/v-layout,
но в Vitest vite-plugin-vuetify auto-import не работает. Решение:
- DealsView/KanbanView тесты: stubs:{DealDetailDrawer:true}
- DealDetailDrawer тесты: stubs:{VNavigationDrawer:passthrough-div}

Vitest +8 (всего 79/79 за 7.57s):
- DealDetailDrawer 8 (open=false скрытие, deal=null no-content, hero+id,
  tel:link, status-chip, params, timeline 6 items, emit update:open(false)).

Регресс: lint+type+format OK; vitest 79/79; vite build (drawer инлайнен в
DealsView+KanbanView lazy-chunks); story:build 15/22 за 31.55s; Pest 48/48.

CLAUDE.md v1.25->v1.26, реестр Открытых_вопросов v1.34->v1.35.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:29:11 +03:00
Дмитрий c8131a39c8 phase2(kanban-dnd): vuedraggable@4 - drag-and-drop карточек между 14 колонками
- vuedraggable@^4.1.0 + sortablejs@1.14.0 (--legacy-peer-deps).
- KanbanColumn: <draggable v-model="localDeals" group="kanban-deals"
  item-key="id" ghost-class="ghost-card" drag-class="drag-card" animation="150">
  + #footer empty-state «пусто · перетащите сюда».
- DraggableChangeEvent типизирован (added/removed/moved discriminated union).
- KanbanView: const → reactive<Record<slug, MockDeal[]>> (vuedraggable v-model
  требует независимые arrays); shallow-clone {...d} чтобы не мутировать MOCK_DEALS.
- onColumnChange: при event.added → element.statusSlug = targetSlug.
  TODO: POST /api/deals/{id}/transition с проверкой allowed-переходов.
- cspell-words.txt: vuedraggable, симулируется.

Visual: ghost-card opacity 0.4 + ivory-tint bg, drag-card rotate 1deg.

Vitest +1 (всего 71/71 за 7.48s): эмулирует $emit('change', {added}) →
проверяет statusSlug update. Полный mouse-DnD не симулируется — JSDOM
не умеет drag-events, но event-handler логика покрыта.

Регресс: lint+type+format OK; vitest 71/71; vite build (KanbanView lazy-chunk
вырос до 180.53KB - SortableJS-обёртка, грузится только на /kanban);
story:build 14/20 за 30.45s; Pest 48/48 за 4.88s.

CLAUDE.md v1.24->v1.25, реестр Открытых_вопросов v1.33->v1.34.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:08:16 +03:00
Дмитрий d39934c8d9 phase2(kanban): KanbanView - 14 колонок по lead_statuses (БЕЗ DnD)
- KanbanCard: компактная карточка (name/phone/project/cost/manager-avatar),
  emit('open',id) на click для будущего DealDetailDrawer.
- KanbanColumn: header с border-top по colorHex статуса (--accent CSS-var) +
  name+count+total ₽; body с v-for карточек + empty-state «пусто».
- KanbanView: orchestrator, 14 колонок (по LEAD_STATUSES) с группировкой
  MOCK_DEALS по statusSlug, horizontal-scroll с custom scrollbar.
- Маршрут /kanban (meta.layout=app) в router + web.php.
- .gitleaks.toml: tests/Frontend/*.spec.ts в allowlist (assertion на mock-телефоны).
- cspell-words.txt: инлайн, vueuse.

DnD НЕ реализован на MVP - отдельный коммит после выбора библиотеки
(vue-draggable-next или @vueuse/integrations/useSortable).

Vitest +14 (всего 70/70 за 7.37s):
- KanbanCard 3 (data + initials + emit open)
- KanbanColumn 5 (header + total + empty + accent CSS-var case-insensitive +
  проброс openDeal)
- KanbanView 6 (заголовок + 14 columns + правильные status'ы + stats + кнопка +
  DnD-предупреждение)

Регресс: lint+type+format OK; vitest 70/70; vite build (KanbanView lazy-chunk);
story:build 14/20 за 31.17s; Pest 48/48 за 5.06s.

CLAUDE.md v1.23->v1.24, реестр Открытых_вопросов v1.32->v1.33.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:56:59 +03:00
Дмитрий a51a94830c phase2(deals): DealsView - центральный экран CRM (список сделок)
- DealsView (/deals): page-head со stats (всего/в работе/ждут оплату) +
  Экспорт/Новая сделка кнопки. filter-bar с 5-tab chiprow (Все/Активные/
  Ждут оплату/Закрытые/Невалидные) + поиск по name/phone/project.
  v-data-table с 6 колонками: Лид (avatar+name+phone), Статус (tonal-chip
  с colorHex из lead_statuses), Проект, Менеджер, Стоимость (Intl.NumberFormat
  ru-RU JBM tnum), Время (formatRelative мин/ч/д назад). show-select для
  будущих bulk-actions. Empty-state.
- composables/mockDeals.ts: 12 mock-сделок + DEALS_TABS (5 срезов с slug[]).
- Маршрут /deals (meta.layout=app) в router + web.php.
- .gitleaks.toml: mockDeals.ts в allowlist (фиктивные имена/телефоны).

Vue regex quirk: ESLint no-irregular-whitespace в regex с literal nbsp -
\s в JS уже matches U+00A0/U+202F из Intl.NumberFormat, использовать \s+.

Vitest +8 (всего 56/56 за 5.66s): заголовок + page-stats + 5 tabs +
дефолт active-фильтр (5/12 строк) + кнопки + 6 колонок + format «2 400 ₽» +
format «7 мин назад».

Регресс: lint+type+format OK; vitest 56/56; vite build (DealsView lazy-chunk
87.54KB - v-data-table крупный, но грузится только на /deals); story:build
11/15 за 31.93s; Pest 48/48 за 4.96s.

CLAUDE.md v1.22->v1.23, реестр Открытых_вопросов v1.31->v1.32.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:45:25 +03:00
Дмитрий 290e7dbc34 phase2(charts): ActivityChart + FunnelChart - Dashboard закрыт по дизайну
- ActivityChart: native SVG line chart (без chart-library, чтобы не +400KB
  зависимость для статичных дашборд-графиков). Y-grid 5 линий, area-gradient,
  7 точек (предпоследняя выделена primary teal как «сегодня»), 3 tabs
  (Принято/Оплачено/Отказ).
- FunnelChart: segmented bar по 14 статусам + funnel-list (sort desc).
- composables/leadStatuses.ts: snapshot 14 статусов из db/schema.sql:2130
  (НЕ из BRANDBOOK §3.6 - расхождение #1 handoff vs ТЗ из реестра v1.13).
  14 правильных slug'ов: new/viewed/worked/base/missed/negotiations/
  waiting_payment/partnership/paid/closed/test_drive/hot/replacement/final_missed.
- DashboardView интегрирует оба чарта в charts-row (md=7+5).
- cspell-words.txt: ldot, композаблом, инлайнингом, инлайнены.

Vue compiler quirk: withDefaults factory не разрешает референсить module-level
const'ы (checkInvalidScopeReference). Обходим инлайнингом литерала.

Vitest +13 тестов (всего 48/48 за 5.5s):
- ActivityChart 6 (3 tabs + 7 circles + 'сегодня' + custom points + legend)
- FunnelChart 7 (14 segments + 14 list-items + assertion на отсутствие
  'Думает'/'Спам' из handoff + сортировка + colorHex + total)

Stories +2 с 3 variants каждый (Histoire 10/14 за 30.43s).

Регресс: lint+type-check+format OK; vitest 48/48; vite build (DashboardView
chunk 14.9->21.17 KB с чартами); story:build 10/14 за 30.43s; Pest 48/48 за 5.10s.

CLAUDE.md v1.21->v1.22, реестр Открытых_вопросов v1.30->v1.31.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:32:58 +03:00
Дмитрий 1e233a70c8 phase2(dashboard): AppLayout + DashboardView - default-layout приложения
- AppLayout: v-navigation-drawer (теало-нуар sidebar 240px) + brand-block
  + nav-tree из 8 пунктов в 3 группах (Работа/Финансы/Команда), v-app-bar
  с crumb «Рабочая область → currentPage» + search ⌘K + bell + user-chip.
  Mobile (md<): drawer toggleable.
- DashboardView: page-head «Доброе утро, Иван» + page-meta + range-toggle
  4 опции (Сегодня/7д/30д/Период). KPI-row из 4 cards: 3 outlined (получено
  лидов/конверсия/активные проекты) + 1 hero balance с runway-bar 4/7
  заполненных сегментов teal #32C8A9.
- AppShell упрощён до layout-mapper (route.meta.layout 'app'/'auth').
- Маршрут /dashboard (meta.layout='app') в router + web.php.
- histoire.setup расширен 8 app-stub-маршрутами для AppLayout.
- Vitest +11 тестов: AppLayout 6 (brand+3 группы+8 пунктов+счётчики+crumb),
  DashboardView 5, AppShell.spec.ts переписан под layout-mapper.
- cspell-words.txt: JBM.

Регресс: lint+type-check+format OK; vitest 35/35 за 4.92s; vite build
DashboardView lazy-chunk 14.9KB; story:build 8/8 за 28.97s; Pest 48/48 за 4.88s.

CLAUDE.md v1.20->v1.21, реестр Открытых_вопросов v1.29->v1.30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:21:19 +03:00
Дмитрий 19096552b4 phase2(auth): закрыты 4 оставшихся auth-экрана из v8_login.html
- RegisterView: email + password (strength-meter 0..4) + 2 click-wrap'а
  (оферта + ПДн). 3-й «маркетинг» из handoff НЕ реализован (расхождение
  #2 реестра v1.13 - handoff противоречит ТЗ §1.5/§4.1).
- TwoFactorView: 6 input-cell с auto-focus вперёд при вводе цифры,
  Backspace назад при empty, paste 6 цифр заполняет все.
- ForgotPasswordView: email + alert «5 попыток / 15 минут» по ТЗ §1.7.
- RecoveryCodesView: 8 кодов в 2-column grid + Скачать .txt (Blob/URL.createObjectURL)
  + Копировать (navigator.clipboard) + warning о невозможности повторного просмотра.

Router: 4 новых маршрута (/register, /2fa, /forgot, /recovery), все
meta.layout='auth', lazy-imports.

Vitest +14 тестов (всего 24/24 за 3.29s):
- RegisterView 4 (вкл. assertion на отсутствие маркетингового click-wrap)
- TwoFactorView 3, ForgotPasswordView 3, RecoveryCodesView 4

Stories +4 (Histoire 6/6 за 29.17s).

Регресс: lint+type-check+format OK; vitest 24/24; vite build 5 lazy-chunks
для views + Vuetify в отдельные chunks (app chunk 198KB→78KB); Pest 48/48 за 4.85s.

CLAUDE.md v1.19→v1.20, реестр Открытых_вопросов v1.28→v1.29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:09:56 +03:00
Дмитрий e909a95a8d phase2(login): vue-router 4 + AuthLayout + LoginView - первый реальный экран
- vue-router@^4.6.4 (--legacy-peer-deps из-за Histoire vs Vite 8 peerDep).
- resources/js/router/index.ts: createWebHistory + lazy-imports + meta.layout.
  Маршрут / -> /login, /login -> LoginView (meta.layout=auth).
- resources/js/layouts/AuthLayout.vue: двухпанельный (brand-pane тёмный с
  radial-gradient акцентами + form-pane warm ivory). На mobile brand-pane скрыт.
- resources/js/views/auth/LoginView.vue: форма Vuetify по v8_login.html
  секция #form-login - email/password (autocomplete + eye-icon), primary submit,
  Yandex 360 SSO, RouterLink на /register и /forgot.
- AppShell.vue: layout-mapper по route.meta.layout (default/auth).
- routes/web.php: явные Route::view для 6 SPA-путей (НЕ catch-all - он перехватывал
  /_test/* в Pest beforeEach и валил 5 SetTenantContextTest).
- Vitest: +LoginView.spec.ts (5), +router.spec.ts (2), AppShell.spec.ts переписан (3).
  Vitest 10/10 за 3.01s.
- LoginView.story.vue для Histoire. Setup расширен memory-router'ом.
- cspell-words.txt: рендерят, коммиты, Лидерру.
- Регресс: lint:vue OK, type-check OK, format:check OK, vitest 10/10,
  vite build 212 модулей за 383ms (LoginView lazy-chunk 43.5KB JS / 51.7KB CSS),
  story:build 2/2 за 29.94s, Pest 48/48 за 4.86s.

CLAUDE.md v1.18->v1.19, реестр Открытых_вопросов v1.27->v1.28.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:59:00 +03:00
Дмитрий c3e45343ac phase2(histoire): #24 Histoire 1.0-beta.1 — каталог компонентов, фаза 2 закрыта 6/6
- histoire@1.0.0-beta.1 + @histoire/plugin-vue@1.0.0-beta.1 (--legacy-peer-deps:
  Histoire требует vite ^7, у нас vite 8 ради @vitejs/plugin-vue 6 — runtime smoke OK).
- app/histoire.config.ts: HstVue plugin + setupFile + Forest primary palette (Teal #0F6E56).
- app/resources/js/histoire.setup.ts: defineSetupVue3 регистрирует vuetify для каждой story.
- app/resources/js/components/AppShell.story.vue — первая story (smoke).
- npm-scripts: story / story:build / story:preview.
- .gitignore: /app/.histoire/ (статическая сборка).
- cspell-words.txt: рендерятся, репо, тулчейна.
- Регресс: lint:vue, type-check, format:check OK; vitest 3/3 за 2.98s; vite build 158 модулей
  за 334ms; story:build 1/1 за 30.25s; Pest 48/48 за 5.12s.

Активно 18/28 инструментов: 9 фазы 0 + 8 фазы 1 + 6 фазы 2 (фаза 2 по тулчейну закрыта).

CLAUDE.md v1.17->v1.18, Tooling v1.6->v1.7, реестр Открытых_вопросов v1.26->v1.27.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:29:42 +03:00
Дмитрий 952373fce5 phase2(trigger): Vue 3 + Vuetify 3 + ESLint+Prettier+Vue + vue-tsc + Vitest
Триггер фазы 2 выполнен — первый коммит в app/resources/js/. Активирует
5 из 6 инструментов фазы 2 (CLAUDE.md §3.3) — без Histoire (отдельно).

Tailwind удалён (правило CLAUDE.md §5 п.2).

Стек:
  - vue@3.5, vuetify@3.12, @vitejs/plugin-vue@6 (для Vite 8 совместимости —
    @vitejs/plugin-vue@5 поддерживает только Vite 5/6), vite-plugin-vuetify@2
    (auto-import), 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

Конфиги:
  - app/vite.config.js — vue + vuetify auto-import
  - app/vitest.config.ts — jsdom + setup file для ResizeObserver/
    IntersectionObserver/matchMedia/CSS.supports stubs (без них Vuetify
    падает в JSDOM)
  - app/eslint.config.js — flat-config
  - app/.prettierrc.json, app/tsconfig.json

Frontend-структура:
  - resources/js/app.ts — точка входа
  - resources/js/plugins/vuetify.ts — палитра Forest (Teal/ivory/теало-нуар
    из BRANDBOOK_v2 §3)
  - resources/js/components/AppShell.vue — первый компонент: v-app +
    v-app-bar + v-card
  - resources/css/app.css — Inter (UI, opsz axis), JetBrains Mono
    (.numeric, tnum)
  - resources/views/welcome.blade.php — упрощён под Vue mount

Smoke-тесты Vitest 3/3 за 2.8 сек:
  - монтируется без ошибок
  - содержит «Лидерра.»
  - рендерит chip с фазой 2

Build: 158 модулей за 386 ms (184 KB JS / 295 KB CSS gzipped).

npm-scripts: lint:vue, format, format:check, type-check, test:vue.
lefthook job #8 (ESLint на staged .ts/.vue) добавлен в pre-commit.
Полный type-check (vue-tsc) и Vitest — отдельно вручную (медленно для
pre-commit: ~3 сек на каждый).

CLAUDE.md v1.16 → v1.17 (фаза 1 → фаза 2 в §6).
Реестр Открытые_вопросы v1.25 → v1.26.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:07:14 +03:00
Дмитрий ba97f952cc phase1(webhook): failed() callback + FailedWebhookJob модель — упавшие jobs после 3 ретраев
После исчерпания всех 3 ретраев Laravel вызывает failed(\Throwable $e) —
упавший job сохраняется в failed_webhook_jobs для ручного разбора и
повторного запуска через админку.

Реализация:
  - app/app/Models/FailedWebhookJob.php — Eloquent для failed_webhook_jobs
  - ProcessWebhookJob::failed() через DB::table->insert (не Eloquent::create)
    чтобы обойти RLS: failed-callback запускается вне транзакции воркера,
    SET LOCAL app.current_tenant_id не выставлен, политика бы отвергла INSERT.
    Запись должна попасть в БД даже в катастрофическом сценарии.
  - payload через json_encode(JSON_UNESCAPED_UNICODE) — UTF-8 кириллица
    сохраняется

Sentry::captureException оставлен как TODO для production (на dev-стеке
нет DSN).

3 новых Pest-теста:
  - failed() пишет упавший job с webhookLogId (через DB::table('webhook_log')
    для FK satisfaction)
  - failed() работает БЕЗ webhookLogId (NULL ok — soft FK)
  - failed() записывает payload с UTF-8 кириллицей корректно

Pest 48/48 зелёные за 4.7 сек. Pint + Larastan чисто.

Webhook-flow покрыт полностью на dev-стеке (за исключением Sentry и
SendNewLeadNotificationJob — Биз-20 Telegram, ждёт настоящего бота).

CLAUDE.md v1.15 → v1.16. Реестр Открытые_вопросы v1.24 → v1.25.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:52:47 +03:00
Дмитрий 1d4738dfa2 phase1(infra): partitions:create-months — Artisan-команда (замена pg_partman)
Закрыт пункт «pg_partman replacement» из project_phase1_strategy.md
(расширение pg_partman недоступно на native Windows-стеке без сборки
из исходников).

Реализация:
  - app/app/Console/Commands/PartitionsCreateMonths.php
    - signature: partitions:create-months {--ahead=2}
    - создаёт партиции для deals + supplier_lead_costs (обе по received_at)
    - идемпотентна (проверка через pg_class WHERE relkind='r' перед CREATE)
    - запускать ежесуточно через Windows Task Scheduler / cron

Smoke-test на dev: --ahead=8 создал 6 партиций (Nov 2026 - Jan 2027) +
12 skipped. После migrate:fresh партиции возвращаются к initial 6.

4 новых Pest-теста в PartitionsCreateMonthsTest:
  - создание партиций на 8 месяцев вперёд для обеих таблиц
  - идемпотентность (повторный --ahead=5 → 0 created, 12 skipped)
  - --ahead=0 создаёт только текущий месяц
  - INSERT в deals с received_at в новой партиции корректно роутится

Тесты используют beforeEach/afterEach для cleanup'а через
DROP TABLE ... CASCADE (FK webhook_dedup_keys на партицию propagates).

Pest 45/45 зелёные за 4.9 сек. Pint + Larastan чисто (phpstan-baseline
регенерирован для динамических свойств $this в Pest closure'ах).

CLAUDE.md v1.14 → v1.15. Реестр Открытые_вопросы v1.23 → v1.24.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:46:41 +03:00
Дмитрий 2d9e84ef1d phase1(antifraud): DuplicateDetector сервис (Биз-19) — антифрод-дедуп по phone в окне 24ч
Закрыт Биз-19 (§10.8.1) на код-уровне. При создании НОВОЙ сделки сервис
DuplicateDetector ищет master по (tenant_id, phone) в окне 24 ч. Если
найден — новой сделке проставляется duplicate_of_id = master.id, баланс
НЕ списывается, SupplierLeadCost НЕ создаётся. ActivityLog пишется с
context.duplicate_of = master.id.

Реализация:
  - app/app/Services/DuplicateDetector.php — отдельный сервис:
    findMaster(tenantId, phone, ?Carbon $now): ?Deal. Ищет deals с
    duplicate_of_id IS NULL и received_at >= now - 24h. Возвращает
    первую по received_at ASC или null. WINDOW_HOURS = 24 — константа.
  - App\Jobs\ProcessWebhookJob::handle() — после upsertDeal() для новой
    сделки вызывает findMaster(). Если master !== создаваемая сделка —
    markAsDuplicate(): UPDATE duplicate_of_id + ActivityLog с context.
  - DI через app(DuplicateDetector::class) внутри handle() (не в
    сигнатуре — для совместимости с прямыми вызовами из Pest без
    Bus::dispatchSync).

4 новых Pest-теста:
  - master в окне 24ч → дубль, баланс НЕ списывается
  - master старше 24ч → НЕ дубль, баланс списан дважды
  - дубли изолированы по tenant_id
  - ActivityLog для дубля содержит context.duplicate_of

Pest 41/41 зелёные за 4.1 сек. Pint + Larastan чисто.

CLAUDE.md v1.13 → v1.14. Реестр Открытые_вопросы v1.22 → v1.23.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:41:37 +03:00
Дмитрий 1ba25e6b4e phase1(webhook): закрыты TODO в ProcessWebhookJob — BalanceTransaction/ActivityLog/RejectedDealsLog/SupplierLeadCost
Закрыты 4 TODO в Webhook PoC. Job теперь полностью реализует §5.5
narrative ТЗ за исключением DuplicateDetector (Биз-19) и
SendNewLeadNotificationJob (Биз-20) — отдельные ветви.

5 новых Eloquent-моделей:
  - app/app/Models/BalanceTransaction.php — списание lead_charge -1,
    type-константы (TYPE_LEAD_CHARGE и т.д.)
  - app/app/Models/ActivityLog.php — event=deal.created с
    context.source=webhook, event-константы
  - app/app/Models/RejectedDealsLog.php — zero_balance ветка вместо
    Log::info (payload сохраняется для возможного восстановления)
  - app/app/Models/SupplierLeadCost.php — composite PK (id, received_at),
    snapshot cost_rub из suppliers, supplier_id resolves через
    project_suppliers m2m (первый активный по sort_order)
  - app/app/Models/Supplier.php — минимальная для FK target

Job-структура реструктурирована: handle() оркестрирует, делегирует в
logRejection() / chargeNewLead() / resolveSupplierId() / upsertDeal().
Все INSERT'ы в одной DB::transaction — атомарность Ю-2 (deal +
balance_transaction + supplier_lead_cost появляются вместе).

Graceful skip SupplierLeadCost если у проекта нет активного supplier
через project_suppliers + Log::warning. TODO для production: SystemSetting
fallback.

6 новых Pest-тестов в ProcessWebhookJobTest:
  - BalanceTransaction lead_charge -1 для новой сделки
  - Дубль vid НЕ создаёт BalanceTransaction
  - ActivityLog event=deal.created с context.source=webhook
  - RejectedDealsLog reason=zero_balance при balance_leads=0
  - SupplierLeadCost snapshot cost_rub (helper seedSupplierForProject)
  - SupplierLeadCost graceful skip без активного supplier

Pest 37/37 зелёные за 3.9 сек. Pint + Larastan чисто (ide-helper:models
регенерирован для 5 новых моделей).

CLAUDE.md v1.12 → v1.13. Реестр Открытые_вопросы v1.21 → v1.22.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:35:28 +03:00
Дмитрий 4803fa0200 phase1(webhook): Deal/WebhookDedupKey + ProcessWebhookJob (advisory lock) — CTO-17 addendum
Webhook PoC раскрыл архитектурный пробел в schema v8.6: §5.5-спецификация
делает INSERT в webhook_dedup_keys ДО INSERT в deals (атомарный захват
ключа), но FK был immediate. Решение в две стадии:

  1. schema.sql v8.6 → v8.7 — DEFERRABLE INITIALLY DEFERRED на FK
     (deal_id, deal_received_at) → deals. ON DELETE CASCADE остаётся
     immediate. В bare-транзакции production worker'а решает проблему.

  2. Pivot Job на pg_advisory_xact_lock — Pest-тесты с DatabaseTransactions
     trait всё равно падали: PG проверяет deferred FK на RELEASE SAVEPOINT,
     не на outer COMMIT. Воспроизведено standalone PHP-скриптом, это
     PG-семантика subtransactions. Advisory lock работает identically
     в любой вложенности транзакций. DEFERRABLE FK сохранён в schema
     как defense-in-depth для batch-импортов без savepoint.

Backend стек:
  - app/app/Models/Deal.php — composite PK через override
    setKeysForSaveQuery (PG требует id+received_at для partition pruning)
  - app/app/Models/WebhookDedupKey.php — мини-модель для тестов и debug
  - app/database/factories/DealFactory.php — fake данные с received_at
    в текущей партиции
  - app/app/Jobs/ProcessWebhookJob.php — advisory-lock-based upsert
    по §5.5 v8.7. PoC scope: dedup + balance check + project findOrCreate.
    TODO для следующих ветвей: BalanceTransaction, SupplierLeadCost,
    ActivityLog, RejectedDealsLog, DuplicateDetector (Биз-19).
  - app/tests/Feature/DealModelTest.php — 6 тестов composite PK + связи
  - app/tests/Feature/ProcessWebhookJobTest.php — 6 тестов: новая сделка,
    дубль vid, balance=0, изоляция тенантов, findOrCreate проекта,
    ON DELETE CASCADE.

Pest 31/31 за 2.7 сек. Pint + Larastan чисто (phpstan-baseline регенерирован,
scanFiles _ide_helper_models.php добавлен в phpstan.neon).

Документы:
  - db/CHANGELOG_schema.md §W (две стадии решения)
  - narrative §2.4/§5.5/§6.5/§11 синхронизированы под advisory lock
  - Реестр Открытые_вопросы v1.20 → v1.21
  - CLAUDE.md v1.11 → v1.12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:24:55 +03:00
Дмитрий e7c075919a phase1(hygiene): narrative ↔ schema v8.6 webhook dedup — закрытие техдолга v1.19
Точечная in-place синхронизация SQL/PHP-примеров webhook handler'а в narrative
ТЗ под двустадийную dedup-логику schema v8.6 (CTO-17). 8 разделов, без bump'а
версии файла (как для L13-гигиены 3a9ed71):

  - §2.4 Поток данных от Webhook до сделки — описание воркера
  - §5.5 ProcessWebhookJob (PHP-код) — Deal::updateOrCreate() заменён на
    DB::selectOne с INSERT INTO webhook_dedup_keys ON CONFLICT RETURNING
    is_new + раздельные ветки Deal::create() / Deal::where(...)->update()
  - §5.6 Таблица крайних случаев (дубль vid)
  - §6.5 Идемпотентность импорта (SQL-пример)
  - §11 DDL deals — UNIQUE INDEX → INDEX, добавлен полный DDL
    webhook_dedup_keys (PK, composite FK на deals(id, received_at)
    ON DELETE CASCADE, RLS USING + WITH CHECK)
  - §20.12.3 Связь с биллингом тенанта — поток в транзакции 4→5 шагов
  - §21.1 Списание баланса при дублях
  - §27.1 Итог по идемпотентности

Не трогалось намеренно:
  - line 302 (глоссарий «Idempotency» — общая концепция UPSERT)
  - line 1238 (диаграмма CSV-импорта — обобщённый текст)
  - line 1180 (маппинг колонки id → deals.source_crm_id)

Реестр Открытые_вопросы v1.19 → v1.20 (новый блок «закрыт техдолг»).
Оперативная карта CLAUDE.md v1.10 → v1.11.

Линты: markdownlint 0 errors, cspell 0 issues на изменённых файлах,
lychee 115/115 OK на CLAUDE.md + 2 narrative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:49:39 +03:00
Дмитрий f502b0058d phase1(backend): multi-tenant фундамент развёрнут — schema v8.5→v8.6 + migrate:fresh
Backend multi-tenant фундамент развёрнут на dev-БД liderra: 68 таблиц
(52 обычных + 16 партиций) + 36 RLS-policies + 5 функций + 13 триггеров.
`php artisan migrate:fresh` за 870 ms через одну raw-SQL миграцию
`load_initial_schema.php` (DB::unprepared с db/schema.sql).

Первый реальный запуск schema.sql на pristine PG 16 поймал 2
несовместимости v8.5, исправлены архитектурно (CTO-17):

1. CREATE UNIQUE INDEX на партиционированной deals (schema:1263)
   PG требует partition key (received_at) в UNIQUE; включить нельзя —
   ломает идемпотентность webhook'ов. Решение: новая таблица
   webhook_dedup_keys (не партиционированная, PK (tenant_id, source_crm_id)
   → deal_id, composite FK на deals(id, received_at) ON DELETE CASCADE,
   RLS tenant_isolation USING+WITH CHECK). UNIQUE INDEX в deals
   заменён на обычный. Webhook handler — двустадийная UPSERT.

2. GENERATED ALWAYS AS на pd_subject_requests.deadline_at (schema:1999)
   `+ INTERVAL '30 days'` не immutable. Решение: обычная TIMESTAMPTZ
   NOT NULL + триггер trg_pd_subject_requests_deadline + функция
   set_pd_subject_request_deadline().

Изменения:

- db/schema.sql: v8.5 → v8.6 (заголовок, 1 новая таблица, 1 RLS-policy,
  1 функция, 1 триггер, замена UNIQUE на обычный INDEX, замена GENERATED
  на TIMESTAMPTZ NOT NULL)
- db/CHANGELOG_schema.md: новая запись §X v8.5→v8.6
- db/00_create_roles.sql (NEW): deployment-скрипт 4 ролей PG для production
  (crm_app_user, crm_admin_user BYPASSRLS, crm_migrator BYPASSRLS+CREATEDB,
  crm_audit_writer). На dev — postgres superuser (schema §13 разрешает)
- db/02_grants.sql (NEW): GRANT/REVOKE из закомментированных секций §13
  schema. REVOKE на 6 saas-таблицах для crm_app_user (defense-in-depth
  поверх RLS, OPEN-И-14). REVOKE DELETE на 4 финансовых таблицах для
  crm_admin_user (только soft markers)
- app/database/migrations: удалены 3 default Laravel (users/cache/jobs
  дублировались с нашей schema), создан 0001_01_01_000000_load_initial_schema.php
- .squawk.toml: + excluded_paths для db/00_create_roles.sql (psql client-side
  variables :'name' не парсятся libpg_query)
- docs/Открытые_вопросы_v8_3.md: v1.18 → v1.19, CTO-17 закрыт фиксом,
  70  / 5 🟦 / 4 ⏸. Техдолг: ТЗ §15-16 webhook handler нужно обновить
  под двустадийную dedup-логику
- CLAUDE.md: v1.9 → v1.10 (§0 ссылки на schema v8.6 + реестр v1.19;
  §2 метрики БД 54→55/91→92/35→36/12→13/4→5; §6 фундамент развёрнут)
- cspell-words.txt: +9 новых терминов

Smoke-test через Boost MCP database-query:
- 68 таблиц (включая webhook_dedup_keys + 16 партиций)
- 36 RLS-policies
- 35 RLS-enabled (relkind='r'; +2 partitioned 'p' = 37 total)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:48:24 +03:00
Дмитрий e81eeef1fe phase1(db-tooling): squawk v2.51 + pgFormatter v5.9 — закрытие тулчейна фазы 1
Активны Прил. Н #15 squawk и #16 pgFormatter; фаза 1 по тулчейну
закрыта 13/17 (без #17 pg_partman — Windows native стек).

squawk:
- bin/squawk.exe (npm i -g squawk-cli + копия из ~/AppData/...
  /squawk-cli/js/binaries/squawk → bin/squawk.exe; npm-wrapper
  не находит spawn-target без .exe на Windows)
- .squawk.toml: 9 правил исключено (5 bootstrap-неприменимых +
  4 дизайнных решения проекта). Smoke-test на db/schema.sql — 0 issues.
- lefthook.yml: pre-commit job на staged *.sql
- npm run lint:sql

pgFormatter:
- bin/pgFormatter/ (pg_format + lib/ из v5.9 release tarball,
  запуск через Cygwin Perl 5.42.2 из Git for Windows — без
  отдельной установки ActivePerl/Strawberry)
- Без авто-fix хука: diff vs db/schema.sql 3255 строк
  (UPPERCASE→lowercase для типов, плотный одностроковый стиль,
  перетасовка inline-комментариев) — стиль schema.sql ручной,
  авто-fix недопустим
- Только ручной режим: npm run format:sql:check (dry-run + diff),
  npm run format:sql (пишет в db/schema.sql.formatted для review)

Документы:
- Tooling Прил. Н v1.5 → v1.6 (§0 «Что нового», §3.3 таблица,
  §10.1 п.8 ⏸→, §11.4 уточнение по Cygwin Perl)
- CLAUDE.md v1.8 → v1.9 (§0 ссылка, §6 текущая фаза 17/28,
  колонтитул)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:56:29 +03:00
Дмитрий 59f8ca04bb phase1(hooks-fix): сужение pre-push lychee + 2 битых ссылки + allowlist demo HTML
При первом запуске pre-push hook'ов после установки lefthook (`4eee06f`)
обнаружились 3 проблемы — все легитимны, не в самом lefthook'е:

1. **gitleaks-full-history** ловил 42 хита `ru-phone-unmasked` в HTML-концептах
   handoff Платона (liderra_v8_handoff/concepts/v8_*.html и web/v8/v8_*.html).
   Это ДЕМО-данные для визуализации, не реальные ПДн. Добавлены в allowlist
   .gitleaks.toml: `liderra_v8_handoff/concepts/.*\.html` + `web/v8/.*\.html`
   + `app/composer.lock`.

2. **lychee-links** ловил 21 ошибку «Cannot resolve root-relative link» на
   ссылки `/login`, `/register`, `/legal/*` в HTML-концептах. Эти маршруты
   появятся только в фазе 2+ (Vue+Vuetify реализация). Сужен glob
   pre-push lychee — выкинут `web/**/*.html`. Дополнительно добавлены
   `liderra_v8_handoff/concepts` и `web/v8` в .lychee.toml exclude_path
   как защита для других вариантов запуска.

3. **2 РЕАЛЬНЫХ битых ссылки** в narrative — спасибо lychee, нашёл:
   - `docs/CRM_bp-gr_Инструкция_v8_5.md:6128` ссылался на `brandbook.md`,
     но этот файл удалён 08.05.2026 (заменён на `liderra_v8_handoff/docs/
     BRANDBOOK_v2.md` v8 Forest). Исправлен относительный путь.
   - `README.md:88` ссылался на `docs/README_АРХИВ_v8_4.md`, но переименован
     в `_v8_5.md` коммитом `4ffc19a` от 07.05.2026. Поправлено + bump v8.4→v8.5
     в подписи.

Финальный smoke-test после правок:
  - gitleaks detect (full history): «no leaks found»
  - lychee на narrative .md: 122 OK / 0 Errors / 5 Excluded

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:00:15 +03:00
Дмитрий 9ee31b71cb phase1(docs): Tooling v1.5 + CLAUDE.md v1.8 — фиксация фактической установки Boost
Doc hygiene после установки Boost (e04f53b) и Roave/SA (0eb2f72).
Прогноз → факт: что планировалось vs что реально сделано.

CLAUDE.md v1.7 → v1.8:
  - §0: реестр инструментов → Tooling v1.5
  - §6 «Текущая фаза»: 0 → 1 (15/28 активных, перечислены 6 backend-tool'ов
    фазы 1, добавлен smoke-test factual status, отмечены остающиеся
    #15/#16, отметка про #17 → ручной cron)
  - §7 «Laravel Boost»: переписан с прогноза на факт. Wizard сломан
    на кириллице → manual setup. «Отключение guidelines» помечено как
    избыточное (Roster auto-detect делает это сам). Путь Vuetify
    guideline скорректирован: app/.ai/guidelines/vuetify.md
    (был resources/boost/guidelines/vuetify.blade.php)

Tooling v1.4 → v1.5:
  - Шапка + блок «Что нового в v1.5» (Boost+SA active, manual setup)
  - §10.1: 12 шагов перехода 0→1 → таблица «план/статус» с галочками
    на фактических коммитах (e04f53b, 0eb2f72, 30f0335, 0345683)
  - §10.2: добавлено уточнение пути userGuidelineDir = '.ai/guidelines'
    после source-проверки GuidelineComposer

Открытые_вопросы — без изменений (это техническая doc-hygiene, не
продуктовое решение). Memory обновляется отдельно (project_state +
feedback_environment п.26).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:14:24 +03:00
Дмитрий 3a9ed7101e phase1(hygiene): narrative ↔ Laravel 13 — закрытие техдолга v1.17
Точечная синхронизация трёх narrative-документов под Laravel 13
(один токен на файл, без bump'а версии каждого):
  - docs/CRM_bp-gr_Инструкция_v8_5.md:6219
  - docs/Vybor_oblaka_v8_3.md:3
  - docs/Админка_SaaS_v8_2.md:103

Не трогалось намеренно (исторические записи):
  - docs/Объединённый_конспект.md:149 — Часть I, фиксация решений v8.0 на 03.05
  - docs/Открытые_вопросы_v8_3.md:354 → 361 — фиксация результата аудита

Реестр Открытые_вопросы v1.17 → v1.18 (новый блок «закрыт техдолг»).
Оперативная карта CLAUDE.md v1.6 → v1.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:35:31 +03:00
Дмитрий 63c7364e6e reopen(stack): Laravel 11 → Laravel 13 — фиксация переоткрытия
Произошло так: 08.05.2026 при `composer create-project laravel/laravel
app` я не зафиксировал `^11` — Composer подтянул свежайшую
`laravel/framework: ^13.7` (Laravel 13.7, релиз ~Feb 2026), вместо
ожидаемого Laravel 11 LTS.

Smoke-test'ы подтвердили работоспособность Laravel 13:
- php artisan migrate: 3/3 default-миграций OK
- pestphp/pest 4.7.0: 2/2 за 281 ms
- laravel/pint 1.29: pint --test passed
- larastan/larastan 3.9.6 + phpstan 2.1.54: analyse passed (с baseline)
- barryvdh/laravel-ide-helper 3.7.0: ide-helper:generate OK

Live-проверка совместимости 5 ключевых плагинов:
- laravel/boost v2.4.6: composer dry-run резолвит lock без conflict
- остальные 4 — установлены и запущены без issue

Заказчик 08.05 (поздний вечер) принял Laravel 13 как latest stable.
Откат дороговат — rm -rf app/ + повторение всей сессии (~10-15 мин).

По правилу «явная фиксация переоткрытий» обновлены 3 источника:

- CLAUDE.md v1.5→v1.6: §2 backend (Laravel 11 → Laravel 13 + объяснение
  обстоятельств), §0 источники (Tooling v1.4, Реестр v1.17), футер
- docs/Tooling_v8_3.md v1.3→v1.4: блок «Что нового в v1.4» (live-
  проверка совместимости 5 плагинов + установка Pint/Larastan/IDE Helper)
- docs/Открытые_вопросы_v8_3.md v1.16→v1.17: блок «Что изменилось в
  v1.17» с обстоятельствами, импактом и техдолгом

Техдолг (для следующих сессий): синхронизация narrative ТЗ + 2 других
архивных документов под Laravel 13:
- docs/CRM_bp-gr_Инструкция_v8_5.md (строка 6219, главное ТЗ)
- docs/Vybor_oblaka_v8_3.md (строка 3, аналитическая записка по облаку)
- docs/Админка_SaaS_v8_2.md (строка 103, описание стека админки)

Не блокер для разработки, но нарушает связность архива. Включить в
hygiene-проход при следующем апдейте narrative до v8.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:20:55 +03:00
Дмитрий 30f0335f5f phase1(test): Pest 4 swap + reopen(CTO-12) — Pest 3 → Pest 4
Произошло так: при `composer require pestphp/pest pestphp/pest-plugin-laravel
--dev --with-all-dependencies` я не зафиксировал `^3` — composer подтянул
свежайшую Pest v4.7.0 + pest-plugin-laravel v4.1.0. Smoke-test
(`./vendor/bin/pest`) на default-тестах Laravel 11 прошёл 2/2 за 281 ms —
backward-compat с Pest 3 syntax подтверждён. Заказчик 08.05 (поздний вечер)
принял Pest 4 после live-проверки на стеке.

Бонус Pest 4: browser testing (без Dusk), stress testing, mutation
testing v2. Откат дёшев — `composer require pestphp/pest:^3`.

Что сделано:
- composer remove phpunit/phpunit (был direct dev-dep) — phpunit остался
  как транзитивная зависимость Pest
- composer require pestphp/pest:^4.7 pestphp/pest-plugin-laravel:^4.1 --dev
- Pest.php создан через `vendor/bin/pest --init` (`tests/Pest.php`)
- `vendor/bin/pest` smoke-test: 2 passed (Unit/Feature ExampleTest), 281 ms
- `vendor/bin/pest --init` упал на интерактивном промпте «Wanna show Pest
  some love?» в non-interactive PowerShell — Pest.php к этому моменту уже
  создан, нефатально

Reopen(CTO-12): по правилу «явная фиксация переоткрытий» обновлены
3 источника:
- docs/Открытые_вопросы_v8_3.md v1.15→v1.16: блок «Что изменилось в v1.16»,
  таблица §0 (CTO-12 переоткрыт+закрыт), запись §3 (Pest 4 + обоснование),
  финальный список §X (Pest 4)
- docs/Tooling_v8_3.md v1.2→v1.3: блок «Что нового в v1.3», §3.1 п.4
  (Pest 4 в boost:install), §3.4 строка 18 (Pest 4 + бонусы), §6 п.2
  (конфликт Pest↔PHPUnit), §10.1 п.9 (процедура установки + warning про
  --init на Windows non-interactive)
- CLAUDE.md v1.4→v1.5: §0 источники (Tooling v1.3, Реестр v1.16),
  §3.2 строка 18 (Pest 4), §7 п.5 (Pest 4 в boost:install), футер с
  историей версий

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:56:20 +03:00
Дмитрий 477e866726 phase1(stack): native Windows вместо Sail — Tooling v1.2, CLAUDE.md v1.4
Обнаружено 08.05.2026 (вечер): машина — VPS на OpenStack
(Manufacturer = OpenStack Foundation, HypervisorPresent = True).
Provider не пробрасывает nested virtualization в guest VM
(VirtualizationFirmwareEnabled = True на CPU, но KVM на хосте
блокирует VT-x для гостя). Docker Desktop показывает «Virtualization
support not detected, Engine stopped».

Следствие: ранее обсуждённые варианты A (полный WSL2) и B
(Sail на Windows FS) — физически невозможны на этой машине.
Переход на вариант D — native Chocolatey-стек.

Изменения:
- Tooling v1.1 → v1.2: §3.1 п.2/п.3 — Sail удалён из allow-list
  Boost. Шапка обновлена под native-стек. Блок «Что нового в v1.2»
  с обоснованием.
- CLAUDE.md v1.3 → v1.4: §0 (версия Tooling v1.2), §6 (стек dev =
  native PG 16 + Memurai + PHP 8.3), §7 п.3 (Sail отключён в
  boost:install). Футер.

Установлено 08.05 вечер:
-  Docker Desktop — удалён (choco uninstall docker-desktop)
-  PostgreSQL 16.13 — устанавливается (choco install postgresql16)
-  Memurai Developer 4.1.8 — устанавливается (Redis 7-совместимый,
  free до 1GB RAM)

pg_partman/pg_audit/pg_anonymizer на native Windows — ручная
замена. На MVP: партиции через php artisan cron-команду
(Биз-партиции уже в schema v8.5), audit-trigger'ы уже встроены.

Подробное обоснование и триггеры пересмотра — в personal memory
project_phase1_strategy.md (не коммитим).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:03:51 +03:00
Дмитрий 7f28721723 fix(typo): laravel/liderra → laravel/laravel в триггере фазы 1
CLAUDE.md §6 и Объединённый_конспект Часть X указывали
`composer create-project laravel/liderra app`. Такого Composer-
шаблона на Packagist нет. `liderra` — название нашего продукта,
а не пакета. Правильный official-стартер: `laravel/laravel`.

Замечено при smoke-test окружения 08.05 (готовка к фазе 1, вариант B —
Sail на Windows FS). Без этого фикса первая команда фазы 1 упала бы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:31:14 +03:00
Дмитрий e1641f2e16 docs(hygiene 08.05): Часть X конспекта — ребрендинг + CTO-12 + Диз-1
Доделка консистентности архива по итогам сессии 08.05.2026
(коммиты 887abf4 / 3dca049 / 62a85f2 / 63c7c83).

Объединённый_конспект.md:
- Шапка: «03–05.05.2026, до v8.3.2» → «03–08.05.2026, до v8.5 +
  ребрендинг»
- Новая Часть X «РЕБРЕНДИНГ + HANDOFF ПЛАТОНА + ЗАКРЫТИЯ CTO-12 /
  Диз-1 (08.05.2026)» — 11 подразделов: handoff, выборочная
  интеграция (4 расхождения handoff vs ТЗ), массовая замена 33
  файла / 449 замен, CLAUDE.md v1.0→v1.1, реестр v1.12→v1.13,
  cspell+push 887abf4/3dca049, CTO-12 закрыт (Pest 3),
  Диз-1 закрыт (handoff покрыл), метрики на конец дня (78/69/5/4),
  что осталось, 5 уроков сессии.

Приложение_Б_В_БД_диаграммы — без изменений: schema 08.05 не
менялась, дельта v8.2→v8.5 уже добавлена 07.05 в a4d15ee.

Lint markdownlint: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:00:33 +03:00
Дмитрий 63c7c83d82 close(Диз-1): HTML-прототипы покрыты handoff'ом Платона — реестр v1.15, CLAUDE.md v1.3
Решение заказчика 08.05.2026: формально закрыть Диз-1 (P1) — handoff
v8 Forest от Платона покрыл HTML-прототипы на 100%+ (13 экранов в
web/v8/ против 8 запланированных + 3 экрана ошибок). Прил. Л не
выпускается (избыточно после handoff'а).

Точечные расхождения handoff vs ТЗ (зафиксированы в шапке v1.13)
реализовывать по ТЗ/schema:
- 14 «обобщённых» статусов в BRANDBOOK_v2 §3.6 ≠ 14 slug'ов в schema
- 3-й click-wrap в v8_login.html ≠ ТЗ §1.5/§4.1
- SSO «локальный 2FA fallback» в v8_admin.html ≠ ТЗ OPEN-И-13

axe: 81 violation на 10/13 HTML (color-contrast на декоративных
separator'ах) — исправлять при реализации в Vue+Vuetify.

Изменения:
- Открытые_вопросы_v8_3.md v1.14 → v1.15: Диз-1 в §5 → ; сводка §0
  (дизайнер 2→3 закрыто, итого 78/68→69 , 5→4 ⏸, 4→3 P1)
- CLAUDE.md v1.2 → v1.3: §0 (версия реестра v1.15), §6 (P0-блокер
  упоминание Диз-1 закрыт), футер

Открыто на 08.05.2026: **4 ⏸ продуктовых** — Б-1 (P0) + 3 P1 (Диз-3,
DO-2, DO-4), все ждут Б-1. Один настоящий блокер — Б-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:51:24 +03:00
Дмитрий 62a85f2a08 close(CTO-12): Pest 3 — реестр v1.14, Tooling v1.1, CLAUDE.md v1.2
Выбран Pest 3 (не PHPUnit). Обоснование:
- datasets лаконично покрывают 14 статусов воронки и 34 RLS-политики
- architecture testing закрепляет правила Claude на код-уровне
  (например, integration тесты не моки́руют БД — линт вместо памяти)
- родной для Laravel/Boost (Nuno Maduro в core team) — один guideline
  вместо разрыва на boost:install
- Pest на PHPUnit под капотом — откат дёшев

Изменения:
- Открытые_вопросы_v8_3.md v1.13 → v1.14: запись о закрытии в §3,
  сводка §0 (CTO 15→16 закрыто, итого 77→78 / 67→68 )
- Tooling_v8_3.md v1.0 → v1.1: §3.1 п.4, §3.4, §6 п.2, §10.1 п.9
- CLAUDE.md v1.1 → v1.2: §0 (версии источников), §3.2 строка 18,
  §7 п.5, футер

Импакт: при composer create-project (триггер фазы 1) — вместо
шага «решить Pest или PHPUnit» теперь composer require pestphp/pest
--dev --with-all-dependencies + vendor/bin/pest --init. Guideline
PHPUnit на boost:install отключить.

P0-блокер фазы 1 остаётся один — Б-1 (юр. лицо).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:44:11 +03:00
Дмитрий 887abf444e rebrand(v8.5→Лидерра): дизайн-handoff Платона v8 Forest + Лидпоток→Лидерра
Получен handoff-пакет liderra_v8_handoff/ от дизайнера Платона
(kpd9363@gmail.com) от 07.05.2026 — v8 Forest. Заказчик 08.05 решил
применить только в части дизайна, имени, логотипа. Функционал, состав
страниц и правила (CTO-11, click-wrap, SSO break-glass, 14 статусов
воронки) — без изменений (источник — ТЗ v8.5/schema v8.5).

Что сделано:

- Массовая замена Лидпоток→Лидерра (с учётом падежей: Лидерры/Лидерре)
  в 33 файлах (449 вхождений) — все .md/.sql/.json/.toml/.yml/.txt/.html,
  кроме исторических упоминаний внутри liderra_v8_handoff/
- Удалён docs/brandbook.md v1.1 — заменён на BRANDBOOK_v2.md из handoff
- Скопированы 13 концептов liderra_v8_handoff/concepts/v8_*.html в
  web/v8/. Удалены старые web/01-login.html, 02-dashboard.html,
  03-deals.html, index.html (палитра v1.1 deprecated)
- CLAUDE.md v1.0→v1.1: §0 (BRANDBOOK_v2 + DEVELOPER_HANDOFF в источниках),
  §2 (палитра Forest, Inter+JBM, Lucide), §5 п.6 (anti-pattern Inter
  снят — в Forest Inter наш основной шрифт), §6 (13 концептов в web/v8/)
- Реестр Открытые_вопросы_v8_3.md v1.12→v1.13: добавлена запись о
  ребрендинге + 4 точечных расхождений handoff vs ТЗ (статусы воронки,
  click-wrap чекбоксы, SSO fallback, axe violations)
- package.json/package-lock.json: name lidpotok→liderra

4 расхождения handoff vs ТЗ (НЕ применены, источник истины — ТЗ/schema):

1. 14 «обобщённых» статусов в BRANDBOOK_v2 §3.6 ≠ 14 slug'ов в
   schema.sql:2076 (совпадает 2 из 14: «Переговоры», «Оплачено»).
   Источник — schema/ТЗ §6.4 (реселлерская модель из аудита crm.bp-gr.ru,
   6 системных + 8 настраиваемых статусов).
2. 3-й click-wrap в v8_login.html («маркетинг-опционально») ≠ ТЗ §1.5/§4.1
   («согласие на ПДн», обязательное, OPEN-Ж-3).
3. SSO в v8_admin.html («локальный 2FA fallback») ≠ ТЗ OPEN-И-13
   (break-glass super_admin, локальный 2FA выключен).
4. Заявление «axe-core 4.10.2 — 0 violations» в README handoff — локально
   Pa11y 9.1.1 + axe нашёл 81 violation на 10/13 HTML (преимущественно
   color-contrast на декоративных separator'ах с --ink-disabled).
   Чисто: settings/errors/palette_options.

Что НЕ включено в коммит:
- лендинг/TZ_landing_v1_0.md — untracked, не моя работа в этой сессии
- .tmp/ — gitignored

Что осталось (для следующих сессий):
- Возможное переименование GitHub-репо CoralMinister/lidpotok → liderra
  (отдельное решение заказчика)
- Опционально: обратная связь Платону по 4 расхождениям handoff vs ТЗ

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:11:58 +03:00
Дмитрий a4d15ee1d5 docs(hygiene v8.5): Часть IX конспекта + дельта v8.5 ER-диаграмм
Доделка консистентности архива после реализации v8.5
(коммиты aabf827 / 038a884 / 4ffc19a).

Объединённый_конспект.md:
- Новая Часть IX «v8.3.3 + v8.4 ФИНАЛ + МИГРАЦИЯ + АУДИТ C → v8.5
  (06–07.05.2026)» — хроника двух дней (8 подразделов: хронология,
  v8.4 финал, миграция на Server 2022, аудит C, реализация v8.5
  тремя коммитами, метрики, что осталось, 5 уроков).

Приложение_Б_В_БД_диаграммы_v8_3.md:
- Шапка обновлена: schema v8.5 (54/91/35/4 роли/12 триггеров/4 функции).
- Новая секция «Дельта v8.2 → v8.5» — компактная сводка изменений
  schema, поскольку Mermaid-диаграммы фиксируют срез v8.1.
  Перерисовка отложена до спринта 0/1 (следуем существующему
  disclaimer'у файла).
- Перечислены: +1 таблица (project_user_assignments), +26 колонок
  с разбивкой по таблицам и Биз/CTO/OPEN-И источникам, +5 индексов,
  +2 WITH CHECK + 1 новая RLS, +6 REVOKE, +1 роль (crm_audit_writer),
  +4 функции (audit_chain_hash/audit_block_mutation/
  report_jobs_log_export/calc_lead_score), +12 триггеров, ALTER
  api_keys.expires_at, закомментированный задел call_recordings.

cspell-words.txt: +KDV (старая машина), +коммита/коммитов.

Артефакт c--Users-KDV-Projects-lidpotok/ (~25 МБ JSONL старых сессий)
удалён из рабочей директории — был в .gitignore, в git не попадал,
коммит не затрагивает.

Lint+spell чистые.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:27:08 +03:00
Дмитрий 4ffc19a7d2 docs(narrative): v8.4 → v8.5 — реализация 27 решений аудита C
Завершает блок реализации v8.5 после schema-коммита 038a884.

Переименования:
- docs/CRM_bp-gr_Инструкция_v8_4.md → _v8_5.md
- docs/README_АРХИВ_v8_4.md → _v8_5.md

Новые подразделы narrative (in-place):
- §10.8: Антифрод дублей (Биз-19), routing (Биз-17, CTO-16), scoring
  (Биз-22), регион (Биз-23), эскалация (OPEN-И-25), call-recordings
  задел (OPEN-И-26)
- §12.5.5: TTFR-SLA (Биз-18), UTM-когорты (CTO-14)
- §14.8: Append-only audit hash chain (OPEN-И-15), report_jobs export
  trigger (OPEN-И-20)
- §17.9: Telegram-бот (Биз-20), эскалация-нотификации (OPEN-И-25),
  late waiting_payment alert (Биз-24)
- §19.10.11–12: DNS-rebinding pin-IP (OPEN-И-18),
  marketing.conversion event (Биз-21)
- §22.13: SSO Yandex 360 + JIT + break-glass (OPEN-И-13), SET LOCAL
  test plan (CTO-13), RLS WITH CHECK + REVOKE (OPEN-И-14), Sentry
  PII whitelist+regex (OPEN-И-16), anti-DDoS Nginx+SmartCaptcha+
  blacklist (OPEN-И-21), TTL secrets 365d (OPEN-И-17/19),
  two-person impersonation (CTO-15+Ю-9), audit hash chain link
- §23.10.11: SSO login UI, break-glass dashboard, two-person UI,
  152-ФЗ ст.21 hard-block UI
- §7.1: метрики обновлены 53→54/86→91/33→34/3→4/12 триггеров/4 функции

Прил. И Часть Г (новая) — 9 операционных процедур:
- Г.1 RLS smoke-test через PgBouncer (CTO-13, BLOCKER для фазы 1)
- Г.2 Cron audit:verify-chain (OPEN-И-15)
- Г.3 Cron secrets:notify-expiring (OPEN-И-17)
- Г.4 Anti-DDoS (Nginx + Yandex SmartCaptcha + disposable-blacklist)
- Г.5 Per-tenant DEK + crypto-shred (OPEN-И-22)
- Г.6 pg_anonymizer для staging (OPEN-И-24)
- Г.7 Yandex 360 SSO setup (OPEN-И-13)
- Г.8 Cron leads:escalate-stale (OPEN-И-25)
- Г.9 Cron payments:notify-stale (Биз-24)

Кросс-ссылки обновлены: CLAUDE.md, README.md, web/index.html,
README архива (переименован v8_4→v8_5). Schema.sql v8.5 шапка
указывает на narrative v8_5.md.

cspell-words.txt: +SPE, +gethostbyname.

Lint+spell чистые. Архитектурно фаза 1 (composer create-project)
разблокирована — требуется только Г.1 (e2e-тест RLS) в спринте 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:09:22 +03:00
Дмитрий aabf827f76 docs(audit-C): закрытие 27 вопросов аудита C → реестр v1.12
Решение заказчика 07.05.2026: «A везде» по всем 27 вопросам аудита C.

8 P0 разблокированы для триггера фазы 1 (composer create-project):
- Биз-17 (manual routing), Биз-18 (TTFR 15 мин + alert),
  Биз-19 (24ч-дедуп без списания, duplicate_of_id)
- CTO-13 (e2e SET LOCAL+PgBouncer тест в спринте 1)
- OPEN-И-13 (OIDC+JIT+break-glass), OPEN-И-14 (WITH CHECK + REVOKE),
  OPEN-И-15 (append-only + hash-chain + crm_audit_writer),
  OPEN-И-16 (Sentry whitelist+regex)

12 P1 + 7 P2 закрыты с импактом на schema/narrative
(см. §13.10.4 — schema → 54 таблицы, +6 триггеров, +1 роль).

Реализация v8.5 — отдельный коммит (схема + narrative + CHANGELOG).

Сводка §0: 67  / 5 🟦 / 5 ⏸ (1 P0 Б-1, 4 P1 ждут Б-1 или у Claude).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:30:01 +03:00
Дмитрий 4d65d6a523 docs(audit-C): раздел 13 — 27 новых вопросов из аудита C (07.05.2026)
Аудит C — архитектурно-функциональный (CRM-best-practices vs narrative
v8.4) + STRIDE threat modeling. Запускались два независимых read-only
general-purpose агента: C-1 (арх) и C-2 (security).

Найдено 27 новых открытых вопросов:
- Бизнес/продукт (8): Биз-17..24 — автораспределение, TTFR-SLA,
  антифрод дублей, Telegram, маркетинг-конверсии, lead scoring,
  гео-таргетинг сделок, нотификация просрочки.
- CTO (4): CTO-13..16 — SET LOCAL+PgBouncer тест (P0), UTM в deals,
  impersonation two-person, skill-based routing.
- OPEN-И — DevOps/security (14): OPEN-И-13..26 — Yandex 360 SSO flow,
  RLS WITH CHECK + REVOKE, append-only audit hash-chain, Sentry PII
  scrubbing, ротация секретов cron, DNS-rebinding, лимит api_keys,
  signed URL, anti-DDoS, crypto-shred, audit-writer role,
  pg_anonymizer, эскалация лидов, call_recordings задел.
- Юрист (1): Ю-9 — 152-ФЗ ст.21 ч.5 запрет impersonation в
  processing_restricted.

8 P0 (4 архитектура + 4 security), 12 P1, 7 P2. Каждый вопрос
с тремя вариантами решения A/B/C и рекомендуемым.

Закрытий нет — правило §2.2. Все 5 ⏸ из v1.10 в работе.

Версия Открытые_вопросы v1.10 → v1.11. Сводка §0 пересчитана:
50 → 77 продуктовых, 5 → 32 ⏸, 1 → 9 P0, 4 → 16 P1, 0 → 7 P2.

cspell-words.txt: +антифрод, TTFR, UTM, utm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:03:48 +07:00
Дмитрий 574154bcf9 fix(schema+narrative): B-5 — schema P1 + синхронизация метрик
schema.sql v8.4 (hotfix Z.5.3-Z.5.6):
- outbound_webhook_subscriptions.events: убран DEFAULT '[]' (конфликт
  с CHECK jsonb_array_length>0). NOT NULL остался — приложение должно
  явно передать список событий ≥1.
- deal_tag_pivot: добавлены ENABLE RLS + CREATE POLICY tenant_isolation
  через JOIN на deal_tags(tenant_id) — паттерн как у saas_invoice_items.
- Шапка schema.sql:107-108: «33 политики / 34 защищённых» → «34/34, 1:1»
  (после правки выше). CHANGELOG_schema.md: расширена запись Z.5
  (Z.5.3-Z.5.6) с финальными метриками.

narrative v8.4:
- §1.4, §3.2, §7.1, §22.6, §27 «33 политики на 34» (5 мест) → «34/34,
  1:1». Шапка «Что нового в v8.4»: +3 RLS вместо +2 (с учётом hotfix).

Прил. Б+В:
- Шапка ссылалась на «schema.sql v8.3, 51 таблица» → актуально:
  «schema.sql v8.4, 53/86/34/34, при расхождении приоритет за schema.sql».
  Добавлены изменения v8.4 в перечень того, что не отражено в ER.

cspell-words.txt: добавлено «партиционированной» (склонение, нужно для
CHANGELOG Z.5.4).

Метрики schema.sql v8.4: 65 CREATE TABLE (53+12), 86 индексов,
34 RLS-политики, 34 ENABLE RLS, 3 роли БД.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:19:20 +07:00
Дмитрий 08c8b3705c fix(narrative): B-4 — внутренняя консистентность v8.4
- §1.4/§3.2/§22.6/§23.* «29 политик» (4 места) → «33 политики на
  34 защищённых таблицах» (расхождение по `saas_invoice_items` —
  намеренное, защита косвенно через FK на `saas_invoices`).
- §3.2: ссылка «schema.sql v8.1, раздел 12» → «db/schema.sql v8.4».
- §7.* «обновление updated_at через trigger» (стр. 1594) уточнено:
  Eloquent `$timestamps = true` (application-уровень). Trigger в БД
  не вводим на MVP, чтобы не дублировать ORM. В schema.sql triggers нет.
- §7.3 — добавлен явный disclaimer: источник истины = `db/schema.sql`,
  inline-DDL ниже могут отставать (см. CHANGELOG_schema.md).
- §4.1 (стр. 660-666): добавлен 3-й чекбокс «Согласие с Политикой
  конфиденциальности» — синхронизация с §1.5 (3 click-wrap, OPEN-Ж-3).
- §5.1 (стр. 5693): «Только webhook (без CSV в MVP)» → «webhook
  основной + CSV-импорт опциональный модуль (раздел 6) + ручное
  создание (раздел 10.7)» — синхронизация с §1.5 и §6.
- Заключение (стр. 5878): шапка v8.1 от 03.05.2026 → v8.4 от
  06.05.2026, P0-блокеры 9→1 (Б-1), следующие шаги переписаны
  с учётом фактического статуса.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:05:53 +07:00
Дмитрий 1ffd5c1609 fix(narrative): B-3 — домены и сервисы синхронизированы с DO-1/Ю-7
- Все 15 упоминаний placeholder-домена `crm-аналог.ru` заменены на
  `lidpotok.ru` (имя проекта; DO-2 «купить домен» — отдельный P1).
  Затронуты: webhook URL, register/verify-email links, wildcard
  subdomains, *.lidpotok.ru SSL, staging.lidpotok.ru, status.lidpotok.ru,
  admin@lidpotok.ru.
- Таблица §11.1 (стр. 363-364): «Yandex / VK Cloud (prod)» → только
  «Yandex Object Storage (prod, по DO-1)». Email: «Mailgun / SendGrid /
  собственный SMTP» → «Unisender Go (по Ю-7)».
- §17.7 Library (стр. 2845): «SMTP / Mailgun API» → «SMTP-relay
  (Unisender Go, см. Ю-7)».
- §22.* (стр. 4497-4499): список РФ-провайдеров получил пометку
  « Yandex Cloud — выбран по DO-1, VK/Selectel — альтернативы Прил. К
  не выбраны». Это точнее, чем простой перечень.

Историческая запись «Ю-7 — Mailgun → Unisender Go» в §1.* шапки
оставлена как есть (показывает решение Ю-7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:47:00 +07:00