Commit Graph

514 Commits

Author SHA1 Message Date
Дмитрий 2130cd74e9 feat(autopodbor): формы создания/правки конкурента через элементы - правка не теряет карточки
Бэк: хелпер CompetitorElements нормализует элементы и выводит легаси site_url/directory_urls
из ВСЕХ карточек. Ручки manual/update принимают elements.
Фронт: компонент CompetitorElementsEditor встроен в формы создания и правки на экранах
Поле и Предложения. Прежний баг - пересбор directory_urls из двух полей - закрыт.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:32:16 +03:00
Дмитрий d7c63a2962 feat(autopodbor): конкурент из элементов - слияние без потерь
Конкурент хранит список элементов {name,sites,cards} - подкомпании/ЖК холдинга.
Слияние складывает элементы всех объединяемых, ничего не роняет.
Миграция бэкфилла: один элемент из текущих site_url + directory_urls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:14:39 +03:00
Дмитрий 82e30d0548 test(автоподбор): end-to-end хранение строк глубокого сбора — близнецы = две записи в БД 2026-07-03 23:59:00 +03:00
Дмитрий 34a43c3969 style(автоподбор): Pint по пакету Study + провайдеру 2026-07-03 23:46:28 +03:00
Дмитрий 8a7b2ec12d feat(автоподбор): DI-сборка DeepStudyCollector за тумблером 2026-07-03 23:43:45 +03:00
Дмитрий 8fafbc0ba8 feat(автоподбор): развилка studyCompetitor по тумблеру deep_study (интерфейс SourceCollector) 2026-07-03 23:40:44 +03:00
Дмитрий 1cd215c23f feat(автоподбор): DeepStudyCollector — движок глобального сбора (близнецы порознь) 2026-07-03 23:32:23 +03:00
Дмитрий 9ff837bdb9 feat(автоподбор): SiteOpener — вскрытие сайта фирмы
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 23:23:53 +03:00
Дмитрий 1f85403fca test(автоподбор): FootprintSearch — гарантия «пустой ключ = 0 запросов» (assertNothingSent) 2026-07-03 23:22:50 +03:00
Дмитрий 3ca6af7a65 feat(автоподбор): FootprintSearch — EXA-поиск следа конкурента 2026-07-03 23:19:59 +03:00
Дмитрий afed14b0bd feat(автоподбор): тумблер deep_study + проброс is_federal в шаг 2 2026-07-03 22:43:58 +03:00
Дмитрий df83fa836c feat(автоподбор): Конкурентное поле — финальный проход склейки, вскрыть всё, группы дублей, телефоны, отсев рекламных номеров 2ГИС
Шаг 1 «Конкурентное поле» — чекпоинт:
- Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча.
- Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента.
- Колонка phones jsonb; телефон-дубли видны уже на «Предложениях».
- Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт».
- Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание.
- Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм.

Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:34:03 +03:00
Дмитрий f78ab4af3d feat(биллинг): баланс блокирует запуск проекта, а не создание — единый гейт
Создавать проекты можно всегда; баланс проверяется в момент ЗАПУСКА
(создать-и-запустить / запустить / возобновить одиночно и пачкой / автоподбор)
под замком на клиента (без гонок). Не хватает — проект остаётся на паузе с
меткой preflight_blocked_at, клиенту сообщение в рублях (сколько пополнить).
Групповой запуск «сколько влезло». Нет активного тарифа на дату → запуск
запрещён (fail-closed). Гейт реквизитов первого проекта добавлен и в автоподбор.

- LaunchBalanceGate — единый гейт вместо 3 копий preflight (ProjectController
  store/update, AutopodborController), под DB::transaction + lockForUpdate(Tenant).
- ProjectService::create($launch) + новый setActive(); bulk resume «сколько влезло».
- AutopodborProjectCreator: пачка в транзакции через общий ProjectService::create.
- Идемпотентность box/phone_type миграций автоподбора (Schema::hasColumn guard).
- Тест-инфра: afterRefreshingDatabase восстанавливает месячные партиции.

Тесты фичи 40/40 зелёные. Спека и план — docs/superpowers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:07:22 +03:00
Дмитрий b3f5158e48 perf/fix автоподбор: параллельный сбор 2ГИС по 3 + большие таймауты + безопасное письмо + node из config
Ускорение шага 1: CategoryScraper грузит страницы категории 2ГИС не по одной, а пачками поперёк рубрик
через BatchPageFetcher. На остывшем xfetch 2ГИС упал с ~23 мин до ~5 мин и собрал 139 фирм вместо 12.
Общий лимит одновременных xfetch снижен 4→3 (безопаснее под оконный rate-limit сервиса).

Таймауты ИИ/EXA подняты с запасом (латентность моделей плавает, сбор асинхронный, клиент ждёт):
sonar 120→300с, отсев агрегаторов 90→240с, общий AITUNNEL 30→120с, EXA 30→90с. Джоба: один прогон
без авто-ретраев (tries 3→1, чтобы таймаут не перезапускал платный движок трижды) и общий таймаут 900→1800с.

Фиксы, найденные на живом прогоне:
- Письмо «готово» больше не роняет прогон при недоступной почте (тесты на сбой SMTP при status done/empty).
- Яндекс-загрузчик берёт путь к node из config (autopodbor.node_bin, дефолт node) — из-под PHP node
  теперь находится; на живом прогоне Яндекс дал 78 фирм вместо 0.

По TDD. Бэкенд автоподбора 262/262, Pint чисто. Живой прогон end-to-end: ~8 мин, 14 новых конкурентов,
списание 300 ₽ корректно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 05:43:34 +03:00
Дмитрий 9a9164fee2 feat автоподбор Stage 2 актуализация — новый сайт фирмы поля через Добавить/Заменить в форму проекта
Секция «На актуализацию»: дельта-карточка показывает текущие сайты и карточки фирмы поля + новый сайт кликабельными ссылками, кнопки Добавить и Заменить ведут через подтверждение «Создать проект?» в существующую форму CreateScreen с подставленным новым источником. Имя фирмы берётся из поля, не перезатирается.

Бэкенд: endpoint POST /competitors/{id}/actualize action=add|replace new_site. add заводит сайт-источник рядом со старым, replace архивирует старые сайт-источники и заводит новый, при активном проекте по старому сайту возвращает 409 manage_via_project. /proposals обогащён полем matched с текущими сайтами и карточками совпавшей фирмы.

По TDD. Бэкенд автоподбора 254/254, фронт автоподбора 64/64, ESLint чисто, Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:27:00 +03:00
Дмитрий 5a861e0e85 style(автоподбор): pint-форматирование — скобки конструкторов, порядок импортов, выравнивание; логика без изменений
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:45:37 +03:00
Дмитрий 6d48503a0e feat(автоподбор): сверка находок со состоянием клиента — 3 группы предложений + скрытые дубли, мягкое удаление в архив, убран вычет себя
Stage 1: опознавалка фирмы по сайту/карточкам, ProposalClassifier new/actualize/archived/hidden, box=archived, bulk удалить всех ранее удалённых. Backend 249/249, front autopodbor 61/61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:45:11 +03:00
Дмитрий f1ab608444 feat(автоподбор): масштаб и чистота шага 1 — пул EXA, очередь-throttle, батч агрегатора, узкие рубрики, дедуп по словарю
Масштаб (лимиты внешних сервисов — на КЛЮЧ, общий на всех клиентов):
- EXA: параллельный пул findSites (concurrency=5) вместо 40 запросов по одному.
- Агрегатор: батч вместо ~90 запросов по одному + чанкинг по 40 и длинный
  таймаут 90с (большой список gpt-4o-mini не успевал в 30с).
- Очередь autopodbor + WithoutOverlapping на всех 3 джобах — глобальный
  потолок = число воркеров; assertNoInFlight держит один подбор на клиента.

Чистота выдачи (универсально, без зашитых ниш):
- Анализатор: узкие рубрики, запрет зонтичных слов («финансовые услуги» тащила
  юрфирмы/банки).
