Browse Source

BurstObservatory & Observatory

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 year ago
parent
commit
1e2be10429
2 changed files with 255 additions and 133 deletions
  1. 1 1
      web/html/xui/form/client.html
  2. 254 132
      web/html/xui/xray.html

+ 1 - 1
web/html/xui/form/client.html

@@ -75,7 +75,7 @@
         </template>
         <a-input-number v-model="client.limitIp" min="0"></a-input-number>
     </a-form-item>
-    <a-form-item v-if="app.ipLimitEnable && client.email && isEdit">
+    <a-form-item v-if="client.limitIp > 0 && client.email && isEdit">
         <template slot="label">
             <a-tooltip>
                 <template slot="title">

+ 254 - 132
web/html/xui/xray.html

@@ -75,7 +75,7 @@
                 <a-space direction="vertical">
                     <a-card hoverable style="margin-bottom: .5rem;">
                         <a-row style="display: flex; flex-wrap: wrap; align-items: center;">
-                            <a-col :xs="24" :sm="10" style="padding: 4px;">
+                            <a-col :xs="24" :sm="8" style="padding: 4px;">
                                 <a-space direction="horizontal">
                                     <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.xray.save" }}</a-button>
                                     <a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">{{ i18n "pages.xray.restart" }}</a-button>
@@ -89,7 +89,7 @@
                                     </a-popover>
                                 </a-space>
                             </a-col>
-                            <a-col :xs="24" :sm="14">
+                            <a-col :xs="24" :sm="16">
                                 <template>
                                     <div>
                                         <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
@@ -103,13 +103,10 @@
                             </a-col>
                         </a-row>
                     </a-card>
-                    <a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1"
-                    @change="(activeKey) => { if(activeKey == 'tpl-advanced') this.changeCode(); }"
+                    <a-tabs class="ant-card-dark-box-nohover" default-active-key="1"
+                    @change="(activeKey) => { this.changePage(activeKey); }"
                     :class="themeSwitcher.currentTheme">
-                        <a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}'>
-                            <a-space direction="horizontal" style="padding: 20px 20px">
-                                <a-button type="danger" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
-                            </a-space>
+                        <a-tab-pane key="tpl-basic" tab='{{ i18n "pages.xray.basicTemplate"}}' style="padding-top: 20px;">
                             <a-collapse>
                                 <a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
                                     <a-row :xs="24" :sm="24" :lg="12">
@@ -284,11 +281,14 @@
                                 </template>
                                 <a-button v-else type="primary" icon="cloud" style="margin: 15px 20px;" @click="showWarp()">WARP</a-button>
                                 </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
+                                    <a-space direction="horizontal" style="padding: 0 20px">
+                                        <a-button type="danger" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
+                                    </a-space>
+                                </a-collapse-panel>
                             </a-collapse>
                         </a-tab-pane>
-                        <a-tab-pane key="tpl-2" tab='{{ i18n "pages.xray.Routings"}}' style="padding-top: 20px;">
-                            <a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
-                            message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon></a-alert>
+                        <a-tab-pane key="tpl-routing" tab='{{ i18n "pages.xray.Routings"}}' style="padding-top: 20px;">
                             <a-button type="primary" icon="plus" @click="addRule">{{ i18n "pages.xray.rules.add" }}</a-button>
                             <a-table-sortable :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered
                                 :row-key="r => r.key"
@@ -410,7 +410,7 @@
                                 </template>
                             </a-table-sortable>
                         </a-tab-pane>
-                        <a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
+                        <a-tab-pane key="tpl-outbound" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
                             <a-row>
                                 <a-col :xs="12" :sm="12" :lg="12">
                                     <a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n
@@ -478,7 +478,7 @@
                                 </template>
                             </a-table>
                         </a-tab-pane>
