Files
QuoteForge/internal/handlers/setup.go
Mikhail Chusavitin 8d84484412 fix: fix online mode after offline-first architecture changes
- 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>
2026-02-03 10:50:07 +03:00

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
}