diff --git a/CLAUDE.md b/CLAUDE.md index 7d30a0d..0fcd25f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,157 +4,125 @@ ## Что это -**TeleWave** — сервис онлайн-каналов: пользователи смотрят сетку каналов, видео отдаётся из -хранилища на сервере. Админ управляет каналами и пользователями. +**TeleWave** — сервис онлайн-каналов: зритель смотрит сетку каналов, видео отдаётся из хранилища +на сервере. Админ ведёт библиотеку, каналы и эфир. -> Текущее состояние — **рабочий вертикальный срез**: вход/регистрация, роли, пользователи, админка; -> библиотека шоу/серий, загрузка и обработка медиа (ffmpeg → HLS-сегменты), реестр изображений, -> метаданные (TMDb/OMDb), каталог каналов, планировщик эфира (реклама, ТВ-заставки-переходы, -> weekly-override'ы) и live-раздача HLS с публичным просмотром сетки. Telegram-бот сознательно -> не делаем. Дальнейшие крупные направления (напр. многоэкземплярное развёртывание, новые доменные -> фичи) — по сверке с пользователем. +Работает: аутентификация, роли, пользователи; библиотека шоу/серий, коллекции, жанры, метаданные +(TMDb/OMDb), обработка медиа (ffmpeg → HLS), изображения; группы, шаблон сетки, стыки и заставки, +автосборка по профилям, обмен конфигурацией файлом и запросом к ИИ; генерация ленты, live-раздача, +публичный просмотр, статистика хранилища. Telegram-бот сознательно не делаем. Конвенции — зеркало +[`PnvPanel`](../PnvPanel) (тот же автор). -Архитектура и код-конвенции — прямое зеркало [`D:\Github\PnvPanel`](../PnvPanel) (тот же автор, -тот же стек), но домен урезан под текущий объём фичи. +**Спеки в `docs/` — источник правды по домену**, читай до правок в подсистеме: +[`tv-scheduler-architecture.md`](docs/tv-scheduler-architecture.md) (группы, сетка, слоты, стыки, +заставки, планировщик, автосборка, обмен конфигурацией — главный документ), +[`media-storage-and-streaming.md`](docs/media-storage-and-streaming.md) и +[`server-storage-setup.md`](docs/server-storage-setup.md) (медиа и хранилище). ## Стек -- **Backend**: C# / .NET 10, ASP.NET Core Web API (Minimal API), Clean Architecture, CQRS через - [`LiteCqrs.Net`](https://github.com/mrleo1nid/LiteCqrs.Net) (NuGet-пакет, лёгкая - CQRS-библиотека, альтернатива MediatR с явным разделением Command/Query — см. её README). - EF Core 10 + Npgsql, ASP.NET Core Identity + JWT (access + refresh), FluentValidation, Serilog. - Маппинг DTO вручную. OpenAPI — нативный `Microsoft.AspNetCore.OpenApi` + Scalar UI. -- **Frontend**: React 19 + Vite + TS, TanStack Query/Router, shadcn-стиль поверх Radix + Tailwind - v4, Zustand (только auth+тема), react-hook-form + zod. pnpm, oxlint. -- **Инфра**: единый Docker-образ (API + статика SPA); PostgreSQL — **внешний сервер**, не в - compose (в отличие от PnvPanel). +- **Backend**: C# / .NET 10, ASP.NET Core Minimal API, Clean Architecture, CQRS через + [`LiteCqrs.Net`](https://github.com/mrleo1nid/LiteCqrs.Net) (лёгкая альтернатива MediatR с явным + разделением Command/Query — см. README). EF Core 10 + Npgsql, Identity + JWT, FluentValidation, + Serilog, маппинг DTO вручную, OpenAPI + Scalar UI, тесты на xUnit + NSubstitute. +- **Frontend**: React 19 + Vite + TS, TanStack Query/Router (файловые роуты, `routeTree.gen.ts` + генерируется сборкой), shadcn поверх Radix + Tailwind v4, Zustand (только auth+тема), + react-hook-form + zod, i18n (ru/en), pnpm + oxlint + prettier. ## Архитектура — жёсткие правила -Слои и направление зависимостей: **Api → Infrastructure → Application → Domain** (внутрь). +Зависимости строго внутрь: **Api → Infrastructure → Application → Domain**. -- **Domain** — без внешних зависимостей. Сейчас единственная сущность — `Auth/RefreshToken` - (rich model: приватные сеттеры, фабричный `Issue(...)`, поведенческий `Revoke(...)`). -- **Application** — CQRS-хендлеры, DTO, валидаторы, **порты** (интерфейсы: - `IAppDbContext`, `ICurrentUser`, `IIdentityService`, `IJwtTokenService`, `IRefreshTokenService`, - `IRoleService`). Зависит только от Domain — никаких `Npgsql`/`AspNetCore.Identity`, только их - абстракции. +- **Domain** — без внешних зависимостей: `Auth`, `Library` (шоу, серии, коллекции, жанры), `Media`, + `Images`, `Programming` (группы, сетка, слоты, стыки + **чистый планировщик** `Planning`), + `Broadcast` (каналы, заставки, лента, live), `Settings`. Модель rich: приватные сеттеры, + фабрики `Create`, поведенческие методы. +- **Application** — CQRS-хендлеры, DTO, валидаторы и **порты** (`Common/Interfaces/I*.cs`: + `IAppDbContext`, `ICurrentUser`, `IMediaProcessor`, `IMediaStorage`, `IBumperRenderer`, + `IMetadataProvider`, `IImageStore`, `IStorageInspector`, …). Зависит только от Domain: никаких + `Npgsql`/`AspNetCore.Identity`, лишь их абстракции. - **Infrastructure** — реализации портов: EF Core (`AppDbContext : IdentityDbContext`), `IdentityService`/`RoleService`/`JwtTokenService`/`RefreshTokenService`, - идемпотентный `DbInitializer` (сидинг ролей + админа). -- **Api** — Minimal API эндпоинты (`Endpoints/*Endpoints.cs`), middleware, DI composition root - (`Program.cs`). + AppRole, Guid>`, конфигурации в `Persistence/Configurations`), ffmpeg, хранилище, метаданные, + идемпотентный `DbInitializer` (роли + админ из env `AdminSeed__*`). +- **Api** — Minimal API (`Endpoints/*Endpoints.cs`), middleware, DI composition root (`Program.cs`). Обязательно: - Команды меняют состояние в транзакции (`UnitOfWorkBehavior`); запросы только читают. Диспетчер — - из `LiteCqrs.Net` (`ISender`/`ICommandHandler`/`IQueryHandler`), регистрация через - `AddLiteCqrs(...)` в `TeleWave.Application/DependencyInjection.cs`. -- Управляемые ошибки — через `Result` (`Common/Models/Result.cs`), не исключениями. -- Валидация — FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода. -- Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP. Никаких `.Result`/`.Wait()`. -- Nullable reference types включены; `Directory.Build.props` включает - `TreatWarningsAsErrors=true` — предупреждения анализаторов не игнорировать. + `ISender`/`ICommandHandler`/`IQueryHandler`, регистрация в `Application/DependencyInjection.cs`. +- Управляемые ошибки — через `Result` (`Common/Models/Result.cs`), не исключениями. Валидация — + FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода. +- Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP; никаких `.Result`/`.Wait()`. + Nullable включён, `TreatWarningsAsErrors=true` — предупреждения анализаторов не игнорировать. +- Планировщик (`Domain/Programming/Planning`) — **чистая функция**: вход `PlanningInput`, никаких + запросов и часов внутри. Данные под него собирает `Application/Programming/Planning`. -## Домен: роли, пользователи, аутентификация +## Инварианты, которые легко нарушить -Полноценной доменной модели (каналы/видео) пока нет — см. заметку в начале файла. Текущие -инварианты: - -- **Роли** (`AppRole : IdentityRole`, `Infrastructure/Identity/`) — динамические, - без квот (в отличие от PnvPanel: тут нет `MaxIpLimit`/`BillingEnabled` — при появлении - доменных фич квоты/лимиты добавляются на `AppRole`/`AppUser` по мере необходимости, не заранее). - У пользователя ровно одна роль. Системные роли `admin`/`user` (`IsSystem=true`) нельзя - переименовать/удалить — проверяется в `RoleService`. Смену роли (`ChangeUserRoleCommand`) - запрещено делать так, чтобы не осталось ни одного `admin` (`RoleErrors.CannotRemoveLastAdmin`). -- **Регистрация** — открытая, без гейта активации (в отличие от PnvPanel): `RegisterCommandHandler` - создаёт пользователя с ролью `user` и сразу выдаёт токены (auto-login). Если в будущем - понадобится модерация новых пользователей — добавлять отдельным полем/флагом по аналогии с - `IsActivated` в PnvPanel, не переиспользовать `IsBlocked`. -- **Блокировка** (`AppUser.IsBlocked`) — админ блокирует/разблокирует пользователя; вход - запрещён (`LoginCommandHandler`/`RefreshCommandHandler` проверяют флаг). Админ не может - заблокировать/удалить самого себя (`UserErrors.CannotBlockSelf`/`CannotDeleteSelf`). -- **Вход по `UserName`** (email не используется, SMTP не нужен). JWT — access (короткий TTL) + - refresh (httpOnly cookie `tw_refresh_token`, `SameSite=Strict`, ротация с обнаружением повторного - использования отозванного токена — см. `RefreshTokenService.RotateAsync`). -- **Сидинг**: идемпотентный `DbInitializer` создаёт системные роли + админа из env - (`AdminSeed__Username`/`Password`) при старте. Источник примера env — `.env.example`. +- **Роли** динамические, у пользователя ровно одна; системные `admin`/`user` (`IsSystem=true`) не + переименовать и не удалить, последнего админа не разжаловать, себя не заблокировать и не удалить. +- **Регистрация открытая** (создаётся `user`, токены сразу); понадобится модерация — заводи отдельный + флаг, не переиспользуй `IsBlocked`. Вход по `UserName`, email и SMTP не используются; JWT access + + refresh в httpOnly `tw_refresh_token` с обнаружением повторного использования отозванного. +- **Правка сетки эфир не двигает**: команда поднимает ревизию шаблона, ленту пересобирает применение. +- **Вещательные сутки** начинаются с `Channel.DayStartTime` (06:00), сетка задаётся во времени + канала (`UtcOffsetMinutes`), в базе всё в UTC. ## Единый контейнер -- Один образ: Api раздаёт REST (`/api`) и статику SPA из `wwwroot` (fallback на `index.html`). - Один origin, база API — относительный `/api`. -- Multi-stage Dockerfile: node (фронт) → dotnet sdk (publish + копирование в `wwwroot`) → aspnet - runtime. Контейнер работает под непривилегированным пользователем образа (`USER $APP_UID`, - uid/gid 1654) — но **статически**, без gosu/entrypoint-скриптов и подгонки uid на старте: права - на bind-mount хранилища выставляет оператор на хосте (см. `docs/server-storage-setup.md` § 5). - Data Protection key-ring не заводим — секретов на диске пока нет (JWT-ключ — обычная конфигурация, - не шифруемый секрет at-rest); появятся зашифрованные данные (например API-ключи внешних - сервисов) — добавлять Data Protection по образцу PnvPanel. -- docker-compose: только `app` — PostgreSQL живёт вне compose (внешний сервер/хост, адрес и - креды — в `ConnectionStrings__Default` из `.env`; база и пользователь на нём создаются - заранее вручную, приложение их не сидит). Миграции применяются авто на старте - (`ApplyMigrationsAsync` в `Program.cs`) — прав `CREATEDB` не требуется, только DDL/DML в - уже существующей базе. -- Не вводи отдельный nginx-контейнер для статики — ломает требование единого контейнера. -- **TLS — внешний**; `app` отдаёт HTTP + доверяет `X-Forwarded-*` (`ForwardedHeaders`). +- Один образ: Api раздаёт REST (`/api`) и статику SPA из `wwwroot` (fallback на `index.html`), один + origin, база API — относительный `/api`. Multi-stage Dockerfile: node → dotnet sdk → aspnet. + Отдельный nginx для статики не вводить — ломает требование единого контейнера. +- Работает под непривилегированным `USER $APP_UID` (uid/gid 1654) **статически**, без + gosu/entrypoint: права на bind-mount выставляет оператор (`docs/server-storage-setup.md` § 5). + Data Protection не заводим — шифруемых секретов на диске нет. **TLS внешний**: `app` отдаёт HTTP + и доверяет `X-Forwarded-*`. +- docker-compose: только `app`. PostgreSQL внешний (`ConnectionStrings__Default` из `.env`), база + и пользователь создаются заранее вручную; миграции применяются на старте (`ApplyMigrationsAsync`), + прав `CREATEDB` не нужно. ## Соглашения по коду -- `Command`/`Query` + `Handler`/`Validator` (валидатор — где есть что - проверить). Папки — по фичам (`Auth/Login/`, `Admin/Roles/CreateRole/`, ...). -- DTO — суффикс `Dto`; тела запросов Api-эндпоинтов, не совпадающие с командой 1:1, — `Body` - (`sealed record` внизу файла эндпоинта). -- Один публичный тип на файл = имя файла (кроме `Body` в файле эндпоинтов). -- Ошибки — статические каталоги `XErrors` (`AuthErrors`, `RoleErrors`, `UserErrors`) с - `Error.Validation/NotFound/Conflict/Unauthorized/Forbidden(...)`. -- Секреты не логировать; логи — Serilog. -- Ошибки API — единый `application/problem+json` (см. `Api/Common/ResultExtensions.cs`). -- **Изображения — через общий реестр `Image`** (домен `Domain/Images`, галерея `features/admin/images`). - Любой новый функционал, где загружается или выбирается картинка, должен использовать контрол - `ImageGallery`/`GalleryBrowser` (пикер по категориям `ImageCategory`) и хранить ссылку на `ImageId`, - а не заводить своё файловое поле. Автоматически полученные картинки (например постер из метадаты) - тоже регистрируются как `Image`. Файлы лежат под `images/{id}{ext}`, отдаются по `/api/images/{id}`. +- `Command`/`Query` + `Handler`/`Validator` (где есть что проверить), + папки по фичам (`Auth/Login/`, `Programming/Templates/Generate/`, …). Один публичный тип на файл = + имя файла, кроме `Body` — тела Api-эндпоинтов, не совпадающие с командой 1:1 (`sealed record` + внизу файла эндпоинта). DTO — суффикс `Dto`. +- Ошибки — каталоги `XErrors` (`Error.Validation/NotFound/Conflict/…`), наружу единым + `application/problem+json` (`Api/Common/ResultExtensions.cs`). Секреты не логировать. +- **Изображения — только через реестр `Image`** (`Domain/Images`, галерея `features/admin/images`): + картинка хранится как `ImageId` и выбирается контролом `ImageGallery`/`GalleryBrowser`, своих + файловых полей не заводить. Файлы — `images/{id}{ext}`, отдаются по `/api/images/{id}`. +- Фронт: фича = папка в `src/features/admin/<фича>`; тексты только через i18n, ключи в `ru.ts` и `en.ts`. +- Комментарии объясняют **почему**, а не что; по-русски, как в существующем коде. ## Команды Backend (из `backend/`): ```bash dotnet build -# Форматирование — csharpier, версия закреплена в .config/dotnet-tools.json (один раз: dotnet tool restore). -# Глобально установленный csharpier может быть другой версии — вызывать только через dotnet: -dotnet csharpier format . -dotnet csharpier check . -# Юнит-тесты (по одному проекту за вызов — MSBuild не принимает несколько): -dotnet test tests/TeleWave.Domain.Tests +dotnet csharpier format . # только через dotnet (версия из .config/); в CI — csharpier check +dotnet test tests/TeleWave.Domain.Tests # по одному проекту: MSBuild не берёт несколько dotnet test tests/TeleWave.Application.Tests -# Интеграционные тесты (Testcontainers-Postgres) — нужен запущенный Docker; без него пропускаются: -dotnet test tests/TeleWave.Integration.Tests +dotnet test tests/TeleWave.Integration.Tests # Testcontainers; без Docker пропускаются dotnet run --project src/TeleWave.Api dotnet ef migrations add --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api -dotnet ef database update --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api ``` Frontend (из `frontend/`): ```bash -pnpm install -pnpm dev -pnpm build -pnpm lint && pnpm typecheck +pnpm install && pnpm dev +pnpm lint && pnpm typecheck && pnpm build +pnpm format && pnpm format:check # CI падает на неотформатированном — прогоняй после правок ``` -Инфраструктура: -```bash -docker compose up -d --build # только api; postgres — внешний, см. ConnectionStrings__Default -``` - -> Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент. +Инфраструктура: `docker compose up -d --build` (только api; postgres внешний). Окружение — Windows, +оболочка **PowerShell**; для POSIX-скриптов есть Bash-инструмент. ## Рабочие принципы -- Базовый вертикальный срез (каналы, планировщик, раздача HLS, обработка медиа) уже реализован — - правь его по месту. Но новые крупные направления с непринятыми архитектурными решениями (напр. - многоэкземплярное развёртывание, транскод-профили, новые доменные подсистемы) начинай только после - сверки с пользователем. При неоднозначности — вопрос пользователю, не предположение. -- Соблюдай границы слоёв — главный инвариант проекта +- Базовый срез реализован — правь его по месту. Новые крупные направления с непринятыми решениями + (многоэкземплярность, транскод-профили, новые подсистемы) — только после сверки с пользователем. + При неоднозначности — вопрос, а не предположение. +- Меняешь поведение подсистемы — обнови её спеку в `docs/` тем же заходом. +- Соблюдай границы слоёв — главный инвариант проекта. - Не коммить и не пуши без явной просьбы. - Отвечай пользователю на русском.