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>
This commit is contained in:
@@ -64,30 +64,41 @@ func main() {
|
|||||||
|
|
||||||
setupLogger(cfg.Logging)
|
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)
|
connMgr := db.NewConnectionManager(local)
|
||||||
slog.Info("starting in offline-first mode")
|
|
||||||
|
|
||||||
dbUser := local.GetDBUser()
|
dbUser := local.GetDBUser()
|
||||||
|
|
||||||
// In offline-first mode, use default user ID
|
|
||||||
// EnsureDBUser will be called lazily when sync happens
|
|
||||||
dbUserID := uint(1)
|
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",
|
slog.Info("starting QuoteForge server",
|
||||||
"host", cfg.Server.Host,
|
"host", cfg.Server.Host,
|
||||||
"port", cfg.Server.Port,
|
"port", cfg.Server.Port,
|
||||||
"db_user", dbUser,
|
"db_user", dbUser,
|
||||||
"db_user_id", dbUserID,
|
"db_user_id", dbUserID,
|
||||||
|
"online", mariaDB != nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if *migrate {
|
if *migrate {
|
||||||
slog.Info("running database migrations...")
|
if mariaDB == nil {
|
||||||
mariaDB, err := connMgr.GetDB()
|
slog.Error("cannot run migrations: database not available")
|
||||||
if err != nil {
|
|
||||||
slog.Error("cannot run migrations: database not available", "error", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
slog.Info("running database migrations...")
|
||||||
if err := models.Migrate(mariaDB); err != nil {
|
if err := models.Migrate(mariaDB); err != nil {
|
||||||
slog.Error("migration failed", "error", err)
|
slog.Error("migration failed", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -100,7 +111,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(cfg.Server.Mode)
|
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 {
|
if err != nil {
|
||||||
slog.Error("failed to setup router", "error", err)
|
slog.Error("failed to setup router", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -189,7 +200,8 @@ func setConfigDefaults(cfg *config.Config) {
|
|||||||
func runSetupMode(local *localdb.LocalDB) {
|
func runSetupMode(local *localdb.LocalDB) {
|
||||||
restartSig := make(chan struct{}, 1)
|
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 {
|
if err != nil {
|
||||||
slog.Error("failed to create setup handler", "error", err)
|
slog.Error("failed to create setup handler", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -300,10 +312,8 @@ func setupDatabaseFromDSN(dsn string) (*gorm.DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, dbUserID uint) (*gin.Engine, *sync.Service, error) {
|
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUserID uint) (*gin.Engine, *sync.Service, error) {
|
||||||
// Don't connect to MariaDB on startup (offline-first architecture)
|
// mariaDB may be nil if we're in offline mode
|
||||||
// Connection will be established lazily when needed
|
|
||||||
var mariaDB *gorm.DB
|
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
var componentRepo *repository.ComponentRepository
|
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
|
// 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 {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("creating setup handler: %w", err)
|
return nil, nil, fmt.Errorf("creating setup handler: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -639,6 +639,18 @@ func (h *PricingHandler) RecalculateAll(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *PricingHandler) ListAlerts(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"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
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) {
|
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)
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"})
|
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) {
|
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)
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"})
|
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) {
|
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)
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid alert id"})
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"git.mchus.pro/mchus/quoteforge/internal/db"
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -17,11 +19,12 @@ import (
|
|||||||
|
|
||||||
type SetupHandler struct {
|
type SetupHandler struct {
|
||||||
localDB *localdb.LocalDB
|
localDB *localdb.LocalDB
|
||||||
|
connMgr *db.ConnectionManager
|
||||||
templates map[string]*template.Template
|
templates map[string]*template.Template
|
||||||
restartSig chan struct{}
|
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{
|
funcMap := template.FuncMap{
|
||||||
"sub": func(a, b int) int { return a - b },
|
"sub": func(a, b int) int { return a - b },
|
||||||
"add": 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{
|
return &SetupHandler{
|
||||||
localDB: localDB,
|
localDB: localDB,
|
||||||
|
connMgr: connMgr,
|
||||||
templates: templates,
|
templates: templates,
|
||||||
restartSig: restartSig,
|
restartSig: restartSig,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -181,12 +185,23 @@ func (h *SetupHandler) SaveConnection(c *gin.Context) {
|
|||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"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 {
|
if h.restartSig != nil {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(500 * time.Millisecond) // Give time for response to be sent
|
time.Sleep(500 * time.Millisecond) // Give time for response to be sent
|
||||||
|
|||||||
@@ -87,12 +87,14 @@
|
|||||||
<script>
|
<script>
|
||||||
function showStatus(message, type) {
|
function showStatus(message, type) {
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
status.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-blue-100', 'text-blue-800');
|
status.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-blue-100', 'text-blue-800', 'bg-yellow-100', 'text-yellow-800');
|
||||||
|
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
status.classList.add('bg-green-100', 'text-green-800');
|
status.classList.add('bg-green-100', 'text-green-800');
|
||||||
} else if (type === 'error') {
|
} else if (type === 'error') {
|
||||||
status.classList.add('bg-red-100', 'text-red-800');
|
status.classList.add('bg-red-100', 'text-red-800');
|
||||||
|
} else if (type === 'warning') {
|
||||||
|
status.classList.add('bg-yellow-100', 'text-yellow-800');
|
||||||
} else {
|
} else {
|
||||||
status.classList.add('bg-blue-100', 'text-blue-800');
|
status.classList.add('bg-blue-100', 'text-blue-800');
|
||||||
}
|
}
|
||||||
@@ -171,12 +173,21 @@
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showStatus('✓ ' + data.message, 'success');
|
showStatus('✓ ' + data.message, 'success');
|
||||||
// Wait for restart and redirect to home
|
|
||||||
setTimeout(() => {
|
// Check if restart is required
|
||||||
showStatus('✓ Настройки сохранены. Проверка подключения...', 'success');
|
if (data.restart_required) {
|
||||||
// Poll until server is back
|
// In normal mode, restart must be done manually
|
||||||
checkServerReady();
|
setTimeout(() => {
|
||||||
}, 2000);
|
showStatus('⚠️ Пожалуйста, перезапустите приложение вручную для применения изменений', 'warning');
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
// In setup mode, auto-restart is happening
|
||||||
|
setTimeout(() => {
|
||||||
|
showStatus('✓ Настройки сохранены. Проверка подключения...', 'success');
|
||||||
|
// Poll until server is back
|
||||||
|
checkServerReady();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showStatus(data.error, 'error');
|
showStatus(data.error, 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user