# NovaPoker — документация команды, полный текст Собрано из https://docs.novapoker.ru. Отдельные статьи и их адреса — в https://docs.novapoker.ru/llms.txt # Документация NovaPoker Источник: https://docs.novapoker.ru/ --- Здесь описано, как платформа работает **сейчас**. Два входа — выбирайте свой. ## Продукт [Продуктовая дорожка](https://docs.novapoker.ru/product.md) отвечает на вопрос «что происходит и по каким правилам»: сценарии игрока и организатора, статусы, пограничные случаи. ## Разработка [Инженерная дорожка](https://docs.novapoker.ru/dev.md) отвечает на вопрос «как устроено и где лежит»: сервисы, контракты, модель данных, запуск и деплой. ## Если вы агент - [llms.txt](https://docs.novapoker.ru/llms.txt) — оглавление со ссылками сразу на markdown. - [llms-full.txt](https://docs.novapoker.ru/llms-full.txt) — вся документация одним файлом. - У каждой страницы есть markdown-близнец: к адресу без завершающего слеша добавьте `.md`. Например `/product/registration/` → `/product/registration.md`. === # Ландшафт и границы Источник: https://docs.novapoker.ru/dev/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-as-built-system-spec.md --- Один бэкенд и три фронта под разные роли. | Сервис | Стек | Таргет | Аудитория | |---|---|---|---| | **poker-api** | Node, TypeScript, NestJS, Prisma, PostgreSQL, socket.io, pg-boss; pnpm-workspace (`apps/server` + `packages/rating`) | сервер, HTTP и WS на одном порту | все клиенты | | **poker-player** | Expo / React Native, expo-router, react-query, zustand, i18next | iOS, Android, web-сборка | игроки | | **poker-organizer** | Expo / React Native, тот же набор | прежде всего Expo Web — планшет или ноутбук в заведении | владельцы клубов, ТД, дилеры | | **poker-admin** | React + Vite, TanStack Router, react-query, zustand, Tailwind | статика в `dist/` | платформенные админы | ## Контракт между фронтами и бэкендом **Связь только по HTTP и WebSocket.** Общего кода у фронтов и бэкенда нет — контракт материализован в `apps/server/openapi.json` (83 пути) и собирается командой `pnpm --filter server openapi` из CLI-плагина `@nestjs/swagger`. У приложения игрока из него генерируются типы: `pnpm gen:api` кладёт результат в `src/api/schema.d.ts`. Базовый адрес API у каждого фронта — переменная окружения: `VITE_API_URL` у админки, `EXPO_PUBLIC_API_URL` у игрока и организатора. Значение — голый origin без версии; префикс `/api/v1` дописывает HTTP-клиент, чтобы он жил в одном месте. У админки эта переменная — **аргумент сборки, а не переменная рантайма**. Подробности и последствия — в [карточке poker-admin](https://docs.novapoker.ru/dev/poker-admin.md). ## Границы: чего в системе нет - **Денежного контура.** `entryFee` — мёртвая колонка со значением 0, `paymentStatus` всегда `not_required`. - **Ре-энтри, ребаев, аддонов.** Вылет окончателен, обратим только через отмену последнего вылета. - **Учёта нокаутов.** Кто кого выбил, не хранится: в рейтинге есть только места и победы. - **Работающего `lateRegLevel`.** Поле хранится в шаблоне блайндов, но нигде не проверяется. - **Статуса `announced`.** Он есть в перечислении, но не используется — турнир создаётся сразу в `registration`. - **Полноценной пагинации.** Ограничения фиксированные: афиша не больше 50, уровневый лидерборд — топ-100, уведомления — последние 50; остальные списки отдаются целиком. - **Английского интерфейса.** Двуязычны только данные, строки интерфейса русские. - **Нативных сборок.** EAS не настроен; рабочие таргеты — Expo Dev Server и web-экспорт. ## Что рядом - Карточки сервисов: [poker-api](https://docs.novapoker.ru/dev/poker-api.md), [poker-player](https://docs.novapoker.ru/dev/poker-player.md), [poker-organizer](https://docs.novapoker.ru/dev/poker-organizer.md), [poker-admin](https://docs.novapoker.ru/dev/poker-admin.md). - [Продуктовый обзор](https://docs.novapoker.ru/product.md) — тот же ландшафт со стороны пользователя. - [Карта документации](https://docs.novapoker.ru/map.md) — все статьи и дата последней сверки с кодом. === # poker-api Источник: https://docs.novapoker.ru/dev/poker-api/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/README.md, repos/poker-api/apps/server/package.json --- Единственный бэкенд платформы: его слушают все три клиентских приложения. HTTP и WebSocket живут на одном порту. **Где код:** `github.com/Klonaps/poker-api`, локально `repos/poker-api`. ## Стек Node 24, TypeScript 5, NestJS 11, Prisma, PostgreSQL 16, `@nestjs/websockets` + socket.io, pg-boss для фоновых задач. Это pnpm-workspace из двух пакетов: `apps/server` — само приложение, `packages/rating` — изолированные формулы рейтинга. ## Как запустить локально ```bash docker compose up -d cp .env.example apps/server/.env pnpm i pnpm --filter server exec prisma migrate dev pnpm dev ``` PostgreSQL поднимается через docker compose на порту **5433**, чтобы не спорить с локальной базой на 5432. Сервер по умолчанию слушает `3000`. ## Как собирается и куда деплоится `Dockerfile` в корне репозитория, деплой через Dokploy. Снаружи API живёт на `novapoker.ru` с префиксом `/api/v1`; проверка живости — `GET /api/v1/health`. Разделение с веб-сборкой игрока на том же хосте делает Dokploy по префиксу пути. ## Контракт `apps/server/openapi.json` — 83 пути, собирается командой `pnpm --filter server openapi`. Из него приложение игрока генерирует типы. Файл коммитится: он и есть контракт, по которому фронты живут. ## Тесты ```bash pnpm test ``` Запускает `pnpm -r test` по всему workspace. Формулы в `packages/rating` покрыты на 100% — это условие, а не достижение: по ним считаются очки игроков. ## Что рядом - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — как сервис связан с остальными. === # poker-player Источник: https://docs.novapoker.ru/dev/poker-player/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-player/package.json, repos/poker-player/Dockerfile --- Приложение для посетителей турниров: найти турнир, записаться, следить за игрой, расти в рейтинге. **Где код:** `github.com/Klonaps/poker-player`, локально `repos/poker-player`. ## Стек Expo 57 / React Native 0.86, expo-router, TanStack Query, zustand, i18next, socket.io-client, reanimated. Таргеты — iOS, Android и веб-сборка. ## Как запустить локально ```bash pnpm --dir repos/poker-player exec expo start --web --port 8090 --clear ``` Адрес API задаётся переменной `EXPO_PUBLIC_API_URL`; для локального стенда это `http://localhost:3001`. Конфигурация стенда лежит в `.claude/launch.json` корневого репозитория. ## Как собирается и куда деплоится `pnpm build:web` — `expo export --platform web` плюс минификация HTML. В образе статику раздаёт nginx, `app.json` задаёт `web.output = "single"`, то есть SPA с клиентской маршрутизацией: nginx заворачивает неизвестные пути в `index.html`. **Обе переменные `EXPO_PUBLIC_*` — аргументы сборки, а не переменные рантайма.** Metro подставляет значения прямо в бандл, поэтому сменить их можно только пересборкой образа; в Dokploy они задаются в Build Args, не в Env: | Аргумент | Что задаёт | |---|---| | `EXPO_PUBLIC_API_URL` | голый origin API, без `/api/v1` | | `EXPO_PUBLIC_SHARE_BASE_URL` | база для ссылок «поделиться турниром» | Значение `EXPO_PUBLIC_API_URL` — именно голый origin: префикс версии дописывает HTTP-клиент, а socket.io берёт этот же origin для namespace `/live`, который под префиксом не живёт. ## Типы из контракта ```bash pnpm gen:api ``` Генерирует `src/api/schema.d.ts` из `openapi.json` бэкенда. Запускать после каждого изменения контракта, иначе типы разъедутся с реальностью молча. ## Тесты ```bash pnpm test ``` Это `jest --forceExit`. Флаг обязателен: без него открытые хендлы не дают процессу завершиться, и прогон висит до таймаута. ## Что рядом - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — как сервис связан с остальными. === # poker-organizer Источник: https://docs.novapoker.ru/dev/poker-organizer/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-organizer/package.json, repos/poker-organizer/Dockerfile --- Рабочее приложение клуба: завести турнир, провести день игры от чек-ина до результатов, вести live-пульт. Им пользуются владельцы клубов, турнирные директора и дилеры — с разными правами. **Где код:** `github.com/Klonaps/poker-organizer`, локально `repos/poker-organizer`. ## Стек Тот же, что у игрока: Expo 57 / React Native 0.86, expo-router, TanStack Query, zustand, i18next, socket.io-client. Отличие — целевое устройство: прежде всего **Expo Web на планшете или ноутбуке в заведении**, а не телефон. ## Как запустить локально ```bash pnpm --dir repos/poker-organizer exec expo start --web --port 8092 --clear ``` Адрес API — `EXPO_PUBLIC_API_URL`, для локального стенда `http://localhost:3001`. ## Как собирается и куда деплоится `pnpm build:web` — `expo export --platform web` плюс минификация HTML, дальше nginx в образе. Единственный аргумент сборки — `EXPO_PUBLIC_API_URL`, и он тоже **Build Arg, а не Env**: Metro вшивает значение в бандл. ## Реалтайм Организатор подключается к namespace `/live` с токеном в handshake (`auth: { token }`) — гейт требует JWT на подключении. Это стоит помнить при отладке: молчащий сокет чаще всего означает отсутствующий или протухший токен, а не потерянное соединение. ## Тесты ```bash pnpm test ``` Это `jest --forceExit` — по той же причине, что у игрока. ## Что рядом - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — как сервис связан с остальными. === # poker-admin Источник: https://docs.novapoker.ru/dev/poker-admin/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-admin/package.json, repos/poker-admin/Dockerfile, repos/poker-admin/nginx.conf --- Внутренний инструмент команды платформы: клубы, контент, справочники, модерация игроков, метрики и настройки рейтинга. **Где код:** `github.com/Klonaps/poker-admin`, локально `repos/poker-admin`. Снаружи — `admin.novapoker.ru`. ## Стек React 19, Vite 8, TanStack Router и TanStack Query, zustand, Tailwind 4, i18next, lucide-react. Обычный SPA, собираемый в статику. ## Как запустить локально ```bash pnpm --dir repos/poker-admin dev --port 5199 --strictPort ``` ## Как собирается и куда деплоится `pnpm build` — это `tsc -b && vite build`. Дальше multi-stage Dockerfile кладёт `dist/` в `nginx:alpine`; Dokploy тянет образ и вешает домен. ### Грабля, на которую наступают все **`VITE_API_URL` — аргумент сборки, а не переменная рантайма.** Vite подставляет значение прямо в бандл, поэтому сменить адрес API можно **только пересборкой образа**. В Dokploy он задаётся в Build Args, не в Env: правка переменной окружения без пересборки не меняет ничего, и это выглядит как «деплой не применился». Значение — голый origin (`https://novapoker.ru`), **без** `/api/v1`: префикс версии дописывает HTTP-клиент в `src/api/client.ts`, чтобы он жил в одном месте. Как проверить, что реально вшито в прод-бандл: скачать `/assets/index-*.js` через `curl --compressed` и найти в нём `/api/v1` — рядом стоит стрелочная функция базового адреса. ## Отдача статики `nginx.conf` кеширует `/assets/` навсегда (имена содержат хеш содержимого), отдаёт `index.html` с `no-cache` и заворачивает неизвестные пути в `index.html`: это SPA с клиентской маршрутизацией, прямой заход на `/organizers` обязан вернуть приложение, а не 404. ## Тесты ```bash pnpm test ``` Это `vitest run`. Сетевые запросы в тестах перехватывает msw. ## Что рядом - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — как сервис связан с остальными. === # Доменная модель Источник: https://docs.novapoker.ru/dev/domain-model/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/prisma/schema.prisma --- Схема — `repos/poker-api/apps/server/prisma/schema.prisma`: 30 моделей, 476 строк, 23 миграции на 2026-08-24. ## Люди и профили | Модель | Смысл | |---|---| | `User` | аккаунт: телефон (уникален), имя, ник, аватар, город, `adminRole?`, `status` (`active`/`banned`) | | `PlayerProfile` | 1:1 с `User`: `xp`, `level`, `skill`, `gamesPlayed`, `wins`, `banUntil?` | | `DealerProfile` | витрина дилера: фото, био, стаж, `gamesDealt` | | `GuestPlayer` | walk-in без аккаунта: телефон и имя, `claimedByUserId?` | | `OtpCode`, `RefreshToken` | материал аутентификации: хеши, TTL, счётчики | `PlayerProfile` держит производные величины (уровень, силу, число игр). Они пересчитываются, а не правятся поверх, — см. [рейтинг](https://docs.novapoker.ru/dev/rating.md). ## Клубы | Модель | Смысл | |---|---| | `Organizer` | клуб: имя, статус, `cancelDeadlineHours` (по умолчанию 3) | | `OrganizerMember` | членство `(organizerId, userId)` с ролью `owner \| td \| dealer` | | `DealerInvite` | приглашение дилера: телефон и одноразовый код с TTL | | `Location` | зал: имя, город, адрес, таймзона, координаты, описание, фото | | `BlindTemplate` | структура блайндов: `levels` (JSON), `startingStack`, `lateRegLevel` | У `BlindTemplate` значение `organizerId = null` означает **платформенный шаблон**, доступный всем клубам. ## Игра | Модель | Смысл | |---|---| | `Season` | квартал: `startsOn`/`endsOn`, статус `current \| archived` | | `Tournament` | организатор, зал, сезон, шаблон, тип (`mtt \| sng`), старт, вместимость, статус, призы, чат, баннер и серверные поля часов: `currentLevelIdx`, `levelStartedAt`, `pausedAt`, `chipsInPlay` | | `TournamentTranslation` | заголовок и подпись по локали (`ru`/`en`) | | `TournamentDealer` | назначение дилера на турнир | | `Registration` | запись игрока или гостя: статус, источник `app \| walk_in`, `eliminatedAt?` | | `TableSeat` | посадка: `(tournamentId, tableNo, seatNo)` уникальна, `active` гасится при вылете | | `Result` | итог: место, очки сезона, xp, сила до и после; `(tournamentId, place)` уникальна | | `NoShowStrike` | страйк за неявку или позднюю отмену, прощаемый | **Часы турнира живут на сервере, а не в клиенте.** `currentLevelIdx`, `levelStartedAt` и `pausedAt` в `Tournament` — источник истины; клиент считает оставшееся время сам, но от серверных значений. Уникальность `(tournamentId, place)` в `Result` — та причина, по которой перестановка мест делается перестановкой отметок между игроками, а не переписыванием номеров подряд. ## Платформа и контент | Модель | Смысл | |---|---| | `City` + `CityTranslation` | справочник городов (ru/en), `slug` — стабильный ключ сида | | `LevelTier` + `LevelTierTranslation` | тиры уровней: `minLevel`, цвет, картинка, названия | | `PromoSlide` + `PromoSlideTranslation` | слайдер главного экрана: картинка, цель (`tournament \| url`), окно показа, порядок | | `RatingConfigRow` | единственная строка с конфигом рейтинга и санкций | | `Follow`, `PushToken`, `Notification`, `AuditLog` | подписки, токены устройств, журнал уведомлений, аудит | ## Сид `prisma db seed`, идемпотентен. Кладёт: - платформенный шаблон блайндов «Стандарт 20 мин» — 13 отрезков, стек 20 000, перерыв 15 минут после шестого уровня; - 10 городов, от Москвы до Владивостока; - 6 тиров уровней: `fish` с уровня 1, `grinder` 3, `regular` 6, `shark` 10, `pro` 15, `legend` 25. ## Что рядом - [Жизненный цикл турнира](https://docs.novapoker.ru/product/tournament-lifecycle.md) — те же сущности в движении. - [Рейтинг: пакет формул](https://docs.novapoker.ru/dev/rating.md) — что пишется в `Result` и `PlayerProfile`. - [poker-api](https://docs.novapoker.ru/dev/poker-api.md) — как накатить миграции локально. === # Аутентификация и роли Источник: https://docs.novapoker.ru/dev/auth/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/auth, repos/poker-api/apps/server/src/auth/permissions.ts --- Одна схема во всех трёх фронтах: **телефон → одноразовый код → пара токенов**. ## Контракт | Метод и путь | Что делает | |---|---| | `POST /auth/otp/request` | выдаёт код, отвечает именем канала доставки | | `POST /auth/otp/verify` | проверяет код, отдаёт пару токенов | | `POST /auth/refresh` | обменивает refresh на новую пару, ротируя старый | | `GET /auth/me` | текущий пользователь | | `PATCH /me` | профиль игрока | | `GET`/`POST /admin/admins`, `PUT /admin/admins/{userId}/role` | управление админ-ролями | ## Одноразовый код Сервер генерирует шестизначный код и хранит **только SHA-256-хеш**, TTL — 5 минут. Лимит: **3 запроса на телефон за 10 минут**. Проверка и запись идут под `pg_advisory_xact_lock` по номеру, поэтому параллельные запросы лимит не обходят. На проверку — не больше **5 попыток на код**; успешная проверка гасит код (`consumedAt`). **Доставка — цепочка каналов** в порядке `OTP_CHANNELS`, по умолчанию `telegram,max,sms`. Канал без настроек в окружении возвращает `false`, и `OtpSender` идёт к следующему. Вне `production` в хвост цепочки добавляется `FakeOtpChannel` — поэтому dev и тесты работают без секретов. Печать кода в stdout включается только флагом `OTP_LOG_CODES` и в проде не работает никогда. Телефон из `PLATFORM_ADMIN_PHONES` автоматически получает `adminRole = super_admin` — в том числе если аккаунт уже существовал. Забаненный аккаунт получает `403 USER_BANNED`. ## Токены **Access — JWT на 15 минут**, внутри `sub` и флаг `adm`. **Refresh — на 30 дней**, это случайные 32 байта, в базе лежит хеш. `POST /auth/refresh` **ротирует** токен: старый помечается `revokedAt` guarded-апдейтом. Повторное использование уже потраченного refresh даёт `401 INVALID_REFRESH_TOKEN` — это не баг, а обнаружение переиспользования. Клиенты хранят токены в `SecureStore` (нативно) или `localStorage` (веб и админка). В HTTP-клиенте каждого фронта единый `api()`: подставляет `Authorization`, на `401` **один раз** делает refresh и повторяет запрос. Конкурентные вызовы разделяют один промис refresh — иначе десяток параллельных запросов сжёг бы токен гонкой. Неудачный refresh чистит хранилище. ## Клейм гостя При создании аккаунта все `GuestPlayer` с тем же телефоном привязываются к нему, их `Result` переезжают на нового пользователя, и профиль пересчитывается по хронологии через `ProfileRebuildService`. История walk-in-игр не теряется. ## Два контура прав **Платформенный `adminRole`** — матрица в `apps/server/src/auth/permissions.ts`: | Право | super_admin | moderator | support | marketing | |---|:--:|:--:|:--:|:--:| | `organizers.manage` | ✓ | | | | | `seasons.manage` | ✓ | | | | | `rating_config.manage` | ✓ | | | | | `moderation` | ✓ | ✓ | | | | `push.send` | ✓ | ✓ | | ✓ | | `metrics.view` | ✓ | ✓ | ✓ | ✓ | | `admins.manage` | ✓ | | | | | `cities.manage` | ✓ | | | | | `content.manage` | ✓ | | | ✓ | | `tournaments.manage` | ✓ | | | | **Роль внутри клуба** — `owner | td | dealer` в `OrganizerMember`. Проверка `OrganizersService.requireRole` стоит на всех операциях клуба; день игры и live ведут `owner` и `td`. **Источник истины — серверная проверка.** Интерфейс лишь прячет недоступное, и полагаться на это как на защиту нельзя. ## Что рядом - [Аккаунт и вход](https://docs.novapoker.ru/product/account.md) — то же словами продукта. - [Запись: контракт и переходы](https://docs.novapoker.ru/dev/registration.md) — где эти роли применяются. === # Формат ошибок и загрузка файлов Источник: https://docs.novapoker.ru/dev/api-conventions/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/common, repos/poker-api/apps/server/src/uploads --- Два сквозных механизма, которые встречаются в каждом разделе. ## Формат ошибок Все ошибки API приходят одной формой: ```json { "error": { "code": "SNAKE_CASE", "message": "..." } } ``` **Коды стабильны, и на них завязаны фронты** — по коду они выбирают текст и поведение, а не по `message`. Менять код — ломающее изменение, даже если сообщение осталось прежним. Что бывает сейчас: | Область | Коды | |---|---| | Общее | `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND` | | Вход | `INVALID_OTP`, `OTP_RATE_LIMITED`, `INVALID_REFRESH_TOKEN`, `USER_BANNED` | | Запись | `REGISTRATION_CLOSED`, `REGISTRATION_BANNED`, `ALREADY_REGISTERED`, `INVALID_STATUS` | | Live | `NOT_ENOUGH_PLAYERS`, `TOURNAMENT_ALREADY_STARTED`, `TOURNAMENT_NOT_RUNNING`, `TOURNAMENT_NOT_FINISHED`, `ALREADY_ELIMINATED`, `NOTHING_TO_UNDO`, `SEAT_TAKEN`, `INVALID_LEVEL`, `INVALID_LEVELS` | | Результаты | `RESULTS_ALREADY_CONFIRMED` | ## Загрузка картинок `POST /uploads`, multipart, поле `file`. Кладёт файл в S3-совместимое хранилище и возвращает `{ url, key }`. **Presigned URL не используются — файл идёт через сервер.** Это осознанный выбор: так проверка прав и ограничений остаётся в одном месте. Доступ даёт `UploadAccessGuard`. Загрузить может: - платформенный админ с правом `content.manage` **или** `tournaments.manage`; - **либо** любой `owner`/`td` какого-нибудь клуба. Ограничения — белый список MIME-типов и лимит размера, оба в `storage.ts`. ## Что рядом - [Аутентификация и роли](https://docs.novapoker.ru/dev/auth.md) — откуда берутся права. - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — где лежит контракт целиком. === # Запись — контракт и переходы Источник: https://docs.novapoker.ru/dev/registration/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/registrations/registrations.controller.ts, repos/poker-api/apps/server/src/registrations/registrations.service.ts --- Модуль `registrations` в `poker-api`: запись, отмена, чек-ин, walk-in, страйки. Все эндпоинты — за `JwtAuthGuard`. **Где код:** `repos/poker-api/apps/server/src/registrations/`. ## Контракт | Метод и путь | Кто может | Что делает | |---|---|---| | `POST /tournaments/{id}/register` | игрок за себя | запись; отдаёт `{ status: 'registered' \| 'waitlist' }` | | `POST /tournaments/{id}/cancel-registration` | игрок за себя | отмена; после дедлайна с занятого места — страйк | | `POST /tournaments/{id}/check-in` | `owner`, `td` | отметить пришедшего | | `POST /tournaments/{id}/cancel-check-in` | `owner`, `td` | снять отметку | | `POST /tournaments/{id}/walk-in` | `owner`, `td` | посадить по телефону и имени | | `GET /tournaments/{id}/registrations` | `owner`, `td` | список с именем, ником, аватаром и телефоном | | `GET /tournaments/{id}/strikes` | `owner`, `td` | страйки турнира | | `POST /strikes/{id}/forgive` | `owner`, `td` | простить страйк | Пути даны без префикса `/api/v1`. ## Коды ошибок при записи | Код | Когда | |---|---| | `409 REGISTRATION_CLOSED` | статус турнира не `registration` | | `403 REGISTRATION_BANNED` | у профиля игрока `banUntil` в будущем | | `409 ALREADY_REGISTERED` | живая запись уже есть | | `404 NOT_FOUND` | турнира или записи нет | | `409 INVALID_STATUS` | операция не подходит текущему статусу записи | ## Как работает внутри **Место и запись — одна транзакция под блокировкой строки турнира.** `SELECT … FOR UPDATE` по `tournaments` сериализует конкурентные записи, поэтому подсчёт занятых мест точен и `capacity` не переполняется. Занятыми считаются `registered` и `checked_in`. ```ts await tx.$queryRaw`SELECT id FROM tournaments WHERE id = ${tournamentId}::uuid FOR UPDATE` const takenSeats = await tx.registration.count({ where: { tournamentId, status: { in: ['registered', 'checked_in'] } }, }) const seat = takenSeats < tournament.capacity ? 'registered' : 'waitlist' ``` **Повторная запись после отмены переиспользует ту же строку** и заново отсчитывает позицию в очереди: `createdAt = now`. Уникальный индекс по `(tournamentId, userId)` не даёт завести вторую строку; гонка на создании ловится по `P2002` и превращается в `409 ALREADY_REGISTERED`. **Освободившееся место сразу поднимает первого из листа ожидания** guarded-write'ом, и пуш `waitlist.promoted` уходит только тем, кто реально поднялся, — не всей очереди. **Отмена чек-ина смотрит на `seatedFromWaitlist`.** Если человека сажали из очереди, он возвращается в `waitlist`, иначе в `registered`. Без этого флага человек из листа ожидания получил бы при старте неявку и страйк за турнир, куда его не звали. **Страйки и бан — под блокировкой строки профиля.** `strikesToBan` (2) непрощённых страйков за `strikeWindowDays` (30) дают `banUntil = now + banDays` (14). Прощение пересчитывает бан и снимает его, если активных страйков стало меньше порога. ## Ограничения и грабли - **Walk-in намеренно не проверяет `capacity`.** Это решение ТД, а не баг; но значит, число записей может превысить заявленное число мест. - **Walk-in идемпотентен:** повтор и гонка возвращают ту же строку. Если телефон принадлежит существующему пользователю, запись привязывается к нему, иначе заводится `GuestPlayer` — он клеймится при первом входе владельца телефона. - **`lateRegLevel` не работает.** Поле в шаблоне блайндов есть, но нигде не проверяется: walk-in доступен на любом уровне, пока турнир `running`. - **Пуш подписчикам `followee.registered` — best-effort.** Сбой отправки не роняет запись, и это осознанно. - **Отмена турнира гасит живые регистрации без страйков.** ## Что рядом - [Запись и лист ожидания](https://docs.novapoker.ru/product/registration.md) — те же правила словами продукта. - [День игры: чек-ин, walk-in, санкции](https://docs.novapoker.ru/dev/game-day.md) — что происходит дальше. - [poker-api](https://docs.novapoker.ru/dev/poker-api.md) — как запустить сервис локально. === # День игры — контракт и фон Источник: https://docs.novapoker.ru/dev/game-day/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/registrations/registrations.service.ts, docs/superpowers/specs/2026-08-22-as-built-system-spec.md --- День игры трогает три вещи: эндпоинты стойки регистрации, фоновые напоминания и операцию старта, которая фиксирует состав. ## Эндпоинты стойки | Метод и путь | Кто может | |---|---| | `POST /tournaments/{id}/check-in` | `owner`, `td` | | `POST /tournaments/{id}/cancel-check-in` | `owner`, `td` | | `POST /tournaments/{id}/walk-in` | `owner`, `td` | | `GET /tournaments/{id}/registrations` | `owner`, `td` | Чек-ин возможен только из статуса `registered` и только пока турнир в `registration`. Полный контракт модуля — в статье [Запись: контракт и переходы](https://docs.novapoker.ru/dev/registration.md). ## Walk-in Принимает телефон и имя. Логика поиска человека: 1. телефон принадлежит существующему пользователю → запись привязывается к нему; 2. иначе заводится `GuestPlayer`, который клеймится при первом входе владельца телефона. Операция **идемпотентна**: повтор и гонка возвращают ту же строку. Доступна пока турнир `registration` **или** `running` — то есть это же и поздняя дорегистрация. `capacity` намеренно не проверяется. ## Фоновые напоминания pg-boss на том же PostgreSQL, без Redis; стартует по DI-токену `BOSS`, в тестах подменён `FakeBoss`. | Джоба | Расписание | Что делает | |---|---|---| | `tournament-reminders` | `*/5 * * * *` | пуш за 24 ч до старта и пуш в момент дедлайна отмены | | `season-rollover` | `5 0 * * *` UTC | архивирует истёкший сезон, заводит текущий квартал | **Окно поиска равно периоду запуска** — пять минут. Дедупликация идёт по журналу `Notification`: ключ складывается из `kind`, `payload.tournamentId` и `payload.window`, поэтому повторный тик или ручной перезапуск джобы не задваивает напоминание. Гости без аккаунта пропускаются — им некуда слать. Дедлайн отмены у каждого организатора свой: `organizer.cancelDeadlineHours`, по умолчанию 3. ## Что делает старт турнира Требует минимум двух записей в `checked_in`. В одной операции: - рассадка — случайно и равномерно, `SEATS_PER_TABLE = 9`, разброс между столами не больше одного игрока; - всем, кто остался в `registered`, ставится `no_show` и выдаётся страйк; - запускается серверный таймер блайндов с первого уровня. Отсюда важное следствие для отладки: **после старта состав уже зафиксирован**, и «потерянный» игрок — это почти всегда несделанный чек-ин, а не сбой рассадки. ## Реалтайм Живое состояние турнира отдаёт `GET /tournaments/{id}/live`, подписка — namespace `/live` через socket.io. Гейт `/live` требует JWT на handshake: приложение организатора передаёт `auth: { token }` при подключении. Молчащий сокет чаще всего означает отсутствующий или протухший токен, а не потерянное соединение. ## Что рядом - [День игры и чек-ин](https://docs.novapoker.ru/product/game-day.md) — те же правила словами продукта. - [Запись: контракт и переходы](https://docs.novapoker.ru/dev/registration.md) — статусы и коды ошибок. - [poker-api](https://docs.novapoker.ru/dev/poker-api.md) — как запустить сервис локально. === # Live-цикл и реалтайм Источник: https://docs.novapoker.ru/dev/live/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/live, repos/poker-api/apps/server/src/live/seating.ts, repos/poker-api/apps/server/src/live/balance.ts --- Модуль `live` в `poker-api`. Все операции ведут `owner` и `td`. Особенность раздела в том, что почти каждая операция защищена от конкурентного нажатия — пульт стоит в зале, и по нему бьют пальцем, а не курсором. ## Старт Требует минимум двух `checked_in`, иначе `409 NOT_ENOUGH_PLAYERS`. В одной операции: - оставшиеся `registered` → `no_show` плюс страйк; - рассадка; - `chipsInPlay = startingStack × число вошедших`; - часы с нулевого отрезка. Переход в `running` — **атомарный guarded-update**; повторный старт получает `409 TOURNAMENT_ALREADY_STARTED`. ## Рассадка `src/live/seating.ts`, `SEATS_PER_TABLE = 9`. Число столов — `⌈игроков / 9⌉`, перемешивание Фишера–Йейтса с **инъектируемым `rng`** (поэтому рассадка тестируема), раздача по кругу: ``` tableNo = (i mod столов) + 1 seatNo = ⌊i / столов⌋ + 1 ``` Отсюда разница размеров столов не больше одного и места подряд с первого. ## Часы `pause`, `resume`, `level` (`next`/`prev`). Все три — **compare-and-swap**: конкурентные нажатия не задваивают аудит, не шлют лишние broadcast'ы и не теряют уровень. Смена уровня на паузе начинает новый отрезок «в момент паузы». Источник истины — серверные поля `Tournament`: `currentLevelIdx`, `levelStartedAt`, `pausedAt`. Клиент считает остаток сам, но от этих значений. ## Вылеты `POST /eliminations` гасит место (`TableSeat.active = false`) и штампует `eliminatedAt` **строго возрастающими** метками. **Место = число активных игроков до вылета**: первый вылетевший из N получает место N. Рядом три операции: | Операция | Что делает | |---|---| | `undo` | отменяет последний вылет; если исходное место занято, сажает на первое свободное | | `reorder` | перераспределяет **уже снятые** метки времени между игроками, новых не создаёт | | `reseat` | ручная пересадка | `reorder` именно перераспределяет, а не переписывает номера: `(tournamentId, place)` в `Result` уникальна, и набор мест обязан остаться прежним. ## Балансировка `src/live/balance.ts`. Подсказка считается после каждого вылета и **приходит в ответе на вылет**, а не отдельным запросом: - разница между самым большим и самым маленьким живым столом ≥ 2 → одна пересадка: игрок с максимальным `seatNo` большого стола на первое свободное место маленького (при равенстве размеров выигрывает стол с меньшим номером); - активных ≤ 9 → `finalTable`, пакет пересадок на самый большой стол; - иначе `null`. Под капотом кнопки «Пересадить» — те же вызовы `reseat`. Сервер сам никого не двигает. ## Подтверждение результатов Разрешено только для `running` и когда активных игроков осталось не больше одного. `N` (размер поля) = число чек-инувшихся; **неявки в поле не входят**. Порядок мест: не выбывший — место 1, дальше по убыванию времени вылета, тай-брейки по `createdAt` и `id`. Гейт атомарный: конкурентный второй вызов получает `409 RESULTS_ALREADY_CONFIRMED` **до** начисления, а не после. ## Пересчёт `POST /admin/tournaments/{id}/recount`, только для `finished`. Удаляет и пересоздаёт `Result`, **сохраняя исходные `createdAt`**, учитывает уже заклеймленных гостей и пересобирает профили всех задетых игроков по хронологии. Сохранение `createdAt` здесь не косметика: сила считается экспоненциальным сглаживанием, то есть зависит от порядка турниров. Пересчёт не последнего турнира без этого сдвинул бы его в конец истории и дал бы дрейф силы. ## LiveState Что получают клиенты: статус, часы (индекс, старт отрезка, пауза, полный список уровней), `remaining`, `entrants`, `chipsInPlay`, `avgStack`, столы с местами и именами, список вылетов с местами. **Телефоны наружу не отдаются.** Подписка — namespace `/live` через socket.io, полный `LiveState` рассылается в комнату турнира после каждого изменения. Handshake требует JWT. ## Что рядом - [Старт, рассадка и баланс](https://docs.novapoker.ru/product/start-and-seating.md) — то же словами продукта. - [Рейтинг: пакет формул](https://docs.novapoker.ru/dev/rating.md) — что считается при подтверждении. - [Результаты и аудит](https://docs.novapoker.ru/dev/results.md) — что пишется на финише. === # Результаты и аудит Источник: https://docs.novapoker.ru/dev/results/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/live/results.service.ts, repos/poker-api/packages/rating/src/index.ts --- `src/live/results.service.ts` плюс пакет `packages/rating`. Всё начисление — **одна транзакция**, а статус `running → finished` служит гейтом от второго нажатия. ## Порядок мест Не выбывший — место 1, дальше по убыванию времени вылета. Тай-брейки по `createdAt` и `id`, чтобы порядок был детерминирован даже при совпадении меток. `N` — число чек-инувшихся. **Неявки в размер поля не входят**, и это заметно влияет на очки: формула зависит от `√N`. ## Что пишется | Куда | Что | |---|---| | `Result` | место, очки сезона, xp, `skillBefore` и `skillAfter` | | `PlayerProfile` | `xp`, пересчитанный `level`, новая `skill`, счётчики игр и побед | | `AuditLog` | запись о подтверждении | | пуш | `tournament.results` каждому участнику с аккаунтом | | WS `/live` | рассылка нового состояния | Формулы — в [пакете rating](https://docs.novapoker.ru/dev/rating.md), здесь только момент их применения. ## Гости У записи гостя есть `guestPlayerId` и нет профиля. Такой участник получает **только строку `Result`** — начислять некуда. Рейтинг подтянется при клейме аккаунта, см. [аутентификацию](https://docs.novapoker.ru/dev/auth.md). ## Защита от второго нажатия Гейт подтверждения атомарный: конкурентный второй вызов получает `409 RESULTS_ALREADY_CONFIRMED` **до** начисления. Это важнее, чем кажется: без такого порядка два нажатия в зале дали бы двойные очки. ## Пересчёт `POST /admin/tournaments/{id}/recount`, только для `finished`, только у платформы. Переиспользует то же начисление, но: - удаляет и пересоздаёт `Result`, **сохраняя исходные `createdAt`**; - учитывает уже заклеймленных гостей; - полностью пересобирает профили всех задетых игроков по хронологии. Сохранение `createdAt` обязательно потому, что сила — экспоненциальное сглаживание и зависит от порядка турниров. Пересчёт не последнего турнира без этого сдвинул бы его в конец истории игрока и дал бы дрейф силы. По той же причине профиль **пересобирается**, а не правится дельтой. ## Аудит В `AuditLog` пишется каждая операция ТД: старт, вылет, отмена вылета, пересадка, подтверждение результатов. Это же источник для ленты событий турнира. ## Что рядом - [Результаты и итоги вечера](https://docs.novapoker.ru/product/results.md) — то же словами продукта. - [Live-цикл и реалтайм](https://docs.novapoker.ru/dev/live.md) — что происходит до подтверждения. - [Рейтинг: пакет формул](https://docs.novapoker.ru/dev/rating.md) — сами формулы. === # Рейтинг — пакет формул и начисление Источник: https://docs.novapoker.ru/dev/rating/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/packages/rating/src/index.ts, repos/poker-api/apps/server/openapi.json --- Все рейтинговые формулы живут **только** в пакете `repos/poker-api/packages/rating` — чистые функции без обращений к базе, сети и времени. Это не стилистика: по этим числам игроки сравнивают себя друг с другом, поэтому формулы обязаны быть проверяемы отдельно от всего остального. Пакет покрыт тестами на 100%, и это условие, а не достижение. Правило простое: **если формула появилась вне этого пакета — это ошибка**. ## Конфигурация Одна структура `RatingConfig` со значениями по умолчанию: | Поле | Значение | Что задаёт | |---|---|---| | `pointsBase` | 10 | множитель формулы очков | | `minPoints` | 1 | минимум очков за участие | | `xpLevelBase` | 50 | база порога уровня | | `xpLevelExponent` | 1.5 | показатель порога уровня | | `skillAlpha` | 0.1 | сглаживание силы | | `skillInitial` | 0.4444 | стартовая сила, даёт отображаемые 5.0 | | `strikesToBan` | 2 | страйков до блокировки записи | | `strikeWindowDays` | 30 | окно подсчёта страйков | | `banDays` | 14 | длительность блокировки | Значения меняет админ платформы через `GET`/`PUT /admin/rating-config`. Обратите внимание: пороги страйков и бана лежат **здесь же**, а не в модуле записи — это одна настроечная поверхность. ## Формулы **Очки сезона.** `round(pointsBase * sqrt(N) * (N - place + 1) / N)`, не меньше `minPoints`. Здесь `N` — число реально игравших, `place` — от 1 до `N`. При `N <= 0` или месте вне диапазона бросается `RangeError`. **Перформанс.** `(N - place) / (N - 1)`; при `N === 1` возвращает 1. **Новая сила.** `prev + skillAlpha * (perf - prev)`, зажатая в `[0, 1]`. Это экспоненциальное сглаживание: один турнир смещает силу на десятую часть разрыва между текущим значением и результатом. **Отображаемая сила.** `1 + 9 * skill`, округление до 0.1. Отсюда стартовые `0.4444 → 5.0`. **Порог уровня.** `reachXp(L) = round(xpLevelBase * (L - 1) ^ xpLevelExponent)`, причём `reachXp(1) === 0`. Уровень по XP — максимальный `L`, для которого `reachXp(L) <= xp`; цикл ограничен `MAX_LEVEL = 1000` как защита от зацикливания на аномальном значении. XP начисляется один в один с очками сезона. ## Эндпоинты | Метод и путь | Что отдаёт | |---|---| | `POST /tournaments/{id}/confirm-results` | подтверждение результатов — момент начисления | | `GET /tournaments/{id}/results` | итоги турнира | | `GET /players/{userId}/results` | результаты игрока | | `GET /leaderboards/season` | сезонный лидерборд | | `GET /leaderboards/season/summary` | топ-3 и строка игрока | | `GET /leaderboards/level` | уровневый, топ-100 по XP | | `GET /seasons/current`, `GET /seasons` | сезоны | | `GET`/`PUT /admin/rating-config` | настройки платформы | Пути даны без префикса `/api/v1`. ## Сезоны Сезон — календарный квартал. `ensureCurrentSeason()` идемпотентен: гонка ловится по уникальному `startsOn`. Вызывается и джобой `season-rollover` (`5 0 * * *` UTC), и лидербордами — то есть первый же запрос после полуночи заведёт квартал, даже если джоба не отработала. ## Ограничения - **Начисление происходит только при подтверждении результатов** и необратимо для клуба. Пересчёта задним числом нет — исправление делает платформа вручную. - **Нокауты не учитываются.** В рейтинге есть только места и победы. - **Уровневый лидерборд ограничен топ-100**, сезонный отдаётся целиком. ## Что рядом - [Рейтинг, уровни и сезоны](https://docs.novapoker.ru/product/rating.md) — те же формулы словами продукта. - [poker-api](https://docs.novapoker.ru/dev/poker-api.md) — как запустить сервис и прогнать тесты. === # Фоновые задачи и уведомления Источник: https://docs.novapoker.ru/dev/notifications/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/notifications, docs/superpowers/specs/2026-08-22-as-built-system-spec.md --- ## Уведомления Единственная точка входа — `NotificationsService.notify(userIds, kind, payload, push?)`. Она **всегда** пишет строки `Notification` (это журнал), и, если передан `push`, шлёт на все токены адресатов и штампует `sentAt`. Журнал пишется даже когда пуш не ушёл — иначе нельзя было бы ни показать ленту, ни дедуплицировать повторы. | Метод и путь | Что делает | |---|---| | `POST /me/push-tokens` | регистрирует токен устройства | | `DELETE /me/push-tokens/{token}` | снимает токен | | `GET /me/notifications` | лента, последние 50 | Регистрация токена — **upsert по самому токену**, а не по паре с пользователем: устройство может сменить владельца, и тогда старая привязка должна уйти. Виды, которые система шлёт сейчас: `tournament.reminder`, `waitlist.promoted`, `followee.registered`, `tournament.results`, `admin_custom`. ## Фоновые задачи pg-boss на **том же PostgreSQL, без Redis**. Стартует по DI-токену `BOSS`, в тестах подменён `FakeBoss` — поэтому в тестах нет ни очереди, ни таймеров. | Джоба | Расписание | Что делает | |---|---|---| | `tournament-reminders` | `*/5 * * * *` | пуш за 24 ч до старта и пуш в момент дедлайна отмены | | `season-rollover` | `5 0 * * *` UTC | архивирует истёкший сезон, заводит текущий квартал | **Окно поиска равно периоду запуска** — пять минут. **Дедупликация — по журналу `Notification`.** Ключ складывается из `kind`, `payload.tournamentId` и `payload.window`, поэтому повторный тик, ручной перезапуск джобы или наложение двух воркеров не задваивают напоминание. Гости без аккаунта пропускаются. Дедлайн отмены берётся у организатора: `cancelDeadlineHours`, по умолчанию 3. ## Побочный эффект, о котором стоит знать Второе напоминание привязано **к дедлайну отмены**, а не к «за три часа до старта». Если клуб поставит дедлайн в 24 часа, оба напоминания придут почти одновременно. ## Рассылки `admin_custom` шлётся вручную из админки с выбором аудитории: все, участники турнира, город, поимённый список. В ответе — сколько адресатов и скольким реально ушёл пуш; расхождение нормально, у части людей нет активных токенов. Отправка требует права `push.send` — оно есть у `super_admin`, `moderator` и `marketing`. ## Что рядом - [Уведомления](https://docs.novapoker.ru/product/notifications.md) — то же словами продукта. - [День игры — контракт и фон](https://docs.novapoker.ru/dev/game-day.md) — где напоминания встречаются с записью. - [Аутентификация и роли](https://docs.novapoker.ru/dev/auth.md) — матрица прав. === # Окружения, стенды и деплой Источник: https://docs.novapoker.ru/dev/environments/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/README.md, repos/poker-admin/Dockerfile, repos/poker-player/Dockerfile --- ## Переменные бэкенда Полная таблица — в README `poker-api`; здесь то, без чего не поднимается или ведёт себя неожиданно. | Переменная | Зачем | |---|---| | `DATABASE_URL` | подключение к PostgreSQL | | `JWT_SECRET` | подпись access-токенов; в production обязателен сильный, иначе старт падает | | `PLATFORM_ADMIN_PHONES` | телефоны, получающие роль супер-админа при входе | | `PORT` | порт HTTP и WS — они на одном | | `OTP_CHANNELS` | порядок каналов доставки кода, по умолчанию `telegram,max,sms` | | `CORS_ORIGINS` | **без него кросс-доменный доступ выключен** — самая частая причина «фронт не видит API» | | `OTP_LOG_CODES` | печать кода в stdout, только для разработки | Плюс секреты каналов доставки и настройки S3-хранилища. Значений здесь нет и быть не может: сайт публичный. ## Локальный стенд Четыре процесса. Конфигурация — в `.claude/launch.json` корневого репозитория. | Что | Команда | Порт | |---|---|---| | PostgreSQL | `docker compose up -d` в `poker-api` | 5433 | | API | `pnpm --filter server dev` | 3001 на стенде, 3000 по умолчанию | | Игрок | `expo start --web --port 8090` | 8090 | | Организатор | `expo start --web --port 8092` | 8092 | | Админка | `vite dev --port 5199` | 5199 | Две грабли, на которые наступают: - **Сервер не читает `.env` из корня** — файл кладётся в `apps/server/`. - **Коды OTP по умолчанию не видны.** Чтобы войти локально, нужны `OTP_CHANNELS=fake` и `OTP_LOG_CODES=1`, иначе код уходит в реальный канал. Хранится он только хешем, подсмотреть в базе нельзя. PostgreSQL слушает **5433**, а не 5432 — чтобы не спорить с локальной базой. ## Стейджинг | Адрес | Что | |---|---| | `novapoker.ru` | API с префиксом `/api/v1`, здоровье — `/api/v1/health`; в корне веб-сборка игрока | | `admin.novapoker.ru` | админка | | `docs.novapoker.ru` | эта документация | Разделение сервисов на одном хосте делает Dokploy **по префиксу пути**: в карточке домена задаётся поле Path, и Traefik предпочитает более длинное правило. Поэтому `/api/v1` выигрывает у `/`. ## Деплой У всех сервисов один паттерн: multi-stage `Dockerfile`, `nginx:alpine` в рантайме для фронтов, Dokploy тянет образ и вешает домен с letsencrypt. Проверка живости — `/healthz` у фронтов, `/api/v1/health` у API. ### Правило, которое стоит всех остальных **Переменные, которые сборщик вшивает в бандл, задаются в Build Args, а не в Env.** | Сервис | Переменная | Где задавать | |---|---|---| | `poker-admin` | `VITE_API_URL` | Build Args | | `poker-player` | `EXPO_PUBLIC_API_URL`, `EXPO_PUBLIC_SHARE_BASE_URL` | Build Args | | `poker-organizer` | `EXPO_PUBLIC_API_URL` | Build Args | Vite и Metro подставляют значения прямо в бандл. Смена переменной окружения без пересборки образа **не меняет ничего** — и выглядит это как «деплой не применился». Значение адреса API везде — голый origin, **без** `/api/v1`: префикс дописывает HTTP-клиент. Проверить, что реально вшито в живой бандл: скачать `/assets/index-*.js` через `curl --compressed` и найти в нём `/api/v1` — рядом стоит функция базового адреса. ## Что рядом - [Карточки сервисов](https://docs.novapoker.ru/dev/poker-api.md) — запуск и сборка каждого по отдельности. - [Тесты и CI](https://docs.novapoker.ru/dev/testing.md) — что проверяется до деплоя. === # Тесты и CI Источник: https://docs.novapoker.ru/dev/testing/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/test, repos/poker-admin/test, repos/poker-player/test --- ## Что где | Репозиторий | Инструменты | Тест-файлов | Что делает CI | |---|---|---|---| | `poker-api` | Jest + supertest, unit для `packages/rating` | 51 | `prisma generate` → сборка → тайпчек → тесты, PostgreSQL 16 сервисом | | `poker-admin` | Vitest + Testing Library + msw | 20 | тесты → `tsc -b && vite build` | | `poker-organizer` | jest-expo + Testing Library + msw | 24 | `tsc --noEmit` → тесты | | `poker-player` | jest-expo + Testing Library + msw | 50 | `tsc --noEmit` → тесты | CI — GitHub Actions, во всех четырёх репозиториях, на `push` в `main` и на pull request. Число файлов посчитано 2026-08-24; число кейсов здесь не приводится, чтобы не устаревало молча. ## Серверные тесты E2E-стиль, **по файлу на модуль**, против реальной базы. Особенности, без которых они ведут себя загадочно: - **`maxWorkers: 1`.** Тесты идут последовательно. - **База пересоздаётся в `globalSetup`**, своя на прогон. Общая база между параллельными прогонами — источник флейков, которые ловятся долго. - **Сети в тестах нет.** Внешние интеграции подменены фейками: `FakeOtpChannel`, `FakePushSender`, `FakeBoss`. - **Рассадка тестируема**, потому что `rng` в неё инъектируется. ## Фронтовые тесты Сеть перехватывает msw, и **незамоканный запрос валит тест** — это осознанно: молча ушедший в никуда запрос хуже красного теста. Две вещи, на которых теряют время: - **В Expo-приложениях нужен `--forceExit`.** Он уже прописан в скрипте `test`: без него открытые хендлы не дают процессу завершиться, и прогон висит до таймаута. - **Фильтр `-t` с кириллицей молча не фильтрует.** Запуск вида `pnpm test -- -t "название"` с русским текстом не отберёт ничего и не пожалуется — прогонит всё либо ничего. Фильтровать надёжнее по имени файла. ## Что покрыто строже всего `packages/rating` — 100% покрытия, и это условие, а не достижение: по этим формулам считаются очки игроков. Подробнее — в [статье про рейтинг](https://docs.novapoker.ru/dev/rating.md). ## Что рядом - [Окружения, стенды и деплой](https://docs.novapoker.ru/dev/environments.md) — что происходит после зелёного CI. - [Карточки сервисов](https://docs.novapoker.ru/dev/poker-api.md) — команды запуска тестов по репозиториям. === # Клубы — контракт и оргконтекст Источник: https://docs.novapoker.ru/dev/organizers/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/organizers, repos/poker-api/apps/server/openapi.json, repos/poker-organizer/src --- Клуб в коде — `Organizer`. Всё, что делает клуб, проходит проверку `OrganizersService.requireRole`, поэтому раздел стоит читать вместе с [аутентификацией и ролями](https://docs.novapoker.ru/dev/auth.md). ## Оргконтекст на фронте Поверх обычной авторизации приложение организатора держит **оргконтекст**: хук `useOrg` тянет членства из `GET /organizers/mine`. - одно членство — выбирается автоматически; - несколько — показывается экран `select-org`, клуб можно сменить в любой момент. Роль внутри клуба определяет и набор вкладок, и доступные действия. Вкладка «Клуб» существует только у владельца, у дилера «Турниры» называется «Назначения». ## Контракт | Метод и путь | Кто может | |---|---| | `GET /organizers/mine` | любой вошедший — свои членства | | `GET`/`PATCH /organizers/{id}` | `owner` | | `GET`/`POST /organizers/{id}/members` | `owner` | | `DELETE /organizers/{id}/members/{memberId}` | `owner` | | `GET`/`POST /organizers/{id}/dealer-invites` | `owner` | | `POST /dealer-invites/claim` | приглашённый дилер | | `POST /organizers/{id}/locations`, `GET`/`PATCH /locations/{id}`, `GET /locations` | `owner`; каталог залов публичный | | `GET`/`POST /organizers/{id}/blind-templates` | `owner` | | `POST /organizers/{id}/tournaments` | `owner`, `td` | | `POST /tournaments/{id}/dealers`, `PATCH /tournaments/{id}/dealers/{dealerUserId}` | `owner` | | `GET /me/dealer-assignments` | дилер — свои назначения | | `GET /dealers`, `GET /dealers/{userId}`, `PATCH /dealers/me` | каталог публичный, правка своя | Пути даны без префикса `/api/v1`. Заведение самого клуба — операция платформы: `POST /admin/organizers`, право `organizers.manage`. ## Приглашение дилера `DealerInvite` — телефон плюс одноразовый код с TTL. Особенность, которая удивляет: **платформа этот код не доставляет**. Клуб получает его в ответе и показывает на экране, дальше передаёт человеку сам. Код срабатывает **только с того номера**, на который выписан: `claim` сверяет телефон вошедшего с телефоном приглашения. Канал доставки одноразовых кодов у платформы есть — тот же, что для входа, — но приглашение через него не идёт. Это осознанное состояние, а не забытая ветка. ## Залы и структуры блайндов `Location` — зал: имя, город из справочника, адрес, таймзона, координаты, описание, фото. Каталог залов читается публично, правит владелец. `BlindTemplate` — уровни в JSON, стартовый стек, `lateRegLevel`. Значение `organizerId = null` означает **платформенный шаблон**, доступный всем клубам; сид кладёт один такой — «Стандарт 20 мин». **`lateRegLevel` нигде не проверяется.** Поле хранится и отдаётся, но ни одна ветка кода на него не смотрит: walk-in доступен на любом уровне, пока турнир `running`. ## Назначение дилера на стол `POST /tournaments/{id}/dealers` создаёт строку в `tournament_dealers`, `PATCH` уточняет стол. Дилер читает свои назначения через `GET /me/dealer-assignments`. Живое рабочее место дилера **не потребовало ни одной серверной строки**: оно собрано из уже существующего `LiveState` — `clock` с уровнями и `tables` с местами. Стоит помнить при отладке: `GET /tournaments/{id}/live` отдаётся **без гарда**, а сокет `/live` требует JWT на handshake, но роль при входе в комнату не проверяет. ## Что рядом - [Клуб — залы, блайнды, настройки](https://docs.novapoker.ru/product/club.md) — то же словами продукта. - [Дилеры на турнире](https://docs.novapoker.ru/product/dealers.md) — что видит дилер. - [Live-цикл и реалтайм](https://docs.novapoker.ru/dev/live.md) — состав `LiveState`. - [poker-organizer](https://docs.novapoker.ru/dev/poker-organizer.md) — как запустить приложение. === # Админка — права, модерация, метрики Источник: https://docs.novapoker.ru/dev/admin/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-admin/src, repos/poker-api/apps/server/src/admin, repos/poker-api/apps/server/src/auth/permissions.ts --- Вход тот же, что везде — телефон и код. Дальше `AdminGate`: аккаунт **без** `adminRole` видит экран «Нет доступа» и кнопку «Выйти». Первый супер-админ бутстрапится из `PLATFORM_ADMIN_PHONES` на бэкенде и дальше раздаёт роли остальным. Подробности гейта — в [аутентификации и ролях](https://docs.novapoker.ru/dev/auth.md). ## Разделы и права Навигация фильтруется дважды: компонентом `RequirePermission` и фильтром в `Layout`. Источник истины всё равно серверный — интерфейс лишь прячет недоступное. | Раздел | Право | |---|---| | Дашборд | `metrics.view` | | Организаторы | `organizers.manage` | | Турниры | `content.manage` | | Игроки | `moderation` | | Рейтинг | `rating_config.manage` | | Сезоны | `seasons.manage` | | Города | `cities.manage` | | Слайдеры | `content.manage` | | Тиры | `content.manage` | | Админы | `admins.manage` | | Рассылка | `push.send` | Матрица «роль → права» — в `apps/server/src/auth/permissions.ts`, она же приведена в [статье про аутентификацию](https://docs.novapoker.ru/dev/auth.md). ## Модерация | Метод и путь | Что делает | |---|---| | `GET /admin/users` | поиск | | `PATCH /admin/users/{id}` | бан и разбан guarded-апдейтом | | `POST /admin/users/{id}/merge` | слияние дублей | **Слияние идёт в одной интерактивной транзакции:** регистрации дубля переезжают на основной аккаунт — кроме турниров, где у основного уже есть своя запись. Иначе нарушился бы уникальный индекс `(tournamentId, userId)`. ## Метрики `GET /admin/metrics` считает: - новых игроков за 30 дней; - активных игроков за 30 дней; - retention — долю сыгравших два турнира и больше; - завершённые турниры за 30 дней; - разбивку по 8 неделям. **Недели считаются с понедельника, в UTC.** Это стоит помнить, сверяя цифры с календарём клуба в другом часовом поясе. ## Рассылка `POST /admin/push` с таргетом `all | tournament | city | users`; для `users` принимаются идентификаторы и телефоны. Возвращает `{ targeted, pushed }` и пишет аудит. Два числа расходятся, когда у части адресатов нет активных токенов устройств. ## Управление турнирами со стороны платформы Платформа может завести турнир **за любой клуб**: `POST /admin/organizers/{id}/tournaments`. Ей же доступен пересчёт результатов — `POST /admin/tournaments/{id}/recount`, описанный в [результатах и аудите](https://docs.novapoker.ru/dev/results.md). ## Аудит `AuditLog` пишется на действия live (старт, пауза, смена уровня, вылеты, подтверждение), на рассылки и на админские операции. ## Что рядом - [Что ведёт платформа](https://docs.novapoker.ru/product/platform.md) — то же словами продукта. - [poker-admin](https://docs.novapoker.ru/dev/poker-admin.md) — как запустить админку. - [Рейтинг: пакет формул](https://docs.novapoker.ru/dev/rating.md) — что меняет раздел «Рейтинг». === # Как поддерживать документацию Источник: https://docs.novapoker.ru/dev/how-to-maintain/ Дорожка: разработка Сверено с кодом: Mon Aug 24 Код: repos/poker-docs/scripts/agent-bundle.mjs, docs/superpowers/specs/2026-08-24-docs-site-design.md --- Сайт собирается из markdown в репозитории `poker-docs`. Правка — обычный коммит. ## Как добавить статью Положите файл в `src/content/docs/product/` или `src/content/docs/dev/` — дорожка определяется директорией. Фронтматтер: ```yaml --- title: Запись на турнир description: Одна строка о том, что читатель узнает track: product # product или dev verified: 2026-08-24 # дата сверки с кодом sources: # файлы, по которым сверяли - repos/poker-api/apps/server/src/registrations sidebar: order: 20 --- ``` `sidebar.order` задаёт порядок **и в боковом меню, и в `llms.txt`** — это один источник, второго списка нигде нет. Шаблон продуктовой статьи: зачем → что видит пользователь → правила и пограничные случаи → статусы → что рядом. Шаблон инженерной: что это и где код → контракт → как работает внутри → ограничения и грабли → что рядом. Раздел «что рядом» обязателен. Он заменяет дублирование: продуктовая статья кончается ссылкой на инженерную и наоборот. ## Что значит «сверено» `verified` — это день, когда статью **читали рядом с кодом**, а не день, когда правили текст. Опечатку можно исправить не открывая репозиторий, и это не делает статью верной. Поэтому: поправили формулировку — дату не трогаем. Перечитали код и убедились, что написанное всё ещё правда — ставим сегодняшнюю. Все даты видны на [карте документации](https://docs.novapoker.ru/map.md). ## Правила, которые проверяются автоматически **Ссылки только корневые-абсолютные:** `/product/rating/`, не `../rating/` и не `rating.md`. Относительная ссылка бесполезна для агента, скачавшего один файл, а битая — заметна тесту раньше, чем читателю. **Секретов быть не может.** Сайт публичный, поэтому грепа по паттернам (`postgres://`, `_SECRET`, `-----BEGIN` и подобные) **роняет сборку**, а не предупреждает. Адреса стендов секретами не считаются: секрет — это то, чем можно воспользоваться. Если статья обязана назвать паттерн дословно — как эта, — исключение заводится в `content-allowlist.json`: страница, список **точных строк текста** и обязательное поле `why`. Разрешается именно строка целиком, а не паттерн: добавьте на той же странице новую строку с настоящей строкой подключения — она не совпадёт с разрешённой и уронит сборку. Отредактировали разрешённую строку — разрешение перестало действовать, и её нужно пересмотреть заново. Это осознанно. **Фронтматтер валидируется схемой** — неверный тип поля тоже роняет сборку. Прогнать всё локально: ```bash pnpm build && pnpm test ``` ## Что нельзя делать **Копировать спеки из `docs/superpowers/specs/` целиком.** Спека — датированный снимок замысла под конкретную задачу; после реализации она устаревает, но продолжает выглядеть как истина. Статья описывает сегодняшнее поведение и сверяется с кодом. Спека — сырьё, не источник. ## Честное ограничение Связать правку кода с правкой статьи автоматически нельзя: приложения живут в разных репозиториях, общего pull request нет. Единственная защита — поле `verified` и привычка заглядывать в карту. Изображать здесь процесс, которого нет, было бы хуже, чем признать дыру. ## Что рядом - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — с чего начать чтение. - [Карта документации](https://docs.novapoker.ru/map.md) — что давно не сверяли. === # Продукт и действующие лица Источник: https://docs.novapoker.ru/product/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-product-spec-as-built.md --- NovaPoker — платформа **офлайн-турниров по спортивному покеру**. Турниры проводят клубы в своих залах; платформа даёт им инструмент проведения и приводит игроков, которые видят единую афишу и растут в **одном общем рейтинге** — не клубном, а платформенном. ## Три свойства, определяющие продукт **Мультиклубность.** Клубов много, они независимы, но игрок ходит по всем, и рейтинг у него один. Это отличает платформу от системы учёта отдельного клуба. **Спорт, а не игра на деньги.** Денежного контура нет вообще: ни взносов, ни призовых, ни платежей внутри продукта. Призы организатор описывает текстом в карточке турнира, и они остаются вне платформы. Это продуктовое решение, а не недоделка: в данных поле взноса существует, всегда равно нулю и не читается ни одной строкой кода. **Живой турнир онлайн.** Пока турнир идёт, таймер, столы и вылеты видят все — и турнирный директор за пультом, и игрок в зале, и тот, кто следит со стороны. ## Три приложения под три работы | Приложение | Для кого | Главная работа | |---|---|---| | Игрок | посетители турниров | найти турнир, записаться, играть, расти в рейтинге | | Организатор | клубы | завести турнир и провести день игры от чек-ина до результатов | | Админка | команда платформы | вести клубы, контент, справочники, модерацию и правила рейтинга | ## Действующие лица | Роль | Где живёт | Что может | |---|---|---| | Игрок | приложение игрока | афиша, запись, live, рейтинг, профиль, подписки на других игроков | | Владелец клуба | приложение организатора | всё, что ТД, плюс залы, структуры блайндов, сотрудники, приглашения дилеров, настройки клуба | | Турнирный директор (ТД) | приложение организатора | создать турнир, провести день игры, вести live-пульт, подтвердить результаты | | Дилер | приложение организатора | видеть свои назначения, вести витринный профиль | | Гость | нигде | человек, посаженный за стол по телефону без аккаунта; его история ждёт его в системе | | Супер-админ | админка | всё | | Модератор | админка | модерация игроков, метрики, рассылки | | Поддержка | админка | только метрики | | Маркетинг | админка | рассылки, метрики, контент — слайдеры, тиры, оформление турниров | **Роли в клубе и роли на платформе — разные контуры.** Один человек может быть игроком, дилером в одном клубе и ТД в другом одновременно: аккаунт один, роли разные. Путать эти два набора — самая частая ошибка при обсуждении прав. ## Чего продукт сознательно не делает Это границы текущего среза, а не список задач: - **Никаких денег** — ни взносов, ни призового фонда, ни выплат, ни истории платежей. - **Нет ре-энтри, ребаев и аддонов.** Вылет окончателен; отменить можно только ошибочно отмеченный последний вылет. - **Нет учёта нокаутов.** Кто кого выбил — не фиксируется, и в рейтинге этого измерения нет. - **Нет ограничения поздней регистрации.** Уровень late reg задаётся в структуре блайндов, но продукт его не применяет: посадить можно на любом уровне, пока турнир идёт. - **Нет фазы «анонсирован».** Турнир создаётся сразу открытым на запись, отложить открытие записи нельзя. - **Игрок не видит свою позицию в листе ожидания** — только сам факт, что он в нём. - **Нет ленты новостей и чатов внутри продукта.** Чат турнира — внешняя ссылка. - **Нет отзывов и рейтингов залов и дилеров** — каталоги справочные. - **Нет английского интерфейса** и нет опубликованных мобильных сборок. ## Если вы развиваете продукт Три статьи, с которых стоит начать — они отвечают не «как работает», а «что с этим делать»: - [Что можно менять без разработки](https://docs.novapoker.ru/product/levers.md) — полный список рычагов: что крутится в интерфейсе, а что потребует кода. - [Принятые решения и почему](https://docs.novapoker.ru/product/decisions.md) — что выглядит недоделкой, но является решением, и по какой причине. - [Открытые вопросы](https://docs.novapoker.ru/product/open-questions.md) — где поведение расходится с ожиданием и нужно решение. ## Что рядом - [Инженерный обзор](https://docs.novapoker.ru/dev.md) — те же четыре сервиса, но со стороны устройства. - [Карта документации](https://docs.novapoker.ru/map.md) — все статьи и дата последней сверки с кодом. === # Что можно менять без разработки Источник: https://docs.novapoker.ru/product/levers/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/packages/rating/src/index.ts, repos/poker-admin/src, repos/poker-organizer/app --- Первый вопрос при любой продуктовой идее — нужен ли для неё код. Здесь полный список того, что уже крутится руками. ## Рычаги платформы Раздел «Рейтинг» в админке. Меняется на ходу и **сразу действует на новые начисления**; уже посчитанные результаты не пересчитываются. | Рычаг | Значение сейчас | На что влияет | |---|---|---| | Множитель очков | 10 | вся шкала очков сезона | | Минимум очков | 1 | сколько даёт последнее место | | База порога уровня | 50 | как быстро растут уровни | | Показатель порога уровня | 1.5 | насколько каждый следующий уровень дороже | | Сглаживание силы | 0.1 | как сильно один турнир двигает силу | | Страйков до блокировки | 2 | строгость санкций | | Окно подсчёта страйков | 30 дней | за какой период они складываются | | Длительность блокировки | 14 дней | насколько закрывается запись | Остальное на платформе: - **Города** — справочник двумя языками, активность и порядок. - **Тиры уровней** — названия, пороги, цвета, картинки. - **Промо-слайды** — картинка, цель (турнир или ссылка), окно показа, порядок. - **Оформление любого турнира** — заголовок, подпись, баннер. - **Сезоны** — заводятся сами по кварталам, видны в админке. ## Рычаги клуба - **Дедлайн отмены записи** — по умолчанию 3 часа. Двойного назначения: после него отмена даёт страйк, и в этот же момент уходит второе напоминание. - **Структуры блайндов** — уровни, длительность, перерывы, стартовый стек. - **Залы** — адрес, описание, фото, таймзона. - **Состав турнира** — вместимость, тип, время старта, призы текстом, ссылка на чат. ## Что потребует разработки Это не «нельзя», а «стоит закладывать срок»: | Хотелка | Почему нужен код | |---|---| | Взносы, призовой фонд, любые деньги | денежного контура нет вообще — ни модели, ни экранов | | Ре-энтри, ребай, аддон | вылет считается окончательным по всей цепочке подсчёта мест | | Учёт нокаутов | кто кого выбил, нигде не хранится | | Ограничение поздней регистрации | поле есть, но ни одна ветка на него не смотрит | | Анонс турнира без открытой записи | статус заведён, но продукт его не выставляет | | Самостоятельный чек-ин игроком | чек-ин умеет только ТД | | Выбор числа столов | считается формулой от числа игроков | | Отзывы о залах и дилерах | каталоги справочные | | Английский интерфейс | переведена часть строк, автоопределение выключено | ## Что рядом - [Принятые решения и почему](https://docs.novapoker.ru/product/decisions.md) — что из этого решено намеренно. - [Открытые вопросы](https://docs.novapoker.ru/product/open-questions.md) — что ждёт продуктового решения. - [Рейтинг, уровни и сезоны](https://docs.novapoker.ru/product/rating.md) — как считаются очки. === # Принятые решения и почему Источник: https://docs.novapoker.ru/product/decisions/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-product-spec-as-built.md, docs/superpowers/specs/2026-08-24-player-stack-decision.md, docs/superpowers/specs/2026-08-24-club-settings-decision.md --- Список того, что выглядит как недоделка, но является решением. Каждое стоило обсуждения, и без причины рядом его переспорят заново. ## Денег в продукте нет Ни взносов, ни призового фонда, ни выплат, ни истории платежей. Призы — свободный текст в карточке турнира, дальше они живут вне платформы. **Почему:** платформа про спортивный покер, а не про игру на деньги. В данных поле взноса существует со значением ноль и не читается ни одной строкой кода — это след, а не заготовка. **Что это значит для развития:** любая денежная идея — новый контур целиком, а не «включить поле». ## Вылет окончателен Ре-энтри, ребаев и аддонов нет. Отменить можно только ошибочно отмеченный последний вылет. **Почему:** место в итоговой таблице вычисляется из числа живых игроков на момент вылета. Возврат игрока в игру ломает уже розданные места и всё, что от них считается. ## Нокауты не учитываются Кто кого выбил, не фиксируется. В рейтинге есть только места и победы. **Почему:** рейтинг сознательно считается из двух величин — размера поля и занятого места. Чем меньше входов в формулу, тем меньше поводов для спора о справедливости. ## Walk-in не проверяет вместимость Посадить человека сверх заявленного лимита можно, и система не возражает. **Почему:** решение принимает турнирный директор на месте, у него больше контекста, чем у системы. **Цена:** турнир может уйти за заявленное число мест, а человек в листе ожидания этого не поймёт — он видит только факт, что стоит в очереди. ## Подтверждение результатов необратимо для клуба Исправить итог может только платформа, запуском пересчёта. **Почему:** сила игрока — сглаженная величина, зависящая от порядка турниров. Подправить одно число «на месте» нельзя: разъедется всё, что считалось после. Пересчёт пересобирает профили по всей истории, сохраняя исходные даты результатов. ## Тёмная тема у игрока — единственная Светлой темы в приложении игрока не будет. **Почему:** визуальный язык «Полночь» построен на одном акценте — розовой вывеске на мокром асфальте. В светлой теме этот акцент теряет смысл, и пришлось бы вести два языка вместо одного. Админка при этом светлая: другая поверхность и другая работа. ## Индивидуальный стек игрока не показываем **Почему:** сервер знает только общее число фишек в игре — стартовый стек, умноженный на число вошедших. Стек конкретного человека взять неоткуда, и показывать его пришлось бы с чужих слов. ## Настройки клуба — это только дедлайн отмены Раздел обещал «Рейтинг, санкции, Telegram», но ни одной из трёх вещей на уровне клуба нет. **Почему:** рейтинг и санкции — правила платформы, общие для всех клубов. Иначе общий рейтинг перестал бы быть общим. ## Автоопределение языка выключено Код написан, но не включён. **Почему:** интерфейс переведён не целиком. Включить автоопределение — значит показать человеку наполовину английский экран. ## Что рядом - [Открытые вопросы](https://docs.novapoker.ru/product/open-questions.md) — что ещё не решено. - [Что можно менять без разработки](https://docs.novapoker.ru/product/levers.md) — где рычаги. - [Продукт и действующие лица](https://docs.novapoker.ru/product.md) — границы продукта целиком. === # Открытые вопросы Источник: https://docs.novapoker.ru/product/open-questions/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/live/live.controller.ts, repos/poker-api/apps/server/src/registrations/registrations.service.ts, docs/as-is/2026-08-22-player-organizer-journeys.md --- Не задачи и не баги — места, где поведение расходится с ожиданием и нужно продуктовое решение. Статус каждого пункта проверен по коду **24 августа 2026**; дальше он может разойтись, сверяйтесь заново. Отличие от [принятых решений](https://docs.novapoker.ru/product/decisions.md) простое: там причина известна и записана, здесь — нет. ## Поздний walk-in остаётся без места за столом Посадить человека после старта можно, и запись создаётся. Но **место за столом ему никто не выделяет**: он не появляется в столах, не входит в число живых, не учитывается в фишках, и выбить его нельзя. Эндпоинта «посадить опоздавшего за стол» нет. **Что решать:** либо walk-in при идущем турнире сажает за стол по тем же правилам, что балансировка, либо позднюю дорегистрацию закрывают совсем. ## Живое состояние турнира читает кто угодно Снимок по адресу турнира отдаётся **без авторизации** — поимённая рассадка идущего турнира доступна любому, кто знает идентификатор. Подписка по сокету при этом требует токена. Сейчас на это опираются намеренно: неавторизованный гость видит карточку идущего турнира. Вопрос в том, осознанная ли это цена. **Что решать:** либо признать состав турнира публичным и записать это решением, либо закрыть эндпоинт и отдельно решить, что показывать гостю. ## Ссылка «поделиться» открывается в браузере, а не в приложении Страница турнира по короткой ссылке работает: отдаёт карточку с заголовком и превью для мессенджеров. Но **файлы верификации универсальных ссылок недоступны** — ни андроидный, ни эппловский. Без них система не связывает домен с приложением, и тап по ссылке остаётся в браузере. **Что решать:** это техническая недоделка, а не развилка, но продукту стоит знать, что обещание «ссылка открывает приложение» сегодня не выполняется. ## Второе напоминание привязано к дедлайну отмены Оно приходит не «за три часа до старта», а в момент дедлайна отмены. Клуб, поставивший дедлайн в 24 часа, получит оба напоминания почти одновременно — за сутки до игры. **Что решать:** развести две сущности — момент, когда закрывается безнаказанная отмена, и момент, когда полезно напомнить. ## Страйк одинаков за неявку и за позднюю отмену Человек, отменившийся за два часа, и человек, просто не пришедший, получают одну и ту же метку. **Что решать:** нужна ли разница в весе. Сейчас продукт говорит, что предупредить и не предупредить — одно и то же. ## Уровень поздней регистрации не работает Поле в структуре блайндов есть, заполняется, отдаётся наружу — и **ни одна ветка кода на него не смотрит**. Посадить опоздавшего можно на любом уровне. **Что решать:** либо применять, либо убрать поле, чтобы оно не обещало лишнего. ## Статуса «анонсирован» не существует на практике Турнир создаётся сразу открытым на запись. Статус для анонса заведён в данных, но продукт его не выставляет. **Что решать:** нужна ли фаза «объявили, запись позже» — например, для крупных турниров, которые хочется анонсировать заранее. ## Число столов считает система Столов ровно столько, сколько нужно по формуле от числа игроков. Турнирный директор их не выбирает. **Цена:** клубу с шестью физическими столами система может предложить восемь. **Что решать:** давать ли ТД задавать доступное число столов. ## Тип турнира ни на что не влияет МТТ и SNG различаются только подписью на карточке. Никакой разницы в поведении нет — SNG получается сам собой, когда игроков девять или меньше. **Что решать:** либо наполнить тип смыслом, либо убрать выбор. ## Чек-ин делает только турнирный директор Самостоятельного чек-ина у игрока нет. На большом поле стойка становится узким местом заранее предсказуемо. **Что решать:** давать ли игроку отмечаться самому — например, по коду или геопозиции в зале. ## Код приглашения дилера платформа не доставляет Клуб получает код на экране и передаёт его человеку сам, хотя канал доставки одноразовых кодов у платформы есть — тот же, что для входа. **Что решать:** доставлять ли приглашение тем же каналом. ## Что рядом - [Принятые решения и почему](https://docs.novapoker.ru/product/decisions.md) — то, что уже обсудили. - [Что можно менять без разработки](https://docs.novapoker.ru/product/levers.md) — что можно попробовать без кода. - [Запись и лист ожидания](https://docs.novapoker.ru/product/registration.md), [День игры](https://docs.novapoker.ru/product/game-day.md) — правила, которых касаются вопросы. === # Аккаунт и вход Источник: https://docs.novapoker.ru/product/account/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/auth, docs/superpowers/specs/2026-08-22-product-spec-as-built.md --- **Аккаунт один на человека и общий для всех трёх приложений.** Ключ — номер телефона. Пароля нет вообще. ## Как выглядит вход 1. Человек вводит телефон и получает одноразовый код. 2. Код уходит по цепочке каналов: Telegram → MAX → SMS. Доставку забирает первый доступный, и в ответе видно, какой именно сработал. 3. Код живёт **5 минут**. Запросить код можно **3 раза за 10 минут**, ввести — **5 попыток на код**. ## Обязательная анкета после первого входа Пока не заполнены имя, ник, город из справочника и не дано согласие на обработку персональных данных, приложение игрока дальше этого экрана не пускает. Это не онбординг, который можно пропустить, — без ника и города приложение не работает. ## Куда пускает вход | Приложение | Кого пускает | |---|---| | Игрок | любого вошедшего | | Организатор | только у кого есть членство хотя бы в одном клубе | | Админка | только с назначенной админ-ролью; остальные видят «Нет доступа» | **Первый супер-админ** заводится настройкой платформы — списком телефонов. Дальше роли раздаёт он. **Несколько клубов у одного человека.** Одно членство — приложение организатора выбирает клуб молча. Несколько — показывает экран выбора, и клуб можно сменить в любой момент. ## Гость становится игроком сам Если человека посадили за стол walk-in'ом по телефону, а он потом завёл аккаунт с тем же номером — прошлые результаты сами приезжают в новый профиль, и рейтинг пересчитывается по всей истории. Склеивать руками ничего не нужно, и это стоит помнить, обсуждая «потерянные» результаты гостей: они не теряются, они ждут. ## Роли: два независимых контура **Роль на платформе** — супер-админ, модератор, поддержка, маркетинг. Живёт в админке. **Роль в клубе** — владелец, турнирный директор, дилер. Живёт в приложении организатора. Это разные наборы, и они не пересекаются. Один человек может быть игроком, дилером в одном клубе и ТД в другом одновременно: аккаунт один, роли разные. ## Бан Забаненный платформой аккаунт не входит вообще. Это отдельная история от [временной блокировки записи за неявки](https://docs.novapoker.ru/product/registration.md): там аккаунт работает, недоступна только кнопка записи. ## Что рядом - [Аутентификация и роли](https://docs.novapoker.ru/dev/auth.md) — токены, лимиты и матрица прав. - [Запись и лист ожидания](https://docs.novapoker.ru/product/registration.md) — что человек делает дальше. === # Жизненный цикл турнира Источник: https://docs.novapoker.ru/product/tournament-lifecycle/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-product-spec-as-built.md, repos/poker-api/apps/server/prisma/schema.prisma --- Сквозной сценарий: кто что делает и что при этом видит игрок. Отдельные шаги расписаны подробнее в своих статьях — здесь важна связность. ## Шаг 1. Турнир заводят Создаёт **ТД или владелец клуба**: зал → структура блайндов → тип (МТТ или SNG) → дата и время старта → вместимость. Опционально — призы текстом и ссылка на чат турнира. Отдельно задаётся оформление: заголовок, подпись и баннер, двумя языками. Турнир **сразу открыт на запись.** Промежуточной фазы «анонсирован, но запись закрыта» в продукте нет, отложить открытие записи нельзя. Платформенный админ может завести турнир за любой клуб сам, выбрав организатора. ## Шаг 2. Игроки записываются Турнир появляется в афише и в поиске по городу. На карточке видно заполнение (например `12/24`), участников и назначенных дилеров. Подписчикам игрока приходит пуш «твой игрок записался», и турнир появляется у них в ленте подписок. Подробно: [запись и лист ожидания](https://docs.novapoker.ru/product/registration.md). ## Шаг 3. Напоминания Два автоматических пуша: за 24 часа до старта и в момент дедлайна отмены. Подробно: [день игры и чек-ин](https://docs.novapoker.ru/product/game-day.md). ## Шаг 4. День игры ТД за стойкой отмечает пришедших, сажает walk-in'ов, при необходимости прощает страйки. ## Шаг 5. Старт Нужно минимум двое с чек-ином. Система рассаживает, отмечает неявки и запускает часы. Подробно: [старт, рассадка и баланс](https://docs.novapoker.ru/product/start-and-seating.md). ## Шаг 6. Игра У ТД пульт с часами и столами, у игрока — тот же турнир в реальном времени. Подробно: [live-экран](https://docs.novapoker.ru/product/start-and-seating.md). ## Шаг 7. Результаты Когда остаётся один игрок, в пульте появляется «К результатам». ТД видит итоговый порядок мест, **может поправить порядок стрелками** и жмёт «Подтвердить результаты» — с предупреждением, что начислятся очки и XP и изменить это будет нельзя. После подтверждения турнир становится завершённым, участникам с аккаунтом начисляется рейтинг, каждому уходит пуш с его местом и очками, результат появляется в истории профиля и в лидербордах. Подробно: [результаты и итоги вечера](https://docs.novapoker.ru/product/results.md). ## Шаг 8. Если что-то пошло не так **Турнир отменили** — клуб или платформа. Все записи гасятся, страйки при этом **никому не выдаются**: виноват не игрок. **Результаты подтвердили с ошибкой.** Исправить может только платформа: админ запускает пересчёт турнира, и профили задетых игроков пересобираются по всей их истории, а не правятся поверх. ## Статусы турнира | Статус | Что значит | |---|---| | `registration` | открыт на запись — состояние сразу после создания | | `running` | идёт: часы запущены, состав зафиксирован | | `finished` | результаты подтверждены, рейтинг начислен | | `cancelled` | отменён, записи погашены без страйков | | `announced` | **не используется**: заведён в данных, но продукт его не выставляет | ## Что рядом - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — те же сущности со стороны кода. - [Доменная модель](https://docs.novapoker.ru/dev/domain-model.md) — как турнир и его окружение лежат в базе. === # Запись и лист ожидания Источник: https://docs.novapoker.ru/product/registration/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/registrations/registrations.service.ts, docs/superpowers/specs/2026-08-22-product-spec-as-built.md --- Запись — это точка, где игрок берёт на себя обязательство прийти. Отсюда растут и лист ожидания, и страйки: место конечно, и занятое зря место кого-то не пустило. ## Как это выглядит для игрока Игрок открывает карточку турнира и нажимает «Записаться». Дальше возможны два исхода, и выбирает не он: - **мест хватает** — он в основном составе; - **мест нет** — он в листе ожидания. Лист ожидания — честная очередь по времени записи. Освободилось место — поднимается первый в очереди, и ему приходит пуш «ты прошёл из листа ожидания». **Свою позицию в очереди игрок не видит** — только сам факт, что он в ней. Отменился и записался снова — встал в конец очереди заново. Это не наказание, а следствие того, что очередь считается по времени записи, а отмена его сбрасывает. ## Правила **Записаться можно один раз и только пока турнир открыт на запись.** Мест ровно столько, сколько задал организатор: переполнения основного состава не бывает даже при одновременных попытках — решение о месте принимается под блокировкой турнира. **Отмена до дедлайна — без последствий.** Дедлайн задаёт клуб, по умолчанию это 3 часа до старта. **Отмена после дедлайна с занятого места — страйк.** С места в листе ожидания — нет: человек ничего не занимал. **Не пришёл и не отменился — неявка и тоже страйк.** Он выставляется в момент старта турнира. **Отмена самого турнира гасит все живые записи без страйков.** Виноват не игрок. ## Страйки и блокировка Страйк — метка за то, что игрок занял место и не сыграл. Организатор может **простить** страйк: предупредил заранее, уважительная причина. Прощённый страйк перестаёт считаться. **Два непрощённых страйка за 30 дней закрывают запись на 14 дней.** Это не бан аккаунта: приложение работает, рейтинг на месте, недоступна только кнопка записи. Все три числа — настраиваемые правила платформы, их меняет админ в разделе «Рейтинг». Прощение страйка пересчитывает блокировку и снимает её, если активных страйков стало меньше порога. ## Walk-in и опоздавшие Организатор может посадить человека прямо у входа — по телефону и имени, даже без аккаунта. Walk-in работает и пока идёт запись, и **пока турнир уже идёт**: это же и поздняя дорегистрация. Две особенности, которые стоит знать продукту: - **Вместимость при walk-in не проверяется.** Это намеренно: сажать сверх лимита решает ТД на месте. Следствие — турнир может уйти за заявленное число мест, а человек в листе ожидания этого не поймёт. - **Ограничение по уровню поздней регистрации не применяется.** Поле в структуре блайндов есть, эффекта у него нет: посадить можно на любом уровне, пока турнир идёт. ## Статусы записи | Статус | Что значит | |---|---| | `registered` | место в основном составе | | `waitlist` | в очереди, места пока нет | | `checked_in` | пришёл, отмечен организатором | | `no_show` | не пришёл; выставляется при старте, даёт страйк | | `cancelled` | запись отменена | Переходы: запись даёт `registered` или `waitlist`; из `waitlist` поднимает в `registered` освободившееся место; чек-ин переводит в `checked_in`; старт турнира переводит неявившихся в `no_show`. **Отмена чек-ина возвращает человека туда, откуда его сажали.** Если он попал за стол из листа ожидания — вернётся в `waitlist`, а не в `registered`. Иначе при старте он получил бы неявку и страйк за турнир, куда его не звали. ## Что рядом - [Запись: контракт и переходы](https://docs.novapoker.ru/dev/registration.md) — та же механика со стороны кода. - [День игры и чек-ин](https://docs.novapoker.ru/product/game-day.md) — что происходит дальше. - [Продукт и действующие лица](https://docs.novapoker.ru/product.md) — кто есть кто. === # День игры и чек-ин Источник: https://docs.novapoker.ru/product/game-day/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-product-spec-as-built.md, repos/poker-api/apps/server/src/registrations/registrations.service.ts --- День игры — единственный момент, когда список записавшихся превращается в реальный состав за столами. Всё, что не отмечено здесь, при старте станет неявкой. ## Напоминания приходят до того, как игрок придёт Система шлёт записавшимся два пуша: - **за 24 часа до старта**; - **в момент дедлайна отмены** — у каждого клуба он свой, по умолчанию 3 часа до старта. Каждое напоминание приходит ровно один раз, повторы не задваиваются. Гостям без аккаунта напоминания не уходят — им некуда. Побочный эффект, о котором стоит помнить: **второе напоминание привязано к дедлайну отмены, а не к «за три часа»**. Если клуб поставит дедлайн в 24 часа, оба напоминания придут почти одновременно. ## Что делает организатор за стойкой **Чек-ин** — отмечает пришедших из списка записавшихся. Отметить можно только того, кто в статусе «записан». **Walk-in** — сажает пришедших без записи, по телефону и имени. Если телефон принадлежит зарегистрированному игроку, запись привяжется к его аккаунту; если нет — заведётся гость, и его история будет ждать человека в системе до первого входа. **Страйки** — тут же виден список страйков игроков этого турнира, и любой можно простить. ## Что зафиксировано в момент старта ТД жмёт «Старт турнира». Нужно **минимум двое с чек-ином**. В этот момент система: - рассаживает игроков случайно и равномерно — 9 мест за столом, столы отличаются не больше чем на одного игрока; - **записывает всем, кто не пришёл, неявку и выдаёт страйк**; - запускает часы с первого уровня и считает фишки в игре. До этой секунды состав ещё можно поправить. После — нет. ## Пограничные случаи **Walk-in работает и после старта.** Пока турнир идёт, посадить человека можно — это же и поздняя дорегистрация. Ограничение по уровню поздней регистрации продукт не применяет: решение всегда за ТД. **Вместимость при walk-in не проверяется.** Турнир может уйти за заявленное число мест, и человек в листе ожидания этого не поймёт. **Отмена чек-ина возвращает человека в тот статус, из которого его сажали** — в лист ожидания, если он попал за стол оттуда. Иначе он получил бы неявку и страйк за турнир, куда его не звали. ## Что рядом - [День игры: контракт](https://docs.novapoker.ru/dev/game-day.md) — эндпоинты и фоновые задачи. - [Запись и лист ожидания](https://docs.novapoker.ru/product/registration.md) — как игрок сюда попал. === # Старт, рассадка и баланс Источник: https://docs.novapoker.ru/product/start-and-seating/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/live/seating.ts, repos/poker-api/apps/server/src/live/balance.ts, docs/as-is/2026-08-22-player-organizer-journeys.md --- Старт — граница, после которой состав уже не меняется, а вечер начинает идти сам. ## Что делает кнопка «Старт турнира» Нужно **минимум двое с чек-ином**. В один момент система: - рассаживает игроков случайно и равномерно; - переводит всех неявившихся в неявку и выдаёт им страйк; - считает фишки в игре — стартовый стек, умноженный на число вошедших; - запускает часы с первого уровня. Повторное нажатие ничего не задваивает: турнир уже идёт. ## Как считается рассадка За столом **девять мест**. Число столов система считает сама — ТД его не задаёт. Игроки перемешиваются и раздаются по столам по кругу, поэтому **размеры столов отличаются не больше чем на одного человека**, а места занимаются подряд с первого. Из этого следует то, что иногда удивляет: при 10 игроках будет два стола по пять, а не стол на девять и стол на одного. ## Пульт турнирного директора - **Часы** — пауза, продолжить, следующий и предыдущий уровень. Смена уровня во время паузы начинает новый отрезок с момента паузы. - **Сводка** — в игре, вошло, средний стек. - **Столы** — места с именами. ## Вылеты Вылет отмечается кнопкой у места. **Место в итоговой таблице определяется автоматически: оно равно числу живых игроков до этого вылета.** То есть первый вылетевший из 18 получает 18-е место, следующий 17-е и так далее. Ошибку можно отменить — «Отменить последний вылет» вернёт игрока за стол. Если его прежнее место успели занять, он сядет на первое свободное. Порядок мест можно и **переставить**: система перераспределяет уже снятые отметки времени между игроками, а не выдумывает новые. Поэтому набор мест остаётся тем же, меняется только то, кому какое досталось. ## Балансировка подсказывает сама После каждого вылета система смотрит на столы и предлагает ход: - **разница между самым большим и самым маленьким столом два игрока и больше** — предлагает одну конкретную пересадку; - **живых осталось девять или меньше** — предлагает собрать финальный стол одним пакетом. ТД применяет подсказку одной кнопкой или откладывает её. Решение всегда за человеком: система не пересаживает никого сама. ## Что видит игрок Тот же турнир в реальном времени: таймер уровня, столы, список вылетевших с местами. Экран обновляется сам, без перезагрузки. Телефоны участников наружу не отдаются — в живом состоянии только имена. ## Что рядом - [Live-цикл и реалтайм](https://docs.novapoker.ru/dev/live.md) — эндпоинты, гонки и формат живого состояния. - [Результаты и итоги вечера](https://docs.novapoker.ru/product/results.md) — что происходит, когда остался один. - [День игры и чек-ин](https://docs.novapoker.ru/product/game-day.md) — что было до старта. === # Результаты и итоги вечера Источник: https://docs.novapoker.ru/product/results/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/live/results.service.ts, docs/superpowers/specs/2026-08-22-product-spec-as-built.md --- Подтверждение результатов — единственное действие в продукте, которое клуб не может отменить. Поэтому вокруг него больше всего правил. ## Как это выглядит Когда остаётся один игрок, в пульте появляется «К результатам». ТД видит итоговый порядок мест и **может поправить его стрелками** — на случай, если вылеты отмечали второпях. Дальше «Подтвердить результаты», с явным предупреждением: начислятся очки и XP, и изменить это будет нельзя. ## Что происходит после подтверждения - турнир становится завершённым; - всем участникам с аккаунтом начисляются очки сезона и XP, пересчитываются уровень и сила, победителю плюсуется победа; - каждому уходит пуш с его местом и очками; - результат появляется в истории профиля и в лидербордах. ## Размер поля — это вошедшие Очки зависят от того, сколько людей играло. **Считаются чек-инувшиеся; неявки в поле не входят.** Турнир, на который записались 30, а пришли 12, — это турнир на 12 человек, и очки за победу в нём соответствующие. ## Гости получают место, но не рейтинг Человек, посаженный walk-in'ом без аккаунта, попадает в протокол и занимает своё место. Рейтинг ему начислять некуда — профиля нет. Но результат не пропадает: [когда гость заведёт аккаунт](https://docs.novapoker.ru/product/account.md) с тем же телефоном, его прошлые игры приедут в новый профиль, и рейтинг пересчитается по всей истории. ## Если подтвердили с ошибкой Исправить может **только платформа**. Админ запускает пересчёт турнира, и профили задетых игроков пересобираются по всей их истории, а не правятся поверх. Причина такой строгости в том, что сила игрока — сглаженная величина и зависит от порядка турниров. Подправить одно число «на месте» нельзя: разъедется всё, что считалось после. ## Что рядом - [Рейтинг, уровни и сезоны](https://docs.novapoker.ru/product/rating.md) — что именно начисляется. - [Результаты и аудит](https://docs.novapoker.ru/dev/results.md) — как это устроено внутри. - [Старт, рассадка и баланс](https://docs.novapoker.ru/product/start-and-seating.md) — что было до финиша. === # Рейтинг, уровни и сезоны Источник: https://docs.novapoker.ru/product/rating/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/packages/rating/src/index.ts, docs/superpowers/specs/2026-08-22-product-spec-as-built.md --- Единая система прогресса, общая для всех клубов. Считается ровно из двух вещей: **сколько людей играло и какое место занял игрок**. Ни взносов, ни нокаутов, ни бай-инов в формуле нет — их в продукте вообще не существует. ## Очки сезона Очки растут и от места, и от размера поля: победа в турнире на 40 человек ценнее победы на 10. Последнее место всё равно даёт минимум одно очко — участие не бесплатно. Пример для турнира на 18 человек при текущих настройках платформы: | Место | Очки | |---|---| | 1 | 42 | | 2 | 40 | | 9 | 24 | | 18 | 2 | Очки сезона обнуляются на границе квартала. ## Опыт и уровни **XP начисляется один в один с очками сезона.** Разница в том, что XP не обнуляется никогда и копится всю жизнь аккаунта. Пороги уровней растут нелинейно — каждый следующий уровень дороже предыдущего: | Уровень | Нужно XP | Тир | |---|---|---| | 1 | 0 | Рыба | | 3 | 141 | Гриндер | | 6 | 559 | Регуляр | | 10 | 1350 | Акула | | 15 | 2619 | Про | | 25 | 5879 | Легенда | **Тиры — это витрина уровня:** название, цвет и картинка. Их пороги и оформление ведёт платформа через админку, это не зашито в код. В профиле игрока видны текущий тир и прогресс до следующего уровня. ## Сила игрока Отдельная метрика. Она отвечает на другой вопрос: не «сколько игрок наиграл», а **насколько высоко он финиширует относительно поля**. Сила сглаженная — один турнир двигает её понемногу, тренд важнее выброса. Показывается числом от 1.0 до 10.0, стартовое значение у всех **5.0**. Для того же турнира на 18 человек: победа поднимет силу с 5.0 до 5.5, последнее место опустит до 4.6. То есть даже разгромный результат не обрушивает метрику — и это сделано намеренно. ## Сезоны **Сезон — календарный квартал.** Смена происходит сама, ночью: очки сезона начинают копиться заново, а XP, уровень, сила и победы остаются. Прошлые сезоны доступны — в лидерборде можно выбрать сезон. ## Лидерборды и место игрока **Сезонный** — по очкам сезона. При равенстве очков выше тот, у кого больше первых мест; если и они равны — тот, кто набрал их за меньшее число игр. **Уровневый** — топ-100 по накопленному XP. **Моё место** игрок видит на главной: топ-3 и отдельной строкой он сам — с рангом, победами и очками. Если он ещё не играл в этом сезоне, он стоит сразу за последним сыгравшим, с нулями и подсказкой «сыграй первый турнир». ## Когда начисляется Только в момент, когда ТД подтверждает результаты турнира. **Это действие необратимо для клуба:** ошибку может исправить только платформа, пересчётом. Автоматического пересчёта задним числом в продукте нет. ## Что рядом - [Рейтинг: пакет формул и начисление](https://docs.novapoker.ru/dev/rating.md) — те же формулы в коде. - [Продукт и действующие лица](https://docs.novapoker.ru/product.md) — где рейтинг виден игроку. === # Уведомления Источник: https://docs.novapoker.ru/product/notifications/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: repos/poker-api/apps/server/src/notifications, docs/superpowers/specs/2026-08-22-product-spec-as-built.md --- Всё, что система шлёт игроку сегодня, — пять автоматических поводов и одна ручная рассылка. ## Автоматические | Повод | Что видит игрок | |---|---| | За 24 часа до турнира | «Скоро турнир» | | В момент дедлайна отмены | «Скоро турнир» — с напоминанием про дедлайн | | Поднялся из листа ожидания | «Ты прошёл из листа ожидания» | | Результаты турнира подтверждены | место и очки | | Игрок, на которого он подписан, записался | «Твой игрок записался» | Все они дублируются в ленту уведомлений внутри приложения — там видны последние 50 — и **не задваиваются при повторах**: система помнит, что уже отправляла. Пуш из листа ожидания приходит **только тем, кто реально поднялся**, а не всей очереди. ## Рассылка от команды Делается вручную из админки, с выбором аудитории: - все; - участники конкретного турнира; - город; - поимённый список — по номерам или идентификаторам. Отправитель сразу видит, скольких зацепило и скольким пуш реально ушёл. Это два разных числа: у части людей может не быть ни одного зарегистрированного устройства. ## Чего не приходит Уведомлений о старте турнира, смене уровня или вылете нет — во время игры человек смотрит live-экран, а не ленту. Гостям без аккаунта не приходит ничего: пуш слать некуда. ## Что рядом - [Фоновые задачи и уведомления](https://docs.novapoker.ru/dev/notifications.md) — расписания, дедупликация, токены. - [День игры и чек-ин](https://docs.novapoker.ru/product/game-day.md) — откуда берутся напоминания. === # Языки Источник: https://docs.novapoker.ru/product/languages/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-as-built-system-spec.md --- Коротко: **интерфейс русский, данные двуязычные.** ## Интерфейс Переключатель языка есть во всех трёх приложениях, и выбор запоминается. Но полного английского интерфейса сегодня нет — переведена только часть строк. Автоопределение языка устройства **написано, но намеренно выключено**: пока перевод неполный, включать его значит показать человеку наполовину английский экран. ## Данные Двуязычны сущности, которые заводит команда или клуб: - города в справочнике; - тиры уровней; - заголовок и подпись турнира; - промо-слайды на главной. Это значит, что при заведении турнира или слайда **поля заполняются дважды** — на русском и на английском. ## Что видит человек, если перевода нет Пустой английский перевод **падает на русский**, а не показывает пустоту. То есть не заполнить английское поле безопасно: интерфейс не сломается, просто покажет русский текст. Отсюда практическое следствие: английские поля можно заполнять по мере необходимости, а не блокировать ими публикацию турнира. ## Что рядом - [Продукт и действующие лица](https://docs.novapoker.ru/product.md) — где эти данные показываются. - [Ландшафт и границы](https://docs.novapoker.ru/dev.md) — какие сущности двуязычны в модели. === # Клуб — залы, блайнды, настройки Источник: https://docs.novapoker.ru/product/club/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-product-spec-as-built.md, repos/poker-organizer/app --- Раздел «Клуб» в приложении организатора виден **только владельцу**. Это настройки, которые задаются один раз и дальше подставляются при создании каждого турнира. ## Что здесь настраивается | Что | Зачем | |---|---| | **Залы** | где проходят турниры: имя, город из справочника, адрес, таймзона, координаты, описание, фото | | **Структуры блайндов** | уровни, стартовый стек, перерывы — то, по чему идут часы турнира | | **Сотрудники и дилеры** | кто из людей что может делать в клубе | | **Настройки лиги** | дедлайн отмены записи | ## Залы Зал выбирается при создании турнира, поэтому без хотя бы одного зала турнир не завести. Город берётся из общего справочника платформы, а не вводится текстом — по нему игроки ищут турниры в афише. ## Структуры блайндов Структура — это набор уровней с длительностью и перерывами плюс стартовый стек. У платформы есть готовый шаблон «Стандарт 20 мин»: 13 отрезков, стек 20 000, перерыв 15 минут после шестого уровня. Он доступен всем клубам, свои структуры клуб заводит рядом. В структуре есть поле уровня поздней регистрации, но **продукт его сегодня не применяет** — посадить опоздавшего можно на любом уровне. Поле заполнять можно, эффекта у него нет. ## Настройки лиги Здесь одна значимая вещь — **дедлайн отмены записи**, по умолчанию 3 часа до старта. От него зависят сразу два поведения: - отмена после дедлайна с занятого места даёт игроку страйк; - в момент дедлайна всем записавшимся уходит второе напоминание. Отсюда неочевидное следствие: **поставив дедлайн в 24 часа, вы сдвинете и напоминание** — оба пуша придут почти одновременно, за сутки до игры. ## Что рядом - [Сотрудники и дилеры](https://docs.novapoker.ru/product/staff.md) — кто что может в клубе. - [Жизненный цикл турнира](https://docs.novapoker.ru/product/tournament-lifecycle.md) — что происходит дальше. - [Запись и лист ожидания](https://docs.novapoker.ru/product/registration.md) — где всплывает дедлайн отмены. === # Сотрудники и дилеры Источник: https://docs.novapoker.ru/product/staff/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-product-spec-as-built.md, docs/superpowers/specs/2026-08-24-club-staff-showcase-design.md --- В клубе три роли, и от роли зависит даже набор вкладок в приложении. | Роль | Что видит и может | |---|---| | **Владелец** | всё, что ТД, плюс раздел «Клуб»: залы, структуры блайндов, сотрудники, настройки лиги, назначение дилеров на турниры | | **Турнирный директор** | создать турнир, провести день игры, вести пульт, подтвердить результаты | | **Дилер** | список своих назначений и витринный профиль | У дилера вкладка «Турниры» называется «Назначения», а вместо «Ещё» — «Профиль». Раздел «Клуб» он не видит вовсе. Роль в клубе никак не связана с ролью на платформе: один человек может быть игроком, дилером в одном клубе и ТД в другом. Подробнее — в [аккаунте и входе](https://docs.novapoker.ru/product/account.md). ## Добавить турнирного директора По номеру телефона, сразу — человек получает доступ, ничего подтверждать не нужно. ## Позвать дилера Иначе. Клуб создаёт приглашение на номер и получает **код на экране**. Дальше клуб передаёт код дилеру сам — письмом, в мессенджере, голосом. Дилер вводит код у себя, и код сработает **только с того номера**, на который выписан. **Платформа этот код не доставляет.** Это стоит знать заранее: канал доставки одноразовых кодов у платформы есть, но приглашение дилера через него не идёт, и клуб отвечает за передачу сам. ## Убрать человека из клуба Кнопка «Убрать» в том же разделе. Прошлые турниры и назначения при этом остаются в истории. ## Витрина дилера У дилера есть публичный профиль: фото, био, стаж, число отработанных турниров. Он ведёт его сам во вкладке «Профиль», а игроки видят дилеров в каталоге и на карточке турнира. Отзывов и рейтингов у дилеров нет — каталог справочный. ## Что рядом - [Дилеры на турнире](https://docs.novapoker.ru/product/dealers.md) — назначение и рабочее место. - [Клуб — залы, блайнды, настройки](https://docs.novapoker.ru/product/club.md) — остальные настройки клуба. - [Аккаунт и вход](https://docs.novapoker.ru/product/account.md) — два контура ролей. === # Дилеры на турнире Источник: https://docs.novapoker.ru/product/dealers/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-23-dealer-table-assignment-design.md, docs/superpowers/specs/2026-08-24-dealer-live-workstation-design.md --- Дилер отвечает на два вопроса: **куда мне идти** и **что объявлять**. Организатор на третий: **какой стол остался без человека**. ## Назначение на стол Дилера назначают на турнир из его карточки — это делает владелец клуба. Назначение не общее «на турнир», а **на конкретный стол**: на турнире из 48 человек столов шесть, и раньше их расставляли запиской в чате или голосом. В списке столов организатор видит, какие закрыты дилером, а какие нет. ## Что видит дилер на смене Во вкладке «Назначения» — его турниры. У турнира, который идёт, открывается живое рабочее место: - **отсчёт до смены уровня** — по нему дилер понимает, когда объявлять следующие блайнды; - **текущие блайнды с анте**; - **состав своего стола** — места и имена. Это ровно то, что дилер раньше узнавал у турнирного директора или с настенного табло, если оно в зале было. Экран живой: он обновляется сам вместе с пультом организатора, отдельно ничего обновлять не нужно. ## Витринный профиль У дилера есть публичная страница: фото, био, стаж, число отработанных турниров. Ведёт её он сам, игроки видят её в каталоге дилеров и на карточке турнира. ## Границы - **Дилер не управляет игрой.** Он видит стол, но вылеты, пересадки и часы — только у турнирного директора. - **Отзывов и оценок у дилеров нет.** ## Что рядом - [Сотрудники и дилеры](https://docs.novapoker.ru/product/staff.md) — как дилера зовут в клуб. - [Старт, рассадка и баланс](https://docs.novapoker.ru/product/start-and-seating.md) — откуда берутся столы. - [Live-цикл и реалтайм](https://docs.novapoker.ru/dev/live.md) — что лежит в живом состоянии турнира. === # Что ведёт платформа Источник: https://docs.novapoker.ru/product/platform/ Дорожка: продукт Сверено с кодом: Mon Aug 24 Код: docs/superpowers/specs/2026-08-22-product-spec-as-built.md, repos/poker-admin/src --- Админка — рабочее место команды платформы. Разделы фильтруются по роли: человек видит только то, на что у него есть право. ## Разделы | Раздел | Что делает платформа | Кому | |---|---|---| | **Дашборд** | новые и активные игроки за 30 дней, доля возвращающихся, завершённые турниры и разбивка по неделям | всем админ-ролям | | **Организаторы** | заводит клубы, ведёт состав и статус | супер-админ | | **Турниры** | видит турниры всех клубов, включая отменённые; оформляет, заводит турнир за клуб, отменяет, запускает пересчёт | супер-админ, маркетинг | | **Игроки** | ищет, банит и разбанивает, склеивает задвоенные аккаунты | супер-админ, модератор | | **Рейтинг** | правила: коэффициенты очков, пороги уровней, сглаживание силы, число страйков, окно и длительность блокировки | супер-админ | | **Сезоны** | сезоны и текущий квартал | супер-админ | | **Города** | справочник двумя языками, активность и порядок | супер-админ | | **Слайдеры** | промо-слайды на главной игрока | супер-админ, маркетинг | | **Тиры** | названия, пороги, цвета и картинки уровней | супер-админ, маркетинг | | **Админы** | раздаёт админ-роли | супер-админ | | **Рассылка** | пуш выбранной аудитории | супер-админ, модератор, маркетинг | Аккаунт без админ-роли видит экран «Нет доступа» — в админку он не попадёт. ## Правила рейтинга — настройка, а не константа Это важное продуктовое следствие, которое часто упускают: **коэффициенты очков, пороги уровней, сглаживание силы, число страйков до блокировки, её окно и длительность живут в настройке платформы**, а не в коде. Их можно менять на ходу, и они сразу действуют на новые начисления. Уже посчитанные результаты при этом не пересчитываются — для этого есть отдельная операция пересчёта турнира. ## Модерация игроков - **Бан** закрывает вход целиком. Это не то же самое, что [временная блокировка записи за неявки](https://docs.novapoker.ru/product/registration.md). - **Склейка дублей** переносит регистрации второго аккаунта на основной — кроме турниров, где у основного уже есть своя запись. ## Рассылка Пуш уходит выбранной аудитории: все, участники конкретного турнира, город или поимённый список. Отправитель видит два числа: скольких зацепило и скольким пуш реально ушёл. Расхождение нормально — у части людей нет активных устройств. ## Кто чем управляет из контента | Что | Кто ведёт | Где видно игроку | |---|---|---| | Города | платформа | город в профиле, фильтр афиши, адреса залов | | Тиры уровней | платформа | профиль, главная | | Слайдер на главной | платформа | главный экран | | Оформление турнира | клуб — свои, платформа — любые | афиша, карточка турнира | | Призы и ссылка на чат | клуб | карточка турнира | | Залы | клуб | каталог залов, карточка турнира | | Профиль дилера | сам дилер | каталог дилеров, карточка турнира | | Структуры блайндов | клуб плюс платформенный шаблон | таймер на live-экране | ## Что рядом - [Админка — права и метрики](https://docs.novapoker.ru/dev/admin.md) — как это устроено внутри. - [Рейтинг, уровни и сезоны](https://docs.novapoker.ru/product/rating.md) — что именно настраивается. - [Уведомления](https://docs.novapoker.ru/product/notifications.md) — что уходит игроку.