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 @@