# Ревью 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). - [ ] **M3. Stream-токен неотзывной, TTL 6ч, игнорирует userId/блокировку** — `StreamTokenService.cs:15`, `StreamingEndpoints.cs:98,123`. _(дизайн-решение — отложено)_ - [x] **M4. Rate-limiter глобальный (не партиционирован), только на `/api/auth`** — `Program.cs:66`. ### Архитектура / транзакции - [ ] **M5. `ExecuteDeleteAsync` ломает границу UnitOfWork + файлы удаляются до коммита** — `DeleteShowMediaCommandHandler.cs:36`, `ClearAllMediaCommandHandler.cs`, `DeleteAllShowsCommandHandler.cs`. _(дизайн-решение — отложено)_ - [ ] **M6. Query с побочным эффектом на ФС** — `RenderBumperPreviewQueryHandler.cs:18` (рендер файлов под видом запроса). _(дизайн-решение — отложено)_ - [ ] **M7. Файловый/HTTP I/O до коммита** — Delete/Register/RefreshEpisodes хендлеры (orphan-файлы при откате). _(дизайн-решение — отложено)_ ### Планировщик / медиа - [ ] **M8. Нет таймаута на ffmpeg/ffprobe** — `ProcessRunner.cs:63` (зависший процесс держит слот). _(нужен разумный таймаут — отложено на согласование)_ - [ ] **M9. Гонка при конкурентной генерации расписания канала** — `ScheduleGenerator.cs:62` (тик + regenerate → дубли записей). _(дизайн-решение — отложено)_ - [ ] **M10. Рендер заставок синхронно внутри тика планировщика** — `ScheduleBumperResolver.cs:174`. _(дизайн-решение — отложено)_ - [x] **M11. Override через полночь не работает** — `SchedulePlannerModels.cs:40` (`EndMinute <= StartMinute` → пустое окно). ### Фронтенд - [x] **M12. Молчаливое проглатывание ошибки → вечный скелетон** — `AirPage.tsx:43`. - [x] **M13. Повторный 401 после refresh не разлогинивает** — `client.ts:80`. - [ ] **M14. Клиентская пагинация поверх усечённого ответа** — `ShowDetail.tsx:42` (`pageSize:500`), `ChannelDetail.tsx:32` (`pageSize:100`). _(нужна серверная пагинация — отложено)_ - [ ] **M15. Дедуп загрузок только по имени файла** — `upload-store.ts:143`. _(зависит от контракта серверной уникальности — отложено)_ --- ## 🟡 Низкое / прочее - [ ] **L1. Синхронный I/O без CancellationToken в портах удаления** (`IMediaStorage`/`IImageStore`/`IBumperTemplateStorage`). _(меняет интерфейсы портов — отложено)_ - [x] **L2. `ValidationBehavior` вызывает `Validate` вместо `ValidateAsync`** — `ValidationBehavior.cs:24`. - [ ] **L3. Инвариант «Single = 1 серия» проверяется в хендлере, а не в агрегате** — `Show.AddEpisode`. _(рефактор агрегата — отложено)_ - [ ] **L4. Двойной `SaveChanges` в командном пути ScheduleGenerator** — `ScheduleGenerator.cs:79,132`. _(связано с M9 — отложено)_ - [x] **L5. HLS-плеер не восстанавливается после fatal network/media error** — `ChannelPlayer.tsx:106` (+ симметричная очистка слушателей нативного HLS). - [x] **L6. Бесконечная повторная регистрация «падающего» файла из inbox** — `InboxScannerBackgroundService.cs:92`. - [x] **L7. Возможное переполнение int в весах шоу** — `SchedulePlanner.cs:256,265`. - [ ] **L8. OpenAPI/Scalar мапятся всегда, без гейта по окружению** — `Program.cs:108`. _(возможно намеренно для внутреннего инструмента — решение за владельцем)_ - [ ] **L9. `AllowedHosts: "*"` и dev-креды БД в appsettings.json**. _(dev-дефолты; решение за владельцем)_ - [x] **L10. SSRF-поверхность в ImageDownloader** (без allowlist схемы/хоста) — `ImageDownloader.cs:16` (allowlist http/https). - [ ] **L11. Документация (CLAUDE.md/README) отстала**: заявлено «каталог каналов и раздача видео не реализованы», хотя реализованы. _(нужна сверка с владельцем по объёму)_ - [ ] **L12. Пробелы в тестах**: фоновые сервисы, ScheduleGenerator-оркестрация, media-конвейер, эндпоинты без тестов. _(частично: добавлены тесты на M11)_ --- ## Журнал исправлений ### 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` без новых предупреждений.