1
0

3 Коммиты 7cd26a0583 ... 444b05cac9

Автор SHA1 Сообщение Дата
  MHSanaei 444b05cac9 perf(frontend): code-split heavy components to improve LCP 1 день назад
  MHSanaei f70e131dfe fix(nodes): bind form-encoded posts and skip node inbounds in central xray 1 день назад
  Amirmohammad Sadat Shokouhi 14165fc54d avoid reset in QueryStatsRequest (#4202) 1 день назад

+ 9 - 9
database/model/model.go

@@ -128,15 +128,15 @@ type Setting struct {
 // endpoint over HTTP using the per-node ApiToken to populate the runtime
 // status fields below.
 type Node struct {
-	Id       int    `json:"id" gorm:"primaryKey;autoIncrement"`
-	Name     string `json:"name" gorm:"uniqueIndex"`
-	Remark   string `json:"remark"`
-	Scheme   string `json:"scheme"`  // "https" | "http"
-	Address  string `json:"address"` // host or IP
-	Port     int    `json:"port"`
-	BasePath string `json:"basePath"` // "/" or "/myprefix/"
-	ApiToken string `json:"apiToken"` // plaintext, matches existing tg/ldap pattern
-	Enable   bool   `json:"enable" gorm:"default:true"`
+	Id       int    `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
+	Name     string `json:"name" form:"name" gorm:"uniqueIndex"`
+	Remark   string `json:"remark" form:"remark"`
+	Scheme   string `json:"scheme" form:"scheme"`
+	Address  string `json:"address" form:"address"`
+	Port     int    `json:"port" form:"port"`
+	BasePath string `json:"basePath" form:"basePath"`
+	ApiToken string `json:"apiToken" form:"apiToken"`
+	Enable   bool   `json:"enable" form:"enable" gorm:"default:true"`
 
 	// Heartbeat-updated fields. UpdatedAt advances on every probe even when
 	// the row is otherwise unchanged so the UI's "last seen" tooltip is

+ 2 - 10
frontend/src/components/DateTimePicker.vue

@@ -1,17 +1,9 @@
 <script setup>
-import { computed } from 'vue';
+import { computed, defineAsyncComponent } from 'vue';
 import dayjs from 'dayjs';
-import PersianDatePicker from 'vue3-persian-datetime-picker';
 import { useDatepicker } from '@/composables/useDatepicker.js';
 
-// Drop-in replacement for <a-date-picker> that swaps to a real Jalali
-// calendar (vue3-persian-datetime-picker, backed by moment-jalaali)
-// when the panel's "Calendar Type" setting is `jalalian`.
-//
-// The v-model contract matches AD-Vue: the parent works with a dayjs
-// object (or null). For the persian picker we serialize to/from the
-// `YYYY-MM-DD HH:mm:ss` string it expects so callers don't need to
-// know which renderer is active.
+const PersianDatePicker = defineAsyncComponent(() => import('vue3-persian-datetime-picker'));
 
 const props = defineProps({
   value: { type: [Object, null], default: null },

+ 9 - 8
frontend/src/pages/inbounds/InboundsPage.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { computed, onMounted, ref } from 'vue';
+import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { Modal, message } from 'ant-design-vue';
 import {
@@ -18,16 +18,17 @@ import AppSidebar from '@/components/AppSidebar.vue';
 import CustomStatistic from '@/components/CustomStatistic.vue';
 import { useNodeList } from '@/composables/useNodeList.js';
 import InboundList from './InboundList.vue';
-import InboundFormModal from './InboundFormModal.vue';
-import ClientFormModal from './ClientFormModal.vue';
-import ClientBulkModal from './ClientBulkModal.vue';
-import InboundInfoModal from './InboundInfoModal.vue';
-import QrCodeModal from './QrCodeModal.vue';
-import TextModal from '@/components/TextModal.vue';
-import PromptModal from '@/components/PromptModal.vue';
 import { useInbounds } from './useInbounds.js';
 import { useWebSocket } from '@/composables/useWebSocket.js';
 
+const InboundFormModal = defineAsyncComponent(() => import('./InboundFormModal.vue'));
+const ClientFormModal = defineAsyncComponent(() => import('./ClientFormModal.vue'));
+const ClientBulkModal = defineAsyncComponent(() => import('./ClientBulkModal.vue'));
+const InboundInfoModal = defineAsyncComponent(() => import('./InboundInfoModal.vue'));
+const QrCodeModal = defineAsyncComponent(() => import('./QrCodeModal.vue'));
+const TextModal = defineAsyncComponent(() => import('@/components/TextModal.vue'));
+const PromptModal = defineAsyncComponent(() => import('@/components/PromptModal.vue'));
+
 const { t } = useI18n();
 
 const {

+ 3 - 2
frontend/src/pages/login/LoginPage.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
+import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { UserOutlined, LockOutlined, KeyOutlined, SettingOutlined } from '@ant-design/icons-vue';
 
@@ -9,7 +9,8 @@ import {
   currentTheme,
   theme as themeState,
 } from '@/composables/useTheme.js';
-import ThemeSwitchLogin from '@/components/ThemeSwitchLogin.vue';
+
+const ThemeSwitchLogin = defineAsyncComponent(() => import('@/components/ThemeSwitchLogin.vue'));
 
 const { t } = useI18n();
 

+ 5 - 1
web/service/xray.go

@@ -113,6 +113,9 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 		if !inbound.Enable {
 			continue
 		}
+		if inbound.NodeID != nil {
+			continue
+		}
 		// get settings clients
 		settings := map[string]any{}
 		json.Unmarshal([]byte(inbound.Settings), &settings)
@@ -212,7 +215,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
 	}
 	defer s.xrayAPI.Close()
 
-	traffic, clientTraffic, err := s.xrayAPI.GetTraffic(true)
+	traffic, clientTraffic, err := s.xrayAPI.GetTraffic()
 	if err != nil {
 		logger.Debug("Failed to fetch Xray traffic:", err)
 		return nil, nil, err
@@ -242,6 +245,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
 
 	p = xray.NewProcess(xrayConfig)
 	result = ""
+	s.xrayAPI.StatsLastValues = nil
 	err = p.Start()
 	if err != nil {
 		return err

+ 15 - 4
xray/api.go

@@ -35,6 +35,7 @@ type XrayAPI struct {
 	StatsServiceClient   *statsService.StatsServiceClient
 	grpcClient           *grpc.ClientConn
 	isConnected          bool
+	StatsLastValues      map[string]int64
 }
 
 func getRequiredUserString(user map[string]any, key string) (string, error) {
@@ -79,6 +80,9 @@ func (x *XrayAPI) Init(apiPort int) error {
 
 	x.grpcClient = conn
 	x.isConnected = true
+	if x.StatsLastValues == nil {
+		x.StatsLastValues = make(map[string]int64)
+	}
 
 	hsClient := command.NewHandlerServiceClient(conn)
 	ssClient := statsService.NewStatsServiceClient(conn)
@@ -274,7 +278,7 @@ func (x *XrayAPI) RemoveUser(inboundTag, email string) error {
 }
 
 // GetTraffic queries traffic statistics from the Xray core, optionally resetting counters.
-func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
+func (x *XrayAPI) GetTraffic() ([]*Traffic, []*ClientTraffic, error) {
 	if x.grpcClient == nil {
 		return nil, nil, common.NewError("xray api is not initialized")
 	}
@@ -289,7 +293,7 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
 		return nil, nil, common.NewError("xray StatusServiceClient is not initialized")
 	}
 
-	resp, err := (*x.StatsServiceClient).QueryStats(ctx, &statsService.QueryStatsRequest{Reset_: reset})
+	resp, err := (*x.StatsServiceClient).QueryStats(ctx, &statsService.QueryStatsRequest{Reset_: false})
 	if err != nil {
 		logger.Debug("Failed to query Xray stats:", err)
 		return nil, nil, err
@@ -299,10 +303,17 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
 	emailTrafficMap := make(map[string]*ClientTraffic)
 
 	for _, stat := range resp.GetStat() {
+		lastValue, ok := x.StatsLastValues[stat.Name]
+		x.StatsLastValues[stat.Name] = stat.Value
+		if !ok || stat.Value < lastValue {
+			// skip first time of seen stat
+			continue
+		}
+		value := stat.Value - lastValue
 		if matches := trafficRegex.FindStringSubmatch(stat.Name); len(matches) == 4 {
-			processTraffic(matches, stat.Value, tagTrafficMap)
+			processTraffic(matches, value, tagTrafficMap)
 		} else if matches := clientTrafficRegex.FindStringSubmatch(stat.Name); len(matches) == 3 {
-			processClientTraffic(matches, stat.Value, emailTrafficMap)
+			processClientTraffic(matches, value, emailTrafficMap)
 		}
 	}
 	return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil