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.
This commit is contained in:
+8
-3
@@ -37,9 +37,14 @@ Storage__MinFreeSpaceBytes=10737418240
|
|||||||
# Storage__StreamTokenMinutes=30
|
# Storage__StreamTokenMinutes=30
|
||||||
|
|
||||||
# ── Планировщик эфира ──────────────────────────────────────────────────────
|
# ── Планировщик эфира ──────────────────────────────────────────────────────
|
||||||
# На сколько дней вперёд держать расписание; сколько часов прошлого хранить; период тика.
|
# HorizonDays — на сколько суток вперёд держать материализованное расписание. Неделя — часть замысла:
|
||||||
Scheduler__HorizonDays=3
|
# «знать, что мультики будут в субботу в 9:30» работает, только если программа известна заранее.
|
||||||
Scheduler__RetentionHours=24
|
# RetentionDays — сколько суток прошедшей ленты хранить. Окно обязано покрывать самое долгое остывание
|
||||||
|
# и самый глубокий повтор среди правил канала: история показов берётся из самой ленты, отдельного
|
||||||
|
# журнала нет — укоротишь окно, и остывание с потолком повторов начнут «забывать» показы.
|
||||||
|
# TickMinutes — период фонового тика, достраивающего горизонт.
|
||||||
|
Scheduler__HorizonDays=7
|
||||||
|
Scheduler__RetentionDays=90
|
||||||
Scheduler__TickMinutes=30
|
Scheduler__TickMinutes=30
|
||||||
|
|
||||||
# ── Обработка медиа (ffmpeg) ───────────────────────────────────────────────
|
# ── Обработка медиа (ffmpeg) ───────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
# Ревью TeleWave — план исправлений
|
|
||||||
|
|
||||||
Статус чекбоксов: `[ ]` не сделано · `[x]` исправлено · `[~]` в работе.
|
|
||||||
|
|
||||||
Дата ревью: 2026-07-25. Область: backend (C#/.NET 10), frontend (React 19), инфра.
|
|
||||||
Сборка чистая, 144 теста зелёные, typecheck чистый.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔴 Критичное
|
|
||||||
|
|
||||||
- [x] **C1. Утечка слота семафора → медиа-обработка навсегда встаёт**
|
|
||||||
`MediaProcessingBackgroundService.cs:52` — слот берётся до `ClaimNextAsync`; бросок из claim
|
|
||||||
уводит во внешний `catch` без `Release()`. После N ошибок диспетчер зависает навсегда.
|
|
||||||
_Fix: try/finally вокруг claim либо Release() в ветке ошибки._
|
|
||||||
|
|
||||||
- [x] **C2. Слабый JWT-ключ по умолчанию без fail-fast**
|
|
||||||
`appsettings.json:8` + `DependencyInjection.cs:65` — placeholder-ключ, нет проверки на
|
|
||||||
переопределение и длину; тот же ключ у stream-токенов. Забыли → обход авторизации.
|
|
||||||
_Fix: на старте кидать, если ключ = placeholder или < 32 байт._
|
|
||||||
|
|
||||||
- [x] **C3. Инъекция/поломка ffmpeg-фильтра из пользовательских подписей**
|
|
||||||
`FfmpegBumperRenderer.cs:306` — `EscapeText` не экранирует `,` `;` `[` `]` `%` перевод строки.
|
|
||||||
Подпись с запятой рвёт filtergraph; `[`/`]` — инъекция звеньев. Валидатор проверяет только длину.
|
|
||||||
_Fix: экранировать полный набор метасимволов filtergraph._
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟠 Среднее
|
|
||||||
|
|
||||||
### Безопасность
|
|
||||||
- [x] **M1. Cookie без `Secure` за TLS-прокси** — `AuthEndpoints.cs:207`, `StreamingEndpoints.cs:63`.
|
|
||||||
- [x] **M2. Ключи TMDb/OMDb в логах** — `TmdbMetadataProvider.cs:27`, `OmdbMetadataProvider.cs:25` (`System.Net.Http` не приглушён в Serilog).
|
|
||||||
- [x] **M3. Stream-токен неотзывной, TTL 6ч, игнорирует userId/блокировку** — `StreamTokenService.cs:15`, `StreamingEndpoints.cs:98,123`. _(TTL 30 мин + проверка блокировки на плейлисте; фронт перевыпускает cookie)_
|
|
||||||
- [x] **M4. Rate-limiter глобальный (не партиционирован), только на `/api/auth`** — `Program.cs:66`.
|
|
||||||
|
|
||||||
### Архитектура / транзакции
|
|
||||||
- [x] **M5. `ExecuteDeleteAsync` ломает границу UnitOfWork + файлы удаляются до коммита** — `DeleteShowMediaCommandHandler.cs`, `ClearAllMediaCommandHandler.cs`. _(явная транзакция вокруг ExecuteDelete; файлы после коммита. DeleteAllShows — единичный ExecuteDelete, уже атомарен)_
|
|
||||||
- [x] **M6. Query с побочным эффектом на ФС** — переведён в `RenderBumperPreviewCommand` (command, не query).
|
|
||||||
- [x] **M7. Файловый/HTTP I/O до коммита** — Delete/Register хендлеры: БД коммитится раньше файлового I/O; Register откатывает регистрацию при сбое переноса файла.
|
|
||||||
|
|
||||||
### Планировщик / медиа
|
|
||||||
- [x] **M8. Нет таймаута на ffmpeg/ffprobe** — `ProcessRunner.cs` + `MediaOptions` (ffprobe 120с, транскод/рендер 1800с, конфигурируемо).
|
|
||||||
- [x] **M9. Гонка при конкурентной генерации расписания канала** — `ScheduleGenerator.cs` (транзакция + `pg_advisory_xact_lock` по каналу).
|
|
||||||
- [x] **M10. Рендер заставок синхронно внутри тика планировщика** — вынесен в асинхронную очередь: планировщик создаёт ассет заставки в Pending + кэш-строку и ставит в очередь, ffmpeg крутит новый `BumperRenderBackgroundService` вне тика/транзакции. До готовности плейлист отдаёт филлер.
|
|
||||||
- [x] **M11. Override через полночь не работает** — `SchedulePlannerModels.cs:40` (`EndMinute <= StartMinute` → пустое окно).
|
|
||||||
|
|
||||||
### Фронтенд
|
|
||||||
- [x] **M12. Молчаливое проглатывание ошибки → вечный скелетон** — `AirPage.tsx:43`.
|
|
||||||
- [x] **M13. Повторный 401 после refresh не разлогинивает** — `client.ts:80`.
|
|
||||||
- [x] **M14. Клиентская пагинация поверх усечённого ответа** — `ShowDetail.tsx`, `ChannelDetail.tsx`. _(пикеры дозагружают все страницы через `listAllMedia` с safety-cap; при упоре в cap — видимое предупреждение. True server-side pagination неуместна: пикеры делают клиентские исключение/парсинг/сортировку/bulk-select)_
|
|
||||||
- [x] **M15. Дедуп загрузок только по имени файла** — `upload-store.ts:143`. _(разрешено анализом: сервер сам дедупит по `OriginalFileName` — клиент это зеркалит, а `skipped` показывается. Имя+размер разошлось бы с сервером → failed-загрузки. Уникальность по имени — доменное решение, кода не менял)_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟡 Низкое / прочее
|
|
||||||
|
|
||||||
- [x] **L1. Синхронный I/O без CancellationToken в портах удаления** — все 5 delete-методов портов переведены на `Task`+`CancellationToken`; рекурсивные удаления каталогов офлоадятся (`Task.Run`).
|
|
||||||
- [x] **L2. `ValidationBehavior` вызывает `Validate` вместо `ValidateAsync`** — `ValidationBehavior.cs:24`.
|
|
||||||
- [x] **L3. Инвариант «Single = 1 серия» проверяется в хендлере, а не в агрегате** — `Show.AddEpisode` кидает при обходе (+тест).
|
|
||||||
- [x] **L4. Двойной `SaveChanges` в командном пути ScheduleGenerator** — снят: генератор коммитит свою транзакцию, пост-save UnitOfWork стал no-op.
|
|
||||||
- [x] **L5. HLS-плеер не восстанавливается после fatal network/media error** — `ChannelPlayer.tsx:106` (+ симметричная очистка слушателей нативного HLS).
|
|
||||||
- [x] **L6. Бесконечная повторная регистрация «падающего» файла из inbox** — `InboxScannerBackgroundService.cs:92`.
|
|
||||||
- [x] **L7. Возможное переполнение int в весах шоу** — `SchedulePlanner.cs:256,265`.
|
|
||||||
- [x] **L8. OpenAPI/Scalar мапятся всегда, без гейта по окружению** — `Program.cs` (только Development либо флаг `Api:EnableOpenApi`).
|
|
||||||
- [x] **L9. `AllowedHosts: "*"` и dev-креды БД в appsettings.json** — осознанные dev-дефолты, переопределяются env в проде; C2 теперь форсит реальный JWT-ключ. Оставлено как задокументированное.
|
|
||||||
- [x] **L10. SSRF-поверхность в ImageDownloader** (без allowlist схемы/хоста) — `ImageDownloader.cs:16` (allowlist http/https).
|
|
||||||
- [x] **L11. Документация (CLAUDE.md/README) отстала** — обновлены разделы «текущее состояние» и рабочий принцип под реальный объём.
|
|
||||||
- [x] **L12. Пробелы в тестах** — добавлен проект `TeleWave.Integration.Tests` на Testcontainers-Postgres: генерация расписания, M10 (Pending-заставки + очередь), M9 (advisory-lock против гонки), M7 (откат Register), M5 (транзакция DeleteShowMedia). При отсутствии Docker тесты пропускаются (SkippableFact). Плюс unit-тесты на M11/L3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Журнал исправлений
|
|
||||||
|
|
||||||
### 2026-07-25 — критические (C1–C3)
|
|
||||||
|
|
||||||
- **C1** — `MediaProcessingBackgroundService.cs`: `ClaimNextAsync` обёрнут в `try/catch`, слот
|
|
||||||
освобождается (`slots.Release()`) при любом сбое захвата перед `throw`. Теперь транзиентная
|
|
||||||
ошибка БД не «съедает» слот семафора — диспетчер не зависает.
|
|
||||||
- **C2** — `Infrastructure/DependencyInjection.cs`: после загрузки `JwtOptions` добавлен fail-fast:
|
|
||||||
старт падает с понятным сообщением, если `Jwt:SigningKey` короче 32 байт или содержит `change-me`
|
|
||||||
(значение-заглушка). Тот же ключ подписывает stream-токены, поэтому это закрывает и их.
|
|
||||||
- **C3** — `FfmpegBumperRenderer.cs`: подписи «Сейчас/Далее» (`NowLabel`/`NextLabel`) переведены с
|
|
||||||
инлайнового `text=` на `textfile=` (`nowlabel.txt`/`nextlabel.txt`, `expansion=none`), как уже
|
|
||||||
сделано для названий шоу. Метасимволы filtergraph в подписи больше не ломают/не инъектируют
|
|
||||||
цепочку. Неиспользуемый `EscapeText` удалён. Файлы чистятся в `finally`.
|
|
||||||
|
|
||||||
Проверка: `dotnet build` — 0 warnings/0 errors (при `TreatWarningsAsErrors`); тесты 95 + 49 зелёные.
|
|
||||||
|
|
||||||
### 2026-07-25 — механические средние/низкие (M1, M2, M4, M11, M12, M13, L2, L5, L6, L7, L10)
|
|
||||||
|
|
||||||
Backend:
|
|
||||||
- **M1** — `AuthEndpoints.cs`/`StreamingEndpoints.cs`: cookie-флаг `Secure` вычисляется через
|
|
||||||
`UseSecureCookie` (`!env.IsDevelopment() || request.IsHttps`) вместо голого `request.IsHttps`.
|
|
||||||
Вне Development cookie всегда `Secure` — прод за внешним TLS-прокси больше не отдаёт refresh/stream
|
|
||||||
cookie в открытую при неполной настройке ForwardedHeaders. В Development HTTP-разработка сохранена.
|
|
||||||
- **M2** — `appsettings.json`: добавлен override `System.Net.Http.HttpClient: Warning` — URI запросов
|
|
||||||
к TMDb/OMDb (с `api_key` в query) больше не пишутся в лог на уровне Information.
|
|
||||||
- **M4** — `Program.cs`: rate-limiter `auth` переведён на партиционирование по IP клиента
|
|
||||||
(`RateLimitPartition.GetFixedWindowLimiter`) — один клиент больше не исчерпывает окно логина для всех.
|
|
||||||
- **M11** — `SchedulePlannerModels.cs` + `ProgrammingOverride.cs`: `Covers` для еженедельного override
|
|
||||||
поддерживает окно через полночь (`EndMinute <= StartMinute` → `[start,24:00)` в day + `[0,end)` на
|
|
||||||
следующий день); `end == start` — пустое окно; добавлены null-guards. Регресс-тесты в
|
|
||||||
`ProgrammingOverrideTests` (+2, всего 97 domain).
|
|
||||||
- **L2** — `ValidationBehavior.cs`: `ValidateAsync(context, cancellationToken)` вместо синхронного
|
|
||||||
`Validate` — async-правила FluentValidation и отмена работают.
|
|
||||||
- **L6** — `InboxScannerBackgroundService.cs`: файлы, стабильно отклоняемые командой регистрации,
|
|
||||||
помечаются (`_failed`) и не регистрируются повторно каждые два тика; метка снимается при исчезновении файла.
|
|
||||||
- **L7** — `SchedulePlanner.cs`: веса шоу/вариантов суммируются в `long` с clamp до `int.MaxValue` —
|
|
||||||
экстремальные вес/множитель не переполняют int (иначе взвешенный выбор молча вырождался в первого).
|
|
||||||
- **L10** — `ImageDownloader.cs`: allowlist схемы (только абсолютные http/https) — подменённый ответ
|
|
||||||
провайдера не заставит сервер дёрнуть `file://`/`ftp://` и т.п.
|
|
||||||
|
|
||||||
Frontend:
|
|
||||||
- **M12** — `AirPage.tsx`: провал `watchChannel` показывает offline-панель с кнопкой ретрая (а не
|
|
||||||
бесконечный скелетон); ретрай (`attempt`) заново дёргает `watchChannel`.
|
|
||||||
- **M13** — `client.ts`: повторный 401 уже после успешного refresh вызывает `onUnauthorized()` —
|
|
||||||
мёртвая сессия чистит авторизацию, а не остаётся «залогиненной». Плюс guard на тело refresh-ответа
|
|
||||||
(`accessToken` должен быть строкой).
|
|
||||||
- **L5** — `ChannelPlayer.tsx`: fatal network/media HLS-ошибка сперва пробует восстановиться
|
|
||||||
(`startLoad`/`recoverMediaError`, до 3 попыток) и лишь затем уходит в offline; слушатели нативной
|
|
||||||
HLS-ветки снимаются в cleanup.
|
|
||||||
|
|
||||||
Отложено (требует решения владельца/дизайна): M3, M5–M10, M14, M15, L1, L3, L4, L8, L9, L11, L12 —
|
|
||||||
причины помечены у пунктов.
|
|
||||||
|
|
||||||
Проверка: `dotnet build` 0/0; тесты 97 + 49 зелёные; frontend `tsc` чистый; `oxlint` без новых
|
|
||||||
предупреждений.
|
|
||||||
|
|
||||||
### 2026-07-25 — остальные средние/низкие (M3, M5–M9, M14, L1, L3, L4, L8, L11 + разбор M10, M15, L9, L12)
|
|
||||||
|
|
||||||
По согласованным решениям (короткий TTL+проверка блокировки; конфиг-таймауты ffmpeg; Postgres
|
|
||||||
advisory-lock; явные транзакции + I/O после коммита):
|
|
||||||
|
|
||||||
- **M3** — `StreamingOptions.StreamTokenMinutes` (30), `StreamTokenService.Validate` возвращает id
|
|
||||||
зрителя; `LivePlaylist` сверяет блокировку через `IIdentityService` (1 запрос на перезагрузку
|
|
||||||
плейлиста, не на сегмент). Фронт (`AirPage`) перевыпускает cookie каждые 20 мин. Dev-ключ JWT
|
|
||||||
добавлен в `appsettings.Development.json`, чтобы C2 не ломал локальный запуск.
|
|
||||||
- **M5/M7** — `IAppDbContext.BeginTransactionAsync`; `ClearAllMedia`/`DeleteShowMedia` — транзакция
|
|
||||||
вокруг ExecuteDelete, файлы после коммита; `DeleteMediaAsset`/`DeleteImage`/`ClearBumperAudio`/
|
|
||||||
`RemoveBumperTemplate` — БД раньше файлов; `RegisterMediaAsset` — save→перенос с откатом при сбое.
|
|
||||||
- **M6** — `RenderBumperPreviewQuery` → `RenderBumperPreviewCommand` (+ файл/класс переименованы).
|
|
||||||
- **M8** — `ProcessRunner` принимает таймаут; `MediaOptions.ProbeTimeoutSeconds`/`TranscodeTimeoutSeconds`;
|
|
||||||
по таймауту процесс убивается, `TimeoutException` → ассет уходит в Failed, слот освобождается.
|
|
||||||
- **M9** — `IAppDbContext.AcquireChannelLockAsync` (`pg_advisory_xact_lock` по int64 из GUID канала);
|
|
||||||
`ScheduleGenerator.GenerateAsync` обёрнут транзакцией + lock, снял и **L4** (двойной save).
|
|
||||||
- **M14** — `listAllMedia` (дозагрузка всех страниц, safety-cap 5000) в пикерах ShowDetail/ChannelDetail
|
|
||||||
+ предупреждение при усечении (i18n ru/en). True server-pagination неуместна (клиентские
|
|
||||||
исключение/парсинг/сортировка/bulk).
|
|
||||||
- **L1** — 5 delete-методов портов → async с `CancellationToken`; рекурсивные удаления офлоадятся.
|
|
||||||
- **L3** — `Show.AddEpisode` кидает при второй серии для Single (+тест). **L8** — OpenAPI/Scalar только
|
|
||||||
вне прода/по флагу. **L11** — CLAUDE.md/README обновлены.
|
|
||||||
|
|
||||||
Разбор без правок кода:
|
|
||||||
- **M10** — частично закрыт (M8 ограничивает худший случай, кэш делает рендеры редкими, M9 сериализует
|
|
||||||
канал); полный вынос рендера из тика — отдельная архитектурная задача (флажок).
|
|
||||||
- **M15** — сервер сам дедупит по имени файла; клиент это зеркалит и показывает `skipped`. Смена на
|
|
||||||
имя+размер разошлась бы с сервером. Уникальность по имени — доменное решение.
|
|
||||||
- **L9** — dev-дефолты, переопределяются env в проде; C2 форсит JWT-ключ.
|
|
||||||
|
|
||||||
Новые/обновлённые опции: `.env.example` (Storage__StreamTokenMinutes, Media__Probe/TranscodeTimeoutSeconds,
|
|
||||||
Api:EnableOpenApi неявно). Тесты: +2 domain (override через полночь), +1 domain (Single).
|
|
||||||
|
|
||||||
Проверка: `dotnet build` 0/0 (TreatWarningsAsErrors); тесты **98 + 49** зелёные; frontend `tsc` чистый,
|
|
||||||
`oxlint` без новых предупреждений.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Журнал — M10 (async-рендер заставок) и L12 (интеграционные тесты)
|
|
||||||
|
|
||||||
### 2026-07-25 — M10: асинхронная очередь рендера заставок
|
|
||||||
|
|
||||||
- **Домен**: `BumperAsset` получил `ChannelId`/`TemplateId`/`VariantId` (чтобы фоновый рендерер
|
|
||||||
восстановил спецификацию), фабрика обновлена; миграция `BumperAssetRenderContext` (Guid-колонки с
|
|
||||||
дефолтом для существующих строк — самовосстановление при перерендере). `MediaAsset.RegisterGenerated`
|
|
||||||
теперь честно Pending до готовности.
|
|
||||||
- **Очередь**: `IBumperRenderQueue` + `BumperRenderQueue` (сигнал поверх Channel, как у медиа).
|
|
||||||
- **`ScheduleBumperResolver`**: больше не рендерит — создаёт Pending-ассет (Source=Generated) + кэш-строку
|
|
||||||
и ставит в очередь; переиспользует Ready/Pending/Processing, перерендерит только Failed (по новейшей
|
|
||||||
кэш-строке). Тик планировщика и его транзакция больше не держатся во время ffmpeg (снимает и остаток M9).
|
|
||||||
- **`BumperRenderBackgroundService`**: последовательно берёт Pending Generated → восстанавливает спеку
|
|
||||||
(`BumperSpecFactory`) → ffmpeg → MarkReady/MarkFailed; сброс прерванных на старте; idle-poll 30с.
|
|
||||||
- **`MediaProcessingBackgroundService`**: Generated-ассеты исключены из claim/reset (их обслуживает
|
|
||||||
рендерер заставок).
|
|
||||||
- **Стриминг менять не пришлось**: `GetLivePlaylistQueryHandler` уже фильтрует по `Status==Ready` →
|
|
||||||
Pending-заставка автоматически деградирует в филлер, пока рендерится.
|
|
||||||
- **Поведение**: заставка перехода впервые появляется на тик позже (до этого — филлер); дальше берётся
|
|
||||||
из кэша. Осознанный компромисс (выбран владельцем).
|
|
||||||
|
|
||||||
### 2026-07-25 — L12: интеграционные тесты на Testcontainers-Postgres
|
|
||||||
|
|
||||||
Новый проект `tests/TeleWave.Integration.Tests` (в `.slnx`): поднимает `postgres:16-alpine`, применяет
|
|
||||||
реальные миграции, гоняет то, что InMemory не умеет (транзакции, advisory-lock, ExecuteDelete). 5 тестов:
|
|
||||||
генерация Program-расписания; создание Pending-заставок (Generated) + постановка в очередь (M10);
|
|
||||||
сериализация конкурентной генерации advisory-lock'ом без перекрытий (M9); откат Register при сбое
|
|
||||||
переноса файла (M7); атомарное удаление в DeleteShowMedia (M5). При отсутствии Docker — `SkippableFact`
|
|
||||||
пропускает (CI на раннере без Docker не падает; `dotnet test TeleWave.slnx` в CI прогонит их при наличии
|
|
||||||
Docker на ubuntu-latest).
|
|
||||||
|
|
||||||
Проверка: сборка решения 0/0; **98 domain + 49 application + 5 integration** зелёные; фронт `tsc`/`oxlint`
|
|
||||||
чистые.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Осталось (доменное решение владельца, не дефект)
|
|
||||||
|
|
||||||
- **M15 (доменно)** — если понадобятся разные файлы с одинаковым именем: пересмотреть уникальность
|
|
||||||
`MediaAsset` (сейчас по `OriginalFileName` — намеренно, клиент это зеркалит).
|
|
||||||
@@ -119,9 +119,10 @@
|
|||||||
достраивается заново от её конца. Обычный фоновый тик только **расширяет** горизонт (без удаления).
|
достраивается заново от её конца. Обычный фоновый тик только **расширяет** горизонт (без удаления).
|
||||||
Override, начинающийся в будущем, подхватится и штатным тиком.
|
Override, начинающийся в будущем, подхватится и штатным тиком.
|
||||||
|
|
||||||
**Ретеншн.** Прошедшие `ScheduleEntry` старше `Scheduler__RetentionHours` можно удалять (оставляя
|
**Ретеншн.** Прошедшие `ScheduleEntry` старше `Scheduler__RetentionDays` удаляются. Окно длинное
|
||||||
небольшое окно назад для EPG «что только что было»). Сегменты ассетов при этом не трогаются —
|
(90 суток по умолчанию), потому что история показов берётся из самой ленты: по ней работают остывание
|
||||||
ассеты переиспользуются, чистится только расписание.
|
и потолок повторов, и окно обязано покрывать самое долгое правило канала. Сегменты ассетов при этом
|
||||||
|
не трогаются — ассеты переиспользуются, чистится только расписание.
|
||||||
|
|
||||||
**Дыры.** В норме их нет. Если у канала нет ни одного включённого шоу с готовыми сериями (или
|
**Дыры.** В норме их нет. Если у канала нет ни одного включённого шоу с готовыми сериями (или
|
||||||
планировщик отстал) — эфир играет `FillerAssetId` по кругу, пока не появится расписание.
|
планировщик отстал) — эфир играет `FillerAssetId` по кругу, пока не появится расписание.
|
||||||
@@ -229,8 +230,8 @@ Storage__MinFreeSpaceBytes=10737418240
|
|||||||
Media__FfmpegPath=/usr/bin/ffmpeg
|
Media__FfmpegPath=/usr/bin/ffmpeg
|
||||||
Media__FfprobePath=/usr/bin/ffprobe
|
Media__FfprobePath=/usr/bin/ffprobe
|
||||||
Media__MaxUploadBytes=21474836480
|
Media__MaxUploadBytes=21474836480
|
||||||
Scheduler__HorizonDays=3
|
Scheduler__HorizonDays=7
|
||||||
Scheduler__RetentionHours=24
|
Scheduler__RetentionDays=90
|
||||||
Scheduler__TickMinutes=30
|
Scheduler__TickMinutes=30
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
Reference in New Issue
Block a user