6 Commits

Author SHA1 Message Date
github-actions[bot]
e50b10a433 Merge remote-tracking branch 'upstream/main' 2026-08-04 08:28:07 +00:00
Egor
b866bebeeb Merge pull request #538 from BEDOLAGA-DEV/release-please--branches--main--components--cabinet-frontend
chore(main): release 1.65.0
2026-08-03 19:22:47 +03:00
github-actions[bot]
4c8e17a5a4 chore(main): release 1.65.0 2026-08-03 16:22:13 +00:00
Egor
c718c4ca34 Merge pull request #537 from BEDOLAGA-DEV/feat/remnawave-3.0.0
Feat/remnawave 3.0.0
2026-08-03 19:21:32 +03:00
Fringg
e840ed2b60 feat(admin): панельная идентичность под Remnawave 3.0.0
Remnawave 3.0.0 удалил `uuid` из модели пользователя — панельный аккаунт
адресуется числовым `id`, и бот переименовал соответствующие поля своего
API. Без этих правок вкладка Sync показывала бы «не привязан» у ВСЕХ
пользователей: оба операнда стали бы undefined, причём без ошибки TS —
интерфейсы продолжали бы декларировать несуществующие поля.

Типы: `UserDetailResponse.remnawave_uuid` → `remnawave_id: number | null`,
`PanelSyncStatusResponse.remnawave_uuid` → `remnawave_id`,
`SyncToPanelResponse.panel_uuid` → `panel_user_id`,
`PanelUserInfo.uuid` → `id: number` (бэкенд отдаёт его обязательным).

В SyncTab используется `??`, а не `||`: идентификатор числовой, и `0`
нужно отличать от отсутствия значения. Ярлык — «Remnawave ID».

Локаль: ключ настройки бота переименован в TRAFFIC_EXCLUDED_USER_IDS,
подпись обновлена — значения теперь числовые id, а не UUID.
2026-08-02 19:38:11 +03:00
Fringg
653d683835 fix(settings): вернуть в админку категории, которых не было в дереве
Дерево разделов настроек — захардкоженный список категорий, а на бэкенде категория
выводится из имени ключа. Любая настройка, создавшая новую категорию, просто
исчезала из админки: настройки были в API, но ни один раздел их не показывал.

Так пропали 33 категории из 102, в том числе:
- CABINET целиком (13 настроек, включая CABINET_ENABLED, CABINET_URL,
  CABINET_REQUIRE_LEGAL_CONSENT и CABINET_LEGAL_CONSENT_PRECHECKED);
- INFO_PAGES — режимы показа оферты, политики, правил и FAQ;
- MENU — кнопка меню Telegram с открытием кабинета.

Эти три разложены по дереву явно. Остальное собирает новый пункт «Прочие
настройки» — по образцу админки бота, где такой раздел уже есть. Это и есть
главная часть фикса: теперь новая категория на бэкенде не приводит к молчаливому
исчезновению настроек, максимум — к неудачному разделу.

Локали ru/en/zh/fa, +5 тестов на само дерево.
2026-07-30 01:58:05 +03:00
12 changed files with 143 additions and 21 deletions

View File

@@ -1,5 +1,17 @@
# Changelog
## [1.65.0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/compare/v1.64.0...v1.65.0) (2026-08-03)
### Features
* **admin:** панельная идентичность под Remnawave 3.0.0 ([e840ed2](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/e840ed2b60f5e525cb290ce5bddc70ef6dfe8a66))
### Bug Fixes
* **settings:** вернуть в админку категории, которых не было в дереве ([653d683](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/commit/653d683835e2c10f7690f1f71df3552c3ba9ab2f))
## [1.64.0](https://github.com/BEDOLAGA-DEV/bedolaga-cabinet/compare/v1.63.0...v1.64.0) (2026-07-29)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "cabinet-frontend",
"version": "1.64.0",
"version": "1.65.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cabinet-frontend",
"version": "1.64.0",
"version": "1.65.0",
"license": "AGPL-3.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",

View File