-                        <a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.outbound.reverse"}}' style="padding-top: 20px;" force-render="true">
+                        <a-tab-pane key="tpl-reverse" tab='{{ i18n "pages.xray.outbound.reverse"}}' style="padding-top: 20px;" force-render="true">
                             <a-button type="primary" icon="plus" @click="addReverse()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addReverse" }}</a-button>
                             <a-table :columns="reverseColumns" bordered v-if="reverseData.length>0"
                             :row-key="r => r.key"
@@ -506,9 +506,7 @@
                                 </template>
                             </a-table>
                         </a-tab-pane>
-                        <a-tab-pane key="tpl-5" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
-                            <a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
-                            message='{{ i18n "pages.xray.balancer.balancerDesc" }}' show-icon></a-alert>
+                        <a-tab-pane key="tpl-balancer" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
                             <a-button type="primary" icon="plus" @click="addBalancer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.balancer.addBalancer"}}</a-button>
                             <a-table :columns="balancerColumns" bordered v-if="balancersData.length>0"
                             :row-key="r => r.key"
@@ -544,8 +542,19 @@
                                     <a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
                                 </template>
                             </a-table>
+                            <a-radio-group
+                                v-model="obsSettings"
+                                v-if="observatoryEnable || burstObservatoryEnable"
+                                @change="changeObsCode"
+                                button-style="solid"
+                                style="margin: 10px 0;"
+                                :size="isMobile ? 'small' : ''">
+                                <a-radio-button value="observatory" v-if="observatoryEnable">Observatory</a-radio-button>
+                                <a-radio-button value="burstObservatory" v-if="burstObservatoryEnable">Burst Observatory</a-radio-button>
+                            </a-radio-group>
+                            <textarea style="position:absolute; left: -800px;" id="obsSetting"></textarea>
                         </a-tab-pane>
-                        <a-tab-pane key="tpl-6" tab='DNS' style="padding-top: 20px;" force-render="true">
+                        <a-tab-pane key="tpl-dns" tab='DNS' style="padding-top: 20px;" force-render="true">
                             <setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}' desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item>
                             <template v-if="enableDNS">
                                 <setting-list-item type="text" title='{{ i18n "pages.xray.dns.tag" }}' desc='{{ i18n "pages.xray.dns.tagDesc" }}' v-model="dnsTag"></setting-list-item>
@@ -696,6 +705,13 @@
         { title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 },
     ];
 
+    const balancerColumns = [
+        { title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
+        { title: '{{ i18n "pages.xray.balancer.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
+        { title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' }},
+        { title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' }},
+    ];
+
     const dnsColumns = [
         { title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
         { title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
@@ -708,13 +724,6 @@
         { title: '{{ i18n "pages.xray.fakedns.poolSize"}}', dataIndex: 'poolSize', align: 'center', width: 50 },
     ];
 
-    const balancerColumns = [
-        { title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
-        { title: '{{ i18n "pages.xray.balancer.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
-        { title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' }},
-        { title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' }},
-    ];
-
     const app = new Vue({
         delimiters: ['[[', ']]'],
         el: '#app',
@@ -733,6 +742,7 @@
             showAlert: false,
             isMobile: window.innerWidth <= 768,
             advSettings: 'xraySetting',
+            obsSettings: '',
             cm: null,
             cmOptions: {
                 lineNumbers: true,
@@ -827,6 +837,22 @@
                     ],
                     "queryStrategy": "UseIP"
                 },
+            },
+            defaultObservatory: {
+                subjectSelector: [],
+                probeURL: "http://www.google.com/gen_204",
+                probeInterval: "10m",
+                enableConcurrency: true
+            },
+            defaultBurstObservatory: {
+                subjectSelector: [],
+                pingConfig: {
+                    destination: "http://www.google.com/gen_204",
+                    interval: "30m",
+                    connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204",
+                    timeout: "10s",
+                    sampling: 2
+                }
             }
         },
         methods: {
@@ -927,6 +953,10 @@
                     this.saveBtnDisable = true;
                 }
             },
+            changePage(pageKey) {
+                if(pageKey == 'tpl-advanced') this.changeCode();
+                if(pageKey == 'tpl-balancer') this.changeObsCode();
+            },
             syncRulesWithOutbound(tag, setting) {
                 const newTemplateSettings = this.templateSettings;
                 const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
@@ -1009,6 +1039,23 @@
                     }
                 });
             },
+            changeObsCode() {
+                if (this.obsSettings == ''){
+                    return
+                }
+                if(this.cm != null) {
+                    this.cm.toTextArea();
+                }
+                textAreaObj = document.getElementById('obsSetting');
+                textAreaObj.value = this[this.obsSettings];
+                this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
+                this.cm.on('change',editor => {
+                    value = editor.getValue();
+                    if(this.isJsonString(value)){
+                        this[this.obsSettings] = value;
+                    }
+                });
+            },
             isJsonString(str) {
                 try {
                     JSON.parse(str);
@@ -1087,6 +1134,91 @@
                 outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
                 this.outboundSettings = JSON.stringify(outbounds);
             },
+            addReverse(){
+                reverseModal.show({
+                    title: '{{ i18n "pages.xray.outbound.addReverse"}}',
+                    okText: '{{ i18n "pages.xray.outbound.addReverse" }}',
+                    confirm: (reverse, rules) => {
+                        reverseModal.loading();
+                        if(reverse.tag.length > 0){
+                            newTemplateSettings = this.templateSettings;
+                            if(newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};
+                            if(newTemplateSettings.reverse[reverse.type+'s']  == undefined) newTemplateSettings.reverse[reverse.type+'s'] = [];
+                            newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
+                            this.templateSettings = newTemplateSettings;
+
+                            // Add related rules
+                            this.templateSettings.routing.rules.push(...rules);
+                            this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
+                        }
+                        reverseModal.close();
+                    },
+                    isEdit: false
+                });
+            },
+            editReverse(index){
+                if(this.reverseData[index].type == "bridge") {
+                    oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this.reverseData[index].tag);
+                } else {
+                    oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this.reverseData[index].tag);
+                }
+                reverseModal.show({
+                    title: '{{ i18n "pages.xray.outbound.editReverse"}} ' + (index+1),
+                    reverse: this.reverseData[index],
+                    rules: oldRules,
+                    confirm: (reverse, rules) => {
+                        reverseModal.loading();
+                        if(reverse.tag.length > 0){
+                            oldData = this.reverseData[index];
+                            newTemplateSettings = this.templateSettings;
+                            oldReverseIndex = newTemplateSettings.reverse[oldData.type+'s'].findIndex(rs => rs.tag == oldData.tag);
+                            oldRuleIndex0 = oldRules.length>0 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[0])) : -1;
+                            oldRuleIndex1 = oldRules.length==2 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[1])) : -1;
+                            if(oldData.type == reverse.type){
+                                newTemplateSettings.reverse[oldData.type + 's'][oldReverseIndex] = { tag: reverse.tag, domain: reverse.domain };
+                            } else {
+                                newTemplateSettings.reverse[oldData.type+'s'].splice(oldReverseIndex,1);
+                                // delete empty object
+                                if(newTemplateSettings.reverse[oldData.type+'s'].length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s');
+                                // add other type of reverse if it is not exist
+                                if(!newTemplateSettings.reverse[reverse.type+'s']) newTemplateSettings.reverse[reverse.type+'s'] = [];
+                                newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
+                            }
+                            this.templateSettings = newTemplateSettings;
+
+                            // Adjust Rules
+                            newRules = this.templateSettings.routing.rules;
+                            oldRuleIndex0 != -1 ? newRules[oldRuleIndex0] = rules[0] : newRules.push(rules[0]);
+                            oldRuleIndex1 != -1 ? newRules[oldRuleIndex1] = rules[1] : newRules.push(rules[1]);
+                            this.routingRuleSettings = JSON.stringify(newRules);
+                        }
+                        reverseModal.close();
+                    },
+                    isEdit: true
+                });
+            },
+            deleteReverse(index){
+                oldData = this.reverseData[index];
+                newTemplateSettings = this.templateSettings;
+                reverseTypeObj = newTemplateSettings.reverse[oldData.type+'s'];
+                realIndex = reverseTypeObj.findIndex(r => r.tag==oldData.tag && r.domain==oldData.domain);
+                newTemplateSettings.reverse[oldData.type+'s'].splice(realIndex,1);
+
+                // delete empty objects
+                if(reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s');
+                if(Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings, 'reverse');
+
+                // delete related routing rules
+                newRules = newTemplateSettings.routing.rules;
+                if(oldData.type == "bridge"){
+                    newRules = newTemplateSettings.routing.rules.filter(r => !( r.inboundTag && r.inboundTag.length == 1 && r.inboundTag[0] == oldData.tag));
+                } else if(oldData.type == "portal"){
+                    newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
+                }
+                newTemplateSettings.routing.rules = newRules;
+
+                this.templateSettings = newTemplateSettings;
+            },
             async refreshOutboundTraffic() {
                 if (!this.refreshing) {
                     this.refreshing = true;
@@ -1133,14 +1265,27 @@
                             'tag': balancer.tag,
                             'selector': balancer.selector
                         };
-                        if (balancer.strategy === 'roundRobin' || balancer.strategy === 'leastload' || balancer.strategy === 'leastping') {
+                        if (balancer.strategy && balancer.strategy != 'random') {
                             tmpBalancer.strategy = {
                                 'type': balancer.strategy
                             };
+                            if (balancer.strategy == 'leastPing'){
+                                if (!newTemplateSettings.observatory)
+                                    newTemplateSettings.observatory = this.defaultObservatory;
+                                if (!newTemplateSettings.observatory.subjectSelector.includes(balancer.tag))
+                                    newTemplateSettings.observatory.subjectSelector.push(balancer.tag);
+                            }
+                            if (balancer.strategy == 'leastLoad'){
+                                if (!newTemplateSettings.burstObservatory)
+                                    newTemplateSettings.burstObservatory = this.defaultBurstObservatory;
+                                if (!newTemplateSettings.burstObservatory.subjectSelector.includes(balancer.tag))
+                                    newTemplateSettings.burstObservatory.subjectSelector.push(balancer.tag);
+                            }
                         }
                         newTemplateSettings.routing.balancers.push(tmpBalancer);
                         this.templateSettings = newTemplateSettings;
                         balancerModal.close();
+                        this.changeObsCode();
                     },
                     isEdit: false
                 });
@@ -1160,10 +1305,31 @@
                             'tag': balancer.tag,
                             'selector': balancer.selector
                         };
