Browse Source

feat(env_group): migrate site_category to env_group

Jacky 2 months ago
parent
commit
a379211e3c
66 changed files with 3463 additions and 2736 deletions
  1. 48 0
      api/cluster/group.go
  2. 8 0
      api/cluster/router.go
  3. 0 48
      api/sites/category.go
  4. 17 17
      api/sites/list.go
  5. 0 10
      api/sites/router.go
  6. 6 6
      api/sites/site.go
  7. 1 0
      api/streams/router.go
  8. 159 13
      api/streams/streams.go
  9. 2 2
      app/package.json
  10. 223 282
      app/pnpm-lock.yaml
  11. 11 0
      app/src/api/env_group.ts
  12. 6 4
      app/src/api/site.ts
  13. 0 11
      app/src/api/site_category.ts
  14. 3 0
      app/src/api/stream.ts
  15. 34 34
      app/src/components/Notification/notifications.ts
  16. 168 150
      app/src/language/ar/app.po
  17. 166 149
      app/src/language/de_DE/app.po
  18. 166 149
      app/src/language/en/app.po
  19. 167 149
      app/src/language/es/app.po
  20. 166 149
      app/src/language/fr_FR/app.po
  21. 166 149
      app/src/language/ko_KR/app.po
  22. 166 152
      app/src/language/messages.pot
  23. 167 149
      app/src/language/ru_RU/app.po
  24. 166 149
      app/src/language/tr_TR/app.po
  25. 166 149
      app/src/language/vi_VN/app.po
  26. 171 156
      app/src/language/zh_CN/app.po
  27. 168 150
      app/src/language/zh_TW/app.po
  28. 19 1
      app/src/routes/modules/environments.ts
  29. 0 7
      app/src/routes/modules/sites.ts
  30. 4 5
      app/src/views/environments/group/EnvGroup.vue
  31. 0 0
      app/src/views/environments/group/columns.ts
  32. 0 0
      app/src/views/environments/list/BatchUpgrader.vue
  33. 2 2
      app/src/views/environments/list/Environment.vue
  34. 0 0
      app/src/views/environments/list/envColumns.tsx
  35. 7 7
      app/src/views/site/site_edit/RightSettings.vue
  36. 1 1
      app/src/views/site/site_edit/SiteEdit.vue
  37. 9 9
      app/src/views/site/site_list/SiteList.vue
  38. 7 7
      app/src/views/site/site_list/columns.tsx
  39. 1 0
      app/src/views/stream/StreamEdit.vue
  40. 70 2
      app/src/views/stream/StreamList.vue
  41. 35 1
      app/src/views/stream/components/RightSettings.vue
  42. 25 24
      go.mod
  43. 58 0
      go.sum
  44. 4 3
      internal/config/config.go
  45. 2 2
      internal/config/config_list.go
  46. 47 0
      internal/migrate/1.site_category_to_env_group.go
  47. 64 0
      internal/migrate/2.fix_site_and_stream_unique.go
  48. 44 0
      internal/migrate/3.rename_auths_to_users.go
  49. 14 0
      internal/migrate/migrate.go
  50. 0 5
      internal/nginx_log/nginx_log.go
  51. 4 4
      internal/site/save.go
  52. 3 3
      internal/site/sync.go
  53. 6 1
      internal/stream/sync.go
  54. 6 0
      main.go
  55. 2 1
      model/env_group.go
  56. 3 2
      model/model.go
  57. 5 5
      model/site.go
  58. 5 3
      model/stream.go
  59. 1 1
      model/user.go
  60. 374 0
      query/env_groups.gen.go
  61. 8 8
      query/gen.go
  62. 0 374
      query/site_categories.gen.go
  63. 28 28
      query/sites.gen.go
  64. 83 1
      query/streams.gen.go
  65. 1 1
      query/users.gen.go
  66. 0 1
      router/routers.go

+ 48 - 0
api/cluster/group.go

@@ -0,0 +1,48 @@
+package cluster
+
+import (
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+	"gorm.io/gorm"
+)
+
+func GetGroup(c *gin.Context) {
+	cosy.Core[model.EnvGroup](c).Get()
+}
+
+func GetGroupList(c *gin.Context) {
+	cosy.Core[model.EnvGroup](c).GormScope(func(tx *gorm.DB) *gorm.DB {
+		return tx.Order("order_id ASC")
+	}).PagingList()
+}
+
+func AddGroup(c *gin.Context) {
+	cosy.Core[model.EnvGroup](c).
+		SetValidRules(gin.H{
+			"name":          "required",
+			"sync_node_ids": "omitempty",
+		}).
+		Create()
+}
+
+func ModifyGroup(c *gin.Context) {
+	cosy.Core[model.EnvGroup](c).
+		SetValidRules(gin.H{
+			"name":          "required",
+			"sync_node_ids": "omitempty",
+		}).
+		Modify()
+}
+
+func DeleteGroup(c *gin.Context) {
+	cosy.Core[model.EnvGroup](c).Destroy()
+}
+
+func RecoverGroup(c *gin.Context) {
+	cosy.Core[model.EnvGroup](c).Recover()
+}
+
+func UpdateGroupsOrder(c *gin.Context) {
+	cosy.Core[model.EnvGroup](c).UpdateOrder()
+}

+ 8 - 0
api/cluster/router.go

@@ -16,4 +16,12 @@ func InitRouter(r *gin.RouterGroup) {
 	}
 	// Node
 	r.GET("node", GetCurrentNode)
+
+	r.GET("env_groups", GetGroupList)
+	r.GET("env_groups/:id", GetGroup)
+	r.POST("env_groups", AddGroup)
+	r.POST("env_groups/:id", ModifyGroup)
+	r.DELETE("env_groups/:id", DeleteGroup)
+	r.POST("env_groups/:id/recover", RecoverGroup)
+	r.POST("env_groups/order", UpdateGroupsOrder)
 }

+ 0 - 48
api/sites/category.go

@@ -1,48 +0,0 @@
-package sites
-
-import (
-	"github.com/0xJacky/Nginx-UI/model"
-	"github.com/gin-gonic/gin"
-	"github.com/uozi-tech/cosy"
-	"gorm.io/gorm"
-)
-
-func GetCategory(c *gin.Context) {
-	cosy.Core[model.SiteCategory](c).Get()
-}
-
-func GetCategoryList(c *gin.Context) {
-	cosy.Core[model.SiteCategory](c).GormScope(func(tx *gorm.DB) *gorm.DB {
-		return tx.Order("order_id ASC")
-	}).PagingList()
-}
-
-func AddCategory(c *gin.Context) {
-	cosy.Core[model.SiteCategory](c).
-		SetValidRules(gin.H{
-			"name":          "required",
-			"sync_node_ids": "omitempty",
-		}).
-		Create()
-}
-
-func ModifyCategory(c *gin.Context) {
-	cosy.Core[model.SiteCategory](c).
-		SetValidRules(gin.H{
-			"name":          "required",
-			"sync_node_ids": "omitempty",
-		}).
-		Modify()
-}
-
-func DeleteCategory(c *gin.Context) {
-	cosy.Core[model.SiteCategory](c).Destroy()
-}
-
-func RecoverCategory(c *gin.Context) {
-	cosy.Core[model.SiteCategory](c).Recover()
-}
-
-func UpdateCategoriesOrder(c *gin.Context) {
-	cosy.Core[model.SiteCategory](c).UpdateOrder()
-}

+ 17 - 17
api/sites/list.go

