# Планировщик эфира: задачи Разбивка [tv-scheduler-architecture.md](tv-scheduler-architecture.md) на задачи для разработки. Ссылки вида «см. 3.4» — на разделы спеки. Задачи внутри среза идут в порядке зависимостей: следующая опирается на предыдущие. Срезы — последовательно, каждый заканчивается работающим каналом. ## Статус `[x]` сделано · `[~]` частично (что осталось — под задачей) · `[ ]` не начато. **Срез 1 закрыт целиком.** Канал вещает по сетке: библиотека с жанрами и коллекциями, группы, шаблон со слоями и слотами, генератор с трейсом, применение по кнопке, UI сетки. Старая ротация снесена. **Срез 2 закрыт целиком.** Ролики со своим экраном и сборкой блоков, шаблоны стыков с редактором цепочки, привязка стыков к слотам, предпросмотр без записи. Мёртвые настройки заставок (`BumperMinIntervalMinutes`, обе `*Chance`, `NextBumperIndex`) удалены — условия показа живут в элементе стыка; из `BumperSelection` убрана «ротация», у которой не было реализации. **Срез 3 закрыт целиком.** Применимость слоёв правится из UI, слои переупорядочиваются перетаскиванием и выключаются, сетку можно посмотреть на конкретную дату, слоты двигаются и растягиваются мышью, день копируется на другие дни. Добавлены жёсткие фильтры кандидатов — детское время и потолок повторов. **Срез 4** не начинался. Ничего из этого не проверялось на живой базе: только сборка, юнит-тесты и typecheck. --- ## Общие требования к любой задаче Выполняется для каждой задачи, ниже в тексте не повторяется: - **Границы слоёв.** `Api → Infrastructure → Application → Domain`. В `Application` — только порты, никаких `Npgsql`/`AspNetCore.Identity`. - **Миграция.** Изменение модели → `dotnet ef migrations add --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api`. - **Ошибки** — через `Result` и каталоги `XErrors`, не исключениями. Валидация — FluentValidation. - **Тесты.** Доменная логика — `tests/TeleWave.Domain.Tests`, хендлеры — `tests/TeleWave.Application.Tests`, эндпоинты — `tests/TeleWave.Integration.Tests`. Сборка не должна давать предупреждений (`TreatWarningsAsErrors=true`). - **Фронт.** Типы и клиент — в `api.ts` соответствующей фичи. Все строки — в `frontend/src/shared/lib/i18n.ts`, **обе локали** (`ru` и `en`). Проверка: `pnpm lint && pnpm typecheck`. - **Изображения** — только через реестр `Image` и контрол `ImageGallery`, своих файловых полей не заводить. --- ## Срез 1 — сетка вещает Цель: канал вещает по слотам вместо взвешенной ротации. Реклама и заставки временно работают как сейчас, стыков ещё нет. ### 1A. Библиотека #### [x] 1.1. Справочник жанров `Domain` `Infrastructure` `Application` `Api` `Frontend` Сущность `Genre` (id, name, externalIds) и связь `ShowGenre` (showId, genreId, isPrimary). Идемпотентный сид жанров TMDb в `DbInitializer`. CRUD жанров в админке (список, создание, переименование, удаление с проверкой на использование). Готово: жанры есть в БД после старта, админ может править справочник. #### [x] 1.2. Жанры на шоу `Application` `Api` `Frontend` Проставление жанров шоу вручную (мультиселект + выбор основного). Автоматический маппинг жанров провайдера при `ApplyShowMetadata` / `UpdateShowMetadata` по `Genre.externalIds`; нераспознанные жанры логируются, шоу не ломают. Фильтр по жанру в `ListShows`. Готово: у шоу видны и правятся жанры, TMDb-метаданные проставляют их сами, список шоу фильтруется. #### [x] 1.3. Расширение `ShowAudience` `Domain` `Infrastructure` `Frontend` Пять значений по возрастанию строгости: `Kids → Family → Teen → General → Adult` (см. 3.1). Текущий enum не отсортирован и переписывается; миграция переносит существующие значения один-в-один. Обновить селекты и отображение в UI шоу. Готово: порядок значений в enum монотонен по строгости, старые данные не потеряны. #### [x] 1.4. Коллекции `Domain` `Infrastructure` `Application` `Api` `Frontend` `Collection` (id, name, posterImageId) + `CollectionItem` (collectionId, showId, position). CRUD, изменение порядка перетаскиванием, постер через `ImageGallery`. На экране шоу — блок «входит в коллекции». Готово: франшизу из трёх фильмов можно собрать, переупорядочить и открыть с экрана шоу. ### 1B. Группы #### [x] 1.5. Модель группы `Domain` `Infrastructure` `Group` (id, name, description, filter jsonb, cachedStats jsonb) + `GroupItem` (groupId, elementKind, elementId, weight, position). См. 3.3. Группы глобальные, без привязки к каналу. Готово: модель и миграция. #### [x] 1.6. CRUD групп и состава `Application` `Api` Создание/переименование/удаление группы (с проверкой, что она не используется слотами), добавление и удаление позиций, изменение порядка и весов. Удаление шоу или коллекции из библиотеки чистит позиции групп. Готово: состав группы правится через API, висячих ссылок не остаётся. #### [x] 1.7. Фильтр набора и статистика `Application` `Api` Выполнение `filter` (см. 3.3) как **разового набора**: возвращает подходящие позиции, отдельная команда добавляет их в состав. Пересчёт `cachedStats` (число позиций, число единиц, суммарная длительность) при изменении состава и фоново при изменении библиотеки. Готово: «добавить найденное по фильтру» работает, в API группы отдаётся актуальная статистика. #### [x] 1.8. Редактор группы `Frontend` Двухпанельный экран (см. 6.2): слева конструктор фильтра, справа состав с drag & drop порядка. Перетаскивание шоу и коллекций из библиотеки. Вес — колонка за «дополнительно». Бейдж «доступно N новых позиций по фильтру». Готово: группу можно собрать целиком мышкой, статистика видна на экране. ### 1C. Шаблон, слои, слоты #### [x] 1.9. Модель шаблона `Domain` `Infrastructure` `ScheduleTemplate` (channelId, name, defaultJunctionId, fallbackGroupId, revision) → `GridLayer` (templateId, name, priority, applicability jsonb, isEnabled) → `Slot` (все поля из 3.4, включая `slotKind`, `snapToMinutes`, `overflowPolicy`, `isAnchor`). `SlotState` (см. 3.6). В этом срезе используются только базовый слой и фоновый; применимость слоёв — в срезе 3. Готово: модель и миграция; у канала при создании появляется шаблон с фоновым слоем. #### [x] 1.10. Изменения в `Channel` `Domain` `Infrastructure` `Application` `Api` Добавить `utcOffsetMinutes` (дефолт 180), `dayStartTime` (дефолт 06:00), `templateId`, `number`. Обновить создание канала и команду настроек. Готово: канал создаётся с шаблоном, оффсетом и началом вещательных суток. #### [x] 1.11. CRUD слоёв и слотов `Application` `Api` Создание/правка/удаление слотов, проверка пересечений внутри слоя, валидация кратности времён, проверка что группа существует и не пуста. Пометка шаблона изменённым (`revision`) при любой правке. Готово: сетку можно собрать через API, некорректный слот не сохраняется. ### 1D. Генератор #### [x] 1.12. Разрешение сетки `Domain` Чистая функция: на момент времени вернуть активный слот — применимые слои по убыванию приоритета, первый слот, накрывающий момент. Учёт вещательных суток (`dayStartTime`) при определении дня недели. Юнит-тесты на границы суток и перекрытие слоёв. Готово: `tests/TeleWave.Domain.Tests` покрывает выбор слота, включая ночь после полуночи. #### [x] 1.13. Стратегии выбора `Domain` `sequential`, `randomWithCooldown`, `fixed` (см. 3.5). Внутри элемента единицы всегда по порядку от курсора. Разворачивание элемента в последовательность единиц: сериал → серии, коллекция → фильмы (сериал внутри коллекции разворачивается), одиночное шоу → одна единица (см. 3.2). Готово: юнит-тесты на каждую стратегию, включая заворот в конце и пустую группу. #### [x] 1.14. Наполнение слота `Domain` Бюджет по `blockMode` (`count` / `duration` / `fillSlot`), `overflowPolicy`, продвижение `SlotState`. Пока без стыков и якорей — контент встык. Готово: юнит-тесты на все три режима блока и три политики переполнения. #### [x] 1.15. Фоновый слой и филлер `Domain` Незакрытые интервалы отдаются слоту фонового слоя; если пуст и он — `fallbackGroupId` шаблона; если пуст и он — зацикленный аварийный ассет канала. Все длительности округляются вверх до сегмента (см. 2.2). Готово: дыр в ленте не бывает; юнит-тест на канал с пустой группой. #### [x] 1.16. Оркестратор генерации `Application` Новый генератор по образцу `ScheduleGenerator`: загрузка конфигурации, вызов чистой части, материализация с записью `trace` (см. 3.9), продвижение `SlotState`, advisory-lock канала, транзакция. Горизонт из `SchedulerOptions.HorizonDays` (поднять дефолт до 7). Готово: команда генерации наполняет расписание канала на неделю вперёд. #### [x] 1.17. Ретеншн и обслуживание `Application` `Infrastructure` `SchedulerOptions.RetentionHours` → `RetentionDays` (дефолт 90). Фоновая задача обслуживания (см. 3.6): чистка записей сверх ретеншна, осиротевших `BumperAsset`, временных файлов. Валидация: правило требует истории глубже, чем хранится → предупреждение. Готово: старые записи и неиспользуемые заставки удаляются по расписанию. #### [x] 1.18. Применение по кнопке `Application` `Api` Правка правил не трогает эфир, только помечает шаблон изменённым. Отдельная команда «применить» пересобирает хвост от `now` (см. 4.5). В ответ — сколько записей затронуто. Готово: сохранение слота эфир не двигает; применение пересобирает будущее и идемпотентно. #### [x] 1.19. Золотые тесты генератора `Domain.Tests` Набор эталонных конфигураций (канал с одним слоем, с фоном, с коллекцией, с пустой группой) и зафиксированные сутки эфира. Сравнение посуточной ленты целиком, обновление эталона — явной командой. Готово: правка генератора показывает построчный диф вместо «тесты упали». ### 1E. Снос старого #### [x] 1.20. Удаление ротации `Domain` `Infrastructure` `Application` `Api` `Frontend` Удалить `ChannelShow`, `ChannelShowHour`, `ChannelAd`, `ProgrammingOverride`, `OverrideShow`, `AdInsertion`, `OverrideMode`, `OverrideRecurrence`, `SchedulePlanner` и все `Planner*`-модели, соответствующие команды и эндпоинты, компоненты `AddShowForm`, `AddAdForm`, `ChannelShowRow`, `OverrideForm` (см. 9). Реклама и заставки на этом шаге временно генерируются старым способом — переезжают в стыки в срезе 2. Готово: старый планировщик удалён, сборка и тесты зелёные. ### 1F. UI #### [x] 1.21. Read-only сетка и инспектор слота `Frontend` Недельная сетка (ось X — дни, ось Y — время от `dayStartTime`), пока без перетаскивания: добавление слота кнопкой на пустом месте, правка в инспекторе (см. 6.1). Индикатор «правила изменены» с кнопкой применения. Экран `ChannelDetail` переписывается. Готово: канал настраивается и вещает по сетке целиком через новый UI. --- ## Срез 2 — врезки #### [x] 2.1. Ролики как `Show(Kind = Interstitial)` `Domain` `Infrastructure` `Application` Новое значение `ShowKind`. Исключение таких шоу из обычного списка библиотеки и из запросов метаданных. #### [x] 2.2. Раздел «Ролики» `Api` `Frontend` Отдельный экран (см. 6.7): плоский список с длительностями и превью, массовая загрузка, сборка блока перетаскиванием с показом суммарной длительности (сохраняется как коллекция), группы роликов. #### [x] 2.3. Модель шаблона стыка `Domain` `Infrastructure` `JunctionTemplate` + `JunctionElement` (см. 3.7), включая `amountMode` (`count` / `duration`) и структурированные `conditions`. Ссылки `junctionBetweenId` / `junctionAfterId` на слоте. #### [x] 2.4. Раскладка врезок `Domain` Наполнение стыка: обязательные элементы, набор по бюджету, проверка условий, округление до сегмента. Коллекция входит целиком и не разрезается. Юнит-тесты на смешанную группу блоков и одиночных роликов. #### [x] 2.5. Переезд заставок в стык `Application` `BumperTemplate` / `ScheduleBumperResolver` и рендер не меняются — меняется точка вызова: вместо настроек канала (`BumperSelection`, `NextBumperIndex`, `BumperMinIntervalMinutes`, обе `*Chance`) условия берутся из элемента стыка. Перечисленные поля канала удаляются, `BumperFont` остаётся. #### [x] 2.6. Якоря и округление стартов `Domain` Жёсткие старты якорных слотов (не начинать единицу, перелезающую через якорь), мягкое округление `snapToMinutes` с отказом при превышении `maxDriftMinutes` (см. 4.3). Юнит-тесты на оба механизма и на их сочетание. #### [x] 2.7. Редактор стыка `Frontend` Горизонтальная цепочка с перетаскиванием элементов, линейка суммарной длительности, попап параметров (см. 6.3). #### [x] 2.8. Предпросмотр `Application` `Api` `Frontend` Прогон генератора на черновике правил **без записи и без продвижения курсоров**. Заставки — плейсхолдерами известной длины, реальный рендер только при применении (см. 3.7). Вкладки «Программа» и «Лента» с гистограммой нагрузки врезок по часам. --- ## Срез 3 — гибкость и календарь #### [x] 3.1. Применимость слоёв `Domain` `Infrastructure` `Application` `applicability`: дни недели, разовые диапазоны дат, ежегодные диапазоны, конкретные даты (см. 3.4). Приоритеты, разрешение конфликтов. Юнит-тесты на пересекающиеся слои и на границу года в ежегодном диапазоне. #### [x] 3.2. Фильтры кандидатов `Domain` `maxAudienceByTime`, `cooldown`, `maxRepeatsInWindow` (см. 3.8) — применяются **до** взвешенного выбора. История берётся из материализованной ленты. `fallback` стратегии, когда остывание отсекло всех. #### [x] 3.3. Слот `repeat` `Domain` `Application` Чтение уже записанной ленты по `repeatSource` (daysAgo, time, durationMinutes) и перенос найденных программ. Источник пуст → фон + предупреждение. #### [x] 3.4. Слот `signOff` `Domain` `Application` `Frontend` Конец вещания: заполнение зацикленным ассетом, пометка в EPG «эфир не ведётся», отображение в программе. #### [x] 3.5. Панель слоёв `Frontend` Список слоёв с видимостью и приоритетом, drag для переупорядочивания, выбор редактируемого слоя, штриховка перекрытых слотов, переключатель даты («показать сетку на 25 декабря»). #### [x] 3.6. Drag & drop в календаре `Frontend` Перетаскивание слотов и изменение длительности за края, копирование дня на другие дни, копирование недели. Своя реализация — готовые календари не покрывают слои, вещательные сутки и якоря (см. 6.1). --- ## Срез 4 — эксплуатация #### [ ] 4.1. Валидация до генерации `Application` `Api` `Frontend` Проверки из 5.1: нехватка контента, пустая группа, дыра в сетке, пересечение слотов, недостижимый кулдаун, возрастной конфликт. Отдаются вместе с шаблоном, показываются в редакторе. #### [ ] 4.2. Пост-проверки `Application` Потолок врезок в час, доля жанра за сутки, превышение дрейфа, доля эфира у фона (см. 3.8, 5.2). Предупреждения, не ошибки — ничего не переигрывается. #### [ ] 4.3. Вкладка «Проблемы» и тепловая карта `Frontend` Сгруппированные предупреждения с переходом к источнику; матрица «элемент × день» с яркостью по числу показов. #### [ ] 4.4. «Почему это здесь» `Api` `Frontend` Отдача `trace` записи и экран цепочки происхождения (см. 6.5). #### [ ] 4.5. Диф перед применением `Application` `Api` `Frontend` Сравнение текущего хвоста с пересчитанным, список изменений, отдельная подсветка ближайших суток (см. 6.6). #### [ ] 4.6. Копирование шаблона `Application` `Api` `Frontend` Глубокая копия шаблона, слоёв и слотов на другой канал; группы не копируются, они общие. --- ## Смежное — зрительская часть От планировщика не зависит, делается параллельно в любой момент. Всё опционально и по умолчанию выключено (см. 6.8). #### [~] V.1. Номер канала `Domain` `Application` `Api` `Frontend` > Частично: поле, уникальность и правка в настройках канала готовы; сортировки публичного списка нет. `Channel.number`, уникальность, сортировка публичного списка по номеру. #### [ ] V.2. Переключение по номерам `Frontend` Вверх-вниз по номерам, короткий чёрный кадр, номер в углу на секунду. Включается глобальным флагом в `AppSetting` рядом с флагом регистрации. Сетка каналов остаётся вторым способом навигации. #### [ ] V.3. Логотип канала `Domain` `Api` `Frontend` `logoImageId` (реестр изображений), угол и прозрачность. Оверлей поверх `