Внутренняя документация · 2026-05-04

Как работает панель

Архитектура, БД, NATS, REST API и UI panel.recore.su — управляющего слоя над всеми клубами RE:CORE. Для операторов, интеграторов, тех кто пишет код вокруг панели.

Сервер: 176.114.85.85 SSH: port 2222, key id_ed25519 Стек: Fastify + Drizzle + React + NATS + PostgreSQL 17

1Что такое panel.recore.su

Centralized control plane для всей платформы RE:CORE. Из неё:

  • Создаются и удаляются клубы (provisioning).
  • Управляется их per-club .NET backend (контейнеры club-N).
  • Регистрируются и публикуются версии всех продуктов (rebornapi, ReBornClient, panel-web, биллинг и т.д.).
  • Раскатываются обновления на ПК внутри клубов через комбинацию pull (HTTP polling) + push (NATS).
  • Хранятся служебные данные: clubs, computers, versions, audit log, billing plans, alerts.

Сама панель — два сервиса: panel-api (REST + NATS-publisher на Fastify) и panel-web (React SPA на Vite). Плюс рядом стоят клубные backend'ы, биллинг и общие инфра-контейнеры.

Главное

Панель — координатор. Она не «играет» сессии, не считает деньги в моменте, не показывает игрокам интерфейс. Она управляет, видит и раскатывает. Игровая логика живёт в per-club rebornapi backend'ах. Биллинговая — в отдельных сервисах.

2Архитектура и контейнеры

На сервере 176.114.85.85 в одном docker-compose крутятся:

КонтейнерСтекНазначение
recore-panel-apiNode.js Fastify 5 + Drizzle ORM + TypeScriptREST API панели, NATS publisher/subscriber, версионирование, провижн клубов
recore-panel-webReact + Vite + TypeScriptSPA админки за nginx
recore-postgresPostgreSQL 17База платформы (reborn_platform) + клубные БД (reborn_club_*) на одном инстансе
recore-pgbounceredoburu/pgbouncer 1.25Connection pooler перед postgres (transaction mode). panel-api ходит через него
recore-redisRedis 7Кэш user-данных (60с TTL), сессии админов
recore-transport-natsNATS 2.10Сообщения panel ↔ club ↔ ПК. Subjects recore.cmd.*, recore.req.*, recore.status.*, recore.ack.*
recore-nginxnginxTLS-терминатор, роутинг по subdomain'ам
club-N (×N клубов).NET 8 ReBornAPIBackend конкретного клуба. Слушает на 1-N.recore.su, имеет свою БД reborn_club_<slug>
reborn-billing-*отдельный стекБиллинг (UI + API + БД), пока на этом же сервере, переезд на bill.recore.su (87.228.83.164) запланирован

Сети

  • recore-internal — управляющая сеть (panel-api ↔ postgres ↔ redis ↔ nats).
  • recore-clubs — сеть клубных контейнеров (panel-api → club-N).
  • recore-public — экспонированные наружу контейнеры.

3Модель данных

База reborn_platform (Identity DB) — управляется через panel/api/src/db/schema.ts в Drizzle:

ТаблицаЧто хранит
clubsКлубы: slug, владелец, биллинг, release_channel, client_target_version_id, флаг production
computersПК всех клубов: pcNumber, MAC, override-версия (client_version_override), heartbeat (last_reported_version, last_reported_at)
api_versionsРелизы rebornapi (docker-image): tag, channel, validation, manifest
client_versionsРелизы desktop-клиента (win-installer): version, channel, downloadUrl, sha256, sizeBytes
promotion_rulesПравила перевода версий между каналами (min_hours_in_from = 48ч для beta→stable)
version_historyЖурнал всех applyVersionToClub: что на что, когда, кто, статус, backup
audit_logВсе действия админов (apply-version, pin PC, channel switch, role change)
platform_admins / platform_usersУчётки админов и платформы
command_journalNATS-команды от panel-api на ПК (для дедупа и трейсинга)

Клубные БД reborn_club_<slug> — управляются клубным rebornapi (.NET EF Core):

  • Computers, Sessions, Users, Bookings, BarItems, BarTransactions, FiscalReceipts, Achievements, Admins, Tariffs, Alerts и т.д. — всё специфичное для одного клуба.

users дрейфит

Таблица Users повторяется в каждой клубной БД и в platform_users — единого источника правды нет. План на 2026-Q3: убрать Users из клубных БД, оставить Identity (panel) единственным источником, а клубный backend ходит через HTTP + Redis-кэш. Прогресс — в specs/.

