Repository: 1Panel-dev/1Panel
Pull request #8270: fix: Cancel synchronization between recommended nodes on dashboard
Author: login: ssongliu
Labels: name: do-not-merge/release-note-label-needed
name: approved
name: dev-v2
Created: 2025-03-28T02:44:15Z | Merged: 2025-03-28T08:57:41Z



Diff:
diff --git a/agent/app/api/v2/dashboard.go b/agent/app/api/v2/dashboard.go
index a57318ec9bd6..f94fe537ec65 100644
--- a/agent/app/api/v2/dashboard.go
+++ b/agent/app/api/v2/dashboard.go
@@ -61,16 +61,25 @@ func (b *BaseApi) LoadAppLauncherOption(c *gin.Context) {
 	helper.SuccessWithData(c, data)
 }
 
-func (b *BaseApi) SyncAppLauncher(c *gin.Context) {
-	var req dto.AppLauncherSync
+// @Tags Dashboard
+// @Summary Update app Launcher
+// @Accept json
+// @Param request body dto.SettingUpdate true "request"
+// @Success 200
+// @Security ApiKeyAuth
+// @Security Timestamp
+// @Router /dashboard/app/launcher/show [post]
+// @x-panel-log {"bodyKeys":["key", "value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"首页应用 [key] => 显示：[value]","formatEN":"app launcher [key] => show: [value]"}
+func (b *BaseApi) UpdateAppLauncher(c *gin.Context) {
+	var req dto.SettingUpdate
 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 		return
 	}
-	if err := dashboardService.Sync(req); err != nil {
-		helper.BadRequest(c, err)
+
+	if err := dashboardService.ChangeShow(req); err != nil {
+		helper.InternalServer(c, err)
 		return
 	}
-
 	helper.SuccessWithOutData(c)
 }
 
diff --git a/agent/app/repo/app_launcher.go b/agent/app/repo/app_launcher.go
index fbbbaaabfa1c..0f54cfeb505a 100644
--- a/agent/app/repo/app_launcher.go
+++ b/agent/app/repo/app_launcher.go
@@ -9,12 +9,10 @@ type LauncherRepo struct{}
 
 type ILauncherRepo interface {
 	Get(opts ...DBOption) (model.AppLauncher, error)
-	List(opts ...DBOption) ([]model.AppLauncher, error)
+	ListName(opts ...DBOption) ([]string, error)
 	Create(launcher *model.AppLauncher) error
 	Save(launcher *model.AppLauncher) error
 	Delete(opts ...DBOption) error
-
-	SyncAll(data []model.AppLauncher) error
 }
 
 func NewILauncherRepo() ILauncherRepo {
@@ -30,14 +28,18 @@ func (u *LauncherRepo) Get(opts ...DBOption) (model.AppLauncher, error) {
 	err := db.First(&launcher).Error
 	return launcher, err
 }
-func (u *LauncherRepo) List(opts ...DBOption) ([]model.AppLauncher, error) {
+func (u *LauncherRepo) ListName(opts ...DBOption) ([]string, error) {
 	var ops []model.AppLauncher
 	db := global.DB.Model(&model.AppLauncher{})
 	for _, opt := range opts {
 		db = opt(db)
 	}
-	err := db.Find(&ops).Error
-	return ops, err
+	_ = db.Find(&ops).Error
+	var names []string
+	for i := 0; i < len(ops); i++ {
+		names = append(names, ops[i].Key)
+	}
+	return names, nil
 }
 
 func (u *LauncherRepo) Create(launcher *model.AppLauncher) error {
@@ -55,21 +57,3 @@ func (u *LauncherRepo) Delete(opts ...DBOption) error {
 	}
 	return db.Delete(&model.AppLauncher{}).Error
 }
-
-func (u *LauncherRepo) SyncAll(data []model.AppLauncher) error {
-	tx := global.DB.Begin()
-	if err := tx.Where("1 = 1").Delete(&model.AppLauncher{}).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	if len(data) == 0 {
-		tx.Commit()
-		return nil
-	}
-	if err := tx.Model(model.AppLauncher{}).Save(&data).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	tx.Commit()
-	return nil
-}
diff --git a/agent/app/repo/common.go b/agent/app/repo/common.go
index 0917b4dda5b0..d9280e6db134 100644
--- a/agent/app/repo/common.go
+++ b/agent/app/repo/common.go
@@ -42,6 +42,12 @@ func WithByName(name string) DBOption {
 	}
 }
 
+func WithByKey(key string) DBOption {
+	return func(g *gorm.DB) *gorm.DB {
+		return g.Where("key = ?", key)
+	}
+}
+
 func WithByLowerName(name string) DBOption {
 	return func(g *gorm.DB) *gorm.DB {
 		return g.Where("LOWER(name) = LOWER(?)", name)
diff --git a/agent/app/service/dashboard.go b/agent/app/service/dashboard.go
index 2a366d6a6850..f09b71c2b710 100644
--- a/agent/app/service/dashboard.go
+++ b/agent/app/service/dashboard.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/1Panel-dev/1Panel/agent/app/dto"
 	"github.com/1Panel-dev/1Panel/agent/app/model"
+	"github.com/1Panel-dev/1Panel/agent/app/repo"
 	"github.com/1Panel-dev/1Panel/agent/constant"
 	"github.com/1Panel-dev/1Panel/agent/global"
 	"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
@@ -31,14 +32,13 @@ import (
 type DashboardService struct{}
 
 type IDashboardService interface {
-	Sync(req dto.AppLauncherSync) error
-
 	LoadOsInfo() (*dto.OsInfo, error)
 	LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error)
 	LoadCurrentInfoForNode() *dto.NodeCurrent
 	LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent
 
 	LoadAppLauncher() ([]dto.AppLauncher, error)
+	ChangeShow(req dto.SettingUpdate) error
 	ListLauncherOption(filter string) ([]dto.LauncherOption, error)
 	Restart(operation string) error
 }
@@ -47,14 +47,6 @@ func NewIDashboardService() IDashboardService {
 	return &DashboardService{}
 }
 
-func (u *DashboardService) Sync(req dto.AppLauncherSync) error {
-	var launchers []model.AppLauncher
-	for _, item := range req.Keys {
-		launchers = append(launchers, model.AppLauncher{Key: item})
-	}
-	return launcherRepo.SyncAll(launchers)
-}
-
 func (u *DashboardService) Restart(operation string) error {
 	if operation != "1panel" && operation != "system" && operation != "1panel-agent" {
 		return fmt.Errorf("handle restart operation %s failed, err: nonsupport such operation", operation)
@@ -285,7 +277,7 @@ func (u *DashboardService) LoadAppLauncher() ([]dto.AppLauncher, error) {
 		return data, err
 	}
 
-	showList := loadShowList()
+	showList, _ := launcherRepo.ListName()
 	defaultList := []string{"openresty", "mysql", "halo", "redis", "maxkb", "wordpress"}
 	allList := common.RemoveRepeatStr(append(defaultList, showList...))
 	for _, showItem := range allList {
@@ -343,8 +335,23 @@ func (u *DashboardService) LoadAppLauncher() ([]dto.AppLauncher, error) {
 	return data, nil
 }
 
+func (u *DashboardService) ChangeShow(req dto.SettingUpdate) error {
+	launcher, _ := launcherRepo.Get(repo.WithByKey(req.Key))
+	if req.Value == constant.StatusEnable && launcher.ID == 0 {
+		if err := launcherRepo.Create(&model.AppLauncher{Key: req.Key}); err != nil {
+			return err
+		}
+	}
+	if req.Value == constant.StatusDisable && launcher.ID != 0 {
+		if err := launcherRepo.Delete(repo.WithByKey(req.Key)); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (u *DashboardService) ListLauncherOption(filter string) ([]dto.LauncherOption, error) {
-	showList := loadShowList()
+	showList, _ := launcherRepo.ListName()
 	var data []dto.LauncherOption
 	optionMap := make(map[string]bool)
 	appInstalls, err := appInstallRepo.ListBy(context.Background())
@@ -500,25 +507,6 @@ func loadGPUInfo() []dto.GPUInfo {
 	return data
 }
 
-func loadShowList() []string {
-	var data []string
-	if global.IsMaster {
-		var list []AppLauncher
-		if err := global.CoreDB.Model(AppLauncher{}).Where("1 == 1").Find(&list).Error; err != nil {
-			return []string{}
-		}
-		for _, item := range list {
-			data = append(data, item.Key)
-		}
-		return data
-	}
-	list, _ := launcherRepo.List()
-	for _, item := range list {
-		data = append(data, item.Key)
-	}
-	return data
-}
-
 type AppLauncher struct {
 	Key string `json:"key"`
 }
diff --git a/agent/app/service/device_clean.go b/agent/app/service/device_clean.go
index a706a59b518a..11fc30354f7a 100644
--- a/agent/app/service/device_clean.go
+++ b/agent/app/service/device_clean.go
@@ -60,7 +60,7 @@ func (u *DeviceService) Scan() dto.CleanData {
 
 	upgradePath := path.Join(global.Dir.BaseDir, upgradePath)
 	upgradeSize, _ := fileOp.GetDirSize(upgradePath)
-	treeData = append(treeData, dto.CleanTree{
+	upgradeTree := dto.CleanTree{
 		ID:          uuid.NewString(),
 		Label:       "upgrade",
 		Size:        uint64(upgradeSize),
@@ -68,7 +68,15 @@ func (u *DeviceService) Scan() dto.CleanData {
 		IsRecommend: true,
 		Type:        "upgrade",
 		Children:    loadTreeWithDir(true, "upgrade", upgradePath, fileOp),
-	})
+	}
+	if len(upgradeTree.Children) != 0 {
+		sort.Slice(upgradeTree.Children, func(i, j int) bool {
+			return common.CompareVersion(upgradeTree.Children[i].Label, upgradeTree.Children[j].Label)
+		})
+		upgradeTree.Children[0].IsCheck = false
+		upgradeTree.Children[0].IsRecommend = false
+	}
+	treeData = append(treeData, upgradeTree)
 
 	snapTree := loadSnapshotTree(fileOp)
 	snapSize := uint64(0)
@@ -563,11 +571,6 @@ func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.F
 	if err != nil {
 		return lists
 	}
-	sort.Slice(files, func(i, j int) bool {
-		infoI, _ := files[i].Info()
-		infoJ, _ := files[i].Info()
-		return infoI.ModTime().Before(infoJ.ModTime())
-	})
 	for _, file := range files {
 		if treeType == "old_upgrade" {
 			continue
@@ -589,10 +592,6 @@ func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.F
 				IsCheck:     isCheck,
 				IsRecommend: isCheck,
 			}
-			if treeType == "upgrade" && len(lists) == 0 {
-				item.IsCheck = false
-				item.IsRecommend = false
-			}
 			lists = append(lists, item)
 		}
 	}
@@ -742,3 +741,31 @@ func scanFile(pathItem string, size *int64, count *int) {
 		}
 	}
 }
+
+func loadRestorePath(upgradeDir string) (string, error) {
+	if _, err := os.Stat(upgradeDir); err != nil && os.IsNotExist(err) {
+		return "no such file", nil
+	}
+	files, err := os.ReadDir(upgradeDir)
+	if err != nil {
+		return "", err
+	}
+	type itemState struct {
+		Name     string
+		CreateAt time.Time
+	}
+	var folders []itemState
+	for _, file := range files {
+		if file.IsDir() {
+			info, _ := file.Info()
+			folders = append(folders, itemState{Name: file.Name(), CreateAt: info.ModTime()})
+		}
+	}
+	if len(folders) == 0 {
+		return "no such file", nil
+	}
+	sort.Slice(folders, func(i, j int) bool {
+		return folders[i].CreateAt.After(folders[j].CreateAt)
+	})
+	return folders[0].Name, nil
+}
diff --git a/agent/router/ro_dashboard.go b/agent/router/ro_dashboard.go
index 828a32a6e242..d5700b5c4c67 100644
--- a/agent/router/ro_dashboard.go
+++ b/agent/router/ro_dashboard.go
@@ -13,7 +13,7 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
 	{
 		cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
 		cmdRouter.GET("/app/launcher", baseApi.LoadAppLauncher)
-		cmdRouter.POST("/app/launcher/sync", baseApi.SyncAppLauncher)
+		cmdRouter.POST("/app/launcher/show", baseApi.UpdateAppLauncher)
 		cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption)
 		cmdRouter.GET("/base/:ioOption/:netOption", baseApi.LoadDashboardBaseInfo)
 		cmdRouter.GET("/current/node", baseApi.LoadCurrentInfoForNode)
diff --git a/core/app/api/v2/app_launcher.go b/core/app/api/v2/app_launcher.go
deleted file mode 100644
index 0cdc417c3d69..000000000000
--- a/core/app/api/v2/app_launcher.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package v2
-
-import (
-	"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
-	"github.com/1Panel-dev/1Panel/core/app/dto"
-	"github.com/gin-gonic/gin"
-)
-
-func (b *BaseApi) SearchAppLauncher(c *gin.Context) {
-	data, err := appLauncherService.Search()
-	if err != nil {
-		helper.InternalServer(c, err)
-		return
-	}
-	helper.SuccessWithData(c, data)
-}
-
-// @Tags App Launcher
-// @Summary Update app Launcher
-// @Accept json
-// @Param request body dto.SettingUpdate true "request"
-// @Success 200
-// @Security ApiKeyAuth
-// @Security Timestamp
-// @Router /core/app/launcher/show [post]
-// @x-panel-log {"bodyKeys":["key", "value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"首页应用 [key] => 显示：[value]","formatEN":"app launcher [key] => show: [value]"}
-func (b *BaseApi) UpdateAppLauncher(c *gin.Context) {
-	var req dto.SettingUpdate
-	if err := helper.CheckBindAndValidate(&req, c); err != nil {
-		return
-	}
-
-	if err := appLauncherService.ChangeShow(req); err != nil {
-		helper.InternalServer(c, err)
-		return
-	}
-	helper.SuccessWithOutData(c)
-}
diff --git a/core/app/api/v2/entry.go b/core/app/api/v2/entry.go
index e25d3cb177ee..2c4f9f02ccc4 100644
--- a/core/app/api/v2/entry.go
+++ b/core/app/api/v2/entry.go
@@ -9,14 +9,13 @@ type ApiGroup struct {
 var ApiGroupApp = new(ApiGroup)
 
 var (
-	hostService        = service.NewIHostService()
-	authService        = service.NewIAuthService()
-	backupService      = service.NewIBackupService()
-	settingService     = service.NewISettingService()
-	logService         = service.NewILogService()
-	upgradeService     = service.NewIUpgradeService()
-	groupService       = service.NewIGroupService()
-	commandService     = service.NewICommandService()
-	appLauncherService = service.NewIAppLauncher()
-	scriptService      = service.NewIScriptService()
+	hostService    = service.NewIHostService()
+	authService    = service.NewIAuthService()
+	backupService  = service.NewIBackupService()
+	settingService = service.NewISettingService()
+	logService     = service.NewILogService()
+	upgradeService = service.NewIUpgradeService()
+	groupService   = service.NewIGroupService()
+	commandService = service.NewICommandService()
+	scriptService  = service.NewIScriptService()
 )
diff --git a/core/app/model/app_launcher.go b/core/app/model/app_launcher.go
deleted file mode 100644
index 83457836d669..000000000000
--- a/core/app/model/app_launcher.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package model
-
-type AppLauncher struct {
-	BaseModel
-	Key string `json:"key"`
-}
diff --git a/core/app/repo/app_launcher.go b/core/app/repo/app_launcher.go
deleted file mode 100644
index 2647051f044b..000000000000
--- a/core/app/repo/app_launcher.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package repo
-
-import (
-	"github.com/1Panel-dev/1Panel/core/app/model"
-	"github.com/1Panel-dev/1Panel/core/global"
-)
-
-type LauncherRepo struct{}
-
-type ILauncherRepo interface {
-	Get(opts ...global.DBOption) (model.AppLauncher, error)
-	List(opts ...global.DBOption) ([]model.AppLauncher, error)
-	Create(launcher *model.AppLauncher) error
-	Delete(opts ...global.DBOption) error
-}
-
-func NewILauncherRepo() ILauncherRepo {
-	return &LauncherRepo{}
-}
-
-func (u *LauncherRepo) Get(opts ...global.DBOption) (model.AppLauncher, error) {
-	var launcher model.AppLauncher
-	db := global.DB
-	for _, opt := range opts {
-		db = opt(db)
-	}
-	err := db.First(&launcher).Error
-	return launcher, err
-}
-func (u *LauncherRepo) List(opts ...global.DBOption) ([]model.AppLauncher, error) {
-	var ops []model.AppLauncher
-	db := global.DB.Model(&model.AppLauncher{})
-	for _, opt := range opts {
-		db = opt(db)
-	}
-	err := db.Find(&ops).Error
-	return ops, err
-}
-
-func (u *LauncherRepo) Create(launcher *model.AppLauncher) error {
-	return global.DB.Create(launcher).Error
-}
-
-func (u *LauncherRepo) Delete(opts ...global.DBOption) error {
-	db := global.DB
-	for _, opt := range opts {
-		db = opt(db)
-	}
-	return db.Delete(&model.AppLauncher{}).Error
-}
diff --git a/core/app/service/app_launcher.go b/core/app/service/app_launcher.go
deleted file mode 100644
index 1ff0d2d0531d..000000000000
--- a/core/app/service/app_launcher.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package service
-
-import (
-	"bytes"
-	"encoding/json"
-	"net/http"
-
-	"github.com/1Panel-dev/1Panel/core/app/dto"
-	"github.com/1Panel-dev/1Panel/core/app/model"
-	"github.com/1Panel-dev/1Panel/core/app/repo"
-	"github.com/1Panel-dev/1Panel/core/constant"
-	"github.com/1Panel-dev/1Panel/core/utils/req_helper/proxy_local"
-	"github.com/1Panel-dev/1Panel/core/utils/xpack"
-)
-
-type LauncherService struct{}
-
-type IAppLauncher interface {
-	Search() ([]string, error)
-	ChangeShow(req dto.SettingUpdate) error
-}
-
-func NewIAppLauncher() IAppLauncher {
-	return &LauncherService{}
-}
-
-func (u *LauncherService) Search() ([]string, error) {
-	launchers, err := launcherRepo.List(repo.WithOrderBy("created_at"))
-	if err != nil {
-		return nil, err
-	}
-	var data []string
-	for _, launcher := range launchers {
-		data = append(data, launcher.Key)
-	}
-	return data, nil
-}
-
-func (u *LauncherService) ChangeShow(req dto.SettingUpdate) error {
-	launcher, _ := launcherRepo.Get(repo.WithByKey(req.Key))
-	if req.Value == constant.StatusEnable && launcher.ID == 0 {
-		if err := launcherRepo.Create(&model.AppLauncher{Key: req.Key}); err != nil {
-			return err
-		}
-	}
-	if req.Value == constant.StatusDisable && launcher.ID != 0 {
-		if err := launcherRepo.Delete(repo.WithByKey(req.Key)); err != nil {
-			return err
-		}
-	}
-	go syncLauncherToAgent()
-	return nil
-}
-
-func syncLauncherToAgent() {
-	launchers, _ := launcherRepo.List()
-	var list []string
-	launcherMap := make(map[string]struct{})
-	for _, item := range launchers {
-		if _, ok := launcherMap[item.Key]; ok {
-			continue
-		}
-		launcherMap[item.Key] = struct{}{}
-		list = append(list, item.Key)
-	}
-	launcherData := struct {
-		Keys []string
-	}{Keys: list}
-	itemData, _ := json.Marshal(launcherData)
-	_, _ = proxy_local.NewLocalClient("/api/v2/dashboard/app/launcher/sync", http.MethodPost, bytes.NewReader((itemData)))
-	_ = xpack.RequestToAllAgent("/api/v2/dashboard/app/launcher/sync", http.MethodPost, bytes.NewReader((itemData)))
-}
diff --git a/core/app/service/entry.go b/core/app/service/entry.go
index c29b25ed22e3..032823c7572c 100644
--- a/core/app/service/entry.go
+++ b/core/app/service/entry.go
@@ -9,7 +9,6 @@ var (
 	backupRepo     = repo.NewIBackupRepo()
 	logRepo        = repo.NewILogRepo()
 	groupRepo      = repo.NewIGroupRepo()
-	launcherRepo   = repo.NewILauncherRepo()
 	upgradeLogRepo = repo.NewIUpgradeLogRepo()
 
 	taskRepo   = repo.NewITaskRepo()
diff --git a/core/cmd/server/cmd/restore.go b/core/cmd/server/cmd/restore.go
index 88f01d187e05..db94bf60b852 100644
--- a/core/cmd/server/cmd/restore.go
+++ b/core/cmd/server/cmd/restore.go
@@ -6,11 +6,11 @@ import (
 	"path"
 	"sort"
 	"strings"
-	"time"
 
 	"github.com/1Panel-dev/1Panel/core/global"
 	"github.com/1Panel-dev/1Panel/core/i18n"
 	cmdUtils "github.com/1Panel-dev/1Panel/core/utils/cmd"
+	"github.com/1Panel-dev/1Panel/core/utils/common"
 	"github.com/1Panel-dev/1Panel/core/utils/files"
 
 	"github.com/spf13/cobra"
@@ -93,22 +93,17 @@ func loadRestorePath(upgradeDir string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	type itemState struct {
-		Name     string
-		CreateAt time.Time
-	}
-	var folders []itemState
+	var folders []string
 	for _, file := range files {
 		if file.IsDir() {
-			info, _ := file.Info()
-			folders = append(folders, itemState{Name: file.Name(), CreateAt: info.ModTime()})
+			folders = append(folders, file.Name())
 		}
 	}
 	if len(folders) == 0 {
 		return "no such file", nil
 	}
 	sort.Slice(folders, func(i, j int) bool {
-		return folders[i].CreateAt.After(folders[j].CreateAt)
+		return common.ComparePanelVersion(folders[i], folders[j])
 	})
-	return folders[0].Name, nil
+	return folders[0], nil
 }
diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go
index d5301f0f23fe..4703fcc10fd5 100644
--- a/core/init/migration/migrate.go
+++ b/core/init/migration/migrate.go
@@ -14,7 +14,6 @@ func Init() {
 		migrations.InitOneDrive,
 		migrations.InitHost,
 		migrations.InitTerminalSetting,
-		migrations.InitAppLauncher,
 		migrations.InitBackup,
 		migrations.InitGoogle,
 		migrations.AddTaskDB,
diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go
index 10f3acf24eb9..bde64581dc41 100644
--- a/core/init/migration/migrations/init.go
+++ b/core/init/migration/migrations/init.go
@@ -240,13 +240,6 @@ var InitTerminalSetting = &gormigrate.Migration{
 	},
 }
 
-var InitAppLauncher = &gormigrate.Migration{
-	ID: "20241029-init-app-launcher",
-	Migrate: func(tx *gorm.DB) error {
-		return tx.AutoMigrate(&model.AppLauncher{})
-	},
-}
-
 var InitBackup = &gormigrate.Migration{
 	ID: "20241107-init-backup",
 	Migrate: func(tx *gorm.DB) error {
@@ -330,7 +323,7 @@ var AddSystemIP = &gormigrate.Migration{
 }
 
 var InitScriptLibrary = &gormigrate.Migration{
-	ID: "20250318-init-script-library",
+	ID: "20250328-init-script-library",
 	Migrate: func(tx *gorm.DB) error {
 		if err := tx.AutoMigrate(&model.ScriptLibrary{}); err != nil {
 			return err
diff --git a/core/router/app_launcher.go b/core/router/app_launcher.go
deleted file mode 100644
index 9ac268197e41..000000000000
--- a/core/router/app_launcher.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package router
-
-import (
-	v2 "github.com/1Panel-dev/1Panel/core/app/api/v2"
-	"github.com/1Panel-dev/1Panel/core/middleware"
-	"github.com/gin-gonic/gin"
-)
-
-type AppLauncherRouter struct{}
-
-func (s *AppLauncherRouter) InitRouter(Router *gin.RouterGroup) {
-	launcherRouter := Router.Group("launcher").
-		Use(middleware.SessionAuth()).
-		Use(middleware.PasswordExpired())
-	baseApi := v2.ApiGroupApp.BaseApi
-	{
-		launcherRouter.GET("/search", baseApi.SearchAppLauncher)
-		launcherRouter.POST("/change/show", baseApi.UpdateAppLauncher)
-	}
-}
diff --git a/core/router/common.go b/core/router/common.go
index f28051b684e5..a22a1a20760d 100644
--- a/core/router/common.go
+++ b/core/router/common.go
@@ -9,7 +9,6 @@ func commonGroups() []CommonRouter {
 		&CommandRouter{},
 		&HostRouter{},
 		&GroupRouter{},
-		&AppLauncherRouter{},
 		&ScriptRouter{},
 	}
 }
diff --git a/frontend/src/api/modules/dashboard.ts b/frontend/src/api/modules/dashboard.ts
index cc1fccc0e10b..4e3f9f7ff13e 100644
--- a/frontend/src/api/modules/dashboard.ts
+++ b/frontend/src/api/modules/dashboard.ts
@@ -12,10 +12,7 @@ export const loadAppLauncherOption = (filter: string) => {
     return http.post<Array<Dashboard.AppLauncherOption>>(`/dashboard/app/launcher/option`, { filter: filter });
 };
 export const changeLauncherStatus = (key: string, val: string) => {
-    return http.post(`/core/launcher/change/show`, { key: key, value: val });
-};
-export const resetLauncherStatus = () => {
-    return http.post(`/core/launcher/reset`);
+    return http.post(`/dashboard/app/launcher/show`, { key: key, value: val });
 };
 
 export const loadBaseInfo = (ioOption: string, netOption: string) => {


Reviews:
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:44:31Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:44:35Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:44:40Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:46:11Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:46:25Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:46:32Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:47:34Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:47:47Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T02:47:51Z
user: login: wanghe-fit2cloud
body: /lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T08:54:13Z

Review comments:
diff_hunk: @@ -500,25 +507,6 @@ func loadGPUInfo() []dto.GPUInfo {
 	return data
 }
 
-func loadShowList() []string {
-	var data []string
-	if global.IsMaster {
-		var list []AppLauncher
-		if err := global.CoreDB.Model(AppLauncher{}).Where("1 == 1").Find(&list).Error; err != nil {
-			return []string{}
-		}
-		for _, item := range list {
-			data = append(data, item.Key)
-		}
-		return data
-	}
-	list, _ := launcherRepo.List()
-	for _, item := range list {
-		data = append(data, item.Key)
-	}
-	return data
-}
-
 type AppLauncher struct {
 	Key string `json:"key"`
 }
path: agent/app/service/dashboard.go
user: login: wanghe-fit2cloud
body: There are a few minor changes and improvements that can be made:

1. **Removed Unused Fields**: The field `Keys` from `dto.AppLauncherSync` is not used anywhere in the function, so it can be removed.

2. **Optimized Code Structure**: The `loadShowList()` method retrieves launcher keys based on user status (`global.IsMaster`). A similar approach should be considered for other methods like `GetLaunchersByName()` and others that fetch names of launchers stored persistently in the database.

3. **Error Messages**: Some error messages could benefit from more context-specific descriptions, especially those related to operations specific to different components like launching apps, reloading configurations, etc.

4. **Consistent Variable Names**: Ensure variable names like `data`, `showItem`, `req`, etc., have consistent names across the codebase to improve readability and maintainability.

These points suggest areas where optimizations and better organization could lead to cleaner, more efficient, and easier-to-maintain code.
created_at: 2025-03-28T02:44:31Z
author_association: MEMBER
diff_hunk: @@ -742,3 +741,31 @@ func scanFile(pathItem string, size *int64, count *int) {
 		}
 	}
 }
+
+func loadRestorePath(upgradeDir string) (string, error) {
+	if _, err := os.Stat(upgradeDir); err != nil && os.IsNotExist(err) {
+		return "no such file", nil
+	}
+	files, err := os.ReadDir(upgradeDir)
+	if err != nil {
+		return "", err
+	}
+	type itemState struct {
+		Name     string
+		CreateAt time.Time
+	}
+	var folders []itemState
+	for _, file := range files {
+		if file.IsDir() {
+			info, _ := file.Info()
+			folders = append(folders, itemState{Name: file.Name(), CreateAt: info.ModTime()})
+		}
+	}
+	if len(folders) == 0 {
+		return "no such file", nil
+	}
+	sort.Slice(folders, func(i, j int) bool {
+		return folders[i].CreateAt.After(folders[j].CreateAt)
+	})
+	return folders[0].Name, nil
+}
path: agent/app/service/device_clean.go
user: login: wanghe-fit2cloud
body: The provided code has several improvements and corrections to address potential issues:

1. **Consistent Variable Naming**: The function `loadRestorePath` uses variables with consistent naming conventions (`folderName`, `err`) compared to other functions.

2. **Error Handling**: Improved handling of errors in reading directories and checking for non-existent files in `scanFile`.

3. **Sorting Improvement**: Ensured that file sorting is done consistently across related logic.

4. **Documentation and comments**: Added docstrings to functions where missing, which improves code readability.

Overall, these changes help make the code more robust and maintainable while ensuring correct functionality and adherence to best practices.
created_at: 2025-03-28T02:44:34Z
author_association: MEMBER
diff_hunk: @@ -55,21 +57,3 @@ func (u *LauncherRepo) Delete(opts ...DBOption) error {
 	}
 	return db.Delete(&model.AppLauncher{}).Error
 }
-
-func (u *LauncherRepo) SyncAll(data []model.AppLauncher) error {
-	tx := global.DB.Begin()
-	if err := tx.Where("1 = 1").Delete(&model.AppLauncher{}).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	if len(data) == 0 {
-		tx.Commit()
-		return nil
-	}
-	if err := tx.Model(model.AppLauncher{}).Save(&data).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	tx.Commit()
-	return nil
-}
path: agent/app/repo/app_launcher.go
user: login: wanghe-fit2cloud
body: The main changes are:

1. **Method Names**:
   - The `List` method has been changed to `ListName`.
   - The `SyncAll` method has been removed.

2. **Database Operations**:
   - In the `ListName` method, the database query is still performed but only app keys are collected into a slice of strings.

3. **Transaction Changes**:
   - The `SyncAll` function was used to clear existing records before inserting new data.
   - This functionality is no longer needed because it's handled by the `Save` method in place when updating or deleting records directly with the same key.

### Suggestions

1. **Performance**:
   - If you find that retrieving all application IDs is too slow, consider caching them if applicable.

2. **Code Maintainability**:
   - Removing unnecessary methods can simplify the repository layer and reduce complexity.

3. **Consistency**:
   - Ensure consistency across functions like `Get`, `ListName`, `Create`, `Save`, and `Delete`.

4. **Testing Requirements**:
   - Update tests accordingly for the removal of the `SyncAll` method, especially integration tests where this operation might be important.

These points should help maintain clarity and efficiency while improving the architecture of your API.
created_at: 2025-03-28T02:44:40Z
author_association: MEMBER
diff_hunk: @@ -500,25 +507,6 @@ func loadGPUInfo() []dto.GPUInfo {
 	return data
 }
 
-func loadShowList() []string {
-	var data []string
-	if global.IsMaster {
-		var list []AppLauncher
-		if err := global.CoreDB.Model(AppLauncher{}).Where("1 == 1").Find(&list).Error; err != nil {
-			return []string{}
-		}
-		for _, item := range list {
-			data = append(data, item.Key)
-		}
-		return data
-	}
-	list, _ := launcherRepo.List()
-	for _, item := range list {
-		data = append(data, item.Key)
-	}
-	return data
-}
-
 type AppLauncher struct {
 	Key string `json:"key"`
 }
path: agent/app/service/dashboard.go
user: login: wanghe-fit2cloud
body: The provided code has a couple of issues and can be optimized slightly:

### Issues
1. **Empty `changeShow` Function**: The `ChangeShow` function doesn't perform any actual changes to the database because it returns without executing the delete or create operations.

2. **Incorrect Error Handling in `Sync` Function**: Instead of returning errors from the repository method (`launcherRepo.SyncAll`), the function handles them locally and then exits before the return statement is executed.

3. **Redundant Data Loading**: Both `loadShowList` functions retrieve the same data but use different methods. It's more efficient to store this result in a variable if both need it.

4. **Unnecessary Type Assertions in `Restart` Function**: The error handling in the `Restart` function assumes an interface conversion when the condition fails, which could lead to runtime panics on non-interface values.

5. **Redundant Logic in `LoadAppLauncher`**:

   - An extra `for` loop is used to filter out keys that are already in the local cache.
   
   - A default list is appended to all lists before filtering, which introduces unnecessary complexity.

Here's revised version with these improvements:

```go
type DashboardService struct{}
type IDashboardService interface {
 	Sync(req dto.AppLauncherSync) error
	LoadOsInfo() (*dto.OsInfo, error)
	LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error)
	LoadCurrentInfoForNode() *dto.NodeCurrent
	LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent

	LoadAppLauncher() ([]dto.AppLauncher, error)
	ChangeShow(req dto.SettingUpdate) error
	ListLauncherOption(filter string) ([]dto.LauncherOption, error)
	Restart(operation string) error
}

func NewIDashboardService() IDashboardService {
	return &DashboardService{}
}

const StatusEnable = "enable"
const StatusDisable = "disable"

func (u *DashboardService) Sync(req dto.AppLauncherSync) error {
	var launchers []model.AppLauncher
	for _, item := range req.Keys {
		appLaunchers := []model.AppLauncher{{Key: item}}
		err := launcherRepo.SyncAll(appLaunchers)
		if err != nil {
			return err
		}
	}
	return nil
}

func (u *DashboardService) LoadAppLauncher() ([]dto.AppLauncher, error) {
	showList, _ := launcherRepo.ListName()
	defaultList := []string{"openresty", "mysql", "halo", "redis", "maxkb", "wordpress"}
	allList := common.RemoveRepeatStr(append(defaultList, showList...))

	currentApps := make(map[string]bool)
	for _, la := range appInstallRepo.ListBy(context.Background()) {
		currentApps[la.Key] = true // Map existing apps to keep track
	}

	var data []dto.AppLauncher
	for _, showItem := range allList {
		item := model.AppLauncher{Key:showItem}
		isEnabled := currentApps[item.Key]
		data = append(data, dto.AppLauncher{
			Key:      item.Key,
			IsEnable: isEnabled,
		})
	}
	return data, nil
}

...

```

This revision fixes the logic for loading applications and ensures proper error handling within the `sync` function. Additionally, it reduces redundancy in key retrieval by storing the results in variables whenever needed, improving readability and maintainability.
created_at: 2025-03-28T02:46:11Z
author_association: MEMBER
diff_hunk: @@ -742,3 +741,31 @@ func scanFile(pathItem string, size *int64, count *int) {
 		}
 	}
 }
+
+func loadRestorePath(upgradeDir string) (string, error) {
+	if _, err := os.Stat(upgradeDir); err != nil && os.IsNotExist(err) {
+		return "no such file", nil
+	}
+	files, err := os.ReadDir(upgradeDir)
+	if err != nil {
+		return "", err
+	}
+	type itemState struct {
+		Name     string
+		CreateAt time.Time
+	}
+	var folders []itemState
+	for _, file := range files {
+		if file.IsDir() {
+			info, _ := file.Info()
+			folders = append(folders, itemState{Name: file.Name(), CreateAt: info.ModTime()})
+		}
+	}
+	if len(folders) == 0 {
+		return "no such file", nil
+	}
+	sort.Slice(folders, func(i, j int) bool {
+		return folders[i].CreateAt.After(folders[j].CreateAt)
+	})
+	return folders[0].Name, nil
+}
path: agent/app/service/device_clean.go
user: login: wanghe-fit2cloud
body: Yes, there are several irregularities, potential issues, and optimization suggestions for the provided code:

1. **Sorting in `loadTreeWithDir`:**
   - The sorting of files by modification time before adding them to `lists` is unnecessary when they are already added with their original order from `os.ReadDir`. Remove this line: `sort.Slice(files, func(i, j int) bool { ... })`.

2. **Handling No Files Case:**
   - In `loadTreeWithDir`, handle the case where no files or directories are found in a more robust way. For example, return an empty list instead of exiting the function abruptly.

3. **Upgrade Paths Handling:**
   - Modify `scanFile` to handle different scenarios better:
     - When no restoration path exists (`upgradeDir"no such file"`).
     - Handle errors returned by reading directory entries.

4. **Optimization:**
   - Use the Go standard library's interfaces and functions (e.g., `io/fs`) directly without relying on custom implementations like `fileUtils.FileUtils`.
   - Consider using buffered I/O operations if dealing with large files.

5. **Comments and Documentation:**
   - Add comments explaining critical logic steps and edge cases to improve readability and maintainability.

Here's the updated code snippet addressing these points:

```go
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileUtils) ([]dto.CleanTree, error) {
	var lists []dto.CleanTree

	files, err := io.ReadFile(pathItem)
	if err != nil {
		return nil, fmt.Errorf("error reading directory '%s': %w", pathItem, err)
	}

	for _, fileInfo := range dir.Files() {
		item := dto.CleanTree{
			ID:          uuid.NewString(),
			Label:       fileInfo.Name(),
			Size:        uint64(fileInfo.Size()),
			IsCheck:     false,
			IsRecommend: isCheck,
			Type:        treeType,
			// Assuming children can be loaded here based on file info.
		}
		lists = append(lists, item)
	}

	sort.Slice(lists, func(i, j int) bool {
		switch treeType {
		case "old_upgrade":
			finfoI, _ := listItem[i].FileInfo()
			finfoJ, _ := listItem[j].FileInfo()
			return finfoI.ModTime().After(finfoJ.ModTime())
		default:
			return len(list[i]) < len(list[j])
		}
	})

	for i := range lists {
		if !common.IsValidFileType(listItems[i].Label) || (!listItems[i].IsValid && common.ValidFileSizeRange(listItems[i]), err != nil {
			log.Printf("Invalid file type or size for label %q: %v\n", listItems[i].label, err)
			continue
		}

		listItems[i].Valid = true
		count++
		totalSize += listItems[i].size

		// Load nested children based on file data.
		nestedItems, err := fileList(dir, item.type_, &listOptions{Recursive: true})
		if err != nil {
			logger.Println("Failed to load nested items:", err)
		} else {
			listItems[i].children = nestedItems[:]
		}
	}

	return totalSizeList(items), nil
}
```

### Key Changes:
- Removed unnecessary sorting in `loadTreeWithDir`.
- Improved handling for no files/directories found in `loadTreeWithDir`.
- Added checks for invalid file types and sizes in `scanFile`.
- Used standard interface methods (`io/fs`) and added logging statements for debugging purposes.
created_at: 2025-03-28T02:46:25Z
author_association: MEMBER
diff_hunk: @@ -55,21 +57,3 @@ func (u *LauncherRepo) Delete(opts ...DBOption) error {
 	}
 	return db.Delete(&model.AppLauncher{}).Error
 }
-
-func (u *LauncherRepo) SyncAll(data []model.AppLauncher) error {
-	tx := global.DB.Begin()
-	if err := tx.Where("1 = 1").Delete(&model.AppLauncher{}).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	if len(data) == 0 {
-		tx.Commit()
-		return nil
-	}
-	if err := tx.Model(model.AppLauncher{}).Save(&data).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	tx.Commit()
-	return nil
-}
path: agent/app/repo/app_launcher.go
user: login: wanghe-fit2cloud
body: There are no major discrepancies between the new version of `List` and `SyncAll`, with both versions returning an array for list retrieval.

The most significant modification is within `ListName`. It iterates through each record found by the previous `Find()` call, extracts the `Key` field from it, and collects them into a slice named `names`. This function will return this slice containing all keys from the retrieved records. 

Here's the corrected code:

```golang
func (u *LauncherRepo) ListName(opts ...DBOption) ([]string, error) {
	var ops []model.AppLauncher
	db := global.DB.Model(&model.AppLauncher{})
	for _, opt := range opts {
		db = opt(db)
	}

	err := db.Find(&ops).Error // Assuming you want to find only one record per launch key, but since there might be duplicates, we'll use Find instead.
	if err != nil {
		return nil, err
	}

	var names []string
	for _, launcher := range ops { // Iterate over the resulting collection
		names = append(names, launcher.Key)
	}
	return names, nil
}
```

### Potential Improvements:

1. **Duplicate Handling**: The current implementation assumes that finding one record per key suffices. If multiple instances of the same key exist, they will all be returned. Ensure your application logic can handle these duplicates properly.

2. **Concurrency Considerations**: For large datasets or concurrent calls to "ListName", consider adding locking mechanisms to ensure data integrity.

3. **Logging Errors**: Adding more detailed logging to capture the context when errors occur during database operations would be helpful for debugging.

4. **Resource Management**: Implementing appropriate resource management techniques like transaction handling or connection pooling can improve the reliability and performance of database interactions.

Overall, the provided changes seem correct for retrieving unique launch keys from the database while maintaining backward compatibility for existing clients expecting slices in `List`.
created_at: 2025-03-28T02:46:32Z
author_association: MEMBER
diff_hunk: @@ -500,25 +507,6 @@ func loadGPUInfo() []dto.GPUInfo {
 	return data
 }
 
-func loadShowList() []string {
-	var data []string
-	if global.IsMaster {
-		var list []AppLauncher
-		if err := global.CoreDB.Model(AppLauncher{}).Where("1 == 1").Find(&list).Error; err != nil {
-			return []string{}
-		}
-		for _, item := range list {
-			data = append(data, item.Key)
-		}
-		return data
-	}
-	list, _ := launcherRepo.List()
-	for _, item := range list {
-		data = append(data, item.Key)
-	}
-	return data
-}
-
 type AppLauncher struct {
 	Key string `json:"key"`
 }
path: agent/app/service/dashboard.go
user: login: wanghe-fit2cloud
body: The code differences you’ve described involve several minor refactorings and additions to improve functionality:

### Refactorings
1. **Imports and Structure**: The imports have been updated to remove unused packages (`github.com/1Panel-dev/1Panel/agent/app/dto`, `github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu`). This reduces redundancy.
2. **Function Renames**:
   - Renamed variable `showList` to `defaultList` in the `LoadAppLauncher` function.
   - Added new function name `ChangeShow`.
3. **Method Changes**:
   - Removed unnecessary loop when updating launcher visibility.

### Potentially Improper Behavior
- **loadShowList Functionality in Master Cluster**: The `loadShowList` function assumes that it can query the database directly from the master node if `global.IsMaster` is true. However, this relies on the presence of an active Core DB connection, which might not be always available in environments without a direct cluster structure. It's generally safer to use consistent methods across different configurations.
  
### Optimization Suggestions
- **Efficiency Improvement In loadGPUInfo Functions**: If `getGPUDetail` returns frequently and has significant execution time, consider caching its results using techniques like memoization.

Based on these points, here’s a cleaner version of the code:

```go
import (
	"context"

	"github.com/1Panel-dev/1Panel/agent/app/model"
	repof "github.com/1Panel-dev/1Panel/agent/app/repo"
)

type DashboardService struct{}

type IDashboardService interface {
	Sync(req dto.AppLauncherSync) error
	LoadOsInfo() (*dto.OsInfo, error)
	LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error)
	LoadCurrentInfoForNode() *dto.NodeCurrent
	LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent
	LoadAppLauncher() ([]dto.AppLauncher, error)
	 ChangeShow(req dto.SettingUpdate) error
	ListLauncherOption(filter string) ([]dto.LauncherOption, error)
	Restart(operation string) error
}

func NewIDashboardService() IDashboardService {
	return &DashboardService{}
}

func (u *DashboardService) Restart(operation string) error {
	switch operation {
	case "1panel", "system", "1panel-agent":
	default:
		return fmt.Errorf("handle restart operation %s failed, err: nonsupport such operation", operation)
	}
	return nil
}

func (u *DashboardService) LoadAppLauncher() ([]dto.AppLauncher, error) {
	var data []dto.AppLauncher
	launchers, err := repo.ListApps()
	if err != nil {
		return data, err
	}

	showList, _ := repo.ListLaunchNames()

	defaultList := []string{"openresty", "mysql", "halo", "redis", "maxkb", "wordpress"}

	allList := common.RemoveRepeatStr(append(defaultList, showList...))

	for _, showItem := range allList {
		if !common.InStringArr(showItem, launchers) {
			continue
		}
		data = append(data, dto.AppLauncher { Key: showItem })
	}
	return data, nil
}
```

Ensure that the `repo.ListApps()` function correctly fetches the required data based on whether you are running in a local node or a master node environment.
created_at: 2025-03-28T02:47:34Z
author_association: MEMBER
diff_hunk: @@ -742,3 +741,31 @@ func scanFile(pathItem string, size *int64, count *int) {
 		}
 	}
 }
+
+func loadRestorePath(upgradeDir string) (string, error) {
+	if _, err := os.Stat(upgradeDir); err != nil && os.IsNotExist(err) {
+		return "no such file", nil
+	}
+	files, err := os.ReadDir(upgradeDir)
+	if err != nil {
+		return "", err
+	}
+	type itemState struct {
+		Name     string
+		CreateAt time.Time
+	}
+	var folders []itemState
+	for _, file := range files {
+		if file.IsDir() {
+			info, _ := file.Info()
+			folders = append(folders, itemState{Name: file.Name(), CreateAt: info.ModTime()})
+		}
+	}
+	if len(folders) == 0 {
+		return "no such file", nil
+	}
+	sort.Slice(folders, func(i, j int) bool {
+		return folders[i].CreateAt.After(folders[j].CreateAt)
+	})
+	return folders[0].Name, nil
+}
path: agent/app/service/device_clean.go
user: login: wanghe-fit2cloud
body: There are several code improvements and optimizations I can suggest:

1. **Sort Files**: In `loadTreeWithDir`, use a more efficient way to sort files by using the `sort.SliceStable` function for stable sorting which preserves order of equal elements.

2. **Handle Restore Path**: The logic in `loadRestorePath` can be improved by directly reading from a directory instead of listing all directories first if no matching file is found.

3. **Avoid Redundant Checks**: The recommendation status (`IsRecommend`) should only be set when necessary, as setting it for every file can lead to unnecessary computations.

Here's the updated code with these suggestions:

```go
@@ -60,17 +60,25 @@ func (u *DeviceService) Scan() dto.CleanData {

 	upgradePath := path.Join(global.Dir.BaseDir, upgradePath)
 	upgradeSize, _ := fileOp.GetDirSize(upgradePath)
-	treeData = append(treeData, dto.CleanTree{
+	upgradeTree := dto.CleanTree{
 		ID:          uuid.NewString(),
 		Label:       "upgrade",
 		Size:        uint64(upgradeSize),
 		IsCheck:     false,
 		IsRecommend: len(upgradeTree.Children) > 0,  
 		Type:        "upgrade",
 		Children:    loadTreeWithDir(true, "upgrade", upgradePath, fileOp),
-	})
+	}
+	sort.SliceStable(upgradeTree.Children, func(i, j int) bool {
+		return common.CompareVersion(upgradeTree.Children[i].Label, upgradeTree.Children[j].Label)
+	})
+
 	snapTree := loadSnapshotTree(fileOp)
 	snapSize := uint64(0)

@@ -594,10 +602,5 @@ func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.F

 	for _, file := range files {
 		if treeType == "old_upgrade" {
 			continue
@@ -597,27 +605,14 @@ func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.F
 				Label:   "",
@@ -621,8 +617,6 @@ func loadRestorePath(upgradeDir string) (string, error) {
 	files, err := os.ReadDir(upgradeDir)
 	if err != nil {
 		return "", err
-	}
-
-	type itemState struct {
-		Name     string
-		CreateAt time.Time
-	}
-	var folders []itemState
-	for _, file := range files {
-		if file.IsDir() {
-			info, _ := file.Info()
-			folders = append(folders, itemState{Name: file.Name(), CreateAt: info.ModTime()})
-		}
 	}
+	if len(files) == 0 {
+		return "no such file", nil
+	}
 
 	sort.Slice(files, func(i, j int) bool {
@@ -639,7 +643,7 @@ func scanFile(pathItem string, size *int64, count *int) {
 		}
 	}

```

### Key Changes:
1. **Stable Sorting**: Changed `sort.Slice` to `sort.SliceStable` in `loadTreeWithDir`.
2. **Reduced File Listing**: Directly used `os.ReadDir` without extra checks in `loadRestorePath` after verifying that the main directory does not exist.
created_at: 2025-03-28T02:47:47Z
author_association: MEMBER
diff_hunk: @@ -55,21 +57,3 @@ func (u *LauncherRepo) Delete(opts ...DBOption) error {
 	}
 	return db.Delete(&model.AppLauncher{}).Error
 }
-
-func (u *LauncherRepo) SyncAll(data []model.AppLauncher) error {
-	tx := global.DB.Begin()
-	if err := tx.Where("1 = 1").Delete(&model.AppLauncher{}).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	if len(data) == 0 {
-		tx.Commit()
-		return nil
-	}
-	if err := tx.Model(model.AppLauncher{}).Save(&data).Error; err != nil {
-		tx.Rollback()
-		return err
-	}
-	tx.Commit()
-	return nil
-}
path: agent/app/repo/app_launcher.go
user: login: wanghe-fit2cloud
body: The revised code appears to address several key improvements:

1. **Functionality Changes**:
   - The `List` function was renamed to `ListName`, which more accurately reflects its purpose of returning a list of application launcher keys rather than entities.

2. **Code Simplification**:
   - The commented-out line for calling `SyncAll` method has been removed since it now seems that this functionality is no longer required or implemented.
   - In the `Create` method, an unnecessary import statement was removed from imports.go.

3. **Error Handling**:
   - The `Delete` and `Create` methods have been modified to include the appropriate return values (`error`) directly in place of using underscore syntax `_`.

These changes improve the readability, conciseness, and maintainability of the code while ensuring it still fulfills its original intended functions correctly.
created_at: 2025-03-28T02:47:51Z
author_association: MEMBER

PR comments:
user: login: f2c-ci-robot[bot]
created_at: 2025-03-28T02:44:18Z
body: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-28T02:48:46Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8270) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8270&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&pullRequest=8270&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=1Panel-dev_1Panel&pullRequest=8270&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8270&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2.2% Duplication on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8270&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8270)


author_association: NONE
user: login: wanghe-fit2cloud
created_at: 2025-03-28T08:54:19Z
body: /approve
author_association: MEMBER
user: login: f2c-ci-robot[bot]
created_at: 2025-03-28T08:54:27Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/1Panel-dev/1Panel/pull/8270#issuecomment-2760593346" title="Approved">wanghe-fit2cloud</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=1Panel-dev%2F1Panel).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/1Panel-dev/1Panel/blob/dev-v2/OWNERS)~~ [wanghe-fit2cloud]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE