From 653d683835e2c10f7690f1f71df3552c3ba9ab2f Mon Sep 17 00:00:00 2001 From: Fringg Date: Thu, 30 Jul 2026 01:58:05 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(settings):=20=D0=B2=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D1=8C=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=D0=BA=D1=83=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B8,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D1=85=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B1=D1=8B=D0=BB=D0=BE=20=D0=B2=20=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дерево разделов настроек — захардкоженный список категорий, а на бэкенде категория выводится из имени ключа. Любая настройка, создавшая новую категорию, просто исчезала из админки: настройки были в 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 тестов на само дерево. --- src/components/admin/constants.test.ts | 44 ++++++++++++++++++++++++++ src/components/admin/constants.ts | 26 ++++++++++++++- src/locales/en.json | 9 +++++- src/locales/fa.json | 9 +++++- src/locales/ru.json | 9 +++++- src/locales/zh.json | 9 +++++- src/pages/AdminSettings.tsx | 20 ++++++++++-- 7 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 src/components/admin/constants.test.ts diff --git a/src/components/admin/constants.test.ts b/src/components/admin/constants.test.ts new file mode 100644 index 0000000..a8699b5 --- /dev/null +++ b/src/components/admin/constants.test.ts @@ -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); + }); +}); diff --git a/src/components/admin/constants.ts b/src/components/admin/constants.ts index 207b361..2381c9f 100644 --- a/src/components/admin/constants.ts +++ b/src/components/admin/constants.ts @@ -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 { + const mapped = new Set(); + 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, diff --git a/src/locales/en.json b/src/locales/en.json index d29d33f..b07b36d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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)" }, diff --git a/src/locales/fa.json b/src/locales/fa.json index eebca41..cbbb785 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -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)" }, diff --git a/src/locales/ru.json b/src/locales/ru.json index 38614c1..0e1e6a1 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2313,7 +2313,11 @@ "sys_maintenance": "Обслуживание", "sys_backup": "Бэкап", "sys_version": "Версия", - "sys_logging": "Логи" + "sys_logging": "Логи", + "iface_info": "Правовые документы", + "iface_menu": "Кнопка меню", + "sys_cabinet": "Кабинет", + "sys_other": "Прочие настройки" }, "presets": { "standard": "Стандарт", @@ -2769,6 +2773,9 @@ "telegramOidcClientSecret": "Client Secret" }, "categories": { + "CABINET": "Кабинет", + "INFO_PAGES": "Правовые документы и FAQ", + "MENU": "Кнопка меню", "PAYMENT": "Платежи", "PAYMENT_VERIFICATION": "Верификация платежей", "YOOKASSA": "YooKassa", diff --git a/src/locales/zh.json b/src/locales/zh.json index 09f525e..49b202d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -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)" }, diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index db1b127..438c353 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -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(); 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; From e840ed2b60f5e525cb290ce5bddc70ef6dfe8a66 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sun, 2 Aug 2026 19:38:11 +0300 Subject: [PATCH 2/2] =?UTF-8?q?feat(admin):=20=D0=BF=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=B4=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=D1=87=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=20Remnawave=203.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/api/adminUsers.ts | 8 ++++---- src/components/admin/userDetail/SyncTab.tsx | 10 +++++----- src/locales/ru.json | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index ef47cda..abd4863 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -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; 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; diff --git a/src/components/admin/userDetail/SyncTab.tsx b/src/components/admin/userDetail/SyncTab.tsx index d88565d..f508926 100644 --- a/src/components/admin/userDetail/SyncTab.tsx +++ b/src/components/admin/userDetail/SyncTab.tsx @@ -151,16 +151,16 @@ export function SyncTab({ )} - {/* UUID info */} + {/* Panel identity */}
{syncStatus?.subscription_tariff_name && (
{syncStatus.subscription_tariff_name}
)} -
Remnawave UUID
+
Remnawave ID
- {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')}
diff --git a/src/locales/ru.json b/src/locales/ru.json index 0e1e6a1..080e88f 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2580,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": "Порог (ГБ)",