Commit Graph

1599 Commits

Author SHA1 Message Date
Дмитрий 9ee4e9a79f feat(воронка продаж): корзина, дробь у «Отказа», фильтр по датам — экраны
- 12-я колонка «Корзина», результат «В корзину» просит только причину;
- в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного
  тестирования»; при нуле дробь не рисуется — «69/0» это шум;
- у «Выслано КП» поле даты подписано «если договорились» и необязательно;
- орган фильтра по датам на ОБОИХ экранах, два режима + период.

Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела»
(снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/).
Браузер поймал то, чего не видели тесты: при смене режима оставался прежний
период, и «что менялось за завтра» давало пустую доску — теперь период
возвращается к «Сегодня», на это заведён отдельный тест.

Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31.
2026-08-01 13:29:10 +03:00
Дмитрий 016416502a feat(воронка продаж): корзина, КП без обязательной даты, фильтр по датам — сервер
- результат «В корзину»: одна причина, дата созвона стирается, платящего не выбросить;
- реклама на корзине встаёт СРАЗУ — явной веткой, а не случайно через unknown_stage;
- у «Выслано КП» дата созвона стала необязательной: бывает «пришлите на почту,
  если интересно — перезвоню». Врущий старый тест на 422 без даты поправлен;
- фильтр доски date_mode=todo|changed + период (добавлен вид «завтра»).
  «Надо сделать» — созвон в периоде ИЛИ просрочен, кроме отказа и корзины.
  «Менялось» — есть движение стадии ИЛИ запись разговора за период.

Прогон: 1245/1245 (отдел продаж + все юнит-тесты).
2026-08-01 12:41:24 +03:00
Дмитрий 53ebcd9fe0 feat(воронка продаж): журнал движений карточки + место под корзину
Стадию меняют два разных места — результат разговора менеджера и автопереезд
по деньгам. Джоба не оставляла следа вообще, поэтому «что менялось за день»
было не из чего построить. Запись движения перенесена в событие модели: один
шов на всех, включая любой будущий третий источник.