-                        if (balancer.strategy === 'roundRobin' || balancer.strategy === 'leastload' || balancer.strategy === 'leastping') {
+
+                        // Remove old tag
+                        if (newTemplateSettings.observatory){
+                            newTemplateSettings.observatory.subjectSelector = newTemplateSettings.observatory.subjectSelector.filter(s => s != oldTag);
+                        }
+                        if (newTemplateSettings.burstObservatory){
+                            newTemplateSettings.burstObservatory.subjectSelector = newTemplateSettings.burstObservatory.subjectSelector.filter(s => s != oldTag);
+                        }
+
+                        if (balancer.strategy && balancer.strategy != 'random') {
                             tmpBalancer.strategy = {
                                 'type': balancer.strategy
                             };
+                            if (balancer.strategy == 'leastPing'){
+                                if (!newTemplateSettings.observatory)
+                                    newTemplateSettings.observatory = this.defaultObservatory;
+                                if (!newTemplateSettings.observatory.subjectSelector.includes(balancer.tag))
+                                    newTemplateSettings.observatory.subjectSelector.push(balancer.tag);
+                            }
+                            if (balancer.strategy == 'leastLoad'){
+                                if (!newTemplateSettings.burstObservatory)
+                                    newTemplateSettings.burstObservatory = this.defaultBurstObservatory;
+                                if (!newTemplateSettings.burstObservatory.subjectSelector.includes(balancer.tag))
+                                    newTemplateSettings.burstObservatory.subjectSelector.push(balancer.tag);
+                            }
                         }
 
                         newTemplateSettings.routing.balancers[index] = tmpBalancer;
@@ -1177,6 +1343,7 @@
                         }
                         this.templateSettings = newTemplateSettings;
                         balancerModal.close();
