- JivoWidget.vue: подгружает скрипт Jivo при заданном VITE_JIVO_WIDGET_ID
(по образцу SmartCaptchaWidget); ключ пуст → ничего не грузит, виджет спит.
- Смонтирован в AppLayout (клиентский портал). +VITE_JIVO_WIDGET_ID в vite-env.d.ts.
- TDD: JivoWidget.spec (грузит при ключе / спит без) GREEN; AppLayout.spec 16/16; eslint 0.
- Активируется вставкой VITE_JIVO_WIDGET_ID (твой ID канала Jivo). На прод не выкачено.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ReportsView.onCancel снимает submitSuccess — success-баннер больше не висит после отмены отчёта. TDD: тест «отмена задачи сбрасывает зелёный» (RED→GREEN), весь ReportsView.spec 22/22, eslint 0.
- docs: ui-audit-round2 — минор тоста помечен исправленным (открытых пунктов не осталось); stage5-checklist — supplier:rekey-orphans dry-run проверен на проде 22.06 («No orphan»), разовая миграция не нужна.
- На прод не выкачено.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- NewProjectDialog.submit(): обязательны название и источник (домен/номер/отправители по типу сигнала); ошибка показывается сразу, без обращения к серверу. +2 TDD-теста, два существующих теста обновлены под новый гейт. Полный фронт-сьют — без новых падений; проверено глазами в браузере на локалке.
- docs: пометки находок приёмки/UI/impersonation/ADR-018 приведены к фактическому прод-статусу (M-1, M-2 капча Yandex, FN-RESET/2/3/ENC, F-CSV, apiv1-rate, N-4, F-T1, F-P1 — на проде; failed_jobs очищены 494191->0; tenant 24 удалён soft-delete).
- решение владельца 22.06: admin-area доделки (saas-admin SSO, two-person approval, role-guard супер-админа, supplier fallback, обезличенный admin_user_id) сняты как отдельные задачи — доделать единым пакетом вместе с подключением Yandex SSO после ООО.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Поправки после первой версии (по замечаниям владельца):
- «Запас» считаем по текущему заказу проектов (requiredLeadsPerDay), так же как
BalanceCapacityIndicator: 0/день → ∞ (баланс не расходуется), N/день → лиды/N.
Раньше брался бэкендный runway_days (историческая скорость за 30 дней) —
давал «2192 дн.» рядом с «при 0 лидов в день», выглядело фейком/противоречиво.
- Убрана строка-чтение под рядом (дубль).
- Убран BalanceCapacityIndicator из BillingView: дублировал ряд и писал
«по тарифу», хотя тарифов нет.
- Рамки трёх карточек выровнены по высоте (единый border/radius + flex-stretch,
убран конфликтный height:100%).
Тесты BalanceCard/BillingView обновлены. vitest 19/19, vue-tsc и build — зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Тарифов как сущности нет (цена за лид считается по объёму, 7 ступеней) —
карточка «Тариф» («не выбран» + вечно-неактивная кнопка) убрана. На её место —
«Запас» (runway_days): на сколько хватит баланса при текущем заказе. Ряд связан
стрелками + строка-чтение «N ₽ = M лидов = надолго / на K дн.». Число лидов
теперь с разделителем тысяч. Дубль «хватит на N дн.» убран из шапки.
Тесты BalanceCard.spec / BillingView.spec обновлены под новую структуру.
Сборка и vue-tsc — зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На странице Канбан кнопка создания сделки в правом верхнем углу не нужна —
убрана. Оставшаяся «Обновить» прижимается к правому краю прежним
space-between заголовка, отступ — обычный pa-6 контейнера.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
При зелёной проверке всех партиций таблицы audit:verify-chains теперь закрывает
оставшиеся открытые инциденты разрыва hash-chain по этой таблице. Убирает класс
вечно-открытых ложных инцидентов после транзиентного разрыва — например строк
тест-тенантов приёмки, удалённых teardown.
Диагностика прогона 22.06: 4 m06-инцидента 576-579 были только по строкам
тест-тенантов; teardown их удалил, боевые цепочки tenant 2 целы.
TDD: 2 теста (целая таблица закрывает инцидент; сломанная — не трогает).
Pint и Larastan чисто, регрессий нет.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Один невалидный UTF-8-байт в выгрузке лидов crm.bp-gr.ru ронял весь импорт на
INSERT (PG: invalid byte sequence for encoding UTF8, SQLSTATE 22021). str_getcsv
пропускает любые байты, и невалидная последовательность доходила до БД.
CsvLeadsParser::parse теперь чистит невалидный UTF-8 через mb_convert_encoding
до парсинга — битый байт заменяется, строка импортируется, очередь не падает.
TDD CsvLeadsParserUtf8Test, проверено руками на PG.
Также зафиксирован вывод по FN-RLS-CTX: no-action — путь projects уже под
tenant-middleware, старый no-auth endpoint заменён, не воспроизводится.
Прод не трогался. Накат — позже вместе с остальным.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FN-3: онбординг-confirm не ставил users.email_verified_at — поле вне fillable,
mass-assign его гасил. RegistrationService::confirm теперь forceFill is_active +
email_verified_at. TDD ConfirmSetsEmailVerifiedAtTest.
FN-2: миграция 2026_06_19_130000 дропнула таблицу reminders, но забыла
ALTER COLUMN notification_preferences SET DEFAULT — реальный DB-дефолт оставался
с мёртвым ключом reminder, расходясь с каноном schema.sql v8.45. Новая миграция
2026_06_22_120000 метаданные-only выравнивает дефолт под канон. squawk 0 issues,
применено dev+testing, проверено psql. CHANGELOG v8.50 + шапка schema.sql.
TDD NotifPrefsDefaultNoReminderTest.
FN-1 переоценён как не-баг кода: resolvePlatforms даёт B2+B3 корректно, B3-miss —
supplier-side Doubles, джоба сама ретраит. Зафиксировано в отчёте приёмки.
Прод не трогался. Накат — позже вместе с остальным.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FN-RESET: письмо сброса строило именованный роут password.reset которого нет в SPA.
ResetPassword::createUrlUsing → /reset/{token}?email= в AppServiceProvider boot.
FN-LOGIN-ROUTE: гость без Accept json на auth:sanctum уводил в именованный роут
login которого нет → 500. redirectGuestsTo /login + render AuthenticationException
→ 401 JSON для api/*.
FN-SESSION: chromium.launch стоял вне try/catch — отказ запуска браузера маскировался
unhandled-rejection в opaque exit 1 двойник login-rejected. launch в try + top-level
catch → чистый exit 4 + JSON stderr в refresh-session.js и manage-project.js.
Тесты: PasswordResetUrlTest, UnauthenticatedApiResponseTest, node:test launch-failure
в обоих playwright-скриптах. Разбор FN-SESSION + ops-долг playwright install под
www-data + поправки отчёта приёмки + новая находка FN-INN-LOOKUP.
Прод не трогался. Накат — позже вместе с остальным.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vite.config: в command=build VITE_YANDEX_SMARTCAPTCHA_SITEKEY дефолтит на
публичный ключ капчи liderra-signup, если не задан в env. Сборка теперь
воспроизводима без gitignored .env (не регрессирует в stub при пересборке).
env/.env переопределяет; dev/serve и Vitest остаются на stub-фоллбэке.
Прод уже несёт идентичную сборку (manifest sha совпадает) — редеплой не нужен.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
N-4: пометка digest_sent ПОСЛЕ отправки (mark-after-send) — повтор не
дублирует И сбой джоба до отправки не теряет дайджест. TDD-тест GREEN 6/6.
N-2: инъекции R2 с TEST-диапазоном vid 9e12..1e13 → R5 чистит 4 orphan-таблицы
детерминированно по vid/lead_id, без ручного манифеста.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI-аудит раунд 2: APP_LOCALE=ru, но директории lang/ не было → Laravel отдавал
сырой ключ «validation.required» во все формы без кастомных messages() (профиль,
создание проекта, реквизиты для оплаты и т.д.). Auth-формы свой messages() имеют.
Добавлен каноничный ru-перевод (laravel-lang) + секция attributes с русскими
именами полей продукта (Имя/Телефон/Лимит лидов в день/ИНН/…).
Верификация: trans(validation.required)→«Поле … обязательно для заполнения.»;
форма профиля в Playwright показывает «Поле Имя обязательно для заполнения.»
вместо «validation.required».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
N-4: SendNewLeadsDigestJob выбирал сделки received_at>now()-30min без маркера
отправки → ручной/повторный прогон (R3b велит дёргать вручную) дублировал
письмо-дайджест. Окно «непересекается» только при ровно-30-мин прогонах.
Фикс без схемы: идемпотентность по сделке через Redis SETNX
(Cache::add 'digest_sent:<id>', TTL 1 сутки) — паттерн как rate-limit
ZeroBalancePausedMail. Уже отправленная сделка в дайджест повторно не входит.
TDD: тест «повторный прогон НЕ дублирует» (RED 2 письма → GREEN 1), сюит 5/5.
R3b DIGEST-ON + owner-decisions + NEW-статус обновлены (N-4 → закрыто).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F-REMIND хвост (ревью раунда 2): комментарий currentPageTitle приводил
«Напоминания» как пример страницы вне sidebar — фича снята (G3). Оставлен
живой пример «Импорт данных».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ревью раунда 2: F-CSV шире — формула может уйти не только через DealExport, но
и через центральные писатели ОТЧЁТОВ (Managers/Sources/Billing/Deals × csv/xlsx).
Прежний фикс нейтрализовал в DealsExportProvider — неверный слой: он кормит и
JSON-формат, где апостроф портил данные (ReportJobControllerTest).
Перенос защиты в писатели:
- CsvFormatter — CsvFormulaGuard::neutralizeCell (апостроф, числа не трогаются)
- XlsxFormatter — опасные строки через setCellValueExplicit TYPE_STRING
(Excel НЕ вычисляет формулу; XLSX опаснее — считает без предупреждения)
- DealsExportProvider — откат к сырым данным (JSON больше не портится)
CsvFormulaGuard: + isDangerous()/neutralizeCell() — numeric-aware (ведущий «-»
числа не экранируется).
TDD: unit-тесты CsvFormatter + XlsxFormatter (load xlsx → datatype 's', не 'f').
DealExportController (OpenSpout, отдельный путь Сделок) — без изменений.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F-REMIND: фича «Напоминания» снята (G3, drop_reminders_table), но во фронте
остались следы. Убран осиротевший член типа NotificationEventKey 'reminder'
(api/auth.ts) — он нигде не рендерился (в EVENTS NotificationsTab его нет,
мёртвого переключателя не было) — и устаревшее упоминание «напоминания» в
докблоке DealDetailBody.vue. type-check + eslint чисты.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F-CSV: ячейки экспорта писались «как есть» — значение вроде =HYPERLINK(...)
или @SUM(...) в комментарии/контакте/городе при открытии файла в Excel/
LibreOffice исполнялось как формула (OWASP Formula Injection).
Новый App\Support\CsvFormulaGuard::neutralize() префиксует апострофом ячейку,
начинающуюся с = + - @ TAB CR. Применён к свободному тексту в:
- DealExportController (телефон/источник/город/статус/комментарий)
- DealsExportProvider (телефон/контакт/статус/проект/менеджер)
Числовые колонки (id/суммы/даты) не трогаются — ведущий «-» там легитимен.
TenantChargesController НЕ уязвим: колонки enum(CHECK)/число/дата, свободного
текста нет.
TDD: 13 unit-тестов хелпера + feature-тест экспорта сделок + provider-тест.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
G2-B (19.06) флипнул new_lead.email false→true — дайджест по умолчанию ВКЛ.
Докблок NotificationService ссылался на устаревший дефолт (email:false) и
старую строку schema.sql:699. Поправлено на канон-схему (email:true).
Только комментарий, без изменения логики.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F-DEPTRAC: middleware ImpersonationContext зависел от слоя Mail
(ImpersonationEndedMail) — нарушение deptrac ruleset, блокировало ВСЕ
php-коммиты. Отправка письма + завершение сессии вынесены в новый
ImpersonationExpiryService (Service-слой, которому Mail разрешён,
идемпотентно). Middleware теперь зависит только от Service.
deptrac: 0 нарушений. TDD: 2 теста сервиса (письмо в очереди + идемпотентность).
phpstan-baseline.neon перегенерён под Pest $this-false-positives новых тестов
этой серии правок приёмки (level 5, composer stan = 0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>