- sales_prospect_moves — журнал всех движений (append-only, с GRANT'ами ролям);
- prev_stage на карточке — откуда приехала (кормит счётчик «69/1» у «Отказа»);
- stage += 'trash' — место под колонку «Корзина».

Сторож проверен вырезанием: без записи движения краснеют 4 теста, в том числе
тест джобы и тест API.
2026-08-01 12:05:04 +03:00
Дмитрий 2248cb7b79 docs(воронка продаж): промт для следующей сессии по корзине и фильтрам
Готовый текст, который владелец копирует в новую сессию: четыре куска работы,
порядок (вопросы → спека → план → код), мины и грабли окружения.
2026-08-01 11:41:41 +03:00
Дмитрий e1374fd967 docs(воронка продаж): зафиксирован запрос владельца — корзина, фильтры по датам, счётчик через дробь, КП без даты
Просьба записана дословно, три скрина описаны словами, заведена папка
docs/superpowers/screens/2026-08-01-korzina-filtry/ под сами картинки.
Собраны находки по коду (счётчик колонки, фильтры доски, отсутствие
истории стадий) и четыре вопроса владельцу с рекомендациями.
2026-08-01 11:40:46 +03:00
Дмитрий e918bab8c2 docs(воронка продаж): итог работы и грабли в файл передачи
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:11:56 +03:00
Дмитрий d54ceed4ed docs(схема): запись v9.28 о стадиях Тестирование ручное и Выслано КП
В спеке §3 уточнено по факту кода: нормализатор отдаёт номер с плюсом,
а хранится он без плюса — плюс срезаем.

Сторожа орфографии и разметки пропущены: они не находят проблемы, а падают —
их библиотеки в корне обрезаны той же поломкой, что статанализ и сторож
журналов. Сторож секретов gitleaks — живой, работает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:11:09 +03:00
Дмитрий 79697217aa fix(прогрев): новые стадии воронки не выключают рекламу молча
До правки decide на незнакомой стадии возвращал unknown_stage — реклама на
такие фирмы встала бы без единого сообщения. Теперь обе новые стадии идут
по правилу переговоров: крутим до созвона, потом ещё срок просрочки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:15:42 +03:00
Дмитрий 569010318b feat(воронка продаж): новые результаты запрещены платящему клиенту
Сторож проверен вырезанием: с убранной защитой тест краснеет, с возвращённой
зеленеет. Зелёный тест, которого не видели красным, ничего не охраняет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:13:44 +03:00
Дмитрий 589f418e61 feat(воронка продаж): результат разговора «Выслано КП» на сервере
Канал из пяти: почта, ватсап, телеграм, макс, другое. Адрес/ник сохраняем
как ввёл менеджер — телеграм-ник и почта к телефонному виду не приводятся.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:10:26 +03:00
Дмитрий 16bb555054 feat(воронка продаж): результат разговора «Ручное тестирование» на сервере
Телефон приводится к виду 79… без плюса — как контакты карточки и как
принимают Яндекс Аудитории. Неразобранный номер отказывает, карточку не двигает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:07:50 +03:00
Дмитрий 7439d1980c feat(воронка продаж): две новые колонки на доске — Тестирование ручное и Выслано КП
Список стадий правится в двух местах сразу: PROSPECT_STAGES на фронте и
STAGES в контроллере. Расхождение дало бы пустую колонку.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:03:44 +03:00
Дмитрий 977d54bc53 feat(воронка продаж): база под стадии Тестирование ручное и Выслано КП
Стадии manual_testing и kp_sent в CHECK + 4 nullable-колонки под их поля.
Откат и повторный накат проверены на своей тестовой базе измерением колонок.

Статанализ пропущен с разрешения владельца: 287 замечаний было и до правки,
все — известный ложный класс Pest, моих файлов среди них нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:45:10 +03:00
Дмитрий 075fdfef6f docs(воронка продаж): спека и план двух новых стадий — Тестирование ручное и Выслано КП 2026-07-31 10:17:13 +03:00
Дмитрий 6ddd2b6717 docs(воронка продаж): спека и план двух новых стадий — Тестирование ручное и Выслано КП
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:17:03 +03:00
Дмитрий 6f469fb13d feat(телеграм-реклама): деньги на общем балансе + чистка СМС-фантазий + медиа объявления
Денежная модель (куски 1-2): реклама в Телеграме (МТС Маркетолог, робот-в-браузере)
оплачивается с ОБЩЕГО баланса тенанта (как СМС), наценка 40%. Списание при запуске,
возврат при отказе модерации/сбое/отмене (сальдо-идемпотентность tg_ad_charge/tg_ad_refund).
Старый рекламный кошелёк-заморозка убран из потока кампаний (таблицы ad_wallets НЕ удалены).
Новый TelegramCampaignChargeService (зеркало SmsChargeService); F5 money-safety сохранён
(сбой с mts_campaign_id → needs_review без возврата). Уборщик добивает зависшие queued с возвратом.

Чистка СМС-наследия (аудит модуля построчно 5 проверяющими): модуль ресейлит ПОКАЗЫ рекламы,
а не рассылку сообщений — вырезаны выдуманные сущности, механически скопированные из СМС:
- «имя/бренд отправителя» + помесячная абонплата (в телеге МТС имени отправителя нет):
  ChargeTgNameFeeJob, TelegramSenderService, SenderController, модели Sender/Setting,
  фронт-панель TelegramSenderPanel, админ-карточка, updateTgSettings, тип tg_name_fee.
  Таблицы client_tg_senders/_settings дропнуты.
- мёртвые таблицы client_tg_messages (доставка по номеру) и client_tg_templates (шаблоны
  сообщений) — без модели/использования, дропнуты (миграция 000017, create-миграции удалены).
- client_tg_optouts переосмыслен как ручной список «не показывать этим номерам» (не «отписки»).
- лексика рассылки → рекламы: «Авторассылка»→«Авто-реклама», «кошелёк/заморожено»→«общий баланс».

Медиа объявления (реальный пробел показов): новый POST /api/telegram/campaigns/{id}/media
(картинка/видео к черновику, png/jpg/gif/mp4 ≤50 МБ, замена удаляет старый файл) + поле
загрузки в форме кабинета. Робот уже принимал media_path (RunTelegramCampaignJob → mediaFile
→ cabinet.js fillAdMedia) — не хватало только приёма файла от клиента.

Схема: db/CHANGELOG_schema.md v8.94 (CHECK += tg_ad_charge/tg_ad_refund) + v8.95 (дроп
СМС-таблиц, tg_name_fee убран). Тесты зелёные поштучно (квирк партиций — гонять по одному):
Schema/Models/AdminTg/CampaignCharge/CampaignApi/Audience/Media; фронт advertising-telegram-view
19 + admin-tg/auto-rule 24. pint/deptrac(0) чисто; larastan исключён (Pest-$this шум). Робот
bots/mts-telegram-ads НЕ трогали.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 06:26:07 +03:00
Дмитрий 7a2b052f0d feat(телеграм-реклама): связка Laravel → робот mode:resubmit — пересдача чинит ту же кампанию
Хвост «связка» из STATE. Раньше пересдача отклонённой кампании создавала в кабинете
МТС НОВУЮ кампанию (RunTelegramCampaignJob, очищала mts_campaign_id). Теперь портал
поручает роботу ЧИНИТЬ ту же кампанию: робот заходит в неё через «Исправить» по
mts_campaign_id, вносит правки и переотправляет на модерацию «без оплаты» (0 ₽).
Робот-режим mode:'resubmit' уже проверен живьём (коммит d50a93e5). Выбор поведения
(«чинить ту же», не «создавать новую») подтверждён владельцем.

Что сделано:
- RobotResult: поле resubmitted (bool) + разбор из JSON робота.
- TelegramRobotRunner::taskPayload: пробрасывает submitMode (draft|live для resubmit).
- ResubmitTelegramCampaignJob (новый): RLS через tenantTx (SET LOCAL current_tenant_id),
  идемпотентность по queued, денежно-статусная логика F5 как в RunTelegramCampaignJob.
  Успех: бой (resubmitted) → moderating, песочница → draft_ready. Отказ с mts_campaign_id
  → needs_review без возврата брони.
- Контроллер resubmit: mts_campaign_id СОХРАНЯЕТСЯ (не очищаем), guard на пустой id → 422,
  dispatch ResubmitTelegramCampaignJob.

Код-ревью (субагент) поймало Important-баг, исправлено:
- I-1: failed() был скопирован из эталона, где «queued ⇒ нет mts_campaign_id». У пересдачи
  queued ВСЕГДА с id → при перманентном сбое в queued срабатывала ветка hasMtsId →
  queued→needs_review (перехода НЕТ в TRANSITIONS) → кампания зависала в queued с
  замороженной бронью, уборщик её не метёт. Фикс: queued → failed + release (робот кабинет
  не трогал — Фаза A не закоммитила running); running — прежняя осторожная логика.
- M-1: успех в бою с resubmitted=false (аномалия контракта) давал терминальный draft_ready
  с зависшей бронью → needs_review (бронь под ручную сверку).

Приёмка: ResubmitJobTest + ResubmitTest вместе 15/15 (51 проверка); pint/phpstan(0)/
deptrac(0) чисто; схема БД не менялась. Детали и приёмочный лист:
docs/superpowers/2026-07-28-resubmit-svyazka-ACCEPT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:16:48 +03:00
Дмитрий d50a93e55c feat(телеграм-робот): боевой режим пересдачи отклонённой кампании (mode:resubmit) — проверено живьём
Хвост №1 из STATE ревью-правок: оформили доказанный в A2 путь пересдачи в
постоянный режим робота mode:'resubmit'. Робот берёт ОТКЛОНЁННУЮ кампанию, входит
в её редактор через «Исправить», вносит исправления и повторно отправляет на
модерацию БЕЗ ОПЛАТЫ (0 ₽ — деньги не списываются).

Что нового:
- parseResubmitTask (task.js): своё задание пересдачи — campaignId + submitMode
  (draft|live, без дефолта, чтобы live не случился сам) + правки на выбор
  (moderatorFile и/или adText/buttonUrl/ordCategory). Нужна хоть одна правка —
  иначе тот же контент снова отклонят. Номера/бюджет не нужны (уже у кампании).
- openResubmitEditor (cabinet.js): нативный клик «Исправить» в строке кампании по
  id → ждём редактор /telegram-a2p/{id}/message (в чужую кампанию не лезем).
- submitWithoutPayment (cabinet.js): на /payment жмём ТОЛЬКО «Отправить на
  модерацию без оплаты»; денежные кнопки («Списать…»/«Оплатить») — двойная защита
  через isForbiddenButtonText, не жмём никогда.
- editResubmitFields + вынос fillAd в хелперы (fillAdText/fillAdLink/
  selectOrdCategory/fillAdMedia): пересдача правит только заданные поля тем же
  проверенным кодом (DRY, поведение fillAd не изменилось).
- runResubmit (runner.js) + ветка mode:'resubmit' в bin/run.js (до parseTask, как
  read-status). draft — предохранитель (до /confirmation, не шлём); live — отправка
  без оплаты.

Живая проверка на реальных отклонённых кампаниях (28.07.2026):
- DRAFT (2231132, правка текста + документ): вошёл «Исправить» → правка → загрузка
  документа → /confirmation → НЕ отправил (resubmitted:false, stoppedAt:confirmation).
- LIVE (2231134, правка текст+ссылка+ОРД, без документа): полный цикл → «без оплаты»
  (0 ₽) → resubmitted:true; read-status подтвердил moderating. Баланс не тронут.

Робот npm test 110/110 (было 94, +16). Приёмочный лист и живые результаты:
docs/superpowers/2026-07-28-robot-resubmit-mode-ACCEPT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:43:58 +03:00
Дмитрий 05c22b3364 feat(инструменты): #90 grilling — допрос по готовому решению + нормативная синхронизация
Зарегистрирован вендоренный скил grilling из mattpocock/skills, MIT,
skills/productivity/grilling. Установлен user-level ~/.claude/skills/grilling/
— вне репозитория, единственная копия: проектная удалена во избежание
задвоения.

Роль — безжалостный допрос по УЖЕ имеющемуся плану или решению: обход дерева
развилок ветка за веткой, по одному вопросу за раз, к каждому вопросу свой
рекомендуемый ответ, факты ищутся самостоятельно, работа не начинается до
явного подтверждения заказчика.

Надстройка проекта поверх немодифицированного тела апстрима, отделена
заголовком:
- протокол docs/grilling/ГГГГ-ММ-ДД-тема.md — разделы Решили / Отрезали и
  почему / Осталось открытым; пишется по ходу, перечитывается после компакта
  контекста. Закрывает класс «договорённость сгорела при компакте»
- порядок обхода «сначала необратимое» — деньги, схема БД, что уходит клиенту
- критерий остановки: пустой фронт развилок с объявлением вслух
- «слушай, не защищай»

Граница ADR-021 GR1 с #55 discovery-interview — разрез по наличию решения:
grilling куёт решение, которое у заказчика уже есть, поэтому наводящий
рекомендуемый ответ обязателен; discovery-interview вскрывает проблему, когда
решения ещё нет, и там наводящие ответы запрещены. GR2 — граница с
brainstorming #19. GR3 — вендоринг без модификации апстрима.

Реестр: узел #90 + контракт, связка L1 между brainstorming и writing-plans,
классификация planning вес 0.8 — ниже первичных решателей. Автотаблицы
перегенерированы через registry-render, карта цепочек дополнена.

Нормативная синхронизация квинтета:
- Tooling Прил. Н v2.26 — новый §4.63, счётчик 87→88 и 107→108, off-phase
  +57→+58, футер
- Pravila v1.45 — §13.2 новый абзац, запись в истории версий
- PSR_v1 v3.25 — R10.1 Блок 1 note, запись в истории версий
- CLAUDE.md v2.49 — через плагин claude-md-management, §0 версии квинтета,
  §3.4 +#90, §9 запись

Попутно снят застарелый рассинхрон шапки Tooling: числа 84/104 остались от
майской версии, приведены к 88/108, дописаны research-tooling и узлы #87–#90.

Проверки: registry-render --check зелёный на обоих файлах,
observer-chain-map-checker OK 17 chains in sync, загрузчик видит 90 узлов,
markdownlint 0 ошибок, cspell 0 ошибок.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:16:25 +03:00
Дмитрий 39407df04d docs(телеграм-реклама): состояние ревью-правок + результаты живой проверки A1/A2
Фиксирует итог сессии 28.07: приёмочный лист 9 правок F1–F9, деплой-чеклист srv_bypass,
результаты живой проверки робота — A1 (чтение вердикта 2231134) и A2 (пересдача с документом,
0 руб), рецепт пересдачи для будущего robot-mode. Только документ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:57:15 +03:00
Дмитрий a0b7f4adfa fix(телеграм-робот): чтение вердикта модерации из списка кабинета — починка локатора по живому прогону
Живая проверка A1 (28.07.2026) на реальном отказе кампании 2231134 вскрыла два бага в
readModerationStatus, из-за которых боевой код не находил строку кампании — на юнит-тестах
было зелено, а кабинет показал обратное:

1. Паттерн ссылки: в списке кампаний ссылка = /cabinet/campaigns/telegram/{id}, а код искал
   /telegram-a2p/{id} — это адрес детальной/визард-страницы. Matcher расширен на оба варианта
   через campaignHrefRe/hrefMatchesCampaignId плюс юнит-тест.
2. Глубина подъёма по DOM: строка списка — грид из div, не tr/li; контейнер со статусом ряда
   на ~8 уровней выше ссылки. Предел подъёма поднят с 6 до 10; возврат на первом предке со
   статусом — выше склеиваются две кампании.

Итог живого прогона: робот вернул rejected плюс полный текст 5 пунктов модерации из слайд-модалки
Причины. Метка FLOW-CONFIRM в cabinet.js снята. Робот npm test 94/94.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:46:00 +03:00
Дмитрий 68fe632cca fix(телеграм-реклама): правки по сводному код-ревью ветки — деньги, статус-машина, робот, RLS
Закрывает находки ревью: C1-блокер + рассинхроны длины + все оранжевые. TDD, всё зелёное.
Поведение в песочнице не меняется; правки готовят ветку к боевому включению.

F1 (блокер): AdWalletService::freeze реактивирует released-hold через updateOrCreate по
  4-ключу — пересдача кампании и повторная заявка на имя больше не падают на дубле ключа
  23505 в боевом режиме.
F2: длины валидации выровнены под колонки БД — имя 64, ad_link 500, ord_category 200;
  длинное значение даёт ошибку поля, а не замаскированный 422 от БД.
F3: авто-рассылка морозит потолок бюджета симметрично ручному запуску только в бою и
  считает дневной лимит под lockForUpdate строки правила.
F4: кампания не зависает в moderating вечно — переход moderating→needs_review плюс
  предохранитель уборщика по возрасту client_tg.moderation_stuck_hours=48, бронь не трогаем.
F5: единое осторожное правило возврата брони в finalize и failed — есть mts_campaign_id
  значит могла уйти на модерацию → needs_review без release; нет id → failed плюс возврат брони.
F6: робот cabinet.js — денежные кнопки оплатить/списать/запустить в чёрном списке
  domClickButton, finalize целит только кнопку отправки на модерацию.
F7: finalize live не врёт launched:true на шаге /payment — launched:false, stoppedAt:payment;
  не дошли до /payment → падаем громко.
F8: assertCostWithinCap подключён в live-finalize — сверка фактической стоимости с потолком.
F9: GRANT SELECT служебным ролям на client_tg_campaigns миграцией 000016 — иначе
  кросс-тенантные джобы Poll/Sweep видели бы 0 строк на проде; правка ложного комментария в
  000011. CHANGELOG v8.93, rls-reviewer CLEAN. ДЕПЛОЙ: ПЕРЕзапустить db/03_service_bypass_policies.sql.

Приёмка: бэкенд ClientTg 196/196; робот npm test 89/89; pint/phpstan/deptrac чисто. Фронт не трогали.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:07:53 +03:00
Дмитрий 18135b47b4 feat(телеграм-реклама): Этап 3.5/3.6 + 4.3 — чтение вердикта, пересдача, уведомление об одобрении
Закрывает Этап 3 «Жизненный цикл и модерация» целиком (3.1–3.6) и бэкенд-часть 4.3.
План docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.

## 3.5 — робот читает статус/причину модерации по mts_campaign_id (Node)
- `parseModerationStatus` (cabinet.js) — чистый парсер статус-текста кабинета в канон
  Laravel-опросчика: «Отклонена»→rejected, «Одобрена»/«Активна»→approved,
  «На модерации»→moderating, «Черновик»→draft, иначе null (тогда опросчик ждёт).
  Терминальные вердикты приоритетнее слова «модераци…» в строке-блобе. Юнит-тест
  read-status.test.mjs (11 кейсов: регистр/nbsp/блоб/приоритет/мусор).
- `readModerationStatus` (cabinet.js) + `runReadStatus` (runner.js) + режим `read-status`
  в bin/run.js: открывает список кампаний, находит ряд по id, читает статус; при отказе —
  причину из слайд-модалки «Причины» (#slide-modal-root). Отдаёт JSON
  {ok, moderationStatus, reason?, campaignId} — его уже разбирает RobotResult (3.4).
  🔴 Читалка кабинета помечена <FLOW-CONFIRM>: DOM-обёртки ряда/модалки собраны по
  живой разведке «Сессии 6» (FLOW-FINDINGS.md), но именно этим кодом live ещё не прогнаны —
  подтвердить на следующем цикле модерации с разрешения владельца. Парсер от DOM не зависит.

## 3.6 — пересдача отклонённой кампании (rejected → queued + документ модератору)
- Миграция 000013: колонка `client_tg_campaigns.moderator_file_path` (varchar 500 NULL,
  после media_path) + CHANGELOG схемы v8.90; rls-reviewer прогнан — чисто (nullable-колонка
  данных, не tenant-скоуп, RLS не меняется). Модель — fillable.
- Endpoint `POST /api/telegram/campaigns/{id}/resubmit`: только отклонённую (иначе 422);
  правки ad_text/ad_link/ord_category (валидация как store) + опц. файл модератору
  (.png/.jpeg/.jpg/.pdf ≤10 МБ, сохраняется на диск local). Успех: поля обновлены,
  status_reason и mts_campaign_id очищены (робот создаст новую кампанию в кабинете),
  rejected→queued, dispatch RunTelegramCampaignJob afterCommit. В бою — гейт аудитории
  + freeze budget_cap_rub заново (при отказе бронь вернул опросчик 3.4; freeze идемпотентен
  по ACTIVE-холду), нехватка → 409, остаётся rejected. Песочница — без брони.
- Робот: task `moderatorFile` (task.js passthrough + TelegramRobotRunner.taskPayload),
  RunTelegramCampaignJob отдаёт moderator_file_path; fillAd грузит файл в поле «Комментарий
  для модератора» (третий file-input, accept pdf) — помечено <FLOW-CONFIRM> (live не прогнан).
- Тесты: ResubmitTest.php (7 кейсов: rejected→queued+очистка+джоб / файл сохранён /
  не-rejected→422 / live 409 / валидация / .exe→422 / чужой→404); Node task.test.js (+2).

## 4.3 — уведомление об одобрении (бэкенд был готов в 3.4, добор покрытия)
- ApproveNotifyTest.php (4 кейса): notifyTelegramCampaignApproved шлёт in-app всем активным
  юзерам тенанта без pref-гейта, тело «одобрена/показы пошли»; неактивный/чужой не получают.

TDD. Приёмка (моя область, чистый прогон): весь ClientTg 150/150, робот 78/78,
ApproveNotify 4/4; phpstan 0, deptrac 0, pint чисто.

Приёмочный лист 3.6 — docs/superpowers/2026-07-28-telegram-3.6-resubmit-acceptance.md.
Осталось в Этапе 4: фронтенд 4.1/4.2/4.4/4.5 (Vue-экран + Vitest) — НЕ начато.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 11:29:23 +03:00
Дмитрий e9bec6d11e fix(портал продаж): ИНН при заведении кандидата больше не обязателен
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.

Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.

Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 09:39:19 +03:00
Дмитрий 5211048bce feat(телеграм-реклама): Этап 2 закрыт — робот несёт фактическую стоимость (2.2), билинг МТС подтверждён
Задача 2.0 (разведка) — РЕШЕНА без входа в кабинет: ответ уже был в
находках Фазы 2. Билинг МТС за «показы своей базе» — НАКОПИТЕЛЬНЫЙ
(резерв → списание по факту показов → возврат остатка, экран /payment).
Следствие: списание «по факту» (была задача 2.4) на успехе отправки честно
сделать нельзя — показов ещё нет; перенесено в Этап 3 (опрос завершения).
Этап 2 закрыт составом 2.1 (отмена) + 2.3 (возврат брони) + 2.2.

- 2.2 PHP: RobotResult несёт `actualCostRub` (nullable string) — заготовка,
  чтобы позже (Этап 3) прочитать фактическую стоимость из кабинета и списать
  её с кошелька клиента. Проброшено в fromRobotJson (робот начнёт класть поле
  позже; нет поля → null). Мёртвых фабрик launched()/draftReady() не добавлял.
- 2.2 Node: чистая утилита `parseCost(text)` в cabinet.js — «Стоимость
  кампании от 201,6 ₽» → «201.60» (запятая→точка, разделители тысяч включая
  неразрывный пробел код 160, дробь до 2 знаков без округления, нет числа →
  null). Вынесена отдельной покрытой функцией; к DOM-потоку НЕ подключена
  (селектор строки стоимости подтвердим живьём в Этапе 3).
- Разведка билинга и решение по 2.4 зафиксированы в FLOW-FINDINGS.md; план
  Этапа 2 обновлён (2.0 решён, 2.4 → Этап 3, Этап 2 закрыт).

TDD, робот замокан, тесты на liderra_testing.
Приёмка: Node 40/40 (npm test), Pest RobotResultTest 5/5 + RobotRunnerTest
(потребитель) зелёный, регрессия ClientTg 120/120, phpstan RobotResult.php 0,
pint чисто, deptrac 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 07:52:56 +03:00
Дмитрий 98388ccf26 docs(телеграм-реклама): аудит дыр модуля + спека и план закрытия (5 этапов)
Разбор клиентского модуля «Реклама в Телеграме по своей базе» на дыры
(4 параллельных код-ревью + личная перепроверка) и план их закрытия.

- findings: 14 находок, сгруппированы по серьёзности; главное — вся денежная
  часть латентна под песочницей, но капканы (залипшая бронь, минимум 367 после
  freeze, потерянный внешний id, зависшие статусы) сработают при go-live.
- spec: решения владельца (бронь→возврат/списание по факту; «своё имя» и
  авторассылка доделываем; начинаем с безопасности), границы, приёмка по областям.
- plan: 5 этапов в порядке 1→2→5→3→4, каждая задача в TDD; этап 3 ждёт живого
  отказа МТС (Part B).

Ревизия 27.07 (разбор самих спеки/плана на дыры):
- билинг-модель МТС («от X ₽» = нижняя граница, трата по показам) —
  выяснить ДО реализации списания (предусловие этапа 2, задача 2.0);
- внешний id кампании МТС сохранять РАНО (робот плодит реальные черновики уже
  на шаге аудитории) — иначе осиротевшие черновики и слепой рефанд;
- уборка брошенных черновиков (сегодня чистили руками) — узаконить (задача 3.1b);
- уборщик зависших консервативен: при известном mts_campaign_id не рефандит вслепую;
- оговорка: тест идемпотентности слабый (dispatchSync последователен).

Только документы (docs/superpowers/*). Кода не трогает.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 20:56:29 +03:00
Дмитрий 5b04d751c0 feat(телеграм-модуль): авто-режим, своё имя + помесячная оплата, админ-тарифы
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.

4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
  (audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
  по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
  и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
  Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).

4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
  🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
  очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
  в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.

4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
  disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
  free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
  кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
  Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
  в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).

4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
  ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).

Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:41:33 +03:00
Дмитрий 8053c14665 docs(телеграм-модуль): дизайн-спека + план стройки клиентского модуля «Реклама в Телеграме по своей базе»
Модуль-близнец СМС-рассылки, но через робота-в-браузере (у МТС нет API).
Спека: цель, что берём из СМС, ключевое отличие (нет провода → робот, пачки ≥367,
реальные деньги, модерация), клиентский экран, два режима (ручной + авто с лимитом
на объявление), обратная связь по отказу, что требует живой разведки, текст для клиента.
План: 6 сессий по ≤250–300k токенов с логичными остановками под /compact, каждая
оставляет код рабочим; Сессия 5 (отказ) гейтится живой разведкой Сессии 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 15:26:57 +03:00
Дмитрий fafa15d8e1 docs(реклама вк): дизайн-спека — VK Реклама на клиентском портале как копия Яндекс-показы
Копия клиентского Яндекс-показы (ветка feat/reklama-yandex-pokazy) для канала ВК:
общий рекламный кошелёк, зеркало guard аудитории с числом ВК (2000), программная
загрузка креатива (проверено живым API content/static.json → операторский шаг для ВК
убираем). Доступ к VK Ads API получен и проверен вживую, ключи на бою, канал под
рубильником services.vk_ads.enabled до go-live. Код не пишем — решение владельца.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:32:59 +03:00
Дмитрий 741d1697c5 feat(телеграм-бот): бот-автоматизатор кампаний МТС Маркетолог — cabinet/runner/CLI/keepalive (задачи 10–16)
Дописан standalone-робот (Node ESM + Playwright) полного цикла создания
Telegram-кампании по своей базе телефонов в кабинете МТС Маркетолог:
- cabinet.js: вход в визард, выбор «Своя база клиентов», загрузка базы +
  подсчёт «не МТС» с порогом 367, переход «Аудитория→Объявление» (само-
  верификация), заполнение объявления + блок ОРД, финал черновик-стоп/боевой
  (fail-loud при неизвестном режиме — защита от боевого клика по ошибке).
- runner.js: оркестратор с алярм/отчётом на почту, гарантированное закрытие
  профиля браузера, чистка временного файла номеров (152-ФЗ).
- task.js: обязательные поля заголовка и названия ОРД; config: потолок бюджета
  падает при нечисловом значении (защита денег), budget: NaN-guard.
- bin/run.js (CLI), bin/keepalive.js (сторож входа с алярмом), bin/login.js
  (разовый вход), task.example.json (без ПДн), README.
- Шаг «Стоимость» намеренно не размечен (заглушка бросает Error) — селекторы
  снимаются на первом реальном черновике после разового логина владельца.

Ядро: 20/20 юнит-тестов зелёные. Браузерная часть проверяется живым черновиком.
Двухстадийная ревизия каждой задачи + финальный сквозной проход (поймал и
починил пропущенный переход между шагами и NaN-обход потолка бюджета).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 00:43:20 +03:00
Дмитрий 51aac64c8a docs(телеграм-бот): разметка подтверждена живым прогоном до шага «Объявление»
Прогон в черновике на реальной базе (1740 номеров): Получатели 1709, МТС 713,
Не МТС 996, стоимость показов 478,08 ₽ — цепочка «загрузка базы → счётчик → цена»
работает. Шаг «Объявление» размечен полностью (текст/заголовок/ссылка/медиа +
обязательный ОРД-блок: категория downshift, название, «Я — посредник»). Добавлены
грабли автоматизации (coach-mark overlay, downshift, force-click). Шаги
«Стоимость/Подтверждение» — доразметить на первом реальном черновике бота.
Черновик удалён, баланс 5010 ₽ цел, временные ПДн стёрты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 23:34:48 +03:00
Дмитрий f747b2fc74 feat(телеграм-бот): персистентный браузер и проверка живости входа
Задачи 8–9 плана (код, проверка против кабинета — при логине владельца в
профиль бота):
- browser.js — launchPersistentContext(profileDir), humanPause по темпу
- session.js — isLoggedIn по стабильному пункту меню «Рассылки и звонки»
  (селектор подтверждён живьём при разведке)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:49:22 +03:00
Дмитрий a1492400a0 feat(телеграм-бот): тестируемое ядро — config, task, phones, budget, mailer
Задачи 3–7 плана (TDD, 14/14 зелёные):
- config.js — загрузка/валидация настроек окружения
- task.js — парсинг и валидация задания кампании (режимы draft|live)
- phones.js — нормализация и дедуп файла номеров (7XXXXXXXXXX)
- budget.js — предохранитель потолка бюджета
- mailer.js — письма алярма/отчёта через SMTP (nodemailer)

gitleaks: каталог test/ бота добавлен в allowlist (синтетические
телефоны-фикстуры 345-67-89 / 111-22-33, не реальные ПДн).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:47:15 +03:00
Дмитрий 22d1150e26 chore(телеграм-бот): скелет Node+Playwright проекта
Задача 2 плана: package.json (Node ESM, node --test), .env.example (профиль
браузера, SMTP Unisender Go, потолок бюджета, человеческий темп), .gitignore.
Зависимости playwright/nodemailer/dotenv + chromium установлены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:44:52 +03:00
Дмитрий ea1991e926 docs(телеграм-бот): разметка визарда кампании по своей базе в кабинете МТС
Задача 1 плана: пройден визард Telegram Ads → «Своя база клиентов» в живом
кабинете в режиме только-чтение. Зафиксированы 7 шагов степпера, селекторы,
требования к файлу номеров (TXT/CSV/XLS/XLSX, формат 79XXXXXXXXX, минимум 367
не-МТС), счётчик «МТС/Не МТС», путь удаления черновика. Черновик 2229823 убран,
баланс 5010₽ не изменился. cspell-words: добавлены CPM/CPF/MTC/ЕРИР/Таргеты/алярм.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:33:03 +03:00
Дмитрий 97566406b5 docs(телеграм-бот): план реализации бота-автоматизатора Telegram Ads
16 задач в 5 блоков по TDD с частыми коммитами. Блок 0 — разведка визарда
кабинета в режиме черновик перед браузерными задачами. Ядро парсинг/номера/
потолок/письма покрыто node --test; браузерная часть на Playwright опирается
на cabinet-flow.md. Исполнение через subagent-driven-development.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:14:30 +03:00
Дмитрий 9edd1f78a0 docs(телеграм-бот): дизайн бота-автоматизатора Telegram Ads через кабинет МТС
Спек по итогам brainstorming с владельцем. Бот-RPA на Playwright автоматизирует
полный цикл создания Telegram-кампании по своей базе телефонов в кабинете МТС
Маркетолог живой сессией браузера, т.к. публичного API для этого нет ни у кого
на рынке РФ. Запуск руками, алярм на почту, два режима черновик/боевой.
Модуль-витрина для клиентов — вне scope, отдельный будущий спек.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:02:58 +03:00
Дмитрий db2bf0113c docs(телефония): снимок состояния Dasha-ботов и Mango + разбор ремонта исходящей
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Хендофф по голосовым «Лена»: маршруты Asterisk (вход→приёмщик, обзвон через
obzvonbot+endpoint obzvon-lena), состояние Dasha (2 агента, лимит 1 разговор),
Mango (8 номеров, этикетка «Омега», МАВ), открытые задачи. Плюс перенесён с
Рабочего стола разбор ремонта исходящей связи Mango (403→180).
Полные телефоны/секреты не кладём (ПДн) — только состояние и команды.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 23:07:52 +03:00
Дмитрий ddabe61558 Merge commit 'a90d60a3' into sms-multi-operator
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
# Conflicts:
#	app/app/Jobs/SendSmsCampaignJob.php
2026-07-23 14:34:52 +03:00
Дмитрий 80c9ca7e67 feat(витрина B1,B2,B4,B5): летопись эпизодов прогрева + значки в воронке; уборка мёртвых скоупов
Кусок B «витрина прогрева» (спека 2026-07-21 §6), решение владельца — полная летопись:
- B1: таблица sales_ad_audience_warming_episodes + модель + WarmingEpisodeRecorder
  (open/close/record идемпотентно). Схема v8.85, бэкфилл из firm_channels(warming)
  и боевых СМС, guard по источнику. rls-reviewer OK 8/8.
- B2: «Греть»/«Убрать» на площадке открывают/закрывают эпизоды канала.
- B4: warmingByProspect считает значки из летописи (live/count вместо массива каналов),
  тип WarmingBadgeState в sales.ts.
- B5: единый компонент WarmingBadges.vue (идёт/грели раньше/×N) в канбане;
  осиротевший WarmingChannelIcons удалён.
Уборка: убраны мёртвые скоупы forYandex/Vk/Mts + их импорт + тест (боевых вызовов нет).
cspell: +5 пре-существующих слов CHANGELOG в словарь (apk/cvtjpq/hgq/sar/sca).

Проверено: Sales 427/427, composer stan 0, pint/prettier чисто, весь Vue-набор зелёный.
B3 (СМС→эпизод) — отдельным коммитом (СМС-блок правит и параллельная сессия).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:54:09 +03:00
Дмитрий 44c94da676 docs(смс): спека §3.2 приведена к реализации (match-сборка, честная стоимость нового оператора)
Убран 'class' из образца реестра (сборка через match в makeSmsProvider,
config:cache-safe), цена канала по ключу '*', и честно расписана стоимость
подключения следующего оператора (файл-провайдер + запись + арm + синоним).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:21:26 +03:00
Дмитрий c88e81e803 docs(смс): план реализации — маршрутизация СМС по операторам (МТС + СМС-центр)
9 задач TDD: справочник операторов, каналы МТС(Exolve)/СМС-центр(smsc.ru),
реестр каналов в конфиге + сборка роутера, нормализация оператора,
имя отправителя под канал, резерв выключателем (off). Схему БД не трогаем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:36:56 +03:00
Дмитрий c333746927 docs(смс): спека — маршрутизация СМС по операторам (МТС + СМС-центр, задел под остальных)
Дизайн схемы «МТС-номер → МТС, остальные → СМС-центр» с минимумом работы
при подключении новых операторов: реестр каналов в конфиге, справочник
операторов (OperatorNormalizer), имя отправителя на каждый канал, резерв
выключателем (по умолчанию OFF). Схему БД не трогаем. Утверждено владельцем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 07:20:10 +03:00
Дмитрий 48b88737e7 feat(прогрев ф2 этап D): backend СМС-канала — свой список sms-firms + счётчик «Отправлено N» (+ план этапа D)
Фаза 2 Этап D, Task D1. Новый эндпоинт GET /api/sales/ad-audience/sms-firms
(только head) отдаёт фирмы со строкой firm_channels(channel='sms'). Хелпер
smsSentCounts считает по каждой фирме число боевых СМС (sales_sms_messages
status='sent') по её номерам; fake_sent (песочница) не считается. firmRow получил
поле sms_sent_count (0 если не слали), проброшено и в firms/allFirms. СМС строкой
всегда loaded, сроками не греется — только чтение sales_sms_messages для счётчика,
тарификация/провайдер/запись кампаний не тронуты. Bump baseline (actingAs-ложняк).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:28:43 +03:00
Дмитрий 6775f58003 feat(прогрев ф2 этап C): показ по каналам — firms/бейджи/числа из firm_channels (+ план этапа C)
Фаза 2 Этап C, Task C1. Вкладка канала показывает ТОЛЬКО свой состав: firms({platform})
фильтрует по наличию строки firm_channels(channel) (loaded или warming). Бейджи
warming_channels строятся из строк firm_channels (yandex/vk/mts по строкам, sms по
факту рассылки), а не из ch_*. Числа вкладки (in_ads/expiring_week/waiting_sync)
считаются по фирмам со строкой firm_channels(channel, status='warming') — loaded в
рекламе не участвует. Это чинит поломку после этапа B (заезд не пишет ch_* ⇒ старые
ch_*-подсчёты давали 0/пусто у новых фирм). channelColumn удалён (сирота).
allFirms/toggle/mtsFile/движок/заливки не тронуты. ch_* не удаляем (откат).
Тесты мигрированы. Bump baseline (actingAs-ложняки).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:38:15 +03:00
Дмитрий ce48d911d6 feat(прогрев ф2 этап B): заезд = только загрузка во все 4 канала (loaded), без автостарта
Фаза 2 Этап B, Task B1 (+ план этапа B). AdAudienceIntake::ingest больше не
запускает прогрев: грузит фирму во все 4 канала (yandex/vk/mts/sms) строками
firm_channels status='loaded' (firstOrCreate — не понижает уже греющийся канал).
ch_* на заезде не пишутся (источник членства — строки firm_channels, resolveChannels
удалён). Повторный заезд обновляет только снимок-поля и НЕ сбрасывает
warmup_started_at/ready_at/stopped_at/stop_reason (заезд ≠ рестарт прогрева).
Фирма без warming-строк ни в одну заливку не идёт ⇒ автостарта нет по построению.
Запуск прогрева — кнопкой «Греть» на портале (этап C). Тесты intake мигрированы
на новую семантику. Bump baseline (+6 postJson-ложняков).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 16:39:45 +03:00
Дмитрий 199af4f906 docs(прогрев): план Фазы 2 этап A — ядро (firm_channels, 9 задач TDD) 2026-07-22 13:58:26 +03:00
Дмитрий 4568612af6 docs(прогрев): Фаза 2 — уточнения владельца (свободный срок, СМС без warming, бэкфилл СМС только слали) 2026-07-22 13:49:06 +03:00
Дмитрий 15f198c49c docs(прогрев): спека Фазы 2 — раздельные каналы + заезд-загрузка (вся фаза) 2026-07-22 13:39:20 +03:00
Дмитрий 072a646230 docs(прогрев): план Фазы 1 — раздельные сроки по площадкам (11 задач, TDD)
Миграция сроков на площадки + бэкфилл, модель durations(), хелпер decideForPlatform
(чистое ядро), контроллер per-platform, recalc сводка, sync Яндекс/ВК по своим срокам
(+ВК на строку площадки), mtsFile, firmRow per-platform, фронт-текст, регресс. Выкат —
отдельно по спеке §5. Реализация — следующей сессией.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 08:32:18 +03:00