From 1bf078c51e2a1089fd76982a46953562ef98cd5a Mon Sep 17 00:00:00 2001
From: Sentiago <76674116+Ssentiago@users.noreply.github.com>
Date: Wed, 2 Sep 2026 21:22:50 +0300
Subject: [PATCH] feat(routing): add panel-only comment field to routing rules
(#6361)
Can annotate rules with a human-readable note for easier management.
The comment is stripped before sending the config to xray-core (same
pattern as the existing 'enabled' flag).
Backend: stripDisabledRules now removes 'comment' from generated config.
Frontend: input field in RuleFormModal, column in desktop table, chip
with tooltip in mobile card list. Schema and type definitions updated.
---
.../src/pages/xray/routing/RoutingTab.css | 30 +++++++++++++++++++
.../src/pages/xray/routing/RoutingTab.tsx | 1 +
.../src/pages/xray/routing/RuleCardList.tsx | 7 +++++
.../src/pages/xray/routing/RuleFormModal.tsx | 8 +++++
frontend/src/pages/xray/routing/types.ts | 1 +
.../pages/xray/routing/useRoutingColumns.tsx | 16 +++++++++-
frontend/src/schemas/routing.ts | 1 +
frontend/src/schemas/xray.ts | 1 +
internal/web/service/xray.go | 10 +++++--
9 files changed, 71 insertions(+), 4 deletions(-)
diff --git a/frontend/src/pages/xray/routing/RoutingTab.css b/frontend/src/pages/xray/routing/RoutingTab.css
index d920bafe1..6f09bf680 100644
--- a/frontend/src/pages/xray/routing/RoutingTab.css
+++ b/frontend/src/pages/xray/routing/RoutingTab.css
@@ -14,6 +14,36 @@
transition: opacity 0.15s;
}
+.rule-comment-cell {
+ display: block;
+ max-width: 140px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 12px;
+ color: var(--ant-color-text-tertiary);
+}
+
+.rule-comment {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-top: 6px;
+ padding: 2px 6px;
+ font-size: 12px;
+ color: var(--ant-color-text-tertiary);
+ border-radius: 4px;
+ background: var(--ant-color-fill-tertiary);
+ max-width: 100%;
+ overflow: hidden;
+}
+
+.rule-comment-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.drag-handle:hover {
opacity: 0.8;
}
diff --git a/frontend/src/pages/xray/routing/RoutingTab.tsx b/frontend/src/pages/xray/routing/RoutingTab.tsx
index b828dc603..845e1639f 100644
--- a/frontend/src/pages/xray/routing/RoutingTab.tsx
+++ b/frontend/src/pages/xray/routing/RoutingTab.tsx
@@ -89,6 +89,7 @@ export default function RoutingTab({
if (rule.attrs && typeof rule.attrs === 'object' && !Array.isArray(rule.attrs)) {
r.attrs = JSON.stringify(rule.attrs, null, 2);
}
+ r.comment = rule.comment || undefined;
r.outboundTag = rule.outboundTag;
r.balancerTag = rule.balancerTag;
return r;
diff --git a/frontend/src/pages/xray/routing/RuleCardList.tsx b/frontend/src/pages/xray/routing/RuleCardList.tsx
index 3b2f40107..b8e9b8a7a 100644
--- a/frontend/src/pages/xray/routing/RuleCardList.tsx
+++ b/frontend/src/pages/xray/routing/RuleCardList.tsx
@@ -181,6 +181,13 @@ export default function RuleCardList({
))}
)}
+ {rule.comment && (
+
+
+ {rule.comment}
+
+
+ )}
))
)}
diff --git a/frontend/src/pages/xray/routing/RuleFormModal.tsx b/frontend/src/pages/xray/routing/RuleFormModal.tsx
index 08c1189f3..cf46d047c 100644
--- a/frontend/src/pages/xray/routing/RuleFormModal.tsx
+++ b/frontend/src/pages/xray/routing/RuleFormModal.tsx
@@ -13,6 +13,7 @@ import { buildRemarkByTag, formatInboundTag, isApiRule } from './helpers';
export interface RoutingRule {
enabled?: boolean;
+ comment?: string;
type?: string;
domain?: string | string[];
ip?: string | string[];
@@ -42,6 +43,7 @@ interface RuleFormModalProps {
const initialForm = (): RuleFormValues => ({
enabled: true,
+ comment: '',
domain: '',
ip: '',
port: '',
@@ -104,6 +106,7 @@ export default function RuleFormModal({
if (rule) {
methods.reset({
enabled: rule.enabled !== false,
+ comment: rule.comment || '',
domain: Array.isArray(rule.domain) ? rule.domain.join(',') : rule.domain || '',
ip: Array.isArray(rule.ip) ? rule.ip.join(',') : rule.ip || '',
port: rule.port || '',
@@ -132,6 +135,7 @@ export default function RuleFormModal({
const built: Record = {
type: 'field',
enabled: v.enabled,
+ comment: v.comment,
domain: csv(v.domain),
ip: csv(v.ip),
port: v.port,
@@ -185,6 +189,10 @@ export default function RuleFormModal({
+
+
+
+
),
},
+ {
+ title: t('comment'),
+ align: 'left',
+ width: 150,
+ key: 'comment',
+ render: (_v, record) =>
+ record.comment ? (
+
+ {record.comment}
+
+ ) : (
+ —
+ ),
+ },
{
title: t('pages.inbounds.network'),
align: 'left',
diff --git a/frontend/src/schemas/routing.ts b/frontend/src/schemas/routing.ts
index f5a2d7496..c14047240 100644
--- a/frontend/src/schemas/routing.ts
+++ b/frontend/src/schemas/routing.ts
@@ -15,6 +15,7 @@ export type RuleWebhook = z.infer;
export const RuleObjectSchema = z.object({
type: z.literal('field').default('field'),
enabled: z.boolean().optional(),
+ comment: z.string().optional(),
domain: z.array(z.string()).optional(),
ip: z.array(z.string()).optional(),
port: PortValueSchema.optional(),
diff --git a/frontend/src/schemas/xray.ts b/frontend/src/schemas/xray.ts
index 984edd3c2..7b7e1c60c 100644
--- a/frontend/src/schemas/xray.ts
+++ b/frontend/src/schemas/xray.ts
@@ -110,6 +110,7 @@ export const OutboundTestResultListSchema = z.array(OutboundTestResultSchema);
export const RuleFormSchema = z.object({
enabled: z.boolean(),
+ comment: z.string(),
domain: z.string(),
ip: z.string(),
port: z.string(),
diff --git a/internal/web/service/xray.go b/internal/web/service/xray.go
index 6e11543a0..ecb1d9060 100644
--- a/internal/web/service/xray.go
+++ b/internal/web/service/xray.go
@@ -1060,9 +1060,9 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
}
// stripDisabledRules removes routing rules marked `enabled: false` from the
-// generated runtime config and strips the panel-only `enabled` key from the
-// rest, since xray-core has no such field. The internal api rule is always
-// kept (see isApiRule) so traffic stats can't be toggled off. The stored
+// generated runtime config and strips panel-only keys (`enabled`, `comment`)
+// from the rest, since xray-core has no such fields. The internal api rule is
+// always kept (see isApiRule) so traffic stats can't be toggled off. The stored
// template is untouched — only the generated config is filtered.
func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
if len(routerCfg) == 0 {
@@ -1097,6 +1097,10 @@ func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
delete(rule, "enabled")
changed = true
}
+ if _, exists := rule["comment"]; exists {
+ delete(rule, "comment")
+ changed = true
+ }
activeRules = append(activeRules, rule)
}