- Дедуп: ключ имени минус слова из СЛОВАРЯ рубрик прогона («Яричъ Ломбард»→«Яричъ»);
  слово-категория из данных, не из списка. Гард: склейка только если остаётся
  ровно один слово-токен (генерики вроде «займы под залог» не трогаем).

TDD, 233/233 (unit+feature) зелёные. НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:18:09 +03:00
Дмитрий 062d46e15e feat(автоподбор): письмо «подбор готов» — клиент не ждёт у экрана (фон + уведомление)
RunAutopodborSearchJob по завершении (done/empty) шлёт AutopodborReadyMail на contact_email тенанта
(с числом найденных + ссылкой на портал). Письмо не роняет подбор при недоступной почте (try/catch).
Клиент ставит задачу, работает дальше, получает «готово» письмом.

Тесты: SearchJob 4/4 (вкл. Mail::assertSent); Автоподбор unit+feature 183/183; Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:02:28 +03:00
Дмитрий 5851ac48d3 feat(автоподбор): включён настоящий движок шага 2 + ретрай флаки 2ГИС
Провайдер биндит RealCompetitorAgent вместо заглушки: сайт конкурента —
обычный curl плюс локальный Playwright, справочники 2ГИС/Яндекс — через
антибот xfetch. Поиск/резолв шага 1 ещё не реальны — делегируются заглушке
через fallback, поведение поиска не изменилось.

- AutopodborServiceProvider биндит RealCompetitorAgent с CompositeFetcher
- XfetchClient повторяет запрос при пустом флаки-рендере 2ГИС, до 3 раз
- тесты привязки контейнера плюс ретрая; study-тест пинит заглушку явно
- тесты заглушки строят FakeCompetitorAgent напрямую
- бэкенд автоподбора 126/126

НЕ на проде, воркстри avtopodbor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:51:31 +03:00
Дмитрий a42647c6fe feat(автоподбор): богатый провенанс источника — список «где нашли», офис, подтверждения
Шаг 2 «Конкурентного поля»: один номер встречается в нескольких местах —
код сайта плюс карточки 2ГИС/Яндекс с разными адресами. Раньше хранилось
одно provenance_url/label — список терялся. Теперь сквозной провод
движок→контракт→джоб→БД→API; фронт уже умел показывать кликабельным
списком с подтверждениями.

- autopodbor_sources +3 колонки where_found/office/confirmations
  миграция 2026_06_30_120000, идемпотентная, RLS-review APPROVE 7/7
- canon-sync schema.sql v8.59 плюс CHANGELOG, вкл. catch-up phone_type/box 29.06
- тесты бэкенда автоподбора 122/122

НЕ на проде, воркстри avtopodbor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:40:30 +03:00
Дмитрий 1b3683c6b1 fix(конкурентное поле): 6 находок теста «тупого клиента» — ошибки, регион, дедуп, миграции
- адресные сообщения в окнах сбора/изучения (маппер autopodborErrorMessage)
- регион по умолчанию = пустой плейсхолдер «выберите регион»
- кнопка «Собрать источники» у изучённого конкурента → «Источники собраны»
- сквозной дедуп предложений между прогонами (без двойного списания, ретрай цел)
- убран захардкоженный admin_user_id с фронта (id ставит бэкенд)
- идемпотентный гард в 3 миграции автоподбора (migrate:fresh снова зелёный)
- заглушка Агента: +тип 8-800 (tollfree) для полноты эмуляции

Тесты: Pest автоподбор 82/82, Vitest 62/62, vite build зелёный.

эскейп: фиксируй (авторизовано владельцем)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 06:42:33 +03:00
Дмитрий 4387333118 feat(Конкурентное поле): рабочее место конкуренты→источники→проекты (поверх автоподбора)
Фича «Конкурентное поле» на dev до уровня прототипа 2026-06-29-konkurentnoe-pole-proto.html.

Данные: box (proposal|field) на competitors+sources; phone_type city/mobile/tollfree рядом
с phone_kind (вариант C). 3 миграции, дефолты тарифов 300/50.

API (AutopodborController): GET /field (+счётчики), GET /proposals, PATCH/DELETE competitors
и sources с гвардами активного проекта, переключение box, POST /competitors/manual (+directory_urls),
competitor(id) обогащён box+project-статусом; projectStatus отдаёт limit/delivered/days/regions.
Смена источника проекта = PATCH /api/projects/{id} (реальный гвард слепка §14.10).

