Update .env.example to extend scheduler configuration: increase HorizonDays to 7 and change RetentionHours to RetentionDays with a default of 90. Revise documentation in media-storage-and-streaming.md to reflect these changes. Remove outdated tv-scheduler-tasks.md file.
ci / build-backend (push) Successful in 1m11s
ci / build-frontend (push) Successful in 36s
ci / tests (push) Successful in 1m35s
ci / sonar (push) Successful in 6m25s

This commit is contained in:
Leonid Pershin
2026-07-26 21:27:31 +03:00
parent 34701bd6b1
commit e79769be81
4 changed files with 14 additions and 664 deletions
+6 -5
View File
@@ -119,9 +119,10 @@
достраивается заново от её конца. Обычный фоновый тик только **расширяет** горизонт (без удаления).
Override, начинающийся в будущем, подхватится и штатным тиком.
**Ретеншн.** Прошедшие `ScheduleEntry` старше `Scheduler__RetentionHours` можно удалять (оставляя
небольшое окно назад для EPG «что только что было»). Сегменты ассетов при этом не трогаются —
ассеты переиспользуются, чистится только расписание.
**Ретеншн.** Прошедшие `ScheduleEntry` старше `Scheduler__RetentionDays` удаляются. Окно длинное
(90 суток по умолчанию), потому что история показов берётся из самой ленты: по ней работают остывание
и потолок повторов, и окно обязано покрывать самое долгое правило канала. Сегменты ассетов при этом
не трогаются — ассеты переиспользуются, чистится только расписание.
**Дыры.** В норме их нет. Если у канала нет ни одного включённого шоу с готовыми сериями (или
планировщик отстал) — эфир играет `FillerAssetId` по кругу, пока не появится расписание.
@@ -229,8 +230,8 @@ Storage__MinFreeSpaceBytes=10737418240
Media__FfmpegPath=/usr/bin/ffmpeg
Media__FfprobePath=/usr/bin/ffprobe
Media__MaxUploadBytes=21474836480
Scheduler__HorizonDays=3
Scheduler__RetentionHours=24
Scheduler__HorizonDays=7
Scheduler__RetentionDays=90
Scheduler__TickMinutes=30
```
-448
View File
@@ -1,448 +0,0 @@
# Планировщик эфира: задачи
Разбивка [tv-scheduler-architecture.md](tv-scheduler-architecture.md) на задачи для разработки.
Ссылки вида «см. 3.4» — на разделы спеки.
Задачи внутри среза идут в порядке зависимостей: следующая опирается на предыдущие. Срезы —
последовательно, каждый заканчивается работающим каналом.
## Статус
`[x]` сделано · `[~]` частично (что осталось — под задачей) · `[ ]` не начато.
**Срез 1 закрыт целиком.** Канал вещает по сетке: библиотека с жанрами и коллекциями, группы,
шаблон со слоями и слотами, генератор с трейсом, применение по кнопке, UI сетки. Старая ротация снесена.
**Срез 2 закрыт целиком.** Ролики со своим экраном и сборкой блоков, шаблоны стыков с редактором
цепочки, привязка стыков к слотам, предпросмотр без записи. Мёртвые настройки заставок
(`BumperMinIntervalMinutes`, обе `*Chance`, `NextBumperIndex`) удалены — условия показа живут
в элементе стыка; из `BumperSelection` убрана «ротация», у которой не было реализации.
**Срез 3 закрыт целиком.** Применимость слоёв правится из UI, слои переупорядочиваются
перетаскиванием и выключаются, сетку можно посмотреть на конкретную дату, слоты двигаются
и растягиваются мышью, день копируется на другие дни. Добавлены жёсткие фильтры кандидатов —
детское время и потолок повторов.
**Срез 4 закрыт целиком.** Проверки по правилам показываются прямо в редакторе, пост-проверки
дают предупреждения по готовой ленте, у предпросмотра появилась вкладка «Проблемы» с тепловой
картой повторов, у каждой записи — трейс «почему это здесь», применение идёт через диф
с подсветкой ближайших суток, сетка копируется на другой канал.
**Смежная зрительская часть закрыта.** Номера каналов с сортировкой публичного списка,
переключение по номерам стрелками (глобальный флаг рядом с флагом регистрации), логотип-оверлей,
часы, плашка «Далее» и аналоговый фильтр. Всё опционально и по умолчанию выключено.
**Проверено:** 142 доменных, 72 прикладных и 11 интеграционных тестов. Интеграционные поднимают
настоящий Postgres в контейнере, применяют к нему все миграции и гоняют конвейер генерации,
копирование шаблона и проверки сетки — то есть миграции и слой «Application ↔ EF» работают.
**Не проверено:** приложение ни разу не поднималось целиком, ни один экран не открывался в браузере,
на реальной базе с реальным контентом ничего не запускалось.
---
## Общие требования к любой задаче
Выполняется для каждой задачи, ниже в тексте не повторяется:
- **Границы слоёв.** `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 — эксплуатация
#### [x] 4.1. Валидация до генерации
`Application` `Api` `Frontend`
Проверки из 5.1: нехватка контента, пустая группа, дыра в сетке, пересечение слотов, недостижимый
кулдаун, возрастной конфликт. Отдаются вместе с шаблоном, показываются в редакторе.
#### [x] 4.2. Пост-проверки
`Application`
Потолок врезок в час, доля жанра за сутки, превышение дрейфа, доля эфира у фона (см. 3.8, 5.2).
Предупреждения, не ошибки — ничего не переигрывается.
#### [x] 4.3. Вкладка «Проблемы» и тепловая карта
`Frontend`
Сгруппированные предупреждения с переходом к источнику; матрица «элемент × день» с яркостью
по числу показов.
#### [x] 4.4. «Почему это здесь»
`Api` `Frontend`
Отдача `trace` записи и экран цепочки происхождения (см. 6.5).
#### [x] 4.5. Диф перед применением
`Application` `Api` `Frontend`
Сравнение текущего хвоста с пересчитанным, список изменений, отдельная подсветка ближайших суток
(см. 6.6).
#### [x] 4.6. Копирование шаблона
`Application` `Api` `Frontend`
Глубокая копия шаблона, слоёв и слотов на другой канал; группы не копируются, они общие.
---
## Смежное — зрительская часть
От планировщика не зависит, делается параллельно в любой момент. Всё опционально и по умолчанию
выключено (см. 6.8).
#### [x] V.1. Номер канала
`Domain` `Application` `Api` `Frontend`
`Channel.number`, уникальность, сортировка публичного списка по номеру.
#### [x] V.2. Переключение по номерам
`Frontend`
Вверх-вниз по номерам, короткий чёрный кадр, номер в углу на секунду. Включается глобальным флагом
в `AppSetting` рядом с флагом регистрации. Сетка каналов остаётся вторым способом навигации.
#### [x] V.3. Логотип канала
`Domain` `Api` `Frontend`
`logoImageId` (реестр изображений), угол и прозрачность. Оверлей поверх `<video>`, без касания
ffmpeg.
#### [x] V.4. Часы и плашка «Далее»
`Frontend`
Часы — опция канала. Плашка в конце программы по данным EPG.
#### [x] 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.