package handlers import ( "fmt" "html/template" "log/slog" "net" "net/http" "os" "path/filepath" "strconv" "time" qfassets "git.mchus.pro/mchus/priceforge" "git.mchus.pro/mchus/priceforge/internal/db" "git.mchus.pro/mchus/priceforge/internal/localdb" "github.com/gin-gonic/gin" mysqlDriver "github.com/go-sql-driver/mysql" gormmysql "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) type SetupHandler struct { localDB *localdb.LocalDB connMgr *db.ConnectionManager templates map[string]*template.Template restartSig chan struct{} } func NewSetupHandler(localDB *localdb.LocalDB, connMgr *db.ConnectionManager, templatesPath string, restartSig chan struct{}) (*SetupHandler, error) { funcMap := template.FuncMap{ "sub": func(a, b int) int { return a - b }, "add": func(a, b int) int { return a + b }, } templates := make(map[string]*template.Template) // Load setup template (standalone, no base needed) setupPath := filepath.Join(templatesPath, "setup.html") var tmpl *template.Template var err error if stat, statErr := os.Stat(templatesPath); statErr == nil && stat.IsDir() { tmpl, err = template.New("").Funcs(funcMap).ParseFiles(setupPath) } else { tmpl, err = template.New("").Funcs(funcMap).ParseFS(qfassets.TemplatesFS, "web/templates/setup.html") } if err != nil { return nil, fmt.Errorf("parsing setup template: %w", err) } templates["setup.html"] = tmpl return &SetupHandler{ localDB: localDB, connMgr: connMgr, templates: templates, restartSig: restartSig, }, nil } // ShowSetup renders the database setup form func (h *SetupHandler) ShowSetup(c *gin.Context) { c.Header("Content-Type", "text/html; charset=utf-8") // Get existing settings if any settings, _ := h.localDB.GetSettings() data := gin.H{ "Settings": settings, } tmpl := h.templates["setup.html"] if err := tmpl.ExecuteTemplate(c.Writer, "setup.html", data); err != nil { c.String(http.StatusInternalServerError, "Template error: %v", err) } } // TestConnection tests the database connection without saving func (h *SetupHandler) TestConnection(c *gin.Context) { host := c.PostForm("host") portStr := c.PostForm("port") database := c.PostForm("database") user := c.PostForm("user") password := c.PostForm("password") port := 3306 if p, err := strconv.Atoi(portStr); err == nil { port = p } // If password is empty, try to use saved password if password == "" { if settings, err := h.localDB.GetSettings(); err == nil && settings != nil { password = settings.PasswordEncrypted // GetSettings returns decrypted password in this field } } dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second) db, err := gorm.Open(gormmysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Connection failed: %v", err), }) return } sqlDB, err := db.DB() if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Failed to get database handle: %v", err), }) return } defer sqlDB.Close() if err := sqlDB.Ping(); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Ping failed: %v", err), }) return } // Check for required tables var lotCount int64 if err := db.Table("lot").Count(&lotCount).Error; err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("Table 'lot' not found or inaccessible: %v", err), }) return } // Check write permission canWrite := testWritePermission(db) c.JSON(http.StatusOK, gin.H{ "success": true, "lot_count": lotCount, "can_write": canWrite, "message": fmt.Sprintf("Connected successfully! Found %d components.", lotCount), }) } // SaveConnection saves the connection settings and signals restart func (h *SetupHandler) SaveConnection(c *gin.Context) { existingSettings, _ := h.localDB.GetSettings() host := c.PostForm("host") portStr := c.PostForm("port") database := c.PostForm("database") user := c.PostForm("user") password := c.PostForm("password") port := 3306 if p, err := strconv.Atoi(portStr); err == nil { port = p } // If password is empty, use saved password if password == "" { if settings, err := h.localDB.GetSettings(); err == nil && settings != nil { password = settings.PasswordEncrypted // GetSettings returns decrypted password in this field } } // Test connection first dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second) db, err := gorm.Open(gormmysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": fmt.Sprintf("Connection failed: %v", err), }) return } sqlDB, _ := db.DB() sqlDB.Close() // Save settings if err := h.localDB.SaveSettings(host, port, database, user, password); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": fmt.Sprintf("Failed to save settings: %v", err), }) return } // Try to connect immediately to verify settings if h.connMgr != nil { if err := h.connMgr.TryConnect(); err != nil { slog.Warn("failed to connect after saving settings", "error", err) } else { slog.Info("successfully connected to database after saving settings") } } settingsChanged := existingSettings == nil || existingSettings.Host != host || existingSettings.Port != port || existingSettings.Database != database || existingSettings.User != user || existingSettings.PasswordEncrypted != password // Always restart after successful save in normal mode so handlers are reinitialized // with a fresh MariaDB connection state. restartQueued := h.restartSig != nil c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Settings saved.", "restart_required": settingsChanged || restartQueued, "restart_queued": restartQueued, }) // Signal restart after response is sent (if restart signal is configured) if restartQueued { go func() { time.Sleep(500 * time.Millisecond) // Give time for response to be sent select { case h.restartSig <- struct{}{}: default: } }() } } // GetStatus returns the current setup status func (h *SetupHandler) GetStatus(c *gin.Context) { hasSettings := h.localDB.HasSettings() c.JSON(http.StatusOK, gin.H{ "configured": hasSettings, }) } func testWritePermission(db *gorm.DB) bool { // Simple check: try to create a temporary table and drop it testTable := fmt.Sprintf("qt_write_test_%d", time.Now().UnixNano()) // Try to create a test table err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INT)", testTable)).Error if err != nil { return false } // Drop it immediately db.Exec(fmt.Sprintf("DROP TABLE %s", testTable)) return true } func buildMySQLDSN(host string, port int, database, user, password string, timeout time.Duration) string { cfg := mysqlDriver.NewConfig() cfg.User = user cfg.Passwd = password cfg.Net = "tcp" cfg.Addr = net.JoinHostPort(host, strconv.Itoa(port)) cfg.DBName = database cfg.ParseTime = true cfg.Loc = time.Local cfg.Timeout = timeout cfg.Params = map[string]string{ "charset": "utf8mb4", } return cfg.FormatDSN() }