Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
+435
-411
@@ -1,411 +1,435 @@
|
||||
# Планировщик эфира: задачи
|
||||
|
||||
Разбивка [tv-scheduler-architecture.md](tv-scheduler-architecture.md) на задачи для разработки.
|
||||
Ссылки вида «см. 3.4» — на разделы спеки.
|
||||
|
||||
Задачи внутри среза идут в порядке зависимостей: следующая опирается на предыдущие. Срезы —
|
||||
последовательно, каждый заканчивается работающим каналом.
|
||||
|
||||
---
|
||||
|
||||
## Общие требования к любой задаче
|
||||
|
||||
Выполняется для каждой задачи, ниже в тексте не повторяется:
|
||||
|
||||
- **Границы слоёв.** `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. Библиотека
|
||||
|
||||
#### 1.1. Справочник жанров
|
||||
`Domain` `Infrastructure` `Application` `Api` `Frontend`
|
||||
|
||||
Сущность `Genre` (id, name, externalIds) и связь `ShowGenre` (showId, genreId, isPrimary).
|
||||
Идемпотентный сид жанров TMDb в `DbInitializer`. CRUD жанров в админке (список, создание,
|
||||
переименование, удаление с проверкой на использование).
|
||||
|
||||
Готово: жанры есть в БД после старта, админ может править справочник.
|
||||
|
||||
#### 1.2. Жанры на шоу
|
||||
`Application` `Api` `Frontend`
|
||||
|
||||
Проставление жанров шоу вручную (мультиселект + выбор основного). Автоматический маппинг жанров
|
||||
провайдера при `ApplyShowMetadata` / `UpdateShowMetadata` по `Genre.externalIds`; нераспознанные
|
||||
жанры логируются, шоу не ломают. Фильтр по жанру в `ListShows`.
|
||||
|
||||
Готово: у шоу видны и правятся жанры, TMDb-метаданные проставляют их сами, список шоу фильтруется.
|
||||
|
||||
#### 1.3. Расширение `ShowAudience`
|
||||
`Domain` `Infrastructure` `Frontend`
|
||||
|
||||
Пять значений по возрастанию строгости: `Kids → Family → Teen → General → Adult` (см. 3.1).
|
||||
Текущий enum не отсортирован и переписывается; миграция переносит существующие значения
|
||||
один-в-один. Обновить селекты и отображение в UI шоу.
|
||||
|
||||
Готово: порядок значений в enum монотонен по строгости, старые данные не потеряны.
|
||||
|
||||
#### 1.4. Коллекции
|
||||
`Domain` `Infrastructure` `Application` `Api` `Frontend`
|
||||
|
||||
`Collection` (id, name, posterImageId) + `CollectionItem` (collectionId, showId, position).
|
||||
CRUD, изменение порядка перетаскиванием, постер через `ImageGallery`. На экране шоу — блок
|
||||
«входит в коллекции».
|
||||
|
||||
Готово: франшизу из трёх фильмов можно собрать, переупорядочить и открыть с экрана шоу.
|
||||
|
||||
### 1B. Группы
|
||||
|
||||
#### 1.5. Модель группы
|
||||
`Domain` `Infrastructure`
|
||||
|
||||
`Group` (id, name, description, filter jsonb, cachedStats jsonb) + `GroupItem` (groupId,
|
||||
elementKind, elementId, weight, position). См. 3.3. Группы глобальные, без привязки к каналу.
|
||||
|
||||
Готово: модель и миграция.
|
||||
|
||||
#### 1.6. CRUD групп и состава
|
||||
`Application` `Api`
|
||||
|
||||
Создание/переименование/удаление группы (с проверкой, что она не используется слотами), добавление
|
||||
и удаление позиций, изменение порядка и весов. Удаление шоу или коллекции из библиотеки чистит
|
||||
позиции групп.
|
||||
|
||||
Готово: состав группы правится через API, висячих ссылок не остаётся.
|
||||
|
||||
#### 1.7. Фильтр набора и статистика
|
||||
`Application` `Api`
|
||||
|
||||
Выполнение `filter` (см. 3.3) как **разового набора**: возвращает подходящие позиции, отдельная
|
||||
команда добавляет их в состав. Пересчёт `cachedStats` (число позиций, число единиц, суммарная
|
||||
длительность) при изменении состава и фоново при изменении библиотеки.
|
||||
|
||||
Готово: «добавить найденное по фильтру» работает, в API группы отдаётся актуальная статистика.
|
||||
|
||||
#### 1.8. Редактор группы
|
||||
`Frontend`
|
||||
|
||||
Двухпанельный экран (см. 6.2): слева конструктор фильтра, справа состав с drag & drop порядка.
|
||||
Перетаскивание шоу и коллекций из библиотеки. Вес — колонка за «дополнительно». Бейдж «доступно
|
||||
N новых позиций по фильтру».
|
||||
|
||||
Готово: группу можно собрать целиком мышкой, статистика видна на экране.
|
||||
|
||||
### 1C. Шаблон, слои, слоты
|
||||
|
||||
#### 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.
|
||||
|
||||
Готово: модель и миграция; у канала при создании появляется шаблон с фоновым слоем.
|
||||
|
||||
#### 1.10. Изменения в `Channel`
|
||||
`Domain` `Infrastructure` `Application` `Api`
|
||||
|
||||
Добавить `utcOffsetMinutes` (дефолт 180), `dayStartTime` (дефолт 06:00), `templateId`, `number`.
|
||||
Обновить создание канала и команду настроек.
|
||||
|
||||
Готово: канал создаётся с шаблоном, оффсетом и началом вещательных суток.
|
||||
|
||||
#### 1.11. CRUD слоёв и слотов
|
||||
`Application` `Api`
|
||||
|
||||
Создание/правка/удаление слотов, проверка пересечений внутри слоя, валидация кратности времён,
|
||||
проверка что группа существует и не пуста. Пометка шаблона изменённым (`revision`) при любой правке.
|
||||
|
||||
Готово: сетку можно собрать через API, некорректный слот не сохраняется.
|
||||
|
||||
### 1D. Генератор
|
||||
|
||||
#### 1.12. Разрешение сетки
|
||||
`Domain`
|
||||
|
||||
Чистая функция: на момент времени вернуть активный слот — применимые слои по убыванию приоритета,
|
||||
первый слот, накрывающий момент. Учёт вещательных суток (`dayStartTime`) при определении дня недели.
|
||||
Юнит-тесты на границы суток и перекрытие слоёв.
|
||||
|
||||
Готово: `tests/TeleWave.Domain.Tests` покрывает выбор слота, включая ночь после полуночи.
|
||||
|
||||
#### 1.13. Стратегии выбора
|
||||
`Domain`
|
||||
|
||||
`sequential`, `randomWithCooldown`, `fixed` (см. 3.5). Внутри элемента единицы всегда по порядку
|
||||
от курсора. Разворачивание элемента в последовательность единиц: сериал → серии, коллекция →
|
||||
фильмы (сериал внутри коллекции разворачивается), одиночное шоу → одна единица (см. 3.2).
|
||||
|
||||
Готово: юнит-тесты на каждую стратегию, включая заворот в конце и пустую группу.
|
||||
|
||||
#### 1.14. Наполнение слота
|
||||
`Domain`
|
||||
|
||||
Бюджет по `blockMode` (`count` / `duration` / `fillSlot`), `overflowPolicy`, продвижение `SlotState`.
|
||||
Пока без стыков и якорей — контент встык.
|
||||
|
||||
Готово: юнит-тесты на все три режима блока и три политики переполнения.
|
||||
|
||||
#### 1.15. Фоновый слой и филлер
|
||||
`Domain`
|
||||
|
||||
Незакрытые интервалы отдаются слоту фонового слоя; если пуст и он — `fallbackGroupId` шаблона;
|
||||
если пуст и он — зацикленный аварийный ассет канала. Все длительности округляются вверх до сегмента
|
||||
(см. 2.2).
|
||||
|
||||
Готово: дыр в ленте не бывает; юнит-тест на канал с пустой группой.
|
||||
|
||||
#### 1.16. Оркестратор генерации
|
||||
`Application`
|
||||
|
||||
Новый генератор по образцу `ScheduleGenerator`: загрузка конфигурации, вызов чистой части,
|
||||
материализация с записью `trace` (см. 3.9), продвижение `SlotState`, advisory-lock канала,
|
||||
транзакция. Горизонт из `SchedulerOptions.HorizonDays` (поднять дефолт до 7).
|
||||
|
||||
Готово: команда генерации наполняет расписание канала на неделю вперёд.
|
||||
|
||||
#### 1.17. Ретеншн и обслуживание
|
||||
`Application` `Infrastructure`
|
||||
|
||||
`SchedulerOptions.RetentionHours` → `RetentionDays` (дефолт 90). Фоновая задача обслуживания
|
||||
(см. 3.6): чистка записей сверх ретеншна, осиротевших `BumperAsset`, временных файлов.
|
||||
Валидация: правило требует истории глубже, чем хранится → предупреждение.
|
||||
|
||||
Готово: старые записи и неиспользуемые заставки удаляются по расписанию.
|
||||
|
||||
#### 1.18. Применение по кнопке
|
||||
`Application` `Api`
|
||||
|
||||
Правка правил не трогает эфир, только помечает шаблон изменённым. Отдельная команда «применить»
|
||||
пересобирает хвост от `now` (см. 4.5). В ответ — сколько записей затронуто.
|
||||
|
||||
Готово: сохранение слота эфир не двигает; применение пересобирает будущее и идемпотентно.
|
||||
|
||||
#### 1.19. Золотые тесты генератора
|
||||
`Domain.Tests`
|
||||
|
||||
Набор эталонных конфигураций (канал с одним слоем, с фоном, с коллекцией, с пустой группой) и
|
||||
зафиксированные сутки эфира. Сравнение посуточной ленты целиком, обновление эталона — явной
|
||||
командой.
|
||||
|
||||
Готово: правка генератора показывает построчный диф вместо «тесты упали».
|
||||
|
||||
### 1E. Снос старого
|
||||
|
||||
#### 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
|
||||
|
||||
#### 1.21. Read-only сетка и инспектор слота
|
||||
`Frontend`
|
||||
|
||||
Недельная сетка (ось X — дни, ось Y — время от `dayStartTime`), пока без перетаскивания: добавление
|
||||
слота кнопкой на пустом месте, правка в инспекторе (см. 6.1). Индикатор «правила изменены» с кнопкой
|
||||
применения. Экран `ChannelDetail` переписывается.
|
||||
|
||||
Готово: канал настраивается и вещает по сетке целиком через новый UI.
|
||||
|
||||
---
|
||||
|
||||
## Срез 2 — врезки
|
||||
|
||||
#### 2.1. Ролики как `Show(Kind = Interstitial)`
|
||||
`Domain` `Infrastructure` `Application`
|
||||
|
||||
Новое значение `ShowKind`. Исключение таких шоу из обычного списка библиотеки и из запросов
|
||||
метаданных.
|
||||
|
||||
#### 2.2. Раздел «Ролики»
|
||||
`Api` `Frontend`
|
||||
|
||||
Отдельный экран (см. 6.7): плоский список с длительностями и превью, массовая загрузка, сборка
|
||||
блока перетаскиванием с показом суммарной длительности (сохраняется как коллекция), группы роликов.
|
||||
|
||||
#### 2.3. Модель шаблона стыка
|
||||
`Domain` `Infrastructure`
|
||||
|
||||
`JunctionTemplate` + `JunctionElement` (см. 3.7), включая `amountMode` (`count` / `duration`) и
|
||||
структурированные `conditions`. Ссылки `junctionBetweenId` / `junctionAfterId` на слоте.
|
||||
|
||||
#### 2.4. Раскладка врезок
|
||||
`Domain`
|
||||
|
||||
Наполнение стыка: обязательные элементы, набор по бюджету, проверка условий, округление до сегмента.
|
||||
Коллекция входит целиком и не разрезается. Юнит-тесты на смешанную группу блоков и одиночных роликов.
|
||||
|
||||
#### 2.5. Переезд заставок в стык
|
||||
`Application`
|
||||
|
||||
`BumperTemplate` / `ScheduleBumperResolver` и рендер не меняются — меняется точка вызова: вместо
|
||||
настроек канала (`BumperSelection`, `NextBumperIndex`, `BumperMinIntervalMinutes`, обе `*Chance`)
|
||||
условия берутся из элемента стыка. Перечисленные поля канала удаляются, `BumperFont` остаётся.
|
||||
|
||||
#### 2.6. Якоря и округление стартов
|
||||
`Domain`
|
||||
|
||||
Жёсткие старты якорных слотов (не начинать единицу, перелезающую через якорь), мягкое округление
|
||||
`snapToMinutes` с отказом при превышении `maxDriftMinutes` (см. 4.3). Юнит-тесты на оба механизма
|
||||
и на их сочетание.
|
||||
|
||||
#### 2.7. Редактор стыка
|
||||
`Frontend`
|
||||
|
||||
Горизонтальная цепочка с перетаскиванием элементов, линейка суммарной длительности, попап параметров
|
||||
(см. 6.3).
|
||||
|
||||
#### 2.8. Предпросмотр
|
||||
`Application` `Api` `Frontend`
|
||||
|
||||
Прогон генератора на черновике правил **без записи и без продвижения курсоров**. Заставки —
|
||||
плейсхолдерами известной длины, реальный рендер только при применении (см. 3.7). Вкладки «Программа»
|
||||
и «Лента» с гистограммой нагрузки врезок по часам.
|
||||
|
||||
---
|
||||
|
||||
## Срез 3 — гибкость и календарь
|
||||
|
||||
#### 3.1. Применимость слоёв
|
||||
`Domain` `Infrastructure` `Application`
|
||||
|
||||
`applicability`: дни недели, разовые диапазоны дат, ежегодные диапазоны, конкретные даты (см. 3.4).
|
||||
Приоритеты, разрешение конфликтов. Юнит-тесты на пересекающиеся слои и на границу года в ежегодном
|
||||
диапазоне.
|
||||
|
||||
#### 3.2. Фильтры кандидатов
|
||||
`Domain`
|
||||
|
||||
`maxAudienceByTime`, `cooldown`, `maxRepeatsInWindow` (см. 3.8) — применяются **до** взвешенного
|
||||
выбора. История берётся из материализованной ленты. `fallback` стратегии, когда остывание отсекло
|
||||
всех.
|
||||
|
||||
#### 3.3. Слот `repeat`
|
||||
`Domain` `Application`
|
||||
|
||||
Чтение уже записанной ленты по `repeatSource` (daysAgo, time, durationMinutes) и перенос найденных
|
||||
программ. Источник пуст → фон + предупреждение.
|
||||
|
||||
#### 3.4. Слот `signOff`
|
||||
`Domain` `Application` `Frontend`
|
||||
|
||||
Конец вещания: заполнение зацикленным ассетом, пометка в EPG «эфир не ведётся», отображение
|
||||
в программе.
|
||||
|
||||
#### 3.5. Панель слоёв
|
||||
`Frontend`
|
||||
|
||||
Список слоёв с видимостью и приоритетом, drag для переупорядочивания, выбор редактируемого слоя,
|
||||
штриховка перекрытых слотов, переключатель даты («показать сетку на 25 декабря»).
|
||||
|
||||
#### 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.
|
||||
# Планировщик эфира: задачи
|
||||
|
||||
Разбивка [tv-scheduler-architecture.md](tv-scheduler-architecture.md) на задачи для разработки.
|
||||
Ссылки вида «см. 3.4» — на разделы спеки.
|
||||
|
||||
Задачи внутри среза идут в порядке зависимостей: следующая опирается на предыдущие. Срезы —
|
||||
последовательно, каждый заканчивается работающим каналом.
|
||||
|
||||
## Статус
|
||||
|
||||
`[x]` сделано · `[~]` частично (что осталось — под задачей) · `[ ]` не начато.
|
||||
|
||||
**Срез 1 закрыт целиком.** Канал вещает по сетке: библиотека с жанрами и коллекциями, группы,
|
||||
шаблон со слоями и слотами, генератор с трейсом, применение по кнопке, UI сетки. Старая ротация снесена.
|
||||
|
||||
**Срез 2 — бэкенд закрыт, UI нет.** Работают ролики, шаблоны стыков, раскладка врезок, рендер
|
||||
заставок и API управления стыками; собрать канал с рекламой и заставками можно через HTTP, но не
|
||||
из интерфейса.
|
||||
|
||||
**Срезы 3 и 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`. Исключение таких шоу из обычного списка библиотеки и из запросов
|
||||
метаданных.
|
||||
|
||||
#### [ ] 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). Юнит-тесты на оба механизма
|
||||
и на их сочетание.
|
||||
|
||||
#### [ ] 2.7. Редактор стыка
|
||||
`Frontend`
|
||||
|
||||
Горизонтальная цепочка с перетаскиванием элементов, линейка суммарной длительности, попап параметров
|
||||
(см. 6.3).
|
||||
|
||||
#### [ ] 2.8. Предпросмотр
|
||||
`Application` `Api` `Frontend`
|
||||
|
||||
Прогон генератора на черновике правил **без записи и без продвижения курсоров**. Заставки —
|
||||
плейсхолдерами известной длины, реальный рендер только при применении (см. 3.7). Вкладки «Программа»
|
||||
и «Лента» с гистограммой нагрузки врезок по часам.
|
||||
|
||||
---
|
||||
|
||||
## Срез 3 — гибкость и календарь
|
||||
|
||||
#### [~] 3.1. Применимость слоёв
|
||||
`Domain` `Infrastructure` `Application`
|
||||
|
||||
> Частично: модель применимости и разрешение слоёв на дату готовы; нет UI правки применимости.
|
||||
|
||||
`applicability`: дни недели, разовые диапазоны дат, ежегодные диапазоны, конкретные даты (см. 3.4).
|
||||
Приоритеты, разрешение конфликтов. Юнит-тесты на пересекающиеся слои и на границу года в ежегодном
|
||||
диапазоне.
|
||||
|
||||
#### [~] 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 «эфир не ведётся», отображение
|
||||
в программе.
|
||||
|
||||
#### [~] 3.5. Панель слоёв
|
||||
`Frontend`
|
||||
|
||||
> Частично: список слоёв с приоритетом и выбором готов; нет drag-переупорядочивания и переключателя даты.
|
||||
|
||||
Список слоёв с видимостью и приоритетом, drag для переупорядочивания, выбор редактируемого слоя,
|
||||
штриховка перекрытых слотов, переключатель даты («показать сетку на 25 декабря»).
|
||||
|
||||
#### [ ] 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.
|
||||
|
||||
Reference in New Issue
Block a user