feat: implement background task system with notifications

- Added background task manager with goroutine execution and panic recovery
- Replaced SSE streaming with background task execution for:
  * Price recalculation (RecalculateAll)
  * Stock import (ImportStockLog)
  * Pricelist creation (CreateWithProgress)
- Implemented unified polling for task status and DB connection in frontend
- Added task indicator in top bar showing running tasks count
- Added toast notifications for task completion/error
- Tasks automatically cleaned up after 10 minutes
- Tasks show progress (0-100%) with descriptive messages
- Updated handler constructors to receive task manager
- Added API endpoints for task status (/api/tasks, /api/tasks/:id)

Fixes issue with SSE disconnection on slow connections during long-running operations
This commit is contained in:
2026-02-08 20:39:59 +03:00
parent 06aa7c7067
commit e97cd5048c
15 changed files with 1080 additions and 555 deletions

View File

@@ -30,6 +30,7 @@ import (
"git.mchus.pro/mchus/priceforge/internal/services/alerts"
"git.mchus.pro/mchus/priceforge/internal/services/pricelist"
"git.mchus.pro/mchus/priceforge/internal/services/pricing"
"git.mchus.pro/mchus/priceforge/internal/tasks"
"github.com/gin-gonic/gin"
mysqlDriver "github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v3"
@@ -338,6 +339,9 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
pricelistService := pricelist.NewService(mariaDB, pricelistRepo, componentRepo, pricingService)
stockImportService := services.NewStockImportService(mariaDB, pricelistService)
// Create task manager
taskManager := tasks.NewManager()
templatesPath := filepath.Join("web", "templates")
componentHandler := handlers.NewComponentHandler(componentService)
pricingHandler := handlers.NewPricingHandler(
@@ -345,12 +349,15 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
pricingService,
alertService,
componentRepo,
componentService,
priceRepo,
statsRepo,
stockImportService,
dbUser,
taskManager,
)
pricelistHandler := handlers.NewPricelistHandler(pricelistService, dbUser)
pricelistHandler := handlers.NewPricelistHandler(pricelistService, dbUser, taskManager)
taskHandler := tasks.NewHandler(taskManager)
webHandler, err := handlers.NewWebHandler(templatesPath, componentService)
if err != nil {
return nil, err
@@ -550,6 +557,9 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
api.GET("/tasks", taskHandler.List)
api.GET("/tasks/:id", taskHandler.Get)
components := api.Group("/components")
{
components.GET("", componentHandler.List)
@@ -583,6 +593,7 @@ func setupRouter(cfg *config.Config, configPath string, connMgr *db.ConnectionMa
pricingAdmin.GET("/lots", pricingHandler.ListLots)
pricingAdmin.GET("/lots-table", pricingHandler.ListLotsTable)
pricingAdmin.POST("/lots", pricingHandler.CreateLot)
pricingAdmin.POST("/lots/sync-metadata", pricingHandler.SyncLotsMetadata)
pricingAdmin.POST("/stock/import", pricingHandler.ImportStockLog)
pricingAdmin.GET("/stock/mappings", pricingHandler.ListStockMappings)
pricingAdmin.POST("/stock/mappings", pricingHandler.UpsertStockMapping)