26 KiB
Планировщик эфира: задачи
Разбивка 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 <Name> --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api. - Ошибки — через
Result<T>и каталоги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 (реестр изображений), угол и прозрачность. Оверлей поверх <video>, без касания
ffmpeg.
[ ] V.4. Часы и плашка «Далее»
Frontend
Часы — опция канала. Плашка в конце программы по данным EPG.
[ ] V.5. Аналоговый фильтр
Domain Frontend
analogFilterStrength на канале, CSS-фильтр или шейдер, по умолчанию выключен.
Порядок и параллельность
- 1A (библиотека) и 1B (группы) можно вести параллельно; 1C зависит от 1B.
- 1D (генератор) зависит от 1C, но чистые части (1.12–1.15) пишутся и тестируются без БД.
- 1E (снос) выполняется только после 1.16 — иначе канал остаётся без эфира.
- Срез 2 целиком зависит от среза 1; внутри него 2.1–2.2 (ролики) независимы от 2.3–2.7 (стыки).
- Смежное не зависит ни от чего, кроме V.2, которому нужен V.1.