diff --git a/backend/internal/ghost/ghost.go b/backend/internal/ghost/ghost.go index d99c74a..d4564b4 100644 --- a/backend/internal/ghost/ghost.go +++ b/backend/internal/ghost/ghost.go @@ -13,6 +13,7 @@ import ( // GhOstTask defines a gh-ost execution task type GhOstTask struct { Host string `json:"host"` + Port int `json:"port,omitempty"` User string `json:"user"` Password string `json:"password"` Database string `json:"database"` @@ -60,12 +61,18 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu args := []string{ "--host=" + task.Host, + fmt.Sprintf("--port=%d", task.Port), "--user=" + task.User, "--password=" + task.Password, "--database=" + task.Database, "--table=" + task.Table, "--alter=" + task.Alter, "--verbose", + "--skip-port-validation", + "--allow-on-master", + "--initially-drop-ghost-table", + "--initially-drop-old-table", + "--initially-drop-socket-file", } if task.ChunkSize > 0 { @@ -91,11 +98,11 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu } // Dry-Run vs Execute - if task.DryRun || !task.Execute { - args = append(args, "--dry-run") - } else { + // gh-ost defaults to noop (dry-run), only --execute actually applies changes + if task.Execute { args = append(args, "--execute") } + // always verbose for output cmd := exec.CommandContext(ctx, ghOstPath, args...) var stdout, stderr bytes.Buffer @@ -193,12 +200,18 @@ func (m *TaskManager) ListTasks() map[string]string { func buildArgs(task GhOstTask, ghOstPath string) []string { args := []string{ "--host=" + task.Host, + fmt.Sprintf("--port=%d", task.Port), "--user=" + task.User, "--password=" + task.Password, "--database=" + task.Database, "--table=" + task.Table, "--alter=" + task.Alter, "--verbose", + "--skip-port-validation", + "--allow-on-master", + "--initially-drop-ghost-table", + "--initially-drop-old-table", + "--initially-drop-socket-file", } if task.ChunkSize > 0 { @@ -223,9 +236,8 @@ func buildArgs(task GhOstTask, ghOstPath string) []string { args = append(args, "--postpone-cut-over-flag-file=/tmp/gh-ost-cutover.flag") } - if task.DryRun || !task.Execute { - args = append(args, "--dry-run") - } else { + // gh-ost defaults to noop, only --execute actually applies changes + if task.Execute { args = append(args, "--execute") } diff --git a/backend/internal/handler/ghost.go b/backend/internal/handler/ghost.go index 4f6f523..f06270e 100644 --- a/backend/internal/handler/ghost.go +++ b/backend/internal/handler/ghost.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" "time" @@ -13,15 +14,27 @@ import ( // POST /api/v1/ghost/dry-run func GhOstDryRun(c *gin.Context) { - var task ghost.GhOstTask - if err := c.ShouldBindJSON(&task); err != nil { + var req struct { + InstanceID int `json:"instance_id"` + Database string `json:"database"` + Table string `json:"table"` + Alter string `json:"alter"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) + return + } + + // Look up instance connection info + task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter) + if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task.DryRun = true task.Execute = false - result, err := ghost.RunGhOst(c.Request.Context(), task, config.C.GhOstPath) + result, err := ghost.RunGhOst(c.Request.Context(), *task, config.C.GhOstPath) if err != nil { c.JSON(http.StatusInternalServerError, result) return @@ -29,17 +42,51 @@ func GhOstDryRun(c *gin.Context) { c.JSON(http.StatusOK, result) } +// buildGhOstTask resolves instance_id to connection params +func buildGhOstTask(instanceID int, database, table, alter string) (*ghost.GhOstTask, error) { + var inst model.Instance + if err := model.DB.First(&inst, instanceID).Error; err != nil { + return nil, fmt.Errorf("instance %d not found", instanceID) + } + if inst.DBType != "mysql" { + return nil, fmt.Errorf("gh-ost only supports MySQL instances, got %s", inst.DBType) + } + // gh-ost needs separate host and port + host := inst.Host + port := inst.Port + return &ghost.GhOstTask{ + Host: host, + Port: port, + User: inst.Username, + Password: inst.Password, + Database: database, + Table: table, + Alter: alter, + }, nil +} + // POST /api/v1/ghost/execute func GhOstExecute(c *gin.Context) { - var task ghost.GhOstTask - if err := c.ShouldBindJSON(&task); err != nil { + var req struct { + InstanceID int `json:"instance_id"` + Database string `json:"database"` + Table string `json:"table"` + Alter string `json:"alter"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) + return + } + + task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter) + if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task.DryRun = false task.Execute = true - taskID := ghost.RunGhOstAsync(task, config.C.GhOstPath, nil) + taskID := ghost.RunGhOstAsync(*task, config.C.GhOstPath, nil) c.JSON(http.StatusAccepted, gin.H{ "task_id": taskID, "message": "gh-ost execution started", diff --git a/backend/internal/handler/preferences.go b/backend/internal/handler/preferences.go index 8884dbb..bf2cb66 100644 --- a/backend/internal/handler/preferences.go +++ b/backend/internal/handler/preferences.go @@ -43,26 +43,41 @@ func UpdateUserPreferences(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"detail": result.Error.Error()}) return } + + // Sync login_style and theme to SystemSetting so public endpoint picks them up + syncToSystemSetting := func(key, value string) { + var s model.SystemSetting + if model.DB.Where("`key` = ?", key).First(&s).Error == nil { + model.DB.Model(&s).Update("value", value) + } else { + model.DB.Create(&model.SystemSetting{ + Key: key, Value: value, ValueType: "string", + Group: "appearance", Label: key, SortOrder: 0, + }) + } + } + if v, ok := prefs["login_style"]; ok { + syncToSystemSetting("login_style", v) + } + if v, ok := prefs["theme"]; ok { + syncToSystemSetting("default_theme", v) + } + c.JSON(http.StatusOK, gin.H{"message": "Preferences updated", "preferences": prefs}) } // GET /api/v1/public/login-style (no auth required) func GetPublicLoginStyle(c *gin.Context) { - // Check system settings first - var setting model.SystemSetting - style := "split" // default - if model.DB.Where("`key` = ?", "login_style").First(&setting).Error == nil { - if setting.Value != "" { - style = setting.Value - } - } - - // Also return theme default + style := "split" theme := "dark" - if model.DB.Where("`key` = ?", "default_theme").First(&setting).Error == nil { - if setting.Value != "" { - theme = setting.Value - } + + var s model.SystemSetting + if model.DB.Where("`key` = ?", "login_style").Take(&s).Error == nil && s.Value != "" { + style = s.Value + } + s = model.SystemSetting{} // reset to avoid GORM reusing primary key + if model.DB.Where("`key` = ?", "default_theme").Take(&s).Error == nil && s.Value != "" { + theme = s.Value } c.JSON(http.StatusOK, gin.H{"login_style": style, "theme": theme}) diff --git a/frontend/src/components/login/CenteredCard.tsx b/frontend/src/components/login/CenteredCard.tsx index 1343cd9..62940ac 100644 --- a/frontend/src/components/login/CenteredCard.tsx +++ b/frontend/src/components/login/CenteredCard.tsx @@ -23,9 +23,9 @@ export default function CenteredCard() {
Sign in to your account
+登录您的账号
Sign in to your workspace
+登录您的工作区
Secure database auditing & compliance
+安全的数据库审核与合规管理
- Empowering teams with intelligent SQL auditing, real-time compliance monitoring, and actionable insights for database security. + 为团队提供智能 SQL 审核、实时合规监控与数据库安全保障。
- Enterprise-grade SQL auditing and compliance management. + 企业级 SQL 审核与合规管理平台。
Enter your credentials to continue
+请输入您的账号信息
{error &&