From 5f98a2db72fc130b2d65d4fb2c53d064021cc4fc Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 17 May 2026 23:41:07 +0200 Subject: [PATCH] feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/xray/BalancerFormModal.vue | 14 ++---------- frontend/src/pages/xray/BalancersTab.vue | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/frontend/src/pages/xray/BalancerFormModal.vue b/frontend/src/pages/xray/BalancerFormModal.vue index a0b8b032..6e798f07 100644 --- a/frontend/src/pages/xray/BalancerFormModal.vue +++ b/frontend/src/pages/xray/BalancerFormModal.vue @@ -61,16 +61,6 @@ const isValid = computed( () => !tagEmpty.value && !duplicateTag.value && !emptySelector.value, ); -const fallbackSupported = computed( - () => form.strategy === 'leastPing' || form.strategy === 'leastLoad', -); - -watch(() => form.strategy, (next) => { - if (next !== 'leastPing' && next !== 'leastLoad') { - form.fallbackTag = ''; - } -}); - const tagValidateStatus = computed(() => { if (tagEmpty.value) return 'error'; if (duplicateTag.value) return 'warning'; @@ -121,8 +111,8 @@ const okText = computed(() => - - + + {{ tag || `(${t('none')})` }} diff --git a/frontend/src/pages/xray/BalancersTab.vue b/frontend/src/pages/xray/BalancersTab.vue index 255ed108..1f4fe620 100644 --- a/frontend/src/pages/xray/BalancersTab.vue +++ b/frontend/src/pages/xray/BalancersTab.vue @@ -133,23 +133,25 @@ function syncObservatories() { delete t.observatory; } - const leastLoads = balancers.filter((b) => b.strategy?.type === 'leastLoad'); - if (leastLoads.length > 0) { + const burstFeeders = balancers.filter((b) => { + const type = b.strategy?.type || 'random'; + return type === 'leastLoad' || type === 'random' || type === 'roundRobin'; + }); + if (burstFeeders.length > 0) { if (!t.burstObservatory) { t.burstObservatory = JSON.parse(JSON.stringify(DEFAULT_BURST_OBSERVATORY)); } - t.burstObservatory.subjectSelector = collectSelectors(leastLoads); + t.burstObservatory.subjectSelector = collectSelectors(burstFeeders); } else { delete t.burstObservatory; } } function buildWireBalancer(form) { - const supportsFallback = form.strategy === 'leastPing' || form.strategy === 'leastLoad'; const out = { tag: form.tag, selector: [...form.selector], - fallbackTag: supportsFallback ? form.fallbackTag : '', + fallbackTag: form.fallbackTag || '', }; if (form.strategy && form.strategy !== 'random') { out.strategy = { type: form.strategy }; @@ -218,11 +220,11 @@ const showObsEditor = computed(() => hasObservatory.value || hasBurstObservatory const obsView = ref('observatory'); -// Keep the radio selection valid as observatories appear/disappear — -// e.g. deleting the last leastPing balancer should flip the editor to -// the burstObservatory pane instead of leaving it pointing at the -// (now-removed) observatory key. -watch(showObsEditor, () => { +// Watch each flag individually — watching showObsEditor (OR of the two) +// misses the case where one observatory swaps for the other in the same +// tick, leaving obsView pointing at a now-deleted key and JsonEditor +// trying to parse an empty string. +watch([hasObservatory, hasBurstObservatory], () => { if (obsView.value === 'observatory' && !hasObservatory.value && hasBurstObservatory.value) { obsView.value = 'burstObservatory'; } else if (obsView.value === 'burstObservatory' && !hasBurstObservatory.value && hasObservatory.value) {