Просмотр исходного кода

feat: allow disabling proxy targets availability test #1327

0xJacky 4 месяцев назад
Родитель
Сommit
de0467b9e7
46 измененных файлов с 4363 добавлено и 2452 удалено
  1. 3 1
      .claude/settings.local.json
  2. 35 0
      api/upstream/init.go
  3. 131 0
      api/upstream/list.go
  4. 2 0
      api/upstream/router.go
  5. 155 0
      api/upstream/socket.go
  6. 21 0
      api/upstream/util.go
  7. 0 30
      app/components.d.ts
  8. 30 0
      app/src/api/upstream.ts
  9. 3 0
      app/src/constants/errors/analytic.ts
  10. 1 0
      app/src/constants/errors/cert.ts
  11. 2 0
      app/src/constants/errors/docker.ts
  12. 3 0
      app/src/constants/errors/nginx_log.indexer.ts
  13. 6 0
      app/src/constants/errors/nginx_log.parser.ts
  14. 0 3
      app/src/constants/errors/nginx_log.ts
  15. 1 0
      app/src/constants/errors/notification.ts
  16. 1 0
      app/src/constants/errors/performance.ts
  17. 7 0
      app/src/constants/errors/upgrader.ts
  18. 3 0
      app/src/constants/errors/user.ts
  19. 4 0
      app/src/constants/errors/version.ts
  20. 258 169
      app/src/language/ar/app.po
  21. 231 177
      app/src/language/de_DE/app.po
  22. 125 24
      app/src/language/en/app.po
  23. 229 157
      app/src/language/es/app.po
  24. 234 174
      app/src/language/fr_FR/app.po
  25. 219 229
      app/src/language/ja_JP/app.po
  26. 222 211
      app/src/language/ko_KR/app.po
  27. 125 23
      app/src/language/messages.pot
  28. 232 166
      app/src/language/pt_PT/app.po
  29. 236 164
      app/src/language/ru_RU/app.po
  30. 241 173
      app/src/language/tr_TR/app.po
  31. 246 173
      app/src/language/uk_UA/app.po
  32. 244 171
      app/src/language/vi_VN/app.po
  33. 222 195
      app/src/language/zh_CN/app.po
  34. 228 199
      app/src/language/zh_TW/app.po
  35. 2 0
      app/src/routes/index.ts
  36. 14 0
      app/src/routes/modules/upstream.ts
  37. 193 0
      app/src/views/upstream/SocketList.vue
  38. BIN
      cmd/generate_licenses/internal/license/licenses.xz
  39. BIN
      internal/cert/config/config.tar.xz
  40. 24 0
      internal/nginx/nginx_directives.json
  41. 43 8
      internal/upstream/service.go
  42. 1 0
      model/model.go
  43. 7 0
      model/upstream_config.go
  44. 8 0
      query/gen.go
  45. 1 5
      query/llm_sessions.gen.go
  46. 370 0
      query/upstream_configs.gen.go

+ 3 - 1
.claude/settings.local.json

@@ -15,7 +15,9 @@
       "Bash(find:*)",
       "Bash(sed:*)",
       "Bash(cp:*)",
-      "mcp__eslint__lint-files"
+      "mcp__eslint__lint-files",
+      "Bash(go generate:*)",
+      "Bash(pnpm eslint:*)"
     ],
     "deny": []
   }

+ 35 - 0
api/upstream/init.go

@@ -0,0 +1,35 @@
+package upstream
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/upstream"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+func init() {
+	// Register the disabled sockets checker callback
+	service := upstream.GetUpstreamService()
+	service.SetDisabledSocketsChecker(getDisabledSockets)
+}
+
+// getDisabledSockets queries the database for disabled sockets
+func getDisabledSockets() map[string]bool {
+	disabled := make(map[string]bool)
+
+	db := model.UseDB()
+	if db == nil {
+		return disabled
+	}
+
+	var configs []model.UpstreamConfig
+	if err := db.Where("enabled = ?", false).Find(&configs).Error; err != nil {
+		logger.Error("Failed to query disabled sockets:", err)
+		return disabled
+	}
+
+	for _, config := range configs {
+		disabled[config.Socket] = true
+	}
+
+	return disabled
+}

+ 131 - 0
api/upstream/list.go

@@ -0,0 +1,131 @@
+package upstream
+
+import (
+	"net/http"
+	"sort"
+
+	"github.com/0xJacky/Nginx-UI/internal/upstream"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// UpstreamInfo represents an upstream with its configuration and health status
+type UpstreamInfo struct {
+	Name       string                     `json:"name"`
+	Servers    []upstream.ProxyTarget     `json:"servers"`
+	ConfigPath string                     `json:"config_path"`
+	LastSeen   string                     `json:"last_seen"`
+	Status     map[string]*upstream.Status `json:"status"`
+	Enabled    bool                       `json:"enabled"`
+}
+
+// GetUpstreamList returns all upstreams with their configuration and health status
+func GetUpstreamList(c *gin.Context) {
+	service := upstream.GetUpstreamService()
+
+	// Get all upstream definitions
+	upstreams := service.GetAllUpstreamDefinitions()
+
+	// Get availability map
+	availabilityMap := service.GetAvailabilityMap()
+
+	// Get all upstream configurations from database
+	u := query.UpstreamConfig
+	configs, err := u.Find()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// Create a map for quick lookup of enabled status by upstream name
+	configMap := make(map[string]bool)
+	for _, config := range configs {
+		configMap[config.Socket] = config.Enabled
+	}
+
+	// Build response
+	result := make([]UpstreamInfo, 0, len(upstreams))
+	for name, def := range upstreams {
+		// Get enabled status from database, default to true if not found
+		enabled := true
+		if val, exists := configMap[name]; exists {
+			enabled = val
+		}
+
+		// Get status for each server in this upstream
+		serverStatus := make(map[string]*upstream.Status)
+		for _, server := range def.Servers {
+			key := formatSocketAddress(server.Host, server.Port)
+			if status, exists := availabilityMap[key]; exists {
+				serverStatus[key] = status
+			}
+		}
+
+		info := UpstreamInfo{
+			Name:       name,
+			Servers:    def.Servers,
+			ConfigPath: def.ConfigPath,
+			LastSeen:   def.LastSeen.Format("2006-01-02 15:04:05"),
+			Status:     serverStatus,
+			Enabled:    enabled,
+		}
+		result = append(result, info)
+	}
+
+	// Sort by name for stable ordering
+	sort.Slice(result, func(i, j int) bool {
+		return result[i].Name < result[j].Name
+	})
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": result,
+	})
+}
+
+// UpdateUpstreamConfigRequest represents the request body for updating upstream config
+type UpdateUpstreamConfigRequest struct {
+	Enabled bool `json:"enabled"`
+}
+
+// UpdateUpstreamConfig updates the enabled status of an upstream
+func UpdateUpstreamConfig(c *gin.Context) {
+	name := c.Param("name")
+
+	var req UpdateUpstreamConfigRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	u := query.UpstreamConfig
+
+	// Check if config exists
+	config, err := u.Where(u.Socket.Eq(name)).First()
+	if err != nil {
+		// Create new config if not found
+		config = &model.UpstreamConfig{
+			Socket:  name,
+			Enabled: req.Enabled,
+		}
+		if err := u.Create(config); err != nil {
+			logger.Error("Failed to create upstream config:", err)
+			cosy.ErrHandler(c, err)
+			return
+		}
+	} else {
+		// Update existing config
+		if _, err := u.Where(u.Socket.Eq(name)).Update(u.Enabled, req.Enabled); err != nil {
+			logger.Error("Failed to update upstream config:", err)
+			cosy.ErrHandler(c, err)
+			return
+		}
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "Upstream config updated successfully",
+	})
+}
+

+ 2 - 0
api/upstream/router.go

@@ -5,4 +5,6 @@ import "github.com/gin-gonic/gin"
 func InitRouter(r *gin.RouterGroup) {
 	r.GET("/upstream/availability", GetAvailability)
 	r.GET("/upstream/availability_ws", AvailabilityWebSocket)
+	r.GET("/upstream/sockets", GetSocketList)
+	r.PUT("/upstream/socket/:socket", UpdateSocketConfig)
 }

+ 155 - 0
api/upstream/socket.go

