Files
TeleWave/REVIEW.md
T

18 KiB
Raw Blame History

Ревью TeleWave — план исправлений

Статус чекбоксов: [ ] не сделано · [x] исправлено · [~] в работе.

Дата ревью: 2026-07-25. Область: backend (C#/.NET 10), frontend (React 19), инфра. Сборка чистая, 144 теста зелёные, typecheck чистый.


🔴 Критичное

  • C1. Утечка слота семафора → медиа-обработка навсегда встаёт MediaProcessingBackgroundService.cs:52 — слот берётся до ClaimNextAsync; бросок из claim уводит во внешний catch без Release(). После N ошибок диспетчер зависает навсегда. Fix: try/finally вокруг claim либо Release() в ветке ошибки.

  • C2. Слабый JWT-ключ по умолчанию без fail-fast appsettings.json:8 + DependencyInjection.cs:65 — placeholder-ключ, нет проверки на переопределение и длину; тот же ключ у stream-токенов. Забыли → обход авторизации. Fix: на старте кидать, если ключ = placeholder или < 32 байт.

  • C3. Инъекция/поломка ffmpeg-фильтра из пользовательских подписей FfmpegBumperRenderer.cs:306EscapeText не экранирует , ; [ ] % перевод строки. Подпись с запятой рвёт filtergraph; [/] — инъекция звеньев. Валидатор проверяет только длину. Fix: экранировать полный набор метасимволов filtergraph.


🟠 Среднее

Безопасность

  • M1. Cookie без Secure за TLS-проксиAuthEndpoints.cs:207, StreamingEndpoints.cs:63.
  • M2. Ключи TMDb/OMDb в логахTmdbMetadataProvider.cs:27, OmdbMetadataProvider.cs:25 (System.Net.Http не приглушён в Serilog).
  • M3. Stream-токен неотзывной, TTL 6ч, игнорирует userId/блокировкуStreamTokenService.cs:15, StreamingEndpoints.cs:98,123. (TTL 30 мин + проверка блокировки на плейлисте; фронт перевыпускает cookie)
  • M4. Rate-limiter глобальный (не партиционирован), только на /api/authProgram.cs:66.

Архитектура / транзакции

  • M5. ExecuteDeleteAsync ломает границу UnitOfWork + файлы удаляются до коммитаDeleteShowMediaCommandHandler.cs, ClearAllMediaCommandHandler.cs. (явная транзакция вокруг ExecuteDelete; файлы после коммита. DeleteAllShows — единичный ExecuteDelete, уже атомарен)
  • M6. Query с побочным эффектом на ФС — переведён в RenderBumperPreviewCommand (command, не query).
  • M7. Файловый/HTTP I/O до коммита — Delete/Register хендлеры: БД коммитится раньше файлового I/O; Register откатывает регистрацию при сбое переноса файла.

Планировщик / медиа

  • M8. Нет таймаута на ffmpeg/ffprobeProcessRunner.cs + MediaOptions (ffprobe 120с, транскод/рендер 1800с, конфигурируемо).
  • M9. Гонка при конкурентной генерации расписания каналаScheduleGenerator.cs (транзакция + pg_advisory_xact_lock по каналу).
  • [~] M10. Рендер заставок синхронно внутри тика планировщикаScheduleBumperResolver.cs:174. (частично: M8 ограничивает худший случай, кэш делает рендеры редкими, M9 сериализует канал. Полный вынос рендера из тика — отдельная архитектурная задача, флажок ниже.)
  • M11. Override через полночь не работаетSchedulePlannerModels.cs:40 (EndMinute <= StartMinute → пустое окно).

Фронтенд

  • M12. Молчаливое проглатывание ошибки → вечный скелетонAirPage.tsx:43.
  • M13. Повторный 401 после refresh не разлогиниваетclient.ts:80.
  • M14. Клиентская пагинация поверх усечённого ответаShowDetail.tsx, ChannelDetail.tsx. (пикеры дозагружают все страницы через listAllMedia с safety-cap; при упоре в cap — видимое предупреждение. True server-side pagination неуместна: пикеры делают клиентские исключение/парсинг/сортировку/bulk-select)
  • M15. Дедуп загрузок только по имени файлаupload-store.ts:143. (разрешено анализом: сервер сам дедупит по OriginalFileName — клиент это зеркалит, а skipped показывается. Имя+размер разошлось бы с сервером → failed-загрузки. Уникальность по имени — доменное решение, кода не менял)

🟡 Низкое / прочее

  • L1. Синхронный I/O без CancellationToken в портах удаления — все 5 delete-методов портов переведены на Task+CancellationToken; рекурсивные удаления каталогов офлоадятся (Task.Run).
  • L2. ValidationBehavior вызывает Validate вместо ValidateAsyncValidationBehavior.cs:24.
  • L3. Инвариант «Single = 1 серия» проверяется в хендлере, а не в агрегатеShow.AddEpisode кидает при обходе (+тест).
  • L4. Двойной SaveChanges в командном пути ScheduleGenerator — снят: генератор коммитит свою транзакцию, пост-save UnitOfWork стал no-op.
  • L5. HLS-плеер не восстанавливается после fatal network/media errorChannelPlayer.tsx:106 (+ симметричная очистка слушателей нативного HLS).
  • L6. Бесконечная повторная регистрация «падающего» файла из inboxInboxScannerBackgroundService.cs:92.
  • L7. Возможное переполнение int в весах шоуSchedulePlanner.cs:256,265.
  • L8. OpenAPI/Scalar мапятся всегда, без гейта по окружениюProgram.cs (только Development либо флаг Api:EnableOpenApi).
  • L9. AllowedHosts: "*" и dev-креды БД в appsettings.json — осознанные dev-дефолты, переопределяются env в проде; C2 теперь форсит реальный JWT-ключ. Оставлено как задокументированное.
  • L10. SSRF-поверхность в ImageDownloader (без allowlist схемы/хоста) — ImageDownloader.cs:16 (allowlist http/https).
  • L11. Документация (CLAUDE.md/README) отстала — обновлены разделы «текущее состояние» и рабочий принцип под реальный объём.
  • [~] L12. Пробелы в тестах — частично: добавлены тесты на M11 (override через полночь), L3 (Single). Полное покрытие фоновых сервисов/оркестрации/эндпоинтов — отдельная задача.

Журнал исправлений

2026-07-25 — критические (C1–C3)

  • C1MediaProcessingBackgroundService.cs: ClaimNextAsync обёрнут в try/catch, слот освобождается (slots.Release()) при любом сбое захвата перед throw. Теперь транзиентная ошибка БД не «съедает» слот семафора — диспетчер не зависает.
  • C2Infrastructure/DependencyInjection.cs: после загрузки JwtOptions добавлен fail-fast: старт падает с понятным сообщением, если Jwt:SigningKey короче 32 байт или содержит change-me (значение-заглушка). Тот же ключ подписывает stream-токены, поэтому это закрывает и их.
  • C3FfmpegBumperRenderer.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:

  • M1AuthEndpoints.cs/StreamingEndpoints.cs: cookie-флаг Secure вычисляется через UseSecureCookie (!env.IsDevelopment() || request.IsHttps) вместо голого request.IsHttps. Вне Development cookie всегда Secure — прод за внешним TLS-прокси больше не отдаёт refresh/stream cookie в открытую при неполной настройке ForwardedHeaders. В Development HTTP-разработка сохранена.
  • M2appsettings.json: добавлен override System.Net.Http.HttpClient: Warning — URI запросов к TMDb/OMDb (с api_key в query) больше не пишутся в лог на уровне Information.
  • M4Program.cs: rate-limiter auth переведён на партиционирование по IP клиента (RateLimitPartition.GetFixedWindowLimiter) — один клиент больше не исчерпывает окно логина для всех.
  • M11SchedulePlannerModels.cs + ProgrammingOverride.cs: Covers для еженедельного override поддерживает окно через полночь (EndMinute <= StartMinute[start,24:00) в day + [0,end) на следующий день); end == start — пустое окно; добавлены null-guards. Регресс-тесты в ProgrammingOverrideTests (+2, всего 97 domain).
  • L2ValidationBehavior.cs: ValidateAsync(context, cancellationToken) вместо синхронного Validate — async-правила FluentValidation и отмена работают.
  • L6InboxScannerBackgroundService.cs: файлы, стабильно отклоняемые командой регистрации, помечаются (_failed) и не регистрируются повторно каждые два тика; метка снимается при исчезновении файла.
  • L7SchedulePlanner.cs: веса шоу/вариантов суммируются в long с clamp до int.MaxValue — экстремальные вес/множитель не переполняют int (иначе взвешенный выбор молча вырождался в первого).
  • L10ImageDownloader.cs: allowlist схемы (только абсолютные http/https) — подменённый ответ провайдера не заставит сервер дёрнуть file:///ftp:// и т.п.

Frontend:

  • M12AirPage.tsx: провал watchChannel показывает offline-панель с кнопкой ретрая (а не бесконечный скелетон); ретрай (attempt) заново дёргает watchChannel.
  • M13client.ts: повторный 401 уже после успешного refresh вызывает onUnauthorized() — мёртвая сессия чистит авторизацию, а не остаётся «залогиненной». Плюс guard на тело refresh-ответа (accessToken должен быть строкой).
  • L5ChannelPlayer.tsx: fatal network/media HLS-ошибка сперва пробует восстановиться (startLoad/recoverMediaError, до 3 попыток) и лишь затем уходит в offline; слушатели нативной HLS-ветки снимаются в cleanup.

Отложено (требует решения владельца/дизайна): M3, M5M10, M14, M15, L1, L3, L4, L8, L9, L11, L12 — причины помечены у пунктов.

Проверка: dotnet build 0/0; тесты 97 + 49 зелёные; frontend tsc чистый; oxlint без новых предупреждений.

2026-07-25 — остальные средние/низкие (M3, M5M9, M14, L1, L3, L4, L8, L11 + разбор M10, M15, L9, L12)

По согласованным решениям (короткий TTL+проверка блокировки; конфиг-таймауты ffmpeg; Postgres advisory-lock; явные транзакции + I/O после коммита):

  • M3StreamingOptions.StreamTokenMinutes (30), StreamTokenService.Validate возвращает id зрителя; LivePlaylist сверяет блокировку через IIdentityService (1 запрос на перезагрузку плейлиста, не на сегмент). Фронт (AirPage) перевыпускает cookie каждые 20 мин. Dev-ключ JWT добавлен в appsettings.Development.json, чтобы C2 не ломал локальный запуск.
  • M5/M7IAppDbContext.BeginTransactionAsync; ClearAllMedia/DeleteShowMedia — транзакция вокруг ExecuteDelete, файлы после коммита; DeleteMediaAsset/DeleteImage/ClearBumperAudio/ RemoveBumperTemplate — БД раньше файлов; RegisterMediaAsset — save→перенос с откатом при сбое.
  • M6RenderBumperPreviewQueryRenderBumperPreviewCommand (+ файл/класс переименованы).
  • M8ProcessRunner принимает таймаут; MediaOptions.ProbeTimeoutSeconds/TranscodeTimeoutSeconds; по таймауту процесс убивается, TimeoutException → ассет уходит в Failed, слот освобождается.
  • M9IAppDbContext.AcquireChannelLockAsync (pg_advisory_xact_lock по int64 из GUID канала); ScheduleGenerator.GenerateAsync обёрнут транзакцией + lock, снял и L4 (двойной save).
  • M14listAllMedia (дозагрузка всех страниц, safety-cap 5000) в пикерах ShowDetail/ChannelDetail
    • предупреждение при усечении (i18n ru/en). True server-pagination неуместна (клиентские исключение/парсинг/сортировка/bulk).
  • L1 — 5 delete-методов портов → async с CancellationToken; рекурсивные удаления офлоадятся.
  • L3Show.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 (полностью) — вынести рендер ТВ-заставок из тика планировщика: генерировать расписание с плейсхолдерами и рендерить ассеты асинхронно, чтобы один канал не задерживал достройку остальных и чтобы не держать транзакцию/advisory-lock во время ffmpeg.
  • L12 (полностью) — интеграционные тесты фоновых сервисов (media-конвейер, планировщик), оркестрации ScheduleGenerator, эндпоинтов (нужен реальный/контейнерный Postgres — InMemory не тянет транзакции/advisory-lock/raw SQL).
  • M15 (доменно) — если нужны разные файлы с одинаковым именем: пересмотреть уникальность MediaAsset (сейчас по OriginalFileName).