Refactor CLAUDE.md for clarity and updated architecture details
ci / build-backend (push) Successful in 3m56s
ci / build-frontend (push) Successful in 56s
ci / tests (push) Successful in 4m8s
ci / sonar (push) Successful in 4m36s

Revised the CLAUDE.md file to enhance clarity and accuracy in the project description. Updated terminology to reflect current functionality, including changes in user roles and media processing. Improved the architecture section by detailing the domain model and dependencies, ensuring a clearer understanding of the system's structure. Added references to documentation for better guidance on subsystem specifications.
This commit is contained in:
Leonid Pershin
2026-07-28 02:31:30 +03:00
parent fe11034b16
commit 0b9ea9f0f4
+81 -113
View File
@@ -4,157 +4,125 @@
## Что это ## Что это
**TeleWave** — сервис онлайн-каналов: пользователи смотрят сетку каналов, видео отдаётся из **TeleWave** — сервис онлайн-каналов: зритель смотрит сетку каналов, видео отдаётся из хранилища
хранилища на сервере. Админ управляет каналами и пользователями. на сервере. Админ ведёт библиотеку, каналы и эфир.
> Текущее состояние — **рабочий вертикальный срез**: вход/регистрация, роли, пользователи, админка; Работает: аутентификация, роли, пользователи; библиотека шоу/серий, коллекции, жанры, метаданные
> библиотека шоу/серий, загрузка и обработка медиа (ffmpeg → HLS-сегменты), реестр изображений, (TMDb/OMDb), обработка медиа (ffmpeg → HLS), изображения; группы, шаблон сетки, стыки и заставки,
> метаданные (TMDb/OMDb), каталог каналов, планировщик эфира (реклама, ТВ-заставки-переходы, автосборка по профилям, обмен конфигурацией файлом и запросом к ИИ; генерация ленты, live-раздача,
> weekly-override'ы) и live-раздача HLS с публичным просмотром сетки. Telegram-бот сознательно публичный просмотр, статистика хранилища. 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 через - **Backend**: C# / .NET 10, ASP.NET Core Minimal API, Clean Architecture, CQRS через
[`LiteCqrs.Net`](https://github.com/mrleo1nid/LiteCqrs.Net) (NuGet-пакет, лёгкая [`LiteCqrs.Net`](https://github.com/mrleo1nid/LiteCqrs.Net) (лёгкая альтернатива MediatR с явным
CQRS-библиотека, альтернатива MediatR с явным разделением Command/Query — см. её README). разделением Command/Query — см. README). EF Core 10 + Npgsql, Identity + JWT, FluentValidation,
EF Core 10 + Npgsql, ASP.NET Core Identity + JWT (access + refresh), FluentValidation, Serilog. Serilog, маппинг DTO вручную, OpenAPI + Scalar UI, тесты на xUnit + NSubstitute.
Маппинг DTO вручную. OpenAPI — нативный `Microsoft.AspNetCore.OpenApi` + Scalar UI. - **Frontend**: React 19 + Vite + TS, TanStack Query/Router (файловые роуты, `routeTree.gen.ts`
- **Frontend**: React 19 + Vite + TS, TanStack Query/Router, shadcn-стиль поверх Radix + Tailwind генерируется сборкой), shadcn поверх Radix + Tailwind v4, Zustand (только auth+тема),
v4, Zustand (только auth+тема), react-hook-form + zod. pnpm, oxlint. react-hook-form + zod, i18n (ru/en), pnpm + oxlint + prettier.
- **Инфра**: единый Docker-образ (API + статика SPA); PostgreSQL — **внешний сервер**, не в
compose (в отличие от PnvPanel).
## Архитектура — жёсткие правила ## Архитектура — жёсткие правила
Слои и направление зависимостей: **Api → Infrastructure → Application → Domain** (внутрь). Зависимости строго внутрь: **Api → Infrastructure → Application → Domain**.
- **Domain** — без внешних зависимостей. Сейчас единственная сущность — `Auth/RefreshToken` - **Domain** — без внешних зависимостей: `Auth`, `Library` (шоу, серии, коллекции, жанры), `Media`,
(rich model: приватные сеттеры, фабричный `Issue(...)`, поведенческий `Revoke(...)`). `Images`, `Programming` (группы, сетка, слоты, стыки + **чистый планировщик** `Planning`),
- **Application** — CQRS-хендлеры, DTO, валидаторы, **порты** (интерфейсы: `Broadcast` (каналы, заставки, лента, live), `Settings`. Модель rich: приватные сеттеры,
`IAppDbContext`, `ICurrentUser`, `IIdentityService`, `IJwtTokenService`, `IRefreshTokenService`, фабрики `Create`, поведенческие методы.
`IRoleService`). Зависит только от Domain — никаких `Npgsql`/`AspNetCore.Identity`, только их - **Application** — CQRS-хендлеры, DTO, валидаторы и **порты** (`Common/Interfaces/I*.cs`:
абстракции. `IAppDbContext`, `ICurrentUser`, `IMediaProcessor`, `IMediaStorage`, `IBumperRenderer`,
`IMetadataProvider`, `IImageStore`, `IStorageInspector`, …). Зависит только от Domain: никаких
`Npgsql`/`AspNetCore.Identity`, лишь их абстракции.
- **Infrastructure** — реализации портов: EF Core (`AppDbContext : IdentityDbContext<AppUser, - **Infrastructure** — реализации портов: EF Core (`AppDbContext : IdentityDbContext<AppUser,
AppRole, Guid>`), `IdentityService`/`RoleService`/`JwtTokenService`/`RefreshTokenService`, AppRole, Guid>`, конфигурации в `Persistence/Configurations`), ffmpeg, хранилище, метаданные,
идемпотентный `DbInitializer` (сидинг ролей + админа). идемпотентный `DbInitializer` (роли + админ из env `AdminSeed__*`).
- **Api** — Minimal API эндпоинты (`Endpoints/*Endpoints.cs`), middleware, DI composition root - **Api** — Minimal API (`Endpoints/*Endpoints.cs`), middleware, DI composition root (`Program.cs`).
(`Program.cs`).
Обязательно: Обязательно:
- Команды меняют состояние в транзакции (`UnitOfWorkBehavior`); запросы только читают. Диспетчер — - Команды меняют состояние в транзакции (`UnitOfWorkBehavior`); запросы только читают. Диспетчер —
из `LiteCqrs.Net` (`ISender`/`ICommandHandler`/`IQueryHandler`), регистрация через `ISender`/`ICommandHandler`/`IQueryHandler`, регистрация в `Application/DependencyInjection.cs`.
`AddLiteCqrs(...)` в `TeleWave.Application/DependencyInjection.cs`. - Управляемые ошибки — через `Result<T>` (`Common/Models/Result.cs`), не исключениями. Валидация —
- Управляемые ошибки — через `Result<T>` (`Common/Models/Result.cs`), не исключениями. FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода.
- Валидация — FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода. - Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP; никаких `.Result`/`.Wait()`.
- Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP. Никаких `.Result`/`.Wait()`. Nullable включён, `TreatWarningsAsErrors=true` — предупреждения анализаторов не игнорировать.
- Nullable reference types включены; `Directory.Build.props` включает - Планировщик (`Domain/Programming/Planning`) — **чистая функция**: вход `PlanningInput`, никаких
`TreatWarningsAsErrors=true` — предупреждения анализаторов не игнорировать. запросов и часов внутри. Данные под него собирает `Application/Programming/Planning`.
## Домен: роли, пользователи, аутентификация ## Инварианты, которые легко нарушить
Полноценной доменной модели (каналы/видео) пока нет — см. заметку в начале файла. Текущие - **Роли** динамические, у пользователя ровно одна; системные `admin`/`user` (`IsSystem=true`) не
инварианты: переименовать и не удалить, последнего админа не разжаловать, себя не заблокировать и не удалить.
- **Регистрация открытая** (создаётся `user`, токены сразу); понадобится модерация — заводи отдельный
- **Роли** (`AppRole : IdentityRole<Guid>`, `Infrastructure/Identity/`) — динамические, флаг, не переиспользуй `IsBlocked`. Вход по `UserName`, email и SMTP не используются; JWT access +
без квот (в отличие от PnvPanel: тут нет `MaxIpLimit`/`BillingEnabled` — при появлении refresh в httpOnly `tw_refresh_token` с обнаружением повторного использования отозванного.
доменных фич квоты/лимиты добавляются на `AppRole`/`AppUser` по мере необходимости, не заранее). - **Правка сетки эфир не двигает**: команда поднимает ревизию шаблона, ленту пересобирает применение.
У пользователя ровно одна роль. Системные роли `admin`/`user` (`IsSystem=true`) нельзя - **Вещательные сутки** начинаются с `Channel.DayStartTime` (06:00), сетка задаётся во времени
переименовать/удалить — проверяется в `RoleService`. Смену роли (`ChangeUserRoleCommand`) канала (`UtcOffsetMinutes`), в базе всё в UTC.
запрещено делать так, чтобы не осталось ни одного `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`.
## Единый контейнер ## Единый контейнер
- Один образ: Api раздаёт REST (`/api`) и статику SPA из `wwwroot` (fallback на `index.html`). - Один образ: Api раздаёт REST (`/api`) и статику SPA из `wwwroot` (fallback на `index.html`), один
Один origin, база API — относительный `/api`. origin, база API — относительный `/api`. Multi-stage Dockerfile: node → dotnet sdk → aspnet.
- Multi-stage Dockerfile: node (фронт) → dotnet sdk (publish + копирование в `wwwroot`) → aspnet Отдельный nginx для статики не вводить — ломает требование единого контейнера.
runtime. Контейнер работает под непривилегированным пользователем образа (`USER $APP_UID`, - Работает под непривилегированным `USER $APP_UID` (uid/gid 1654) **статически**, без
uid/gid 1654) — но **статически**, без gosu/entrypoint-скриптов и подгонки uid на старте: права gosu/entrypoint: права на bind-mount выставляет оператор (`docs/server-storage-setup.md` § 5).
на bind-mount хранилища выставляет оператор на хосте (см. `docs/server-storage-setup.md` § 5). Data Protection не заводим — шифруемых секретов на диске нет. **TLS внешний**: `app` отдаёт HTTP
Data Protection key-ring не заводим — секретов на диске пока нет (JWT-ключ — обычная конфигурация, и доверяет `X-Forwarded-*`.
не шифруемый секрет at-rest); появятся зашифрованные данные (например API-ключи внешних - docker-compose: только `app`. PostgreSQL внешний (`ConnectionStrings__Default` из `.env`), база
сервисов) — добавлять Data Protection по образцу PnvPanel. и пользователь создаются заранее вручную; миграции применяются на старте (`ApplyMigrationsAsync`),
- docker-compose: только `app` — PostgreSQL живёт вне compose (внешний сервер/хост, адрес и прав `CREATEDB` не нужно.
креды — в `ConnectionStrings__Default` из `.env`; база и пользователь на нём создаются
заранее вручную, приложение их не сидит). Миграции применяются авто на старте
(`ApplyMigrationsAsync` в `Program.cs`) — прав `CREATEDB` не требуется, только DDL/DML в
уже существующей базе.
- Не вводи отдельный nginx-контейнер для статики — ломает требование единого контейнера.
- **TLS — внешний**; `app` отдаёт HTTP + доверяет `X-Forwarded-*` (`ForwardedHeaders`).
## Соглашения по коду ## Соглашения по коду
- `<Verb><Noun>Command`/`<Get|List><Noun>Query` + `Handler`/`Validator` (валидатор — где есть что - `<Verb><Noun>Command`/`<Get|List><Noun>Query` + `Handler`/`Validator` (где есть что проверить),
проверить). Папки по фичам (`Auth/Login/`, `Admin/Roles/CreateRole/`, ...). папки по фичам (`Auth/Login/`, `Programming/Templates/Generate/`, …). Один публичный тип на файл =
- DTO — суффикс `Dto`; тела запросов Api-эндпоинтов, не совпадающие с командой 1:1, — `Body` имя файла, кроме `Body` — тела Api-эндпоинтов, не совпадающие с командой 1:1 (`sealed record`
(`sealed record` внизу файла эндпоинта). внизу файла эндпоинта). DTO — суффикс `Dto`.
- Один публичный тип на файл = имя файла (кроме `Body` в файле эндпоинтов). - Ошибки — каталоги `XErrors` (`Error.Validation/NotFound/Conflict/…`), наружу единым
- Ошибки — статические каталоги `XErrors` (`AuthErrors`, `RoleErrors`, `UserErrors`) с `application/problem+json` (`Api/Common/ResultExtensions.cs`). Секреты не логировать.
`Error.Validation/NotFound/Conflict/Unauthorized/Forbidden(...)`. - **Изображения — только через реестр `Image`** (`Domain/Images`, галерея `features/admin/images`):
- Секреты не логировать; логи — Serilog. картинка хранится как `ImageId` и выбирается контролом `ImageGallery`/`GalleryBrowser`, своих
- Ошибки API — единый `application/problem+json` (см. `Api/Common/ResultExtensions.cs`). файловых полей не заводить. Файлы — `images/{id}{ext}`, отдаются по `/api/images/{id}`.
- **Изображения — через общий реестр `Image`** (домен `Domain/Images`, галерея `features/admin/images`). - Фронт: фича = папка в `src/features/admin/<фича>`; тексты только через i18n, ключи в `ru.ts` и `en.ts`.
Любой новый функционал, где загружается или выбирается картинка, должен использовать контрол - Комментарии объясняют **почему**, а не что; по-русски, как в существующем коде.
`ImageGallery`/`GalleryBrowser` (пикер по категориям `ImageCategory`) и хранить ссылку на `ImageId`,
а не заводить своё файловое поле. Автоматически полученные картинки (например постер из метадаты)
тоже регистрируются как `Image`. Файлы лежат под `images/{id}{ext}`, отдаются по `/api/images/{id}`.
## Команды ## Команды
Backend (из `backend/`): Backend (из `backend/`):
```bash ```bash
dotnet build dotnet build
# Форматирование — csharpier, версия закреплена в .config/dotnet-tools.json (один раз: dotnet tool restore). dotnet csharpier format . # только через dotnet (версия из .config/); в CI — csharpier check
# Глобально установленный csharpier может быть другой версии — вызывать только через dotnet: dotnet test tests/TeleWave.Domain.Tests # по одному проекту: MSBuild не берёт несколько
dotnet csharpier format .
dotnet csharpier check .
# Юнит-тесты (по одному проекту за вызов — MSBuild не принимает несколько):
dotnet test tests/TeleWave.Domain.Tests
dotnet test tests/TeleWave.Application.Tests dotnet test tests/TeleWave.Application.Tests
# Интеграционные тесты (Testcontainers-Postgres) — нужен запущенный Docker; без него пропускаются: dotnet test tests/TeleWave.Integration.Tests # Testcontainers; без Docker пропускаются
dotnet test tests/TeleWave.Integration.Tests
dotnet run --project src/TeleWave.Api dotnet run --project src/TeleWave.Api
dotnet ef migrations add <Name> --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api dotnet ef migrations add <Name> --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api
dotnet ef database update --project src/TeleWave.Infrastructure --startup-project src/TeleWave.Api
``` ```
Frontend (из `frontend/`): Frontend (из `frontend/`):
```bash ```bash
pnpm install pnpm install && pnpm dev
pnpm dev pnpm lint && pnpm typecheck && pnpm build
pnpm build pnpm format && pnpm format:check # CI падает на неотформатированном — прогоняй после правок
pnpm lint && pnpm typecheck
``` ```
Инфраструктура: Инфраструктура: `docker compose up -d --build` (только api; postgres внешний). Окружение — Windows,
```bash оболочка **PowerShell**; для POSIX-скриптов есть Bash-инструмент.
docker compose up -d --build # только api; postgres — внешний, см. ConnectionStrings__Default
```
> Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент.
## Рабочие принципы ## Рабочие принципы
- Базовый вертикальный срез (каналы, планировщик, раздача HLS, обработка медиа) уже реализован — - Базовый срез реализован — правь его по месту. Новые крупные направления с непринятыми решениями
правь его по месту. Но новые крупные направления с непринятыми архитектурными решениями (напр. (многоэкземплярность, транскод-профили, новые подсистемы) — только после сверки с пользователем.
многоэкземплярное развёртывание, транскод-профили, новые доменные подсистемы) начинай только после При неоднозначности — вопрос, а не предположение.
сверки с пользователем. При неоднозначности — вопрос пользователю, не предположение. - Меняешь поведение подсистемы — обнови её спеку в `docs/` тем же заходом.
- Соблюдай границы слоёв — главный инвариант проекта - Соблюдай границы слоёв — главный инвариант проекта.
- Не коммить и не пуши без явной просьбы. - Не коммить и не пуши без явной просьбы.
- Отвечай пользователю на русском. - Отвечай пользователю на русском.