From b10a9f1de7f920b0069b0a86099d00218ccb8850 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 13 May 2026 17:42:40 +0200 Subject: [PATCH 1/3] fix(inbounds): hide node UI when no enabled node exists The Deploy-to selector, node column, node stat row, and node filter all appeared whenever a node row existed in the DB. Local-only deployments with no nodes (or only disabled nodes) saw a dropdown that only had "Local Panel" and a filter that did nothing. useNodeList now exposes hasActive (any node with enable === true). Inbounds form and list gate node UI on hasActive instead of map size. --- frontend/src/composables/useNodeList.js | 4 +++- frontend/src/pages/inbounds/InboundFormModal.vue | 2 +- frontend/src/pages/inbounds/InboundList.vue | 7 ++++--- frontend/src/pages/inbounds/InboundsPage.vue | 5 +++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/composables/useNodeList.js b/frontend/src/composables/useNodeList.js index ca4f416d..9d817bce 100644 --- a/frontend/src/composables/useNodeList.js +++ b/frontend/src/composables/useNodeList.js @@ -36,7 +36,9 @@ export function useNodeList() { return n != null && n.enable && n.status === 'online'; } + const hasActive = computed(() => nodes.value.some((n) => n.enable)); + onMounted(refresh); - return { nodes, fetched, refresh, byId, nameFor, isOnline }; + return { nodes, fetched, refresh, byId, nameFor, isOnline, hasActive }; } diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue index 7151350c..45c30a9e 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.vue +++ b/frontend/src/pages/inbounds/InboundFormModal.vue @@ -582,7 +582,7 @@ watch( - + {{ t('pages.inbounds.localPanel') }} diff --git a/frontend/src/pages/inbounds/InboundList.vue b/frontend/src/pages/inbounds/InboundList.vue index cface361..12668cd3 100644 --- a/frontend/src/pages/inbounds/InboundList.vue +++ b/frontend/src/pages/inbounds/InboundList.vue @@ -49,6 +49,7 @@ const props = defineProps({ // Map node id -> node row, supplied by the parent page so each // inbound row can render its node name without an extra fetch. nodesById: { type: Map, default: () => new Map() }, + hasActiveNode: { type: Boolean, default: false }, }); const emit = defineEmits([ @@ -234,7 +235,7 @@ const desktopColumns = computed(() => { if (hasAnyRemark.value) { cols.push(sortableCol({ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'center', width: 60 }, 'remark')); } - if (props.nodesById.size > 0) { + if (props.hasActiveNode) { cols.push(sortableCol({ title: t('pages.inbounds.node'), key: 'node', align: 'center', width: 60 }, 'node')); } cols.push( @@ -374,7 +375,7 @@ function showQrCodeMenu(dbInbound) { {{ protocol }} - {{ node.label }} @@ -466,7 +467,7 @@ function showQrCodeMenu(dbInbound) { {{ t('pages.inbounds.port') }} {{ record.port }} -
+
{{ t('pages.inbounds.node') }} {{ t('pages.inbounds.localPanel') }} diff --git a/frontend/src/pages/inbounds/InboundsPage.vue b/frontend/src/pages/inbounds/InboundsPage.vue index 99c8084a..3ee76f51 100644 --- a/frontend/src/pages/inbounds/InboundsPage.vue +++ b/frontend/src/pages/inbounds/InboundsPage.vue @@ -66,7 +66,7 @@ useWebSocket({ const { isMobile } = useMediaQuery(); // Node list lives on the central panel; the Inbounds page consumes // the id→node map for the new "Node" column. Fetched once on mount. -const { byId: nodesById } = useNodeList(); +const { byId: nodesById, hasActive: hasActiveNode } = useNodeList(); const basePath = window.X_UI_BASE_PATH || ''; const requestUri = window.location.pathname; @@ -647,7 +647,8 @@ function onRowAction({ key, dbInbound }) { Date: Wed, 13 May 2026 19:01:12 +0200 Subject: [PATCH 2/3] fix(forms): validate JSON tabs before applying or saving InboundFormModal: switching out of the Advanced tab now parses the three JSON textareas and rebuilds the structured Inbound via Inbound.fromJson, so the Basic tab reflects what was pasted. Invalid JSON keeps the user on Advanced with a specific parse error. XrayPage: Save now parses xraySetting upfront and snaps the user back to the Advanced tab on invalid JSON instead of letting the backend reject a generic blob. --- .../src/pages/inbounds/InboundFormModal.vue | 54 ++++++++++++- frontend/src/pages/xray/XrayPage.vue | 16 +++- web/service/config.json | 78 +++++++++---------- 3 files changed, 101 insertions(+), 47 deletions(-) diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue index 45c30a9e..2c814dc2 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.vue +++ b/frontend/src/pages/inbounds/InboundFormModal.vue @@ -70,6 +70,7 @@ const inbound = ref(null); const dbForm = ref(null); const saving = ref(false); const advancedJson = ref({ stream: '', sniffing: '', settings: '' }); +const activeTabKey = ref('basic'); // Cached default cert/key paths from /panel/setting/defaultSettings — // powers the "Set default cert" button on the TLS form. const defaultCert = ref(''); @@ -241,9 +242,60 @@ watch(() => props.open, (next) => { dbForm.value = freshDbForm(); primeAdvancedJson(); } + activeTabKey.value = 'basic'; fetchDefaultCertSettings(); }); +function applyAdvancedJsonToBasic() { + if (!inbound.value) return true; + let parsedSettings; + let parsedStream; + let parsedSniffing; + try { + parsedSettings = advancedJson.value.settings.trim() + ? JSON.parse(advancedJson.value.settings) + : inbound.value.settings?.toJson?.(); + } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; } + try { + parsedStream = advancedJson.value.stream.trim() + ? JSON.parse(advancedJson.value.stream) + : inbound.value.stream?.toJson?.(); + } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; } + try { + parsedSniffing = advancedJson.value.sniffing.trim() + ? JSON.parse(advancedJson.value.sniffing) + : inbound.value.sniffing?.toJson?.(); + } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; } + + try { + inbound.value = Inbound.fromJson({ + port: inbound.value.port, + listen: inbound.value.listen, + protocol: inbound.value.protocol, + settings: parsedSettings, + streamSettings: parsedStream, + tag: inbound.value.tag, + sniffing: parsedSniffing, + clientStats: inbound.value.clientStats, + }); + } catch (e) { + message.error(`Advanced JSON: ${e.message}`); + return false; + } + return true; +} + +let isRevertingTab = false; +watch(activeTabKey, (next, prev) => { + if (isRevertingTab) { isRevertingTab = false; return; } + if (prev === 'advanced' && next !== 'advanced') { + if (!applyAdvancedJsonToBasic()) { + isRevertingTab = true; + activeTabKey.value = 'advanced'; + } + } +}); + // In add mode, switching protocol restamps settings + re-syncs port. function onProtocolChange(next) { if (props.mode === 'edit' || !inbound.value) return; @@ -572,7 +624,7 @@ watch( + + + + + + {{ t('pages.settings.subTitle') }} diff --git a/sub/subService.go b/sub/subService.go index 887cf87c..4679165c 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -28,6 +28,7 @@ type SubService struct { showInfo bool remarkModel string datepicker string + emailInRemark bool inboundService service.InboundService settingService service.SettingService // nodesByID is populated per request from the Node table so @@ -76,6 +77,12 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C if err != nil { s.datepicker = "gregorian" } + + s.emailInRemark, err = s.settingService.GetSubEmailInRemark() + if err != nil { + s.emailInRemark = true + } + seenEmails := make(map[string]struct{}) for _, inbound := range inbounds { clients, err := s.inboundService.GetClients(inbound) @@ -886,7 +893,7 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin 'e': "", 'o': "", } - if len(email) > 0 { + if len(email) > 0 && s.emailInRemark { orders['e'] = email } if len(inbound.Remark) > 0 { diff --git a/web/entity/entity.go b/web/entity/entity.go index b1f02a37..bc4ce5a1 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -75,6 +75,7 @@ type AllSetting struct { RestartXrayOnClientDisable bool `json:"restartXrayOnClientDisable" form:"restartXrayOnClientDisable"` // Restart Xray when clients are auto-disabled by expiry/traffic limit SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` // Encrypt subscription responses SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` // Show client information in subscriptions + SubEmailInRemark bool `json:"subEmailInRemark" form:"subEmailInRemark"` // Include email in subscription remark/name SubURI string `json:"subURI" form:"subURI"` // Subscription server URI SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` // Path for JSON subscription endpoint SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` // JSON subscription server URI diff --git a/web/service/setting.go b/web/service/setting.go index a6cbffdb..1d5173a6 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -70,6 +70,7 @@ var defaultValueMap = map[string]string{ "subUpdates": "12", "subEncrypt": "true", "subShowInfo": "true", + "subEmailInRemark": "true", "subURI": "", "subJsonPath": "/json/", "subJsonURI": "", @@ -592,6 +593,10 @@ func (s *SettingService) GetSubShowInfo() (bool, error) { return s.getBool("subShowInfo") } +func (s *SettingService) GetSubEmailInRemark() (bool, error) { + return s.getBool("subEmailInRemark") +} + func (s *SettingService) GetPageSize() (int, error) { return s.getInt("pageSize") } diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index cc0c1a51..e73eee06 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -547,6 +547,8 @@ "subEncryptDesc": "المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64.", "subShowInfo": "اظهر معلومات الاستخدام", "subShowInfoDesc": "هيظهر الترافيك المتبقي والتاريخ في تطبيقات العملاء.", + "subEmailInRemark": "تضمين البريد الإلكتروني في الاسم", + "subEmailInRemarkDesc": "تضمين بريد العميل الإلكتروني في اسم ملف تعريف الاشتراك.", "subURI": "مسار البروكسي العكسي", "subURIDesc": "مسار URI لرابط الاشتراك عشان تستخدمه ورا البروكسي.", "externalTrafficInformEnable": "تنبيه الترافيك الخارجي", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index bf3889d4..1e8d01c6 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -547,6 +547,8 @@ "subEncryptDesc": "The returned content of subscription service will be Base64 encoded.", "subShowInfo": "Show Usage Info", "subShowInfoDesc": "The remaining traffic and date will be displayed in the client apps.", + "subEmailInRemark": "Include Email in Name", + "subEmailInRemarkDesc": "Include the client email in the subscription profile name.", "subURI": "Reverse Proxy URI", "subURIDesc": "The URI path of the subscription URL for use behind proxies.", "externalTrafficInformEnable": "External Traffic Inform", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index 9dcc100d..c02b4de4 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -547,6 +547,8 @@ "subEncryptDesc": "Encriptar las configuraciones devueltas en la suscripción.", "subShowInfo": "Mostrar información de uso", "subShowInfoDesc": "Mostrar tráfico restante y fecha después del nombre de configuración.", + "subEmailInRemark": "Incluir Email en el nombre", + "subEmailInRemarkDesc": "Incluir el correo del cliente en el nombre del perfil de suscripción.", "subURI": "URI de proxy inverso", "externalTrafficInformEnable": "Informe de tráfico externo", "externalTrafficInformEnableDesc": "Informar a la API externa sobre cada actualización de tráfico.", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index da768679..f2052f66 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -547,6 +547,8 @@ "subEncryptDesc": "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه", "subShowInfo": "نمایش اطلاعات مصرف", "subShowInfoDesc": "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد", + "subEmailInRemark": "گنجاندن ایمیل در نام", + "subEmailInRemarkDesc": "ایمیل کاربر در نام پروفایل اشتراک گنجانده می‌شود.", "subURI": "پروکسی معکوس URI مسیر", "subURIDesc": "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر", "externalTrafficInformEnable": "اطلاع رسانی خارجی مصرف ترافیک", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index a30f14af..095984e3 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -547,6 +547,8 @@ "subEncryptDesc": "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64.", "subShowInfo": "Tampilkan Info Penggunaan", "subShowInfoDesc": "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien.", + "subEmailInRemark": "Sertakan Email dalam Nama", + "subEmailInRemarkDesc": "Sertakan email klien dalam nama profil langganan.", "subURI": "URI Proxy Terbalik", "subURIDesc": "Path URI dari URL langganan untuk digunakan di belakang proxy.", "externalTrafficInformEnable": "Informasikan API eksternal pada setiap pembaruan lalu lintas.", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index d952bfd2..93c2dbbf 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -547,6 +547,8 @@ "subEncryptDesc": "サブスクリプションサービスが返す内容をBase64エンコードする", "subShowInfo": "利用情報を表示", "subShowInfoDesc": "クライアントアプリで残りのトラフィックと日付情報を表示する", + "subEmailInRemark": "名前にメールを含める", + "subEmailInRemarkDesc": "サブスクリプションプロファイル名にクライアントのメールアドレスを含めます。", "subURI": "リバースプロキシURI", "subURIDesc": "プロキシ後ろのサブスクリプションURLのURIパスに使用する", "externalTrafficInformEnable": "外部トラフィック情報", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index bcbf8efc..fa56b477 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -547,6 +547,8 @@ "subEncryptDesc": "O conteúdo retornado pelo serviço de assinatura será codificado em Base64.", "subShowInfo": "Mostrar Informações de Uso", "subShowInfoDesc": "O tráfego restante e a data serão exibidos nos aplicativos de cliente.", + "subEmailInRemark": "Incluir Email no nome", + "subEmailInRemarkDesc": "Incluir o email do cliente no nome do perfil de assinatura.", "subURI": "URI de Proxy Reverso", "subURIDesc": "O caminho URI da URL de assinatura para uso por trás de proxies.", "externalTrafficInformEnable": "Informações de tráfego externo", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index f5ac4b2d..f6f59287 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -547,6 +547,8 @@ "subEncryptDesc": "Шифровать возвращенные конфиги в подписке", "subShowInfo": "Показать информацию об использовании", "subShowInfoDesc": "Отображать остаток трафика и дату окончания после имени конфигурации", + "subEmailInRemark": "Включать Email в название", + "subEmailInRemarkDesc": "Включать email клиента в название профиля подписки.", "subURI": "URI обратного прокси", "subURIDesc": "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами", "externalTrafficInformEnable": "Информация о внешнем трафике", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index fa7684ae..84064211 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -547,6 +547,8 @@ "subEncryptDesc": "Abonelik hizmetinin döndürülen içeriği Base64 ile şifrelenir.", "subShowInfo": "Kullanım Bilgisini Göster", "subShowInfoDesc": "Kalan trafik ve tarih müşteri uygulamalarında görüntülenir.", + "subEmailInRemark": "Ada Email Ekle", + "subEmailInRemarkDesc": "Abonelik profil adına istemcinin e-postasını dahil edin.", "subURI": "Ters Proxy URI", "subURIDesc": "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu.", "externalTrafficInformEnable": "Harici Trafik Bilgisi", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 16cce701..150decb5 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -547,6 +547,8 @@ "subEncryptDesc": "Повернений вміст послуги підписки матиме кодування Base64.", "subShowInfo": "Показати інформацію про використання", "subShowInfoDesc": "Залишок трафіку та дата відображатимуться в клієнтських програмах.", + "subEmailInRemark": "Включати Email до назви", + "subEmailInRemarkDesc": "Включати email клієнта до назви профілю підписки.", "subURI": "URI зворотного проксі", "subURIDesc": "URI до URL-адреси підписки для використання за проксі.", "externalTrafficInformEnable": "Інформація про зовнішній трафік", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index 82f457b5..9eaef4f2 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -547,6 +547,8 @@ "subEncryptDesc": "Mã hóa các cấu hình được trả về trong gói đăng ký", "subShowInfo": "Hiển thị thông tin sử dụng", "subShowInfoDesc": "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình", + "subEmailInRemark": "Thêm Email vào tên", + "subEmailInRemarkDesc": "Thêm email của client vào tên hồ sơ đăng ký.", "subURI": "URI proxy trung gian", "subURIDesc": "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian", "externalTrafficInformEnable": "Thông báo giao thông bên ngoài", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 821d6df1..beb78ccb 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -547,6 +547,8 @@ "subEncryptDesc": "订阅服务返回的内容将采用 Base64 编码", "subShowInfo": "显示使用信息", "subShowInfoDesc": "客户端应用中将显示剩余流量和日期信息", + "subEmailInRemark": "在名称中包含邮箱", + "subEmailInRemarkDesc": "在订阅配置名称中包含客户端邮箱。", "subURI": "反向代理 URI", "subURIDesc": "用于代理后面的订阅 URL 的 URI 路径", "externalTrafficInformEnable": "外部交通通知", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index d410080f..36b60a67 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -547,6 +547,8 @@ "subEncryptDesc": "訂閱服務返回的內容將採用 Base64 編碼", "subShowInfo": "顯示使用資訊", "subShowInfoDesc": "客戶端應用中將顯示剩餘流量和日期資訊", + "subEmailInRemark": "在名稱中包含郵箱", + "subEmailInRemarkDesc": "在訂閱配置名稱中包含客戶端郵箱。", "subURI": "反向代理 URI", "subURIDesc": "用於代理後面的訂閱 URL 的 URI 路徑", "externalTrafficInformEnable": "外部交通通知",