Фронт: FieldWorkspaceScreen/FieldCompetitorScreen/FieldProposalsScreen/FieldManualCompetitorScreen
+ field-shared.css (Forest) + AutopodborServicesPanel в Биллинге. Дословно по прототипу: подзаголовки,
баннер предложений, баннер правил времени 18:00 МСК, Справочник 2ГИС·Яндекс, статус проекта
5/день·заявки, окна сбора с ценами 300/50 + «что известно», полные формы. Пункт меню «Конкурентное поле».

Тесты: backend автоподбор 80/80, фронт автоподбор 49/49. Движок шага 2 = заглушка FakeCompetitorAgent.
OmegaDemoFieldSeeder — только для визуальной проверки (НЕ на прод).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 04:18:46 +03:00
Дмитрий ef815c0b8c fix(автоподбор): идемпотентность джоб при ретрае + zero-price short-circuit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:17:44 +03:00
Дмитрий 17d93a144b feat(автоподбор): эндпоинт конкурентов прогона + competitor_id в RunResource
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:15:16 +03:00
Дмитрий 8cc6511edd feat(автоподбор): ручные эндпоинты — manual-study и добавление источника
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:50:37 +03:00
Дмитрий 02d2163e75 feat(автоподбор): API ядро — контроллер, роуты, ресурсы
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:42:41 +03:00
Дмитрий f208fe2f65 feat(автоподбор): создатель проектов из источников (имя+значок+суффикс)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:27:04 +03:00
Дмитрий 98b26f6191 feat(автоподбор): RunService — старт, гейт баланса, один in-flight
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:20:54 +03:00
Дмитрий d9b3e8dbe1 feat(автоподбор): джоба резолва по названию
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:16:14 +03:00
Дмитрий 78d1965430 feat(автоподбор): джоба шага 2 (изучение конкурента, источники)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:49:57 +03:00
Дмитрий 1de6984035 feat(автоподбор): джоба шага 1 (подбор конкурентов)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:46:42 +03:00
Дмитрий 4042890b0a feat(автоподбор): идемпотентное списание за прогон (bcmath, only-on-success)
- AutopodborChargeService::chargeForRun — DB::transaction + lockForUpdate
  на AutopodborRun (guard идемпотентности по balance_transaction_id) и Tenant;
  bcmath (bcsub/bccomp/bcmul), никаких float; throw InsufficientBalanceException
  до любых изменений баланса при нехватке средств.
- Миграция 2026_06_28_110100: расширяет CHECK constraint
  balance_transactions_type_check — добавляет 'autopodbor_charge'.
- Тест: 2 money-инварианта (идемпотентность + noop при нехватке).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:41:06 +03:00
Дмитрий 77498df63b feat(автоподбор): дедуп конкурентов/источников/проектов
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:35:38 +03:00
Дмитрий 3c2bb18537 feat(автоподбор): тип проводки autopodbor_charge + ключи настроек
- BalanceTransaction::TYPE_AUTOPODBOR_CHARGE = 'autopodbor_charge'
- сид-миграция 4 ключей system_settings (idempotent):
  autopodbor_enabled (bool, 0), autopodbor_price_search_rub (decimal, 0),
  autopodbor_price_study_rub (decimal, 0), autopodbor_max_competitors (int, 15)
- Unit + Feature тесты, оба PASS

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:16:59 +03:00
Дмитрий df19af99f9 feat(автоподбор): Eloquent-модели run/competitor/source
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:12:35 +03:00
Дмитрий cc73a70f9e feat(автоподбор): таблица autopodbor_sources + RLS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:58:27 +03:00
Дмитрий 786f796223 feat(автоподбор): таблица autopodbor_competitors + RLS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:58:26 +03:00
Дмитрий e7660edd79 feat(автоподбор): таблица autopodbor_runs + RLS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:58:10 +03:00
Дмитрий c92d498b57 feat(админка): экран Тенанты на серверную пагинацию/поиск/фильтры (масштаб 1000+)
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
AdminTenantsView грузил всех тенантов разом и фильтровал в браузере — на 1000
клиентов поиск/чипы видели только первую страницу. Теперь страница из limit/offset
+ v-pagination; поиск (ILIKE), статус (производный trial/overdue/active/suspended)
и тариф — серверные multi-фильтры. AdminTenantsController::index: statuses/tariffs
через CASE/whereIn (статус зеркалит adminTenantsMapper.deriveStatus). Опции тарифов —
отдельным запросом listAdminTariffPlans. Демо локально подтверждено.

