From 0b9ea9f0f4ec9a2058b36eab084e5d98022b43bc Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Tue, 28 Jul 2026 02:31:30 +0300 Subject: [PATCH] Refactor CLAUDE.md for clarity and updated architecture details 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. --- CLAUDE.md | 194 +++++++++++++++++++++++------------------------------- 1 file changed, 81 insertions(+), 113 deletions(-) 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/` тем же заходом. +- Соблюдай границы слоёв — главный инвариант проекта. - Не коммить и не пуши без явной просьбы. - Отвечай пользователю на русском.