Как работает панель
Архитектура, БД, NATS, REST API и UI panel.recore.su — управляющего слоя над всеми клубами RE:CORE. Для операторов, интеграторов, тех кто пишет код вокруг панели.
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-api | Node.js Fastify 5 + Drizzle ORM + TypeScript | REST API панели, NATS publisher/subscriber, версионирование, провижн клубов |
recore-panel-web | React + Vite + TypeScript | SPA админки за nginx |
recore-postgres | PostgreSQL 17 | База платформы (reborn_platform) + клубные БД (reborn_club_*) на одном инстансе |
recore-pgbouncer | edoburu/pgbouncer 1.25 | Connection pooler перед postgres (transaction mode). panel-api ходит через него |
recore-redis | Redis 7 | Кэш user-данных (60с TTL), сессии админов |
recore-transport-nats | NATS 2.10 | Сообщения panel ↔ club ↔ ПК. Subjects recore.cmd.*, recore.req.*, recore.status.*, recore.ack.* |
recore-nginx | nginx | TLS-терминатор, роутинг по subdomain'ам |
club-N (×N клубов) | .NET 8 ReBornAPI | Backend конкретного клуба. Слушает на 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_journal | NATS-команды от 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 | — |
| MAC | EF Core Computers.MacAddress в клубной БД (бэкфилл) | — |
| Текущая версия | computers.last_reported_version (от NATS heartbeat'а v9.2.4+). Если --- — клиент старый или офлайн | — |
| Override | computers.client_version_override | Pin ▾ / Очистить |
Кнопки на уровне всего клуба:
- ⟳ Перепроверить обновления у всех ПК — отправляет NATS-команду
RecheckUpdatesвсем подключённым ПК. Они сразу опросят/api/clients/latestи поставят актуальную версию. - Команды для ПК (как у всего клуба):
Start/Stop/Lock/Unlock/Reboot— публикуются в NATS наrecore.cmd.<club>.<pc>.*.
6Раздел «Версии»
Страница /versions показывает две таблицы:
- API versions (rebornapi) — то что катится в клубный контейнер. Каждая строка: tag, channel, статус валидации, метрики после деплоя. Кнопки Promote → beta / Promote → stable применяют
promotion_rules(минимум 48 часов в beta для stable). - Версии десктоп-клиента (client_versions) — то что ставится на ПК внутри клубов. С counters: «N клубов / M ПК на этой версии».
Только версии со статусом passed могут быть применены в production-клубах. Регрессионные тесты прогоняются через CI и пишут результат в api_versions.smoke_test_results.
7Управление обновлениями ПО
Главное в трёх абзацах
1. Каналы. Любая собранная версия попадает на канал по git-ветке: test → beta → main (=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-version | JWT | Установить target версию для клуба + push RecheckUpdates |
DELETE | /api/clubs/:id/apply-client-version | JWT | Сбросить target — клуб вернётся на channel-latest |
POST | /api/computers/:id/version-override | JWT | Pin ПК на конкретную версию |
DELETE | /api/computers/:id/version-override | JWT | Снять pin |
POST | /api/clubs/:id/recheck-updates | JWT | Только pushнуть RecheckUpdates без смены target. Body {"pcIds":[...]} для фильтра |
GET | /api/computers/:id/effective-version | JWT | Diagnostic: что target / что installed |
GET | /api/clients/versions?productKey=… | JWT | Список версий с counters клубов/ПК |
POST | /api/clubs/:id/apply-version | JWT | Применить rebornapi версию (docker-image) к контейнеру club-N |
POST | /api/clubs/:id/rollback/:historyId | JWT | Откатить rebornapi на предыдущую запись из version_history |
PUT | /api/clubs/:id/channel | JWT | Сменить 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-api | desktop-client v9 | Команды: Lock, Unlock, Reboot, RecheckUpdates и т.п. |
recore.req.<club>.<pc>.<method> | desktop-client | panel-api / club-N rebornapi | RegisterClient, ReportPcStatus, UserAuthorized, SessionStarted и т.п. |
recore.status.<club>.<pc> | desktop-client | panel-api (heartbeat-collector) | Heartbeat каждые 10 секунд + installedClientVersion начиная с v9.2.4 |
recore.ack.<club>.<pc>.<commandId> | desktop-client | panel-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.
Канал берётся из ветки автоматически:
| Ветка | Канал | Стадия |
|---|---|---|
test | test | Активная разработка, льётся на staging-клубы |
beta | beta | Pilot-клубы для обкатки. Минимум 48 часов перед stable |
main | stable | Production-клубы видят только этот канал |
develop | test | backward-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_SECRET | Header Authorization: Bearer … на любой mutating endpoint |
| CI_PANEL_TOKEN | Group-level CI/CD variable в GitLab | CI шаблон публикует версии: 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-overrideclub.apply-client-version,club.clear-client-target,club.recheck-updatesclub.apply-version(rebornapi),club.rollbackclub.set-channel,club.set-production-flagauth.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=, маскировал тайпы. Это правильное поведение.
Нажал [Перепроверить] — НИЧЕГО не происходит
- ПК вообще онлайн? —
docker logs --since 5m recore-transport-nats | grep recore.status.<club>.<pc>, должны быть heartbeat'ы. - ПК на NATS-транспорте? — если в логах нет heartbeat'ов от этого ПК, значит он на legacy SignalR; RecheckUpdates его не достанет, нужен polling /latest.
- ПК на 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.