Тесты: фронт 34/34 (tenants), бэкенд 13/13 (+2 на statuses/tariffs); baseline getJson 13→15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:06:56 +03:00
Дмитрий 6536c19c96 feat(дашборд): Этап A — сквозная вложенность Лиды до источника
Экран «Лиды» (/admin/leads): серверный список с фильтрами (дата/канал/поставщик/
статус/поиск) + пагинация (масштаб 10⁴+ лидов). Карточка лида (/admin/leads/{id}):
полная цепочка — ОТКУДА (поставщик B1/B2/B3 + канал + источник + регион) → КОМУ
(сделки клиентов через deals.source_crm_id = supplier_leads.vid). Дашборд: drill
Лиды +топ-10 последних + «Открыть все лиды →». Nav-пункт «Лиды». ПДн-телефон
маскируется (152-ФЗ). Тесты: backend 3 + FE 5 (38 FE всего зелёные).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:14:47 +03:00
Дмитрий 5c68b24c7b feat(дашборд): выбор периода — свой диапазон дат + спека вложенности/масштаба
Фундамент под сквозную вложенность: periodRange() читает date_from/date_to
(приоритет) либо preset; Финансы и Клиенты считаются по выбранному периоду через
whereBetween. FE: «Свой период» + два date-поля + «Применить» → date_from/date_to.
Спека дизайна A+B+C+масштаб сохранена. Baseline перегенерирован (getJson тестов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:54:09 +03:00
Дмитрий 1ecb965981 feat(дашборд): плитка «Клиенты» — активность + новые + спящие
6-я плитка «👥 Клиенты» со светофором (amber если есть спящие) + drill:
KPI за период (всего активных / новых / заходили / получали лиды / платили),
список новых клиентов (с датой входа/лидами/балансом) и «спящих» (активные
без входа 14+ дней или ни разу = не активировались). Клик по строке → карточка
клиента. Backend: clients() endpoint + clientsTile в summary (cross-tenant через
pgsql_admin); сигналы — users.last_login_at, deals, balance_transactions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:28:47 +03:00
Дмитрий fa404e98ec fix(балансы): свежий query-builder на итерацию джобы (PK violation на 2-м прогоне)
Переиспользование одного DB-билдера в цикле накапливало where-клаузы →
updateOrInsert уходил в INSERT существующей строки → SQLSTATE 23505 на проде
при повторном сборе. Билдер теперь создаётся внутри цикла. + тест на 2 прогона.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:32:17 +03:00
Дмитрий 88e816c576 feat(балансы): backend плитки балансов внешних сервисов
Ежедневный контроль баланса DaData/Поставщик/Yandex Cloud плиткой дашборда.

- Таблица external_service_balances (pgsql_supplier, BYPASSRLS, last-value upsert)
- BalanceHealth: чистая логика светофора (red <floor или <3д; amber <floor или <7д)
- BalanceProvider+DTO; провайдеры DaData(API)/YC(OAuth→IAM→billing)/Supplier(Playwright)
- RefreshExternalBalancesJob: изоляция провайдеров (try/catch), расписание 06:30 МСК
- AdminDashboardController::balances() + плитка в summary + topup_url (кнопка «Пополнить»)
- Тесты: BalanceHealth, 3 провайдера, джоба, endpoint (102 теста зелёные)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:12:14 +03:00
Дмитрий f30c6612c0 fix дашборд: достоверность метрик (здоровье/лиды/заказ) + периоды 60/90д
По сверке прод-данных с реальностью (часть чисел вводила в заблуждение):
- Финансы: +периоды 60 и 90 дней (крупные пополнения старше 30д теперь видны).
- Здоровье: «инциденты» больше не считают авто-лог ошибок джоб (summary
  'Автоматически:%') — раньше копилось 975 и держало красный ложно. Теперь:
  open_incidents = только реальные; добавлен job_errors_24h (повторяющиеся
  ошибки джоб за сутки) в подсистему queues.
- Лиды: убраны обманчивый «% доставки» (это было «обработано», не доставлено)
  и «нераспределённые по менеджерам» (менеджеры не используются). Добавлено
  «получено от поставщика сегодня»; доставлено = реально созданные сегодня сделки.
- Заказ: показаны дата снимка и полная картина (всего активных заказов /
  Σ лимита у поставщика) — сверка по снимку больше не выглядит занижено.

Тесты: admin-срез 87 зелёных, unit 3/3, фронт 10/10. stan 0, pint/eslint/
type-check/build чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:57:35 +03:00
Дмитрий 02a8a90e4d feat дашборд: Этап 2 — живые плитки Лиды и Заказ у поставщика
Backend: AdminDashboardController +leads/+supply эндпоинты, summary дополнен
плитками leads/supply; сверка заказа вынесена в чистый сервис
SupplyReconciliation (спрос → формула computeOrder=max(max,⌈Σ/3⌉) → факт →
рассинхрон). Лиды: доставлено сегодня / зависшие 4ч+ / нераспределённые /
% доставки — cross-tenant под pgsql_admin.

Frontend: плитки Лиды и Заказ оживлены (убраны заглушки «Этап 2»), drill
с KPI и таблицей групп спрос→формула→факт→совпадает.

Тесты: SupplyReconciliation unit 3/3, Leads/Supply/Summary feature,
admin-срез 87 зелёных, фронт 10/10. stan 0, pint/eslint/type-check/build чисто.
phpstan-baseline перегенерирован (getJson false-positive на новых тестах).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:32:31 +03:00
Дмитрий 9eaa9322dc feat(дашборд): backend Командного центра — summary/finance/health (Этап 1)
3 read-only эндпоинта под группой [saas-admin,admin-db] (cross-tenant через
pgsql_admin): L1 сводка (Финансы+Здоровье), L2 Финансы (KPI+внимание+топ),
L2 Здоровье (6 подсистем+светофор). TDD, 83 admin-теста зелёные. baseline:
+3 Pest getJson false-positive. Без маржи, без новых таблиц.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:58:58 +03:00
Дмитрий 7ac9af7c79 feat: убрать лимит по числу проектов — ограничение только по балансу/лидам
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Правило продукта: ограничений по количеству проектов нет, лимит только
по балансу и заказанным лидам. Убран гейт tenants.limits.max_projects
в ProjectService::create и показ лимита проектов на дашборде. Поле limits
оставлено как резерв; max_users и api_rps в коде не используются.

Заодно фикс типа в EditProjectDialog.spec: sampleProject типизирован
настоящим Project, source_locked больше не краснит vue-tsc.

Тесты: ProjectsStore 13/13, DashboardSummary 11/11, DashboardView 8/8,
EditProjectDialog 7/7; vue-tsc чисто; pint чисто; vite build ок.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:47:49 +03:00
Дмитрий b38fe0c875 feat(админка): admin-db middleware в группе saas-admin + SharesAdminPdo для тестов
bootstrap: alias admin-db=UseAdminConnection; web.php: группа saas-admin теперь
['saas-admin','admin-db'] (swap default→pgsql_admin после гейта). Тест: admin-db
в пайплайне /api/admin/tenants, saas-admin не потерян.

SharesAdminPdo (зеркало SharesSupplierPdo) применён глобально к Feature suite
(Pest.php): admin-db висит на всей группе → admin-эндпоинты в тестах читают
через pgsql_admin (separate PDO) и не видели бы засеянные в транзакции данные;
sharing PDO даёт cross-connection visibility. baseline: +trait.unused
(Pest применяет трейт в рантайме, phpstan не видит uses() из Pest.php).
261 supplier+admin тестов зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:54:23 +03:00