Browse Source

Merge pull request #1310 from 0xJacky/feat/site-navigation

feat(dashboard): add sites navigation #1054
Jacky 1 day ago
parent
commit
89046bfc79
49 changed files with 4131 additions and 284 deletions
  1. 1 1
      api/cluster/namespace.go
  2. 9 9
      api/config/list.go
  3. 0 1
      api/openai/router.go
  4. 5 5
      api/sites/list.go
  5. 12 0
      api/sites/router.go
  6. 2 2
      api/sites/site.go
  7. 195 0
      api/sites/sitecheck.go
  8. 164 0
      api/sites/websocket.go
  9. 6 6
      api/streams/streams.go
  10. 2 2
      api/user/passkey.go
  11. 4 0
      app/components.d.ts
  12. 151 0
      app/src/api/site_navigation.ts
  13. 8 8
      app/src/components/Notification/notifications.ts
  14. 34 0
      app/src/constants/site-status.ts
  15. 8 0
      app/src/routes/modules/dashboard.ts
  16. 292 0
      app/src/views/dashboard/SiteNavigation.vue
  17. 323 0
      app/src/views/dashboard/components/SiteCard.vue
  18. 709 0
      app/src/views/dashboard/components/SiteHealthCheckModal.vue
  19. 106 0
      app/src/views/dashboard/components/SiteNavigationToolbar.vue
  20. 1 3
      go.mod
  21. 2 163
      go.sum
  22. 7 7
      internal/analytic/node.go
  23. 2 2
      internal/analytic/node_stat.go
  24. 2 2
      internal/cert/sync.go
  25. 13 13
      internal/config/config.go
  26. 5 5
      internal/config/generic_list.go
  27. 6 6
      internal/config/sync.go
  28. 20 20
      internal/helper/copy.go
  29. 1 1
      internal/helper/docker.go
  30. 5 0
      internal/kernel/boot.go
  31. 1 1
      internal/migrate/6.rename_environments_to_nodes.go
  32. 7 0
      internal/nginx/nginx_directives.json
  33. 3 3
      internal/notification/wecom.go
  34. 8 8
      internal/site/list.go
  35. 1 1
      internal/site/save.go
  36. 651 0
      internal/sitecheck/checker.go
  37. 452 0
      internal/sitecheck/enhanced_checker.go
  38. 95 0
      internal/sitecheck/ordering.go
  39. 155 0
      internal/sitecheck/service.go
  40. 55 0
      internal/sitecheck/types.go
  41. 7 7
      internal/stream/errors.go
  42. 5 5
      internal/stream/list.go
  43. 1 1
      model/chatgpt_log.go
  44. 1 0
      model/model.go
  45. 1 1
      model/namespace.go
  46. 1 1
      model/node.go
  47. 110 0
      model/site_config.go
  48. 8 0
      query/gen.go
  49. 474 0
      query/site_configs.gen.go

+ 1 - 1
api/cluster/namespace.go

@@ -139,4 +139,4 @@ func RestartNginx(c *gin.Context) {
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
-}
+}

+ 9 - 9
api/config/list.go