+                        this.changeObsCode();
                     },
                     isEdit: true
                 });
@@ -1191,6 +1358,14 @@
                 let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag === removedBalancer.tag);
                 newTemplateSettings.routing.balancers.splice(realIndex, 1);
 
+                // Remove tag from observatory
+                if (newTemplateSettings.observatory){
+                    newTemplateSettings.observatory.subjectSelector = newTemplateSettings.observatory.subjectSelector.filter(s => s != removedBalancer.tag);
+                }
+                if (newTemplateSettings.burstObservatory){
+                    newTemplateSettings.burstObservatory.subjectSelector = newTemplateSettings.burstObservatory.subjectSelector.filter(s => s != removedBalancer.tag);
+                }
+
                 // Remove related routing rules
                 newTemplateSettings.routing.rules.forEach((rule) => {
                     if (rule.balancerTag === removedBalancer.tag) {
@@ -1203,91 +1378,7 @@
                     delete newTemplateSettings.routing.balancers;
                 }
                 this.templateSettings = newTemplateSettings;
-            },
-            addReverse(){
-                reverseModal.show({
-                    title: '{{ i18n "pages.xray.outbound.addReverse"}}',
-                    okText: '{{ i18n "pages.xray.outbound.addReverse" }}',
-                    confirm: (reverse, rules) => {
-                        reverseModal.loading();
-                        if(reverse.tag.length > 0){
-                            newTemplateSettings = this.templateSettings;
-                            if(newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};
-                            if(newTemplateSettings.reverse[reverse.type+'s']  == undefined) newTemplateSettings.reverse[reverse.type+'s'] = [];
-                            newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
-                            this.templateSettings = newTemplateSettings;
-
-                            // Add related rules
-                            this.templateSettings.routing.rules.push(...rules);
-                            this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
-                        }
-                        reverseModal.close();
-                    },
-                    isEdit: false
-                });
-            },
-            editReverse(index){
-                if(this.reverseData[index].type == "bridge") {
-                    oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this.reverseData[index].tag);
-                } else {
-                    oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this.reverseData[index].tag);
-                }
-                reverseModal.show({
-                    title: '{{ i18n "pages.xray.outbound.editReverse"}} ' + (index+1),
-                    reverse: this.reverseData[index],
-                    rules: oldRules,
-                    confirm: (reverse, rules) => {
-                        reverseModal.loading();
-                        if(reverse.tag.length > 0){
-                            oldData = this.reverseData[index];
-                            newTemplateSettings = this.templateSettings;
-                            oldReverseIndex = newTemplateSettings.reverse[oldData.type+'s'].findIndex(rs => rs.tag == oldData.tag);
-                            oldRuleIndex0 = oldRules.length>0 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[0])) : -1;
-                            oldRuleIndex1 = oldRules.length==2 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[1])) : -1;
-                            if(oldData.type == reverse.type){
-                                newTemplateSettings.reverse[oldData.type + 's'][oldReverseIndex] = { tag: reverse.tag, domain: reverse.domain };
-                            } else {
-                                newTemplateSettings.reverse[oldData.type+'s'].splice(oldReverseIndex,1);
-                                // delete empty object
-                                if(newTemplateSettings.reverse[oldData.type+'s'].length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s');
-                                // add other type of reverse if it is not exist
-                                if(!newTemplateSettings.reverse[reverse.type+'s']) newTemplateSettings.reverse[reverse.type+'s'] = [];
-                                newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
-                            }
-                            this.templateSettings = newTemplateSettings;
-
-                            // Adjust Rules
-                            newRules = this.templateSettings.routing.rules;
-                            oldRuleIndex0 != -1 ? newRules[oldRuleIndex0] = rules[0] : newRules.push(rules[0]);
-                            oldRuleIndex1 != -1 ? newRules[oldRuleIndex1] = rules[1] : newRules.push(rules[1]);
-                            this.routingRuleSettings = JSON.stringify(newRules);
-                        }
-                        reverseModal.close();
-                    },
-                    isEdit: true
-                });
-            },
-            deleteReverse(index){
-                oldData = this.reverseData[index];
-                newTemplateSettings = this.templateSettings;
-                reverseTypeObj = newTemplateSettings.reverse[oldData.type+'s'];
-                realIndex = reverseTypeObj.findIndex(r => r.tag==oldData.tag && r.domain==oldData.domain);
-                newTemplateSettings.reverse[oldData.type+'s'].splice(realIndex,1);
-
-                // delete empty objects
-                if(reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s');
-                if(Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings, 'reverse');
-
-                // delete related routing rules
-                newRules = newTemplateSettings.routing.rules;
-                if(oldData.type == "bridge"){
-                    newRules = newTemplateSettings.routing.rules.filter(r => !( r.inboundTag && r.inboundTag.length == 1 && r.inboundTag[0] == oldData.tag));
-                } else if(oldData.type == "portal"){
-                    newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
-                }
-                newTemplateSettings.routing.rules = newRules;
-
-                this.templateSettings = newTemplateSettings;
+                this.changeObsCode()
             },
             addDNSServer(){
                 dnsModal.show({
@@ -1479,27 +1570,6 @@
                     return data;
                 },
             },
-            balancersData: {
-                get: function () {
-                    data = []
-                    if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings.routing.balancers != null) {
-                        this.templateSettings.routing.balancers.forEach((o, index) => {
-                            let strategy = "random"
-                            if (o.strategy && (o.strategy.type == "roundRobin" || o.strategy.type == "leastload" || o.strategy.type == "leastping")) {
-                                strategy = o.strategy.type;
-                            }
-
-                            data.push({
-                                'key': index,
-                                'tag': o.tag ? o.tag : "",
-                                'strategy': strategy,
-                                'selector': o.selector ? o.selector : []
-                            });
-                        });
-                    }
-                    return data;
-                }
-            },
             routingRuleSettings: {
                 get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
                 set: function (newValue) {
@@ -1529,6 +1599,58 @@
                     return data;
                 }
             },
+            balancersData: {
+                get: function () {
+                    data = []
+                    if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings.routing.balancers != null) {
+                        this.templateSettings.routing.balancers.forEach((o, index) => {
+                            data.push({
+                                'key': index,
+                                'tag': o.tag ? o.tag : "",
+                                'strategy': o.strategy?.type ?? "random",
+                                'selector': o.selector ? o.selector : []
+                            });
+                        });
+                    }
+                    return data;
+                }
+            },
+            observatory: {
+                get: function () { 
+                    return this.templateSettings?.observatory ? JSON.stringify(this.templateSettings.observatory, null, 2) : null;
+                },
+                set: function (newValue) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.observatory = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
+                },
+            },
+            burstObservatory: {
+                get: function () { 
+                    return this.templateSettings?.burstObservatory ? JSON.stringify(this.templateSettings.burstObservatory, null, 2) : null; 
+                },
+                set: function (newValue) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.burstObservatory = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
+                },
+            },
+            observatoryEnable: {
+                get: function () { return this.templateSettings != null && this.templateSettings.observatory },
+                set: function (v) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.observatory = v ? this.defaultObservatory : undefined;
+                    this.templateSettings = newTemplateSettings;
+                }
+            },
+            burstObservatoryEnable: {
+                get: function () { return this.templateSettings != null && this.templateSettings.burstObservatory },
+                set: function (v) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.burstObservatory = v ? this.defaultBurstObservatory : undefined;
+                    this.templateSettings = newTemplateSettings;
+                }
+            },
             freedomStrategy: {
                 get: function () {
                     if (!this.templateSettings) return "AsIs";