# CLAUDE.md

Инструкции для Claude Code при работе в этом репозитории.

## Что это

**TeleWave** — сервис онлайн-каналов: пользователи смотрят сетку каналов, видео отдаётся из
хранилища на сервере. Админ управляет каналами и пользователями.

> Текущее состояние — **рабочий вертикальный срез**: вход/регистрация, роли, пользователи, админка;
> библиотека шоу/серий, загрузка и обработка медиа (ffmpeg → HLS-сегменты), реестр изображений,
> метаданные (TMDb/OMDb), каталог каналов, планировщик эфира (реклама, ТВ-заставки-переходы,
> weekly-override'ы) и live-раздача HLS с публичным просмотром сетки. Telegram-бот сознательно
> не делаем. Дальнейшие крупные направления (напр. многоэкземплярное развёртывание, новые доменные
> фичи) — по сверке с пользователем.

Архитектура и код-конвенции — прямое зеркало [`D:\Github\PnvPanel`](../PnvPanel) (тот же автор,
тот же стек), но домен урезан под текущий объём фичи.

## Стек

- **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).

## Архитектура — жёсткие правила

Слои и направление зависимостей: **Api → Infrastructure → Application → Domain** (внутрь).

- **Domain** — без внешних зависимостей. Сейчас единственная сущность — `Auth/RefreshToken`
  (rich model: приватные сеттеры, фабричный `Issue(...)`, поведенческий `Revoke(...)`).
- **Application** — CQRS-хендлеры, DTO, валидаторы, **порты** (интерфейсы:
  `IAppDbContext`, `ICurrentUser`, `IIdentityService`, `IJwtTokenService`, `IRefreshTokenService`,
  `IRoleService`). Зависит только от Domain — никаких `Npgsql`/`AspNetCore.Identity`, только их
  абстракции.
- **Infrastructure** — реализации портов: EF Core (`AppDbContext : IdentityDbContext<AppUser,
  AppRole, Guid>`), `IdentityService`/`RoleService`/`JwtTokenService`/`RefreshTokenService`,
  идемпотентный `DbInitializer` (сидинг ролей + админа).
- **Api** — Minimal API эндпоинты (`Endpoints/*Endpoints.cs`), middleware, DI composition root
  (`Program.cs`).

Обязательно:
- Команды меняют состояние в транзакции (`UnitOfWorkBehavior`); запросы только читают. Диспетчер —
  из `LiteCqrs.Net` (`ISender`/`ICommandHandler`/`IQueryHandler`), регистрация через
  `AddLiteCqrs(...)` в `TeleWave.Application/DependencyInjection.cs`.
- Управляемые ошибки — через `Result<T>` (`Common/Models/Result.cs`), не исключениями.
- Валидация — FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода.
- Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP. Никаких `.Result`/`.Wait()`.
- Nullable reference types включены; `Directory.Build.props` включает
  `TreatWarningsAsErrors=true` — предупреждения анализаторов не игнорировать.

## Домен: роли, пользователи, аутентификация

Полноценной доменной модели (каналы/видео) пока нет — см. заметку в начале файла. Текущие
инварианты:

- **Роли** (`AppRole : IdentityRole<Guid>`, `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`.

## Единый контейнер

- Один образ: 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`).

## Соглашения по коду

- `<Verb><Noun>Command`/`<Get|List><Noun>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}`.

## Команды

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 test tests/TeleWave.Application.Tests
# Интеграционные тесты (Testcontainers-Postgres) — нужен запущенный Docker; без него пропускаются:
dotnet test tests/TeleWave.Integration.Tests
dotnet run --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/`):
```bash
pnpm install
pnpm dev
pnpm build
pnpm lint && pnpm typecheck
```

Инфраструктура:
```bash
docker compose up -d --build   # только api; postgres — внешний, см. ConnectionStrings__Default
```

> Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент.

## Рабочие принципы

- Базовый вертикальный срез (каналы, планировщик, раздача HLS, обработка медиа) уже реализован —
  правь его по месту. Но новые крупные направления с непринятыми архитектурными решениями (напр.
  многоэкземплярное развёртывание, транскод-профили, новые доменные подсистемы) начинай только после
  сверки с пользователем. При неоднозначности — вопрос пользователю, не предположение.
- Соблюдай границы слоёв — главный инвариант проекта, как и в PnvPanel.
- Не коммить и не пуши без явной просьбы.
- Отвечай пользователю на русском.