@@ -1,7 +1,7 @@
{
"name": "cabinet-frontend",
"private": true,
"version": "1.64.0",
"version": "1.65.0",
"description": "Remnawave Bedolaga Web App",
"license": "AGPL-3.0",
"type": "module",

View File

@@ -142,7 +142,7 @@ export interface UserDetailResponse {
promo_offer_discount_source: string | null;
promo_offer_discount_expires_at: string | null;
recent_transactions: UserTransactionItem[];
remnawave_uuid: string | null;
remnawave_id: number | null;
}
export interface UserPanelInfo {
@@ -271,7 +271,7 @@ export interface UserAvailableTariffsResponse {
// Sync types
export interface PanelUserInfo {
uuid: string | null;
id: number;
short_uuid: string | null;
username: string | null;
status: string | null;
@@ -295,7 +295,7 @@ export interface SyncToPanelResponse {
success: boolean;
message: string;
action: string;
panel_uuid: string | null;
panel_user_id: number | null;
changes: Record<string, unknown>;
errors: string[];
}
@@ -303,7 +303,7 @@ export interface SyncToPanelResponse {
export interface PanelSyncStatusResponse {
user_id: number;
telegram_id: number;
remnawave_uuid: string | null;
remnawave_id: number | null;
subscription_id: number | null;
subscription_tariff_name: string | null;
last_sync: string | null;

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { OTHER_CATEGORIES, SETTINGS_TREE, getMappedCategoryKeys } from './constants';
// Категории настроек на бэкенде выводятся из имени ключа, поэтому каждая новая
// настройка может создать категорию, которой в дереве нет. Раньше такие настройки
// просто не отображались: так исчезла вся категория CABINET (13 настроек, включая
// CABINET_ENABLED и CABINET_URL) и правовые документы из INFO_PAGES.
describe('SETTINGS_TREE', () => {
it('содержит пункт-сборник для категорий без своего места', () => {
const children = SETTINGS_TREE.groups.flatMap((group) => group.children);
const catchAll = children.filter((child) => child.categories.includes(OTHER_CATEGORIES));
expect(catchAll).toHaveLength(1);
expect(catchAll[0].id).toBe('sys_other');
});
it('не считает маркер сборника обычной категорией', () => {
expect(getMappedCategoryKeys().has(OTHER_CATEGORIES)).toBe(false);
});
it('раскладывает по дереву категории, которые раньше терялись', () => {
const mapped = getMappedCategoryKeys();
for (const category of ['CABINET', 'INFO_PAGES', 'MENU']) {
expect(mapped.has(category)).toBe(true);
}
});
it('незнакомая категория попадает в сборник, а не пропадает', () => {
const mapped = getMappedCategoryKeys();
// Так выглядит любая категория, появившаяся на бэкенде после этого релиза.
expect(mapped.has('SOME_FUTURE_CATEGORY')).toBe(false);
});
it('не содержит категорию дважды', () => {
const all = SETTINGS_TREE.groups
.flatMap((group) => group.children)
.flatMap((child) => child.categories)
.filter((category) => category !== OTHER_CATEGORIES);
expect(new Set(all).size).toBe(all.length);
});
});

View File

@@ -1,4 +1,4 @@
import { ThemeColors, DEFAULT_THEME_COLORS } from '../../types/theme';
import { type ThemeColors, DEFAULT_THEME_COLORS } from '../../types/theme';
// Tree sidebar types
export interface TreeSubItem {
@@ -23,6 +23,9 @@ export interface SettingsTreeConfig {
groups: TreeGroup[];
}
// Маркер пункта-сборника: подставляется вместо списка категорий.
export const OTHER_CATEGORIES = '*';
// Hierarchical settings tree — all 61 backend category keys mapped into 7 groups
export const SETTINGS_TREE: SettingsTreeConfig = {
specialItems: [
@@ -91,6 +94,8 @@ export const SETTINGS_TREE: SettingsTreeConfig = {
{ id: 'iface_widget', categories: ['TELEGRAM_WIDGET'] },
{ id: 'iface_oidc', categories: ['TELEGRAM_OIDC'] },
{ id: 'iface_skip', categories: ['SKIP'] },
{ id: 'iface_info', categories: ['INFO_PAGES'] },
{ id: 'iface_menu', categories: ['MENU'] },
{ id: 'iface_additional', categories: ['ADDITIONAL'] },
],
},
@@ -131,6 +136,7 @@ export const SETTINGS_TREE: SettingsTreeConfig = {
{ id: 'sys_core', categories: ['CORE', 'DEBUG'] },
{ id: 'sys_remnawave', categories: ['REMNAWAVE'] },
{ id: 'sys_webapi', categories: ['WEB_API', 'EXTERNAL_ADMIN'] },
{ id: 'sys_cabinet', categories: ['CABINET'] },
{ id: 'sys_webhook', categories: ['WEBHOOK'] },
{ id: 'sys_server', categories: ['SERVER_STATUS'] },
{ id: 'sys_monitoring', categories: ['MONITORING'] },
@@ -138,11 +144,29 @@ export const SETTINGS_TREE: SettingsTreeConfig = {
{ id: 'sys_backup', categories: ['BACKUP'] },
{ id: 'sys_version', categories: ['VERSION'] },
{ id: 'sys_logging', categories: ['LOG'] },
// Категории на бэкенде выводятся из имени ключа, поэтому каждая новая
// настройка может создать категорию, которой в дереве нет — и тогда она
// просто исчезала из админки (так пропала вся категория CABINET).
// Этот пункт собирает всё неразложенное, как «Прочее» в админке бота.
{ id: 'sys_other', categories: [OTHER_CATEGORIES] },
],
},
],
};
// Категории, у которых есть своё место в дереве (маркер сборника не считается).
export function getMappedCategoryKeys(): Set<string> {
const mapped = new Set<string>();
for (const group of SETTINGS_TREE.groups) {
for (const child of group.children) {
for (const category of child.categories) {
if (category !== OTHER_CATEGORIES) mapped.add(category);
}
}
}
return mapped;
}
// Helper: find which group and sub-item a backend category key belongs to
export function findTreeLocation(
categoryKey: string,

View File

@@ -151,16 +151,16 @@ export function SyncTab({
</div>
)}
{/* UUID info */}
{/* Panel identity */}
<div className="rounded-xl bg-dark-800/50 p-4">
{syncStatus?.subscription_tariff_name && (
<div className="mb-2 text-xs text-dark-500">{syncStatus.subscription_tariff_name}</div>
)}
<div className="mb-1 text-sm text-dark-400">Remnawave UUID</div>
<div className="mb-1 text-sm text-dark-400">Remnawave ID</div>
<div className="break-all font-mono text-sm text-dark-100">
{syncStatus?.remnawave_uuid ||
user.remnawave_uuid ||
t('admin.users.detail.sync.notLinked')}
{/* Remnawave 3.0.0 identifies a panel user by a numeric id, so 0 is not
a valid id but ?? would still render it — check for null explicitly. */}
{syncStatus?.remnawave_id ?? user.remnawave_id ?? t('admin.users.detail.sync.notLinked')}
</div>
</div>

View File

@@ -2306,9 +2306,16 @@
"sys_maintenance": "Maintenance",
"sys_backup": "Backup",
"sys_version": "Version",
"sys_logging": "Logging"
"sys_logging": "Logging",
"iface_info": "Legal documents",
"iface_menu": "Menu button",
"sys_cabinet": "Cabinet",
"sys_other": "Other settings"
},
"categories": {
"CABINET": "Cabinet",
"INFO_PAGES": "Legal documents and FAQ",
"MENU": "Menu button",
"TELEGRAM_WIDGET": "Telegram Login Widget",
"TELEGRAM_OIDC": "Telegram Login (OIDC)"
},

View File

@@ -1969,9 +1969,16 @@
"sys_maintenance": "نگهداری",
"sys_backup": "پشتیبان‌گیری",
"sys_version": "نسخه",
"sys_logging": "لاگ‌ها"
"sys_logging": "لاگ‌ها",
"iface_info": "اسناد حقوقی",
"iface_menu": "دکمه منو",
"sys_cabinet": "کابین",
"sys_other": "سایر تنظیمات"
},
"categories": {
"CABINET": "کابین",
"INFO_PAGES": "اسناد حقوقی و پرسش‌های متداول",
"MENU": "دکمه منو",
"TELEGRAM_WIDGET": "Telegram Login Widget",
"TELEGRAM_OIDC": "Telegram Login (OIDC)"
},

View File

@@ -2313,7 +2313,11 @@
"sys_maintenance": "Обслуживание",
"sys_backup": "Бэкап",
"sys_version": "Версия",
"sys_logging": "Логи"
"sys_logging": "Логи",
"iface_info": "Правовые документы",
"iface_menu": "Кнопка меню",
"sys_cabinet": "Кабинет",
"sys_other": "Прочие настройки"
},
"presets": {
"standard": "Стандарт",
@@ -2576,7 +2580,7 @@
"Traffic Daily Check Enabled": "Ежедневная проверка",
"Traffic Daily Check Time": "Время проверки",
"Traffic Daily Threshold Gb": "Порог в день (ГБ)",
"Traffic Excluded User Uuids": "Исключённые UUID",
"Traffic Excluded User Ids": "Исключённые ID пользователей панели",
"Traffic Fast Check Enabled": "Быстрая проверка",
"Traffic Fast Check Interval Minutes": "Интервал (мин)",
"Traffic Fast Check Threshold Gb": "Порог (ГБ)",
@@ -2769,6 +2773,9 @@
"telegramOidcClientSecret": "Client Secret"
},
"categories": {
"CABINET": "Кабинет",
"INFO_PAGES": "Правовые документы и FAQ",
"MENU": "Кнопка меню",
"PAYMENT": "Платежи",
"PAYMENT_VERIFICATION": "Верификация платежей",
"YOOKASSA": "YooKassa",

View File

@@ -2036,9 +2036,16 @@
"sys_maintenance": "维护",
"sys_backup": "备份",
"sys_version": "版本",
"sys_logging": "日志"
"sys_logging": "日志",
"iface_info": "法律文件",
"iface_menu": "菜单按钮",
"sys_cabinet": "用户中心",
"sys_other": "其他设置"
},
"categories": {
"CABINET": "用户中心",
"INFO_PAGES": "法律文件与 FAQ",
"MENU": "菜单按钮",
"TELEGRAM_WIDGET": "Telegram Login Widget",
"TELEGRAM_OIDC": "Telegram Login (OIDC)"
},

View File

@@ -2,10 +2,16 @@ import { useState, useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminSettingsApi, SettingDefinition } from '../api/adminSettings';
import { adminSettingsApi, type SettingDefinition } from '../api/adminSettings';
import { themeColorsApi } from '../api/themeColors';
import { useFavoriteSettings } from '../hooks/useFavoriteSettings';
import { SETTINGS_TREE, findTreeLocation, formatSettingKey } from '../components/admin';
import {
OTHER_CATEGORIES,
SETTINGS_TREE,
findTreeLocation,
formatSettingKey,
getMappedCategoryKeys,
} from '../components/admin';
import { usePlatform } from '../platform/hooks/usePlatform';
import { AnalyticsTab } from '../components/admin/AnalyticsTab';
import { BrandingTab } from '../components/admin/BrandingTab';
@@ -75,10 +81,18 @@ export default function AdminSettings() {
if (!activeTreeInfo || !allSettings || !Array.isArray(allSettings)) return [];
const categoryKeys = activeTreeInfo.child.categories;
// Пункт-сборник показывает всё, чему не нашлось места в дереве: категории на
// бэкенде выводятся из имени ключа, поэтому новая настройка легко создаёт
// категорию, которой здесь нет — раньше такие настройки просто исчезали.
const isOtherSection = categoryKeys.includes(OTHER_CATEGORIES);
const mappedCategories = isOtherSection ? getMappedCategoryKeys() : null;
const categoryMap = new Map<string, SettingDefinition[]>();
for (const setting of allSettings) {
if (categoryKeys.includes(setting.category.key)) {
const belongsHere = mappedCategories
? !mappedCategories.has(setting.category.key)
: categoryKeys.includes(setting.category.key);
if (belongsHere) {
// Hide tariff-dependent settings when not in tariffs mode
if (!isTariffsMode && TARIFF_MODE_SETTINGS.includes(setting.key)) continue;