From 8d8448441208e20b7b27c44be3c97cbea664aeed Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Tue, 3 Feb 2026 10:50:07 +0300 Subject: [PATCH] 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 --- cmd/server/main.go | 42 ++++++++++++++++++++++-------------- internal/handlers/pricing.go | 39 +++++++++++++++++++++++++++++++++ internal/handlers/setup.go | 21 +++++++++++++++--- web/templates/setup.html | 25 +++++++++++++++------ 4 files changed, 101 insertions(+), 26 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 3f607f9..d95108f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -64,30 +64,41 @@ func main() { setupLogger(cfg.Logging) - // Create connection manager (lazy connection, no connect on startup) + // Create connection manager and try to connect immediately if settings exist connMgr := db.NewConnectionManager(local) - slog.Info("starting in offline-first mode") dbUser := local.GetDBUser() - - // In offline-first mode, use default user ID - // EnsureDBUser will be called lazily when sync happens dbUserID := uint(1) + // Try to connect to MariaDB on startup + mariaDB, err := connMgr.GetDB() + if err != nil { + slog.Warn("failed to connect to MariaDB on startup, starting in offline mode", "error", err) + mariaDB = nil + } else { + slog.Info("successfully connected to MariaDB on startup") + // Ensure DB user exists and get their ID + if dbUserID, err = models.EnsureDBUser(mariaDB, dbUser); err != nil { + slog.Error("failed to ensure DB user", "error", err) + // Continue with default ID + dbUserID = uint(1) + } + } + slog.Info("starting QuoteForge server", "host", cfg.Server.Host, "port", cfg.Server.Port, "db_user", dbUser, "db_user_id", dbUserID, + "online", mariaDB != nil, ) if *migrate { - slog.Info("running database migrations...") - mariaDB, err := connMgr.GetDB() - if err != nil { - slog.Error("cannot run migrations: database not available", "error", err) + if mariaDB == nil { + slog.Error("cannot run migrations: database not available") os.Exit(1) } + slog.Info("running database migrations...") if err := models.Migrate(mariaDB); err != nil { slog.Error("migration failed", "error", err) os.Exit(1) @@ -100,7 +111,7 @@ func main() { } gin.SetMode(cfg.Server.Mode) - router, syncService, err := setupRouter(cfg, local, connMgr, dbUserID) + router, syncService, err := setupRouter(cfg, local, connMgr, mariaDB, dbUserID) if err != nil { slog.Error("failed to setup router", "error", err) os.Exit(1) @@ -189,7 +200,8 @@ func setConfigDefaults(cfg *config.Config) { func runSetupMode(local *localdb.LocalDB) { restartSig := make(chan struct{}, 1) - setupHandler, err := handlers.NewSetupHandler(local, "web/templates", restartSig) + // In setup mode, we don't have a connection manager yet (will restart after setup) + setupHandler, err := handlers.NewSetupHandler(local, nil, "web/templates", restartSig) if err != nil { slog.Error("failed to create setup handler", "error", err) os.Exit(1) @@ -300,10 +312,8 @@ func setupDatabaseFromDSN(dsn string) (*gorm.DB, error) { return db, nil } -func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, dbUserID uint) (*gin.Engine, *sync.Service, error) { - // Don't connect to MariaDB on startup (offline-first architecture) - // Connection will be established lazily when needed - var mariaDB *gorm.DB +func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUserID uint) (*gin.Engine, *sync.Service, error) { + // mariaDB may be nil if we're in offline mode // Repositories var componentRepo *repository.ComponentRepository @@ -375,7 +385,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect } // Setup handler (for reconfiguration) - no restart signal in normal mode - setupHandler, err := handlers.NewSetupHandler(local, "web/templates", nil) + setupHandler, err := handlers.NewSetupHandler(local, connMgr, "web/templates", nil) if err != nil { return nil, nil, fmt.Errorf("creating setup handler: %w", err) } diff --git a/internal/handlers/pricing.go b/internal/handlers/pricing.go index 9bcc7f0..c928bdf 100644 --- a/internal/handlers/pricing.go +++ b/internal/handlers/pricing.go @@ -639,6 +639,18 @@ func (h *PricingHandler) RecalculateAll(c *gin.Context) { } func (h *PricingHandler) ListAlerts(c *gin.Context) { + // Check if we're in offline mode + if h.db == nil { + c.JSON(http.StatusOK, gin.H{ + "alerts": []interface{}{}, + "total": 0, + "page": 1, + "per_page": 20, + "offline": true, + }) + return + } + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20")) @@ -664,6 +676,15 @@ func (h *PricingHandler) ListAlerts(c *gin.Context) { } func (h *PricingHandler) AcknowledgeAlert(c *gin.Context) { + // Check if we're in offline mode + if h.db == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "Управление алертами доступно только в онлайн режиме", + "offline": true, + }) + return + } + id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"}) @@ -679,6 +700,15 @@ func (h *PricingHandler) AcknowledgeAlert(c *gin.Context) { } func (h *PricingHandler) ResolveAlert(c *gin.Context) { + // Check if we're in offline mode + if h.db == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "Управление алертами доступно только в онлайн режиме", + "offline": true, + }) + return + } + id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"}) @@ -694,6 +724,15 @@ func (h *PricingHandler) ResolveAlert(c *gin.Context) { } func (h *PricingHandler) IgnoreAlert(c *gin.Context) { + // Check if we're in offline mode + if h.db == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "Управление алертами доступно только в онлайн режиме", + "offline": true, + }) + return + } + id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"}) diff --git a/internal/handlers/setup.go b/internal/handlers/setup.go index 7fcd972..2a9c858 100644 --- a/internal/handlers/setup.go +++ b/internal/handlers/setup.go @@ -3,12 +3,14 @@ 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" @@ -17,11 +19,12 @@ import ( type SetupHandler struct { localDB *localdb.LocalDB + connMgr *db.ConnectionManager templates map[string]*template.Template restartSig chan struct{} } -func NewSetupHandler(localDB *localdb.LocalDB, templatesPath string, restartSig chan struct{}) (*SetupHandler, error) { +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 }, @@ -39,6 +42,7 @@ func NewSetupHandler(localDB *localdb.LocalDB, templatesPath string, restartSig return &SetupHandler{ localDB: localDB, + connMgr: connMgr, templates: templates, restartSig: restartSig, }, nil @@ -181,12 +185,23 @@ func (h *SetupHandler) SaveConnection(c *gin.Context) { 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. Restarting application...", + "message": "Settings saved. Please restart the application to apply changes.", + "restart_required": true, }) - // Signal restart after response is sent + // 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 diff --git a/web/templates/setup.html b/web/templates/setup.html index b67a5c5..003f7fb 100644 --- a/web/templates/setup.html +++ b/web/templates/setup.html @@ -87,12 +87,14 @@