@@ -0,0 +1,155 @@
+package upstream
+
+import (
+	"net/http"
+	"sort"
+
+	"github.com/0xJacky/Nginx-UI/internal/upstream"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// SocketInfo represents a socket with its configuration and health status
+type SocketInfo struct {
+	Socket       string           `json:"socket"`        // host:port
+	Host         string           `json:"host"`          // hostname/IP
+	Port         string           `json:"port"`          // port number
+	Type         string           `json:"type"`          // proxy_pass, grpc_pass, or upstream
+	IsConsul     bool             `json:"is_consul"`     // whether this is a consul service
+	UpstreamName string           `json:"upstream_name"` // which upstream this belongs to (if any)
+	LastCheck    string           `json:"last_check"`    // last time health check was performed
+	Status       *upstream.Status `json:"status"`        // health check status
+	Enabled      bool             `json:"enabled"`       // whether health check is enabled
+}
+
+// GetSocketList returns all sockets with their configuration and health status
+func GetSocketList(c *gin.Context) {
+	service := upstream.GetUpstreamService()
+
+	// Get all target infos
+	targets := service.GetTargetInfos()
+
+	// Get availability map
+	availabilityMap := service.GetAvailabilityMap()
+
+	// Get all socket configurations from database
+	u := query.UpstreamConfig
+	configs, err := u.Find()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// Create a map for quick lookup of enabled status
+	configMap := make(map[string]bool)
+	for _, config := range configs {
+		configMap[config.Socket] = config.Enabled
+	}
+
+	// Build response
+	result := make([]SocketInfo, 0, len(targets))
+	for _, target := range targets {
+		socketAddr := formatSocketAddress(target.Host, target.Port)
+
+		// Get enabled status from database, default to true if not found
+		enabled := true
+		if val, exists := configMap[socketAddr]; exists {
+			enabled = val
+		}
+
+		// Get health status
+		var status *upstream.Status
+		if s, exists := availabilityMap[socketAddr]; exists {
+			status = s
+		}
+
+		// Find which upstream this belongs to
+		upstreamName := findUpstreamForSocket(service, target.ProxyTarget)
+
+		info := SocketInfo{
+			Socket:       socketAddr,
+			Host:         target.Host,
+			Port:         target.Port,
+			Type:         target.Type,
+			IsConsul:     target.IsConsul,
+			UpstreamName: upstreamName,
+			LastCheck:    target.LastSeen.Format("2006-01-02 15:04:05"),
+			Status:       status,
+			Enabled:      enabled,
+		}
+		result = append(result, info)
+	}
+
+	// Sort by socket address for stable ordering
+	sort.Slice(result, func(i, j int) bool {
+		return result[i].Socket < result[j].Socket
+	})
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": result,
+	})
+}
+
+// UpdateSocketConfigRequest represents the request body for updating socket config
+type UpdateSocketConfigRequest struct {
+	Enabled bool `json:"enabled"`
+}
+
+// UpdateSocketConfig updates the enabled status of a socket
+func UpdateSocketConfig(c *gin.Context) {
+	socket := c.Param("socket")
+
+	var req UpdateSocketConfigRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	u := query.UpstreamConfig
+
+	// Check if config exists
+	config, err := u.Where(u.Socket.Eq(socket)).First()
+	if err != nil {
+		// Create new config if not found
+		config = &model.UpstreamConfig{
+			Socket:  socket,
+			Enabled: req.Enabled,
+		}
+		if err := u.Create(config); err != nil {
+			logger.Error("Failed to create socket config:", err)
+			cosy.ErrHandler(c, err)
+			return
+		}
+	} else {
+		// Update existing config
+		if _, err := u.Where(u.Socket.Eq(socket)).Update(u.Enabled, req.Enabled); err != nil {
+			logger.Error("Failed to update socket config:", err)
+			cosy.ErrHandler(c, err)
+			return
+		}
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "Socket config updated successfully",
+	})
+}
+
+// findUpstreamForSocket finds which upstream a socket belongs to
+func findUpstreamForSocket(service *upstream.Service, target upstream.ProxyTarget) string {
+	socketAddr := formatSocketAddress(target.Host, target.Port)
+	upstreams := service.GetAllUpstreamDefinitions()
+
+	for name, upstream := range upstreams {
+		for _, server := range upstream.Servers {
+			serverAddr := formatSocketAddress(server.Host, server.Port)
+			if serverAddr == socketAddr {
+				return name
+			}
+		}
+	}
+	return ""
+}
+

+ 21 - 0
api/upstream/util.go

@@ -0,0 +1,21 @@
+package upstream
+
+// formatSocketAddress formats a host:port combination into a proper socket address
+// For IPv6 addresses, it adds brackets around the host if they're not already present
+func formatSocketAddress(host, port string) string {
+	// Reuse the logic from service package
+	if len(host) > 0 && host[0] != '[' && containsColon(host) {
+		return "[" + host + "]:" + port
+	}
+	return host + ":" + port
+}
+
+// containsColon checks if string contains a colon
+func containsColon(s string) bool {
+	for i := 0; i < len(s); i++ {
+		if s[i] == ':' {
+			return true
+		}
+	}
+	return false
+}

+ 0 - 30
app/components.d.ts