4Раздел «Клубы» в UI

Страница /clubs — список всех клубов с быстрыми действиями. Внутри каждого клуба (/clubs/:id):

  • Шапка: название, slug, владелец, статус контейнера (running / stopped / starting), флаг production.
  • Биллинг и брендинг: тариф, оплачено до, цвета и логотип для клиентских ПК.
  • Версия rebornapi backend'а: текущая версия контейнера, история переключений, кнопки Apply Version / Rollback.
  • Канал релиза: селект test / beta / stable. На production-клубе доступен только stable.
  • Клиент ПО (ReBornClient): блок управления десктоп-клиентами на ПК клуба (см. ниже раздел «Управление обновлениями»).
  • Команды: Start / Stop / Restart контейнера club-N, очистка кэша, выкл. ПК всех/выборочно.
  • Метрики: CPU, RAM, диск контейнера club-N + последние алерты.

5ПК клуба и команды

Внутри карточки клуба секция «ПК клуба» показывает таблицу — каждая строка = ПК (от 1 до N в клубе). Колонки:

КолонкаОткудаДействия
#computers.pc_number
MACEF Core Computers.MacAddress в клубной БД (бэкфилл)
Текущая версияcomputers.last_reported_version (от NATS heartbeat'а v9.2.4+). Если --- — клиент старый или офлайн
Overridecomputers.client_version_overridePin ▾ / Очистить

Кнопки на уровне всего клуба:

  • ⟳ Перепроверить обновления у всех ПК — отправляет NATS-команду RecheckUpdates всем подключённым ПК. Они сразу опросят /api/clients/latest и поставят актуальную версию.
  • Команды для ПК (как у всего клуба): Start / Stop / Lock / Unlock / Reboot — публикуются в NATS на recore.cmd.<club>.<pc>.*.

6Раздел «Версии»

Страница /versions показывает две таблицы:

  1. API versions (rebornapi) — то что катится в клубный контейнер. Каждая строка: tag, channel, статус валидации, метрики после деплоя. Кнопки Promote → beta / Promote → stable применяют promotion_rules (минимум 48 часов в beta для stable).
  2. Версии десктоп-клиента (client_versions) — то что ставится на ПК внутри клубов. С counters: «N клубов / M ПК на этой версии».

Только версии со статусом passed могут быть применены в production-клубах. Регрессионные тесты прогоняются через CI и пишут результат в api_versions.smoke_test_results.

7Управление обновлениями ПО

Главное в трёх абзацах

1. Каналы. Любая собранная версия попадает на канал по git-ветке: testbetamain (=stable). Production-клубы видят только stable. Promote из канала в канал — кнопка в панели, между beta и stable обязательны 48 часов обкатки.

2. Resolution. Каждый ПК клуба периодически дёргает GET /api/clients/latest?clubSlug=…&pcId=… — сервер возвращает целевую версию по приоритету: (1) pin на ПК → (2) target на клуб → (3) последняя в канале клуба. Клиент ставит то что вернулось — даже если это даунгрейд.

3. Push «обновись сейчас». Кнопка [Перепроверить обновления] в карточке клуба публикует NATS-команду recore.cmd.<club>.<pc>.RecheckUpdates — десктоп-клиент v9.2.4+ сразу опрашивает /latest и обновляется. Без push'а — обновляется при следующем плановом poll'е (~10 мин).

Типичные операции

Раскатить новую версию на конкретный клуб

Открой panel.recore.su/clubs/<id> → секция «Клиент ПО (ReBornClient)»[Применить версию] → выбери версию из списка → [Применить]. Если хочешь чтобы ПК обновились прямо сейчас (а не через ~10 минут polling) — отметь «push RecheckUpdates сейчас». Backend проверит производственный флаг — на production-клубе можно ставить только версии канала stable.

Откатить отдельный ПК на старую версию

В таблице ПК внутри клуба — кнопка [Pin ▾] → выбираешь любую активную версию из списка → [Pin & Push]. ПК сразу получит NATS-команду и переустановит указанную версию. Чтобы снять pin — кнопка [Очистить] в той же строке.

Поменять канал клуба test ↔ beta ↔ stable

В карточке клуба — селект «Канал релиза». Сменил → следующий polling клиента подхватит новую версию из этого канала. На production-клубе селект жёстко stable, изменить нельзя пока не снять флаг is_production.

Понять что фактически установлено на ПК

В таблице ПК колонка «Текущая» показывает last_reported_version от heartbeat'а. Если стоит --- — клиент старый и не шлёт installedClientVersion в heartbeat (нужен v9.2.4+). Альтернатива: GET /api/computers/<pcId>/effective-version — diagnostic endpoint показывает что должно стоять и что фактически прилетало.

Promote версию test → beta или beta → stable

Страница /versions → найти строку → [Promote → beta] или [Promote → stable]. Backend проверит min_hours_in_from (для beta→stable это 48 часов). Если кнопка серая или возвращает 409 — cooldown ещё не прошёл, посмотри в audit_log когда версия попала в текущий канал.

8REST API

Все мутирующие endpoints требуют JWT (Bearer token из POST /api/auth/login). Public endpoints без auth — для самих десктоп-клиентов.

Логин (получить токен)

curl -X POST -H 'Content-Type: application/json' \
  -d '{"login":"admin","password":"…"}' \
  https://panel.recore.su/api/auth/login
# → { "token": "eyJ…" }

Endpoints управления версиями

МетодПутьAuthНазначение
GET/api/clients/latest
?clubSlug=&pcId=&productKey=
То что десктоп-клиент дёргает сам. Resolution по приоритету (override→target→channel)
POST/api/clubs/:id/apply-client-versionJWTУстановить target версию для клуба + push RecheckUpdates
DELETE/api/clubs/:id/apply-client-versionJWTСбросить target — клуб вернётся на channel-latest
POST/api/computers/:id/version-overrideJWTPin ПК на конкретную версию
DELETE/api/computers/:id/version-overrideJWTСнять pin
POST/api/clubs/:id/recheck-updatesJWTТолько pushнуть RecheckUpdates без смены target. Body {"pcIds":[...]} для фильтра
GET/api/computers/:id/effective-versionJWTDiagnostic: что target / что installed
GET/api/clients/versions?productKey=…JWTСписок версий с counters клубов/ПК
POST/api/clubs/:id/apply-versionJWTПрименить rebornapi версию (docker-image) к контейнеру club-N
POST/api/clubs/:id/rollback/:historyIdJWTОткатить rebornapi на предыдущую запись из version_history
PUT/api/clubs/:id/channelJWTСменить release_channel клуба

Примеры curl

# Получить целевую версию для ПК (без auth — это делает десктоп):
curl 'https://panel.recore.su/api/clients/latest?clubSlug=rio&pcId=12&productKey=reborn-client'

# Применить версию ко всему клубу:
curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"versionId": 87}' \
  https://panel.recore.su/api/clubs/4/apply-client-version

# Pin ПК:
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -d '{"versionId": 87}' \
  https://panel.recore.su/api/computers/1234/version-override

# Triggered recheck (все ПК):
curl -X POST -H "Authorization: Bearer $TOKEN" -d '{}' \
  https://panel.recore.su/api/clubs/4/recheck-updates

9NATS subjects

SubjectКто публикуетКто слушаетЧто внутри
recore.cmd.<club>.<pc>.<type>panel-apidesktop-client v9Команды: Lock, Unlock, Reboot, RecheckUpdates и т.п.
recore.req.<club>.<pc>.<method>desktop-clientpanel-api / club-N rebornapiRegisterClient, ReportPcStatus, UserAuthorized, SessionStarted и т.п.
recore.status.<club>.<pc>desktop-clientpanel-api (heartbeat-collector)Heartbeat каждые 10 секунд + installedClientVersion начиная с v9.2.4
recore.ack.<club>.<pc>.<commandId>desktop-clientpanel-api (ack-collector)ack_success / ack_rejected / ack_duplicate

Тестовый листенер прямо на сервере

ssh -i ~/.ssh/id_ed25519 -p 2222 root@176.114.85.85
docker logs recore-transport-nats --tail 100 | grep "recore.cmd.4"

Envelope команды

{
  "commandId": "uuid-v4",
  "pcId": 12, "clubId": 4,
  "type": "RecheckUpdates",
  "payload": { "productKey": "reborn-client", "reason": "club-target-bumped", "targetVersionId": 87 },
  "sentAt": "2026-05-04T...",
  "ttlMs": 300000,
  "correlationId": null,
  "routingEpoch": 1,
  "handlerVersion": "v1.0.0"
}

TTL 300000 мс = 5 минут. Если ПК офлайн дольше, команда устаревает на стороне клиента и отбрасывается без выполнения.

10CI и публикация версий

Все продукты RE:CORE публикуют версии в panel через единый CI-шаблон ci-templates/standard-version-manifest.gitlab-ci.yml. Подключение в .gitlab-ci.yml продукта:

include:
  - project: 'root/recore-platform'
    file: '/ci-templates/standard-version-manifest.gitlab-ci.yml'
    ref: main

variables:
  PRODUCT_KEY: "my-product"
  ARTIFACT_TYPE: "docker-image"   # или win-installer | static-site | mobile-android | mobile-ios | ansible-config

stages:
  - build
  - publish

В job сборки положи в $CI_PROJECT_DIR/manifest.json type-specific блок:

Для docker-image (rebornapi и подобные):

jq -n --arg img "$REGISTRY/$IMAGE:$TAG" \
  --arg minPanel "1.5.0" --argjson dbSchema 14 \
  '{ artifactType:"docker-image", imageRegistryUrl:$img,
     minPanelVersion:$minPanel, dbSchemaVersion:$dbSchema, breakingChanges:false }' \
  > manifest.json

Для win-installer (ReBornClient):

jq -n --arg url "$B2_URL" --arg sha "$INSTALLER_SHA256" --argjson size "$INSTALLER_SIZE" \
  '{ artifactType:"win-installer",
     installer:{ url:$url, sha256:$sha, sizeBytes:$size },
     changelog:"" }' \
  > manifest.json

Шаблон сам добавит productKey, tag, channel (из ветки), gitCommitSha, gitlabPipelineId, publishedAt и POST в $CI_PANEL_URL/api/versions/register. Нужны переменные CI_PANEL_URL + CI_PANEL_TOKEN на уровне Group в GitLab.

Канал берётся из ветки автоматически:

ВеткаКаналСтадия
testtestАктивная разработка, льётся на staging-клубы
betabetaPilot-клубы для обкатки. Минимум 48 часов перед stable
mainstableProduction-клубы видят только этот канал
developtestbackward-compat alias на test

11Деплой и сборка

Контейнеры panel-api и panel-web собираются на сервере из исходников, не из registry. После merge MR в main:

ssh -i ~/.ssh/id_ed25519 -p 2222 root@176.114.85.85
cd /opt/recore-platform
git pull origin main
docker compose build panel-api panel-web
docker compose up -d panel-api panel-web

# Если есть новая миграция (NN_*.sql):
docker compose exec -e DATABASE_URL=postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/reborn_platform \
  panel-api npm run db:migrate

⚠ Drizzle через PgBouncer

Прямой postgres URL обязателен — drizzle-kit не работает через transaction-mode pooling. Если делаешь миграции через pgbouncer:5432 — получишь cryptic error «prepared statement does not exist». Override DATABASE_URL на postgres:5432 только для миграций.

Backup перед миграциями

docker exec recore-postgres pg_dump -U $PG_USER reborn_platform | \
  gzip > /root/backups/pre-NNN-$(date +%s).sql.gz

Откат БД

gunzip -c /root/backups/pre-NNN-*.sql.gz | \
  docker exec -i recore-postgres psql -U $PG_USER reborn_platform

Контейнеры клубов

Per-club club-N backend'ы собираются в CI (rebornapi), пушатся как docker-image. panel-api через POST /api/clubs/:id/apply-version делает docker pull + recreate контейнера, обновляет nginx-vhost, проверяет /health 30 секунд. При фейле — авто-откат.

12Работа с БД

Postgres крутится в контейнере recore-postgres. Подключиться напрямую:

ssh -i ~/.ssh/id_ed25519 -p 2222 root@176.114.85.85
docker exec -it recore-postgres psql -U recore_admin -d reborn_platform

Полезные запросы

-- Распределение версий по клубам:
SELECT slug, release_channel, client_target_version_id FROM clubs ORDER BY id;

-- Какие ПК запинены:
SELECT c.slug, comp.pc_number, cv.version, cv.channel
FROM computers comp
JOIN clubs c ON c.id = comp.club_id
JOIN client_versions cv ON cv.id = comp.client_version_override
ORDER BY c.slug, comp.pc_number;

-- Все админ-действия за последний час:
SELECT created_at, admin_id, action, entity_type, entity_id, details
FROM audit_log WHERE created_at > now() - interval '1 hour'
ORDER BY created_at DESC;

-- Какие версии доступны для канала:
SELECT id, version, channel, is_active FROM client_versions
WHERE product_key='reborn-client' AND channel='test'
ORDER BY released_at DESC;

Schema через Drizzle

Все таблицы определены в panel/api/src/db/schema.ts. Drizzle Kit генерит миграции:

cd panel/api
npm run db:generate   # создаст migrations/NNN_*.sql из diff schema.ts vs текущей БД
npm run db:migrate    # применит pending миграции (через прямой postgres URL!)

