Refactor scheduler and settings UI

This commit is contained in:
Mikhail Chusavitin
2026-03-07 21:10:20 +03:00
parent 27e33db446
commit b2b2f4774c
23 changed files with 1601 additions and 551 deletions

View File

@@ -26,6 +26,7 @@ import (
"git.mchus.pro/mchus/priceforge/internal/middleware"
"git.mchus.pro/mchus/priceforge/internal/models"
"git.mchus.pro/mchus/priceforge/internal/repository"
"git.mchus.pro/mchus/priceforge/internal/scheduler"
"git.mchus.pro/mchus/priceforge/internal/services"
"git.mchus.pro/mchus/priceforge/internal/services/alerts"
"git.mchus.pro/mchus/priceforge/internal/services/pricelist"
@@ -193,6 +194,10 @@ func main() {
os.Exit(1)
}
appCtx, appCancel := context.WithCancel(context.Background())
defer appCancel()
startEmbeddedScheduler(appCtx, mariaDB, cfg)
srv := &http.Server{
Addr: cfg.Address(),
Handler: router,
@@ -225,6 +230,7 @@ func main() {
<-quit
slog.Info("shutting down server...")
appCancel()
// Shutdown HTTP server
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -238,6 +244,21 @@ func main() {
}
func startEmbeddedScheduler(ctx context.Context, mariaDB *gorm.DB, cfg *config.Config) {
if mariaDB == nil || cfg == nil || !cfg.Scheduler.Enabled {
return
}
statsRepo := repository.NewStatsRepository(mariaDB)
alertRepo := repository.NewAlertRepository(mariaDB)
componentRepo := repository.NewComponentRepository(mariaDB)
priceRepo := repository.NewPriceRepository(mariaDB)
alertService := alerts.NewService(alertRepo, componentRepo, priceRepo, statsRepo, cfg.Alerts, cfg.Pricing)
pricingService := pricing.NewService(componentRepo, priceRepo, cfg.Pricing)
embeddedScheduler := scheduler.New(mariaDB, alertService, pricingService, statsRepo, cfg.Scheduler)
go embeddedScheduler.Start(ctx)
}
func setConfigDefaults(cfg *config.Config) {
if cfg.Server.Host == "" {
cfg.Server.Host = "127.0.0.1"
@@ -272,6 +293,27 @@ func setConfigDefaults(cfg *config.Config) {
if cfg.Pricing.MinQuotesForMedian == 0 {
cfg.Pricing.MinQuotesForMedian = 3
}
if cfg.Scheduler.PollInterval == 0 {
cfg.Scheduler.PollInterval = time.Minute
}
if cfg.Alerts.CheckInterval == 0 {
cfg.Alerts.CheckInterval = time.Hour
}
if cfg.Scheduler.AlertsInterval == 0 {
cfg.Scheduler.AlertsInterval = cfg.Alerts.CheckInterval
}
if cfg.Scheduler.UpdatePricesInterval == 0 {
cfg.Scheduler.UpdatePricesInterval = 24 * time.Hour
}
if cfg.Scheduler.UpdatePopularityInterval == 0 {
cfg.Scheduler.UpdatePopularityInterval = 24 * time.Hour
}
if cfg.Scheduler.ResetWeeklyCountersInterval == 0 {
cfg.Scheduler.ResetWeeklyCountersInterval = 7 * 24 * time.Hour
}
if cfg.Scheduler.ResetMonthlyCountersInterval == 0 {
cfg.Scheduler.ResetMonthlyCountersInterval = 30 * 24 * time.Hour
}
}
func isLoopbackHost(host string) bool {
@@ -385,11 +427,18 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
stockImportService,
vendorMappingService,
partnumberBookService,
cfg.Scheduler,
dbUser,
taskManager,
)
pricelistHandler := handlers.NewPricelistHandler(pricelistService, dbUser, taskManager)
taskHandler := tasks.NewHandler(taskManager)
setupHandler, err := handlers.NewSetupHandler(connMgr, templatesPath, cfg, func(nextCfg *config.Config) error {
return saveConfig(configPath, nextCfg)
})
if err != nil {
return nil, err
}
webHandler, err := handlers.NewWebHandler(templatesPath, componentService)
if err != nil {
return nil, err
@@ -465,107 +514,6 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
})
})
router.GET("/api/connection-settings", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"host": cfg.Database.Host,
"port": cfg.Database.Port,
"database": cfg.Database.Name,
"user": cfg.Database.User,
})
})
router.POST("/api/connection-settings/test", func(c *gin.Context) {
host := strings.TrimSpace(c.PostForm("host"))
database := strings.TrimSpace(c.PostForm("database"))
user := strings.TrimSpace(c.PostForm("user"))
password := c.PostForm("password")
port, err := strconv.Atoi(strings.TrimSpace(c.DefaultPostForm("port", strconv.Itoa(cfg.Database.Port))))
if err != nil || port <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid port"})
return
}
if host == "" || database == "" || user == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "all fields except password are required"})
return
}
if password == "" {
password = cfg.Database.Password
}
dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second)
testDB, 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 := testDB.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
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "connection successful"})
})
router.POST("/api/connection-settings", func(c *gin.Context) {
host := strings.TrimSpace(c.PostForm("host"))
database := strings.TrimSpace(c.PostForm("database"))
user := strings.TrimSpace(c.PostForm("user"))
password := c.PostForm("password")
port, err := strconv.Atoi(strings.TrimSpace(c.DefaultPostForm("port", strconv.Itoa(cfg.Database.Port))))
if err != nil || port <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid port"})
return
}
if host == "" || database == "" || user == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "all fields except password are required"})
return
}
if password == "" {
password = cfg.Database.Password
}
dsn := buildMySQLDSN(host, port, database, user, password, 5*time.Second)
testDB, 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, err := testDB.DB()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": fmt.Sprintf("failed to get database handle: %v", err)})
return
}
if err := sqlDB.Ping(); err != nil {
_ = sqlDB.Close()
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": fmt.Sprintf("ping failed: %v", err)})
return
}
_ = sqlDB.Close()
cfg.Database.Host = host
cfg.Database.Port = port
cfg.Database.Name = database
cfg.Database.User = user
cfg.Database.Password = password
if err := saveConfig(configPath, cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Sprintf("failed to save config: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "settings saved, restart required",
"restart_required": true,
})
})
router.GET("/api/current-user", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"username": dbUser,
@@ -574,6 +522,9 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
})
router.GET("/", webHandler.Index)
router.GET("/setup", setupHandler.ShowSetup)
router.POST("/setup", setupHandler.SaveConnection)
router.POST("/setup/test", setupHandler.TestConnection)
router.GET("/lot", webHandler.Lot)
router.GET("/pricelists", webHandler.Pricelists)
router.GET("/pricelists/:id", webHandler.PricelistDetail)
@@ -647,6 +598,7 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
pricingAdmin.POST("/alerts/:id/acknowledge", pricingHandler.AcknowledgeAlert)
pricingAdmin.POST("/alerts/:id/resolve", pricingHandler.ResolveAlert)
pricingAdmin.POST("/alerts/:id/ignore", pricingHandler.IgnoreAlert)
pricingAdmin.GET("/scheduler-runs", pricingHandler.ListSchedulerRuns)
pricingAdmin.GET("/partnumber-books", pricingHandler.ListPartnumberBooks)
pricingAdmin.POST("/partnumber-books", pricingHandler.CreatePartnumberBook)
}