@@ -21,7 +21,7 @@ func GetSiteList(c *gin.Context) {
 	enabled := c.Query("enabled")
 	orderBy := c.Query("sort_by")
 	sort := c.DefaultQuery("order", "desc")
-	querySiteCategoryId := cast.ToUint64(c.Query("site_category_id"))
+	queryEnvGroupId := cast.ToUint64(c.Query("env_group_id"))
 
 	configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
 	if err != nil {
@@ -36,9 +36,9 @@ func GetSiteList(c *gin.Context) {
 	}
 
 	s := query.Site
-	sTx := s.Preload(s.SiteCategory)
-	if querySiteCategoryId != 0 {
-		sTx.Where(s.SiteCategoryID.Eq(querySiteCategoryId))
+	sTx := s.Preload(s.EnvGroup)
+	if queryEnvGroupId != 0 {
+		sTx.Where(s.EnvGroupID.Eq(queryEnvGroupId))
 	}
 	sites, err := sTx.Find()
 	if err != nil {
@@ -76,28 +76,28 @@ func GetSiteList(c *gin.Context) {
 			}
 		}
 		var (
-			siteCategoryId uint64
-			siteCategory   *model.SiteCategory
+			envGroupId uint64
+			envGroup   *model.EnvGroup
 		)
 
 		if site, ok := sitesMap[file.Name()]; ok {
-			siteCategoryId = site.SiteCategoryID
-			siteCategory = site.SiteCategory
+			envGroupId = site.EnvGroupID
+			envGroup = site.EnvGroup
 		}
 
-		// site category filter
-		if querySiteCategoryId != 0 && siteCategoryId != querySiteCategoryId {
+		// env group filter
+		if queryEnvGroupId != 0 && envGroupId != queryEnvGroupId {
 			continue
 		}
 
 		configs = append(configs, config.Config{
-			Name:           file.Name(),
-			ModifiedAt:     fileInfo.ModTime(),
-			Size:           fileInfo.Size(),
-			IsDir:          fileInfo.IsDir(),
-			Enabled:        enabledConfigMap[file.Name()],
-			SiteCategoryID: siteCategoryId,
-			SiteCategory:   siteCategory,
+			Name:       file.Name(),
+			ModifiedAt: fileInfo.ModTime(),
+			Size:       fileInfo.Size(),
+			IsDir:      fileInfo.IsDir(),
+			Enabled:    enabledConfigMap[file.Name()],
+			EnvGroupID: envGroupId,
+			EnvGroup:   envGroup,
 		})
 	}
 

+ 0 - 10
api/sites/router.go

@@ -23,13 +23,3 @@ func InitRouter(r *gin.RouterGroup) {
 	// duplicate site
 	r.POST("sites/:name/duplicate", DuplicateSite)
 }
-
-func InitCategoryRouter(r *gin.RouterGroup) {
-	r.GET("site_categories", GetCategoryList)
-	r.GET("site_categories/:id", GetCategory)
-	r.POST("site_categories", AddCategory)
-	r.POST("site_categories/:id", ModifyCategory)
-	r.DELETE("site_categories/:id", DeleteCategory)
-	r.POST("site_categories/:id/recover", RecoverCategory)
-	r.POST("site_categories/order", UpdateCategoriesOrder)
-}

+ 6 - 6
api/sites/site.go

@@ -114,17 +114,17 @@ func SaveSite(c *gin.Context) {
 	name := c.Param("name")
 
 	var json struct {
-		Content        string   `json:"content" binding:"required"`
-		SiteCategoryID uint64   `json:"site_category_id"`
-		SyncNodeIDs    []uint64 `json:"sync_node_ids"`
-		Overwrite      bool     `json:"overwrite"`
+		Content     string   `json:"content" binding:"required"`
+		EnvGroupID  uint64   `json:"env_group_id"`
+		SyncNodeIDs []uint64 `json:"sync_node_ids"`
+		Overwrite   bool     `json:"overwrite"`
 	}
 
 	if !cosy.BindAndValid(c, &json) {
 		return
 	}
 
-	err := site.Save(name, json.Content, json.Overwrite, json.SiteCategoryID, json.SyncNodeIDs)
+	err := site.Save(name, json.Content, json.Overwrite, json.EnvGroupID, json.SyncNodeIDs)
 	if err != nil {
 		cosy.ErrHandler(c, err)
 		return
@@ -191,7 +191,7 @@ func DeleteSite(c *gin.Context) {
 
 func BatchUpdateSites(c *gin.Context) {
 	cosy.Core[model.Site](c).SetValidRules(gin.H{
-		"site_category_id": "required",
+		"env_group_id": "required",
 	}).SetItemKey("path").
 		BeforeExecuteHook(func(ctx *cosy.Ctx[model.Site]) {
 			effectedPath := make([]string, len(ctx.BatchEffectedIDs))

+ 1 - 0
api/streams/router.go

@@ -5,6 +5,7 @@ import "github.com/gin-gonic/gin"
 func InitRouter(r *gin.RouterGroup) {
 	r.GET("streams", GetStreams)
 	r.GET("streams/:name", GetStream)
+	r.PUT("streams", BatchUpdateStreams)
 	r.POST("streams/:name", SaveStream)
 	r.POST("streams/:name/rename", RenameStream)
 	r.POST("streams/:name/enable", EnableStream)

+ 159 - 13
api/streams/streams.go

@@ -3,16 +3,21 @@ package streams
 import (
 	"net/http"
 	"os"
+	"path/filepath"
 	"strings"
 	"time"
 
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/internal/stream"
+	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
+	"github.com/samber/lo"
 	"github.com/sashabaranov/go-openai"
+	"github.com/spf13/cast"
 	"github.com/uozi-tech/cosy"
+	"gorm.io/gorm/clause"
 )
 
 type Stream struct {
@@ -24,13 +29,17 @@ type Stream struct {
 	ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
 	Tokenized       *nginx.NgxConfig               `json:"tokenized,omitempty"`
 	Filepath        string                         `json:"filepath"`
+	EnvGroupID      uint64                         `json:"env_group_id"`
+	EnvGroup        *model.EnvGroup                `json:"env_group,omitempty"`
 	SyncNodeIDs     []uint64                       `json:"sync_node_ids" gorm:"serializer:json"`
 }
 
 func GetStreams(c *gin.Context) {
 	name := c.Query("name")
+	enabled := c.Query("enabled")
 	orderBy := c.Query("order_by")
 	sort := c.DefaultQuery("sort", "desc")
+	queryEnvGroupId := cast.ToUint64(c.Query("env_group_id"))
 
 	configFiles, err := os.ReadDir(nginx.GetConfPath("streams-available"))
 	if err != nil {
@@ -51,23 +60,91 @@ func GetStreams(c *gin.Context) {
 
 	var configs []config.Config
 
+	// Get all streams map for environment group lookup
+	s := query.Stream
+	var streams []*model.Stream
+	if queryEnvGroupId != 0 {
+		streams, err = s.Where(s.EnvGroupID.Eq(queryEnvGroupId)).Find()
+	} else {
+		streams, err = s.Find()
+	}
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// Retrieve environment groups data
+	eg := query.EnvGroup
+	envGroups, err := eg.Find()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+	// Create a map of environment groups for quick lookup by ID
+	envGroupMap := lo.SliceToMap(envGroups, func(item *model.EnvGroup) (uint64, *model.EnvGroup) {
+		return item.ID, item
+	})
+
+	// Convert streams slice to map for efficient lookups
+	streamsMap := lo.SliceToMap(streams, func(item *model.Stream) (string, *model.Stream) {
+		// Associate each stream with its corresponding environment group
+		if item.EnvGroupID > 0 {
+			item.EnvGroup = envGroupMap[item.EnvGroupID]
+		}
+		return filepath.Base(item.Path), item
+	})
+
 	for i := range configFiles {
 		file := configFiles[i]
 		fileInfo, _ := file.Info()
-		if !file.IsDir() {
-			if name != "" && !strings.Contains(file.Name(), name) {
+		if file.IsDir() {
+			continue
+		}
+
+		// Apply name filter if specified
+		if name != "" && !strings.Contains(file.Name(), name) {
+			continue
+		}
+
+		// Apply enabled status filter if specified
+		if enabled != "" {
+			if enabled == "true" && !enabledConfigMap[file.Name()] {
 				continue
 			}
-			configs = append(configs, config.Config{
-				Name:       file.Name(),
-				ModifiedAt: fileInfo.ModTime(),
-				Size:       fileInfo.Size(),
-				IsDir:      fileInfo.IsDir(),
-				Enabled:    enabledConfigMap[file.Name()],
-			})
+			if enabled == "false" && enabledConfigMap[file.Name()] {
+				continue
+			}
+		}
+
+		var (
+			envGroupId uint64
+			envGroup   *model.EnvGroup
+		)
+
+		// Lookup stream in the streams map to get environment group info
+		if stream, ok := streamsMap[file.Name()]; ok {
+			envGroupId = stream.EnvGroupID
+			envGroup = stream.EnvGroup
+		}
+
+		// Apply environment group filter if specified
+		if queryEnvGroupId != 0 && envGroupId != queryEnvGroupId {
+			continue
 		}
+
+		// Add the config to the result list after passing all filters
+		configs = append(configs, config.Config{
+			Name:       file.Name(),
+			ModifiedAt: fileInfo.ModTime(),
+			Size:       fileInfo.Size(),
+			IsDir:      fileInfo.IsDir(),
+			Enabled:    enabledConfigMap[file.Name()],
+			EnvGroupID: envGroupId,
+			EnvGroup:   envGroup,
+		})
 	}
 
+	// Sort the configs based on the provided sort parameters
 	configs = config.Sort(orderBy, sort, configs)
 
 	c.JSON(http.StatusOK, gin.H{
@@ -78,6 +155,7 @@ func GetStreams(c *gin.Context) {
 func GetStream(c *gin.Context) {
 	name := c.Param("name")
 
+	// Get the absolute path to the stream configuration file
 	path := nginx.GetConfPath("streams-available", name)
 	file, err := os.Stat(path)
 	if os.IsNotExist(err) {
@@ -87,24 +165,26 @@ func GetStream(c *gin.Context) {
 		return
 	}
 
+	// Check if the stream is enabled
 	enabled := true
-
 	if _, err := os.Stat(nginx.GetConfPath("streams-enabled", name)); os.IsNotExist(err) {
 		enabled = false
 	}
 
+	// Retrieve or create ChatGPT log for this stream
 	g := query.ChatGPTLog
 	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
-
 	if err != nil {
 		cosy.ErrHandler(c, err)
 		return
 	}
 
+	// Initialize empty content if nil
 	if chatgpt.Content == nil {
 		chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
 	}
 
+	// Retrieve or create stream model from database
 	s := query.Stream
 	streamModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
 	if err != nil {
@@ -112,6 +192,7 @@ func GetStream(c *gin.Context) {
 		return
 	}
 
+	// For advanced mode, return the raw content
 	if streamModel.Advanced {
 		origContent, err := os.ReadFile(path)
 		if err != nil {
@@ -127,13 +208,15 @@ func GetStream(c *gin.Context) {
 			Config:          string(origContent),
 			ChatGPTMessages: chatgpt.Content,
 			Filepath:        path,
+			EnvGroupID:      streamModel.EnvGroupID,
+			EnvGroup:        streamModel.EnvGroup,
 			SyncNodeIDs:     streamModel.SyncNodeIDs,
 		})
 		return
 	}
 
+	// For normal mode, parse and tokenize the configuration
 	nginxConfig, err := nginx.ParseNgxConfig(path)
-
 	if err != nil {
 		cosy.ErrHandler(c, err)
 		return
@@ -148,6 +231,8 @@ func GetStream(c *gin.Context) {
 		Tokenized:       nginxConfig,
 		ChatGPTMessages: chatgpt.Content,
 		Filepath:        path,
+		EnvGroupID:      streamModel.EnvGroupID,
+		EnvGroup:        streamModel.EnvGroup,
 		SyncNodeIDs:     streamModel.SyncNodeIDs,
 	})
 }
@@ -157,24 +242,55 @@ func SaveStream(c *gin.Context) {
 
 	var json struct {
 		Content     string   `json:"content" binding:"required"`
+		EnvGroupID  uint64   `json:"env_group_id"`
 		SyncNodeIDs []uint64 `json:"sync_node_ids"`
 		Overwrite   bool     `json:"overwrite"`
 	}
 
+	// Validate input JSON
 	if !cosy.BindAndValid(c, &json) {
 		return
 	}
 
-	err := stream.Save(name, json.Content, json.Overwrite, json.SyncNodeIDs)
+	// Get stream from database or create if not exists
+	path := nginx.GetConfPath("streams-available", name)
+	s := query.Stream
+	streamModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// Update environment group ID if provided
+	if json.EnvGroupID > 0 {
+		streamModel.EnvGroupID = json.EnvGroupID
+	}
+
+	// Update synchronization node IDs if provided
+	if json.SyncNodeIDs != nil {
+		streamModel.SyncNodeIDs = json.SyncNodeIDs
+	}
+
+	// Save the updated stream model to database
+	_, err = s.Where(s.ID.Eq(streamModel.ID)).Updates(streamModel)
 	if err != nil {
 		cosy.ErrHandler(c, err)
 		return
 	}
 
+	// Save the stream configuration file
+	err = stream.Save(name, json.Content, json.Overwrite, json.SyncNodeIDs)
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// Return the updated stream
 	GetStream(c)
 }
 
 func EnableStream(c *gin.Context) {
+	// Enable the stream by creating a symlink in streams-enabled directory
 	err := stream.Enable(c.Param("name"))
 	if err != nil {
 		cosy.ErrHandler(c, err)
@@ -187,6 +303,7 @@ func EnableStream(c *gin.Context) {
 }
 
 func DisableStream(c *gin.Context) {
+	// Disable the stream by removing the symlink from streams-enabled directory
 	err := stream.Disable(c.Param("name"))
 	if err != nil {
 		cosy.ErrHandler(c, err)
@@ -199,6 +316,7 @@ func DisableStream(c *gin.Context) {
 }
 
 func DeleteStream(c *gin.Context) {
+	// Delete the stream configuration file and its symbolic link if exists
 	err := stream.Delete(c.Param("name"))
 	if err != nil {
 		cosy.ErrHandler(c, err)
@@ -215,10 +333,12 @@ func RenameStream(c *gin.Context) {
 	var json struct {
 		NewName string `json:"new_name"`
 	}
+	// Validate input JSON
 	if !cosy.BindAndValid(c, &json) {
 		return
 	}
 
+	// Rename the stream configuration file
 	err := stream.Rename(oldName, json.NewName)
 	if err != nil {
 		cosy.ErrHandler(c, err)
@@ -229,3 +349,29 @@ func RenameStream(c *gin.Context) {
 		"message": "ok",
 	})
 }
+
+func BatchUpdateStreams(c *gin.Context) {
+	cosy.Core[model.Stream](c).SetValidRules(gin.H{
+		"env_group_id": "required",
+	}).SetItemKey("path").
+		BeforeExecuteHook(func(ctx *cosy.Ctx[model.Stream]) {
+			effectedPath := make([]string, len(ctx.BatchEffectedIDs))
+			var streams []*model.Stream
+			for i, name := range ctx.BatchEffectedIDs {
+				path := nginx.GetConfPath("streams-available", name)
+				effectedPath[i] = path
+				streams = append(streams, &model.Stream{
+					Path: path,
+				})
+			}
+			s := query.Stream
+			err := s.Clauses(clause.OnConflict{
+				DoNothing: true,
+			}).Create(streams...)
+			if err != nil {
+				ctx.AbortWithError(err)
+				return
+			}
+			ctx.BatchEffectedIDs = effectedPath
+		}).BatchModify()
+}

+ 2 - 2
app/package.json

@@ -49,7 +49,7 @@
     "vue3-ace-editor": "2.2.4",
     "vue3-apexcharts": "1.5.3",
     "vue3-gettext": "3.0.0-beta.6",
-    "vue3-otp-input": "^0.5.21",
+    "vue3-otp-input": "^0.5.30",
     "vuedraggable": "^4.1.0"
   },
   "devDependencies": {
@@ -77,7 +77,7 @@
     "unplugin-auto-import": "^19.1.2",
     "unplugin-vue-components": "^28.4.1",
     "unplugin-vue-define-options": "^1.5.5",
-    "vite": "^6.2.4",
+    "vite": "^6.2.5",
     "vite-svg-loader": "^5.1.0",
     "vue-tsc": "^2.2.8"
   }

File diff suppressed because it is too large
+ 223 - 282
app/pnpm-lock.yaml


+ 11 - 0
app/src/api/env_group.ts

@@ -0,0 +1,11 @@
+import type { ModelBase } from '@/api/curd'
+import Curd from '@/api/curd'
+
+export interface EnvGroup extends ModelBase {
+  name: string
+  sync_node_ids: number[]
+}
+
+const env_group = new Curd<EnvGroup>('env_groups')
+
+export default env_group

+ 6 - 4
app/src/api/site.ts

@@ -1,13 +1,15 @@
 import type { CertificateInfo } from '@/api/cert'
+import type { ModelBase } from '@/api/curd'
+import type { EnvGroup } from '@/api/env_group'
 import type { NgxConfig } from '@/api/ngx'
 import type { ChatComplicationMessage } from '@/api/openai'
-import type { SiteCategory } from '@/api/site_category'
 import type { PrivateKeyType } from '@/constants'
 import Curd from '@/api/curd'
 import http from '@/lib/http'
 
-export interface Site {
+export interface Site extends ModelBase {
   modified_at: string
+  path: string
   advanced: boolean
   enabled: boolean
   name: string
@@ -17,8 +19,8 @@ export interface Site {
   chatgpt_messages: ChatComplicationMessage[]
   tokenized?: NgxConfig
   cert_info?: Record<number, CertificateInfo[]>
-  site_category_id: number
-  site_category?: SiteCategory
+  env_group_id: number
+  env_group?: EnvGroup
   sync_node_ids: number[]
 }
 

+ 0 - 11
app/src/api/site_category.ts

@@ -1,11 +0,0 @@
-import type { ModelBase } from '@/api/curd'
-import Curd from '@/api/curd'
-
-export interface SiteCategory extends ModelBase {
-  name: string
-  sync_node_ids: number[]
-}
-
-const site_category = new Curd<SiteCategory>('site_categories')
-
-export default site_category

+ 3 - 0
app/src/api/stream.ts

@@ -1,5 +1,6 @@
 import type { NgxConfig } from '@/api/ngx'
 import type { ChatComplicationMessage } from '@/api/openai'
+import type { EnvGroup } from './env_group'
 import Curd from '@/api/curd'
 import http from '@/lib/http'
 
@@ -12,6 +13,8 @@ export interface Stream {
   config: string
   chatgpt_messages: ChatComplicationMessage[]
   tokenized?: NgxConfig
+  env_group_id: number
+  env_group?: EnvGroup
   sync_node_ids: number[]
 }
 

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

@@ -4,6 +4,40 @@
 
 const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {
 
+  // user module notifications
+  'All Recovery Codes Have Been Used': {
+    title: () => $gettext('All Recovery Codes Have Been Used'),
+    content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
+  },
+
+  // cert module notifications
+  'Sync Certificate Error': {
+    title: () => $gettext('Sync Certificate Error'),
+    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args),
+  },
+  'Sync Certificate Success': {
+    title: () => $gettext('Sync Certificate Success'),
+    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args),
+  },
+
+  // config module notifications
+  'Sync Config Error': {
+    title: () => $gettext('Sync Config Error'),
+    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args),
+  },
+  'Sync Config Success': {
+    title: () => $gettext('Sync Config Success'),
+    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args),
+  },
+  '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),
+  },
+  '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),
+  },
+
   // site module notifications
   'Delete Remote Site Error': {
     title: () => $gettext('Delete Remote Site Error'),
@@ -87,40 +121,6 @@ const notifications: Record<string, { title: () => string, content: (args: any)
     title: () => $gettext('Save Remote Stream Success'),
     content: (args: any) => $gettext('Save stream %{name} to %{node} successfully', args),
   },
-
-  // user module notifications
-  'All Recovery Codes Have Been Used': {
-    title: () => $gettext('All Recovery Codes Have Been Used'),
-    content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
-  },
-
-  // cert module notifications
-  'Sync Certificate Error': {
-    title: () => $gettext('Sync Certificate Error'),
-    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} failed', args),
-  },
-  'Sync Certificate Success': {
-    title: () => $gettext('Sync Certificate Success'),
-    content: (args: any) => $gettext('Sync Certificate %{cert_name} to %{env_name} successfully', args),
-  },
-
-  // config module notifications
-  'Sync Config Error': {
-    title: () => $gettext('Sync Config Error'),
-    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} failed', args),
-  },
-  'Sync Config Success': {
-    title: () => $gettext('Sync Config Success'),
-    content: (args: any) => $gettext('Sync config %{config_name} to %{env_name} successfully', args),
-  },
-  '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),
-  },
-  '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),
-  },
 }
 
 export default notifications

File diff suppressed because it is too large
+ 168 - 150
app/src/language/ar/app.po


File diff suppressed because it is too large
+ 166 - 149
app/src/language/de_DE/app.po


File diff suppressed because it is too large
+ 166 - 149
app/src/language/en/app.po


File diff suppressed because it is too large
+ 167 - 149
app/src/language/es/app.po


File diff suppressed because it is too large
+ 166 - 149
app/src/language/fr_FR/app.po


File diff suppressed because it is too large
+ 166 - 149
app/src/language/ko_KR/app.po


File diff suppressed because it is too large
+ 166 - 152
app/src/language/messages.pot


File diff suppressed because it is too large
+ 167 - 149
app/src/language/ru_RU/app.po


File diff suppressed because it is too large
+ 166 - 149
app/src/language/tr_TR/app.po


File diff suppressed because it is too large
+ 166 - 149
app/src/language/vi_VN/app.po


File diff suppressed because it is too large
+ 171 - 156
app/src/language/zh_CN/app.po


File diff suppressed because it is too large
+ 168 - 150
app/src/language/zh_TW/app.po


+ 19 - 1
app/src/routes/modules/environments.ts

@@ -6,7 +6,7 @@ export const environmentsRoutes: RouteRecordRaw[] = [
   {
     path: 'environments',
     name: 'Environments',
-    component: () => import('@/views/environment/Environment.vue'),
+    component: () => import('@/layouts/BaseRouterView.vue'),
     meta: {
       name: () => $gettext('Environments'),
       icon: DatabaseOutlined,
@@ -16,5 +16,23 @@ export const environmentsRoutes: RouteRecordRaw[] = [
         return settings.is_remote
       },
     },
+    children: [
+      {
+        path: 'list',
+        name: 'env.list',
+        component: () => import('@/views/environments/list/Environment.vue'),
+        meta: {
+          name: () => $gettext('Nodes'),
+        },
+      },
+      {
+        path: 'groups',
+        name: 'env.groups',
+        component: () => import('@/views/environments/group/EnvGroup.vue'),
+        meta: {
+          name: () => $gettext('Groups'),
+        },
+      },
+    ],
   },
 ]

+ 0 - 7
app/src/routes/modules/sites.ts

@@ -26,13 +26,6 @@ export const sitesRoutes: RouteRecordRaw[] = [
         name: () => $gettext('Add Site'),
         lastRouteName: 'Sites List',
       },
-    }, {
-      path: 'categories',
-      name: 'Site Categories',
-      component: () => import('@/views/site/site_category/SiteCategory.vue'),
-      meta: {
-        name: () => $gettext('Site Categories'),
-      },
     }, {
       path: ':name',
       name: 'Edit Site',

+ 4 - 5
app/src/views/site/site_category/SiteCategory.vue → app/src/views/environments/group/EnvGroup.vue

@@ -1,14 +1,14 @@
 <script setup lang="ts">
-import site_category from '@/api/site_category'
+import env_group from '@/api/env_group'
 import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 import { StdCurd } from '@/components/StdDesign/StdDataDisplay'
-import columns from '@/views/site/site_category/columns'
+import columns from '@/views/environments/group/columns'
 </script>
 
 <template>
   <StdCurd
-    :title="$gettext('Site Categories')"
-    :api="site_category"
+    :title="$gettext('Environment Groups')"
+    :api="env_group"
     :columns="columns"
     :scroll-x="600"
     sortable
@@ -26,5 +26,4 @@ import columns from '@/views/site/site_category/columns'
 </template>
 
 <style scoped lang="less">
-
 </style>

+ 0 - 0
app/src/views/site/site_category/columns.ts → app/src/views/environments/group/columns.ts


+ 0 - 0
app/src/views/environment/BatchUpgrader.vue → app/src/views/environments/list/BatchUpgrader.vue


+ 2 - 2
app/src/views/environment/Environment.vue → app/src/views/environments/list/Environment.vue

@@ -2,9 +2,9 @@
 import environment from '@/api/environment'
 import FooterToolBar from '@/components/FooterToolbar'
 import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue'
-import BatchUpgrader from '@/views/environment/BatchUpgrader.vue'
-import envColumns from '@/views/environment/envColumns'
 import { message } from 'ant-design-vue'
+import BatchUpgrader from './BatchUpgrader.vue'
+import envColumns from './envColumns'
 
 const route = useRoute()
 const curd = ref()

+ 0 - 0
app/src/views/environment/envColumns.tsx → app/src/views/environments/list/envColumns.tsx


+ 7 - 7
app/src/views/site/site_edit/RightSettings.vue

@@ -3,14 +3,14 @@ import type { ChatComplicationMessage } from '@/api/openai'
 import type { Site } from '@/api/site'
 import type { CheckedType } from '@/types'
 import type { Ref } from 'vue'
+import envGroup from '@/api/env_group'
 import site from '@/api/site'
-import site_category from '@/api/site_category'
 import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
 import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
 import { formatDateTime } from '@/lib/helper'
 import { useSettingsStore } from '@/pinia'
-import siteCategoryColumns from '@/views/site/site_category/columns'
+import envGroupColumns from '@/views/environments/group/columns'
 import ConfigName from '@/views/site/site_edit/components/ConfigName.vue'
 import { InfoCircleOutlined } from '@ant-design/icons-vue'
 import { message, Modal } from 'ant-design-vue'
@@ -88,11 +88,11 @@ function onChangeEnabled(checked: CheckedType) {
           <AFormItem :label="$gettext('Name')">
             <ConfigName v-if="name" :name />
           </AFormItem>
-          <AFormItem :label="$gettext('Category')">
+          <AFormItem :label="$gettext('Environment Group')">
             <StdSelector
-              v-model:selected-key="data.site_category_id"
-              :api="site_category"
-              :columns="siteCategoryColumns"
+              v-model:selected-key="data.env_group_id"
+              :api="envGroup"
+              :columns="envGroupColumns"
               record-value-index="name"
               selection-type="radio"
             />
@@ -114,7 +114,7 @@ function onChangeEnabled(checked: CheckedType) {
             <template #content>
               <div class="max-w-200px mb-2">
                 {{ $gettext('When you enable/disable, delete, or save this site, '
-                  + 'the nodes set in the site category and the nodes selected below will be synchronized.') }}
+                  + 'the nodes set in the environment group and the nodes selected below will be synchronized.') }}
               </div>
               <div class="max-w-200px">
                 {{ $gettext('Note, if the configuration file include other configurations or certificates, '

+ 1 - 1
app/src/views/site/site_edit/SiteEdit.vue

@@ -141,7 +141,7 @@ async function save() {
   return site.save(name.value, {
     content: configText.value,
     overwrite: true,
-    site_category_id: data.value.site_category_id,
+    env_group_id: data.value.env_group_id,
     sync_node_ids: data.value.sync_node_ids,
   }).then(r => {
     handleResponse(r)

+ 9 - 9
app/src/views/site/site_list/SiteList.vue

@@ -1,9 +1,9 @@
 <script setup lang="tsx">
+import type { EnvGroup } from '@/api/env_group'
 import type { Site } from '@/api/site'
-import type { SiteCategory } from '@/api/site_category'
 import type { Column } from '@/components/StdDesign/types'
+import env_group from '@/api/env_group'
 import site from '@/api/site'
-import site_category from '@/api/site_category'
 import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import InspectConfig from '@/views/config/InspectConfig.vue'
@@ -17,8 +17,8 @@ const router = useRouter()
 const table = ref()
 const inspect_config = ref()
 
-const siteCategoryId = ref(Number.parseInt(route.query.site_category_id as string) || 0)
-const siteCategories = ref([]) as Ref<SiteCategory[]>
+const envGroupId = ref(Number.parseInt(route.query.env_group_id as string) || 0)
+const envGroups = ref([]) as Ref<EnvGroup[]>
 
 watch(route, () => {
   inspect_config.value?.test()
@@ -27,10 +27,10 @@ watch(route, () => {
 onMounted(async () => {
   while (true) {
     try {
-      const { data, pagination } = await site_category.get_list()
+      const { data, pagination } = await env_group.get_list()
       if (!data || !pagination)
         return
-      siteCategories.value.push(...data)
+      envGroups.value.push(...data)
       if (data.length < pagination?.per_page) {
         return
       }
@@ -94,9 +94,9 @@ function handleBatchUpdated() {
   <ACard :title="$gettext('Manage Sites')">
     <InspectConfig ref="inspect_config" />
 
-    <ATabs v-model:active-key="siteCategoryId">
+    <ATabs v-model:active-key="envGroupId">
       <ATabPane :key="0" :tab="$gettext('All')" />
-      <ATabPane v-for="c in siteCategories" :key="c.id" :tab="c.name" />
+      <ATabPane v-for="c in envGroups" :key="c.id" :tab="c.name" />
     </ATabs>
 
     <StdTable
@@ -107,7 +107,7 @@ function handleBatchUpdated() {
       disable-delete
       disable-view
       :get-params="{
-        site_category_id: siteCategoryId,
+        env_group_id: envGroupId,
       }"
       :scroll-x="1200"
       @click-edit="(r: string) => router.push({

+ 7 - 7
app/src/views/site/site_list/columns.tsx

@@ -2,13 +2,13 @@ import type {
   CustomRender,
 } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import type { Column, JSXElements } from '@/components/StdDesign/types'
-import site_category from '@/api/site_category'
+import env_group from '@/api/env_group'
 import {
   actualValueRender,
   datetime,
 } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { input, select, selector } from '@/components/StdDesign/StdDataEntry'
-import siteCategoryColumns from '@/views/site/site_category/columns'
+import envGroupColumns from '@/views/environments/group/columns'
 import { Badge } from 'ant-design-vue'
 
 const columns: Column[] = [{
@@ -22,14 +22,14 @@ const columns: Column[] = [{
   search: true,
   width: 120,
 }, {
-  title: () => $gettext('Category'),
-  dataIndex: 'site_category_id',
-  customRender: actualValueRender('site_category.name'),
+  title: () => $gettext('Environment Group'),
+  dataIndex: 'env_group_id',
+  customRender: actualValueRender('env_group.name'),
   edit: {
     type: selector,
     selector: {
-      api: site_category,
-      columns: siteCategoryColumns,
+      api: env_group,
+      columns: envGroupColumns,
       recordValueIndex: 'name',
       selectionType: 'radio',
     },

+ 1 - 0
app/src/views/stream/StreamEdit.vue

@@ -129,6 +129,7 @@ async function save() {
     name: filename.value || name.value,
     content: configText.value,
     overwrite: true,
+    env_group_id: data.value?.env_group_id,
     sync_node_ids: data.value?.sync_node_ids,
   }).then(r => {
     handleResponse(r)

+ 70 - 2
app/src/views/stream/StreamList.vue

@@ -1,11 +1,16 @@
 <script setup lang="tsx">
+import type { EnvGroup } from '@/api/env_group'
+import type { Stream } from '@/api/stream'
 import type { CustomRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import type { Column, JSXElements } from '@/components/StdDesign/types'
+import env_group from '@/api/env_group'
 import stream from '@/api/stream'
+import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
-import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
-import { input } from '@/components/StdDesign/StdDataEntry'
+import { actualValueRender, datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+import { input, selector } from '@/components/StdDesign/StdDataEntry'
 import InspectConfig from '@/views/config/InspectConfig.vue'
+import envGroupColumns from '@/views/environments/group/columns'
 import StreamDuplicate from '@/views/stream/components/StreamDuplicate.vue'
 import { Badge, message } from 'ant-design-vue'
 
@@ -18,6 +23,23 @@ const columns: Column[] = [{
     type: input,
   },
   search: true,
+}, {
+  title: () => $gettext('Environment Group'),
+  dataIndex: 'env_group_id',
+  customRender: actualValueRender('env_group.name'),
+  edit: {
+    type: selector,
+    selector: {
+      api: env_group,
+      columns: envGroupColumns,
+      recordValueIndex: 'name',
+      selectionType: 'radio',
+    },
+  },
+  sorter: true,
+  pithy: true,
+  batch: true,
+  width: 150,
 }, {
   title: () => $gettext('Status'),
   dataIndex: 'enabled',
@@ -95,6 +117,26 @@ function handle_click_duplicate(name: string) {
 
 const route = useRoute()
 
+const envGroupId = ref(Number.parseInt(route.query.env_group_id as string) || 0)
+const envGroups = ref([]) as Ref<EnvGroup[]>
+
+onMounted(async () => {
+  while (true) {
+    try {
+      const { data, pagination } = await env_group.get_list()
+      if (!data || !pagination)
+        return
+      envGroups.value.push(...data)
+      if (data.length < pagination?.per_page) {
+        return
+      }
+    }
+    catch {
+      return
+    }
+  }
+})
+
 watch(route, () => {
   inspect_config.value?.test()
 })
@@ -113,6 +155,17 @@ function handleAddStream() {
     message.success($gettext('Added successfully'))
   })
 }
+
+const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
+
+async function handleClickBatchEdit(batchColumns: Column[], selectedRowKeys: string[], selectedRows: Stream[]) {
+  stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys, selectedRows)
+}
+
+function handleBatchUpdated() {
+  table.value?.get_list()
+  table.value?.resetSelection()
+}
 </script>
 
 <template>
@@ -123,6 +176,11 @@ function handleAddStream() {
 
     <InspectConfig ref="inspect_config" />
 
+    <ATabs v-model:active-key="envGroupId">
+      <ATabPane :key="0" :tab="$gettext('All')" />
+      <ATabPane v-for="c in envGroups" :key="c.id" :tab="c.name" />
+    </ATabs>
+
     <StdTable
       ref="table"
       :api="stream"
@@ -131,9 +189,13 @@ function handleAddStream() {
       disable-delete
       disable-view
       :scroll-x="800"
+      :get-params="{
+        env_group_id: envGroupId,
+      }"
       @click-edit="r => $router.push({
         path: `/streams/${r}`,
       })"
+      @click-batch-modify="handleClickBatchEdit"
     >
       <template #actions="{ record }">
         <AButton
@@ -193,6 +255,12 @@ function handleAddStream() {
       :name="target"
       @duplicated="() => table.get_list()"
     />
+    <StdBatchEdit
+      ref="stdBatchEditRef"
+      :api="stream"
+      :columns="columns"
+      @save="handleBatchUpdated"
+    />
   </ACard>
 </template>
 

+ 35 - 1
app/src/views/stream/components/RightSettings.vue

@@ -3,11 +3,15 @@ import type { ChatComplicationMessage } from '@/api/openai'
 import type { Stream } from '@/api/stream'
 import type { CheckedType } from '@/types'
 import type { Ref } from 'vue'
+import envGroup from '@/api/env_group'
 import stream from '@/api/stream'
 import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
 import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
 import { formatDateTime } from '@/lib/helper'
 import { useSettingsStore } from '@/pinia'
+import envGroupColumns from '@/views/environments/group/columns'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
 import { message, Modal } from 'ant-design-vue'
 import ConfigName from './ConfigName.vue'
 
@@ -68,6 +72,7 @@ function onChangeEnabled(checked: CheckedType) {
     <ACollapse
       v-model:active-key="active_key"
       ghost
+      collapsible="header"
     >
       <ACollapsePanel
         key="1"
@@ -82,6 +87,15 @@ function onChangeEnabled(checked: CheckedType) {
         <AFormItem :label="$gettext('Name')">
           <ConfigName :name="name" />
         </AFormItem>
+        <AFormItem :label="$gettext('Environment Group')">
+          <StdSelector
+            v-model:selected-key="data.env_group_id"
+            :api="envGroup"
+            :columns="envGroupColumns"
+            record-value-index="name"
+            selection-type="radio"
+          />
+        </AFormItem>
         <AFormItem :label="$gettext('Updated at')">
           {{ formatDateTime(data.modified_at) }}
         </AFormItem>
@@ -89,8 +103,28 @@ function onChangeEnabled(checked: CheckedType) {
       <ACollapsePanel
         v-if="!settings.is_remote"
         key="2"
-        :header="$gettext('Sync')"
       >
+        <template #header>
+          {{ $gettext('Synchronization') }}
+        </template>
+        <template #extra>
+          <APopover placement="bottomRight" :title="$gettext('Sync strategy')">
+            <template #content>
+              <div class="max-w-200px mb-2">
+                {{ $gettext('When you enable/disable, delete, or save this stream, '
+                  + 'the nodes set in the environment group and the nodes selected below will be synchronized.') }}
+              </div>
+              <div class="max-w-200px">
+                {{ $gettext('Note, if the configuration file include other configurations or certificates, '
+                  + 'please synchronize them to the remote nodes in advance.') }}
+              </div>
+            </template>
+            <div class="text-trueGray-600">
+              <InfoCircleOutlined class="mr-1" />
+              {{ $gettext('Sync strategy') }}
+            </div>
+          </APopover>
+        </template>
         <NodeSelector
           v-model:target="data.sync_node_ids"
           class="mb-4"

+ 25 - 24
go.mod

@@ -8,15 +8,16 @@ require (
 	github.com/caarlos0/env/v11 v11.3.1
 	github.com/casdoor/casdoor-go-sdk v1.5.0
 	github.com/creack/pty v1.1.24
-	github.com/dgraph-io/ristretto/v2 v2.1.0
+	github.com/dgraph-io/ristretto/v2 v2.2.0
 	github.com/dustin/go-humanize v1.0.1
 	github.com/elliotchance/orderedmap/v3 v3.1.0
-	github.com/fsnotify/fsnotify v1.8.0
+	github.com/fsnotify/fsnotify v1.9.0
 	github.com/gin-contrib/pprof v1.5.2
 	github.com/gin-contrib/static v1.1.3
 	github.com/gin-gonic/gin v1.10.0
 	github.com/go-acme/lego/v4 v4.22.2
 	github.com/go-co-op/gocron/v2 v2.16.1
+	github.com/go-gormigrate/gormigrate/v2 v2.1.4
 	github.com/go-playground/validator/v10 v10.26.0
 	github.com/go-resty/resty/v2 v2.16.5
 	github.com/go-webauthn/webauthn v0.12.3
@@ -36,7 +37,7 @@ require (
 	github.com/spf13/cast v1.7.1
 	github.com/stretchr/testify v1.10.0
 	github.com/tufanbarisyildirim/gonginx v0.0.0-20250225174229-c03497ddaef6
-	github.com/uozi-tech/cosy v1.18.0
+	github.com/uozi-tech/cosy v1.19.0
 	github.com/uozi-tech/cosy-driver-sqlite v0.2.1
 	github.com/urfave/cli/v3 v3.1.1
 	golang.org/x/crypto v0.36.0
@@ -56,9 +57,9 @@ require (
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
 	github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
@@ -77,19 +78,19 @@ require (
 	github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
 	github.com/aliyun/alibaba-cloud-sdk-go v1.63.103 // indirect
 	github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
-	github.com/aws/aws-sdk-go-v2/config v1.29.12 // indirect
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.65 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.29.13 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.66 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
-	github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
+	github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/route53 v1.51.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.18 // indirect
 	github.com/aws/smithy-go v1.22.3 // indirect
 	github.com/benbjohnson/clock v1.3.5 // indirect
 	github.com/boombuler/barcode v1.0.2 // indirect
@@ -115,7 +116,6 @@ require (
 	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/gin-contrib/sse v1.0.0 // indirect
 	github.com/go-errors/errors v1.5.1 // indirect
-	github.com/go-gormigrate/gormigrate/v2 v2.1.4 // indirect
 	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
@@ -143,7 +143,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
 	github.com/hashicorp/go-uuid v1.0.3 // indirect
-	github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.142 // indirect
+	github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.143 // indirect
 	github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
 	github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
 	github.com/itchyny/timefmt-go v0.1.6 // indirect
@@ -172,7 +172,7 @@ require (
 	github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-sqlite3 v1.14.24 // indirect
+	github.com/mattn/go-sqlite3 v1.14.27 // indirect
 	github.com/miekg/dns v1.1.64 // indirect
 	github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -193,7 +193,7 @@ require (
 	github.com/nrdcg/porkbun v0.4.0 // indirect
 	github.com/nzdjb/go-metaname v1.0.0 // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
-	github.com/oracle/oci-go-sdk/v65 v65.88.0 // indirect
+	github.com/oracle/oci-go-sdk/v65 v65.88.1 // indirect
 	github.com/ovh/go-ovh v1.7.0 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
@@ -225,8 +225,8 @@ require (
 	github.com/spf13/viper v1.20.1 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1134 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1134 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1138 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
 	github.com/tjfoc/gmsm v1.4.1 // indirect
 	github.com/tklauser/go-sysconf v0.3.15 // indirect
 	github.com/tklauser/numcpus v0.10.0 // indirect
@@ -237,11 +237,12 @@ require (
 	github.com/uozi-tech/cosy-driver-mysql v0.2.2 // indirect
 	github.com/uozi-tech/cosy-driver-postgres v0.2.1 // indirect
 	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
-	github.com/volcengine/volc-sdk-golang v1.0.201 // indirect
+	github.com/volcengine/volc-sdk-golang v1.0.202 // indirect
 	github.com/vultr/govultr/v3 v3.18.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/yandex-cloud/go-genproto v0.0.0-20250325081613-cd85d9003939 // indirect
 	github.com/yandex-cloud/go-sdk v0.0.0-20250325134853-dcb34ef70818 // indirect
+	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	go.mongodb.org/mongo-driver v1.17.3 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@@ -262,14 +263,14 @@ require (
 	golang.org/x/time v0.11.0 // indirect
 	golang.org/x/tools v0.31.0 // indirect
 	google.golang.org/api v0.228.0 // indirect
-	google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
-	google.golang.org/grpc v1.71.0 // indirect
+	google.golang.org/genproto v0.0.0-20250404141209-ee84b53bf3d0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect
+	google.golang.org/grpc v1.71.1 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
-	gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
+	gopkg.in/ns1/ns1-go.v2 v2.14.1 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 58 - 0
go.sum

@@ -39,6 +39,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
 cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
 cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
+cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
+cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
 cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
 cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
 cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
@@ -178,6 +180,8 @@ cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvj
 cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=
 cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
 cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
+cloud.google.com/go/compute v1.36.0 h1:QzLrJRxytIGE8OJWzmMweIdiu2pIlRVq9kSi7+xnDkU=
+cloud.google.com/go/compute v1.36.0/go.mod h1:+GZuz5prSWFLquViP55zcjRrOm7vRpIllx2MaYpzuiI=
 cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
@@ -614,12 +618,16 @@ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0
 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.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
 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.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 h1:Bg8m3nq/X1DeePkAbCfb6ml6F3F0IunEhE8TMh+lY48=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
@@ -716,8 +724,12 @@ github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38y
 github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
 github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
 github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI=
+github.com/aws/aws-sdk-go-v2/config v1.29.13 h1:RgdPqWoE8nPpIekpVpDJsBckbqT4Liiaq9f35pbTh1Y=
+github.com/aws/aws-sdk-go-v2/config v1.29.13/go.mod h1:NI28qs/IOUIRhsR7GQ/JdexoqRN9tDxkIrYZq0SOF44=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.66 h1:aKpEKaTy6n4CEJeYI1MNj97oSDLi4xro3UzQfwf5RWE=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.66/go.mod h1:xQ5SusDmHb/fy55wU0QqTy0yNfLqxzec59YcsRZB+rI=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
@@ -733,14 +745,24 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 h1:0j58UseBtLuBcP6nY2z4SM1qZEvLF0ylyH6+ggnphLg=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1/go.mod h1:Qy22QnQSdHbZwMZrarsWZBIuK51isPlkD+Z4sztxX0o=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 h1:Bz0MltpmIFP2EBYADc17VHdXYxZw9JPQl8Ksq+w6aEE=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2/go.mod h1:Qy22QnQSdHbZwMZrarsWZBIuK51isPlkD+Z4sztxX0o=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 h1:/nkJHXtJXJeelXHqG0898+fWKgvfaXBhGzbCsSmn9j8=
 github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0/go.mod h1:kGYOjvTa0Vw0qxrqrOLut1vMnui6qLxqv/SX3vYeM8Y=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.51.0 h1:pK3YJIgOzYqctprqQ67kGSjeL+77r9Ue/4/gBonsGNc=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.51.0/go.mod h1:kGYOjvTa0Vw0qxrqrOLut1vMnui6qLxqv/SX3vYeM8Y=
 github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
 github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.18 h1:xz7WvTMfSStb9Y8NpCT82FXLNC3QasqBfuAFHY4Pk5g=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.18/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
 github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
 github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
 github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
@@ -782,6 +804,7 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
 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=
@@ -836,9 +859,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I=
 github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4=
+github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
+github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
@@ -901,6 +927,8 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
 github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
@@ -1217,6 +1245,8 @@ github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfE
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.142 h1:9iOJ8tfNLw8uSiR5yx7VcHEYSOajJq5hb9SXF0BCUdA=
 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.142/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
+github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.143 h1:V+82d6sqyEnG/XtIhC21zqVAMc1hiah3LQgTKZRfMtg=
+github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.143/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
 github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -1419,6 +1449,8 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
 github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
+github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
@@ -1533,6 +1565,8 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mo
 github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
 github.com/oracle/oci-go-sdk/v65 v65.88.0 h1:SbsGKsoRRxJxVTbwUyIPCPwPsHWb8aPgEEpo6qfRJnI=
 github.com/oracle/oci-go-sdk/v65 v65.88.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
+github.com/oracle/oci-go-sdk/v65 v65.88.1 h1:Y9Y5jlQX8oVDe3UN+O4IcQnLN/aQmi4jR1/2RsXMN3M=
+github.com/oracle/oci-go-sdk/v65 v65.88.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
 github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
 github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -1747,8 +1781,13 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1134 h1:NDCzSm7r8OZeWQje1FJNHM73Ku4QRrCP1GymfgZYLSM=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1134/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1136/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1138 h1:eMVp9kzjBptP3K0xaUUS68dF5nNTFLbom3uQREaqftM=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1138/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1134 h1:Iel1hDW0eQt6p8YDRH2EbjiK5mqC4KEzabSKV0ZQ6FY=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1134/go.mod h1:8R/Xhu0hKGRFT30uwoN44bisb3cOoNjV8iwH65DjqUc=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 h1:kMIdSU5IvpOROh27ToVQ3hlm6ym3lCRs9tnGCOBoZqk=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136/go.mod h1:FpyIz3mymKaExVs6Fz27kxDBS42jqZn7vbACtxdeEH4=
 github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
 github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
 github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
@@ -1773,6 +1812,8 @@ github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+
 github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
 github.com/uozi-tech/cosy v1.18.0 h1:L0o1yQ6hTRdzUjWwcT/cJX0AcNaDaaL30gF8pJHUEzM=
 github.com/uozi-tech/cosy v1.18.0/go.mod h1:8s8oQENTTGcmOGas/hkLvE+pZPyNG6AIblRbFgPRCwg=
+github.com/uozi-tech/cosy v1.19.0 h1:ZKfWfIicEIa4mP+4Fd6fF5oNr+pVOVkUssrIZuzIw44=
+github.com/uozi-tech/cosy v1.19.0/go.mod h1:WanqOcNiVoJGY6mPZScxua8yNLliJqcyPrsNTenz8dI=
 github.com/uozi-tech/cosy-driver-mysql v0.2.2 h1:22S/XNIvuaKGqxQPsYPXN8TZ8hHjCQdcJKVQ83Vzxoo=
 github.com/uozi-tech/cosy-driver-mysql v0.2.2/go.mod h1:EZnRIbSj1V5U0gEeTobrXai/d1SV11lkl4zP9NFEmyE=
 github.com/uozi-tech/cosy-driver-postgres v0.2.1 h1:OICakGuT+omva6QOJCxTJ5Lfr7CGXLmk/zD+aS51Z2o=
@@ -1786,6 +1827,8 @@ github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4
 github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
 github.com/volcengine/volc-sdk-golang v1.0.201 h1:AnKtLpuEGCLuH9Yd2TvhG0SeTa+u4+MpLotIMZCdBgU=
 github.com/volcengine/volc-sdk-golang v1.0.201/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
+github.com/volcengine/volc-sdk-golang v1.0.202 h1:8H4Rq7jWfrKdW9p3j+ZyvGvVe796AeVCpqEHb9zdBLo=
+github.com/volcengine/volc-sdk-golang v1.0.202/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
 github.com/vultr/govultr/v3 v3.18.0 h1:nTfxZW7/BRUDdZyEDSWzqrtyQgNolFPXBlwwJuM7EF8=
 github.com/vultr/govultr/v3 v3.18.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -1804,6 +1847,8 @@ github.com/yandex-cloud/go-genproto v0.0.0-20250325081613-cd85d9003939/go.mod h1
 github.com/yandex-cloud/go-sdk v0.0.0-20250325134853-dcb34ef70818 h1:EgfskqIEIv/f5vx/guwfkakNwy5H9Mm+OC17zS1ofus=
 github.com/yandex-cloud/go-sdk v0.0.0-20250325134853-dcb34ef70818/go.mod h1:U2Cc0SZ8kQHcL4ffnfNN78bdSybVP2pQNq0oJfFwvM8=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1924,6 +1969,7 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
 golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
 golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2236,6 +2282,7 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -2253,6 +2300,7 @@ golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
 golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
 golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2582,10 +2630,16 @@ 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-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw=
 google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0=
+google.golang.org/genproto v0.0.0-20250404141209-ee84b53bf3d0 h1:wX+y2uwLyC73sX9zfiJW7E7m68+oxAQGzgCmoM0e/zs=
+google.golang.org/genproto v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:jwIveCnYVWLDIe0ZXnIrfMKNoy/rQRSRrepUPEruz0U=
 google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
 google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 h1:Qbb5RVn5xzI4naMJSpJ7lhvmos6UwZkbekd5Uz7rt9E=
+google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:6T35kB3IPpdw7Wul09by0G/JuOuIFkXV6OOvt8IZeT8=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2630,6 +2684,8 @@ google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3
 google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
 google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
 google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -2675,6 +2731,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST
 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 gopkg.in/ns1/ns1-go.v2 v2.13.0 h1:I5NNqI9Bi1SGK92TVkOvLTwux5LNrix/99H2datVh48=
 gopkg.in/ns1/ns1-go.v2 v2.13.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
+gopkg.in/ns1/ns1-go.v2 v2.14.1 h1:wruE2g1uB90kMW+jHW8BtWa1HvNkqDfyf7SacTKWtBY=
+gopkg.in/ns1/ns1-go.v2 v2.14.1/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=

+ 4 - 3
internal/config/config.go

@@ -1,9 +1,10 @@
 package config
 
 import (
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/sashabaranov/go-openai"
-	"time"
 )
 
 type Config struct {
@@ -14,8 +15,8 @@ type Config struct {
 	ModifiedAt      time.Time                      `json:"modified_at"`
 	Size            int64                          `json:"size,omitempty"`
 	IsDir           bool                           `json:"is_dir"`
-	SiteCategoryID  uint64                         `json:"site_category_id"`
-	SiteCategory    *model.SiteCategory            `json:"site_category,omitempty"`
+	EnvGroupID      uint64                         `json:"env_group_id"`
+	EnvGroup        *model.EnvGroup                `json:"env_group,omitempty"`
 	Enabled         bool                           `json:"enabled"`
 	Dir             string                         `json:"dir"`
 }

+ 2 - 2
internal/config/config_list.go

@@ -33,8 +33,8 @@ func (c ConfigsSort) Less(i, j int) bool {
 		flag = boolToInt(c.ConfigList[i].IsDir) > boolToInt(c.ConfigList[j].IsDir)
 	case "enabled":
 		flag = boolToInt(c.ConfigList[i].Enabled) > boolToInt(c.ConfigList[j].Enabled)
-	case "site_category_id":
-		flag = c.ConfigList[i].SiteCategoryID > c.ConfigList[j].SiteCategoryID
+	case "env_group_id":
+		flag = c.ConfigList[i].EnvGroupID > c.ConfigList[j].EnvGroupID
 	}
 
 	if c.Order == "asc" {

+ 47 - 0
internal/migrate/1.site_category_to_env_group.go

@@ -0,0 +1,47 @@
+package migrate
+
+import (
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/go-gormigrate/gormigrate/v2"
+	"gorm.io/gorm"
+)
+
+var SiteCategoryToEnvGroup = &gormigrate.Migration{
+	ID: "20250405000001",
+	Migrate: func(tx *gorm.DB) error {
+		// Step 1: Create new env_groups table
+		if err := tx.Migrator().AutoMigrate(&model.EnvGroup{}); err != nil {
+			return err
+		}
+
+		// Step 2: Copy data from site_categories to env_groups
+		if tx.Migrator().HasTable("site_categories") {
+			var siteCategories []map[string]interface{}
+			if err := tx.Table("site_categories").Find(&siteCategories).Error; err != nil {
+				return err
+			}
+
+			for _, sc := range siteCategories {
+				if err := tx.Table("env_groups").Create(sc).Error; err != nil {
+					return err
+				}
+			}
+
+			// Step 3: Update sites table to use env_group_id instead of site_category_id
+			if tx.Migrator().HasColumn("sites", "site_category_id") {
+				// First add the new column if it doesn't exist
+				if !tx.Migrator().HasColumn("sites", "env_group_id") {
+					if err := tx.Exec("ALTER TABLE sites ADD COLUMN env_group_id bigint").Error; err != nil {
+						return err
+					}
+				}
+
+				// Copy the values from site_category_id to env_group_id
+				if err := tx.Exec("UPDATE sites SET env_group_id = site_category_id").Error; err != nil {
+					return err
+				}
+			}
+		}
+		return nil
+	},
+}

+ 64 - 0
internal/migrate/2.fix_site_and_stream_unique.go

@@ -0,0 +1,64 @@
+package migrate
+
+import (
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/go-gormigrate/gormigrate/v2"
+	"gorm.io/gorm"
+)
+
+var FixSiteAndStreamPathUnique = &gormigrate.Migration{
+	ID: "20250405000003",
+	Migrate: func(tx *gorm.DB) error {
+		// Check if sites table exists
+		if tx.Migrator().HasTable(&model.Site{}) {
+			// Find duplicated paths in sites table
+			var siteDuplicates []struct {
+				Path  string
+				Count int
+			}
+
+			if err := tx.Model(&model.Site{}).
+				Select("path, count(*) as count").
+			Group("path").
+			Having("count(*) > 1").
+			Find(&siteDuplicates).Error; err != nil {
+			return err
+		}
+
+		// For each duplicated path, delete all but the one with max id
+		for _, dup := range siteDuplicates {
+			if err := tx.Exec(`DELETE FROM sites WHERE path = ? AND id NOT IN 
+				(SELECT max(id) FROM sites WHERE path = ?)`, dup.Path, dup.Path).Error; err != nil {
+				return err
+			}
+		}
+	}
+
+		// Check if streams table exists
+		if tx.Migrator().HasTable(&model.Stream{}) {
+			// Find duplicated paths in streams table
+			var streamDuplicates []struct {
+				Path  string
+			Count int
+		}
+
+		if err := tx.Model(&model.Stream{}).
+			Select("path, count(*) as count").
+			Group("path").
+			Having("count(*) > 1").
+			Find(&streamDuplicates).Error; err != nil {
+			return err
+		}
+
+			// For each duplicated path, delete all but the one with max id
+			for _, dup := range streamDuplicates {
+				if err := tx.Exec(`DELETE FROM streams WHERE path = ? AND id NOT IN 
+					(SELECT max(id) FROM streams WHERE path = ?)`, dup.Path, dup.Path).Error; err != nil {
+					return err
+				}
+			}
+		}
+
+		return nil
+	},
+}

+ 44 - 0
internal/migrate/3.rename_auths_to_users.go

@@ -0,0 +1,44 @@
+package migrate
+
+import (
+	"github.com/go-gormigrate/gormigrate/v2"
+	"gorm.io/gorm"
+)
+
+var RenameAuthsToUsers = &gormigrate.Migration{
+	ID: "20250405000002",
+	Migrate: func(tx *gorm.DB) error {
+		// Check if both tables exist
+		hasAuthsTable := tx.Migrator().HasTable("auths")
+		hasUsersTable := tx.Migrator().HasTable("users")
+
+		if hasAuthsTable {
+			if hasUsersTable {
+				// Both tables exist - we need to check if users table is empty
+				var count int64
+				if err := tx.Table("users").Count(&count).Error; err != nil {
+					return err
+				}
+
+				if count > 0 {
+					// Users table has data - drop auths table as users table is now the source of truth
+					return tx.Migrator().DropTable("auths")
+				} else {
+					// Users table is empty - drop it and rename auths to users
+					return tx.Transaction(func(ttx *gorm.DB) error {
+						if err := ttx.Migrator().DropTable("users"); err != nil {
+							return err
+						}
+						return ttx.Migrator().RenameTable("auths", "users")
+					})
+				}
+			} else {
+				// Only auths table exists - simply rename it
+				return tx.Migrator().RenameTable("auths", "users")
+			}
+		}
+
+		// If auths table doesn't exist, nothing to do
+		return nil
+	},
+}

+ 14 - 0
internal/migrate/migrate.go

@@ -0,0 +1,14 @@
+package migrate
+
+import (
+	"github.com/go-gormigrate/gormigrate/v2"
+)
+
+var Migrations = []*gormigrate.Migration{
+	SiteCategoryToEnvGroup,
+	RenameAuthsToUsers,
+}
+
+var BeforeAutoMigrate = []*gormigrate.Migration{
+	FixSiteAndStreamPathUnique,
+}

+ 0 - 5
internal/nginx_log/nginx_log.go

@@ -24,11 +24,6 @@ func init() {
 
 // scanForLogDirectives scans and parses configuration files for log directives
 func scanForLogDirectives(configPath string, content []byte) error {
-	// Clear previous scan results when scanning the main config
-	if configPath == nginx.GetConfPath("", "nginx.conf") {
-		ClearLogCache()
-	}
-
 	// Find log directives using regex
 	matches := logDirectiveRegex.FindAllSubmatch(content, -1)
 

+ 4 - 4
internal/site/save.go

@@ -17,7 +17,7 @@ import (
 )
 
 // Save saves a site configuration file
-func Save(name string, content string, overwrite bool, siteCategoryId uint64, syncNodeIds []uint64) (err error) {
+func Save(name string, content string, overwrite bool, envGroupId uint64, syncNodeIds []uint64) (err error) {
 	path := nginx.GetConfPath("sites-available", name)
 	if !overwrite && helper.FileExists(path) {
 		return ErrDstFileExists
@@ -46,10 +46,10 @@ func Save(name string, content string, overwrite bool, siteCategoryId uint64, sy
 
 	s := query.Site
 	_, err = s.Where(s.Path.Eq(path)).
-		Select(s.SiteCategoryID, s.SyncNodeIDs).
+		Select(s.EnvGroupID, s.SyncNodeIDs).
 		Updates(&model.Site{
-			SiteCategoryID: siteCategoryId,
-			SyncNodeIDs:    syncNodeIds,
+			EnvGroupID:  envGroupId,
+			SyncNodeIDs: syncNodeIds,
 		})
 	if err != nil {
 		return

+ 3 - 3
internal/site/sync.go

@@ -16,7 +16,7 @@ func getSyncNodes(name string) (nodes []*model.Environment) {
 	configFilePath := nginx.GetConfPath("sites-available", name)
 	s := query.Site
 	site, err := s.Where(s.Path.Eq(configFilePath)).
-		Preload(s.SiteCategory).First()
+		Preload(s.EnvGroup).First()
 	if err != nil {
 		logger.Error(err)
 		return
@@ -24,8 +24,8 @@ func getSyncNodes(name string) (nodes []*model.Environment) {
 
 	syncNodeIds := site.SyncNodeIDs
 	// inherit sync node ids from site category
-	if site.SiteCategory != nil {
-		syncNodeIds = append(syncNodeIds, site.SiteCategory.SyncNodeIds...)
+	if site.EnvGroup != nil {
+		syncNodeIds = append(syncNodeIds, site.EnvGroup.SyncNodeIds...)
 	}
 	syncNodeIds = lo.Uniq(syncNodeIds)
 

+ 6 - 1
internal/stream/sync.go

@@ -15,13 +15,18 @@ import (
 func getSyncNodes(name string) (nodes []*model.Environment) {
 	configFilePath := nginx.GetConfPath("streams-available", name)
 	s := query.Stream
-	stream, err := s.Where(s.Path.Eq(configFilePath)).First()
+	stream, err := s.Where(s.Path.Eq(configFilePath)).
+		Preload(s.EnvGroup).First()
 	if err != nil {
 		logger.Error(err)
 		return
 	}
 
 	syncNodeIds := stream.SyncNodeIDs
+	// inherit sync node ids from site category
+	if stream.EnvGroup != nil {
+		syncNodeIds = append(syncNodeIds, stream.EnvGroup.SyncNodeIds...)
+	}
 
 	e := query.Environment
 	nodes, err = e.Where(e.ID.In(syncNodeIds...)).Find()

+ 6 - 0
main.go

@@ -10,6 +10,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/internal/cert"
 	"github.com/0xJacky/Nginx-UI/internal/cmd"
 	"github.com/0xJacky/Nginx-UI/internal/kernel"
+	"github.com/0xJacky/Nginx-UI/internal/migrate"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/router"
 	"github.com/0xJacky/Nginx-UI/settings"
@@ -28,8 +29,13 @@ func Program(confPath string) func(state overseer.State) {
 	return func(state overseer.State) {
 		defer logger.Sync()
 		defer logger.Info("Server exited")
+
+		cosy.RegisterMigrationsBeforeAutoMigrate(migrate.BeforeAutoMigrate)
+
 		cosy.RegisterModels(model.GenerateAllModel()...)
 
+		cosy.RegisterMigration(migrate.Migrations)
+
 		cosy.RegisterInitFunc(kernel.Boot, router.InitRouter)
 
 		// Initialize settings package

+ 2 - 1
model/site_category.go → model/env_group.go

@@ -1,6 +1,7 @@
 package model
 
-type SiteCategory struct {
+// EnvGroup represents a group of environments that can be synced across nodes
+type EnvGroup struct {
 	Model
 	Name        string   `json:"name"`
 	SyncNodeIds []uint64 `json:"sync_node_ids" gorm:"serializer:json"`

+ 3 - 2
model/model.go

@@ -1,9 +1,10 @@
 package model
 
 import (
+	"time"
+
 	"gorm.io/gen"
 	"gorm.io/gorm"
-	"time"
 )
 
 var db *gorm.DB
@@ -31,7 +32,7 @@ func GenerateAllModel() []any {
 		BanIP{},
 		Config{},
 		Passkey{},
-		SiteCategory{},
+		EnvGroup{},
 	}
 }
 

+ 5 - 5
model/site.go

@@ -2,9 +2,9 @@ package model
 
 type Site struct {
 	Model
-	Path           string        `json:"path"`
-	Advanced       bool          `json:"advanced"`
-	SiteCategoryID uint64        `json:"site_category_id"`
-	SiteCategory   *SiteCategory `json:"site_category,omitempty"`
-	SyncNodeIDs    []uint64      `json:"sync_node_ids" gorm:"serializer:json"`
+	Path        string    `json:"path" gorm:"uniqueIndex"`
+	Advanced    bool      `json:"advanced"`
+	EnvGroupID  uint64    `json:"env_group_id"`
+	EnvGroup    *EnvGroup `json:"env_group,omitempty"`
+	SyncNodeIDs []uint64  `json:"sync_node_ids" gorm:"serializer:json"`
 }

+ 5 - 3
model/stream.go

@@ -2,7 +2,9 @@ package model
 
 type Stream struct {
 	Model
-	Path        string   `json:"path"`
-	Advanced    bool     `json:"advanced"`
-	SyncNodeIDs []uint64 `json:"sync_node_ids" gorm:"serializer:json"`
+	Path        string    `json:"path" gorm:"uniqueIndex"`
+	Advanced    bool      `json:"advanced"`
+	EnvGroupID  uint64    `json:"env_group_id"`
+	EnvGroup    *EnvGroup `json:"env_group,omitempty"`
+	SyncNodeIDs []uint64  `json:"sync_node_ids" gorm:"serializer:json"`
 }

+ 1 - 1
model/user.go

@@ -41,7 +41,7 @@ type AuthToken struct {
 }
 
 func (u *User) TableName() string {
-	return "auths"
+	return "users"
 }
 
 func (u *User) AfterFind(_ *gorm.DB) error {

+ 374 - 0
query/env_groups.gen.go

@@ -0,0 +1,374 @@
+// 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 newEnvGroup(db *gorm.DB, opts ...gen.DOOption) envGroup {
+	_envGroup := envGroup{}
+
+	_envGroup.envGroupDo.UseDB(db, opts...)
+	_envGroup.envGroupDo.UseModel(&model.EnvGroup{})
+
+	tableName := _envGroup.envGroupDo.TableName()
+	_envGroup.ALL = field.NewAsterisk(tableName)
+	_envGroup.ID = field.NewUint64(tableName, "id")
+	_envGroup.CreatedAt = field.NewTime(tableName, "created_at")
+	_envGroup.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_envGroup.DeletedAt = field.NewField(tableName, "deleted_at")
+	_envGroup.Name = field.NewString(tableName, "name")
+	_envGroup.SyncNodeIds = field.NewField(tableName, "sync_node_ids")
+	_envGroup.OrderID = field.NewInt(tableName, "order_id")
+
+	_envGroup.fillFieldMap()
+
+	return _envGroup
+}
+
+type envGroup struct {
+	envGroupDo
+
+	ALL         field.Asterisk
+	ID          field.Uint64
+	CreatedAt   field.Time
+	UpdatedAt   field.Time
+	DeletedAt   field.Field
+	Name        field.String
+	SyncNodeIds field.Field
+	OrderID     field.Int
+
+	fieldMap map[string]field.Expr
+}
+
+func (e envGroup) Table(newTableName string) *envGroup {
+	e.envGroupDo.UseTable(newTableName)
+	return e.updateTableName(newTableName)
+}
+
+func (e envGroup) As(alias string) *envGroup {
+	e.envGroupDo.DO = *(e.envGroupDo.As(alias).(*gen.DO))
+	return e.updateTableName(alias)
+}
+
+func (e *envGroup) updateTableName(table string) *envGroup {
+	e.ALL = field.NewAsterisk(table)
+	e.ID = field.NewUint64(table, "id")
+	e.CreatedAt = field.NewTime(table, "created_at")
+	e.UpdatedAt = field.NewTime(table, "updated_at")
+	e.DeletedAt = field.NewField(table, "deleted_at")
+	e.Name = field.NewString(table, "name")
+	e.SyncNodeIds = field.NewField(table, "sync_node_ids")
+	e.OrderID = field.NewInt(table, "order_id")
+
+	e.fillFieldMap()
+
+	return e
+}
+
+func (e *envGroup) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := e.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (e *envGroup) fillFieldMap() {
+	e.fieldMap = make(map[string]field.Expr, 7)
+	e.fieldMap["id"] = e.ID
+	e.fieldMap["created_at"] = e.CreatedAt
+	e.fieldMap["updated_at"] = e.UpdatedAt
+	e.fieldMap["deleted_at"] = e.DeletedAt
+	e.fieldMap["name"] = e.Name
+	e.fieldMap["sync_node_ids"] = e.SyncNodeIds
+	e.fieldMap["order_id"] = e.OrderID
+}
+
+func (e envGroup) clone(db *gorm.DB) envGroup {
+	e.envGroupDo.ReplaceConnPool(db.Statement.ConnPool)
+	return e
+}
+
+func (e envGroup) replaceDB(db *gorm.DB) envGroup {
+	e.envGroupDo.ReplaceDB(db)
+	return e
+}
+
+type envGroupDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (e envGroupDo) FirstByID(id uint64) (result *model.EnvGroup, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = e.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 (e envGroupDo) DeleteByID(id uint64) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update env_groups set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = e.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (e envGroupDo) Debug() *envGroupDo {
+	return e.withDO(e.DO.Debug())
+}
+
+func (e envGroupDo) WithContext(ctx context.Context) *envGroupDo {
+	return e.withDO(e.DO.WithContext(ctx))
+}
+
+func (e envGroupDo) ReadDB() *envGroupDo {
+	return e.Clauses(dbresolver.Read)
+}
+
+func (e envGroupDo) WriteDB() *envGroupDo {
+	return e.Clauses(dbresolver.Write)
+}
+
+func (e envGroupDo) Session(config *gorm.Session) *envGroupDo {
+	return e.withDO(e.DO.Session(config))
+}
+
+func (e envGroupDo) Clauses(conds ...clause.Expression) *envGroupDo {
+	return e.withDO(e.DO.Clauses(conds...))
+}
+
+func (e envGroupDo) Returning(value interface{}, columns ...string) *envGroupDo {
+	return e.withDO(e.DO.Returning(value, columns...))
+}
+
+func (e envGroupDo) Not(conds ...gen.Condition) *envGroupDo {
+	return e.withDO(e.DO.Not(conds...))
+}
+
+func (e envGroupDo) Or(conds ...gen.Condition) *envGroupDo {
+	return e.withDO(e.DO.Or(conds...))
+}
+
+func (e envGroupDo) Select(conds ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.Select(conds...))
+}
+
+func (e envGroupDo) Where(conds ...gen.Condition) *envGroupDo {
+	return e.withDO(e.DO.Where(conds...))
+}
+
+func (e envGroupDo) Order(conds ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.Order(conds...))
+}
+
+func (e envGroupDo) Distinct(cols ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.Distinct(cols...))
+}
+
+func (e envGroupDo) Omit(cols ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.Omit(cols...))
+}
+
+func (e envGroupDo) Join(table schema.Tabler, on ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.Join(table, on...))
+}
+
+func (e envGroupDo) LeftJoin(table schema.Tabler, on ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.LeftJoin(table, on...))
+}
+
+func (e envGroupDo) RightJoin(table schema.Tabler, on ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.RightJoin(table, on...))
+}
+
+func (e envGroupDo) Group(cols ...field.Expr) *envGroupDo {
+	return e.withDO(e.DO.Group(cols...))
+}
+
+func (e envGroupDo) Having(conds ...gen.Condition) *envGroupDo {
+	return e.withDO(e.DO.Having(conds...))
+}
+
+func (e envGroupDo) Limit(limit int) *envGroupDo {
+	return e.withDO(e.DO.Limit(limit))
+}
+
+func (e envGroupDo) Offset(offset int) *envGroupDo {
+	return e.withDO(e.DO.Offset(offset))
+}
+
+func (e envGroupDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *envGroupDo {
+	return e.withDO(e.DO.Scopes(funcs...))
+}
+
+func (e envGroupDo) Unscoped() *envGroupDo {
+	return e.withDO(e.DO.Unscoped())
+}
+
+func (e envGroupDo) Create(values ...*model.EnvGroup) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return e.DO.Create(values)
+}
+
+func (e envGroupDo) CreateInBatches(values []*model.EnvGroup, batchSize int) error {
+	return e.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 (e envGroupDo) Save(values ...*model.EnvGroup) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return e.DO.Save(values)
+}
+
+func (e envGroupDo) First() (*model.EnvGroup, error) {
+	if result, err := e.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.EnvGroup), nil
+	}
+}
+
+func (e envGroupDo) Take() (*model.EnvGroup, error) {
+	if result, err := e.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.EnvGroup), nil
+	}
+}
+
+func (e envGroupDo) Last() (*model.EnvGroup, error) {
+	if result, err := e.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.EnvGroup), nil
+	}
+}
+
+func (e envGroupDo) Find() ([]*model.EnvGroup, error) {
+	result, err := e.DO.Find()
+	return result.([]*model.EnvGroup), err
+}
+
+func (e envGroupDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.EnvGroup, err error) {
+	buf := make([]*model.EnvGroup, 0, batchSize)
+	err = e.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 (e envGroupDo) FindInBatches(result *[]*model.EnvGroup, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return e.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (e envGroupDo) Attrs(attrs ...field.AssignExpr) *envGroupDo {
+	return e.withDO(e.DO.Attrs(attrs...))
+}
+
+func (e envGroupDo) Assign(attrs ...field.AssignExpr) *envGroupDo {
+	return e.withDO(e.DO.Assign(attrs...))
+}
+
+func (e envGroupDo) Joins(fields ...field.RelationField) *envGroupDo {
+	for _, _f := range fields {
+		e = *e.withDO(e.DO.Joins(_f))
+	}
+	return &e
+}
+
+func (e envGroupDo) Preload(fields ...field.RelationField) *envGroupDo {
+	for _, _f := range fields {
+		e = *e.withDO(e.DO.Preload(_f))
+	}
+	return &e
+}
+
+func (e envGroupDo) FirstOrInit() (*model.EnvGroup, error) {
+	if result, err := e.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.EnvGroup), nil
+	}
+}
+
+func (e envGroupDo) FirstOrCreate() (*model.EnvGroup, error) {
+	if result, err := e.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.EnvGroup), nil
+	}
+}
+
+func (e envGroupDo) FindByPage(offset int, limit int) (result []*model.EnvGroup, count int64, err error) {
+	result, err = e.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 = e.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (e envGroupDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = e.Count()
+	if err != nil {
+		return
+	}
+
+	err = e.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (e envGroupDo) Scan(result interface{}) (err error) {
+	return e.DO.Scan(result)
+}
+
+func (e envGroupDo) Delete(models ...*model.EnvGroup) (result gen.ResultInfo, err error) {
+	return e.DO.Delete(models)
+}
+
+func (e *envGroupDo) withDO(do gen.Dao) *envGroupDo {
+	e.DO = *do.(*gen.DO)
+	return e
+}

+ 8 - 8
query/gen.go

@@ -25,11 +25,11 @@ var (
 	Config        *config
 	ConfigBackup  *configBackup
 	DnsCredential *dnsCredential
+	EnvGroup      *envGroup
 	Environment   *environment
 	Notification  *notification
 	Passkey       *passkey
 	Site          *site
-	SiteCategory  *siteCategory
 	Stream        *stream
 	User          *user
 )
@@ -44,11 +44,11 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	Config = &Q.Config
 	ConfigBackup = &Q.ConfigBackup
 	DnsCredential = &Q.DnsCredential
+	EnvGroup = &Q.EnvGroup
 	Environment = &Q.Environment
 	Notification = &Q.Notification
 	Passkey = &Q.Passkey
 	Site = &Q.Site
-	SiteCategory = &Q.SiteCategory
 	Stream = &Q.Stream
 	User = &Q.User
 }
@@ -64,11 +64,11 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		Config:        newConfig(db, opts...),
 		ConfigBackup:  newConfigBackup(db, opts...),
 		DnsCredential: newDnsCredential(db, opts...),
+		EnvGroup:      newEnvGroup(db, opts...),
 		Environment:   newEnvironment(db, opts...),
 		Notification:  newNotification(db, opts...),
 		Passkey:       newPasskey(db, opts...),
 		Site:          newSite(db, opts...),
-		SiteCategory:  newSiteCategory(db, opts...),
 		Stream:        newStream(db, opts...),
 		User:          newUser(db, opts...),
 	}
@@ -85,11 +85,11 @@ type Query struct {
 	Config        config
 	ConfigBackup  configBackup
 	DnsCredential dnsCredential
+	EnvGroup      envGroup
 	Environment   environment
 	Notification  notification
 	Passkey       passkey
 	Site          site
-	SiteCategory  siteCategory
 	Stream        stream
 	User          user
 }
@@ -107,11 +107,11 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		Config:        q.Config.clone(db),
 		ConfigBackup:  q.ConfigBackup.clone(db),
 		DnsCredential: q.DnsCredential.clone(db),
+		EnvGroup:      q.EnvGroup.clone(db),
 		Environment:   q.Environment.clone(db),
 		Notification:  q.Notification.clone(db),
 		Passkey:       q.Passkey.clone(db),
 		Site:          q.Site.clone(db),
-		SiteCategory:  q.SiteCategory.clone(db),
 		Stream:        q.Stream.clone(db),
 		User:          q.User.clone(db),
 	}
@@ -136,11 +136,11 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		Config:        q.Config.replaceDB(db),
 		ConfigBackup:  q.ConfigBackup.replaceDB(db),
 		DnsCredential: q.DnsCredential.replaceDB(db),
+		EnvGroup:      q.EnvGroup.replaceDB(db),
 		Environment:   q.Environment.replaceDB(db),
 		Notification:  q.Notification.replaceDB(db),
 		Passkey:       q.Passkey.replaceDB(db),
 		Site:          q.Site.replaceDB(db),
-		SiteCategory:  q.SiteCategory.replaceDB(db),
 		Stream:        q.Stream.replaceDB(db),
 		User:          q.User.replaceDB(db),
 	}
@@ -155,11 +155,11 @@ type queryCtx struct {
 	Config        *configDo
 	ConfigBackup  *configBackupDo
 	DnsCredential *dnsCredentialDo
+	EnvGroup      *envGroupDo
 	Environment   *environmentDo
 	Notification  *notificationDo
 	Passkey       *passkeyDo
 	Site          *siteDo
-	SiteCategory  *siteCategoryDo
 	Stream        *streamDo
 	User          *userDo
 }
@@ -174,11 +174,11 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		Config:        q.Config.WithContext(ctx),
 		ConfigBackup:  q.ConfigBackup.WithContext(ctx),
 		DnsCredential: q.DnsCredential.WithContext(ctx),
+		EnvGroup:      q.EnvGroup.WithContext(ctx),
 		Environment:   q.Environment.WithContext(ctx),
 		Notification:  q.Notification.WithContext(ctx),
 		Passkey:       q.Passkey.WithContext(ctx),
 		Site:          q.Site.WithContext(ctx),
-		SiteCategory:  q.SiteCategory.WithContext(ctx),
 		Stream:        q.Stream.WithContext(ctx),
 		User:          q.User.WithContext(ctx),
 	}

+ 0 - 374
query/site_categories.gen.go

@@ -1,374 +0,0 @@
-// 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 newSiteCategory(db *gorm.DB, opts ...gen.DOOption) siteCategory {
-	_siteCategory := siteCategory{}
-
-	_siteCategory.siteCategoryDo.UseDB(db, opts...)
-	_siteCategory.siteCategoryDo.UseModel(&model.SiteCategory{})
-
-	tableName := _siteCategory.siteCategoryDo.TableName()
-	_siteCategory.ALL = field.NewAsterisk(tableName)
-	_siteCategory.ID = field.NewUint64(tableName, "id")
-	_siteCategory.CreatedAt = field.NewTime(tableName, "created_at")
-	_siteCategory.UpdatedAt = field.NewTime(tableName, "updated_at")
-	_siteCategory.DeletedAt = field.NewField(tableName, "deleted_at")
-	_siteCategory.Name = field.NewString(tableName, "name")
-	_siteCategory.SyncNodeIds = field.NewField(tableName, "sync_node_ids")
-	_siteCategory.OrderID = field.NewInt(tableName, "order_id")
-
-	_siteCategory.fillFieldMap()
-
-	return _siteCategory
-}
-
-type siteCategory struct {
-	siteCategoryDo
-
-	ALL         field.Asterisk
-	ID          field.Uint64
-	CreatedAt   field.Time
-	UpdatedAt   field.Time
-	DeletedAt   field.Field
-	Name        field.String
-	SyncNodeIds field.Field
-	OrderID     field.Int
-
-	fieldMap map[string]field.Expr
-}
-
-func (s siteCategory) Table(newTableName string) *siteCategory {
-	s.siteCategoryDo.UseTable(newTableName)
-	return s.updateTableName(newTableName)
-}
-
-func (s siteCategory) As(alias string) *siteCategory {
-	s.siteCategoryDo.DO = *(s.siteCategoryDo.As(alias).(*gen.DO))
-	return s.updateTableName(alias)
-}
-
-func (s *siteCategory) updateTableName(table string) *siteCategory {
-	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.Name = field.NewString(table, "name")
-	s.SyncNodeIds = field.NewField(table, "sync_node_ids")
-	s.OrderID = field.NewInt(table, "order_id")
-
-	s.fillFieldMap()
-
-	return s
-}
-
-func (s *siteCategory) 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 *siteCategory) fillFieldMap() {
-	s.fieldMap = make(map[string]field.Expr, 7)
-	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["name"] = s.Name
-	s.fieldMap["sync_node_ids"] = s.SyncNodeIds
-	s.fieldMap["order_id"] = s.OrderID
-}
-
-func (s siteCategory) clone(db *gorm.DB) siteCategory {
-	s.siteCategoryDo.ReplaceConnPool(db.Statement.ConnPool)
-	return s
-}
-
-func (s siteCategory) replaceDB(db *gorm.DB) siteCategory {
-	s.siteCategoryDo.ReplaceDB(db)
-	return s
-}
-
-type siteCategoryDo struct{ gen.DO }
-
-// FirstByID Where("id=@id")
-func (s siteCategoryDo) FirstByID(id uint64) (result *model.SiteCategory, 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 siteCategoryDo) DeleteByID(id uint64) (err error) {
-	var params []interface{}
-
-	var generateSQL strings.Builder
-	params = append(params, id)
-	generateSQL.WriteString("update site_categories 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 siteCategoryDo) Debug() *siteCategoryDo {
-	return s.withDO(s.DO.Debug())
-}
-
-func (s siteCategoryDo) WithContext(ctx context.Context) *siteCategoryDo {
-	return s.withDO(s.DO.WithContext(ctx))
-}
-
-func (s siteCategoryDo) ReadDB() *siteCategoryDo {
-	return s.Clauses(dbresolver.Read)
-}
-
-func (s siteCategoryDo) WriteDB() *siteCategoryDo {
-	return s.Clauses(dbresolver.Write)
-}
-
-func (s siteCategoryDo) Session(config *gorm.Session) *siteCategoryDo {
-	return s.withDO(s.DO.Session(config))
-}
-
-func (s siteCategoryDo) Clauses(conds ...clause.Expression) *siteCategoryDo {
-	return s.withDO(s.DO.Clauses(conds...))
-}
-
-func (s siteCategoryDo) Returning(value interface{}, columns ...string) *siteCategoryDo {
-	return s.withDO(s.DO.Returning(value, columns...))
-}
-
-func (s siteCategoryDo) Not(conds ...gen.Condition) *siteCategoryDo {
-	return s.withDO(s.DO.Not(conds...))
-}
-
-func (s siteCategoryDo) Or(conds ...gen.Condition) *siteCategoryDo {
-	return s.withDO(s.DO.Or(conds...))
-}
-
-func (s siteCategoryDo) Select(conds ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.Select(conds...))
-}
-
-func (s siteCategoryDo) Where(conds ...gen.Condition) *siteCategoryDo {
-	return s.withDO(s.DO.Where(conds...))
-}
-
-func (s siteCategoryDo) Order(conds ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.Order(conds...))
-}
-
-func (s siteCategoryDo) Distinct(cols ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.Distinct(cols...))
-}
-
-func (s siteCategoryDo) Omit(cols ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.Omit(cols...))
-}
-
-func (s siteCategoryDo) Join(table schema.Tabler, on ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.Join(table, on...))
-}
-
-func (s siteCategoryDo) LeftJoin(table schema.Tabler, on ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.LeftJoin(table, on...))
-}
-
-func (s siteCategoryDo) RightJoin(table schema.Tabler, on ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.RightJoin(table, on...))
-}
-
-func (s siteCategoryDo) Group(cols ...field.Expr) *siteCategoryDo {
-	return s.withDO(s.DO.Group(cols...))
-}
-
-func (s siteCategoryDo) Having(conds ...gen.Condition) *siteCategoryDo {
-	return s.withDO(s.DO.Having(conds...))
-}
-
-func (s siteCategoryDo) Limit(limit int) *siteCategoryDo {
-	return s.withDO(s.DO.Limit(limit))
-}
-
-func (s siteCategoryDo) Offset(offset int) *siteCategoryDo {
-	return s.withDO(s.DO.Offset(offset))
-}
-
-func (s siteCategoryDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *siteCategoryDo {
-	return s.withDO(s.DO.Scopes(funcs...))
-}
-
-func (s siteCategoryDo) Unscoped() *siteCategoryDo {
-	return s.withDO(s.DO.Unscoped())
-}
-
-func (s siteCategoryDo) Create(values ...*model.SiteCategory) error {
-	if len(values) == 0 {
-		return nil
-	}
-	return s.DO.Create(values)
-}
-
-func (s siteCategoryDo) CreateInBatches(values []*model.SiteCategory, 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 siteCategoryDo) Save(values ...*model.SiteCategory) error {
-	if len(values) == 0 {
-		return nil
-	}
-	return s.DO.Save(values)
-}
-
-func (s siteCategoryDo) First() (*model.SiteCategory, error) {
-	if result, err := s.DO.First(); err != nil {
-		return nil, err
-	} else {
-		return result.(*model.SiteCategory), nil
-	}
-}
-
-func (s siteCategoryDo) Take() (*model.SiteCategory, error) {
-	if result, err := s.DO.Take(); err != nil {
-		return nil, err
-	} else {
-		return result.(*model.SiteCategory), nil
-	}
-}
-
-func (s siteCategoryDo) Last() (*model.SiteCategory, error) {
-	if result, err := s.DO.Last(); err != nil {
-		return nil, err
-	} else {
-		return result.(*model.SiteCategory), nil
-	}
-}
-
-func (s siteCategoryDo) Find() ([]*model.SiteCategory, error) {
-	result, err := s.DO.Find()
-	return result.([]*model.SiteCategory), err
-}
-
-func (s siteCategoryDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.SiteCategory, err error) {
-	buf := make([]*model.SiteCategory, 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 siteCategoryDo) FindInBatches(result *[]*model.SiteCategory, batchSize int, fc func(tx gen.Dao, batch int) error) error {
-	return s.DO.FindInBatches(result, batchSize, fc)
-}
-
-func (s siteCategoryDo) Attrs(attrs ...field.AssignExpr) *siteCategoryDo {
-	return s.withDO(s.DO.Attrs(attrs...))
-}
-
-func (s siteCategoryDo) Assign(attrs ...field.AssignExpr) *siteCategoryDo {
-	return s.withDO(s.DO.Assign(attrs...))
-}
-
-func (s siteCategoryDo) Joins(fields ...field.RelationField) *siteCategoryDo {
-	for _, _f := range fields {
-		s = *s.withDO(s.DO.Joins(_f))
-	}
-	return &s
-}
-
-func (s siteCategoryDo) Preload(fields ...field.RelationField) *siteCategoryDo {
-	for _, _f := range fields {
-		s = *s.withDO(s.DO.Preload(_f))
-	}
-	return &s
-}
-
-func (s siteCategoryDo) FirstOrInit() (*model.SiteCategory, error) {
-	if result, err := s.DO.FirstOrInit(); err != nil {
-		return nil, err
-	} else {
-		return result.(*model.SiteCategory), nil
-	}
-}
-
-func (s siteCategoryDo) FirstOrCreate() (*model.SiteCategory, error) {
-	if result, err := s.DO.FirstOrCreate(); err != nil {
-		return nil, err
-	} else {
-		return result.(*model.SiteCategory), nil
-	}
-}
-
-func (s siteCategoryDo) FindByPage(offset int, limit int) (result []*model.SiteCategory, 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 siteCategoryDo) 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 siteCategoryDo) Scan(result interface{}) (err error) {
-	return s.DO.Scan(result)
-}
-
-func (s siteCategoryDo) Delete(models ...*model.SiteCategory) (result gen.ResultInfo, err error) {
-	return s.DO.Delete(models)
-}
-
-func (s *siteCategoryDo) withDO(do gen.Dao) *siteCategoryDo {
-	s.DO = *do.(*gen.DO)
-	return s
-}

+ 28 - 28
query/sites.gen.go

@@ -34,12 +34,12 @@ func newSite(db *gorm.DB, opts ...gen.DOOption) site {
 	_site.DeletedAt = field.NewField(tableName, "deleted_at")
 	_site.Path = field.NewString(tableName, "path")
 	_site.Advanced = field.NewBool(tableName, "advanced")
-	_site.SiteCategoryID = field.NewUint64(tableName, "site_category_id")
+	_site.EnvGroupID = field.NewUint64(tableName, "env_group_id")
 	_site.SyncNodeIDs = field.NewField(tableName, "sync_node_ids")
-	_site.SiteCategory = siteBelongsToSiteCategory{
+	_site.EnvGroup = siteBelongsToEnvGroup{
 		db: db.Session(&gorm.Session{}),
 
-		RelationField: field.NewRelation("SiteCategory", "model.SiteCategory"),
+		RelationField: field.NewRelation("EnvGroup", "model.EnvGroup"),
 	}
 
 	_site.fillFieldMap()
@@ -50,16 +50,16 @@ func newSite(db *gorm.DB, opts ...gen.DOOption) site {
 type site struct {
 	siteDo
 
-	ALL            field.Asterisk
-	ID             field.Uint64
-	CreatedAt      field.Time
-	UpdatedAt      field.Time
-	DeletedAt      field.Field
-	Path           field.String
-	Advanced       field.Bool
-	SiteCategoryID field.Uint64
-	SyncNodeIDs    field.Field
-	SiteCategory   siteBelongsToSiteCategory
+	ALL         field.Asterisk
+	ID          field.Uint64
+	CreatedAt   field.Time
+	UpdatedAt   field.Time
+	DeletedAt   field.Field
+	Path        field.String
+	Advanced    field.Bool
+	EnvGroupID  field.Uint64
+	SyncNodeIDs field.Field
+	EnvGroup    siteBelongsToEnvGroup
 
 	fieldMap map[string]field.Expr
 }
@@ -82,7 +82,7 @@ func (s *site) updateTableName(table string) *site {
 	s.DeletedAt = field.NewField(table, "deleted_at")
 	s.Path = field.NewString(table, "path")
 	s.Advanced = field.NewBool(table, "advanced")
-	s.SiteCategoryID = field.NewUint64(table, "site_category_id")
+	s.EnvGroupID = field.NewUint64(table, "env_group_id")
 	s.SyncNodeIDs = field.NewField(table, "sync_node_ids")
 
 	s.fillFieldMap()
@@ -107,7 +107,7 @@ func (s *site) fillFieldMap() {
 	s.fieldMap["deleted_at"] = s.DeletedAt
 	s.fieldMap["path"] = s.Path
 	s.fieldMap["advanced"] = s.Advanced
-	s.fieldMap["site_category_id"] = s.SiteCategoryID
+	s.fieldMap["env_group_id"] = s.EnvGroupID
 	s.fieldMap["sync_node_ids"] = s.SyncNodeIDs
 
 }
@@ -122,13 +122,13 @@ func (s site) replaceDB(db *gorm.DB) site {
 	return s
 }
 
-type siteBelongsToSiteCategory struct {
+type siteBelongsToEnvGroup struct {
 	db *gorm.DB
 
 	field.RelationField
 }
 
-func (a siteBelongsToSiteCategory) Where(conds ...field.Expr) *siteBelongsToSiteCategory {
+func (a siteBelongsToEnvGroup) Where(conds ...field.Expr) *siteBelongsToEnvGroup {
 	if len(conds) == 0 {
 		return &a
 	}
@@ -141,27 +141,27 @@ func (a siteBelongsToSiteCategory) Where(conds ...field.Expr) *siteBelongsToSite
 	return &a
 }
 
-func (a siteBelongsToSiteCategory) WithContext(ctx context.Context) *siteBelongsToSiteCategory {
+func (a siteBelongsToEnvGroup) WithContext(ctx context.Context) *siteBelongsToEnvGroup {
 	a.db = a.db.WithContext(ctx)
 	return &a
 }
 
-func (a siteBelongsToSiteCategory) Session(session *gorm.Session) *siteBelongsToSiteCategory {
+func (a siteBelongsToEnvGroup) Session(session *gorm.Session) *siteBelongsToEnvGroup {
 	a.db = a.db.Session(session)
 	return &a
 }
 
-func (a siteBelongsToSiteCategory) Model(m *model.Site) *siteBelongsToSiteCategoryTx {
-	return &siteBelongsToSiteCategoryTx{a.db.Model(m).Association(a.Name())}
+func (a siteBelongsToEnvGroup) Model(m *model.Site) *siteBelongsToEnvGroupTx {
+	return &siteBelongsToEnvGroupTx{a.db.Model(m).Association(a.Name())}
 }
 
-type siteBelongsToSiteCategoryTx struct{ tx *gorm.Association }
+type siteBelongsToEnvGroupTx struct{ tx *gorm.Association }
 
-func (a siteBelongsToSiteCategoryTx) Find() (result *model.SiteCategory, err error) {
+func (a siteBelongsToEnvGroupTx) Find() (result *model.EnvGroup, err error) {
 	return result, a.tx.Find(&result)
 }
 
-func (a siteBelongsToSiteCategoryTx) Append(values ...*model.SiteCategory) (err error) {
+func (a siteBelongsToEnvGroupTx) Append(values ...*model.EnvGroup) (err error) {
 	targetValues := make([]interface{}, len(values))
 	for i, v := range values {
 		targetValues[i] = v
@@ -169,7 +169,7 @@ func (a siteBelongsToSiteCategoryTx) Append(values ...*model.SiteCategory) (err
 	return a.tx.Append(targetValues...)
 }
 
-func (a siteBelongsToSiteCategoryTx) Replace(values ...*model.SiteCategory) (err error) {
+func (a siteBelongsToEnvGroupTx) Replace(values ...*model.EnvGroup) (err error) {
 	targetValues := make([]interface{}, len(values))
 	for i, v := range values {
 		targetValues[i] = v
@@ -177,7 +177,7 @@ func (a siteBelongsToSiteCategoryTx) Replace(values ...*model.SiteCategory) (err
 	return a.tx.Replace(targetValues...)
 }
 
-func (a siteBelongsToSiteCategoryTx) Delete(values ...*model.SiteCategory) (err error) {
+func (a siteBelongsToEnvGroupTx) Delete(values ...*model.EnvGroup) (err error) {
 	targetValues := make([]interface{}, len(values))
 	for i, v := range values {
 		targetValues[i] = v
@@ -185,11 +185,11 @@ func (a siteBelongsToSiteCategoryTx) Delete(values ...*model.SiteCategory) (err
 	return a.tx.Delete(targetValues...)
 }
 
-func (a siteBelongsToSiteCategoryTx) Clear() error {
+func (a siteBelongsToEnvGroupTx) Clear() error {
 	return a.tx.Clear()
 }
 
-func (a siteBelongsToSiteCategoryTx) Count() int64 {
+func (a siteBelongsToEnvGroupTx) Count() int64 {
 	return a.tx.Count()
 }
 

+ 83 - 1
query/streams.gen.go

@@ -34,7 +34,13 @@ func newStream(db *gorm.DB, opts ...gen.DOOption) stream {
 	_stream.DeletedAt = field.NewField(tableName, "deleted_at")
 	_stream.Path = field.NewString(tableName, "path")
 	_stream.Advanced = field.NewBool(tableName, "advanced")
+	_stream.EnvGroupID = field.NewUint64(tableName, "env_group_id")
 	_stream.SyncNodeIDs = field.NewField(tableName, "sync_node_ids")
+	_stream.EnvGroup = streamBelongsToEnvGroup{
+		db: db.Session(&gorm.Session{}),
+
+		RelationField: field.NewRelation("EnvGroup", "model.EnvGroup"),
+	}
 
 	_stream.fillFieldMap()
 
@@ -51,7 +57,9 @@ type stream struct {
 	DeletedAt   field.Field
 	Path        field.String
 	Advanced    field.Bool
+	EnvGroupID  field.Uint64
 	SyncNodeIDs field.Field
+	EnvGroup    streamBelongsToEnvGroup
 
 	fieldMap map[string]field.Expr
 }
@@ -74,6 +82,7 @@ func (s *stream) updateTableName(table string) *stream {
 	s.DeletedAt = field.NewField(table, "deleted_at")
 	s.Path = field.NewString(table, "path")
 	s.Advanced = field.NewBool(table, "advanced")
+	s.EnvGroupID = field.NewUint64(table, "env_group_id")
 	s.SyncNodeIDs = field.NewField(table, "sync_node_ids")
 
 	s.fillFieldMap()
@@ -91,14 +100,16 @@ func (s *stream) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
 }
 
 func (s *stream) fillFieldMap() {
-	s.fieldMap = make(map[string]field.Expr, 7)
+	s.fieldMap = make(map[string]field.Expr, 9)
 	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["path"] = s.Path
 	s.fieldMap["advanced"] = s.Advanced
+	s.fieldMap["env_group_id"] = s.EnvGroupID
 	s.fieldMap["sync_node_ids"] = s.SyncNodeIDs
+
 }
 
 func (s stream) clone(db *gorm.DB) stream {
@@ -111,6 +122,77 @@ func (s stream) replaceDB(db *gorm.DB) stream {
 	return s
 }
 
+type streamBelongsToEnvGroup struct {
+	db *gorm.DB
+
+	field.RelationField
+}
+
+func (a streamBelongsToEnvGroup) Where(conds ...field.Expr) *streamBelongsToEnvGroup {
+	if len(conds) == 0 {
+		return &a
+	}
+
+	exprs := make([]clause.Expression, 0, len(conds))
+	for _, cond := range conds {
+		exprs = append(exprs, cond.BeCond().(clause.Expression))
+	}
+	a.db = a.db.Clauses(clause.Where{Exprs: exprs})
+	return &a
+}
+
+func (a streamBelongsToEnvGroup) WithContext(ctx context.Context) *streamBelongsToEnvGroup {
+	a.db = a.db.WithContext(ctx)
+	return &a
+}
+
+func (a streamBelongsToEnvGroup) Session(session *gorm.Session) *streamBelongsToEnvGroup {
+	a.db = a.db.Session(session)
+	return &a
+}
+
+func (a streamBelongsToEnvGroup) Model(m *model.Stream) *streamBelongsToEnvGroupTx {
+	return &streamBelongsToEnvGroupTx{a.db.Model(m).Association(a.Name())}
+}
+
+type streamBelongsToEnvGroupTx struct{ tx *gorm.Association }
+
+func (a streamBelongsToEnvGroupTx) Find() (result *model.EnvGroup, err error) {
+	return result, a.tx.Find(&result)
+}
+
+func (a streamBelongsToEnvGroupTx) Append(values ...*model.EnvGroup) (err error) {
+	targetValues := make([]interface{}, len(values))
+	for i, v := range values {
+		targetValues[i] = v
+	}
+	return a.tx.Append(targetValues...)
+}
+
+func (a streamBelongsToEnvGroupTx) Replace(values ...*model.EnvGroup) (err error) {
+	targetValues := make([]interface{}, len(values))
+	for i, v := range values {
+		targetValues[i] = v
+	}
+	return a.tx.Replace(targetValues...)
+}
+
+func (a streamBelongsToEnvGroupTx) Delete(values ...*model.EnvGroup) (err error) {
+	targetValues := make([]interface{}, len(values))
+	for i, v := range values {
+		targetValues[i] = v
+	}
+	return a.tx.Delete(targetValues...)
+}
+
+func (a streamBelongsToEnvGroupTx) Clear() error {
+	return a.tx.Clear()
+}
+
+func (a streamBelongsToEnvGroupTx) Count() int64 {
+	return a.tx.Count()
+}
+
 type streamDo struct{ gen.DO }
 
 // FirstByID Where("id=@id")

+ 1 - 1
query/auths.gen.go → query/users.gen.go

@@ -142,7 +142,7 @@ func (u userDo) DeleteByID(id uint64) (err error) {
 
 	var generateSQL strings.Builder
 	params = append(params, id)
-	generateSQL.WriteString("update auths set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
+	generateSQL.WriteString("update users set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
 
 	var executeSQL *gorm.DB
 	executeSQL = u.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert

+ 0 - 1
router/routers.go

@@ -59,7 +59,6 @@ func InitRouter() {
 			analytic.InitRouter(g)
 			user.InitManageUserRouter(g)
 			nginx.InitRouter(g)
-			sites.InitCategoryRouter(g)
 			sites.InitRouter(g)
 			streams.InitRouter(g)
 			config.InitRouter(g)

Some files were not shown because too many files changed in this diff