Browse Source

refactor(inbounds): tighten advanced JSON helpers and fix dark-mode subtitles

Collapsed repeated stream/sniffing/settings handling in InboundFormModal
into shared helpers (stampAdvancedTextFor, parseAdvancedSliceWithLabel,
compactAdvancedJson, withSaving) plus a wrapped-config factory for the
single-key editors. Cuts ~120 lines from the script section with no
behavior change.

The advanced-panel subtitle and editor-meta text used a fixed dark color
that was unreadable on the dark and ultra-dark modal backgrounds.
Switched both to opacity-on-inherit so they pick up AntD's theme-aware
foreground color, the same pattern .section-heading already uses.
MHSanaei 13 hours ago
parent
commit
5a1019534f
1 changed files with 144 additions and 267 deletions
  1. 144 267
      frontend/src/pages/inbounds/InboundFormModal.vue

+ 144 - 267
frontend/src/pages/inbounds/InboundFormModal.vue

@@ -226,20 +226,7 @@ function freshDbForm() {
 
 function primeAdvancedJson() {
   if (!inbound.value) return;
-  // Only set stream text for protocols that support it
-  if (canEnableStream.value) {
-    try {
-      advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
-    } catch (_e) { /* keep prior text */ }
-  } else {
-    advancedStreamText.value = '{}';
-  }
-  try {
-    advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
-  } catch (_e) { /* keep prior text */ }
-  try {
-    advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
-  } catch (_e) { /* keep prior text */ }
+  ['stream', 'sniffing', 'settings'].forEach(stampAdvancedTextFor);
 }
 
 watch(() => props.open, (next) => {
@@ -258,34 +245,22 @@ watch(() => props.open, (next) => {
 
 function applyAdvancedJsonToBasic() {
   if (!inbound.value) return true;
-  let parsedSettings;
-  let parsedStream;
-  let parsedSniffing;
-  try {
-    parsedSettings = advancedSettingsText.value.trim()
-      ? JSON.parse(advancedSettingsText.value)
-      : inbound.value.settings?.toJson?.();
-  } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return false; }
-  try {
-    parsedStream = advancedStreamText.value.trim()
-      ? JSON.parse(advancedStreamText.value)
-      : inbound.value.stream?.toJson?.();
-  } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return false; }
+  let settings; let streamSettings; let sniffing;
   try {
-    parsedSniffing = advancedSniffingText.value.trim()
-      ? JSON.parse(advancedSniffingText.value)
-      : inbound.value.sniffing?.toJson?.();
-  } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return false; }
+    settings = parseAdvancedSliceWithLabel(advancedSettingsText.value, settingsFallback(), 'Settings');
+    streamSettings = parseAdvancedSliceWithLabel(advancedStreamText.value, streamFallback(), 'Stream');
+    sniffing = parseAdvancedSliceWithLabel(advancedSniffingText.value, sniffingFallback(), 'Sniffing');
+  } catch (_e) { return false; }
 
   try {
     inbound.value = Inbound.fromJson({
       port: inbound.value.port,
       listen: inbound.value.listen,
       protocol: inbound.value.protocol,
-      settings: parsedSettings,
-      streamSettings: parsedStream,
+      settings,
+      streamSettings,
       tag: inbound.value.tag,
-      sniffing: parsedSniffing,
+      sniffing,
       clientStats: inbound.value.clientStats,
     });
   } catch (e) {
@@ -350,37 +325,102 @@ function unwrapWrappedObject(parsed, key) {
   return parsed;
 }
 
+const settingsFallback = () => inbound.value?.settings?.toJson?.() || {};
+const sniffingFallback = () => inbound.value?.sniffing?.toJson?.() || {};
+const streamFallback = () => inbound.value?.stream?.toJson?.() || {};
+
+const advancedTextRefs = {
+  stream: advancedStreamText,
+  sniffing: advancedSniffingText,
+  settings: advancedSettingsText,
+};
+
+function stampAdvancedTextFor(slice) {
+  const textRef = advancedTextRefs[slice];
+  if (!textRef) return;
+  if (slice === 'stream' && !canEnableStream.value) {
+    textRef.value = '{}';
+    return;
+  }
+  const obj = inbound.value?.[slice];
+  if (!obj) return;
+  try {
+    textRef.value = JSON.stringify(JSON.parse(obj.toString()), null, 2);
+  } catch (_e) { /* keep prior text */ }
+}
+
+function parseAdvancedSliceWithLabel(rawText, fallback, label) {
+  try {
+    return parseAdvancedSliceOrFallback(rawText, fallback);
+  } catch (e) {
+    message.error(`${label} JSON invalid: ${e.message}`);
+    throw e;
+  }
+}
+
+function compactAdvancedJson(raw, fallback, label) {
+  try {
+    return JSON.stringify(JSON.parse(raw || fallback));
+  } catch (e) {
+    message.error(`${label} JSON invalid: ${e.message}`);
+    throw e;
+  }
+}
+
+async function withSaving(fn) {
+  saving.value = true;
+  try { return await fn(); } finally { saving.value = false; }
+}
+
+function makeWrappedAdvancedConfig({ key, textRef, getFallback, label }) {
+  const invalid = `${label} JSON invalid`;
+  return computed({
+    get: () => {
+      if (!inbound.value) return '';
+      try {
+        const value = parseAdvancedSliceOrFallback(textRef.value, getFallback());
+        return JSON.stringify({ [key]: value }, null, 2);
+      } catch (_e) {
+        return '';
+      }
+    },
+    set: (next) => {
+      let parsed;
+      try {
+        parsed = JSON.parse(next);
+      } catch (e) {
+        message.error(`${invalid}: ${e.message}`);
+        return;
+      }
+      const unwrapped = unwrapWrappedObject(parsed, key);
+      if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
+        message.error(`${label} JSON must be an object or { ${key}: { ... } }.`);
+        return;
+      }
+      try {
+        textRef.value = JSON.stringify(unwrapped, null, 2);
+      } catch (e) {
+        message.error(`${invalid}: ${e.message}`);
+      }
+    },
+  });
+}
+
 const advancedAllConfig = computed({
   get: () => {
     if (!inbound.value) return '';
     try {
-      const settings = parseAdvancedSliceOrFallback(
-        advancedSettingsText.value,
-        inbound.value.settings?.toJson?.() || {},
-      );
-      const streamSettings = parseAdvancedSliceOrFallback(
-        advancedStreamText.value,
-        inbound.value.stream?.toJson?.() || {},
-      );
-      const sniffing = parseAdvancedSliceOrFallback(
-        advancedSniffingText.value,
-        inbound.value.sniffing?.toJson?.() || {},
-      );
-
       const result = {
         listen: inbound.value.listen,
         port: inbound.value.port,
         protocol: inbound.value.protocol,
-        settings,
-        sniffing,
+        settings: parseAdvancedSliceOrFallback(advancedSettingsText.value, settingsFallback()),
+        sniffing: parseAdvancedSliceOrFallback(advancedSniffingText.value, sniffingFallback()),
         tag: inbound.value.tag,
       };
-
-      // Only include streamSettings for protocols that support it
       if (canEnableStream.value) {
-        result.streamSettings = streamSettings;
+        result.streamSettings = parseAdvancedSliceOrFallback(advancedStreamText.value, streamFallback());
       }
-
       return JSON.stringify(result, null, 2);
     } catch (_e) {
       return '';
@@ -400,147 +440,47 @@ const advancedAllConfig = computed({
     }
 
     try {
-      if (typeof parsed.listen === 'string') {
-        inbound.value.listen = parsed.listen;
-      }
+      if (typeof parsed.listen === 'string') inbound.value.listen = parsed.listen;
       if (parsed.port !== undefined) {
-        const parsedPort = Number(parsed.port);
-        if (!Number.isNaN(parsedPort) && Number.isFinite(parsedPort)) {
-          inbound.value.port = parsedPort;
-        }
+        const port = Number(parsed.port);
+        if (Number.isFinite(port)) inbound.value.port = port;
       }
       if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
         inbound.value.protocol = parsed.protocol;
       }
-      if (typeof parsed.tag === 'string') {
-        inbound.value.tag = parsed.tag;
-      }
-
-      const existingSettings = parseAdvancedSliceOrFallback(
-        advancedSettingsText.value,
-        inbound.value?.settings?.toJson?.() || {},
-      );
-      const settings = parsed.settings ?? existingSettings;
-      const sniffing = parsed.sniffing ?? (inbound.value?.sniffing?.toJson?.() || {});
-      advancedSettingsText.value = JSON.stringify(settings, null, 2);
-      advancedSniffingText.value = JSON.stringify(sniffing, null, 2);
-
-      // Only update stream settings if protocol supports it
-      if (canEnableStream.value) {
-        const streamSettings = parsed.streamSettings ?? (inbound.value?.stream?.toJson?.() || {});
-        advancedStreamText.value = JSON.stringify(streamSettings, null, 2);
-      } else {
-        advancedStreamText.value = '{}';
-      }
+      if (typeof parsed.tag === 'string') inbound.value.tag = parsed.tag;
+
+      const existingSettings = parseAdvancedSliceOrFallback(advancedSettingsText.value, settingsFallback());
+      advancedSettingsText.value = JSON.stringify(parsed.settings ?? existingSettings, null, 2);
+      advancedSniffingText.value = JSON.stringify(parsed.sniffing ?? sniffingFallback(), null, 2);
+      advancedStreamText.value = canEnableStream.value
+        ? JSON.stringify(parsed.streamSettings ?? streamFallback(), null, 2)
+        : '{}';
     } catch (e) {
       message.error(`All JSON invalid: ${e.message}`);
     }
   },
 });
 
-const advancedSettingsConfig = computed({
-  get: () => {
-    if (!inbound.value) return '';
-    try {
-      const settings = parseAdvancedSliceOrFallback(
-        advancedSettingsText.value,
-        inbound.value.settings?.toJson?.() || {},
-      );
-      return JSON.stringify({
-        settings,
-      }, null, 2);
-    } catch (_e) {
-      return '';
-    }
-  },
-  set: (next) => {
-    let parsed;
-    try {
-      parsed = JSON.parse(next);
-    } catch (e) {
-      message.error(`Settings JSON invalid: ${e.message}`);
-      return;
-    }
-    const unwrapped = unwrapWrappedObject(parsed, 'settings');
-    if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
-      message.error('Settings JSON must be an object or { settings: { ... } }.');
-      return;
-    }
-
-    try {
-      advancedSettingsText.value = JSON.stringify(unwrapped, null, 2);
-    } catch (e) {
-      message.error(`Settings JSON invalid: ${e.message}`);
-    }
-  },
+const advancedSettingsConfig = makeWrappedAdvancedConfig({
+  key: 'settings',
+  textRef: advancedSettingsText,
+  getFallback: settingsFallback,
+  label: 'Settings',
 });
 
-const advancedSniffingConfig = computed({
-  get: () => {
-    if (!inbound.value) return '';
-    try {
-      const sniffing = parseAdvancedSliceOrFallback(
-        advancedSniffingText.value,
-        inbound.value.sniffing?.toJson?.() || {},
-      );
-      return JSON.stringify({ sniffing }, null, 2);
-    } catch (_e) {
-      return '';
-    }
-  },
-  set: (next) => {
-    let parsed;
-    try {
-      parsed = JSON.parse(next);
-    } catch (e) {
-      message.error(`Sniffing JSON invalid: ${e.message}`);
-      return;
-    }
-    const unwrapped = unwrapWrappedObject(parsed, 'sniffing');
-    if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
-      message.error('Sniffing JSON must be an object or { sniffing: { ... } }.');
-      return;
-    }
-    try {
-      advancedSniffingText.value = JSON.stringify(unwrapped, null, 2);
-    } catch (e) {
-      message.error(`Sniffing JSON invalid: ${e.message}`);
-    }
-  },
+const advancedSniffingConfig = makeWrappedAdvancedConfig({
+  key: 'sniffing',
+  textRef: advancedSniffingText,
+  getFallback: sniffingFallback,
+  label: 'Sniffing',
 });
 
-const advancedStreamConfig = computed({
-  get: () => {
-    if (!inbound.value) return '';
-    try {
-      const streamSettings = parseAdvancedSliceOrFallback(
-        advancedStreamText.value,
-        inbound.value.stream?.toJson?.() || {},
-      );
-      return JSON.stringify({ streamSettings }, null, 2);
-    } catch (_e) {
-      return '';
-    }
-  },
-  set: (next) => {
-    let parsed;
-    try {
-      parsed = JSON.parse(next);
-    } catch (e) {
-      message.error(`Stream JSON invalid: ${e.message}`);
-      return;
-    }
-    const unwrapped = unwrapWrappedObject(parsed, 'streamSettings');
-    if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
-      message.error('Stream JSON must be an object or { streamSettings: { ... } }.');
-      return;
-    }
-    try {
-      advancedStreamText.value = JSON.stringify(unwrapped, null, 2);
-    } catch (e) {
-      message.error(`Stream JSON invalid: ${e.message}`);
-    }
-  },
+const advancedStreamConfig = makeWrappedAdvancedConfig({
+  key: 'streamSettings',
+  textRef: advancedStreamText,
+  getFallback: streamFallback,
+  label: 'Stream',
 });
 
 // === Random helpers wired to the form's sync icons ==================
@@ -575,16 +515,13 @@ function regenInboundWg() {
 
 // === Reality keygen via existing API =================================
 async function genRealityKeypair() {
-  saving.value = true;
-  try {
+  await withSaving(async () => {
     const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
     if (msg?.success) {
       inbound.value.stream.reality.privateKey = msg.obj.privateKey;
       inbound.value.stream.reality.settings.publicKey = msg.obj.publicKey;
     }
-  } finally {
-    saving.value = false;
-  }
+  });
 }
 
 function clearRealityKeypair() {
@@ -594,16 +531,13 @@ function clearRealityKeypair() {
 }
 
 async function genMldsa65() {
-  saving.value = true;
-  try {
+  await withSaving(async () => {
     const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
     if (msg?.success) {
       inbound.value.stream.reality.mldsa65Seed = msg.obj.seed;
       inbound.value.stream.reality.settings.mldsa65Verify = msg.obj.verify;
     }
-  } finally {
-    saving.value = false;
-  }
+  });
 }
 
 function clearMldsa65() {
@@ -627,8 +561,7 @@ function randomizeShortIds() {
 // === ECH cert helpers ================================================
 async function getNewEchCert() {
   if (!inbound.value?.stream?.tls) return;
-  saving.value = true;
-  try {
+  await withSaving(async () => {
     const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', {
       sni: inbound.value.stream.tls.sni,
     });
@@ -636,9 +569,7 @@ async function getNewEchCert() {
       inbound.value.stream.tls.echServerKeys = msg.obj.echServerKeys;
       inbound.value.stream.tls.settings.echConfigList = msg.obj.echConfigList;
     }
-  } finally {
-    saving.value = false;
-  }
+  });
 }
 
 function clearEchCert() {
@@ -682,17 +613,14 @@ function matchesVlessAuth(block, authId) {
 
 async function getNewVlessEnc(authId) {
   if (!authId || !inbound.value?.settings) return;
-  saving.value = true;
-  try {
+  await withSaving(async () => {
     const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
     if (!msg?.success) return;
     const block = (msg.obj?.auths || []).find((a) => matchesVlessAuth(a, authId));
     if (!block) return;
     inbound.value.settings.decryption = block.decryption;
     inbound.value.settings.encryption = block.encryption;
-  } finally {
-    saving.value = false;
-  }
+  });
 }
 
 function clearVlessEnc() {
@@ -737,24 +665,16 @@ async function submit() {
   if (!inbound.value || !dbForm.value) return;
   saving.value = true;
   try {
-    // Sniffing tab is structured; stream stays JSON for unsupported
-    // transports — both go to wire as serialized JSON.
-    let streamSettings;
-    let sniffing;
-    let settings;
+    let streamSettings; let sniffing; let settings;
     try {
       streamSettings = canEnableStream.value
-        ? JSON.stringify(JSON.parse(advancedStreamText.value))
+        ? compactAdvancedJson(advancedStreamText.value, '', 'Stream')
         : (inbound.value.stream?.sockopt
           ? JSON.stringify({ sockopt: inbound.value.stream.sockopt.toJson() })
           : '');
-    } catch (e) { message.error(`Stream JSON invalid: ${e.message}`); return; }
-    try {
-      sniffing = JSON.stringify(JSON.parse(advancedSniffingText.value || inbound.value.sniffing.toString()));
-    } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; }
-    try {
-      settings = JSON.stringify(JSON.parse(advancedSettingsText.value || inbound.value.settings.toString()));
-    } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; }
+      sniffing = compactAdvancedJson(advancedSniffingText.value, inbound.value.sniffing.toString(), 'Sniffing');
+      settings = compactAdvancedJson(advancedSettingsText.value, inbound.value.settings.toString(), 'Settings');
+    } catch (_e) { return; }
 
     // The structured form mutates `inbound.stream` directly when the
     // user edits TCP/WS/gRPC/HTTPUpgrade fields, but if they touched
@@ -810,51 +730,15 @@ const okText = computed(() =>
 
 // Whenever the structured form mutates stream / sniffing / settings,
 // refresh the matching slice of the Advanced JSON tab so the user
-// always sees the live state — flipping a switch in Sniffing or
-// editing encryption in Protocol now reflects in Advanced.
-watch(
-  () => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}),
-  () => {
-    if (!inbound.value?.stream) return;
-    // Only update stream text for protocols that support it
-    if (!canEnableStream.value) {
-      advancedStreamText.value = '{}';
-      return;
-    }
-    try {
-      advancedStreamText.value = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2);
-    } catch (_e) { /* leave as is */ }
-  },
-);
-watch(
-  () => inbound.value && JSON.stringify(inbound.value.sniffing?.toJson?.() || {}),
-  () => {
-    if (!inbound.value?.sniffing) return;
-    try {
-      advancedSniffingText.value = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2);
-    } catch (_e) { /* leave as is */ }
-  },
-);
-watch(
-  () => inbound.value && JSON.stringify(inbound.value.settings?.toJson?.() || {}),
-  () => {
-    if (!inbound.value?.settings) return;
-    try {
-      advancedSettingsText.value = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2);
-    } catch (_e) { /* leave as is */ }
-  },
-);
+// always sees the live state.
+['stream', 'sniffing', 'settings'].forEach((slice) => {
+  watch(
+    () => inbound.value && JSON.stringify(inbound.value[slice]?.toJson?.() || {}),
+    () => stampAdvancedTextFor(slice),
+  );
+});
 
-// Watch protocol changes to clear stream settings for protocols that don't support it
-watch(
-  () => inbound.value?.protocol,
-  () => {
-    if (!inbound.value) return;
-    if (!canEnableStream.value) {
-      advancedStreamText.value = '{}';
-    }
-  },
-);
+watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
 </script>
 
 <template>
@@ -2321,7 +2205,7 @@ watch(
 
 .advanced-panel__subtitle {
   margin-top: 4px;
-  color: rgba(0, 0, 0, 0.6);
+  opacity: 0.7;
   line-height: 1.5;
 }
 
@@ -2335,7 +2219,7 @@ watch(
 
 .advanced-editor-meta {
   margin-bottom: 10px;
-  color: rgba(0, 0, 0, 0.65);
+  opacity: 0.75;
   line-height: 1.5;
 }
 
@@ -2350,15 +2234,8 @@ watch(
   }
 }
 
-:global(.dark) .advanced-panel__subtitle,
-:global(.dark) .advanced-editor-meta,
-:global(.ultra) .advanced-panel__subtitle,
-:global(.ultra) .advanced-editor-meta {
-  color: rgba(255, 255, 255, 0.65);
-}
-
-:global(.dark) .advanced-panel,
-:global(.ultra) .advanced-panel {
+:global(body.dark) .advanced-panel,
+:global(html[data-theme='ultra-dark']) .advanced-panel {
   border-color: rgba(255, 255, 255, 0.12);
   background: rgba(255, 255, 255, 0.03);
 }