13Авторизация и токены

НазначениеГде живётИспользование
JWT админаIssued POST /api/auth/login, signing key PANEL_JWT_SECRETHeader Authorization: Bearer … на любой mutating endpoint
CI_PANEL_TOKENGroup-level CI/CD variable в GitLabCI шаблон публикует версии: POST /api/versions/register
PANEL_SYNC_TOKEN.env на сервереCI sync-metadata шаблон: POST /api/products/sync-from-manifest
GITLAB_TOKEN.env на сервере (glpat-…)panel-api проксирует скачивание Generic Packages с GitLab Registry
NATS_PANEL_PASS.env на сервереpanel-api подписывается на NATS как user panel

14Production guards и audit log

Production-клубы защищены

Любые операции apply-client-version / set-version-override на клубе с is_production=true требуют:

  • Версия должна иметь validation_status='passed'.
  • Канал версии = 'stable'.
  • Иначе backend возвращает 403 "Production club requires stable validated version".

UI зеркалит этот check, скрывая non-stable версии в селекте production-клуба.

Audit log

Каждое мутирующее действие пишет запись в audit_log:

{
  admin_id, action, entity_type, entity_id,
  details (jsonb), ip_address, created_at
}

Действия (стартовый список):

  • pc.set-version-override, pc.clear-version-override
  • club.apply-client-version, club.clear-client-target, club.recheck-updates
  • club.apply-version (rebornapi), club.rollback
  • club.set-channel, club.set-production-flag
  • auth.login, auth.failed-attempt — не пишутся пока, в плане

Будущее (logging-spec)

В main уже лежит spec на платформенное логирование: Loki + Grafana + ingest-service + audit-trail с hash-цепочкой и партиционированием по году. После реализации (фаза 0b) текущая audit_log будет заменена на partitioned + immutable + hash-chain. Все текущие db.insert(auditLog) в коде нужно будет переписать через helper-функцию.

15Troubleshooting

/api/clients/latest возвращает 404 «No version available»

В client_versions нет ряда matching productKey + channel + (slug OR '*'). Версии не опубликованы для канала, либо канал клуба переключён на тот где версий нет. Проверь:

SELECT slug, release_channel FROM clubs WHERE slug='rio';
SELECT product_key, channel, club_slug, is_active FROM client_versions
WHERE is_active=true ORDER BY released_at DESC LIMIT 10;

/api/clients/latest возвращает 404 «Club not found»

clubSlug не существует в БД. После MR #27 это strict — раньше был fallback на ?channel=, маскировал тайпы. Это правильное поведение.

Нажал [Перепроверить] — НИЧЕГО не происходит

  1. ПК вообще онлайн? — docker logs --since 5m recore-transport-nats | grep recore.status.<club>.<pc>, должны быть heartbeat'ы.
  2. ПК на NATS-транспорте? — если в логах нет heartbeat'ов от этого ПК, значит он на legacy SignalR; RecheckUpdates его не достанет, нужен polling /latest.
  3. ПК на v9.2.4+? — handler для RecheckUpdates появился только в этой версии. Старые клиенты не падают, просто игнорируют.

Counters в /versions показывают 0/0 везде

Это нормально, если ни один клуб не использовал apply-client-version и ни один ПК не запинен. Counters считают прямые ссылки (clubs.client_target_version_id + computers.client_version_override), не «эффективные» через channel-latest.

Запинил ПК на удалённую версию (is_active=false)

Backend всё равно отдаст эту версию (override priority выше всего). UI показывает ⚠ предупреждение. Если версия битая — сними pin и клуб вернётся на channel-latest. В будущем планируем возвращать 410 Gone на deactivated версии — клиент будет фоллбечиться сам.

drizzle-kit migrate ругается на pgbouncer

Используй прямой postgres URL:

docker compose exec -e DATABASE_URL=postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/reborn_platform \
  panel-api npm run db:migrate

Контейнер club-N не стартует после apply-version

Backend делает 10 health-check попыток с интервалом 3 секунды. Если /health не ответил 200 за 30с — авто-rollback на предыдущую версию. Проверь version_history где status='rolled_back' и rollback_reason. Чаще всего: schema-миграция в новой версии не накатилась или несовместима.

Production-клуб не даёт выбрать test/beta

By design (is_production && channel != 'stable' guard). Чтобы временно отключить production-флаг:

PUT /api/clubs/:id/production
Body: {"isProduction": false}

Понизит release_channel до stable первоначально, потом можно поменять на test/beta.