@@ -17,7 +17,7 @@ import (
 
 // ConfigFileEntity represents a generic configuration file entity
 type ConfigFileEntity struct {
-	path       string
+	path        string
 	namespaceID uint64
 	namespace   *model.Namespace
 }
@@ -68,7 +68,7 @@ func GetConfigs(c *gin.Context) {
 		Search:      search,
 		OrderBy:     sortBy,
 		Sort:        order,
-		NamespaceID:  namespaceID,
+		NamespaceID: namespaceID,
 		IncludeDirs: true, // Keep directories for the list.go endpoint
 	}
 
@@ -90,7 +90,7 @@ func GetConfigs(c *gin.Context) {
 		// For generic config files, we don't have database records
 		// so namespaceID and namespace will be 0 and nil
 		entity := &ConfigFileEntity{
-			path:       filepath.Join(nginx.GetConfPath(dir), file.Name()),
+			path:        filepath.Join(nginx.GetConfPath(dir), file.Name()),
 			namespaceID: 0,
 			namespace:   nil,
 		}
@@ -124,14 +124,14 @@ func GetConfigs(c *gin.Context) {
 func createConfigBuilder(dir string) config.ConfigBuilder {
 	return func(fileName string, fileInfo os.FileInfo, status config.ConfigStatus, namespaceID uint64, namespace *model.Namespace) config.Config {
 		return config.Config{
-			Name:       fileName,
-			ModifiedAt: fileInfo.ModTime(),
-			Size:       fileInfo.Size(),
-			IsDir:      fileInfo.IsDir(),
-			Status:     status,
+			Name:        fileName,
+			ModifiedAt:  fileInfo.ModTime(),
+			Size:        fileInfo.Size(),
+			IsDir:       fileInfo.IsDir(),
+			Status:      status,
 			NamespaceID: namespaceID,
 			Namespace:   namespace,
-			Dir:        dir,
+			Dir:         dir,
 		}
 	}
 }

+ 0 - 1
api/openai/router.go

@@ -2,7 +2,6 @@ package openai
 
 import "github.com/gin-gonic/gin"
 
-
 func InitRouter(r *gin.RouterGroup) {
 	// ChatGPT
 	r.POST("chatgpt", MakeChatCompletionRequest)

+ 5 - 5
api/sites/list.go

@@ -13,11 +13,11 @@ import (
 func GetSiteList(c *gin.Context) {
 	// Parse query parameters
 	options := &site.ListOptions{
-		Search:     c.Query("search"),
-		Name:       c.Query("name"),
-		Status:     c.Query("status"),
-		OrderBy:    c.Query("sort_by"),
-		Sort:       c.DefaultQuery("order", "desc"),
+		Search:      c.Query("search"),
+		Name:        c.Query("name"),
+		Status:      c.Query("status"),
+		OrderBy:     c.Query("sort_by"),
+		Sort:        c.DefaultQuery("order", "desc"),
 		NamespaceID: cast.ToUint64(c.Query("env_group_id")),
 	}
 

+ 12 - 0
api/sites/router.go

@@ -3,6 +3,9 @@ package sites
 import "github.com/gin-gonic/gin"
 
 func InitRouter(r *gin.RouterGroup) {
+	// Initialize WebSocket notifications for site checking
+	InitWebSocketNotifications()
+
 	r.GET("sites", GetSiteList)
 	r.GET("sites/:name", GetSite)
 	r.PUT("sites", BatchUpdateSites)
@@ -10,6 +13,15 @@ func InitRouter(r *gin.RouterGroup) {
 	r.POST("auto_cert/:name", AddDomainToAutoCert)
 	r.DELETE("auto_cert/:name", RemoveDomainFromAutoCert)
 
+	// site navigation endpoints
+	r.GET("site_navigation", GetSiteNavigation)
+	r.GET("site_navigation/status", GetSiteNavigationStatus)
+	r.POST("site_navigation/order", UpdateSiteOrder)
+	r.GET("site_navigation/health_check/:id", GetHealthCheck)
+	r.PUT("site_navigation/health_check/:id", UpdateHealthCheck)
+	r.POST("site_navigation/test_health_check/:id", TestHealthCheck)
+	r.GET("site_navigation_ws", SiteNavigationWebSocket)
+
 	// rename site
 	r.POST("sites/:name/rename", RenameSite)
 	// enable site

+ 2 - 2
api/sites/site.go

@@ -20,7 +20,7 @@ import (
 // buildProxyTargets processes proxy targets similar to list.go logic
 func buildProxyTargets(fileName string) []site.ProxyTarget {
 	indexedSite := site.GetIndexedSite(fileName)
-	
+
 	// Convert proxy targets, expanding upstream references
 	var proxyTargets []site.ProxyTarget
 	upstreamService := upstream.GetUpstreamService()
@@ -132,7 +132,7 @@ func SaveSite(c *gin.Context) {
 
 	var json struct {
 		Content     string   `json:"content" binding:"required"`
-		NamespaceID  uint64   `json:"env_group_id"`
+		NamespaceID uint64   `json:"env_group_id"`
 		SyncNodeIDs []uint64 `json:"sync_node_ids"`
 		Overwrite   bool     `json:"overwrite"`
 		PostAction  string   `json:"post_action"`

+ 195 - 0
api/sites/sitecheck.go

@@ -0,0 +1,195 @@
+package sites
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/sitecheck"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
+	"github.com/uozi-tech/cosy"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// GetSiteNavigation returns all sites for navigation dashboard
+func GetSiteNavigation(c *gin.Context) {
+	service := sitecheck.GetService()
+	sites := service.GetSites()
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": sites,
+	})
+}
+
+// GetSiteNavigationStatus returns the status of site checking service
+func GetSiteNavigationStatus(c *gin.Context) {
+	service := sitecheck.GetService()
+
+	c.JSON(http.StatusOK, gin.H{
+		"running": service.IsRunning(),
+	})
+}
+
+// UpdateSiteOrder updates the custom order of sites
+func UpdateSiteOrder(c *gin.Context) {
+	var req struct {
+		OrderedIds []uint64 `json:"ordered_ids" binding:"required"`
+	}
+
+	if !cosy.BindAndValid(c, &req) {
+		return
+	}
+
+	if err := updateSiteOrderBatchByIds(req.OrderedIds); err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "Order updated successfully",
+	})
+}
+
+// updateSiteOrderBatchByIds updates site order in batch using IDs
+func updateSiteOrderBatchByIds(orderedIds []uint64) error {
+	sc := query.SiteConfig
+
+	for i, id := range orderedIds {
+		if _, err := sc.Where(sc.ID.Eq(id)).Update(sc.CustomOrder, i); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// GetHealthCheck gets health check configuration for a site
+func GetHealthCheck(c *gin.Context) {
+	id := cast.ToUint64(c.Param("id"))
+
+	sc := query.SiteConfig
+	siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	ensureHealthCheckConfig(siteConfig)
+
+	c.JSON(http.StatusOK, siteConfig)
+}
+
+// createDefaultHealthCheckConfig creates default health check configuration
+func createDefaultHealthCheckConfig() *model.HealthCheckConfig {
+	return &model.HealthCheckConfig{
+		Protocol:       "http",
+		Method:         "GET",
+		Path:           "/",
+		ExpectedStatus: []int{200},
+		GRPCMethod:     "Check",
+	}
+}
+
+// ensureHealthCheckConfig ensures health check config is not nil
+func ensureHealthCheckConfig(siteConfig *model.SiteConfig) {
+	if siteConfig.HealthCheckConfig == nil {
+		siteConfig.HealthCheckConfig = createDefaultHealthCheckConfig()
+	}
+}
+
+// UpdateHealthCheck updates health check configuration for a site
+func UpdateHealthCheck(c *gin.Context) {
+	id := cast.ToUint64(c.Param("id"))
+
+	var req model.SiteConfig
+
+	if !cosy.BindAndValid(c, &req) {
+		return
+	}
+
+	sc := query.SiteConfig
+	siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	siteConfig.HealthCheckEnabled = req.HealthCheckEnabled
+	siteConfig.CheckInterval = req.CheckInterval
+	siteConfig.Timeout = req.Timeout
+	siteConfig.UserAgent = req.UserAgent
+	siteConfig.MaxRedirects = req.MaxRedirects
+	siteConfig.FollowRedirects = req.FollowRedirects
+	siteConfig.CheckFavicon = req.CheckFavicon
+
+	if req.HealthCheckConfig != nil {
+		siteConfig.HealthCheckConfig = req.HealthCheckConfig
+	}
+
+	if err = query.SiteConfig.Save(siteConfig); err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "Health check configuration updated successfully",
+	})
+}
+
+// TestHealthCheck tests a health check configuration without saving it
+func TestHealthCheck(c *gin.Context) {
+	id := cast.ToUint64(c.Param("id"))
+
+	var req struct {
+		Config *model.HealthCheckConfig `json:"config" binding:"required"`
+	}
+
+	if !cosy.BindAndValid(c, &req) {
+		return
+	}
+
+	// Get site config to determine the host for testing
+	sc := query.SiteConfig
+	siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// Create enhanced checker and test the configuration
+	enhancedChecker := sitecheck.NewEnhancedSiteChecker()
+
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// Convert host to URL for testing
+	testURL := siteConfig.Scheme + "://" + siteConfig.Host
+	result, err := enhancedChecker.CheckSiteWithConfig(ctx, testURL, req.Config)
+
+	if err != nil {
+		logger.Errorf("Health check test failed for %s: %v", siteConfig.Host, err)
+		c.JSON(http.StatusOK, gin.H{
+			"success":       false,
+			"error":         err.Error(),
+			"response_time": 0,
+		})
+		return
+	}
+
+	success := result.Status == "online"
+	errorMsg := ""
+	if !success && result.Error != "" {
+		errorMsg = result.Error
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success":       success,
+		"response_time": result.ResponseTime,
+		"status":        result.Status,
+		"status_code":   result.StatusCode,
+		"error":         errorMsg,
+	})
+}

+ 164 - 0
api/sites/websocket.go

@@ -0,0 +1,164 @@
+package sites
+
+import (
+	"net/http"
+	"sync"
+
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/sitecheck"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// WebSocket message types
+const (
+	MessageTypeInitial = "initial"
+	MessageTypeUpdate  = "update"
+	MessageTypeRefresh = "refresh"
+	MessageTypePing    = "ping"
+	MessageTypePong    = "pong"
+)
+
+// ClientMessage represents incoming WebSocket messages from client
+type ClientMessage struct {
+	Type string `json:"type"`
+}
+
+// ServerMessage represents outgoing WebSocket messages to client
+type ServerMessage struct {
+	Type string                `json:"type"`
+	Data []*sitecheck.SiteInfo `json:"data,omitempty"`
+}
+
+// PongMessage represents a pong response
+type PongMessage struct {
+	Type string `json:"type"`
+}
+
+var upgrader = websocket.Upgrader{
+	CheckOrigin: func(r *http.Request) bool {
+		return true
+	},
+}
+
+// WebSocket connection manager
+type WSManager struct {
+	connections map[*websocket.Conn]bool
+	mutex       sync.RWMutex
+}
+
+var wsManager = &WSManager{
+	connections: make(map[*websocket.Conn]bool),
+}
+
+// AddConnection adds a WebSocket connection to the manager
+func (wm *WSManager) AddConnection(conn *websocket.Conn) {
+	wm.mutex.Lock()
+	defer wm.mutex.Unlock()
+	wm.connections[conn] = true
+}
+
+// RemoveConnection removes a WebSocket connection from the manager
+func (wm *WSManager) RemoveConnection(conn *websocket.Conn) {
+	wm.mutex.Lock()
+	defer wm.mutex.Unlock()
+	delete(wm.connections, conn)
+}
+
+// BroadcastUpdate sends updates to all connected WebSocket clients
+func (wm *WSManager) BroadcastUpdate(sites []*sitecheck.SiteInfo) {
+	wm.mutex.RLock()
+	defer wm.mutex.RUnlock()
+
+	for conn := range wm.connections {
+		go func(c *websocket.Conn) {
+			if err := sendSiteData(c, MessageTypeUpdate, sites); err != nil {
+				logger.Error("Failed to send broadcast update:", err)
+				wm.RemoveConnection(c)
+				c.Close()
+			}
+		}(conn)
+	}
+}
+
+// GetManager returns the global WebSocket manager instance
+func GetManager() *WSManager {
+	return wsManager
+}
+
+// InitWebSocketNotifications sets up the callback for site check updates
+func InitWebSocketNotifications() {
+	service := sitecheck.GetService()
+	service.SetUpdateCallback(func(sites []*sitecheck.SiteInfo) {
+		wsManager.BroadcastUpdate(sites)
+	})
+}
+
+// SiteNavigationWebSocket handles WebSocket connections for real-time site status updates
+func SiteNavigationWebSocket(c *gin.Context) {
+	ctx := c.Request.Context()
+
+	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+	if err != nil {
+		logger.Error("WebSocket upgrade failed:", err)
+		return
+	}
+	defer func() {
+		wsManager.RemoveConnection(conn)
+		conn.Close()
+	}()
+
+	logger.Info("Site navigation WebSocket connection established")
+
+	// Register connection with manager
+	wsManager.AddConnection(conn)
+
+	service := sitecheck.GetService()
+
+	// Send initial data
+	if err := sendSiteData(conn, MessageTypeInitial, service.GetSites()); err != nil {
+		logger.Error("Failed to send initial data:", err)
+		return
+	}
+
+	// Handle incoming messages from client
+	go handleClientMessages(conn, service)
+
+	<-ctx.Done()
+	logger.Info("Request context cancelled, closing WebSocket")
+}
+
+// sendSiteData sends site data via WebSocket
+func sendSiteData(conn *websocket.Conn, msgType string, sites []*sitecheck.SiteInfo) error {
+	message := ServerMessage{
+		Type: msgType,
+		Data: sites,
+	}
+	return conn.WriteJSON(message)
+}
+
+// handleClientMessages handles incoming WebSocket messages
+func handleClientMessages(conn *websocket.Conn, service *sitecheck.Service) {
+	for {
+		var msg ClientMessage
+		if err := conn.ReadJSON(&msg); err != nil {
+			if helper.IsUnexpectedWebsocketError(err) {
+				logger.Error("WebSocket read error:", err)
+			}
+			return
+		}
+
+		switch msg.Type {
+		case MessageTypeRefresh:
+			logger.Info("Client requested site refresh")
+			service.RefreshSites()
+		case MessageTypePing:
+			pongMsg := PongMessage{Type: MessageTypePong}
+			if err := conn.WriteJSON(pongMsg); err != nil {
+				logger.Error("Failed to send pong:", err)
+				return
+			}
+		}
+	}
+}

+ 6 - 6
api/streams/streams.go

@@ -35,7 +35,7 @@ type Stream struct {
 // buildProxyTargets processes stream proxy targets similar to list.go logic
 func buildStreamProxyTargets(fileName string) []config.ProxyTarget {
 	indexedStream := stream.GetIndexedStream(fileName)
-	
+
 	// Convert proxy targets, expanding upstream references
 	var proxyTargets []config.ProxyTarget
 	upstreamService := upstream.GetUpstreamService()
@@ -67,11 +67,11 @@ func buildStreamProxyTargets(fileName string) []config.ProxyTarget {
 func GetStreams(c *gin.Context) {
 	// Parse query parameters
 	options := &stream.ListOptions{
-		Search:     c.Query("search"),
-		Name:       c.Query("name"),
-		Status:     c.Query("status"),
-		OrderBy:    c.Query("order_by"),
-		Sort:       c.DefaultQuery("sort", "desc"),
+		Search:      c.Query("search"),
+		Name:        c.Query("name"),
+		Status:      c.Query("status"),
+		OrderBy:     c.Query("order_by"),
+		Sort:        c.DefaultQuery("sort", "desc"),
 		NamespaceID: cast.ToUint64(c.Query("namespace_id")),
 	}
 

+ 2 - 2
api/user/passkey.go

@@ -155,8 +155,8 @@ func FinishPasskeyLogin(c *gin.Context) {
 	secureSessionID := user.SetSecureSessionID(outUser.ID)
 
 	c.JSON(http.StatusOK, LoginResponse{
-		Code:            LoginSuccess,
-		Message:         "ok",
+		Code:               LoginSuccess,
+		Message:            "ok",
 		AccessTokenPayload: token,
 		SecureSessionID:    secureSessionID,
 	})

+ 4 - 0
app/components.d.ts

@@ -23,6 +23,7 @@ declare module 'vue' {
     ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
     AComment: typeof import('ant-design-vue/es')['Comment']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
+    ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
     ADivider: typeof import('ant-design-vue/es')['Divider']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
     ADropdown: typeof import('ant-design-vue/es')['Dropdown']
@@ -48,8 +49,11 @@ declare module 'vue' {
     APopover: typeof import('ant-design-vue/es')['Popover']
     AProgress: typeof import('ant-design-vue/es')['Progress']
     AQrcode: typeof import('ant-design-vue/es')['QRCode']
+    ARadio: typeof import('ant-design-vue/es')['Radio']
+    ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
     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']

+ 151 - 0
app/src/api/site_navigation.ts

@@ -0,0 +1,151 @@
+import type { SiteStatusType } from '@/constants/site-status'
+import { http } from '@uozi-admin/request'
+import ws from '@/lib/websocket'
+
+export interface SiteInfo {
+  id: number // primary identifier for API operations
+  host: string // host:port format
+  port: number
+  scheme: string // http, https, grpc, grpcs
+  display_url: string // computed URL for display
+  name: string
+  status: SiteStatusType
+  status_code: number
+  response_time: number
+  favicon_url: string
+  favicon_data: string
+  title: string
+  last_checked: number
+  error?: string
+  // Legacy fields for backward compatibility
+  url?: string // deprecated, use display_url instead
+  health_check_protocol?: string // deprecated, use scheme instead
+  host_port?: string // deprecated, use host instead
+}
+
+export interface HealthCheckConfig {
+  check_interval?: number
+  timeout?: number
+  user_agent?: string
+  max_redirects?: number
+  follow_redirects?: boolean
+  check_favicon?: boolean
+  health_check_config?: {
+    protocol?: string
+    method?: string
+    path?: string
+    headers?: Record<string, string>
+    body?: string
+    expected_status?: number[]
+    expected_text?: string
+    not_expected_text?: string
+    validate_ssl?: boolean
+    verify_hostname?: boolean
+    grpc_service?: string
+    grpc_method?: string
+    dns_resolver?: string
+    source_ip?: string
+    client_cert?: string
+    client_key?: string
+  }
+}
+
+export interface HeaderItem {
+  name: string
+  value: string
+}
+
+export interface EnhancedHealthCheckConfig {
+  // Basic settings
+  enabled: boolean
+  interval: number
+  timeout: number
+  userAgent: string
+  maxRedirects: number
+  followRedirects: boolean
+  checkFavicon: boolean
+
+  // Protocol settings
+  protocol: string
+  method: string
+  path: string
+  headers: HeaderItem[]
+  body: string
+
+  // Response validation
+  expectedStatus: number[]
+  expectedText: string
+  notExpectedText: string
+  validateSSL: boolean
+  verifyHostname: boolean
+
+  // gRPC settings
+  grpcService: string
+  grpcMethod: string
+
+  // Advanced settings
+  dnsResolver: string
+  sourceIP: string
+  clientCert: string
+  clientKey: string
+}
+
+export interface HealthCheckTestConfig {
+  protocol: string
+  method: string
+  path: string
+  headers: Record<string, string>
+  body: string
+  expected_status: number[]
+  expected_text: string
+  not_expected_text: string
+  validate_ssl: boolean
+  grpc_service: string
+  grpc_method: string
+  timeout: number
+}
+
+export interface SiteNavigationResponse {
+  data: SiteInfo[]
+}
+
+export interface SiteNavigationStatusResponse {
+  running: boolean
+}
+
+export const siteNavigationApi = {
+  // Get all sites for navigation
+  getSites(): Promise<SiteNavigationResponse> {
+    return http.get('/site_navigation')
+  },
+
+  // Get service status
+  getStatus(): Promise<SiteNavigationStatusResponse> {
+    return http.get('/site_navigation/status')
+  },
+
+  // Update sites order
+  updateOrder(orderedIds: number[]): Promise<{ message: string }> {
+    return http.post('/site_navigation/order', { ordered_ids: orderedIds })
+  },
+
+  // Get health check configuration
+  getHealthCheck(id: number): Promise<HealthCheckConfig> {
+    return http.get(`/site_navigation/health_check/${id}`)
+  },
+
+  // Update health check configuration
+  updateHealthCheck(id: number, config: HealthCheckConfig): Promise<{ message: string }> {
+    return http.put(`/site_navigation/health_check/${id}`, config)
+  },
+
+  // Test health check configuration
+  testHealthCheck(id: number, config: HealthCheckTestConfig): Promise<{ success: boolean, response_time?: number, error?: string }> {
+    return http.post(`/site_navigation/test_health_check/${id}`, { config })
+  },
+
+  // WebSocket connection using lib/websocket
+  createWebSocket() {
+    return ws('/api/site_navigation_ws', true)
+  },
+}

+ 8 - 8
app/src/components/Notification/notifications.ts

@@ -59,35 +59,35 @@ const notifications: Record<string, { title: () => string, content: (args: any)
   },
   'Sync Certificate Error': {
     title: () => $gettext('Sync Certificate Error'),
-    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args, true),
+    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{node_name} failed', args, true),
   },
   'Sync Certificate Success': {
     title: () => $gettext('Sync Certificate Success'),
-    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args, true),
+    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{node_name} successfully', args, true),
   },
   'Sync Config Error': {
     title: () => $gettext('Sync Config Error'),
-    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args, true),
+    content: (args: any) => $gettext('Sync config %{config_name} to %{node_name} failed', args, true),
   },
   'Sync Config Success': {
     title: () => $gettext('Sync Config Success'),
-    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args, true),
+    content: (args: any) => $gettext('Sync config %{config_name} to %{node_name} successfully', args, true),
   },
   'Rename Remote Config Error': {
     title: () => $gettext('Rename Remote Config Error'),
-    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed', args, true),
+    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{node_name} failed', args, true),
   },
   'Rename Remote Config Success': {
     title: () => $gettext('Rename Remote Config Success'),
-    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{env_name} successfully', args, true),
+    content: (args: any) => $gettext('Rename %{orig_path} to %{new_path} on %{node_name} successfully', args, true),
   },
   'Delete Remote Config Error': {
     title: () => $gettext('Delete Remote Config Error'),
-    content: (args: any) => $gettext('Delete %{path} on %{env_name} failed', args, true),
+    content: (args: any) => $gettext('Delete %{path} on %{node_name} failed', args, true),
   },
   'Delete Remote Config Success': {
     title: () => $gettext('Delete Remote Config Success'),
-    content: (args: any) => $gettext('Delete %{path} on %{env_name} successfully', args, true),
+    content: (args: any) => $gettext('Delete %{path} on %{node_name} successfully', args, true),
   },
   'External Notification Test': {
     title: () => $gettext('External Notification Test'),

+ 34 - 0
app/src/constants/site-status.ts

@@ -0,0 +1,34 @@
+// Site health check status constants
+export const SiteStatus = {
+  ONLINE: 'online',
+  OFFLINE: 'offline',
+  ERROR: 'error',
+  CHECKING: 'checking',
+} as const
+
+// Type for site status
+export type SiteStatusType = typeof SiteStatus[keyof typeof SiteStatus]
+
+// Status display configuration
+export const SiteStatusConfig = {
+  [SiteStatus.ONLINE]: {
+    label: 'Online',
+    color: 'success',
+    icon: 'CheckCircleOutlined',
+  },
+  [SiteStatus.OFFLINE]: {
+    label: 'Offline',
+    color: 'error',
+    icon: 'CloseCircleOutlined',
+  },
+  [SiteStatus.ERROR]: {
+    label: 'Error',
+    color: 'warning',
+    icon: 'ExclamationCircleOutlined',
+  },
+  [SiteStatus.CHECKING]: {
+    label: 'Checking',
+    color: 'processing',
+    icon: 'SyncOutlined',
+  },
+} as const

+ 8 - 0
app/src/routes/modules/dashboard.ts

@@ -27,6 +27,14 @@ export const dashboardRoutes: RouteRecordRaw[] = [
           name: () => $gettext('Nginx'),
         },
       },
+      {
+        path: 'sites',
+        component: () => import('@/views/dashboard/SiteNavigation.vue'),
+        name: 'SiteNavigation',
+        meta: {
+          name: () => $gettext('Sites'),
+        },
+      },
     ],
   },
 ]

+ 292 - 0
app/src/views/dashboard/SiteNavigation.vue

@@ -0,0 +1,292 @@
+<script setup lang="ts">
+import type ReconnectingWebSocket from 'reconnecting-websocket'
+import type { SiteInfo } from '@/api/site_navigation'
+import { GlobalOutlined } from '@ant-design/icons-vue'
+import { message } from 'ant-design-vue'
+import Sortable from 'sortablejs'
+import { siteNavigationApi } from '@/api/site_navigation'
+import SiteCard from './components/SiteCard.vue'
+import SiteHealthCheckModal from './components/SiteHealthCheckModal.vue'
+import SiteNavigationToolbar from './components/SiteNavigationToolbar.vue'
+
+const sites = ref<SiteInfo[]>([])
+const loading = ref(true)
+const refreshing = ref(false)
+const isConnected = ref(false)
+const settingsMode = ref(false)
+const draggableSites = ref<SiteInfo[]>([])
+const configModalVisible = ref(false)
+const configTarget = ref<SiteInfo>()
+
+let sortableInstance: Sortable | null = null
+let websocket: ReconnectingWebSocket | WebSocket | null = null
+
+// Display sites - use draggable sites in settings mode, backend sorted sites otherwise
+const displaySites = computed(() => {
+  return settingsMode.value ? draggableSites.value : sites.value
+})
+
+// WebSocket connection
+function connectWebSocket() {
+  try {
+    websocket = siteNavigationApi.createWebSocket()
+
+    if (!websocket) {
+      isConnected.value = false
+      return
+    }
+
+    websocket.onopen = () => {
+      isConnected.value = true
+    }
+
+    websocket.onmessage = (event: MessageEvent) => {
+      try {
+        const data = JSON.parse(event.data)
+        if (data.type === 'initial' || data.type === 'update') {
+          sites.value = data.data || []
+        }
+      }
+      catch (error) {
+        console.error('Failed to parse WebSocket message:', error)
+      }
+    }
+
+    websocket.onclose = () => {
+      isConnected.value = false
+    }
+
+    websocket.onerror = error => {
+      console.error('Site navigation WebSocket error:', error)
+      isConnected.value = false
+    }
+  }
+  catch (error) {
+    console.error('Failed to connect WebSocket:', error)
+    isConnected.value = false
+  }
+}
+
+// Load sites via HTTP (fallback)
+async function loadSites() {
+  try {
+    loading.value = true
+    const response = await siteNavigationApi.getSites()
+    sites.value = response.data || []
+  }
+  catch (error) {
+    console.error('Failed to load sites:', error)
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+// Refresh sites
+async function handleRefresh() {
+  try {
+    refreshing.value = true
+
+    // Only use WebSocket refresh
+    if (websocket && isConnected.value) {
+      websocket.send(JSON.stringify({ type: 'refresh' }))
+      message.success($gettext('Site refresh initiated'))
+    }
+    else {
+      message.warning($gettext('WebSocket not connected, please wait for connection'))
+    }
+  }
+  catch (error) {
+    console.error('Failed to refresh sites:', error)
+    message.error($gettext('Failed to refresh sites'))
+  }
+  finally {
+    refreshing.value = false
+  }
+}
+
+// Toggle settings mode
+function toggleSettingsMode() {
+  settingsMode.value = !settingsMode.value
+
+  if (settingsMode.value) {
+    draggableSites.value = [...sites.value]
+    nextTick(() => initSortable())
+  }
+  else {
+    destroySortable()
+  }
+}
+
+// Initialize sortable
+function initSortable() {
+  const gridElement = document.querySelector('.site-grid')
+  if (gridElement && !sortableInstance) {
+    sortableInstance = new Sortable(gridElement as HTMLElement, {
+      animation: 150,
+      ghostClass: 'site-card-ghost',
+      chosenClass: 'site-card-chosen',
+      dragClass: 'site-card-drag',
+      onEnd: () => {
+        // Update draggableSites order based on DOM order
+        const cards = Array.from(gridElement.children)
+        const newOrder = cards.map(card => {
+          const url = card.getAttribute('data-url')
+          return draggableSites.value.find(site => site.url === url)!
+        })
+        draggableSites.value = newOrder
+      },
+    })
+  }
+}
+
+// Destroy sortable
+function destroySortable() {
+  if (sortableInstance) {
+    sortableInstance.destroy()
+    sortableInstance = null
+  }
+}
+
+// Save order
+async function saveOrder() {
+  try {
+    const orderedIds = draggableSites.value.map(site => site.id)
+    await siteNavigationApi.updateOrder(orderedIds)
+    message.success($gettext('Order saved successfully'))
+
+    // Update sites.value immediately to reflect the new order
+    sites.value = [...draggableSites.value]
+
+    settingsMode.value = false
+    destroySortable()
+  }
+  catch (error) {
+    console.error('Failed to save order:', error)
+    message.error($gettext('Failed to save order'))
+  }
+}
+
+// Cancel settings mode
+function cancelSettingsMode() {
+  settingsMode.value = false
+  destroySortable()
+  draggableSites.value = []
+}
+
+// Open config modal
+function openConfigModal(site: SiteInfo) {
+  configTarget.value = site
+  configModalVisible.value = true
+}
+
+// Handle health check config save
+async function handleConfigSave(config: import('@/api/site_navigation').HealthCheckConfig) {
+  try {
+    if (configTarget.value) {
+      await siteNavigationApi.updateHealthCheck(configTarget.value.id, config)
+      message.success($gettext('Health check configuration saved'))
+    }
+  }
+  catch (error) {
+    console.error('Failed to save health check config:', error)
+    message.error($gettext('Failed to save configuration'))
+  }
+}
+
+onMounted(async () => {
+  // First load data via HTTP
+  await loadSites()
+  // Then connect WebSocket for real-time updates
+  connectWebSocket()
+})
+
+onUnmounted(() => {
+  destroySortable()
+  if (websocket) {
+    websocket.close()
+  }
+})
+</script>
+
+<template>
+  <div class="site-navigation">
+    <SiteNavigationToolbar
+      :is-connected="isConnected"
+      :refreshing="refreshing"
+      :settings-mode="settingsMode"
+      @refresh="handleRefresh"
+      @toggle-settings="toggleSettingsMode"
+      @save-order="saveOrder"
+      @cancel-settings="cancelSettingsMode"
+    />
+
+    <div v-if="loading" class="flex items-center justify-center py-12">
+      <a-spin size="large" />
+    </div>
+
+    <div v-else-if="displaySites.length === 0" class="empty-state">
+      <GlobalOutlined class="text-6xl text-gray-400 mb-4" />
+      <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
+        {{ $gettext('No sites found') }}
+      </h3>
+      <p class="text-gray-600 dark:text-gray-400 text-center max-w-md">
+        {{ $gettext('Sites will appear here once you configure nginx server blocks with valid server_name directives.') }}
+      </p>
+    </div>
+
+    <div v-else class="site-grid">
+      <SiteCard
+        v-for="site in displaySites"
+        :key="site.id"
+        :site="site"
+        :settings-mode="settingsMode"
+        @open-config="openConfigModal"
+      />
+    </div>
+
+    <SiteHealthCheckModal
+      v-model:open="configModalVisible"
+      :site="configTarget"
+      @save="handleConfigSave"
+      @refresh="handleRefresh"
+    />
+  </div>
+</template>
+
+<style scoped>
+.site-navigation {
+  @apply p-6;
+}
+
+.empty-state {
+  @apply flex flex-col items-center justify-center py-16 text-center;
+}
+
+.site-grid {
+  @apply grid gap-6;
+  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+}
+
+/* Responsive design for narrow screens */
+@media (max-width: 768px) {
+  .site-navigation {
+    @apply p-4;
+  }
+
+  .site-grid {
+    grid-template-columns: 1fr;
+    @apply gap-4;
+  }
+}
+
+@media (max-width: 480px) {
+  .site-navigation {
+    @apply p-3;
+  }
+
+  .site-grid {
+    @apply gap-3;
+  }
+}
+</style>

+ 323 - 0
app/src/views/dashboard/components/SiteCard.vue

@@ -0,0 +1,323 @@
+<script setup lang="ts">
+import type { SiteInfo } from '@/api/site_navigation'
+import {
+  ClockCircleOutlined,
+  CodeOutlined,
+  ExclamationCircleOutlined,
+  SettingOutlined,
+} from '@ant-design/icons-vue'
+import { truncate, upperFirst } from 'lodash'
+import { SiteStatus } from '@/constants/site-status'
+
+interface Props {
+  site: SiteInfo
+  settingsMode: boolean
+}
+
+interface Emits {
+  (e: 'openConfig', site: SiteInfo): void
+}
+
+defineProps<Props>()
+defineEmits<Emits>()
+
+// Check if site can be opened (only HTTP/HTTPS)
+function canOpenSite(site: SiteInfo): boolean {
+  const scheme = site.scheme || site.health_check_protocol || 'http'
+  return scheme === 'http' || scheme === 'https'
+}
+
+// Open site in new tab (only for HTTP/HTTPS)
+function openSite(site: SiteInfo) {
+  if (!canOpenSite(site)) {
+    return
+  }
+
+  // Use display_url if available, otherwise construct from scheme and host_port
+  let targetUrl = site.display_url || site.url
+
+  // If we have scheme and host_port, construct the URL
+  if (site.scheme && site.host_port && (site.scheme === 'http' || site.scheme === 'https')) {
+    targetUrl = `${site.scheme}://${site.host_port}`
+  }
+
+  window.open(targetUrl, '_blank')
+}
+
+// Handle favicon loading error
+function handleFaviconError(event: Event) {
+  const img = event.target as HTMLImageElement
+  img.style.display = 'none'
+}
+
+// Get avatar color based on site name
+function getAvatarColor(name: string): string {
+  const colors = [
+    '#f87171',
+    '#fb923c',
+    '#facc15',
+    '#a3e635',
+    '#4ade80',
+    '#22d3ee',
+    '#60a5fa',
+    '#a78bfa',
+    '#f472b6',
+    '#fb7185',
+  ]
+
+  let hash = 0
+  for (let i = 0; i < name.length; i++) {
+    hash = name.charCodeAt(i) + ((hash << 5) - hash)
+  }
+
+  return colors[Math.abs(hash) % colors.length]
+}
+
+// Get initials from site name
+function getInitials(name: string): string {
+  const parts = name.split('.')
+  return truncate(
+    parts
+      .map(part => upperFirst(part.charAt(0)))
+      .join(''),
+    { length: 2, omission: '' },
+  )
+}
+
+// Get status CSS class
+function getStatusClass(status: string): string {
+  switch (status) {
+    case SiteStatus.ONLINE:
+      return 'status-online'
+    case SiteStatus.OFFLINE:
+      return 'status-offline'
+    case SiteStatus.ERROR:
+      return 'status-error'
+    case SiteStatus.CHECKING:
+      return 'status-checking'
+    default:
+      return 'status-unknown'
+  }
+}
+</script>
+
+<template>
+  <div
+    class="site-card"
+    :class="{
+      'settings-mode': settingsMode,
+      'clickable': !settingsMode && canOpenSite(site),
+      'non-clickable': !settingsMode && !canOpenSite(site),
+    }"
+    :data-url="site.url"
+    @click="!settingsMode && canOpenSite(site) && openSite(site)"
+  >
+    <div class="site-card-header">
+      <div class="site-icon">
+        <img
+          v-if="site.favicon_data"
+          :src="site.favicon_data"
+          :alt="site.name"
+          class="w-8 h-8 rounded"
+          @error="handleFaviconError"
+        >
+        <div
+          v-else
+          class="avatar-fallback"
+          :style="{ backgroundColor: getAvatarColor(site.name) }"
+        >
+          {{ getInitials(site.name) }}
+        </div>
+      </div>
+
+      <div v-if="!settingsMode" class="site-status">
+        <div
+          class="status-indicator"
+          :class="getStatusClass(site.status)"
+        />
+      </div>
+    </div>
+
+    <div class="site-info">
+      <h3 class="site-title">
+        {{ site.title || site.name }}
+      </h3>
+      <p class="site-url">
+        <span v-if="site.scheme && site.host_port" class="url-parts">
+          <span class="scheme">{{ site.scheme }}://</span><span class="host-port">{{ site.host_port }}</span>
+        </span>
+        <span v-else>{{ site.display_url || site.url }}</span>
+      </p>
+
+      <div class="site-details">
+        <div v-if="site.status === SiteStatus.ONLINE" class="detail-item">
+          <ClockCircleOutlined class="detail-icon" />
+          <span>{{ site.response_time }}ms</span>
+        </div>
+        <div v-if="site.status_code" class="detail-item">
+          <CodeOutlined class="detail-icon" />
+          <span>{{ site.status_code }}</span>
+        </div>
+        <div v-if="site.error" class="detail-item error">
+          <ExclamationCircleOutlined class="detail-icon" />
+          <span>{{ site.error }}</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- Settings button in settings mode -->
+    <div v-if="settingsMode" class="site-card-config">
+      <a-button
+        type="text"
+        size="small"
+        @click.stop="$emit('openConfig', site)"
+      >
+        <template #icon>
+          <SettingOutlined />
+        </template>
+      </a-button>
+    </div>
+
+    <!-- Drag handle in settings mode -->
+    <div v-if="settingsMode" class="drag-handle">
+      <div class="drag-dots">
+        <div class="dot" />
+        <div class="dot" />
+        <div class="dot" />
+        <div class="dot" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.site-card {
+  @apply relative bg-white dark:bg-trueGray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 transition-all duration-200;
+}
+
+.site-card.clickable {
+  @apply cursor-pointer hover:scale-105;
+}
+
+.site-card.non-clickable {
+  @apply cursor-default;
+  opacity: 0.8;
+}
+
+.site-card.settings-mode {
+  @apply cursor-move;
+}
+
+.site-card.settings-mode:hover {
+  @apply scale-100;
+}
+
+.site-card-header {
+  @apply flex items-center justify-between mb-3;
+}
+
+.site-icon img {
+  @apply w-8 h-8 rounded object-cover;
+}
+
+.avatar-fallback {
+  @apply w-8 h-8 rounded flex items-center justify-center text-white font-medium text-sm;
+}
+
+.site-status {
+  @apply flex items-center;
+}
+
+.status-indicator {
+  @apply w-3 h-3 rounded-full;
+}
+
+.status-online {
+  @apply bg-green-500;
+}
+
+.status-offline {
+  @apply bg-red-500;
+}
+
+.status-error {
+  @apply bg-yellow-500;
+}
+
+.status-checking {
+  @apply bg-blue-500 animate-pulse;
+}
+
+.status-unknown {
+  @apply bg-gray-400;
+}
+
+.site-info {
+  @apply space-y-2;
+}
+
+.site-title {
+  @apply font-medium text-gray-900 dark:text-gray-100 text-lg truncate;
+}
+
+.scheme {
+  @apply text-sm text-gray-600 dark:text-gray-400;
+}
+
+.site-url {
+  @apply text-sm text-gray-600 dark:text-gray-400 truncate;
+}
+
+.url-parts {
+  @apply inline;
+}
+
+.host-port {
+  @apply text-gray-700 dark:text-gray-300;
+}
+
+.site-details {
+  @apply flex flex-wrap gap-3 text-xs;
+}
+
+.detail-item {
+  @apply flex items-center gap-1 text-gray-600 dark:text-gray-400;
+}
+
+.detail-item.error {
+  @apply text-red-600 dark:text-red-400;
+}
+
+.detail-icon {
+  @apply w-3 h-3;
+}
+
+.site-card-config {
+  @apply absolute top-2 right-2;
+}
+
+.drag-handle {
+  @apply absolute bottom-2 right-2 opacity-50 hover:opacity-100 transition-opacity;
+}
+
+.drag-dots {
+  @apply grid grid-cols-2 gap-1 p-1;
+}
+
+.dot {
+  @apply w-1 h-1 bg-gray-400 rounded-full;
+}
+
+/* Sortable states */
+.site-card-ghost {
+  @apply opacity-50;
+}
+
+.site-card-chosen {
+  @apply transform scale-105;
+}
+
+.site-card-drag {
+  @apply transform rotate-2;
+}
+</style>

+ 709 - 0
app/src/views/dashboard/components/SiteHealthCheckModal.vue

@@ -0,0 +1,709 @@
+<script setup lang="ts">
+import type { EnhancedHealthCheckConfig, HeaderItem, SiteInfo } from '@/api/site_navigation'
+import { CloseOutlined, PlusOutlined } from '@ant-design/icons-vue'
+import { message } from 'ant-design-vue'
+import { siteNavigationApi } from '@/api/site_navigation'
+
+interface Props {
+  open: boolean
+  site?: SiteInfo
+}
+
+interface Emits {
+  (e: 'update:open', value: boolean): void
+  (e: 'save', config: EnhancedHealthCheckConfig): void
+  (e: 'refresh'): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+const testing = ref(false)
+
+const visible = computed({
+  get: () => props.open,
+  set: value => emit('update:open', value),
+})
+
+const formData = ref<EnhancedHealthCheckConfig>({
+  // Basic settings (health check is always enabled)
+  enabled: true,
+  interval: 300,
+  timeout: 10,
+  userAgent: 'Nginx-UI Enhanced Checker/2.0',
+  maxRedirects: 3,
+  followRedirects: true,
+  checkFavicon: true,
+
+  // Protocol settings
+  protocol: 'http',
+  method: 'GET',
+  path: '/',
+  headers: [],
+  body: '',
+
+  // Response validation
+  expectedStatus: [200],
+  expectedText: '',
+  notExpectedText: '',
+  validateSSL: false,
+  verifyHostname: false,
+
+  // gRPC settings
+  grpcService: '',
+  grpcMethod: 'Check',
+
+  // Advanced settings
+  dnsResolver: '',
+  sourceIP: '',
+  clientCert: '',
+  clientKey: '',
+})
+
+// Load existing config when site changes
+watchEffect(async () => {
+  if (props.site) {
+    await loadExistingConfig()
+  }
+})
+
+async function loadExistingConfig() {
+  if (!props.site)
+    return
+
+  try {
+    const config = await siteNavigationApi.getHealthCheck(props.site.id)
+
+    // Convert backend config to frontend format
+    formData.value = {
+      // Basic settings (health check is always enabled)
+      enabled: true,
+      interval: config.check_interval ?? 300,
+      timeout: config.timeout ?? 10,
+      userAgent: config.user_agent ?? 'Nginx-UI Enhanced Checker/2.0',
+      maxRedirects: config.max_redirects ?? 3,
+      followRedirects: config.follow_redirects ?? true,
+      checkFavicon: config.check_favicon ?? true,
+
+      // Protocol settings
+      protocol: config.health_check_config?.protocol ?? 'http',
+      method: config.health_check_config?.method ?? 'GET',
+      path: config.health_check_config?.path ?? '/',
+      headers: convertHeadersToArray(config.health_check_config?.headers ?? {}),
+      body: config.health_check_config?.body ?? '',
+
+      // Response validation
+      expectedStatus: config.health_check_config?.expected_status ?? [200],
+      expectedText: config.health_check_config?.expected_text ?? '',
+      notExpectedText: config.health_check_config?.not_expected_text ?? '',
+      validateSSL: config.health_check_config?.validate_ssl ?? false,
+      verifyHostname: config.health_check_config?.verify_hostname ?? false,
+
+      // gRPC settings
+      grpcService: config.health_check_config?.grpc_service ?? '',
+      grpcMethod: config.health_check_config?.grpc_method ?? 'Check',
+
+      // Advanced settings
+      dnsResolver: config.health_check_config?.dns_resolver ?? '',
+      sourceIP: config.health_check_config?.source_ip ?? '',
+      clientCert: config.health_check_config?.client_cert ?? '',
+      clientKey: config.health_check_config?.client_key ?? '',
+    }
+  }
+  catch (error) {
+    console.error('Failed to load health check config:', error)
+    // Fallback to defaults
+    resetForm()
+  }
+}
+
+function resetForm() {
+  formData.value = {
+    // Basic settings (health check is always enabled)
+    enabled: true,
+    interval: 300,
+    timeout: 10,
+    userAgent: 'Nginx-UI Enhanced Checker/2.0',
+    maxRedirects: 3,
+    followRedirects: true,
+    checkFavicon: true,
+
+    // Protocol settings
+    protocol: 'http',
+    method: 'GET',
+    path: '/',
+    headers: [],
+    body: '',
+
+    // Response validation
+    expectedStatus: [200],
+    expectedText: '',
+    notExpectedText: '',
+    validateSSL: false,
+    verifyHostname: false,
+
+    // gRPC settings
+    grpcService: '',
+    grpcMethod: 'Check',
+
+    // Advanced settings
+    dnsResolver: '',
+    sourceIP: '',
+    clientCert: '',
+    clientKey: '',
+  }
+}
+
+function convertHeadersToArray(headers: { [key: string]: string }): HeaderItem[] {
+  return Object.entries(headers || {}).map(([name, value]) => ({ name, value }))
+}
+
+function isHttpProtocol(protocol: string): boolean {
+  return ['http', 'https'].includes(protocol)
+}
+
+function isGrpcProtocol(protocol: string): boolean {
+  return ['grpc', 'grpcs'].includes(protocol)
+}
+
+function isDefaultHttpPort(port: string, protocol: string): boolean {
+  return (port === '80' && protocol === 'http')
+    || (port === '443' && protocol === 'https')
+    || !port
+}
+
+function isDefaultGrpcPort(port: string, protocol: string): boolean {
+  return (port === '80' && protocol === 'grpc')
+    || (port === '443' && protocol === 'grpcs')
+}
+
+function getGrpcDefaultPort(urlProtocol: string, protocol: string): string {
+  return (urlProtocol === 'https:' || protocol === 'grpcs') ? '443' : '80'
+}
+
+function buildUrl(protocol: string, hostname: string, port?: string): string {
+  return port ? `${protocol}://${hostname}:${port}` : `${protocol}://${hostname}`
+}
+
+function getHttpTestUrl(protocol: string, siteUrl: string): string {
+  try {
+    const url = new URL(siteUrl)
+    const hostname = url.hostname
+    const port = url.port
+
+    if (isDefaultHttpPort(port, protocol)) {
+      return buildUrl(protocol, hostname)
+    }
+    return buildUrl(protocol, hostname, port)
+  }
+  catch {
+    return `${protocol}://${siteUrl}`
+  }
+}
+
+function getGrpcTestUrl(protocol: string, siteUrl: string): string {
+  try {
+    const url = new URL(siteUrl)
+    const hostname = url.hostname
+    let port = url.port
+
+    if (!port) {
+      port = getGrpcDefaultPort(url.protocol, protocol)
+    }
+
+    if (isDefaultGrpcPort(port, protocol)) {
+      return buildUrl(protocol, hostname)
+    }
+    return buildUrl(protocol, hostname, port)
+  }
+  catch {
+    return `${protocol}://${siteUrl}`
+  }
+}
+
+function getTestUrl(): string {
+  if (!props.site) {
+    return ''
+  }
+
+  const protocol = formData.value.protocol
+
+  if (isHttpProtocol(protocol)) {
+    return getHttpTestUrl(protocol, props.site.display_url || props.site.url || '')
+  }
+
+  if (isGrpcProtocol(protocol)) {
+    return getGrpcTestUrl(protocol, props.site.display_url || props.site.url || '')
+  }
+
+  return props.site.display_url || props.site.url || ''
+}
+
+function addHeader() {
+  formData.value.headers.push({ name: '', value: '' })
+}
+
+function removeHeader(index: number) {
+  formData.value.headers.splice(index, 1)
+}
+
+function handleCancel() {
+  visible.value = false
+}
+
+async function handleSave() {
+  if (!props.site)
+    return
+
+  try {
+    // Convert headers array to map for backend
+    const config = { ...formData.value }
+    const headersMap: { [key: string]: string } = {}
+    config.headers.forEach(header => {
+      if (header.name && header.value) {
+        headersMap[header.name] = header.value
+      }
+    })
+
+    // Create the config object for the backend
+    const backendConfig = {
+      url: props.site.url,
+      health_check_enabled: true, // Always enabled
+      check_interval: config.interval,
+      timeout: config.timeout,
+      user_agent: config.userAgent,
+      max_redirects: config.maxRedirects,
+      follow_redirects: config.followRedirects,
+      check_favicon: config.checkFavicon,
+
+      // Enhanced health check config (always included)
+      health_check_config: {
+        protocol: config.protocol,
+        method: config.method,
+        path: config.path,
+        headers: headersMap,
+        body: config.body,
+        expected_status: config.expectedStatus,
+        expected_text: config.expectedText,
+        not_expected_text: config.notExpectedText,
+        validate_ssl: config.validateSSL,
+        grpc_service: config.grpcService,
+        grpc_method: config.grpcMethod,
+        dns_resolver: config.dnsResolver,
+        source_ip: config.sourceIP,
+        verify_hostname: config.verifyHostname,
+        client_cert: config.clientCert,
+        client_key: config.clientKey,
+      },
+    }
+
+    await siteNavigationApi.updateHealthCheck(props.site.id, backendConfig)
+    message.success($gettext('Health check configuration saved successfully'))
+
+    // Trigger site refresh to update display URLs
+    emit('refresh')
+
+    visible.value = false
+  }
+  catch (error) {
+    console.error('Failed to save health check config:', error)
+    message.error($gettext('Failed to save health check configuration'))
+  }
+}
+
+async function handleTest() {
+  if (!props.site)
+    return
+
+  try {
+    testing.value = true
+
+    // Create a test configuration
+    const testConfig = {
+      protocol: formData.value.protocol,
+      method: formData.value.method,
+      path: formData.value.path,
+      headers: formData.value.headers.reduce((acc, header) => {
+        if (header.name && header.value) {
+          acc[header.name] = header.value
+        }
+        return acc
+      }, {} as { [key: string]: string }),
+      body: formData.value.body,
+      expected_status: formData.value.expectedStatus,
+      expected_text: formData.value.expectedText,
+      not_expected_text: formData.value.notExpectedText,
+      validate_ssl: formData.value.validateSSL,
+      grpc_service: formData.value.grpcService,
+      grpc_method: formData.value.grpcMethod,
+      timeout: formData.value.timeout,
+    }
+
+    // Call test API endpoint (we'll need to create this)
+    const result = await siteNavigationApi.testHealthCheck(props.site.id, testConfig)
+
+    if (result.success) {
+      message.success($gettext('Test successful! Response time: %{response_time}ms', { response_time: String(result.response_time || 0) }))
+    }
+    else {
+      message.error($gettext('Test failed: %{error}', { error: result.error || 'Unknown error' }, true))
+    }
+  }
+  catch (error) {
+    console.error('Health check test failed:', error)
+    message.error($gettext('Test failed: Unable to perform health check'))
+  }
+  finally {
+    testing.value = false
+  }
+}
+</script>
+
+<template>
+  <a-modal
+    v-model:open="visible"
+    :title="`${$gettext('Health Check Configuration')} - ${site?.name || getTestUrl()}`"
+    width="800px"
+    @cancel="handleCancel"
+  >
+    <div class="p-2">
+      <a-form
+        :model="formData"
+        layout="vertical"
+        :label-col="{ span: 24 }"
+        :wrapper-col="{ span: 24 }"
+      >
+        <div>
+          <!-- Protocol Selection -->
+          <a-form-item :label="$gettext('Protocol')">
+            <a-radio-group v-model:value="formData.protocol">
+              <a-radio value="http">
+                HTTP
+              </a-radio>
+              <a-radio value="https">
+                HTTPS
+              </a-radio>
+              <a-radio value="grpc">
+                gRPC
+              </a-radio>
+              <a-radio value="grpcs">
+                gRPCS
+              </a-radio>
+            </a-radio-group>
+          </a-form-item>
+
+          <!-- HTTP/HTTPS Settings -->
+          <div v-if="!['grpc', 'grpcs'].includes(formData.protocol)">
+            <a-row :gutter="16">
+              <a-col :span="12">
+                <a-form-item :label="$gettext('HTTP Method')">
+                  <a-select v-model:value="formData.method" style="width: 100%">
+                    <a-select-option value="GET">
+                      GET
+                    </a-select-option>
+                    <a-select-option value="POST">
+                      POST
+                    </a-select-option>
+                    <a-select-option value="PUT">
+                      PUT
+                    </a-select-option>
+                    <a-select-option value="HEAD">
+                      HEAD
+                    </a-select-option>
+                    <a-select-option value="OPTIONS">
+                      OPTIONS
+                    </a-select-option>
+                  </a-select>
+                </a-form-item>
+              </a-col>
+              <a-col :span="12">
+                <a-form-item :label="$gettext('Path')">
+                  <a-input v-model:value="formData.path" placeholder="/" />
+                </a-form-item>
+              </a-col>
+            </a-row>
+
+            <a-form-item :label="$gettext('Custom Headers')" class="mb-4">
+              <div class="space-y-2">
+                <div v-for="(header, index) in formData.headers" :key="index" class="flex gap-2">
+                  <a-input v-model:value="header.name" placeholder="Header Name" class="flex-1" />
+                  <a-input v-model:value="header.value" placeholder="Header Value" class="flex-1" />
+                  <a-button type="text" danger @click="removeHeader(index)">
+                    <template #icon>
+                      <CloseOutlined />
+                    </template>
+                  </a-button>
+                </div>
+                <a-button type="dashed" class="w-full" @click="addHeader">
+                  <template #icon>
+                    <PlusOutlined />
+                  </template>
+                  {{ $gettext('Add Header') }}
+                </a-button>
+              </div>
+            </a-form-item>
+
+            <a-form-item v-if="formData.method !== 'GET'" :label="$gettext('Request Body')">
+              <a-textarea
+                v-model:value="formData.body"
+                :rows="3"
+                placeholder="{&quot;key&quot;: &quot;value&quot;}"
+              />
+            </a-form-item>
+
+            <a-form-item :label="$gettext('Expected Status Codes')">
+              <a-select
+                v-model:value="formData.expectedStatus"
+                mode="multiple"
+                style="width: 100%"
+                placeholder="200, 201, 204..."
+              >
+                <a-select-option :value="200">
+                  200 OK
+                </a-select-option>
+                <a-select-option :value="201">
+                  201 Created
+                </a-select-option>
+                <a-select-option :value="204">
+                  204 No Content
+                </a-select-option>
+                <a-select-option :value="301">
+                  301 Moved Permanently
+                </a-select-option>
+                <a-select-option :value="302">
+                  302 Found
+                </a-select-option>
+                <a-select-option :value="304">
+                  304 Not Modified
+                </a-select-option>
+              </a-select>
+            </a-form-item>
+
+            <a-row :gutter="16">
+              <a-col :span="12">
+                <a-form-item :label="$gettext('Expected Text')">
+                  <a-input v-model:value="formData.expectedText" placeholder="Success" />
+                </a-form-item>
+              </a-col>
+              <a-col :span="12">
+                <a-form-item :label="$gettext('Not Expected Text')">
+                  <a-input v-model:value="formData.notExpectedText" placeholder="Error" />
+                </a-form-item>
+              </a-col>
+            </a-row>
+          </div>
+
+          <!-- gRPC/gRPCS Settings -->
+          <div v-if="['grpc', 'grpcs'].includes(formData.protocol)">
+            <a-alert
+              v-if="['grpc', 'grpcs'].includes(formData.protocol)"
+              :message="formData.protocol === 'grpcs'
+                ? $gettext('gRPCS uses TLS encryption. Server must implement gRPC Health Check service. For testing, SSL validation is disabled by default.')
+                : $gettext('gRPC health check requires server to implement gRPC Health Check service (grpc.health.v1.Health).')"
+              type="info"
+              show-icon
+              class="mb-4"
+            />
+            <a-alert
+              :message="$gettext('Note: If the server does not support gRPC Reflection, health checks may fail. Please ensure your gRPC server has Reflection enabled.')"
+              type="warning"
+              show-icon
+              class="mb-4"
+            />
+            <a-row :gutter="16">
+              <a-col :span="12">
+                <a-form-item :label="$gettext('Service Name')">
+                  <a-input v-model:value="formData.grpcService" placeholder="my.service.v1.MyService" />
+                </a-form-item>
+              </a-col>
+              <a-col :span="12">
+                <a-form-item :label="$gettext('Method Name')">
+                  <a-input v-model:value="formData.grpcMethod" placeholder="Check" />
+                </a-form-item>
+              </a-col>
+            </a-row>
+          </div>
+
+          <!-- Advanced Settings -->
+          <a-collapse>
+            <a-collapse-panel key="advanced" :header="$gettext('Advanced Settings')">
+              <a-row :gutter="16">
+                <a-col :span="12">
+                  <a-form-item :label="$gettext('Check Interval (seconds)')">
+                    <a-input-number
+                      v-model:value="formData.interval"
+                      :min="30"
+                      :max="3600"
+                      style="width: 100%"
+                    />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item :label="$gettext('Timeout (seconds)')">
+                    <a-input-number
+                      v-model:value="formData.timeout"
+                      :min="5"
+                      :max="60"
+                      style="width: 100%"
+                    />
+                  </a-form-item>
+                </a-col>
+              </a-row>
+
+              <a-form-item :label="$gettext('User Agent')">
+                <a-input v-model:value="formData.userAgent" />
+              </a-form-item>
+
+              <div v-if="!['grpc', 'grpcs'].includes(formData.protocol)">
+                <a-row :gutter="16">
+                  <a-col :span="12">
+                    <a-form-item :label="$gettext('Max Redirects')">
+                      <a-input-number
+                        v-model:value="formData.maxRedirects"
+                        :min="0"
+                        :max="10"
+                        style="width: 100%"
+                      />
+                    </a-form-item>
+                  </a-col>
+                  <a-col :span="12">
+                    <a-form-item>
+                      <a-checkbox v-model:checked="formData.followRedirects">
+                        {{ $gettext('Follow Redirects') }}
+                      </a-checkbox>
+                    </a-form-item>
+                  </a-col>
+                </a-row>
+
+                <a-form-item>
+                  <a-checkbox v-model:checked="formData.validateSSL">
+                    {{ $gettext('Validate SSL Certificate') }}
+                  </a-checkbox>
+                </a-form-item>
+
+                <a-form-item>
+                  <a-checkbox v-model:checked="formData.verifyHostname">
+                    {{ $gettext('Verify Hostname') }}
+                  </a-checkbox>
+                </a-form-item>
+
+                <a-form-item>
+                  <a-checkbox v-model:checked="formData.checkFavicon">
+                    {{ $gettext('Check Favicon') }}
+                  </a-checkbox>
+                </a-form-item>
+              </div>
+
+              <!-- DNS & Network -->
+              <a-row :gutter="16">
+                <a-col :span="12">
+                  <a-form-item :label="$gettext('DNS Resolver')">
+                    <a-input v-model:value="formData.dnsResolver" placeholder="8.8.8.8:53" />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item :label="$gettext('Source IP')">
+                    <a-input v-model:value="formData.sourceIP" placeholder="192.168.1.100" />
+                  </a-form-item>
+                </a-col>
+              </a-row>
+
+              <!-- Client Certificates -->
+              <a-row :gutter="16">
+                <a-col :span="12">
+                  <a-form-item :label="$gettext('Client Certificate')">
+                    <a-input v-model:value="formData.clientCert" placeholder="/path/to/client.crt" />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item :label="$gettext('Client Key')">
+                    <a-input v-model:value="formData.clientKey" placeholder="/path/to/client.key" />
+                  </a-form-item>
+                </a-col>
+              </a-row>
+            </a-collapse-panel>
+          </a-collapse>
+        </div>
+      </a-form>
+    </div>
+
+    <template #footer>
+      <a-button @click="handleCancel">
+        {{ $gettext('Cancel') }}
+      </a-button>
+      <a-button type="primary" @click="handleSave">
+        {{ $gettext('Save') }}
+      </a-button>
+      <a-button :loading="testing" @click="handleTest">
+        {{ $gettext('Test') }}
+      </a-button>
+    </template>
+  </a-modal>
+</template>
+
+<style scoped>
+.grpc-help-content {
+  font-size: 14px;
+  line-height: 1.6;
+}
+
+.grpc-help-content h4 {
+  color: #1890ff;
+  margin: 16px 0 8px 0;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.grpc-help-content h5 {
+  color: #595959;
+  margin: 12px 0 4px 0;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.grpc-help-content p {
+  margin: 8px 0;
+  color: #595959;
+}
+
+.code-examples {
+  margin: 16px 0;
+}
+
+.code-examples pre {
+  background-color: #f6f8fa;
+  border: 1px solid #e1e4e8;
+  border-radius: 6px;
+  padding: 12px;
+  margin: 8px 0;
+  overflow-x: auto;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+  font-size: 13px;
+  line-height: 1.4;
+}
+
+.code-examples code {
+  color: #24292e;
+  background: transparent;
+  border: none;
+  padding: 0;
+}
+
+.dark .code-examples pre {
+  background-color: #161b22;
+  border-color: #30363d;
+}
+
+.dark .code-examples code {
+  color: #e6edf3;
+}
+
+.dark .grpc-help-content h4 {
+  color: #58a6ff;
+}
+
+.dark .grpc-help-content h5,
+.dark .grpc-help-content p {
+  color: #c9d1d9;
+}
+</style>

+ 106 - 0
app/src/views/dashboard/components/SiteNavigationToolbar.vue

@@ -0,0 +1,106 @@
+<script setup lang="ts">
+import {
+  CloseOutlined,
+  ReloadOutlined,
+  SaveOutlined,
+  SettingOutlined,
+} from '@ant-design/icons-vue'
+
+interface Props {
+  isConnected: boolean
+  refreshing: boolean
+  settingsMode: boolean
+}
+
+interface Emits {
+  (e: 'refresh'): void
+  (e: 'toggleSettings'): void
+  (e: 'saveOrder'): void
+  (e: 'cancelSettings'): void
+}
+
+defineProps<Props>()
+defineEmits<Emits>()
+</script>
+
+<template>
+  <div class="site-navigation-header">
+    <h2 class="text-2xl font-500 text-gray-900 dark:text-gray-100 mb-4">
+      {{ $gettext('Site Navigation') }}
+    </h2>
+
+    <div class="flex items-center gap-4">
+      <div class="flex items-center gap-2">
+        <div
+          class="w-3 h-3 rounded-full"
+          :class="[isConnected ? 'bg-green-500' : 'bg-red-500']"
+        />
+        <span class="text-sm text-gray-600 dark:text-gray-400">
+          {{ isConnected ? $gettext('Connected') : $gettext('Disconnected') }}
+        </span>
+      </div>
+
+      <div class="flex gap-2">
+        <a-button
+          v-if="settingsMode"
+          type="primary"
+          size="small"
+          @click="$emit('saveOrder')"
+        >
+          <template #icon>
+            <SaveOutlined />
+          </template>
+          {{ $gettext('Save Order') }}
+        </a-button>
+
+        <a-button
+          v-if="settingsMode"
+          size="small"
+          @click="$emit('cancelSettings')"
+        >
+          <template #icon>
+            <CloseOutlined />
+          </template>
+          {{ $gettext('Cancel') }}
+        </a-button>
+
+        <a-button
+          v-if="!settingsMode"
+          type="primary"
+          size="small"
+          :loading="refreshing"
+          @click="$emit('refresh')"
+        >
+          <template #icon>
+            <ReloadOutlined />
+          </template>
+          {{ $gettext('Refresh') }}
+        </a-button>
+
+        <a-button
+          v-if="!settingsMode"
+          size="small"
+          @click="$emit('toggleSettings')"
+        >
+          <template #icon>
+            <SettingOutlined />
+          </template>
+          {{ $gettext('Settings') }}
+        </a-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.site-navigation-header {
+  @apply flex items-center justify-between mb-6;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+  .site-navigation-header {
+    @apply flex-col items-start gap-4;
+  }
+}
+</style>

+ 1 - 3
go.mod

@@ -50,6 +50,7 @@ require (
 	github.com/urfave/cli/v3 v3.4.1
 	golang.org/x/crypto v0.41.0
 	golang.org/x/net v0.43.0
+	google.golang.org/grpc v1.74.2
 	gopkg.in/ini.v1 v1.67.0
 	gorm.io/driver/sqlite v1.6.0
 	gorm.io/gen v0.3.27
@@ -89,7 +90,6 @@ require (
 	github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9 // indirect
 	github.com/alibabacloud-go/debug v1.0.1 // indirect
 	github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
-	github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
 	github.com/alibabacloud-go/tea v1.3.10 // indirect
 	github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
 	github.com/aliyun/aliyun-log-go-sdk v0.1.106 // indirect
@@ -348,11 +348,9 @@ require (
 	golang.org/x/text v0.28.0 // indirect
 	golang.org/x/time v0.12.0 // indirect
 	golang.org/x/tools v0.35.0 // indirect
-	golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
 	google.golang.org/api v0.245.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
-	google.golang.org/grpc v1.74.2 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect

+ 2 - 163
go.sum

@@ -100,8 +100,6 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
 cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
 cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
 cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
-cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
-cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
 cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
 cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
@@ -614,18 +612,12 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
 github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
@@ -721,9 +713,6 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC
 github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
 github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
 github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
-github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11/go.mod h1:wHxkgZT1ClZdcwEVP/pDgYK/9HucsnCfMipmJgCz4xY=
-github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8 h1:AL+nH363NJFS1NXIjCdmj5MOElgKEqgFeoq7vjje350=
-github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8/go.mod h1:d+z3ScRqc7PFzg4h9oqE3h8yunRZvAvU7u+iuPYEhpU=
 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9 h1:7P0KWfed/YMtpeuW3E2iwokzoz9L7H9rB+VZzg5DeBs=
 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9/go.mod h1:kgnXaV74AVjM3ZWJu1GhyXGuCtxljJ677oUfz6MyJOE=
 github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
@@ -749,24 +738,18 @@ github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/Ke
 github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
 github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
 github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
-github.com/alibabacloud-go/tea v1.3.9 h1:bjgt1bvdY780vz/17iWNNtbXl4A77HWntWMeaUF3So0=
-github.com/alibabacloud-go/tea v1.3.9/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
 github.com/alibabacloud-go/tea v1.3.10 h1:J0Ke8iMyoxX2daj90hdPr1QgfxJnhR8SOflB910o/Dk=
 github.com/alibabacloud-go/tea v1.3.10/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
 github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
 github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
-github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
 github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
 github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
-github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
 github.com/aliyun/aliyun-log-go-sdk v0.1.106 h1:qhAiESgl5qmMkbGu13r72JDXTXeEoitP0YCfQsp5kLA=
 github.com/aliyun/aliyun-log-go-sdk v0.1.106/go.mod h1:7QcyHasd4WLdC+lx4uCmdIBcl7WcgRHctwz8t1zAuPo=
 github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
 github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
 github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
 github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
-github.com/aliyun/credentials-go v1.4.6 h1:CG8rc/nxCNKfXbZWpWDzI9GjF4Tuu3Es14qT8Y0ClOk=
-github.com/aliyun/credentials-go v1.4.6/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
 github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw=
 github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
@@ -783,72 +766,42 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
 github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
 github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
 github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
-github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU=
-github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
 github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
 github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
-github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I=
-github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU=
 github.com/aws/aws-sdk-go-v2/config v1.30.3 h1:utupeVnE3bmB221W08P0Moz1lDI3OwYa2fBtUhl7TCc=
 github.com/aws/aws-sdk-go-v2/config v1.30.3/go.mod h1:NDGwOEBdpyZwLPlQkpKIO7frf18BW8PaCmAM9iUxQmI=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY=
 github.com/aws/aws-sdk-go-v2/credentials v1.18.3 h1:ptfyXmv+ooxzFwyuBth0yqABcjVIkjDL0iTYZBSbum8=
 github.com/aws/aws-sdk-go-v2/credentials v1.18.3/go.mod h1:Q43Nci++Wohb0qUh4m54sNln0dbxJw8PvQWkrwOkGOI=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 h1:nRniHAvjFJGUCl04F3WaAj7qp/rcz5Gi1OVoj5ErBkc=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2/go.mod h1:eJDFKAMHHUvv4a0Zfa7bQb//wFNUXGrbFpYRCHe2kD0=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 h1:sPiRHLVUIIQcoVZTNwqQcdtjkqkPopyYmIX0M5ElRf4=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2/go.mod h1:ik86P3sgV+Bk7c1tBFCwI3VxMoSEwl4YkRB9xn1s340=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 h1:ZdzDAg075H6stMZtbD2o+PyB933M/f20e9WmCBC17wA=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2/go.mod h1:eE1IIzXG9sdZCB0pNNpMpsYTLl4YdOQD3njiVN1e/E4=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2 h1:oxmDEO14NBZJbK/M8y3brhMFEIGN4j8a6Aq8eY0sqlo=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2/go.mod h1:4hH+8QCrk1uRWDPsVfsNDUup3taAjO8Dnx63au7smAU=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 h1:DYQbfSAWcMwRM0LbCDyQkPB1AcaZcLzLoaFrYcpyMag=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5/go.mod h1:Lav4KLgncVjjrwLWutOccjEgJ4T/RAdY+Ic0hmNIgI0=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.46.0 h1:nQX2q3dUdqwyxNPEjAw5WgH0F0HuHlVS8iq7TW3xHi8=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.46.0/go.mod h1:c0o7fqQS36cwXMizMSqpG4job2HsU1b8Wb2QoYSWyu0=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 h1:R3nSX1hguRy6MnknHiepSvqnnL8ansFwK2hidPesAYU=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1/go.mod h1:fmSiB4OAghn85lQgk7XN9l9bpFg5Bm1v3HuaXKytPEw=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.55.0 h1:uWgREKbrY/+EYuU9u4llSkbsIKLSEPriOSHmLCK3GAY=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.55.0/go.mod h1:6G0V3ndXAxeBFSDbUEZ3VTZgmL/9yoIuWM3s3AAV97E=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q=
 github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 h1:j7/jTOjWeJDolPwZ/J4yZ7dUsxsWZEsxNwH5O7F8eEA=
 github.com/aws/aws-sdk-go-v2/service/sso v1.27.0/go.mod h1:M0xdEPQtgpNT7kdAX4/vOAPkFj60hSQRb7TvW9B0iug=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 h1:ywQF2N4VjqX+Psw+jLjMmUL2g1RDHlvri3NxHA08MGI=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0/go.mod h1:Z+qv5Q6b7sWiclvbJyPSOT1BRVU9wfSUPaqQzZ1Xg3E=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk=
 github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 h1:bRP/a9llXSSgDPk7Rqn5GD/DQCGo6uk95plBFKoXt2M=
 github.com/aws/aws-sdk-go-v2/service/sts v1.36.0/go.mod h1:tgBsFzxwl65BWkuJ/x2EUs59bD4SfYKgikvFDJi1S58=
 github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
-github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
-github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
 github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 github.com/aziontech/azionapi-go-sdk v0.142.0 h1:1NOHXlC0/7VgbfbTIGVpsYn1THCugM4/SCOXVdUHQ+A=
 github.com/aziontech/azionapi-go-sdk v0.142.0/go.mod h1:cA5DY/VP4X5Eu11LpQNzNn83ziKjja7QVMIl4J45feA=
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
-github.com/baidubce/bce-sdk-go v0.9.235 h1:iAi+seH9w1Go2szFNzyGumahLGDsuYZ3i8hduX3qiM8=
-github.com/baidubce/bce-sdk-go v0.9.235/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
 github.com/baidubce/bce-sdk-go v0.9.237 h1:Y1M6GXubX65LtCKnkrM+8f68Gsl8aVynTGCE1COyu28=
 github.com/baidubce/bce-sdk-go v0.9.237/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
 github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
@@ -862,19 +815,13 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
-github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
-github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
 github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/blevesearch/bleve/v2 v2.5.2 h1:Ab0r0MODV2C5A6BEL87GqLBySqp/s9xFgceCju6BQk8=
-github.com/blevesearch/bleve/v2 v2.5.2/go.mod h1:5Dj6dUQxZM6aqYT3eutTD/GpWKGFSsV8f7LDidFbwXo=
 github.com/blevesearch/bleve/v2 v2.5.3 h1:9l1xtKaETv64SZc1jc4Sy0N804laSa/LeMbYddq1YEM=
 github.com/blevesearch/bleve/v2 v2.5.3/go.mod h1:Z/e8aWjiq8HeX+nW8qROSxiE0830yQA071dwR3yoMzw=
 github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y=
 github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
-github.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg=
-github.com/blevesearch/geo v0.2.3/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
 github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
 github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
 github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
@@ -912,8 +859,6 @@ github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHa
 github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
-github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
-github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
 github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -924,11 +869,8 @@ github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
 github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
 github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
-github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
-github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
 github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
 github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
-github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
 github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
 github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
@@ -936,10 +878,6 @@ github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdf
 github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
 github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
 github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
-github.com/casdoor/casdoor-go-sdk v1.9.0 h1:gJQD+ZpgcwUQefzQUsOf6t/nyubUNjfNXc3GicMNoe4=
-github.com/casdoor/casdoor-go-sdk v1.9.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
-github.com/casdoor/casdoor-go-sdk v1.12.0 h1:EtonFIxyI8ttw78hBwwGvvLcxNHLYgltbEU8oM1SzKM=
-github.com/casdoor/casdoor-go-sdk v1.12.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/casdoor/casdoor-go-sdk v1.14.0 h1:HrvwBxF0Vt+BSuNsf5MfhCvqSfETolpv4hzvP5XcCXc=
 github.com/casdoor/casdoor-go-sdk v1.14.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
@@ -953,7 +891,6 @@ github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -965,17 +902,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
-github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
 github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
-github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
 github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
 github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
-github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
 github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
 github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
-github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -1030,8 +962,6 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/dnsimple/dnsimple-go/v4 v4.0.0 h1:nUCICZSyZDiiqimAAL+E8XL+0sKGks5VRki5S8XotRo=
 github.com/dnsimple/dnsimple-go/v4 v4.0.0/go.mod h1:AXT2yfAFOntJx6iMeo1J/zKBw0ggXFYBt4e97dqqPnc=
-github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA=
-github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
 github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -1046,8 +976,6 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
 github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/ebitengine/purego v0.9.0-alpha.9 h1:+OPDXjPESTGhQ/2zO0aQeUR8r4o1feLMSDQzkA6z9ug=
-github.com/ebitengine/purego v0.9.0-alpha.9/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
 github.com/ebitengine/purego v0.9.0-alpha.10 h1:audsHbrB2mnadP/fVRdQRDc0lymjY7oWTNfzT59XICo=
 github.com/ebitengine/purego v0.9.0-alpha.10/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
@@ -1068,8 +996,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
 github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
 github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
-github.com/exoscale/egoscale/v3 v3.1.24 h1:EUWmjw/JgMj1faX5ojosjrJE5eY0QEWP0KBmLyFU6aE=
-github.com/exoscale/egoscale/v3 v3.1.24/go.mod h1:A53enXfm8nhVMpIYw0QxiwQ2P6AdCF4F/nVYChNEzdE=
 github.com/exoscale/egoscale/v3 v3.1.25 h1:Xy4LdmElaUXdf72vCt8gv9DCivKUlmW5Ar5ATInwWU8=
 github.com/exoscale/egoscale/v3 v3.1.25/go.mod h1:TJCI0OG3Lz2rnleRB0xwiOFg82uNCCytRqw7TxPoIvc=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -1114,19 +1040,13 @@ github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmn
 github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
 github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
 github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
-github.com/go-acme/alidns-20150109/v4 v4.5.10 h1:epLD0VaHR5XUpiM6mjm4MzQFICrk+zpuqDz2aO1/R/k=
-github.com/go-acme/alidns-20150109/v4 v4.5.10/go.mod h1:qGRq8kD0xVgn82qRSQmhHwh/oWxKRjF4Db5OI4ScV5g=
 github.com/go-acme/alidns-20150109/v4 v4.5.11 h1:CtOvASZao+WY9PImlpmWKqn2Dj+O3zzQX55KbqO/QAY=
 github.com/go-acme/alidns-20150109/v4 v4.5.11/go.mod h1:ZCuTWP0+J6sGCQpMNWhOUVK5vLvNsAF+oT2EmMrJA8U=
-github.com/go-acme/lego/v4 v4.25.1 h1:AYPUM7quPN/g2PcjjWw8sAMz3eV+Z8UWkr1kitDOyVA=
-github.com/go-acme/lego/v4 v4.25.1/go.mod h1:OORYyVNZPaNdIdVYCGSBNRNZDIjhQbPuFxwGDgWj/yM=
 github.com/go-acme/lego/v4 v4.25.2 h1:+D1Q+VnZrD+WJdlkgUEGHFFTcDrwGlE7q24IFtMmHDI=
 github.com/go-acme/lego/v4 v4.25.2/go.mod h1:OORYyVNZPaNdIdVYCGSBNRNZDIjhQbPuFxwGDgWj/yM=
 github.com/go-acme/tencentclouddnspod v1.0.1208 h1:xAVy1lmg2KcKKeYmFSBQUttwc1o1S++9QTjAotGC+BM=
 github.com/go-acme/tencentclouddnspod v1.0.1208/go.mod h1:yxG02mkbbVd7lTb97nOn7oj09djhm7hAwxNQw4B9dpQ=
 github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
-github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
-github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
 github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
 github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -1144,8 +1064,6 @@ github.com/go-gormigrate/gormigrate/v2 v2.1.4 h1:KOPEt27qy1cNzHfMZbp9YTmEuzkY4F4
 github.com/go-gormigrate/gormigrate/v2 v2.1.4/go.mod h1:y/6gPAH6QGAgP1UfHMiXcqGeJ88/GRQbfCReE1JJD5Y=
 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
-github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
-github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
 github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
 github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -1228,8 +1146,6 @@ github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
 github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
-github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
 github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
@@ -1445,8 +1361,6 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
 github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
 github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159 h1:6LZysc4iyO4cHB1aJsRklWfSEJr8CEhW7BmcM0SkYcU=
-github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.161 h1:1OLCB4r14cD3slEkoT4rjYM+rnQnq6v5We4jZ5YWEXw=
 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.161/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
 github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
@@ -1544,7 +1458,6 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
 github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -1552,7 +1465,6 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
 github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
@@ -1581,7 +1493,6 @@ github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
-github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
 github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -1615,8 +1526,6 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/linode/linodego v1.53.0 h1:UWr7bUUVMtcfsuapC+6blm6+jJLPd7Tf9MZUpdOERnI=
-github.com/linode/linodego v1.53.0/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
 github.com/linode/linodego v1.54.0 h1:29vTV5YjqjjwPxWLE8Qp1zgDDXM5ifAQ2T6azAYsj/w=
 github.com/linode/linodego v1.54.0/go.mod h1:VHlFAbhj18634Cd7B7L5D723kFKFQMOxzIutSMcWsB4=
 github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
@@ -1636,8 +1545,6 @@ github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
 github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
-github.com/mark3labs/mcp-go v0.34.0 h1:eWy7WBGvhk6EyAAyVzivTCprE52iXJwNtvHV6Cv3bR0=
-github.com/mark3labs/mcp-go v0.34.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
 github.com/mark3labs/mcp-go v0.37.0 h1:BywvZLPRT6Zx6mMG/MJfxLSZQkTGIcJSEGKsvr4DsoQ=
 github.com/mark3labs/mcp-go v0.37.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -1666,8 +1573,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
-github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
 github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
@@ -1681,16 +1586,12 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
 github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
-github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
-github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
 github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
 github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
 github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
 github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
 github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
 github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
-github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
-github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
 github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
 github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
 github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
@@ -1779,12 +1680,8 @@ github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
 github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
 github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
 github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
-github.com/nrdcg/oci-go-sdk/common/v1065 v1065.95.2 h1:a7QUZD5c+NkrFrdkdyJUO9cOUo8VQJyRkcIzk9Wh+DI=
-github.com/nrdcg/oci-go-sdk/common/v1065 v1065.95.2/go.mod h1:O6osg9dPzXq7H2ib/1qzimzG5oXSJFgccR7iawg7SwA=
 github.com/nrdcg/oci-go-sdk/common/v1065 v1065.97.1 h1:sThWygFwYB5gCgM2i9Cnp703igdpQs+cN1P7mH+uIYM=
 github.com/nrdcg/oci-go-sdk/common/v1065 v1065.97.1/go.mod h1:ILhKsVZzfmkaqe0ugNHmEvbYB0VnGHTGkrIth5ssOWk=
-github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.95.2 h1:yflYnbQu4ciWH/GEztqlAccLPw4k5mp11uhW++al5ow=
-github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.95.2/go.mod h1:atPDu37gu8HT7TtPpovrkgNmDAgOGM6TVEJ7ANTblMs=
 github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.97.1 h1:sRKKsf4qZTvIPK9Dx7cbkVMIUcvsLOOePoSZcnrTKGc=
 github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.97.1/go.mod h1:NzMIBls+KkMpcVoyjU/gzSkWw4HML2BQb3bEHOQBFLc=
 github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
@@ -1844,7 +1741,6 @@ github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2
 github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
 github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
-github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
 github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
@@ -1880,8 +1776,6 @@ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
-github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
 github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -1922,8 +1816,6 @@ github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQB
 github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
-github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
 github.com/redis/go-redis/v9 v9.12.0 h1:XlVPGlflh4nxfhsNXPA8Qp6EmEfTo0rp8oaBzPipXnU=
 github.com/redis/go-redis/v9 v9.12.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
 github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
@@ -1958,16 +1850,10 @@ github.com/sacloud/iaas-api-go v1.16.1/go.mod h1:QVPHLwYzpECMsuml55I3FWAggsb4XSu
 github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo=
 github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=
 github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ=
-github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
-github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
 github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
 github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
 github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
 github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
-github.com/sashabaranov/go-openai v1.40.5 h1:SwIlNdWflzR1Rxd1gv3pUg6pwPc6cQ2uMoHs8ai+/NY=
-github.com/sashabaranov/go-openai v1.40.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
-github.com/sashabaranov/go-openai v1.41.0 h1:tPR4Ro4kl4GhY8mroonGQLkSeI8LGzL6atbKLPQkK14=
-github.com/sashabaranov/go-openai v1.41.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/sashabaranov/go-openai v1.41.1 h1:zf5tM+GuxpyiyD9XZg8nCqu52eYFQg9OOew0gnIuDy4=
 github.com/sashabaranov/go-openai v1.41.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@@ -1978,8 +1864,6 @@ github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5
 github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA=
 github.com/selectel/go-selvpcclient/v4 v4.1.0 h1:22lBp+rzg9g2MP4iiGhpVAcCt0kMv7I7uV1W3taLSvQ=
 github.com/selectel/go-selvpcclient/v4 v4.1.0/go.mod h1:eFhL1KUW159KOJVeGO7k/Uxl0TYd/sBkWXjuF5WxmYk=
-github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
-github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
 github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
 github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -2013,8 +1897,6 @@ github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
 github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/sony/sonyflake/v2 v2.2.0 h1:wSzEoewlWnUtc3SZX/MpT8zsWTuAnjwrprUYfuPl9Jg=
 github.com/sony/sonyflake/v2 v2.2.0/go.mod h1:09EcfmR846JLupbkgVfzp8QtQwJ+Y8e69VVayHdawzg=
-github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
-github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
 github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -2037,8 +1919,6 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
-github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
 github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
@@ -2077,8 +1957,6 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW
 github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
 github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1208/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210 h1:waSk2KyI2VvXtR+XQJm0v1lWfgbJg51iSWJh4hWnyeo=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.6 h1:kSt8iJikKhDt2QZOt9BivlR+x3ISQRElm07hX+GOoI8=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.6/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
@@ -2108,14 +1986,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
 github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
-github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
-github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
 github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b68e3iMvkr27fU7JqPKU4j7tIITZnjQX1k=
 github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c=
-github.com/uozi-tech/cosy v1.24.10 h1:rACflQ8RboZ0QX+riYRq8RaF5mhukt1HxVT9JmGkKOQ=
-github.com/uozi-tech/cosy v1.24.10/go.mod h1:h0ViTCx65zdRTW0nL+t96WKUi8cW5ThbE+ciKzsWjsY=
-github.com/uozi-tech/cosy v1.25.1 h1:ftXZYBLdBHDOGRX6ZIa7tAi6xvEr6sLe6vWa/FCx1fk=
-github.com/uozi-tech/cosy v1.25.1/go.mod h1:o0gM8j/bjRM3LApTp+UTVR1PRxcJ1EhQkoJ9nSGvRpA=
 github.com/uozi-tech/cosy v1.25.3 h1:sC3ctnZva7ziuFDiO9YM+5tTvEBt7grLKRaLNbhP55o=
 github.com/uozi-tech/cosy v1.25.3/go.mod h1:o0gM8j/bjRM3LApTp+UTVR1PRxcJ1EhQkoJ9nSGvRpA=
 github.com/uozi-tech/cosy-driver-mysql v0.2.2 h1:22S/XNIvuaKGqxQPsYPXN8TZ8hHjCQdcJKVQ83Vzxoo=
@@ -2125,10 +1997,6 @@ github.com/uozi-tech/cosy-driver-postgres v0.2.1/go.mod h1:eAy1A89yHbAEfjkhNAifa
 github.com/uozi-tech/cosy-driver-sqlite v0.2.1 h1:W+Z4pY25PSJCeReqroG7LIBeffsqotbpHzgqSMqZDIM=
 github.com/uozi-tech/cosy-driver-sqlite v0.2.1/go.mod h1:2ya7Z5P3HzFi1ktfL8gvwaAGx0DDV0bmWxNSNpaLlwo=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
-github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
-github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
-github.com/urfave/cli/v3 v3.3.9 h1:54roEDJcTWuucl6MSQ3B+pQqt1ePh/xOQokhEYl5Gfs=
-github.com/urfave/cli/v3 v3.3.9/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
 github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
 github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
 github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
@@ -2150,16 +2018,10 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/yandex-cloud/go-genproto v0.14.0 h1:yDqD260mICkjodXyAaDhESfrLr6gIGwwRc9MYE0jvW0=
-github.com/yandex-cloud/go-genproto v0.14.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
 github.com/yandex-cloud/go-genproto v0.15.0 h1:1E9ITJGki4g1F/6TkaLxyOXtTxjv772sQ7ifsEfFrxs=
 github.com/yandex-cloud/go-genproto v0.15.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
-github.com/yandex-cloud/go-sdk/services/dns v0.0.3 h1:erphTBXKSpm/tETa6FXrw4niSHjhySzAKHUc2/BZKx0=
-github.com/yandex-cloud/go-sdk/services/dns v0.0.3/go.mod h1:lbBaFJVouETfVnd3YzNF5vW6vgYR2FVfGLUzLexyGlI=
 github.com/yandex-cloud/go-sdk/services/dns v0.0.5 h1:yrUPX9G97WB4jTeuCNzwWT1NwUo2CiXZWH5FSbjJztw=
 github.com/yandex-cloud/go-sdk/services/dns v0.0.5/go.mod h1:UWqmruzRLUXgKJkHXilIuKB6I92d6xM3yPAx4rdz+x8=
-github.com/yandex-cloud/go-sdk/v2 v2.0.8 h1:wQNIzEZYnClSQyo2fjEgnGEErWjJNBpSAinaKcP+VSg=
-github.com/yandex-cloud/go-sdk/v2 v2.0.8/go.mod h1:9Gqpq7d0EUAS+H2OunILtMi3hmMPav+fYoy9rmydM4s=
 github.com/yandex-cloud/go-sdk/v2 v2.2.0 h1:AJrGhvISAeVgqJdbWfrZSCv7UeT6eg6/LLDkc0X+Urc=
 github.com/yandex-cloud/go-sdk/v2 v2.2.0/go.mod h1:MLYmaUcfc0FmergrmXsv0USCIkjzITVsAUoP4acEJYI=
 github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
@@ -2307,8 +2169,6 @@ golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZP
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
-golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
-golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
 golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
 golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2460,8 +2320,6 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
-golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
-golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
 golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
 golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -2644,8 +2502,6 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
-golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
 golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
@@ -2669,8 +2525,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
-golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
-golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2692,8 +2548,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
 golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
 golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2795,8 +2649,6 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
-golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
 gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
 gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
 gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
@@ -2863,8 +2715,6 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
 google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
 google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
-google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
-google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
 google.golang.org/api v0.245.0 h1:YliGvz1rjXB+sTLNIST6Ffeji9WlRdLQ+LPl9ruSa5Y=
 google.golang.org/api v0.245.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -3009,12 +2859,8 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl
 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
 google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
-google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
-google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
 google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
 google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -3059,8 +2905,6 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
 google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
 google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
@@ -3142,14 +2986,10 @@ gorm.io/gen v0.3.27 h1:ziocAFLpE7e0g4Rum69pGfB9S6DweTxK8gAun7cU8as=
 gorm.io/gen v0.3.27/go.mod h1:9zquz2xD1f3Eb/eHq4oLn2z6vDVvQlCY5S3uMBLv4EA=
 gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
-gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
-gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
 gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
 gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
 gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
 gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
-gorm.io/plugin/dbresolver v1.6.0 h1:XvKDeOtTn1EIX6s4SrKpEH82q0gXVemhYjbYZFGFVcw=
-gorm.io/plugin/dbresolver v1.6.0/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
 gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
 gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
 gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
@@ -3205,7 +3045,6 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw
 modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
 modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
-nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=

+ 7 - 7
internal/analytic/node.go

@@ -27,13 +27,13 @@ type NodeInfo struct {
 }
 
 type NodeStat struct {
-	AvgLoad           *load.AvgStat                        `json:"avg_load"`
-	CPUPercent        float64                              `json:"cpu_percent"`
-	MemoryPercent     float64                              `json:"memory_percent"`
-	DiskPercent       float64                              `json:"disk_percent"`
-	Network           net.IOCountersStat                   `json:"network"`
-	Status            bool                                 `json:"status"`
-	ResponseAt        time.Time                            `json:"response_at"`
+	AvgLoad           *load.AvgStat               `json:"avg_load"`
+	CPUPercent        float64                     `json:"cpu_percent"`
+	MemoryPercent     float64                     `json:"memory_percent"`
+	DiskPercent       float64                     `json:"disk_percent"`
+	Network           net.IOCountersStat          `json:"network"`
+	Status            bool                        `json:"status"`
+	ResponseAt        time.Time                   `json:"response_at"`
 	UpstreamStatusMap map[string]*upstream.Status `json:"upstream_status_map"`
 }
 

+ 2 - 2
internal/analytic/node_stat.go

@@ -46,12 +46,12 @@ func GetNodeStat() (data NodeStat) {
 
 	// Get upstream status for current node
 	upstreamService := upstream.GetUpstreamService()
-	
+
 	// Ensure upstream availability test is performed if targets exist
 	if upstreamService.GetTargetCount() > 0 {
 		upstreamService.PerformAvailabilityTest()
 	}
-	
+
 	upstreamStatusMap := upstreamService.GetAvailabilityMap()
 
 	return NodeStat{

+ 2 - 2
internal/cert/sync.go

@@ -80,7 +80,7 @@ func SyncToRemoteServer(c *model.Cert) (err error) {
 type SyncNotificationPayload struct {
 	StatusCode int    `json:"status_code"`
 	CertName   string `json:"cert_name"`
-	NodeName    string `json:"node_name"`
+	NodeName   string `json:"node_name"`
 	Response   string `json:"response"`
 }
 
@@ -115,7 +115,7 @@ func deploy(node *model.Node, c *model.Cert, payloadBytes []byte) (err error) {
 	notificationPayload := &SyncNotificationPayload{
 		StatusCode: resp.StatusCode,
 		CertName:   c.Name,
-		NodeName:    node.Name,
+		NodeName:   node.Name,
 		Response:   string(respBody),
 	}
 

+ 13 - 13
internal/config/config.go

@@ -19,18 +19,18 @@ const (
 type ProxyTarget = upstream.ProxyTarget
 
 type Config struct {
-	Name          string          `json:"name"`
-	Content       string          `json:"content"`
-	FilePath      string          `json:"filepath,omitempty"`
-	ModifiedAt    time.Time       `json:"modified_at"`
-	Size          int64           `json:"size,omitempty"`
-	IsDir         bool            `json:"is_dir"`
-	NamespaceID   uint64          `json:"namespace_id"`
+	Name          string           `json:"name"`
+	Content       string           `json:"content"`
+	FilePath      string           `json:"filepath,omitempty"`
+	ModifiedAt    time.Time        `json:"modified_at"`
+	Size          int64            `json:"size,omitempty"`
+	IsDir         bool             `json:"is_dir"`
+	NamespaceID   uint64           `json:"namespace_id"`
 	Namespace     *model.Namespace `json:"namespace,omitempty"`
-	Status        ConfigStatus    `json:"status"`
-	Dir           string          `json:"dir"`
-	Urls          []string        `json:"urls,omitempty"`
-	ProxyTargets  []ProxyTarget   `json:"proxy_targets,omitempty"`
-	SyncNodeIds   []uint64        `json:"sync_node_ids,omitempty"`
-	SyncOverwrite bool            `json:"sync_overwrite"`
+	Status        ConfigStatus     `json:"status"`
+	Dir           string           `json:"dir"`
+	Urls          []string         `json:"urls,omitempty"`
+	ProxyTargets  []ProxyTarget    `json:"proxy_targets,omitempty"`
+	SyncNodeIds   []uint64         `json:"sync_node_ids,omitempty"`
+	SyncOverwrite bool             `json:"sync_overwrite"`
 }

+ 5 - 5
internal/config/generic_list.go

@@ -302,11 +302,11 @@ func FuzzyFilterMatcher(fileName string, status ConfigStatus, namespaceID uint64
 // DefaultConfigBuilder provides basic config building logic
 func DefaultConfigBuilder(fileName string, fileInfo os.FileInfo, status ConfigStatus, namespaceID uint64, namespace *model.Namespace) Config {
 	return Config{
-		Name:       fileName,
-		ModifiedAt: fileInfo.ModTime(),
-		Size:       fileInfo.Size(),
-		IsDir:      fileInfo.IsDir(),
-		Status:     status,
+		Name:        fileName,
+		ModifiedAt:  fileInfo.ModTime(),
+		Size:        fileInfo.Size(),
+		IsDir:       fileInfo.IsDir(),
+		Status:      status,
 		NamespaceID: namespaceID,
 		Namespace:   namespace,
 	}

+ 6 - 6
internal/config/sync.go

@@ -104,7 +104,7 @@ func SyncRenameOnRemoteServer(origPath, newPath string, syncNodeIds []uint64) (e
 type SyncNotificationPayload struct {
 	StatusCode int    `json:"status_code"`
 	ConfigName string `json:"config_name"`
-	NodeName    string `json:"node_name"`
+	NodeName   string `json:"node_name"`
 	Response   string `json:"response"`
 }
 
@@ -139,7 +139,7 @@ func (p *SyncConfigPayload) deploy(node *model.Node, c *model.Config, payloadByt
 	notificationPayload := &SyncNotificationPayload{
 		StatusCode: resp.StatusCode,
 		ConfigName: c.Name,
-		NodeName:    node.Name,
+		NodeName:   node.Name,
 		Response:   string(respBody),
 	}
 
@@ -162,7 +162,7 @@ type SyncRenameNotificationPayload struct {
 	StatusCode int    `json:"status_code"`
 	OrigPath   string `json:"orig_path"`
 	NewPath    string `json:"new_path"`
-	NodeName    string `json:"node_name"`
+	NodeName   string `json:"node_name"`
 	Response   string `json:"response"`
 }
 
@@ -210,7 +210,7 @@ func (p *RenameConfigPayload) rename(node *model.Node) (err error) {
 		StatusCode: resp.StatusCode,
 		OrigPath:   p.Filepath,
 		NewPath:    p.NewFilepath,
-		NodeName:    node.Name,
+		NodeName:   node.Name,
 		Response:   string(respBody),
 	}
 
@@ -259,7 +259,7 @@ type DeleteConfigPayload struct {
 type SyncDeleteNotificationPayload struct {
 	StatusCode int    `json:"status_code"`
 	Path       string `json:"path"`
-	NodeName    string `json:"node_name"`
+	NodeName   string `json:"node_name"`
 	Response   string `json:"response"`
 }
 
@@ -302,7 +302,7 @@ func (p *DeleteConfigPayload) delete(node *model.Node) (err error) {
 	notificationPayload := &SyncDeleteNotificationPayload{
 		StatusCode: resp.StatusCode,
 		Path:       p.Filepath,
-		NodeName:    node.Name,
+		NodeName:   node.Name,
 		Response:   string(respBody),
 	}
 

+ 20 - 20
internal/helper/copy.go

@@ -1,31 +1,31 @@
 package helper
 
 import (
-    "io"
-    "os"
+	"io"
+	"os"
 )
 
 func CopyFile(src, dst string) (int64, error) {
-    sourceFileStat, err := os.Stat(src)
-    if err != nil {
-        return 0, err
-    }
+	sourceFileStat, err := os.Stat(src)
+	if err != nil {
+		return 0, err
+	}
 
-    if !sourceFileStat.Mode().IsRegular() {
-        return 0, nil
-    }
+	if !sourceFileStat.Mode().IsRegular() {
+		return 0, nil
+	}
 
-    source, err := os.Open(src)
-    if err != nil {
-        return 0, err
-    }
-    defer source.Close()
+	source, err := os.Open(src)
+	if err != nil {
+		return 0, err
+	}
+	defer source.Close()
 
-    destination, err := os.Create(dst)
-    if err != nil {
-        return 0, err
-    }
-    defer destination.Close()
+	destination, err := os.Create(dst)
+	if err != nil {
+		return 0, err
+	}
+	defer destination.Close()
 
-    return io.Copy(destination, source)
+	return io.Copy(destination, source)
 }

+ 1 - 1
internal/helper/docker.go

@@ -8,7 +8,7 @@ import (
 
 func InNginxUIOfficialDocker() bool {
 	return cast.ToBool(os.Getenv("NGINX_UI_OFFICIAL_DOCKER")) &&
-	!cast.ToBool(os.Getenv("NGINX_UI_IGNORE_DOCKER_SOCKET"))
+		!cast.ToBool(os.Getenv("NGINX_UI_IGNORE_DOCKER_SOCKET"))
 }
 
 func DockerSocketExists() bool {

+ 5 - 0
internal/kernel/boot.go

@@ -21,6 +21,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/internal/mcp"
 	"github.com/0xJacky/Nginx-UI/internal/passkey"
 	"github.com/0xJacky/Nginx-UI/internal/self_check"
+	"github.com/0xJacky/Nginx-UI/internal/sitecheck"
 	"github.com/0xJacky/Nginx-UI/internal/validation"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
@@ -85,6 +86,10 @@ func InitAfterDatabase(ctx context.Context) {
 		analytic.RetrieveNodesStatus,
 		passkey.Init,
 		mcp.Init,
+		func(ctx context.Context) {
+			service := sitecheck.GetService()
+			service.Start()
+		},
 	}
 
 	for _, v := range asyncs {

+ 1 - 1
internal/migrate/6.rename_environments_to_nodes.go

@@ -121,4 +121,4 @@ var RenameEnvironmentsToNodes = &gormigrate.Migration{
 
 		return nil
 	},
-}
+}

+ 7 - 0
internal/nginx/nginx_directives.json

@@ -2857,6 +2857,13 @@
       "https://nginx.org/en/docs/stream/ngx_stream_ssl_module.html#ssl_certificate_cache"
     ]
   },
+  "ssl_certificate_compression": {
+    "links": [
+      "https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_compression",
+      "https://nginx.org/en/docs/mail/ngx_mail_ssl_module.html#ssl_certificate_compression",
+      "https://nginx.org/en/docs/stream/ngx_stream_ssl_module.html#ssl_certificate_compression"
+    ]
+  },
   "ssl_certificate_key": {
     "links": [
       "https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key",

+ 3 - 3
internal/notification/wecom.go

@@ -38,16 +38,16 @@ func init() {
 		message := wecomMessage{
 			MsgType: "text",
 		}
-		
+
 		title := msg.GetTitle(n.Language)
 		content := msg.GetContent(n.Language)
-		
+
 		// Combine title and content
 		fullMessage := title
 		if content != "" {
 			fullMessage = fmt.Sprintf("%s\n\n%s", title, content)
 		}
-		
+
 		message.Text.Content = fullMessage
 
 		// Marshal to JSON

+ 8 - 8
internal/site/list.go

@@ -11,11 +11,11 @@ import (
 
 // ListOptions represents the options for listing sites
 type ListOptions struct {
-	Search     string
-	Name       string
-	Status     string
-	OrderBy    string
-	Sort       string
+	Search      string
+	Name        string
+	Status      string
+	OrderBy     string
+	Sort        string
 	NamespaceID uint64
 }
 
@@ -28,7 +28,7 @@ func GetSiteConfigs(ctx context.Context, options *ListOptions, sites []*model.Si
 		Status:      options.Status,
 		OrderBy:     options.OrderBy,
 		Sort:        options.Sort,
-		NamespaceID:  options.NamespaceID,
+		NamespaceID: options.NamespaceID,
 		IncludeDirs: false, // Filter out directories for site configurations
 	}
 
@@ -81,8 +81,8 @@ func buildConfig(fileName string, fileInfo os.FileInfo, status config.ConfigStat
 		Size:         fileInfo.Size(),
 		IsDir:        fileInfo.IsDir(),
 		Status:       status,
-		NamespaceID:   namespaceID,
-		Namespace:     namespace,
+		NamespaceID:  namespaceID,
+		Namespace:    namespace,
 		Urls:         indexedSite.Urls,
 		ProxyTargets: proxyTargets,
 	}

+ 1 - 1
internal/site/save.go

@@ -54,7 +54,7 @@ func Save(name string, content string, overwrite bool, namespaceId uint64, syncN
 	_, err = s.Where(s.Path.Eq(path)).
 		Select(s.NamespaceID, s.SyncNodeIDs).
 		Updates(&model.Site{
-			NamespaceID:  namespaceId,
+			NamespaceID: namespaceId,
 			SyncNodeIDs: syncNodeIds,
 		})
 	if err != nil {

+ 651 - 0
internal/sitecheck/checker.go

@@ -0,0 +1,651 @@
+package sitecheck
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"maps"
+	"net"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/site"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+type SiteChecker struct {
+	sites            map[string]*SiteInfo
+	mu               sync.RWMutex
+	options          CheckOptions
+	client           *http.Client
+	onUpdateCallback func([]*SiteInfo) // Callback for notifying updates
+}
+
+// NewSiteChecker creates a new site checker
+func NewSiteChecker(options CheckOptions) *SiteChecker {
+	transport := &http.Transport{
+		Dial: (&net.Dialer{
+			Timeout: 5 * time.Second,
+		}).Dial,
+		TLSHandshakeTimeout: 5 * time.Second,
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: true, // Skip SSL verification for internal sites
+		},
+	}
+
+	client := &http.Client{
+		Transport: transport,
+		Timeout:   options.Timeout,
+	}
+
+	if !options.FollowRedirects {
+		client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		}
+	} else if options.MaxRedirects > 0 {
+		client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+			if len(via) >= options.MaxRedirects {
+				return fmt.Errorf("stopped after %d redirects", options.MaxRedirects)
+			}
+			return nil
+		}
+	}
+
+	return &SiteChecker{
+		sites:   make(map[string]*SiteInfo),
+		options: options,
+		client:  client,
+	}
+}
+
+// SetUpdateCallback sets the callback function for site updates
+func (sc *SiteChecker) SetUpdateCallback(callback func([]*SiteInfo)) {
+	sc.onUpdateCallback = callback
+}
+
+// CollectSites collects URLs from enabled indexed sites only
+func (sc *SiteChecker) CollectSites() {
+	sc.mu.Lock()
+	defer sc.mu.Unlock()
+
+	// Clear existing sites
+	sc.sites = make(map[string]*SiteInfo)
+
+	// Debug: log indexed sites count
+	logger.Infof("Found %d indexed sites", len(site.IndexedSites))
+
+	// Collect URLs from indexed sites, but only from enabled sites
+	for siteName, indexedSite := range site.IndexedSites {
+		// Check site status - only collect from enabled sites
+		siteStatus := site.GetSiteStatus(siteName)
+		if siteStatus != site.SiteStatusEnabled {
+			logger.Debugf("Skipping site %s (status: %s) - only collecting from enabled sites", siteName, siteStatus)
+			continue
+		}
+
+		logger.Debugf("Processing enabled site: %s with %d URLs", siteName, len(indexedSite.Urls))
+		for _, url := range indexedSite.Urls {
+			if url != "" {
+				logger.Debugf("Adding site URL: %s", url)
+				// Load site config to determine display URL
+				config, err := LoadSiteConfig(url)
+				protocol := "http" // default protocol
+				if err == nil && config != nil && config.HealthCheckConfig != nil && config.HealthCheckConfig.Protocol != "" {
+					protocol = config.HealthCheckConfig.Protocol
+					logger.Debugf("Site %s using protocol: %s", url, protocol)
+				} else {
+					logger.Debugf("Site %s using default protocol: %s (config error: %v)", url, protocol, err)
+				}
+
+				// Parse URL components for legacy fields
+				_, hostPort := parseURLComponents(url, protocol)
+
+				// Get or create site config to get ID
+				siteConfig := getOrCreateSiteConfigForURL(url)
+
+				siteInfo := &SiteInfo{
+					ID:          siteConfig.ID,
+					Host:        siteConfig.Host,
+					Port:        siteConfig.Port,
+					Scheme:      siteConfig.Scheme,
+					DisplayURL:  siteConfig.GetURL(),
+					Name:        extractDomainName(url),
+					Status:      StatusChecking,
+					LastChecked: time.Now().Unix(),
+					// Legacy fields for backward compatibility
+					URL:                 url,
+					HealthCheckProtocol: protocol,
+					HostPort:            hostPort,
+				}
+				sc.sites[url] = siteInfo
+			}
+		}
+	}
+
+	logger.Infof("Collected %d sites for checking (enabled sites only)", len(sc.sites))
+}
+
+// getOrCreateSiteConfigForURL gets or creates a site config for the given URL
+func getOrCreateSiteConfigForURL(url string) *model.SiteConfig {
+	// Parse URL to get host:port
+	tempConfig := &model.SiteConfig{}
+	tempConfig.SetFromURL(url)
+
+	sc := query.SiteConfig
+	siteConfig, err := sc.Where(sc.Host.Eq(tempConfig.Host)).First()
+	if err != nil {
+		// Record doesn't exist, create a new one
+		newConfig := &model.SiteConfig{
+			Host:               tempConfig.Host,
+			Port:               tempConfig.Port,
+			Scheme:             tempConfig.Scheme,
+			DisplayURL:         url,
+			HealthCheckEnabled: true,
+			CheckInterval:      300,
+			Timeout:            10,
+			UserAgent:          "Nginx-UI Site Checker/1.0",
+			MaxRedirects:       3,
+			FollowRedirects:    true,
+			CheckFavicon:       true,
+		}
+
+		// Create the record in database
+		if err := sc.Create(newConfig); err != nil {
+			logger.Errorf("Failed to create site config for %s: %v", url, err)
+			// Return temp config with a fake ID to avoid crashes
+			tempConfig.ID = 0
+			return tempConfig
+		}
+
+		return newConfig
+	}
+
+	// Record exists, ensure it has the correct URL information
+	if siteConfig.DisplayURL == "" {
+		siteConfig.DisplayURL = url
+		siteConfig.SetFromURL(url)
+		// Try to save the updated config, but don't fail if it doesn't work
+		sc.Save(siteConfig)
+	}
+
+	return siteConfig
+}
+
+// CheckSite checks a single site's availability
+func (sc *SiteChecker) CheckSite(ctx context.Context, siteURL string) (*SiteInfo, error) {
+	// Try enhanced health check first if config exists
+	config, err := LoadSiteConfig(siteURL)
+	if err == nil && config != nil && config.HealthCheckConfig != nil {
+		enhancedChecker := NewEnhancedSiteChecker()
+		siteInfo, err := enhancedChecker.CheckSiteWithConfig(ctx, siteURL, config.HealthCheckConfig)
+		if err == nil && siteInfo != nil {
+			// Fill in additional details
+			siteInfo.Name = extractDomainName(siteURL)
+			siteInfo.LastChecked = time.Now().Unix()
+
+			// Set health check protocol and display URL
+			siteInfo.HealthCheckProtocol = config.HealthCheckConfig.Protocol
+			siteInfo.DisplayURL = generateDisplayURL(siteURL, config.HealthCheckConfig.Protocol)
+
+			// Parse URL components
+			scheme, hostPort := parseURLComponents(siteURL, config.HealthCheckConfig.Protocol)
+			siteInfo.Scheme = scheme
+			siteInfo.HostPort = hostPort
+
+			// Try to get favicon if enabled and not a gRPC check
+			if sc.options.CheckFavicon && !isGRPCProtocol(config.HealthCheckConfig.Protocol) {
+				faviconURL, faviconData := sc.tryGetFavicon(ctx, siteURL)
+				siteInfo.FaviconURL = faviconURL
+				siteInfo.FaviconData = faviconData
+			}
+
+			return siteInfo, nil
+		}
+	}
+
+	// Fallback to basic HTTP check, but preserve original protocol if available
+	originalProtocol := "http" // default
+	if config != nil && config.HealthCheckConfig != nil && config.HealthCheckConfig.Protocol != "" {
+		originalProtocol = config.HealthCheckConfig.Protocol
+	}
+	return sc.checkSiteBasic(ctx, siteURL, originalProtocol)
+}
+
+// checkSiteBasic performs basic HTTP health check
+func (sc *SiteChecker) checkSiteBasic(ctx context.Context, siteURL string, originalProtocol string) (*SiteInfo, error) {
+	start := time.Now()
+
+	req, err := http.NewRequestWithContext(ctx, "GET", siteURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("User-Agent", sc.options.UserAgent)
+
+	resp, err := sc.client.Do(req)
+	if err != nil {
+		// Parse URL components for legacy fields
+		_, hostPort := parseURLComponents(siteURL, originalProtocol)
+
+		// Get or create site config to get ID
+		siteConfig := getOrCreateSiteConfigForURL(siteURL)
+
+		return &SiteInfo{
+			ID:           siteConfig.ID,
+			Host:         siteConfig.Host,
+			Port:         siteConfig.Port,
+			Scheme:       siteConfig.Scheme,
+			DisplayURL:   siteConfig.GetURL(),
+			Name:         extractDomainName(siteURL),
+			Status:       StatusOffline,
+			ResponseTime: time.Since(start).Milliseconds(),
+			LastChecked:  time.Now().Unix(),
+			Error:        err.Error(),
+			// Legacy fields for backward compatibility
+			URL:                 siteURL,
+			HealthCheckProtocol: originalProtocol,
+			HostPort:            hostPort,
+		}, nil
+	}
+	defer resp.Body.Close()
+
+	responseTime := time.Since(start).Milliseconds()
+
+	// Parse URL components for legacy fields
+	_, hostPort := parseURLComponents(siteURL, originalProtocol)
+
+	// Get or create site config to get ID
+	siteConfig := getOrCreateSiteConfigForURL(siteURL)
+
+	siteInfo := &SiteInfo{
+		ID:           siteConfig.ID,
+		Host:         siteConfig.Host,
+		Port:         siteConfig.Port,
+		Scheme:       siteConfig.Scheme,
+		DisplayURL:   siteConfig.GetURL(),
+		Name:         extractDomainName(siteURL),
+		StatusCode:   resp.StatusCode,
+		ResponseTime: responseTime,
+		LastChecked:  time.Now().Unix(),
+		// Legacy fields for backward compatibility
+		URL:                 siteURL,
+		HealthCheckProtocol: originalProtocol,
+		HostPort:            hostPort,
+	}
+
+	// Determine status based on status code
+	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
+		siteInfo.Status = StatusOnline
+	} else {
+		siteInfo.Status = StatusError
+		siteInfo.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
+	}
+
+	// Read response body for title and favicon extraction
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		logger.Warnf("Failed to read response body for %s: %v", siteURL, err)
+		return siteInfo, nil
+	}
+
+	// Extract title
+	siteInfo.Title = extractTitle(string(body))
+
+	// Extract favicon if enabled
+	if sc.options.CheckFavicon {
+		faviconURL, faviconData := sc.extractFavicon(ctx, siteURL, string(body))
+		siteInfo.FaviconURL = faviconURL
+		siteInfo.FaviconData = faviconData
+	}
+
+	return siteInfo, nil
+}
+
+// tryGetFavicon attempts to get favicon for enhanced checks
+func (sc *SiteChecker) tryGetFavicon(ctx context.Context, siteURL string) (string, string) {
+	// Make a simple GET request to get the HTML
+	req, err := http.NewRequestWithContext(ctx, "GET", siteURL, nil)
+	if err != nil {
+		return "", ""
+	}
+
+	req.Header.Set("User-Agent", sc.options.UserAgent)
+
+	resp, err := sc.client.Do(req)
+	if err != nil {
+		return "", ""
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
+		return "", ""
+	}
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", ""
+	}
+
+	return sc.extractFavicon(ctx, siteURL, string(body))
+}
+
+// CheckAllSites checks all collected sites concurrently
+func (sc *SiteChecker) CheckAllSites(ctx context.Context) {
+	sc.mu.RLock()
+	urls := make([]string, 0, len(sc.sites))
+	for url := range sc.sites {
+		urls = append(urls, url)
+	}
+	sc.mu.RUnlock()
+
+	// Use a semaphore to limit concurrent requests
+	semaphore := make(chan struct{}, 10) // Max 10 concurrent requests
+	var wg sync.WaitGroup
+
+	for _, url := range urls {
+		wg.Add(1)
+		go func(siteURL string) {
+			defer wg.Done()
+
+			semaphore <- struct{}{}        // Acquire semaphore
+			defer func() { <-semaphore }() // Release semaphore
+
+			siteInfo, err := sc.CheckSite(ctx, siteURL)
+			if err != nil {
+				logger.Errorf("Failed to check site %s: %v", siteURL, err)
+				return
+			}
+
+			sc.mu.Lock()
+			sc.sites[siteURL] = siteInfo
+			sc.mu.Unlock()
+		}(url)
+	}
+
+	wg.Wait()
+	logger.Infof("Completed checking %d sites", len(urls))
+
+	// Notify WebSocket clients of the update
+	if sc.onUpdateCallback != nil {
+		sites := make([]*SiteInfo, 0, len(sc.sites))
+		sc.mu.RLock()
+		for _, site := range sc.sites {
+			sites = append(sites, site)
+		}
+		sc.mu.RUnlock()
+		sc.onUpdateCallback(sites)
+	}
+}
+
+// GetSites returns all checked sites
+func (sc *SiteChecker) GetSites() map[string]*SiteInfo {
+	sc.mu.RLock()
+	defer sc.mu.RUnlock()
+
+	// Create a copy to avoid race conditions
+	result := make(map[string]*SiteInfo)
+	maps.Copy(result, sc.sites)
+	return result
+}
+
+// GetSitesList returns sites as a slice
+func (sc *SiteChecker) GetSitesList() []*SiteInfo {
+	sc.mu.RLock()
+	defer sc.mu.RUnlock()
+
+	result := make([]*SiteInfo, 0, len(sc.sites))
+	for _, site := range sc.sites {
+		result = append(result, site)
+	}
+	return result
+}
+
+// extractDomainName extracts domain name from URL
+func extractDomainName(siteURL string) string {
+	parsed, err := url.Parse(siteURL)
+	if err != nil {
+		return siteURL
+	}
+	return parsed.Host
+}
+
+// extractTitle extracts title from HTML content
+func extractTitle(html string) string {
+	titleRegex := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
+	matches := titleRegex.FindStringSubmatch(html)
+	if len(matches) > 1 {
+		return strings.TrimSpace(matches[1])
+	}
+	return ""
+}
+
+// extractFavicon extracts favicon URL and data from HTML
+func (sc *SiteChecker) extractFavicon(ctx context.Context, siteURL, html string) (string, string) {
+	parsedURL, err := url.Parse(siteURL)
+	if err != nil {
+		return "", ""
+	}
+
+	// Look for favicon link in HTML
+	faviconRegex := regexp.MustCompile(`(?i)<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"']+)["']`)
+	matches := faviconRegex.FindStringSubmatch(html)
+
+	var faviconURL string
+	if len(matches) > 1 {
+		faviconURL = matches[1]
+	} else {
+		// Default favicon location
+		faviconURL = "/favicon.ico"
+	}
+
+	// Convert relative URL to absolute
+	if !strings.HasPrefix(faviconURL, "http") {
+		baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
+		if strings.HasPrefix(faviconURL, "/") {
+			faviconURL = baseURL + faviconURL
+		} else {
+			faviconURL = baseURL + "/" + faviconURL
+		}
+	}
+
+	// Download favicon
+	faviconData := sc.downloadFavicon(ctx, faviconURL)
+
+	return faviconURL, faviconData
+}
+
+// downloadFavicon downloads and encodes favicon as base64
+func (sc *SiteChecker) downloadFavicon(ctx context.Context, faviconURL string) string {
+	req, err := http.NewRequestWithContext(ctx, "GET", faviconURL, nil)
+	if err != nil {
+		return ""
+	}
+
+	req.Header.Set("User-Agent", sc.options.UserAgent)
+
+	resp, err := sc.client.Do(req)
+	if err != nil {
+		return ""
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return ""
+	}
+
+	// Limit favicon size to 1MB
+	body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
+	if err != nil {
+		return ""
+	}
+
+	// Get content type
+	contentType := resp.Header.Get("Content-Type")
+	if contentType == "" {
+		// Try to determine from URL extension
+		if strings.HasSuffix(faviconURL, ".png") {
+			contentType = "image/png"
+		} else if strings.HasSuffix(faviconURL, ".ico") {
+			contentType = "image/x-icon"
+		} else {
+			contentType = "image/x-icon" // default
+		}
+	}
+
+	// Encode as data URL
+	encoded := base64.StdEncoding.EncodeToString(body)
+	return fmt.Sprintf("data:%s;base64,%s", contentType, encoded)
+}
+
+// generateDisplayURL generates the URL to display in UI based on health check protocol
+func generateDisplayURL(originalURL, protocol string) string {
+	parsed, err := url.Parse(originalURL)
+	if err != nil {
+		logger.Debugf("Failed to parse URL %s: %v", originalURL, err)
+		return originalURL
+	}
+
+	logger.Debugf("Generating display URL for %s with protocol %s", originalURL, protocol)
+
+	// Determine the optimal scheme (prefer HTTPS if available)
+	scheme := determineOptimalScheme(parsed, protocol)
+	hostname := parsed.Hostname()
+	port := parsed.Port()
+
+	// For HTTP/HTTPS, return clean URL without default ports
+	if scheme == "http" || scheme == "https" {
+		// Build URL without default ports
+		var result string
+		if port == "" || (port == "80" && scheme == "http") || (port == "443" && scheme == "https") {
+			// No port or default port - don't show port
+			result = fmt.Sprintf("%s://%s", scheme, hostname)
+		} else {
+			// Non-default port - show port
+			result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
+		}
+		logger.Debugf("HTTP/HTTPS display URL: %s", result)
+		return result
+	}
+
+	// For gRPC/gRPCS, show the connection address format without default ports
+	if scheme == "grpc" || scheme == "grpcs" {
+		if port == "" {
+			// Determine default port based on scheme
+			if scheme == "grpcs" {
+				port = "443"
+			} else {
+				port = "80"
+			}
+		}
+
+		// Don't show default ports for gRPC either
+		var result string
+		if (port == "80" && scheme == "grpc") || (port == "443" && scheme == "grpcs") {
+			result = fmt.Sprintf("%s://%s", scheme, hostname)
+		} else {
+			result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
+		}
+		logger.Debugf("gRPC/gRPCS display URL: %s", result)
+		return result
+	}
+
+	// Fallback to original URL
+	logger.Debugf("Using fallback display URL: %s", originalURL)
+	return originalURL
+}
+
+// isGRPCProtocol checks if the protocol is gRPC-based
+func isGRPCProtocol(protocol string) bool {
+	return protocol == "grpc" || protocol == "grpcs"
+}
+
+// parseURLComponents extracts scheme and host:port from URL based on health check protocol
+func parseURLComponents(originalURL, healthCheckProtocol string) (scheme, hostPort string) {
+	parsed, err := url.Parse(originalURL)
+	if err != nil {
+		logger.Debugf("Failed to parse URL %s: %v", originalURL, err)
+		return healthCheckProtocol, originalURL
+	}
+
+	// Determine the best scheme to use
+	scheme = determineOptimalScheme(parsed, healthCheckProtocol)
+
+	// Extract hostname and port
+	hostname := parsed.Hostname()
+	if hostname == "" {
+		// Fallback to original URL if we can't parse hostname
+		return scheme, originalURL
+	}
+
+	port := parsed.Port()
+	if port == "" {
+		// Use default port based on scheme, but don't include it in hostPort for default ports
+		switch scheme {
+		case "https", "grpcs":
+			// Default HTTPS port 443 - don't show in hostPort
+			hostPort = hostname
+		case "http", "grpc":
+			// Default HTTP port 80 - don't show in hostPort
+			hostPort = hostname
+		default:
+			hostPort = hostname
+		}
+	} else {
+		// Non-default port specified
+		isDefaultPort := (port == "80" && (scheme == "http" || scheme == "grpc")) ||
+			(port == "443" && (scheme == "https" || scheme == "grpcs"))
+
+		if isDefaultPort {
+			// Don't show default ports
+			hostPort = hostname
+		} else {
+			// Show non-default ports
+			hostPort = hostname + ":" + port
+		}
+	}
+
+	return scheme, hostPort
+}
+
+// determineOptimalScheme determines the best scheme to use based on original URL and health check protocol
+func determineOptimalScheme(parsed *url.URL, healthCheckProtocol string) string {
+	// If health check protocol is specified, use it, but with special handling for HTTP/HTTPS
+	if healthCheckProtocol != "" {
+		// Special case: Don't downgrade HTTPS to HTTP
+		if healthCheckProtocol == "http" && parsed.Scheme == "https" {
+			// logger.Debugf("Preserving HTTPS scheme instead of downgrading to HTTP")
+			return "https"
+		}
+
+		// For gRPC protocols, always use the specified protocol
+		if healthCheckProtocol == "grpc" || healthCheckProtocol == "grpcs" {
+			return healthCheckProtocol
+		}
+
+		// For HTTPS health check protocol, always use HTTPS
+		if healthCheckProtocol == "https" {
+			return "https"
+		}
+
+		// For HTTP health check protocol, only use HTTP if original was also HTTP
+		if healthCheckProtocol == "http" && parsed.Scheme == "http" {
+			return "http"
+		}
+	}
+
+	// If no health check protocol, or if we need to fall back, prefer HTTPS if the original URL is HTTPS
+	if parsed.Scheme == "https" {
+		return "https"
+	}
+
+	// Default to HTTP
+	return "http"
+}

+ 452 - 0
internal/sitecheck/enhanced_checker.go

@@ -0,0 +1,452 @@
+package sitecheck
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"net/url"
+	"slices"
+	"strings"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/uozi-tech/cosy/logger"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/health/grpc_health_v1"
+)
+
+// EnhancedSiteChecker provides advanced health checking capabilities
+type EnhancedSiteChecker struct {
+	defaultClient *http.Client
+}
+
+// NewEnhancedSiteChecker creates a new enhanced site checker
+func NewEnhancedSiteChecker() *EnhancedSiteChecker {
+	transport := &http.Transport{
+		Dial: (&net.Dialer{
+			Timeout: 10 * time.Second,
+		}).Dial,
+		TLSHandshakeTimeout: 10 * time.Second,
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: true,
+		},
+	}
+
+	client := &http.Client{
+		Transport: transport,
+		Timeout:   30 * time.Second,
+	}
+
+	return &EnhancedSiteChecker{
+		defaultClient: client,
+	}
+}
+
+// CheckSiteWithConfig performs enhanced health check using custom configuration
+func (ec *EnhancedSiteChecker) CheckSiteWithConfig(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
+	if config == nil {
+		// Fallback to basic HTTP check
+		return ec.checkHTTP(ctx, siteURL, &model.HealthCheckConfig{
+			Protocol:       "http",
+			Method:         "GET",
+			Path:           "/",
+			ExpectedStatus: []int{200},
+		})
+	}
+
+	switch config.Protocol {
+	case "grpc", "grpcs":
+		return ec.checkGRPC(ctx, siteURL, config)
+	case "https":
+		return ec.checkHTTPS(ctx, siteURL, config)
+	default: // http
+		return ec.checkHTTP(ctx, siteURL, config)
+	}
+}
+
+// checkHTTP performs HTTP health check
+func (ec *EnhancedSiteChecker) checkHTTP(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
+	startTime := time.Now()
+
+	// Build request URL
+	checkURL := siteURL
+	if config.Path != "" && config.Path != "/" {
+		checkURL = strings.TrimRight(siteURL, "/") + "/" + strings.TrimLeft(config.Path, "/")
+	}
+
+	// Create request
+	req, err := http.NewRequestWithContext(ctx, config.Method, checkURL, nil)
+	if err != nil {
+		// Parse URL components for error case
+		scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
+
+		return &SiteInfo{
+			URL:                 siteURL,
+			Status:              StatusError,
+			Error:               fmt.Sprintf("Failed to create request: %v", err),
+			HealthCheckProtocol: config.Protocol,
+			Scheme:              scheme,
+			HostPort:            hostPort,
+		}, err
+	}
+
+	// Add custom headers
+	for key, value := range config.Headers {
+		req.Header.Set(key, value)
+	}
+
+	// Set User-Agent if not provided
+	if req.Header.Get("User-Agent") == "" {
+		req.Header.Set("User-Agent", "Nginx-UI Enhanced Checker/2.0")
+	}
+
+	// Add request body for POST/PUT methods
+	if config.Body != "" && (config.Method == "POST" || config.Method == "PUT") {
+		req.Body = io.NopCloser(strings.NewReader(config.Body))
+		if req.Header.Get("Content-Type") == "" {
+			req.Header.Set("Content-Type", "application/json")
+		}
+	}
+
+	// Create custom client if needed
+	client := ec.defaultClient
+	if config.ValidateSSL || config.VerifyHostname {
+		transport := &http.Transport{
+			Dial: (&net.Dialer{
+				Timeout: 10 * time.Second,
+			}).Dial,
+			TLSHandshakeTimeout: 10 * time.Second,
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: !config.ValidateSSL,
+			},
+		}
+
+		// Load client certificate if provided
+		if config.ClientCert != "" && config.ClientKey != "" {
+			cert, err := tls.LoadX509KeyPair(config.ClientCert, config.ClientKey)
+			if err != nil {
+				logger.Warnf("Failed to load client certificate: %v", err)
+			} else {
+				transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
+			}
+		}
+
+		client = &http.Client{
+			Transport: transport,
+			Timeout:   30 * time.Second,
+		}
+	}
+
+	// Make request
+	resp, err := client.Do(req)
+	if err != nil {
+		// Parse URL components for error case
+		scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
+
+		return &SiteInfo{
+			URL:                 siteURL,
+			Status:              StatusError,
+			ResponseTime:        time.Since(startTime).Milliseconds(),
+			Error:               err.Error(),
+			HealthCheckProtocol: config.Protocol,
+			Scheme:              scheme,
+			HostPort:            hostPort,
+		}, err
+	}
+	defer resp.Body.Close()
+
+	responseTime := time.Since(startTime).Milliseconds()
+
+	// Read response body
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		logger.Warnf("Failed to read response body: %v", err)
+		body = []byte{}
+	}
+
+	// Validate status code
+	statusValid := false
+	if len(config.ExpectedStatus) > 0 {
+		statusValid = slices.Contains(config.ExpectedStatus, resp.StatusCode)
+	} else {
+		statusValid = resp.StatusCode >= 200 && resp.StatusCode < 400
+	}
+
+	// Validate response text
+	bodyText := string(body)
+	textValid := true
+	if config.ExpectedText != "" {
+		textValid = strings.Contains(bodyText, config.ExpectedText)
+	}
+	if config.NotExpectedText != "" {
+		textValid = textValid && !strings.Contains(bodyText, config.NotExpectedText)
+	}
+
+	// Determine final status
+	status := StatusOffline
+	var errorMsg string
+	if statusValid && textValid {
+		status = StatusOnline
+	} else {
+		if !statusValid {
+			errorMsg = fmt.Sprintf("Unexpected status code: %d", resp.StatusCode)
+		} else {
+			errorMsg = "Response content validation failed"
+		}
+	}
+
+	// Parse URL components for legacy fields
+	_, hostPort := parseURLComponents(siteURL, config.Protocol)
+
+	// Get or create site config to get ID
+	siteConfig := getOrCreateSiteConfigForURL(siteURL)
+
+	return &SiteInfo{
+		ID:           siteConfig.ID,
+		Host:         siteConfig.Host,
+		Port:         siteConfig.Port,
+		Scheme:       siteConfig.Scheme,
+		DisplayURL:   siteConfig.GetURL(),
+		Status:       status,
+		StatusCode:   resp.StatusCode,
+		ResponseTime: responseTime,
+		Error:        errorMsg,
+		// Legacy fields for backward compatibility
+		URL:                 siteURL,
+		HealthCheckProtocol: config.Protocol,
+		HostPort:            hostPort,
+	}, nil
+}
+
+// checkHTTPS performs HTTPS health check with SSL validation
+func (ec *EnhancedSiteChecker) checkHTTPS(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
+	// Force HTTPS protocol
+	httpsConfig := *config
+	httpsConfig.Protocol = "https"
+	httpsConfig.ValidateSSL = true
+
+	return ec.checkHTTP(ctx, siteURL, &httpsConfig)
+}
+
+// checkGRPC performs gRPC health check
+func (ec *EnhancedSiteChecker) checkGRPC(ctx context.Context, siteURL string, config *model.HealthCheckConfig) (*SiteInfo, error) {
+	startTime := time.Now()
+
+	// Parse URL to get host and port
+	parsedURL, err := parseGRPCURL(siteURL)
+	if err != nil {
+		// Parse URL components for error case
+		scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
+
+		return &SiteInfo{
+			URL:                 siteURL,
+			Status:              StatusError,
+			Error:               fmt.Sprintf("Invalid gRPC URL: %v", err),
+			HealthCheckProtocol: config.Protocol,
+			Scheme:              scheme,
+			HostPort:            hostPort,
+		}, err
+	}
+
+	// Set up connection options
+	var opts []grpc.DialOption
+
+	// TLS configuration based on protocol setting, not URL scheme
+	if config.Protocol == "grpcs" || config.ValidateSSL {
+		tlsConfig := &tls.Config{
+			InsecureSkipVerify: !config.ValidateSSL,
+		}
+
+		// For GRPCS, default to skip verification unless explicitly enabled
+		if config.Protocol == "grpcs" && !config.ValidateSSL {
+			tlsConfig.InsecureSkipVerify = true
+		}
+
+		// Load client certificate if provided
+		if config.ClientCert != "" && config.ClientKey != "" {
+			cert, err := tls.LoadX509KeyPair(config.ClientCert, config.ClientKey)
+			if err != nil {
+				logger.Warnf("Failed to load client certificate: %v", err)
+			} else {
+				tlsConfig.Certificates = []tls.Certificate{cert}
+			}
+		}
+
+		opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
+	} else {
+		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	}
+
+	// Create connection with shorter timeout for faster failure detection
+	dialCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+	defer cancel()
+
+	conn, err := grpc.DialContext(dialCtx, parsedURL.Host, opts...)
+	if err != nil {
+		errorMsg := fmt.Sprintf("Failed to connect to gRPC server: %v", err)
+
+		// Provide more specific error messages
+		if strings.Contains(err.Error(), "connection refused") {
+			errorMsg = fmt.Sprintf("Connection refused - server may not be running on %s", parsedURL.Host)
+		} else if strings.Contains(err.Error(), "context deadline exceeded") {
+			errorMsg = fmt.Sprintf("Connection timeout - server at %s did not respond within 5 seconds", parsedURL.Host)
+		} else if strings.Contains(err.Error(), "EOF") {
+			errorMsg = fmt.Sprintf("Protocol mismatch - %s may not be a gRPC server or wrong TLS configuration", parsedURL.Host)
+		}
+
+		// Parse URL components for error case
+		scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
+
+		return &SiteInfo{
+			URL:                 siteURL,
+			Status:              StatusError,
+			ResponseTime:        time.Since(startTime).Milliseconds(),
+			Error:               errorMsg,
+			HealthCheckProtocol: config.Protocol,
+			Scheme:              scheme,
+			HostPort:            hostPort,
+		}, err
+	}
+	defer conn.Close()
+
+	// Use health check service
+	client := grpc_health_v1.NewHealthClient(conn)
+
+	// Determine service name
+	serviceName := ""
+	if config.GRPCService != "" {
+		serviceName = config.GRPCService
+	}
+
+	// Make health check request with shorter timeout
+	checkCtx, checkCancel := context.WithTimeout(ctx, 3*time.Second)
+	defer checkCancel()
+
+	resp, err := client.Check(checkCtx, &grpc_health_v1.HealthCheckRequest{
+		Service: serviceName,
+	})
+
+	responseTime := time.Since(startTime).Milliseconds()
+
+	if err != nil {
+		errorMsg := fmt.Sprintf("Health check failed: %v", err)
+
+		// Provide more specific error messages for gRPC health check failures
+		if strings.Contains(err.Error(), "Unimplemented") {
+			errorMsg = "Server does not implement gRPC health check service"
+		} else if strings.Contains(err.Error(), "context deadline exceeded") {
+			errorMsg = "Health check timeout - server did not respond within 3 seconds"
+		} else if strings.Contains(err.Error(), "EOF") {
+			errorMsg = "Connection lost during health check"
+		}
+
+		// Parse URL components for error case
+		scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
+
+		return &SiteInfo{
+			URL:                 siteURL,
+			Status:              StatusError,
+			ResponseTime:        responseTime,
+			Error:               errorMsg,
+			HealthCheckProtocol: config.Protocol,
+			Scheme:              scheme,
+			HostPort:            hostPort,
+		}, err
+	}
+
+	// Check response status
+	status := StatusOffline
+	if resp.Status == grpc_health_v1.HealthCheckResponse_SERVING {
+		status = StatusOnline
+	}
+
+	// Parse URL components
+	scheme, hostPort := parseURLComponents(siteURL, config.Protocol)
+
+	return &SiteInfo{
+		URL:                 siteURL,
+		Status:              status,
+		ResponseTime:        responseTime,
+		HealthCheckProtocol: config.Protocol,
+		Scheme:              scheme,
+		HostPort:            hostPort,
+	}, nil
+}
+
+// parseGRPCURL parses a URL and extracts host:port for gRPC connection
+func parseGRPCURL(rawURL string) (*url.URL, error) {
+	// Parse the original URL to extract host and port
+	parsedURL, err := url.Parse(rawURL)
+	if err != nil {
+		return nil, err
+	}
+
+	// Create a new URL structure for gRPC connection
+	grpcURL := &url.URL{
+		Scheme: "grpc", // Default to grpc, will be overridden by config.Protocol
+		Host:   parsedURL.Host,
+	}
+
+	// If no port is specified, use default ports based on original scheme
+	if parsedURL.Port() == "" {
+		switch parsedURL.Scheme {
+		case "https":
+			grpcURL.Host = parsedURL.Hostname() + ":443"
+		case "http":
+			grpcURL.Host = parsedURL.Hostname() + ":80"
+		case "grpcs":
+			grpcURL.Host = parsedURL.Hostname() + ":443"
+		case "grpc":
+			grpcURL.Host = parsedURL.Hostname() + ":80"
+		default:
+			// For URLs without scheme, default to port 80
+			grpcURL.Host = parsedURL.Host + ":80"
+		}
+	}
+
+	return grpcURL, nil
+}
+
+// LoadSiteConfig loads health check configuration for a site
+func LoadSiteConfig(siteURL string) (*model.SiteConfig, error) {
+	// Parse URL to get host:port
+	tempConfig := &model.SiteConfig{}
+	tempConfig.SetFromURL(siteURL)
+
+	sc := query.SiteConfig
+	config, err := sc.Where(sc.Host.Eq(tempConfig.Host)).First()
+	if err != nil {
+		// Return default config if not found
+		defaultConfig := &model.SiteConfig{
+			HealthCheckEnabled: true,
+			CheckInterval:      300,
+			Timeout:            10,
+			HealthCheckConfig: &model.HealthCheckConfig{
+				Protocol:       "http",
+				Method:         "GET",
+				Path:           "/",
+				ExpectedStatus: []int{200},
+			},
+		}
+		defaultConfig.SetFromURL(siteURL)
+		return defaultConfig, nil
+	}
+
+	// Set default health check config if nil
+	if config.HealthCheckConfig == nil {
+		config.HealthCheckConfig = &model.HealthCheckConfig{
+			Protocol:       "http",
+			Method:         "GET",
+			Path:           "/",
+			ExpectedStatus: []int{200},
+		}
+	}
+
+	return config, nil
+}

+ 95 - 0
internal/sitecheck/ordering.go

@@ -0,0 +1,95 @@
+package sitecheck
+
+import (
+	"sort"
+
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// applyCustomOrdering applies custom ordering from database to sites
+func applyCustomOrdering(sites []*SiteInfo) []*SiteInfo {
+	if len(sites) == 0 {
+		return sites
+	}
+
+	// Get custom ordering from database
+	sc := query.SiteConfig
+	configs, err := sc.Find()
+	if err != nil {
+		logger.Errorf("Failed to get site configs for ordering: %v", err)
+		// Fall back to default ordering
+		return applyDefaultOrdering(sites)
+	}
+
+	// Create a map of URL to custom order
+	orderMap := make(map[string]int)
+	for _, config := range configs {
+		orderMap[config.GetURL()] = config.CustomOrder
+	}
+
+	// Sort sites based on custom order, with fallback to default ordering
+	sort.Slice(sites, func(i, j int) bool {
+		orderI, hasOrderI := orderMap[sites[i].URL]
+		orderJ, hasOrderJ := orderMap[sites[j].URL]
+
+		// If both have custom order, use custom order
+		if hasOrderI && hasOrderJ {
+			return orderI < orderJ
+		}
+
+		// If only one has custom order, it comes first
+		if hasOrderI && !hasOrderJ {
+			return true
+		}
+		if !hasOrderI && hasOrderJ {
+			return false
+		}
+
+		// If neither has custom order, use default ordering
+		return defaultCompare(sites[i], sites[j])
+	})
+
+	return sites
+}
+
+// applyDefaultOrdering applies the default stable sorting
+func applyDefaultOrdering(sites []*SiteInfo) []*SiteInfo {
+	sort.Slice(sites, func(i, j int) bool {
+		return defaultCompare(sites[i], sites[j])
+	})
+	return sites
+}
+
+// defaultCompare implements the default site comparison logic
+func defaultCompare(a, b *SiteInfo) bool {
+	// Primary sort: by status (online > checking > error > offline)
+	statusPriority := map[string]int{
+		"online":   4,
+		"checking": 3,
+		"error":    2,
+		"offline":  1,
+	}
+
+	priorityA := statusPriority[a.Status]
+	priorityB := statusPriority[b.Status]
+
+	if priorityA != priorityB {
+		return priorityA > priorityB
+	}
+
+	// Secondary sort: by response time (faster first, for online sites)
+	if a.Status == "online" && b.Status == "online" {
+		if a.ResponseTime != b.ResponseTime {
+			return a.ResponseTime < b.ResponseTime
+		}
+	}
+
+	// Tertiary sort: by name (alphabetical, stable)
+	if a.Name != b.Name {
+		return a.Name < b.Name
+	}
+
+	// Final sort: by URL (for complete stability)
+	return a.URL < b.URL
+}

+ 155 - 0
internal/sitecheck/service.go

@@ -0,0 +1,155 @@
+package sitecheck
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// Service manages site checking operations
+type Service struct {
+	checker *SiteChecker
+	ctx     context.Context
+	cancel  context.CancelFunc
+	ticker  *time.Ticker
+	mu      sync.RWMutex
+	running bool
+}
+
+var (
+	globalService *Service
+	serviceOnce   sync.Once
+)
+
+// GetService returns the singleton service instance
+func GetService() *Service {
+	serviceOnce.Do(func() {
+		globalService = NewService(DefaultCheckOptions())
+	})
+	return globalService
+}
+
+// NewService creates a new site checking service
+func NewService(options CheckOptions) *Service {
+	return NewServiceWithContext(context.Background(), options)
+}
+
+// NewServiceWithContext creates a new site checking service with a parent context
+func NewServiceWithContext(parentCtx context.Context, options CheckOptions) *Service {
+	ctx, cancel := context.WithCancel(parentCtx)
+
+	return &Service{
+		checker: NewSiteChecker(options),
+		ctx:     ctx,
+		cancel:  cancel,
+	}
+}
+
+// SetUpdateCallback sets the callback function for site updates
+func (s *Service) SetUpdateCallback(callback func([]*SiteInfo)) {
+	s.checker.SetUpdateCallback(callback)
+}
+
+// Start begins the site checking service
+func (s *Service) Start() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	if s.running {
+		return
+	}
+
+	s.running = true
+	logger.Info("Starting site checking service")
+
+	// Initial collection and check with delay to allow cache scanner to complete
+	go func() {
+		// Wait a bit for cache scanner to collect sites
+		time.Sleep(2 * time.Second)
+		s.checker.CollectSites()
+		s.checker.CheckAllSites(s.ctx)
+	}()
+
+	// Start periodic checking (every 5 minutes)
+	s.ticker = time.NewTicker(5 * time.Minute)
+	go s.periodicCheck()
+}
+
+// Stop stops the site checking service
+func (s *Service) Stop() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	if !s.running {
+		return
+	}
+
+	s.running = false
+	logger.Info("Stopping site checking service")
+
+	if s.ticker != nil {
+		s.ticker.Stop()
+	}
+	s.cancel()
+}
+
+// Restart restarts the site checking service
+func (s *Service) Restart() {
+	s.Stop()
+	time.Sleep(100 * time.Millisecond) // Brief pause
+	s.Start()
+}
+
+// periodicCheck runs periodic site checks
+func (s *Service) periodicCheck() {
+	for {
+		select {
+		case <-s.ctx.Done():
+			return
+		case <-s.ticker.C:
+			logger.Debug("Starting periodic site check")
+			s.checker.CollectSites() // Re-collect in case sites changed
+			s.checker.CheckAllSites(s.ctx)
+		}
+	}
+}
+
+// RefreshSites manually triggers a site collection and check
+func (s *Service) RefreshSites() {
+	go func() {
+		logger.Info("Manually refreshing sites")
+		s.checker.CollectSites()
+		s.checker.CheckAllSites(s.ctx)
+	}()
+}
+
+// GetSites returns all checked sites with custom ordering applied
+func (s *Service) GetSites() []*SiteInfo {
+	sites := s.checker.GetSitesList()
+
+	// Apply custom ordering from database
+	return s.applySiteOrdering(sites)
+}
+
+// GetSiteByURL returns a specific site by URL
+func (s *Service) GetSiteByURL(url string) *SiteInfo {
+	sites := s.checker.GetSites()
+	if site, exists := sites[url]; exists {
+		return site
+	}
+	return nil
+}
+
+// IsRunning returns whether the service is currently running
+func (s *Service) IsRunning() bool {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	return s.running
+}
+
+// applySiteOrdering applies custom ordering from database to sites
+func (s *Service) applySiteOrdering(sites []*SiteInfo) []*SiteInfo {
+	return applyCustomOrdering(sites)
+}

+ 55 - 0
internal/sitecheck/types.go

@@ -0,0 +1,55 @@
+package sitecheck
+
+import (
+	"time"
+)
+
+// Site health check status constants
+const (
+	StatusOnline   = "online"
+	StatusOffline  = "offline"
+	StatusError    = "error"
+	StatusChecking = "checking"
+)
+
+// SiteInfo represents the information about a site
+type SiteInfo struct {
+	ID           uint64 `json:"id"`          // Site config ID for API operations
+	Host         string `json:"host"`        // host:port format
+	Port         int    `json:"port"`        // port number
+	Scheme       string `json:"scheme"`      // http, https, grpc, grpcs
+	DisplayURL   string `json:"display_url"` // computed URL for display
+	Name         string `json:"name"`
+	Status       string `json:"status"` // StatusOnline, StatusOffline, StatusError, StatusChecking
+	StatusCode   int    `json:"status_code"`
+	ResponseTime int64  `json:"response_time"` // in milliseconds
+	FaviconURL   string `json:"favicon_url"`
+	FaviconData  string `json:"favicon_data"` // base64 encoded favicon
+	Title        string `json:"title"`
+	LastChecked  int64  `json:"last_checked"` // Unix timestamp in seconds
+	Error        string `json:"error,omitempty"`
+	// Legacy fields for backward compatibility
+	URL                 string `json:"url,omitempty"`                   // deprecated, use display_url instead
+	HealthCheckProtocol string `json:"health_check_protocol,omitempty"` // deprecated, use scheme instead
+	HostPort            string `json:"host_port,omitempty"`             // deprecated, use host instead
+}
+
+// CheckOptions represents options for site checking
+type CheckOptions struct {
+	Timeout         time.Duration
+	UserAgent       string
+	FollowRedirects bool
+	MaxRedirects    int
+	CheckFavicon    bool
+}
+
+// DefaultCheckOptions returns default checking options
+func DefaultCheckOptions() CheckOptions {
+	return CheckOptions{
+		Timeout:         10 * time.Second,
+		UserAgent:       "Nginx-UI Site Checker/1.0",
+		FollowRedirects: true,
+		MaxRedirects:    3,
+		CheckFavicon:    true,
+	}
+}

+ 7 - 7
internal/stream/errors.go

@@ -3,11 +3,11 @@ package stream
 import "github.com/uozi-tech/cosy"
 
 var (
-	e                = cosy.NewErrorScope("stream")
-	ErrStreamNotFound  = e.New(40401, "stream not found")
-	ErrDstFileExists       = e.New(50001, "destination file already exists")
-	ErrStreamIsEnabled     = e.New(50002, "stream is enabled")
-	ErrNginxTestFailed     = e.New(50003, "nginx test failed: {0}")
-	ErrNginxReloadFailed   = e.New(50004, "nginx reload failed: {0}")
-	ErrReadDirFailed       = e.New(50005, "read dir failed: {0}")
+	e                    = cosy.NewErrorScope("stream")
+	ErrStreamNotFound    = e.New(40401, "stream not found")
+	ErrDstFileExists     = e.New(50001, "destination file already exists")
+	ErrStreamIsEnabled   = e.New(50002, "stream is enabled")
+	ErrNginxTestFailed   = e.New(50003, "nginx test failed: {0}")
+	ErrNginxReloadFailed = e.New(50004, "nginx reload failed: {0}")
+	ErrReadDirFailed     = e.New(50005, "read dir failed: {0}")
 )

+ 5 - 5
internal/stream/list.go

@@ -11,11 +11,11 @@ import (
 
 // ListOptions represents the options for listing streams
 type ListOptions struct {
-	Search     string
-	Name       string
-	Status     string
-	OrderBy    string
-	Sort       string
+	Search      string
+	Name        string
+	Status      string
+	OrderBy     string
+	Sort        string
 	NamespaceID uint64
 }
 

+ 1 - 1
model/chatgpt_log.go

@@ -3,8 +3,8 @@ package model
 import (
 	"database/sql/driver"
 	"encoding/json"
-	"fmt"
 	"errors"
+	"fmt"
 	"github.com/sashabaranov/go-openai"
 )
 

+ 1 - 0
model/model.go

@@ -35,6 +35,7 @@ func GenerateAllModel() []any {
 		Namespace{},
 		ExternalNotify{},
 		AutoBackup{},
+		SiteConfig{},
 	}
 }
 

+ 1 - 1
model/namespace.go

@@ -26,4 +26,4 @@ type Namespace struct {
 	OrderID          int      `json:"-" gorm:"default:0"`
 	PostSyncAction   string   `json:"post_sync_action" gorm:"default:'reload_nginx'"`
 	UpstreamTestType string   `json:"upstream_test_type" gorm:"default:'local'"`
-}
+}

+ 1 - 1
model/node.go

@@ -66,4 +66,4 @@ func (n *Node) GetWebSocketURL(uri string) (decodedUri string, err error) {
 	// http will be replaced with ws, https will be replaced with wss
 	decodedUri = strings.ReplaceAll(decodedUri, "http", "ws")
 	return
-}
+}

+ 110 - 0
model/site_config.go

@@ -0,0 +1,110 @@
+package model
+
+import (
+	"strconv"
+	"strings"
+)
+
+type HealthCheckConfig struct {
+	// Protocol settings
+	Protocol string            `json:"protocol"`                       // http, https, grpc
+	Method   string            `json:"method"`                         // GET, POST, PUT, etc.
+	Path     string            `json:"path"`                           // URL path to check
+	Headers  map[string]string `json:"headers" gorm:"serializer:json"` // Custom headers
+	Body     string            `json:"body"`                           // Request body for POST/PUT
+
+	// Response validation
+	ExpectedStatus  []int  `json:"expected_status" gorm:"serializer:json"` // Expected HTTP status codes
+	ExpectedText    string `json:"expected_text"`                          // Text that should be present in response
+	NotExpectedText string `json:"not_expected_text"`                      // Text that should NOT be present
+	ValidateSSL     bool   `json:"validate_ssl"`                           // Validate SSL certificate
+
+	// GRPC specific settings
+	GRPCService string `json:"grpc_service"` // GRPC service name
+	GRPCMethod  string `json:"grpc_method"`  // GRPC method name
+
+	// Advanced settings
+	DNSResolver    string `json:"dns_resolver"`    // Custom DNS resolver
+	SourceIP       string `json:"source_ip"`       // Source IP for requests
+	VerifyHostname bool   `json:"verify_hostname"` // Verify hostname in SSL cert
+	ClientCert     string `json:"client_cert"`     // Client certificate path
+	ClientKey      string `json:"client_key"`      // Client key path
+}
+
+type SiteConfig struct {
+	Model
+	Host               string             `gorm:"index" json:"host"`            // host:port format
+	Port               int                `gorm:"index" json:"port"`            // port number
+	Scheme             string             `gorm:"default:'http'" json:"scheme"` // http, https, grpc, grpcs
+	DisplayURL         string             `json:"display_url"`                  // computed URL for display
+	CustomOrder        int                `gorm:"default:0" json:"custom_order"`
+	HealthCheckEnabled bool               `gorm:"default:true" json:"health_check_enabled"`
+	CheckInterval      int                `gorm:"default:300" json:"check_interval"` // seconds
+	Timeout            int                `gorm:"default:10" json:"timeout"`         // seconds
+	UserAgent          string             `gorm:"default:'Nginx-UI Site Checker/1.0'" json:"user_agent"`
+	MaxRedirects       int                `gorm:"default:3" json:"max_redirects"`
+	FollowRedirects    bool               `gorm:"default:true" json:"follow_redirects"`
+	CheckFavicon       bool               `gorm:"default:true" json:"check_favicon"`
+	HealthCheckConfig  *HealthCheckConfig `gorm:"serializer:json" json:"health_check_config"`
+}
+
+// GetURL returns the computed URL for this site config
+func (sc *SiteConfig) GetURL() string {
+	if sc.DisplayURL != "" {
+		return sc.DisplayURL
+	}
+	return sc.Scheme + "://" + sc.Host
+}
+
+// SetFromURL parses a URL and sets the Host, Port, and Scheme fields
+func (sc *SiteConfig) SetFromURL(url string) error {
+	// Parse URL to extract host, port, and scheme
+	// This is a simplified implementation - you may want to use net/url package
+	if url == "" {
+		return nil
+	}
+
+	// Store the original URL as display URL for backward compatibility
+	sc.DisplayURL = url
+
+	// Extract scheme
+	if strings.HasPrefix(url, "https://") {
+		sc.Scheme = "https"
+		url = strings.TrimPrefix(url, "https://")
+	} else if strings.HasPrefix(url, "http://") {
+		sc.Scheme = "http"
+		url = strings.TrimPrefix(url, "http://")
+	} else if strings.HasPrefix(url, "grpcs://") {
+		sc.Scheme = "grpcs"
+		url = strings.TrimPrefix(url, "grpcs://")
+	} else if strings.HasPrefix(url, "grpc://") {
+		sc.Scheme = "grpc"
+		url = strings.TrimPrefix(url, "grpc://")
+	} else {
+		sc.Scheme = "http" // default
+	}
+
+	// Extract host and port
+	if strings.Contains(url, "/") {
+		url = strings.Split(url, "/")[0]
+	}
+
+	if strings.Contains(url, ":") {
+		parts := strings.Split(url, ":")
+		sc.Host = parts[0] + ":" + parts[1]
+		if len(parts) > 1 {
+			if port, err := strconv.Atoi(parts[1]); err == nil {
+				sc.Port = port
+			}
+		}
+	} else {
+		sc.Host = url + ":80" // default port
+		sc.Port = 80
+		if sc.Scheme == "https" || sc.Scheme == "grpcs" {
+			sc.Host = url + ":443"
+			sc.Port = 443
+		}
+	}
+
+	return nil
+}

+ 8 - 0
query/gen.go

@@ -32,6 +32,7 @@ var (
 	Notification   *notification
 	Passkey        *passkey
 	Site           *site
+	SiteConfig     *siteConfig
 	Stream         *stream
 	User           *user
 )
@@ -53,6 +54,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	Notification = &Q.Notification
 	Passkey = &Q.Passkey
 	Site = &Q.Site
+	SiteConfig = &Q.SiteConfig
 	Stream = &Q.Stream
 	User = &Q.User
 }
@@ -75,6 +77,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		Notification:   newNotification(db, opts...),
 		Passkey:        newPasskey(db, opts...),
 		Site:           newSite(db, opts...),
+		SiteConfig:     newSiteConfig(db, opts...),
 		Stream:         newStream(db, opts...),
 		User:           newUser(db, opts...),
 	}
@@ -98,6 +101,7 @@ type Query struct {
 	Notification   notification
 	Passkey        passkey
 	Site           site
+	SiteConfig     siteConfig
 	Stream         stream
 	User           user
 }
@@ -122,6 +126,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		Notification:   q.Notification.clone(db),
 		Passkey:        q.Passkey.clone(db),
 		Site:           q.Site.clone(db),
+		SiteConfig:     q.SiteConfig.clone(db),
 		Stream:         q.Stream.clone(db),
 		User:           q.User.clone(db),
 	}
@@ -153,6 +158,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		Notification:   q.Notification.replaceDB(db),
 		Passkey:        q.Passkey.replaceDB(db),
 		Site:           q.Site.replaceDB(db),
+		SiteConfig:     q.SiteConfig.replaceDB(db),
 		Stream:         q.Stream.replaceDB(db),
 		User:           q.User.replaceDB(db),
 	}
@@ -174,6 +180,7 @@ type queryCtx struct {
 	Notification   *notificationDo
 	Passkey        *passkeyDo
 	Site           *siteDo
+	SiteConfig     *siteConfigDo
 	Stream         *streamDo
 	User           *userDo
 }
@@ -195,6 +202,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		Notification:   q.Notification.WithContext(ctx),
 		Passkey:        q.Passkey.WithContext(ctx),
 		Site:           q.Site.WithContext(ctx),
+		SiteConfig:     q.SiteConfig.WithContext(ctx),
 		Stream:         q.Stream.WithContext(ctx),
 		User:           q.User.WithContext(ctx),
 	}

+ 474 - 0
query/site_configs.gen.go

@@ -0,0 +1,474 @@
+// 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 newSiteConfig(db *gorm.DB, opts ...gen.DOOption) siteConfig {
+	_siteConfig := siteConfig{}
+
+	_siteConfig.siteConfigDo.UseDB(db, opts...)
+	_siteConfig.siteConfigDo.UseModel(&model.SiteConfig{})
+
+	tableName := _siteConfig.siteConfigDo.TableName()
+	_siteConfig.ALL = field.NewAsterisk(tableName)
+	_siteConfig.ID = field.NewUint64(tableName, "id")
+	_siteConfig.CreatedAt = field.NewTime(tableName, "created_at")
+	_siteConfig.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_siteConfig.DeletedAt = field.NewField(tableName, "deleted_at")
+	_siteConfig.Host = field.NewString(tableName, "host")
+	_siteConfig.Port = field.NewInt(tableName, "port")
+	_siteConfig.Scheme = field.NewString(tableName, "scheme")
+	_siteConfig.DisplayURL = field.NewString(tableName, "display_url")
+	_siteConfig.CustomOrder = field.NewInt(tableName, "custom_order")
+	_siteConfig.HealthCheckEnabled = field.NewBool(tableName, "health_check_enabled")
+	_siteConfig.CheckInterval = field.NewInt(tableName, "check_interval")
+	_siteConfig.Timeout = field.NewInt(tableName, "timeout")
+	_siteConfig.UserAgent = field.NewString(tableName, "user_agent")
+	_siteConfig.MaxRedirects = field.NewInt(tableName, "max_redirects")
+	_siteConfig.FollowRedirects = field.NewBool(tableName, "follow_redirects")
+	_siteConfig.CheckFavicon = field.NewBool(tableName, "check_favicon")
+	_siteConfig.HealthCheckConfigProtocol = field.NewString(tableName, "hc_protocol")
+	_siteConfig.HealthCheckConfigMethod = field.NewString(tableName, "hc_method")
+	_siteConfig.HealthCheckConfigPath = field.NewString(tableName, "hc_path")
+	_siteConfig.HealthCheckConfigHeaders = field.NewField(tableName, "hc_headers")
+	_siteConfig.HealthCheckConfigBody = field.NewString(tableName, "hc_body")
+	_siteConfig.HealthCheckConfigExpectedStatus = field.NewField(tableName, "hc_expected_status")
+	_siteConfig.HealthCheckConfigExpectedText = field.NewString(tableName, "hc_expected_text")
+	_siteConfig.HealthCheckConfigNotExpectedText = field.NewString(tableName, "hc_not_expected_text")
+	_siteConfig.HealthCheckConfigValidateSSL = field.NewBool(tableName, "hc_validate_ssl")
+	_siteConfig.HealthCheckConfigGRPCService = field.NewString(tableName, "hc_g_rpc_service")
+	_siteConfig.HealthCheckConfigGRPCMethod = field.NewString(tableName, "hc_g_rpc_method")
+	_siteConfig.HealthCheckConfigDNSResolver = field.NewString(tableName, "hc_dns_resolver")
+	_siteConfig.HealthCheckConfigSourceIP = field.NewString(tableName, "hc_source_ip")
+	_siteConfig.HealthCheckConfigVerifyHostname = field.NewBool(tableName, "hc_verify_hostname")
+	_siteConfig.HealthCheckConfigClientCert = field.NewString(tableName, "hc_client_cert")
+	_siteConfig.HealthCheckConfigClientKey = field.NewString(tableName, "hc_client_key")
+
+	_siteConfig.fillFieldMap()
+
+	return _siteConfig
+}
+
+type siteConfig struct {
+	siteConfigDo
+
+	ALL                              field.Asterisk
+	ID                               field.Uint64
+	CreatedAt                        field.Time
+	UpdatedAt                        field.Time
+	DeletedAt                        field.Field
+	Host                             field.String
+	Port                             field.Int
+	Scheme                           field.String
+	DisplayURL                       field.String
+	CustomOrder                      field.Int
+	HealthCheckEnabled               field.Bool
+	CheckInterval                    field.Int
+	Timeout                          field.Int
+	UserAgent                        field.String
+	MaxRedirects                     field.Int
+	FollowRedirects                  field.Bool
+	CheckFavicon                     field.Bool
+	HealthCheckConfigProtocol        field.String
+	HealthCheckConfigMethod          field.String
+	HealthCheckConfigPath            field.String
+	HealthCheckConfigHeaders         field.Field
+	HealthCheckConfigBody            field.String
+	HealthCheckConfigExpectedStatus  field.Field
+	HealthCheckConfigExpectedText    field.String
+	HealthCheckConfigNotExpectedText field.String
+	HealthCheckConfigValidateSSL     field.Bool
+	HealthCheckConfigGRPCService     field.String
+	HealthCheckConfigGRPCMethod      field.String
+	HealthCheckConfigDNSResolver     field.String
+	HealthCheckConfigSourceIP        field.String
+	HealthCheckConfigVerifyHostname  field.Bool
+	HealthCheckConfigClientCert      field.String
+	HealthCheckConfigClientKey       field.String
+
+	fieldMap map[string]field.Expr
+}
+
+func (s siteConfig) Table(newTableName string) *siteConfig {
+	s.siteConfigDo.UseTable(newTableName)
+	return s.updateTableName(newTableName)
+}
+
+func (s siteConfig) As(alias string) *siteConfig {
+	s.siteConfigDo.DO = *(s.siteConfigDo.As(alias).(*gen.DO))
+	return s.updateTableName(alias)
+}
+
+func (s *siteConfig) updateTableName(table string) *siteConfig {
+	s.ALL = field.NewAsterisk(table)
+	s.ID = field.NewUint64(table, "id")
+	s.CreatedAt = field.NewTime(table, "created_at")
+	s.UpdatedAt = field.NewTime(table, "updated_at")
+	s.DeletedAt = field.NewField(table, "deleted_at")
+	s.Host = field.NewString(table, "host")
+	s.Port = field.NewInt(table, "port")
+	s.Scheme = field.NewString(table, "scheme")
+	s.DisplayURL = field.NewString(table, "display_url")
+	s.CustomOrder = field.NewInt(table, "custom_order")
+	s.HealthCheckEnabled = field.NewBool(table, "health_check_enabled")
+	s.CheckInterval = field.NewInt(table, "check_interval")
+	s.Timeout = field.NewInt(table, "timeout")
+	s.UserAgent = field.NewString(table, "user_agent")
+	s.MaxRedirects = field.NewInt(table, "max_redirects")
+	s.FollowRedirects = field.NewBool(table, "follow_redirects")
+	s.CheckFavicon = field.NewBool(table, "check_favicon")
+	s.HealthCheckConfigProtocol = field.NewString(table, "hc_protocol")
+	s.HealthCheckConfigMethod = field.NewString(table, "hc_method")
+	s.HealthCheckConfigPath = field.NewString(table, "hc_path")
+	s.HealthCheckConfigHeaders = field.NewField(table, "hc_headers")
+	s.HealthCheckConfigBody = field.NewString(table, "hc_body")
+	s.HealthCheckConfigExpectedStatus = field.NewField(table, "hc_expected_status")
+	s.HealthCheckConfigExpectedText = field.NewString(table, "hc_expected_text")
+	s.HealthCheckConfigNotExpectedText = field.NewString(table, "hc_not_expected_text")
+	s.HealthCheckConfigValidateSSL = field.NewBool(table, "hc_validate_ssl")
+	s.HealthCheckConfigGRPCService = field.NewString(table, "hc_g_rpc_service")
+	s.HealthCheckConfigGRPCMethod = field.NewString(table, "hc_g_rpc_method")
+	s.HealthCheckConfigDNSResolver = field.NewString(table, "hc_dns_resolver")
+	s.HealthCheckConfigSourceIP = field.NewString(table, "hc_source_ip")
+	s.HealthCheckConfigVerifyHostname = field.NewBool(table, "hc_verify_hostname")
+	s.HealthCheckConfigClientCert = field.NewString(table, "hc_client_cert")
+	s.HealthCheckConfigClientKey = field.NewString(table, "hc_client_key")
+
+	s.fillFieldMap()
+
+	return s
+}
+
+func (s *siteConfig) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := s.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (s *siteConfig) fillFieldMap() {
+	s.fieldMap = make(map[string]field.Expr, 32)
+	s.fieldMap["id"] = s.ID
+	s.fieldMap["created_at"] = s.CreatedAt
+	s.fieldMap["updated_at"] = s.UpdatedAt
+	s.fieldMap["deleted_at"] = s.DeletedAt
+	s.fieldMap["host"] = s.Host
+	s.fieldMap["port"] = s.Port
+	s.fieldMap["scheme"] = s.Scheme
+	s.fieldMap["display_url"] = s.DisplayURL
+	s.fieldMap["custom_order"] = s.CustomOrder
+	s.fieldMap["health_check_enabled"] = s.HealthCheckEnabled
+	s.fieldMap["check_interval"] = s.CheckInterval
+	s.fieldMap["timeout"] = s.Timeout
+	s.fieldMap["user_agent"] = s.UserAgent
+	s.fieldMap["max_redirects"] = s.MaxRedirects
+	s.fieldMap["follow_redirects"] = s.FollowRedirects
+	s.fieldMap["check_favicon"] = s.CheckFavicon
+	s.fieldMap["hc_protocol"] = s.HealthCheckConfigProtocol
+	s.fieldMap["hc_method"] = s.HealthCheckConfigMethod
+	s.fieldMap["hc_path"] = s.HealthCheckConfigPath
+	s.fieldMap["hc_headers"] = s.HealthCheckConfigHeaders
+	s.fieldMap["hc_body"] = s.HealthCheckConfigBody
+	s.fieldMap["hc_expected_status"] = s.HealthCheckConfigExpectedStatus
+	s.fieldMap["hc_expected_text"] = s.HealthCheckConfigExpectedText
+	s.fieldMap["hc_not_expected_text"] = s.HealthCheckConfigNotExpectedText
+	s.fieldMap["hc_validate_ssl"] = s.HealthCheckConfigValidateSSL
+	s.fieldMap["hc_g_rpc_service"] = s.HealthCheckConfigGRPCService
+	s.fieldMap["hc_g_rpc_method"] = s.HealthCheckConfigGRPCMethod
+	s.fieldMap["hc_dns_resolver"] = s.HealthCheckConfigDNSResolver
+	s.fieldMap["hc_source_ip"] = s.HealthCheckConfigSourceIP
+	s.fieldMap["hc_verify_hostname"] = s.HealthCheckConfigVerifyHostname
+	s.fieldMap["hc_client_cert"] = s.HealthCheckConfigClientCert
+	s.fieldMap["hc_client_key"] = s.HealthCheckConfigClientKey
+}
+
+func (s siteConfig) clone(db *gorm.DB) siteConfig {
+	s.siteConfigDo.ReplaceConnPool(db.Statement.ConnPool)
+	return s
+}
+
+func (s siteConfig) replaceDB(db *gorm.DB) siteConfig {
+	s.siteConfigDo.ReplaceDB(db)
+	return s
+}
+
+type siteConfigDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (s siteConfigDo) FirstByID(id uint64) (result *model.SiteConfig, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = s.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 (s siteConfigDo) DeleteByID(id uint64) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update site_configs set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = s.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (s siteConfigDo) Debug() *siteConfigDo {
+	return s.withDO(s.DO.Debug())
+}
+
+func (s siteConfigDo) WithContext(ctx context.Context) *siteConfigDo {
+	return s.withDO(s.DO.WithContext(ctx))
+}
+
+func (s siteConfigDo) ReadDB() *siteConfigDo {
+	return s.Clauses(dbresolver.Read)
+}
+
+func (s siteConfigDo) WriteDB() *siteConfigDo {
+	return s.Clauses(dbresolver.Write)
+}
+
+func (s siteConfigDo) Session(config *gorm.Session) *siteConfigDo {
+	return s.withDO(s.DO.Session(config))
+}
+
+func (s siteConfigDo) Clauses(conds ...clause.Expression) *siteConfigDo {
+	return s.withDO(s.DO.Clauses(conds...))
+}
+
+func (s siteConfigDo) Returning(value interface{}, columns ...string) *siteConfigDo {
+	return s.withDO(s.DO.Returning(value, columns...))
+}
+
+func (s siteConfigDo) Not(conds ...gen.Condition) *siteConfigDo {
+	return s.withDO(s.DO.Not(conds...))
+}
+
+func (s siteConfigDo) Or(conds ...gen.Condition) *siteConfigDo {
+	return s.withDO(s.DO.Or(conds...))
+}
+
+func (s siteConfigDo) Select(conds ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.Select(conds...))
+}
+
+func (s siteConfigDo) Where(conds ...gen.Condition) *siteConfigDo {
+	return s.withDO(s.DO.Where(conds...))
+}
+
+func (s siteConfigDo) Order(conds ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.Order(conds...))
+}
+
+func (s siteConfigDo) Distinct(cols ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.Distinct(cols...))
+}
+
+func (s siteConfigDo) Omit(cols ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.Omit(cols...))
+}
+
+func (s siteConfigDo) Join(table schema.Tabler, on ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.Join(table, on...))
+}
+
+func (s siteConfigDo) LeftJoin(table schema.Tabler, on ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.LeftJoin(table, on...))
+}
+
+func (s siteConfigDo) RightJoin(table schema.Tabler, on ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.RightJoin(table, on...))
+}
+
+func (s siteConfigDo) Group(cols ...field.Expr) *siteConfigDo {
+	return s.withDO(s.DO.Group(cols...))
+}
+
+func (s siteConfigDo) Having(conds ...gen.Condition) *siteConfigDo {
+	return s.withDO(s.DO.Having(conds...))
+}
+
+func (s siteConfigDo) Limit(limit int) *siteConfigDo {
+	return s.withDO(s.DO.Limit(limit))
+}
+
+func (s siteConfigDo) Offset(offset int) *siteConfigDo {
+	return s.withDO(s.DO.Offset(offset))
+}
+
+func (s siteConfigDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *siteConfigDo {
+	return s.withDO(s.DO.Scopes(funcs...))
+}
+
+func (s siteConfigDo) Unscoped() *siteConfigDo {
+	return s.withDO(s.DO.Unscoped())
+}
+
+func (s siteConfigDo) Create(values ...*model.SiteConfig) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return s.DO.Create(values)
+}
+
+func (s siteConfigDo) CreateInBatches(values []*model.SiteConfig, batchSize int) error {
+	return s.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 (s siteConfigDo) Save(values ...*model.SiteConfig) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return s.DO.Save(values)
+}
+
+func (s siteConfigDo) First() (*model.SiteConfig, error) {
+	if result, err := s.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.SiteConfig), nil
+	}
+}
+
+func (s siteConfigDo) Take() (*model.SiteConfig, error) {
+	if result, err := s.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.SiteConfig), nil
+	}
+}
+
+func (s siteConfigDo) Last() (*model.SiteConfig, error) {
+	if result, err := s.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.SiteConfig), nil
+	}
+}
+
+func (s siteConfigDo) Find() ([]*model.SiteConfig, error) {
+	result, err := s.DO.Find()
+	return result.([]*model.SiteConfig), err
+}
+
+func (s siteConfigDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.SiteConfig, err error) {
+	buf := make([]*model.SiteConfig, 0, batchSize)
+	err = s.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 (s siteConfigDo) FindInBatches(result *[]*model.SiteConfig, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return s.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (s siteConfigDo) Attrs(attrs ...field.AssignExpr) *siteConfigDo {
+	return s.withDO(s.DO.Attrs(attrs...))
+}
+
+func (s siteConfigDo) Assign(attrs ...field.AssignExpr) *siteConfigDo {
+	return s.withDO(s.DO.Assign(attrs...))
+}
+
+func (s siteConfigDo) Joins(fields ...field.RelationField) *siteConfigDo {
+	for _, _f := range fields {
+		s = *s.withDO(s.DO.Joins(_f))
+	}
+	return &s
+}
+
+func (s siteConfigDo) Preload(fields ...field.RelationField) *siteConfigDo {
+	for _, _f := range fields {
+		s = *s.withDO(s.DO.Preload(_f))
+	}
+	return &s
+}
+
+func (s siteConfigDo) FirstOrInit() (*model.SiteConfig, error) {
+	if result, err := s.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.SiteConfig), nil
+	}
+}
+
+func (s siteConfigDo) FirstOrCreate() (*model.SiteConfig, error) {
+	if result, err := s.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.SiteConfig), nil
+	}
+}
+
+func (s siteConfigDo) FindByPage(offset int, limit int) (result []*model.SiteConfig, count int64, err error) {
+	result, err = s.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 = s.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (s siteConfigDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = s.Count()
+	if err != nil {
+		return
+	}
+
+	err = s.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (s siteConfigDo) Scan(result interface{}) (err error) {
+	return s.DO.Scan(result)
+}
+
+func (s siteConfigDo) Delete(models ...*model.SiteConfig) (result gen.ResultInfo, err error) {
+	return s.DO.Delete(models)
+}
+
+func (s *siteConfigDo) withDO(do gen.Dao) *siteConfigDo {
+	s.DO = *do.(*gen.DO)
+	return s
+}