Local-first runtime cleanup and recovery hardening
This commit is contained in:
123
cmd/qfs/main.go
123
cmd/qfs/main.go
@@ -32,7 +32,6 @@ import (
|
||||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/middleware"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services/sync"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -151,29 +150,25 @@ func main() {
|
||||
|
||||
setupLogger(cfg.Logging)
|
||||
|
||||
// Create connection manager and try to connect immediately if settings exist
|
||||
// Create connection manager. Runtime stays local-first; MariaDB is used on demand by sync/setup only.
|
||||
connMgr := db.NewConnectionManager(local)
|
||||
|
||||
dbUser := local.GetDBUser()
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
slog.Info("starting QuoteForge server",
|
||||
"version", Version,
|
||||
"host", cfg.Server.Host,
|
||||
"port", cfg.Server.Port,
|
||||
"db_user", dbUser,
|
||||
"online", mariaDB != nil,
|
||||
"online", false,
|
||||
)
|
||||
|
||||
if *migrate {
|
||||
mariaDB, err := connMgr.GetDB()
|
||||
if err != nil {
|
||||
slog.Error("cannot run migrations: database not available", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if mariaDB == nil {
|
||||
slog.Error("cannot run migrations: database not available")
|
||||
os.Exit(1)
|
||||
@@ -190,39 +185,10 @@ func main() {
|
||||
slog.Info("migrations completed")
|
||||
}
|
||||
|
||||
// Always apply SQL migrations on startup when database is available.
|
||||
// This keeps schema in sync for long-running installations without manual steps.
|
||||
// If current DB user does not have enough privileges, continue startup in normal mode.
|
||||
if mariaDB != nil {
|
||||
sqlMigrationsPath := filepath.Join("migrations")
|
||||
needsMigrations, err := models.NeedsSQLMigrations(mariaDB, sqlMigrationsPath)
|
||||
if err != nil {
|
||||
if models.IsMigrationPermissionError(err) {
|
||||
slog.Info("startup SQL migrations skipped: insufficient database privileges", "path", sqlMigrationsPath, "error", err)
|
||||
} else {
|
||||
slog.Error("startup SQL migrations check failed", "path", sqlMigrationsPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if needsMigrations {
|
||||
if err := models.RunSQLMigrations(mariaDB, sqlMigrationsPath); err != nil {
|
||||
if models.IsMigrationPermissionError(err) {
|
||||
slog.Info("startup SQL migrations skipped: insufficient database privileges", "path", sqlMigrationsPath, "error", err)
|
||||
} else {
|
||||
slog.Error("startup SQL migrations failed", "path", sqlMigrationsPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
slog.Info("startup SQL migrations applied", "path", sqlMigrationsPath)
|
||||
}
|
||||
} else {
|
||||
slog.Debug("startup SQL migrations not needed", "path", sqlMigrationsPath)
|
||||
}
|
||||
}
|
||||
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
restartSig := make(chan struct{}, 1)
|
||||
|
||||
router, syncService, err := setupRouter(cfg, local, connMgr, mariaDB, dbUser, restartSig)
|
||||
router, syncService, err := setupRouter(cfg, local, connMgr, dbUser, restartSig)
|
||||
if err != nil {
|
||||
slog.Error("failed to setup router", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -672,46 +638,14 @@ func setupDatabaseFromDSN(dsn string) (*gorm.DB, error) {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, mariaDB *gorm.DB, dbUsername string, restartSig chan struct{}) (*gin.Engine, *sync.Service, error) {
|
||||
// mariaDB may be nil if we're in offline mode
|
||||
|
||||
// Repositories
|
||||
var componentRepo *repository.ComponentRepository
|
||||
var categoryRepo *repository.CategoryRepository
|
||||
var statsRepo *repository.StatsRepository
|
||||
var pricelistRepo *repository.PricelistRepository
|
||||
|
||||
// Only initialize repositories if we have a database connection
|
||||
if mariaDB != nil {
|
||||
componentRepo = repository.NewComponentRepository(mariaDB)
|
||||
categoryRepo = repository.NewCategoryRepository(mariaDB)
|
||||
statsRepo = repository.NewStatsRepository(mariaDB)
|
||||
pricelistRepo = repository.NewPricelistRepository(mariaDB)
|
||||
} else {
|
||||
// In offline mode, we'll use nil repositories or handle them differently
|
||||
// This is handled in the sync service and other components
|
||||
}
|
||||
|
||||
// Services
|
||||
var componentService *services.ComponentService
|
||||
var quoteService *services.QuoteService
|
||||
var exportService *services.ExportService
|
||||
func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.ConnectionManager, dbUsername string, restartSig chan struct{}) (*gin.Engine, *sync.Service, error) {
|
||||
var syncService *sync.Service
|
||||
var projectService *services.ProjectService
|
||||
|
||||
// Sync service always uses ConnectionManager (works offline and online)
|
||||
syncService = sync.NewService(connMgr, local)
|
||||
|
||||
if mariaDB != nil {
|
||||
componentService = services.NewComponentService(componentRepo, categoryRepo, statsRepo)
|
||||
quoteService = services.NewQuoteService(componentRepo, statsRepo, pricelistRepo, local, nil)
|
||||
exportService = services.NewExportService(cfg.Export, categoryRepo, local)
|
||||
} else {
|
||||
// In offline mode, we still need to create services that don't require DB.
|
||||
componentService = services.NewComponentService(nil, nil, nil)
|
||||
quoteService = services.NewQuoteService(nil, nil, nil, local, nil)
|
||||
exportService = services.NewExportService(cfg.Export, nil, local)
|
||||
}
|
||||
componentService := services.NewComponentService(nil, nil, nil)
|
||||
quoteService := services.NewQuoteService(nil, nil, nil, local, nil)
|
||||
exportService := services.NewExportService(cfg.Export, nil, local)
|
||||
|
||||
// isOnline function for local-first architecture
|
||||
isOnline := func() bool {
|
||||
@@ -732,16 +666,6 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
||||
if err := local.BackfillConfigurationProjects(dbUsername); err != nil {
|
||||
slog.Warn("failed to backfill local configuration projects", "error", err)
|
||||
}
|
||||
if mariaDB != nil {
|
||||
serverProjectRepo := repository.NewProjectRepository(mariaDB)
|
||||
if removed, err := serverProjectRepo.PurgeEmptyNamelessProjects(); err == nil && removed > 0 {
|
||||
slog.Info("purged empty nameless server projects", "removed", removed)
|
||||
}
|
||||
if err := serverProjectRepo.EnsureSystemProjectsAndBackfillConfigurations(); err != nil {
|
||||
slog.Warn("failed to backfill server configuration projects", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
type pullState struct {
|
||||
mu syncpkg.Mutex
|
||||
running bool
|
||||
@@ -819,7 +743,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
||||
// Handlers
|
||||
componentHandler := handlers.NewComponentHandler(componentService, local)
|
||||
quoteHandler := handlers.NewQuoteHandler(quoteService)
|
||||
exportHandler := handlers.NewExportHandler(exportService, configService, projectService)
|
||||
exportHandler := handlers.NewExportHandler(exportService, configService, projectService, dbUsername)
|
||||
pricelistHandler := handlers.NewPricelistHandler(local)
|
||||
vendorSpecHandler := handlers.NewVendorSpecHandler(local)
|
||||
partnumberBooksHandler := handlers.NewPartnumberBooksHandler(local)
|
||||
@@ -835,7 +759,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
||||
}
|
||||
|
||||
// Web handler (templates)
|
||||
webHandler, err := handlers.NewWebHandler(templatesPath, componentService)
|
||||
webHandler, err := handlers.NewWebHandler(templatesPath, local)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -891,20 +815,8 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
||||
}
|
||||
}
|
||||
|
||||
// Optional diagnostics mode with server table counts.
|
||||
if includeCounts && status.IsConnected {
|
||||
if db, err := connMgr.GetDB(); err == nil && db != nil {
|
||||
_ = db.Table("lot").Count(&lotCount)
|
||||
_ = db.Table("lot_log").Count(&lotLogCount)
|
||||
_ = db.Table("qt_lot_metadata").Count(&metadataCount)
|
||||
} else if err != nil {
|
||||
dbOK = false
|
||||
dbError = err.Error()
|
||||
} else {
|
||||
dbOK = false
|
||||
dbError = "Database not connected (offline mode)"
|
||||
}
|
||||
} else {
|
||||
// Runtime diagnostics stay local-only. Server table counts are intentionally unavailable here.
|
||||
if !includeCounts || !status.IsConnected {
|
||||
lotCount = 0
|
||||
lotLogCount = 0
|
||||
metadataCount = 0
|
||||
@@ -920,11 +832,10 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
||||
})
|
||||
})
|
||||
|
||||
// Current user info (DB user, not app user)
|
||||
// Current user info (local DB username)
|
||||
router.GET("/api/current-user", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"username": local.GetDBUser(),
|
||||
"role": "db_user",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestConfigurationVersioningAPI(t *testing.T) {
|
||||
|
||||
cfg := &config.Config{}
|
||||
setConfigDefaults(cfg)
|
||||
router, _, err := setupRouter(cfg, local, connMgr, nil, "tester", nil)
|
||||
router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("setup router: %v", err)
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func TestProjectArchiveHidesConfigsAndCloneIntoProject(t *testing.T) {
|
||||
|
||||
cfg := &config.Config{}
|
||||
setConfigDefaults(cfg)
|
||||
router, _, err := setupRouter(cfg, local, connMgr, nil, "tester", nil)
|
||||
router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("setup router: %v", err)
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func TestConfigMoveToProjectEndpoint(t *testing.T) {
|
||||
local, connMgr, _ := newAPITestStack(t)
|
||||
cfg := &config.Config{}
|
||||
setConfigDefaults(cfg)
|
||||
router, _, err := setupRouter(cfg, local, connMgr, nil, "tester", nil)
|
||||
router, _, err := setupRouter(cfg, local, connMgr, "tester", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("setup router: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user