@@ -10,29 +10,16 @@ declare module 'vue' {
   export interface GlobalComponents {
     AAlert: typeof import('ant-design-vue/es')['Alert']
     AApp: typeof import('ant-design-vue/es')['App']
-    AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
-    AAvatar: typeof import('ant-design-vue/es')['Avatar']
     ABadge: typeof import('ant-design-vue/es')['Badge']
     ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
     ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
     AButton: typeof import('ant-design-vue/es')['Button']
     ACard: typeof import('ant-design-vue/es')['Card']
-    ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
-    ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
-    ACol: typeof import('ant-design-vue/es')['Col']
-    ACollapse: typeof import('ant-design-vue/es')['Collapse']
-    ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
     ADivider: typeof import('ant-design-vue/es')['Divider']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
-    ADropdown: typeof import('ant-design-vue/es')['Dropdown']
-    AEmpty: typeof import('ant-design-vue/es')['Empty']
-    AForm: typeof import('ant-design-vue/es')['Form']
-    AFormItem: typeof import('ant-design-vue/es')['FormItem']
     AInput: typeof import('ant-design-vue/es')['Input']
     AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
-    AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
-    AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
     ALayout: typeof import('ant-design-vue/es')['Layout']
     ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
     ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
@@ -43,34 +30,17 @@ declare module 'vue' {
     AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
-    AModal: typeof import('ant-design-vue/es')['Modal']
     APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
     APopover: typeof import('ant-design-vue/es')['Popover']
     AppProvider: typeof import('./src/components/AppProvider.vue')['default']
-    AProgress: typeof import('ant-design-vue/es')['Progress']
-    ARadio: typeof import('ant-design-vue/es')['Radio']
-    ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
-    ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
-    AResult: typeof import('ant-design-vue/es')['Result']
-    ARow: typeof import('ant-design-vue/es')['Row']
-    ASegmented: typeof import('ant-design-vue/es')['Segmented']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
     ASpace: typeof import('ant-design-vue/es')['Space']
-    ASpin: typeof import('ant-design-vue/es')['Spin']
-    AStatistic: typeof import('ant-design-vue/es')['Statistic']
-    AStep: typeof import('ant-design-vue/es')['Step']
-    ASteps: typeof import('ant-design-vue/es')['Steps']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
     ASwitch: typeof import('ant-design-vue/es')['Switch']
     ATable: typeof import('ant-design-vue/es')['Table']
-    ATabPane: typeof import('ant-design-vue/es')['TabPane']
-    ATabs: typeof import('ant-design-vue/es')['Tabs']
     ATag: typeof import('ant-design-vue/es')['Tag']
-    ATextarea: typeof import('ant-design-vue/es')['Textarea']
     ATooltip: typeof import('ant-design-vue/es')['Tooltip']
-    ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
-    ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
     AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default']
     AutoCertFormDNSChallenge: typeof import('./src/components/AutoCertForm/DNSChallenge.vue')['default']
     BaseEditorBaseEditor: typeof import('./src/components/BaseEditor/BaseEditor.vue')['default']

+ 30 - 0
app/src/api/upstream.ts

@@ -19,6 +19,26 @@ export interface UpstreamAvailabilityResponse {
   target_count: number
 }
 
+export interface SocketInfo {
+  socket: string
+  host: string
+  port: string
+  type: string
+  is_consul: boolean
+  upstream_name: string
+  last_check: string
+  status: UpstreamStatus | null
+  enabled: boolean
+}
+
+export interface SocketListResponse {
+  data: SocketInfo[]
+}
+
+export interface UpdateSocketConfigRequest {
+  enabled: boolean
+}
+
 const upstream = {
   // HTTP GET interface to get all upstream availability results
   getAvailability(): Promise<UpstreamAvailabilityResponse> {
@@ -29,6 +49,16 @@ const upstream = {
   availabilityWebSocket() {
     return ws('/api/upstream/availability_ws')
   },
+
+  // Get all sockets with their configuration and health status
+  getSocketList(): Promise<SocketListResponse> {
+    return http.get('/upstream/sockets')
+  },
+
+  // Update socket configuration
+  updateSocketConfig(socket: string, data: UpdateSocketConfigRequest) {
+    return http.put(`/upstream/socket/${encodeURIComponent(socket)}`, data)
+  },
 }
 
 export default upstream

+ 3 - 0
app/src/constants/errors/analytic.ts

@@ -0,0 +1,3 @@
+export default {
+  54001: () => $gettext('Node analytics failed: {0}'),
+}

+ 1 - 0
app/src/constants/errors/cert.ts

@@ -22,4 +22,5 @@ export default {
   50021: () => $gettext('Write private.key error: {0}'),
   50022: () => $gettext('Obtain cert error: {0}'),
   50023: () => $gettext('Revoke cert error: {0}'),
+  50031: () => $gettext('No certificate available'),
 }

+ 2 - 0
app/src/constants/errors/docker.ts

@@ -12,4 +12,6 @@ export default {
   500011: () => $gettext('Failed to inspect current container: {0}'),
   500012: () => $gettext('Failed to create temp container: {0}'),
   500013: () => $gettext('Failed to start temp container: {0}'),
+  500014: () => $gettext('Could not find old container name'),
+  500015: () => $gettext('Could not find temp container'),
 }

+ 3 - 0
app/src/constants/errors/nginx_log.indexer.ts

@@ -0,0 +1,3 @@
+export default {
+  50201: () => $gettext('Log parser is not initialized; call indexer.InitLogParser() before use'),
+}

+ 6 - 0
app/src/constants/errors/nginx_log.parser.ts

@@ -0,0 +1,6 @@
+export default {
+  50101: () => $gettext('Empty log line'),
+  50102: () => $gettext('Log line exceeds maximum length'),
+  50103: () => $gettext('Unsupported log format'),
+  50104: () => $gettext('Invalid timestamp format'),
+}

+ 0 - 3
app/src/constants/errors/nginx_log.ts

@@ -6,9 +6,6 @@ export default {
   50005: () => $gettext('Directive params is empty'),
   50006: () => $gettext('Settings.NginxLogSettings.ErrorLogPath is empty, refer to https://nginxui.com/guide/config-nginx.html for more information'),
   50007: () => $gettext('Settings.NginxLogSettings.AccessLogPath is empty, refer to https://nginxui.com/guide/config-nginx.html for more information'),
-  50008: () => $gettext('Empty log line'),
-  50009: () => $gettext('Invalid timestamp format'),
-  50010: () => $gettext('Unsupported log format'),
   50011: () => $gettext('Log indexer not available'),
   50012: () => $gettext('Analytics service not available'),
   50013: () => $gettext('Log file does not exist'),

+ 1 - 0
app/src/constants/errors/notification.ts

@@ -3,4 +3,5 @@ export default {
   400001: () => $gettext('Invalid notifier config'),
   400002: () => $gettext('Invalid notification ID'),
   404002: () => $gettext('External notification configuration not found'),
+  400003: () => $gettext('Invalid Telegram Chat ID: cannot be zero'),
 }

+ 1 - 0
app/src/constants/errors/performance.ts

@@ -6,4 +6,5 @@ export default {
   51004: () => $gettext('Failed to execute template: {0}'),
   51005: () => $gettext('Failed to parse nginx config: {0}'),
   51006: () => $gettext('Failed to build nginx config: {0}'),
+  51007: () => $gettext('Failed to get nginx.conf path'),
 }

+ 7 - 0
app/src/constants/errors/upgrader.ts

@@ -0,0 +1,7 @@
+export default {
+  52001: () => $gettext('Upgrader core downloadUrl is empty'),
+  52002: () => $gettext('Upgrader core digest is empty'),
+  52003: () => $gettext('Digest file content is empty'),
+  52004: () => $gettext('Executable binary file is empty'),
+  52005: () => $gettext('Update already in progress'),
+}

+ 3 - 0
app/src/constants/errors/user.ts

@@ -12,4 +12,7 @@ export default {
   40401: () => $gettext('Session not found'),
   40402: () => $gettext('Token is empty'),
   50005: () => $gettext('Invalid claims type'),
+  50006: () => $gettext('Config not found'),
+  50007: () => $gettext('Db file not found'),
+  50008: () => $gettext('Init user not exists'),
 }

+ 4 - 0
app/src/constants/errors/version.ts

@@ -0,0 +1,4 @@
+export default {
+  53001: () => $gettext('Invalid commit SHA'),
+  53002: () => $gettext('Release API request failed: {0}'),
+}

Разница между файлами не показана из-за своего большого размера
+ 258 - 169
app/src/language/ar/app.po


Разница между файлами не показана из-за своего большого размера
+ 231 - 177
app/src/language/de_DE/app.po


+ 125 - 24
app/src/language/en/app.po

@@ -297,7 +297,7 @@ msgid ""
 "certificate application will fail."
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:13
+#: src/constants/errors/nginx_log.ts:10
 msgid "Analytics service not available"
 msgstr ""
 
@@ -543,7 +543,7 @@ msgid ""
 "ready."
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:17
+#: src/constants/errors/nginx_log.ts:14
 msgid "Background log service not available"
 msgstr ""
 
@@ -748,7 +748,7 @@ msgstr ""
 msgid "Cannot access backup path {0}: {1}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:16
+#: src/constants/errors/nginx_log.ts:13
 msgid "Cannot access log file"
 msgstr ""
 
@@ -1197,6 +1197,10 @@ msgstr ""
 msgid "Config entry file not exist"
 msgstr ""
 
+#: src/constants/errors/user.ts:15
+msgid "Config not found"
+msgstr ""
+
 #: src/constants/errors/backup.ts:14
 msgid "Config path is empty"
 msgstr ""
@@ -1294,6 +1298,14 @@ msgstr ""
 msgid "Core Upgrade"
 msgstr ""
 
+#: src/constants/errors/docker.ts:15
+msgid "Could not find old container name"
+msgstr ""
+
+#: src/constants/errors/docker.ts:16
+msgid "Could not find temp container"
+msgstr ""
+
 #: src/views/nginx_log/dashboard/components/BrowserStatsTable.vue:18
 #: src/views/nginx_log/dashboard/components/DailyTrendsChart.vue:98
 #: src/views/nginx_log/dashboard/components/DeviceStatsTable.vue:17
@@ -1480,6 +1492,10 @@ msgstr ""
 msgid "Days"
 msgstr ""
 
+#: src/constants/errors/user.ts:16
+msgid "Db file not found"
+msgstr ""
+
 #: src/constants/errors/middleware.ts:3
 msgid "Decryption failed"
 msgstr ""
@@ -1633,10 +1649,18 @@ msgstr ""
 msgid "Device Type"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:4
+msgid "Digest file content is empty"
+msgstr ""
+
 #: src/views/preference/components/ExternalNotify/dingtalk.ts:5
 msgid "DingTalk"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:31
+msgid "Direct"
+msgstr ""
+
 #: src/components/NgxConfigEditor/directive/DirectiveAdd.vue:72
 msgid "Directive"
 msgstr ""
@@ -1924,7 +1948,7 @@ msgstr ""
 msgid "Email (*)"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:9
+#: src/constants/errors/nginx_log.parser.ts:2
 msgid "Empty log line"
 msgstr ""
 
@@ -2153,6 +2177,10 @@ msgstr ""
 msgid "Error processing content"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:5
+msgid "Executable binary file is empty"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:195
 msgid "Executable Path"
 msgstr ""
@@ -2369,7 +2397,7 @@ msgstr ""
 msgid "Failed to decrypt Nginx UI directory: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:22
+#: src/constants/errors/nginx_log.ts:19
 msgid "Failed to delete all indexes"
 msgstr ""
 
@@ -2381,7 +2409,7 @@ msgstr ""
 msgid "Failed to delete certificate from database: %{error}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:21
+#: src/constants/errors/nginx_log.ts:18
 msgid "Failed to delete file index"
 msgstr ""
 
@@ -2455,7 +2483,7 @@ msgstr ""
 msgid "Failed to get container id: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:23
+#: src/constants/errors/nginx_log.ts:20
 msgid "Failed to get index status"
 msgstr ""
 
@@ -2463,11 +2491,15 @@ msgstr ""
 msgid "Failed to get Nginx performance settings"
 msgstr ""
 
+#: src/constants/errors/performance.ts:9
+msgid "Failed to get nginx.conf path"
+msgstr ""
+
 #: src/composables/useNginxPerformance.ts:49
 msgid "Failed to get performance data"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:24
+#: src/constants/errors/nginx_log.ts:21
 msgid "Failed to get persistence stats"
 msgstr ""
 
@@ -2547,11 +2579,11 @@ msgstr ""
 msgid "Failed to read symlink: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:20
+#: src/constants/errors/nginx_log.ts:17
 msgid "Failed to rebuild file index"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:19
+#: src/constants/errors/nginx_log.ts:16
 msgid "Failed to rebuild index"
 msgstr ""
 
@@ -2651,7 +2683,7 @@ msgstr ""
 msgid "File or directory not found: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:18
+#: src/constants/errors/nginx_log.ts:15
 msgid "File path is required"
 msgstr ""
 
@@ -2857,6 +2889,10 @@ msgstr ""
 msgid "GZIP Min Length"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:61
+msgid "Health Check"
+msgstr ""
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:365
 msgid "Health Check Configuration"
 msgstr ""
@@ -2869,6 +2905,10 @@ msgstr ""
 msgid "Health check configuration saved successfully"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:37
+msgid "Health Status"
+msgstr ""
+
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Hide"
 msgstr ""
@@ -2893,7 +2933,7 @@ msgstr ""
 msgid "History"
 msgstr ""
 
-#: src/routes/index.ts:50
+#: src/routes/index.ts:52
 msgid "Home"
 msgstr ""
 
@@ -3032,6 +3072,10 @@ msgstr ""
 msgid "Info"
 msgstr ""
 
+#: src/constants/errors/user.ts:17
+msgid "Init user not exists"
+msgstr ""
+
 #: src/language/constants.ts:25
 msgid "Initial core upgrader error"
 msgstr ""
@@ -3102,6 +3146,10 @@ msgstr ""
 msgid "Invalid claims type"
 msgstr ""
 
+#: src/constants/errors/version.ts:2
+msgid "Invalid commit SHA"
+msgstr ""
+
 #: src/components/SystemRestore/SystemRestoreContent.vue:121
 msgid "Invalid file object"
 msgstr ""
@@ -3160,11 +3208,15 @@ msgstr ""
 msgid "Invalid security token format"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:10
+#: src/constants/errors/notification.ts:6
+msgid "Invalid Telegram Chat ID: cannot be zero"
+msgstr ""
+
+#: src/constants/errors/nginx_log.parser.ts:5
 msgid "Invalid timestamp format"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:26
+#: src/constants/errors/nginx_log.ts:23
 msgid "Invalid websocket message type"
 msgstr ""
 
@@ -3279,6 +3331,10 @@ msgstr ""
 msgid "Last Backup Time"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:52
+msgid "Last Check"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:197
 msgid "Last checked at"
 msgstr ""
@@ -3437,11 +3493,11 @@ msgid ""
 "nginx-log.html for more information."
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:14
+#: src/constants/errors/nginx_log.ts:11
 msgid "Log file does not exist"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:25
+#: src/constants/errors/nginx_log.ts:22
 msgid "Log file is not a regular file"
 msgstr ""
 
@@ -3453,7 +3509,7 @@ msgstr ""
 msgid "Log file not indexed yet"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:12
+#: src/constants/errors/nginx_log.ts:9
 msgid "Log indexer not available"
 msgstr ""
 
@@ -3461,11 +3517,19 @@ msgstr ""
 msgid "Log indexing completed! Loading updated data..."
 msgstr ""
 
+#: src/constants/errors/nginx_log.parser.ts:3
+msgid "Log line exceeds maximum length"
+msgstr ""
+
 #: src/routes/modules/nginx_log.ts:39 src/views/nginx_log/NginxLogList.vue:430
 msgid "Log List"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:15
+#: src/constants/errors/nginx_log.indexer.ts:2
+msgid "Log parser is not initialized; call indexer.InitLogParser() before use"
+msgstr ""
+
+#: src/constants/errors/nginx_log.ts:12
 msgid "Log path is not under whitelist"
 msgstr ""
 
@@ -3687,15 +3751,15 @@ msgstr ""
 msgid "Model"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:28
+#: src/constants/errors/nginx_log.ts:25
 msgid "Modern analytics service not available"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:29
+#: src/constants/errors/nginx_log.ts:26
 msgid "Modern indexer service not available"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:27
+#: src/constants/errors/nginx_log.ts:24
 msgid "Modern searcher service not available"
 msgstr ""
 
@@ -4081,6 +4145,10 @@ msgstr ""
 msgid "No Action"
 msgstr ""
 
+#: src/constants/errors/cert.ts:25
+msgid "No certificate available"
+msgstr ""
+
 #: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:196
 #: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:232
 msgid "No China geographic data available"
@@ -4093,6 +4161,10 @@ msgstr ""
 msgid "No data"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:42
+msgid "No Data"
+msgstr ""
+
 #: src/views/nginx_log/structured/StructuredLogViewer.vue:820
 msgid "No entries in current page"
 msgstr ""
@@ -4140,6 +4212,10 @@ msgstr ""
 msgid "Node"
 msgstr ""
 
+#: src/constants/errors/analytic.ts:2
+msgid "Node analytics failed: {0}"
+msgstr ""
+
 #: src/views/preference/tabs/NodeSettings.vue:15
 msgid "Node name"
 msgstr ""
@@ -4969,6 +5045,10 @@ msgstr ""
 msgid "Reinstall"
 msgstr ""
 
+#: src/constants/errors/version.ts:3
+msgid "Release API request failed: {0}"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:270
 msgid "Release Note"
 msgstr ""
@@ -5774,6 +5854,10 @@ msgstr ""
 msgid "Sleep time between cache manager iterations"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:18
+msgid "Socket"
+msgstr ""
+
 #: src/views/nginx_log/structured/StructuredLogViewer.vue:735
 msgid "Sorted by"
 msgstr ""
@@ -6614,10 +6698,14 @@ msgstr ""
 msgid "Unsupported backup type: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:11
+#: src/constants/errors/nginx_log.parser.ts:4
 msgid "Unsupported log format"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:6
+msgid "Update already in progress"
+msgstr ""
+
 #: src/views/user/UserProfile.vue:218
 msgid "Update Password"
 msgstr ""
@@ -6657,6 +6745,14 @@ msgstr ""
 msgid "Upgraded successfully"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:3
+msgid "Upgrader core digest is empty"
+msgstr ""
+
+#: src/constants/errors/upgrader.ts:2
+msgid "Upgrader core downloadUrl is empty"
+msgstr ""
+
 #: src/views/node/BatchUpgrader.vue:88 src/views/system/Upgrade.vue:80
 msgid "Upgrading Nginx UI, please wait..."
 msgstr ""
@@ -6673,7 +6769,8 @@ msgstr ""
 msgid "Upload Folders"
 msgstr ""
 
-#: src/composables/useUpstreamStatus.ts:132
+#: src/composables/useUpstreamStatus.ts:132 src/routes/modules/upstream.ts:10
+#: src/views/upstream/SocketList.vue:25
 msgid "Upstream"
 msgstr ""
 
@@ -6681,6 +6778,10 @@ msgstr ""
 msgid "Upstream Name"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:134
+msgid "Upstream Sockets"
+msgstr ""
+
 #: src/views/namespace/columns.ts:59
 msgid "Upstream Test Type"
 msgstr ""
@@ -6935,7 +7036,7 @@ msgstr ""
 msgid "Workers"
 msgstr ""
 
-#: src/layouts/HeaderLayout.vue:61 src/routes/index.ts:59
+#: src/layouts/HeaderLayout.vue:61 src/routes/index.ts:61
 #: src/views/workspace/WorkSpace.vue:51
 msgid "Workspace"
 msgstr ""

Разница между файлами не показана из-за своего большого размера
+ 229 - 157
app/src/language/es/app.po


Разница между файлами не показана из-за своего большого размера
+ 234 - 174
app/src/language/fr_FR/app.po


Разница между файлами не показана из-за своего большого размера
+ 219 - 229
app/src/language/ja_JP/app.po


Разница между файлами не показана из-за своего большого размера
+ 222 - 211
app/src/language/ko_KR/app.po


+ 125 - 23
app/src/language/messages.pot

@@ -302,7 +302,7 @@ msgstr ""
 msgid "All selected subdomains must belong to the same DNS Provider, otherwise the certificate application will fail."
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:13
+#: src/constants/errors/nginx_log.ts:10
 msgid "Analytics service not available"
 msgstr ""
 
@@ -549,7 +549,7 @@ msgstr ""
 msgid "Background indexing in progress. Data will be updated automatically when ready."
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:17
+#: src/constants/errors/nginx_log.ts:14
 msgid "Background log service not available"
 msgstr ""
 
@@ -750,7 +750,7 @@ msgstr ""
 msgid "Cannot access backup path {0}: {1}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:16
+#: src/constants/errors/nginx_log.ts:13
 msgid "Cannot access log file"
 msgstr ""
 
@@ -1170,6 +1170,10 @@ msgstr ""
 msgid "Config entry file not exist"
 msgstr ""
 
+#: src/constants/errors/user.ts:15
+msgid "Config not found"
+msgstr ""
+
 #: src/constants/errors/backup.ts:14
 msgid "Config path is empty"
 msgstr ""
@@ -1267,6 +1271,14 @@ msgstr ""
 msgid "Core Upgrade"
 msgstr ""
 
+#: src/constants/errors/docker.ts:15
+msgid "Could not find old container name"
+msgstr ""
+
+#: src/constants/errors/docker.ts:16
+msgid "Could not find temp container"
+msgstr ""
+
 #: src/views/nginx_log/dashboard/components/BrowserStatsTable.vue:18
 #: src/views/nginx_log/dashboard/components/DailyTrendsChart.vue:98
 #: src/views/nginx_log/dashboard/components/DeviceStatsTable.vue:17
@@ -1451,6 +1463,10 @@ msgstr ""
 msgid "Days"
 msgstr ""
 
+#: src/constants/errors/user.ts:16
+msgid "Db file not found"
+msgstr ""
+
 #: src/constants/errors/middleware.ts:3
 msgid "Decryption failed"
 msgstr ""
@@ -1608,10 +1624,18 @@ msgstr ""
 msgid "Device Type"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:4
+msgid "Digest file content is empty"
+msgstr ""
+
 #: src/views/preference/components/ExternalNotify/dingtalk.ts:5
 msgid "DingTalk"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:31
+msgid "Direct"
+msgstr ""
+
 #: src/components/NgxConfigEditor/directive/DirectiveAdd.vue:72
 msgid "Directive"
 msgstr ""
@@ -1900,7 +1924,7 @@ msgstr ""
 msgid "Email (*)"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:9
+#: src/constants/errors/nginx_log.parser.ts:2
 msgid "Empty log line"
 msgstr ""
 
@@ -2128,6 +2152,10 @@ msgstr ""
 msgid "Error processing content"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:5
+msgid "Executable binary file is empty"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:195
 msgid "Executable Path"
 msgstr ""
@@ -2340,7 +2368,7 @@ msgstr ""
 msgid "Failed to decrypt Nginx UI directory: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:22
+#: src/constants/errors/nginx_log.ts:19
 msgid "Failed to delete all indexes"
 msgstr ""
 
@@ -2352,7 +2380,7 @@ msgstr ""
 msgid "Failed to delete certificate from database: %{error}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:21
+#: src/constants/errors/nginx_log.ts:18
 msgid "Failed to delete file index"
 msgstr ""
 
@@ -2426,7 +2454,7 @@ msgstr ""
 msgid "Failed to get container id: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:23
+#: src/constants/errors/nginx_log.ts:20
 msgid "Failed to get index status"
 msgstr ""
 
@@ -2434,11 +2462,15 @@ msgstr ""
 msgid "Failed to get Nginx performance settings"
 msgstr ""
 
+#: src/constants/errors/performance.ts:9
+msgid "Failed to get nginx.conf path"
+msgstr ""
+
 #: src/composables/useNginxPerformance.ts:49
 msgid "Failed to get performance data"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:24
+#: src/constants/errors/nginx_log.ts:21
 msgid "Failed to get persistence stats"
 msgstr ""
 
@@ -2518,11 +2550,11 @@ msgstr ""
 msgid "Failed to read symlink: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:20
+#: src/constants/errors/nginx_log.ts:17
 msgid "Failed to rebuild file index"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:19
+#: src/constants/errors/nginx_log.ts:16
 msgid "Failed to rebuild index"
 msgstr ""
 
@@ -2622,7 +2654,7 @@ msgstr ""
 msgid "File or directory not found: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:18
+#: src/constants/errors/nginx_log.ts:15
 msgid "File path is required"
 msgstr ""
 
@@ -2820,6 +2852,10 @@ msgstr ""
 msgid "GZIP Min Length"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:61
+msgid "Health Check"
+msgstr ""
+
 #: src/views/dashboard/components/SiteHealthCheckModal.vue:365
 msgid "Health Check Configuration"
 msgstr ""
@@ -2832,6 +2868,10 @@ msgstr ""
 msgid "Health check configuration saved successfully"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:37
+msgid "Health Status"
+msgstr ""
+
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Hide"
 msgstr ""
@@ -2856,7 +2896,7 @@ msgstr ""
 msgid "History"
 msgstr ""
 
-#: src/routes/index.ts:50
+#: src/routes/index.ts:52
 msgid "Home"
 msgstr ""
 
@@ -2986,6 +3026,10 @@ msgstr ""
 msgid "Info"
 msgstr ""
 
+#: src/constants/errors/user.ts:17
+msgid "Init user not exists"
+msgstr ""
+
 #: src/language/constants.ts:25
 msgid "Initial core upgrader error"
 msgstr ""
@@ -3054,6 +3098,10 @@ msgstr ""
 msgid "Invalid claims type"
 msgstr ""
 
+#: src/constants/errors/version.ts:2
+msgid "Invalid commit SHA"
+msgstr ""
+
 #: src/components/SystemRestore/SystemRestoreContent.vue:121
 msgid "Invalid file object"
 msgstr ""
@@ -3112,11 +3160,15 @@ msgstr ""
 msgid "Invalid security token format"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:10
+#: src/constants/errors/notification.ts:6
+msgid "Invalid Telegram Chat ID: cannot be zero"
+msgstr ""
+
+#: src/constants/errors/nginx_log.parser.ts:5
 msgid "Invalid timestamp format"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:26
+#: src/constants/errors/nginx_log.ts:23
 msgid "Invalid websocket message type"
 msgstr ""
 
@@ -3229,6 +3281,10 @@ msgstr ""
 msgid "Last Backup Time"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:52
+msgid "Last Check"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:197
 msgid "Last checked at"
 msgstr ""
@@ -3387,11 +3443,11 @@ msgstr ""
 msgid "Log file %{log_path} is not a regular file. If you are using nginx-ui in docker container, please refer to https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information."
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:14
+#: src/constants/errors/nginx_log.ts:11
 msgid "Log file does not exist"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:25
+#: src/constants/errors/nginx_log.ts:22
 msgid "Log file is not a regular file"
 msgstr ""
 
@@ -3403,7 +3459,7 @@ msgstr ""
 msgid "Log file not indexed yet"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:12
+#: src/constants/errors/nginx_log.ts:9
 msgid "Log indexer not available"
 msgstr ""
 
@@ -3411,12 +3467,20 @@ msgstr ""
 msgid "Log indexing completed! Loading updated data..."
 msgstr ""
 
+#: src/constants/errors/nginx_log.parser.ts:3
+msgid "Log line exceeds maximum length"
+msgstr ""
+
 #: src/routes/modules/nginx_log.ts:39
 #: src/views/nginx_log/NginxLogList.vue:430
 msgid "Log List"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:15
+#: src/constants/errors/nginx_log.indexer.ts:2
+msgid "Log parser is not initialized; call indexer.InitLogParser() before use"
+msgstr ""
+
+#: src/constants/errors/nginx_log.ts:12
 msgid "Log path is not under whitelist"
 msgstr ""
 
@@ -3634,15 +3698,15 @@ msgstr ""
 msgid "Model"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:28
+#: src/constants/errors/nginx_log.ts:25
 msgid "Modern analytics service not available"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:29
+#: src/constants/errors/nginx_log.ts:26
 msgid "Modern indexer service not available"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:27
+#: src/constants/errors/nginx_log.ts:24
 msgid "Modern searcher service not available"
 msgstr ""
 
@@ -4039,6 +4103,10 @@ msgstr ""
 msgid "No Action"
 msgstr ""
 
+#: src/constants/errors/cert.ts:25
+msgid "No certificate available"
+msgstr ""
+
 #: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:196
 #: src/views/nginx_log/dashboard/components/ChinaMapChart/ChinaMapChart.vue:232
 msgid "No China geographic data available"
@@ -4051,6 +4119,10 @@ msgstr ""
 msgid "No data"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:42
+msgid "No Data"
+msgstr ""
+
 #: src/views/nginx_log/structured/StructuredLogViewer.vue:820
 msgid "No entries in current page"
 msgstr ""
@@ -4096,6 +4168,10 @@ msgstr ""
 msgid "Node"
 msgstr ""
 
+#: src/constants/errors/analytic.ts:2
+msgid "Node analytics failed: {0}"
+msgstr ""
+
 #: src/views/preference/tabs/NodeSettings.vue:15
 msgid "Node name"
 msgstr ""
@@ -4911,6 +4987,10 @@ msgstr ""
 msgid "Reinstall"
 msgstr ""
 
+#: src/constants/errors/version.ts:3
+msgid "Release API request failed: {0}"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:270
 msgid "Release Note"
 msgstr ""
@@ -5713,6 +5793,10 @@ msgstr ""
 msgid "Sleep time between cache manager iterations"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:18
+msgid "Socket"
+msgstr ""
+
 #: src/views/nginx_log/structured/StructuredLogViewer.vue:735
 msgid "Sorted by"
 msgstr ""
@@ -6505,10 +6589,14 @@ msgstr ""
 msgid "Unsupported backup type: {0}"
 msgstr ""
 
-#: src/constants/errors/nginx_log.ts:11
+#: src/constants/errors/nginx_log.parser.ts:4
 msgid "Unsupported log format"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:6
+msgid "Update already in progress"
+msgstr ""
+
 #: src/views/user/UserProfile.vue:218
 msgid "Update Password"
 msgstr ""
@@ -6552,6 +6640,14 @@ msgstr ""
 msgid "Upgraded successfully"
 msgstr ""
 
+#: src/constants/errors/upgrader.ts:3
+msgid "Upgrader core digest is empty"
+msgstr ""
+
+#: src/constants/errors/upgrader.ts:2
+msgid "Upgrader core downloadUrl is empty"
+msgstr ""
+
 #: src/views/node/BatchUpgrader.vue:88
 #: src/views/system/Upgrade.vue:80
 msgid "Upgrading Nginx UI, please wait..."
@@ -6570,6 +6666,8 @@ msgid "Upload Folders"
 msgstr ""
 
 #: src/composables/useUpstreamStatus.ts:132
+#: src/routes/modules/upstream.ts:10
+#: src/views/upstream/SocketList.vue:25
 msgid "Upstream"
 msgstr ""
 
@@ -6577,6 +6675,10 @@ msgstr ""
 msgid "Upstream Name"
 msgstr ""
 
+#: src/views/upstream/SocketList.vue:134
+msgid "Upstream Sockets"
+msgstr ""
+
 #: src/views/namespace/columns.ts:59
 msgid "Upstream Test Type"
 msgstr ""
@@ -6823,7 +6925,7 @@ msgid "Workers"
 msgstr ""
 
 #: src/layouts/HeaderLayout.vue:61
-#: src/routes/index.ts:59
+#: src/routes/index.ts:61
 #: src/views/workspace/WorkSpace.vue:51
 msgid "Workspace"
 msgstr ""

Разница между файлами не показана из-за своего большого размера
+ 232 - 166
app/src/language/pt_PT/app.po


Разница между файлами не показана из-за своего большого размера
+ 236 - 164
app/src/language/ru_RU/app.po


Разница между файлами не показана из-за своего большого размера
+ 241 - 173
app/src/language/tr_TR/app.po


Разница между файлами не показана из-за своего большого размера
+ 246 - 173
app/src/language/uk_UA/app.po


Разница между файлами не показана из-за своего большого размера
+ 244 - 171
app/src/language/vi_VN/app.po


Разница между файлами не показана из-за своего большого размера
+ 222 - 195
app/src/language/zh_CN/app.po


Разница между файлами не показана из-за своего большого размера
+ 228 - 199
app/src/language/zh_TW/app.po


+ 2 - 0
app/src/routes/index.ts

@@ -18,6 +18,7 @@ import { sitesRoutes } from './modules/sites'
 import { streamsRoutes } from './modules/streams'
 import { systemRoutes } from './modules/system'
 import { terminalRoutes } from './modules/terminal'
+import { upstreamRoutes } from './modules/upstream'
 import { userRoutes } from './modules/user'
 import 'nprogress/nprogress.css'
 
@@ -26,6 +27,7 @@ const mainLayoutChildren: RouteRecordRaw[] = [
   ...dashboardRoutes,
   ...sitesRoutes,
   ...streamsRoutes,
+  ...upstreamRoutes,
   ...configRoutes,
   ...certificatesRoutes,
   ...terminalRoutes,

+ 14 - 0
app/src/routes/modules/upstream.ts

@@ -0,0 +1,14 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { ClusterOutlined } from '@ant-design/icons-vue'
+
+export const upstreamRoutes: RouteRecordRaw[] = [
+  {
+    path: 'upstream',
+    name: 'Upstream Management',
+    component: () => import('@/views/upstream/SocketList.vue'),
+    meta: {
+      name: () => $gettext('Upstream'),
+      icon: ClusterOutlined,
+    },
+  },
+]

+ 193 - 0
app/src/views/upstream/SocketList.vue

@@ -0,0 +1,193 @@
+<script setup lang="ts">
+import type { ColumnsType } from 'ant-design-vue/es/table'
+import type { SocketInfo } from '@/api/upstream'
+import { ReloadOutlined } from '@ant-design/icons-vue'
+import { message, Tag } from 'ant-design-vue'
+import upstream from '@/api/upstream'
+import { formatDateTime } from '@/lib/helper'
+import { useProxyAvailabilityStore } from '@/pinia/moudule/proxyAvailability'
+
+const dataSource = ref<SocketInfo[]>([])
+const loading = ref(false)
+
+// Initialize proxy availability store
+const proxyAvailabilityStore = useProxyAvailabilityStore()
+
+const columns: ColumnsType<SocketInfo> = [
+  {
+    title: () => $gettext('Socket'),
+    dataIndex: 'socket',
+    key: 'socket',
+    width: 200,
+    fixed: 'left',
+  },
+  {
+    title: () => $gettext('Upstream'),
+    dataIndex: 'upstream_name',
+    key: 'upstream_name',
+    width: 150,
+    customRender: ({ record }) => {
+      if (!record.upstream_name) {
+        return $gettext('Direct')
+      }
+      return record.upstream_name
+    },
+  },
+  {
+    title: () => $gettext('Health Status'),
+    key: 'status',
+    width: 180,
+    customRender: ({ record }) => {
+      if (!record.status) {
+        return $gettext('No Data')
+      }
+      const status = record.status
+      return h('div', { class: 'flex items-center' }, [
+        h(Tag, { color: status.online ? 'success' : 'error', class: 'mr-2' }, () => status.online ? 'Online' : 'Offline'),
+        status.online ? h('span', `${status.latency.toFixed(2)}ms`) : null,
+      ])
+    },
+  },
+  {
+    title: () => $gettext('Last Check'),
+    dataIndex: 'last_check',
+    key: 'last_check',
+    width: 180,
+    customRender: ({ text }) => {
+      return text ? formatDateTime(text) : '-'
+    },
+  },
+  {
+    title: () => $gettext('Health Check'),
+    key: 'enabled',
+    width: 150,
+    fixed: 'right',
+  },
+]
+
+// Merge socket list with real-time availability data
+function mergeSocketData(sockets: SocketInfo[]): SocketInfo[] {
+  return sockets.map(socket => {
+    // Get real-time status from availability store
+    const availabilityResult = proxyAvailabilityStore.availabilityResults[socket.socket]
+
+    if (availabilityResult) {
+      return {
+        ...socket,
+        status: {
+          online: availabilityResult.online,
+          latency: availabilityResult.latency,
+        },
+        last_check: new Date().toISOString(),
+      }
+    }
+
+    return socket
+  })
+}
+
+// Computed data source that combines socket list with real-time availability
+const enrichedDataSource = computed(() => {
+  return mergeSocketData(dataSource.value)
+})
+
+async function loadData() {
+  loading.value = true
+  try {
+    const res = await upstream.getSocketList()
+    dataSource.value = res.data
+  }
+  catch {
+    message.error('Failed to load socket data')
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+async function handleToggleEnabled(socket: string, enabled: boolean | string | number) {
+  const isEnabled = typeof enabled === 'boolean' ? enabled : Boolean(enabled)
+  try {
+    await upstream.updateSocketConfig(socket, { enabled: isEnabled })
+    message.success(`Health check ${isEnabled ? 'enabled' : 'disabled'} for ${socket}`)
+    await loadData()
+  }
+  catch {
+    message.error('Failed to update socket configuration')
+  }
+}
+
+// Start monitoring when component mounts
+onMounted(async () => {
+  await loadData()
+  // Start real-time monitoring for availability updates
+  proxyAvailabilityStore.startMonitoring()
+})
+
+// Clean up WebSocket connections when component unmounts
+onUnmounted(() => {
+  proxyAvailabilityStore.stopMonitoring()
+})
+</script>
+
+<template>
+  <ACard :title="$gettext('Upstream Sockets')">
+    <template #extra>
+      <AButton :loading @click="loadData">
+        <template #icon>
+          <ReloadOutlined />
+        </template>
+      </AButton>
+    </template>
+
+    <ATable
+      :columns="columns"
+      :data-source="enrichedDataSource"
+      :loading="loading"
+      :pagination="{
+        pageSize: 20,
+        showSizeChanger: true,
+        showTotal: (total: number) => `Total ${total} items`,
+      }"
+      :scroll="{ x: 1400 }"
+      row-key="socket"
+    >
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.key === 'socket'">
+          <ATag color="default" :bordered="false" class="socket-tag">
+            <template #icon>
+              <span v-if="record.type === 'upstream'" class="target-type-icon">U</span>
+              <span v-else class="target-type-icon">P</span>
+            </template>
+            {{ record.socket }}
+          </ATag>
+        </template>
+        <template v-if="column.key === 'enabled'">
+          <ASwitch
+            v-model:checked="record.enabled"
+            @change="handleToggleEnabled(record.socket, $event)"
+          />
+        </template>
+      </template>
+    </ATable>
+  </ACard>
+</template>
+
+<style scoped lang="less">
+.socket-tag {
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+  font-size: 12px;
+
+  .target-type-icon {
+    display: inline-block;
+    width: 12px;
+    height: 12px;
+    line-height: 12px;
+    text-align: center;
+    border-radius: 2px;
+    font-weight: bold;
+    font-size: 10px;
+    flex-shrink: 0;
+  }
+}
+</style>

BIN
cmd/generate_licenses/internal/license/licenses.xz


BIN
internal/cert/config/config.tar.xz


+ 24 - 0
internal/nginx/nginx_directives.json

@@ -1212,6 +1212,30 @@
       "https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_ciphers"
     ]
   },
+  "js_fetch_keepalive": {
+    "links": [
+      "https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive",
+      "https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive"
+    ]
+  },
+  "js_fetch_keepalive_requests": {
+    "links": [
+      "https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive_requests",
+      "https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive_requests"
+    ]
+  },
+  "js_fetch_keepalive_time": {
+    "links": [
+      "https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive_time",
+      "https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive_time"
+    ]
+  },
+  "js_fetch_keepalive_timeout": {
+    "links": [
+      "https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_keepalive_timeout",
+      "https://nginx.org/en/docs/stream/ngx_stream_js_module.html#js_fetch_keepalive_timeout"
+    ]
+  },
   "js_fetch_max_response_buffer_size": {
     "links": [
       "https://nginx.org/en/docs/http/ngx_http_js_module.html#js_fetch_max_response_buffer_size",

+ 43 - 8
internal/upstream/service.go

@@ -32,12 +32,13 @@ type Service struct {
 	availabilityMap map[string]*Status     // key: host:port
 	configTargets   map[string][]string    // configPath -> []targetKeys
 	// Public upstream definitions storage
-	Upstreams      map[string]*Definition // key: upstream name
-	upstreamsMutex sync.RWMutex
-	targetsMutex   sync.RWMutex
-	lastUpdateTime time.Time
-	testInProgress bool
-	testMutex      sync.Mutex
+	Upstreams                map[string]*Definition // key: upstream name
+	upstreamsMutex           sync.RWMutex
+	targetsMutex             sync.RWMutex
+	lastUpdateTime         time.Time
+	testInProgress         bool
+	testMutex              sync.Mutex
+	disabledSocketsChecker func() map[string]bool
 }
 
 var (
@@ -236,18 +237,30 @@ func (s *Service) PerformAvailabilityTest() {
 
 	// logger.Debug("Performing availability test for", targetCount, "unique targets")
 
+	// Get disabled sockets from database
+	disabledSockets := make(map[string]bool)
+	if s.disabledSocketsChecker != nil {
+		disabledSockets = s.disabledSocketsChecker()
+	}
+
 	// Separate targets into traditional and consul groups from the start
 	s.targetsMutex.RLock()
 	regularTargetKeys := make([]string, 0, len(s.targets))
 	consulTargets := make([]ProxyTarget, 0, len(s.targets))
 
 	for _, targetInfo := range s.targets {
+		// Check if this socket is disabled
+		socketAddr := formatSocketAddress(targetInfo.ProxyTarget.Host, targetInfo.ProxyTarget.Port)
+		if disabledSockets[socketAddr] {
+			// logger.Debug("Skipping disabled socket:", socketAddr)
+			continue
+		}
+
 		if targetInfo.ProxyTarget.IsConsul {
 			consulTargets = append(consulTargets, targetInfo.ProxyTarget)
 		} else {
 			// Traditional target - use properly formatted socket address
-			key := formatSocketAddress(targetInfo.ProxyTarget.Host, targetInfo.ProxyTarget.Port)
-			regularTargetKeys = append(regularTargetKeys, key)
+			regularTargetKeys = append(regularTargetKeys, socketAddr)
 		}
 	}
 	s.targetsMutex.RUnlock()
@@ -277,6 +290,28 @@ func (s *Service) PerformAvailabilityTest() {
 	// logger.Debug("Availability test completed for", len(results), "targets")
 }
 
+// findUpstreamNameForTarget finds which upstream a target belongs to
+func (s *Service) findUpstreamNameForTarget(target ProxyTarget) string {
+	s.upstreamsMutex.RLock()
+	defer s.upstreamsMutex.RUnlock()
+
+	targetKey := formatSocketAddress(target.Host, target.Port)
+	for name, upstream := range s.Upstreams {
+		for _, server := range upstream.Servers {
+			serverKey := formatSocketAddress(server.Host, server.Port)
+			if serverKey == targetKey {
+				return name
+			}
+		}
+	}
+	return ""
+}
+
+// SetDisabledSocketsChecker sets a callback function to check disabled sockets
+func (s *Service) SetDisabledSocketsChecker(checker func() map[string]bool) {
+	s.disabledSocketsChecker = checker
+}
+
 // ClearTargets clears all targets (useful for testing or reloading)
 func (s *Service) ClearTargets() {
 	s.targetsMutex.Lock()

+ 1 - 0
model/model.go

@@ -53,6 +53,7 @@ func GenerateAllModel() []any {
 		AutoBackup{},
 		SiteConfig{},
 		NginxLogIndex{},
+		UpstreamConfig{},
 	}
 }
 

+ 7 - 0
model/upstream_config.go

@@ -0,0 +1,7 @@
+package model
+
+type UpstreamConfig struct {
+	Model
+	Socket  string `json:"socket" gorm:"uniqueIndex"` // host:port address
+	Enabled bool   `json:"enabled" gorm:"default:true"`
+}

+ 8 - 0
query/gen.go

@@ -35,6 +35,7 @@ var (
 	Site           *site
 	SiteConfig     *siteConfig
 	Stream         *stream
+	UpstreamConfig *upstreamConfig
 	User           *user
 )
 
@@ -58,6 +59,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	Site = &Q.Site
 	SiteConfig = &Q.SiteConfig
 	Stream = &Q.Stream
+	UpstreamConfig = &Q.UpstreamConfig
 	User = &Q.User
 }
 
@@ -82,6 +84,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		Site:           newSite(db, opts...),
 		SiteConfig:     newSiteConfig(db, opts...),
 		Stream:         newStream(db, opts...),
+		UpstreamConfig: newUpstreamConfig(db, opts...),
 		User:           newUser(db, opts...),
 	}
 }
@@ -107,6 +110,7 @@ type Query struct {
 	Site           site
 	SiteConfig     siteConfig
 	Stream         stream
+	UpstreamConfig upstreamConfig
 	User           user
 }
 
@@ -133,6 +137,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		Site:           q.Site.clone(db),
 		SiteConfig:     q.SiteConfig.clone(db),
 		Stream:         q.Stream.clone(db),
+		UpstreamConfig: q.UpstreamConfig.clone(db),
 		User:           q.User.clone(db),
 	}
 }
@@ -166,6 +171,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		Site:           q.Site.replaceDB(db),
 		SiteConfig:     q.SiteConfig.replaceDB(db),
 		Stream:         q.Stream.replaceDB(db),
+		UpstreamConfig: q.UpstreamConfig.replaceDB(db),
 		User:           q.User.replaceDB(db),
 	}
 }
@@ -189,6 +195,7 @@ type queryCtx struct {
 	Site           *siteDo
 	SiteConfig     *siteConfigDo
 	Stream         *streamDo
+	UpstreamConfig *upstreamConfigDo
 	User           *userDo
 }
 
@@ -212,6 +219,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		Site:           q.Site.WithContext(ctx),
 		SiteConfig:     q.SiteConfig.WithContext(ctx),
 		Stream:         q.Stream.WithContext(ctx),
+		UpstreamConfig: q.UpstreamConfig.WithContext(ctx),
 		User:           q.User.WithContext(ctx),
 	}
 }

+ 1 - 5
query/llm_sessions.gen.go

@@ -32,7 +32,6 @@ func newLLMSession(db *gorm.DB, opts ...gen.DOOption) lLMSession {
 	_lLMSession.SessionID = field.NewString(tableName, "session_id")
 	_lLMSession.Title = field.NewString(tableName, "title")
 	_lLMSession.Path = field.NewString(tableName, "path")
-	_lLMSession.SessionType = field.NewString(tableName, "session_type")
 	_lLMSession.Messages = field.NewField(tableName, "messages")
 	_lLMSession.MessageCount = field.NewInt(tableName, "message_count")
 	_lLMSession.IsActive = field.NewBool(tableName, "is_active")
@@ -53,7 +52,6 @@ type lLMSession struct {
 	SessionID    field.String
 	Title        field.String
 	Path         field.String
-	SessionType  field.String
 	Messages     field.Field
 	MessageCount field.Int
 	IsActive     field.Bool
@@ -80,7 +78,6 @@ func (l *lLMSession) updateTableName(table string) *lLMSession {
 	l.SessionID = field.NewString(table, "session_id")
 	l.Title = field.NewString(table, "title")
 	l.Path = field.NewString(table, "path")
-	l.SessionType = field.NewString(table, "session_type")
 	l.Messages = field.NewField(table, "messages")
 	l.MessageCount = field.NewInt(table, "message_count")
 	l.IsActive = field.NewBool(table, "is_active")
@@ -103,12 +100,11 @@ func (l *lLMSession) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
 }
 
 func (l *lLMSession) fillFieldMap() {
-	l.fieldMap = make(map[string]field.Expr, 11)
+	l.fieldMap = make(map[string]field.Expr, 10)
 	l.fieldMap["id"] = l.ID
 	l.fieldMap["session_id"] = l.SessionID
 	l.fieldMap["title"] = l.Title
 	l.fieldMap["path"] = l.Path
-	l.fieldMap["session_type"] = l.SessionType
 	l.fieldMap["messages"] = l.Messages
 	l.fieldMap["message_count"] = l.MessageCount
 	l.fieldMap["is_active"] = l.IsActive

+ 370 - 0
query/upstream_configs.gen.go

@@ -0,0 +1,370 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/model"
+)
+
+func newUpstreamConfig(db *gorm.DB, opts ...gen.DOOption) upstreamConfig {
+	_upstreamConfig := upstreamConfig{}
+
+	_upstreamConfig.upstreamConfigDo.UseDB(db, opts...)
+	_upstreamConfig.upstreamConfigDo.UseModel(&model.UpstreamConfig{})
+
+	tableName := _upstreamConfig.upstreamConfigDo.TableName()
+	_upstreamConfig.ALL = field.NewAsterisk(tableName)
+	_upstreamConfig.ID = field.NewUint64(tableName, "id")
+	_upstreamConfig.CreatedAt = field.NewTime(tableName, "created_at")
+	_upstreamConfig.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_upstreamConfig.DeletedAt = field.NewField(tableName, "deleted_at")
+	_upstreamConfig.Socket = field.NewString(tableName, "socket")
+	_upstreamConfig.Enabled = field.NewBool(tableName, "enabled")
+
+	_upstreamConfig.fillFieldMap()
+
+	return _upstreamConfig
+}
+
+type upstreamConfig struct {
+	upstreamConfigDo
+
+	ALL       field.Asterisk
+	ID        field.Uint64
+	CreatedAt field.Time
+	UpdatedAt field.Time
+	DeletedAt field.Field
+	Socket    field.String
+	Enabled   field.Bool
+
+	fieldMap map[string]field.Expr
+}
+
+func (u upstreamConfig) Table(newTableName string) *upstreamConfig {
+	u.upstreamConfigDo.UseTable(newTableName)
+	return u.updateTableName(newTableName)
+}
+
+func (u upstreamConfig) As(alias string) *upstreamConfig {
+	u.upstreamConfigDo.DO = *(u.upstreamConfigDo.As(alias).(*gen.DO))
+	return u.updateTableName(alias)
+}
+
+func (u *upstreamConfig) updateTableName(table string) *upstreamConfig {
+	u.ALL = field.NewAsterisk(table)
+	u.ID = field.NewUint64(table, "id")
+	u.CreatedAt = field.NewTime(table, "created_at")
+	u.UpdatedAt = field.NewTime(table, "updated_at")
+	u.DeletedAt = field.NewField(table, "deleted_at")
+	u.Socket = field.NewString(table, "socket")
+	u.Enabled = field.NewBool(table, "enabled")
+
+	u.fillFieldMap()
+
+	return u
+}
+
+func (u *upstreamConfig) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := u.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (u *upstreamConfig) fillFieldMap() {
+	u.fieldMap = make(map[string]field.Expr, 6)
+	u.fieldMap["id"] = u.ID
+	u.fieldMap["created_at"] = u.CreatedAt
+	u.fieldMap["updated_at"] = u.UpdatedAt
+	u.fieldMap["deleted_at"] = u.DeletedAt
+	u.fieldMap["socket"] = u.Socket
+	u.fieldMap["enabled"] = u.Enabled
+}
+
+func (u upstreamConfig) clone(db *gorm.DB) upstreamConfig {
+	u.upstreamConfigDo.ReplaceConnPool(db.Statement.ConnPool)
+	return u
+}
+
+func (u upstreamConfig) replaceDB(db *gorm.DB) upstreamConfig {
+	u.upstreamConfigDo.ReplaceDB(db)
+	return u
+}
+
+type upstreamConfigDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (u upstreamConfigDo) FirstByID(id uint64) (result *model.UpstreamConfig, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = u.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=@id
+func (u upstreamConfigDo) DeleteByID(id uint64) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update upstream_configs set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = u.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (u upstreamConfigDo) Debug() *upstreamConfigDo {
+	return u.withDO(u.DO.Debug())
+}
+
+func (u upstreamConfigDo) WithContext(ctx context.Context) *upstreamConfigDo {
+	return u.withDO(u.DO.WithContext(ctx))
+}
+
+func (u upstreamConfigDo) ReadDB() *upstreamConfigDo {
+	return u.Clauses(dbresolver.Read)
+}
+
+func (u upstreamConfigDo) WriteDB() *upstreamConfigDo {
+	return u.Clauses(dbresolver.Write)
+}
+
+func (u upstreamConfigDo) Session(config *gorm.Session) *upstreamConfigDo {
+	return u.withDO(u.DO.Session(config))
+}
+
+func (u upstreamConfigDo) Clauses(conds ...clause.Expression) *upstreamConfigDo {
+	return u.withDO(u.DO.Clauses(conds...))
+}
+
+func (u upstreamConfigDo) Returning(value interface{}, columns ...string) *upstreamConfigDo {
+	return u.withDO(u.DO.Returning(value, columns...))
+}
+
+func (u upstreamConfigDo) Not(conds ...gen.Condition) *upstreamConfigDo {
+	return u.withDO(u.DO.Not(conds...))
+}
+
+func (u upstreamConfigDo) Or(conds ...gen.Condition) *upstreamConfigDo {
+	return u.withDO(u.DO.Or(conds...))
+}
+
+func (u upstreamConfigDo) Select(conds ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.Select(conds...))
+}
+
+func (u upstreamConfigDo) Where(conds ...gen.Condition) *upstreamConfigDo {
+	return u.withDO(u.DO.Where(conds...))
+}
+
+func (u upstreamConfigDo) Order(conds ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.Order(conds...))
+}
+
+func (u upstreamConfigDo) Distinct(cols ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.Distinct(cols...))
+}
+
+func (u upstreamConfigDo) Omit(cols ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.Omit(cols...))
+}
+
+func (u upstreamConfigDo) Join(table schema.Tabler, on ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.Join(table, on...))
+}
+
+func (u upstreamConfigDo) LeftJoin(table schema.Tabler, on ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.LeftJoin(table, on...))
+}
+
+func (u upstreamConfigDo) RightJoin(table schema.Tabler, on ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.RightJoin(table, on...))
+}
+
+func (u upstreamConfigDo) Group(cols ...field.Expr) *upstreamConfigDo {
+	return u.withDO(u.DO.Group(cols...))
+}
+
+func (u upstreamConfigDo) Having(conds ...gen.Condition) *upstreamConfigDo {
+	return u.withDO(u.DO.Having(conds...))
+}
+
+func (u upstreamConfigDo) Limit(limit int) *upstreamConfigDo {
+	return u.withDO(u.DO.Limit(limit))
+}
+
+func (u upstreamConfigDo) Offset(offset int) *upstreamConfigDo {
+	return u.withDO(u.DO.Offset(offset))
+}
+
+func (u upstreamConfigDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *upstreamConfigDo {
+	return u.withDO(u.DO.Scopes(funcs...))
+}
+
+func (u upstreamConfigDo) Unscoped() *upstreamConfigDo {
+	return u.withDO(u.DO.Unscoped())
+}
+
+func (u upstreamConfigDo) Create(values ...*model.UpstreamConfig) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return u.DO.Create(values)
+}
+
+func (u upstreamConfigDo) CreateInBatches(values []*model.UpstreamConfig, batchSize int) error {
+	return u.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (u upstreamConfigDo) Save(values ...*model.UpstreamConfig) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return u.DO.Save(values)
+}
+
+func (u upstreamConfigDo) First() (*model.UpstreamConfig, error) {
+	if result, err := u.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.UpstreamConfig), nil
+	}
+}
+
+func (u upstreamConfigDo) Take() (*model.UpstreamConfig, error) {
+	if result, err := u.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.UpstreamConfig), nil
+	}
+}
+
+func (u upstreamConfigDo) Last() (*model.UpstreamConfig, error) {
+	if result, err := u.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.UpstreamConfig), nil
+	}
+}
+
+func (u upstreamConfigDo) Find() ([]*model.UpstreamConfig, error) {
+	result, err := u.DO.Find()
+	return result.([]*model.UpstreamConfig), err
+}
+
+func (u upstreamConfigDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UpstreamConfig, err error) {
+	buf := make([]*model.UpstreamConfig, 0, batchSize)
+	err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (u upstreamConfigDo) FindInBatches(result *[]*model.UpstreamConfig, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return u.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (u upstreamConfigDo) Attrs(attrs ...field.AssignExpr) *upstreamConfigDo {
+	return u.withDO(u.DO.Attrs(attrs...))
+}
+
+func (u upstreamConfigDo) Assign(attrs ...field.AssignExpr) *upstreamConfigDo {
+	return u.withDO(u.DO.Assign(attrs...))
+}
+
+func (u upstreamConfigDo) Joins(fields ...field.RelationField) *upstreamConfigDo {
+	for _, _f := range fields {
+		u = *u.withDO(u.DO.Joins(_f))
+	}
+	return &u
+}
+
+func (u upstreamConfigDo) Preload(fields ...field.RelationField) *upstreamConfigDo {
+	for _, _f := range fields {
+		u = *u.withDO(u.DO.Preload(_f))
+	}
+	return &u
+}
+
+func (u upstreamConfigDo) FirstOrInit() (*model.UpstreamConfig, error) {
+	if result, err := u.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.UpstreamConfig), nil
+	}
+}
+
+func (u upstreamConfigDo) FirstOrCreate() (*model.UpstreamConfig, error) {
+	if result, err := u.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.UpstreamConfig), nil
+	}
+}
+
+func (u upstreamConfigDo) FindByPage(offset int, limit int) (result []*model.UpstreamConfig, count int64, err error) {
+	result, err = u.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = u.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (u upstreamConfigDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = u.Count()
+	if err != nil {
+		return
+	}
+
+	err = u.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (u upstreamConfigDo) Scan(result interface{}) (err error) {
+	return u.DO.Scan(result)
+}
+
+func (u upstreamConfigDo) Delete(models ...*model.UpstreamConfig) (result gen.ResultInfo, err error) {
+	return u.DO.Delete(models)
+}
+
+func (u *upstreamConfigDo) withDO(do gen.Dao) *upstreamConfigDo {
+	u.DO = *do.(*gen.DO)
+	return u
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов