- Fix nil pointer dereference in PricingHandler alert methods - Add automatic MariaDB connection on startup if settings exist - Update setupRouter to accept mariaDB as parameter - Fix offline mode checks: use h.db instead of h.alertService - Update setup handler to show restart required message - Add warning status support in setup.html UI This ensures that after saving connection settings, the application works correctly in online mode after restart. All repositories are properly initialized with MariaDB connection on startup. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
236 lines
6.1 KiB
Go
236 lines
6.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"git.mchus.pro/mchus/quoteforge/internal/db"
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"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")
|
|
tmpl, err := template.New("").Funcs(funcMap).ParseFiles(setupPath)
|
|
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 := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s",
|
|
user, password, host, port, database)
|
|
|
|
db, err := gorm.Open(mysql.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) {
|
|
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 := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s",
|
|
user, password, host, port, database)
|
|
|
|
db, err := gorm.Open(mysql.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")
|
|
}
|
|
}
|
|
|
|
// Always restart to properly initialize all services with the new connection
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Settings saved. Please restart the application to apply changes.",
|
|
"restart_required": true,
|
|
})
|
|
|
|
// Signal restart after response is sent (if restart signal is configured)
|
|
if h.restartSig != nil {
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond) // Give time for response to be sent
|
|
h.restartSig <- struct{}{}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|