package handlers import ( "html/template" "log/slog" "net/http" "path/filepath" "time" "github.com/gin-gonic/gin" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/services/sync" "gorm.io/gorm" ) // SyncHandler handles sync API endpoints type SyncHandler struct { localDB *localdb.LocalDB syncService *sync.Service mariaDB *gorm.DB tmpl *template.Template } // NewSyncHandler creates a new sync handler func NewSyncHandler(localDB *localdb.LocalDB, syncService *sync.Service, mariaDB *gorm.DB, templatesPath string) (*SyncHandler, error) { // Load sync_status partial template partialPath := filepath.Join(templatesPath, "partials", "sync_status.html") tmpl, err := template.ParseFiles(partialPath) if err != nil { return nil, err } return &SyncHandler{ localDB: localDB, syncService: syncService, mariaDB: mariaDB, tmpl: tmpl, }, nil } // SyncStatusResponse represents the sync status type SyncStatusResponse struct { LastComponentSync *time.Time `json:"last_component_sync"` LastPricelistSync *time.Time `json:"last_pricelist_sync"` IsOnline bool `json:"is_online"` ComponentsCount int64 `json:"components_count"` PricelistsCount int64 `json:"pricelists_count"` ServerPricelists int `json:"server_pricelists"` NeedComponentSync bool `json:"need_component_sync"` NeedPricelistSync bool `json:"need_pricelist_sync"` } // GetStatus returns current sync status // GET /api/sync/status func (h *SyncHandler) GetStatus(c *gin.Context) { // Check online status by pinging MariaDB isOnline := h.checkOnline() // Get sync times lastComponentSync := h.localDB.GetComponentSyncTime() lastPricelistSync := h.localDB.GetLastSyncTime() // Get counts componentsCount := h.localDB.CountLocalComponents() pricelistsCount := h.localDB.CountLocalPricelists() // Get server pricelist count if online serverPricelists := 0 needPricelistSync := false if isOnline { status, err := h.syncService.GetStatus() if err == nil { serverPricelists = status.ServerPricelists needPricelistSync = status.NeedsSync } } // Check if component sync is needed (older than 24 hours) needComponentSync := h.localDB.NeedComponentSync(24) c.JSON(http.StatusOK, SyncStatusResponse{ LastComponentSync: lastComponentSync, LastPricelistSync: lastPricelistSync, IsOnline: isOnline, ComponentsCount: componentsCount, PricelistsCount: pricelistsCount, ServerPricelists: serverPricelists, NeedComponentSync: needComponentSync, NeedPricelistSync: needPricelistSync, }) } // SyncResultResponse represents sync operation result type SyncResultResponse struct { Success bool `json:"success"` Message string `json:"message"` Synced int `json:"synced"` Duration string `json:"duration"` } // SyncComponents syncs components from MariaDB to local SQLite // POST /api/sync/components func (h *SyncHandler) SyncComponents(c *gin.Context) { if !h.checkOnline() { c.JSON(http.StatusServiceUnavailable, gin.H{ "success": false, "error": "Database is offline", }) return } result, err := h.localDB.SyncComponents(h.mariaDB) if err != nil { slog.Error("component sync failed", "error", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, SyncResultResponse{ Success: true, Message: "Components synced successfully", Synced: result.TotalSynced, Duration: result.Duration.String(), }) } // SyncPricelists syncs pricelists from MariaDB to local SQLite // POST /api/sync/pricelists func (h *SyncHandler) SyncPricelists(c *gin.Context) { if !h.checkOnline() { c.JSON(http.StatusServiceUnavailable, gin.H{ "success": false, "error": "Database is offline", }) return } startTime := time.Now() synced, err := h.syncService.SyncPricelists() if err != nil { slog.Error("pricelist sync failed", "error", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, SyncResultResponse{ Success: true, Message: "Pricelists synced successfully", Synced: synced, Duration: time.Since(startTime).String(), }) } // SyncAllResponse represents result of full sync type SyncAllResponse struct { Success bool `json:"success"` Message string `json:"message"` ComponentsSynced int `json:"components_synced"` PricelistsSynced int `json:"pricelists_synced"` Duration string `json:"duration"` } // SyncAll syncs both components and pricelists // POST /api/sync/all func (h *SyncHandler) SyncAll(c *gin.Context) { if !h.checkOnline() { c.JSON(http.StatusServiceUnavailable, gin.H{ "success": false, "error": "Database is offline", }) return } startTime := time.Now() var componentsSynced, pricelistsSynced int // Sync components compResult, err := h.localDB.SyncComponents(h.mariaDB) if err != nil { slog.Error("component sync failed during full sync", "error", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Component sync failed: " + err.Error(), }) return } componentsSynced = compResult.TotalSynced // Sync pricelists pricelistsSynced, err = h.syncService.SyncPricelists() if err != nil { slog.Error("pricelist sync failed during full sync", "error", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Pricelist sync failed: " + err.Error(), "components_synced": componentsSynced, }) return } c.JSON(http.StatusOK, SyncAllResponse{ Success: true, Message: "Full sync completed successfully", ComponentsSynced: componentsSynced, PricelistsSynced: pricelistsSynced, Duration: time.Since(startTime).String(), }) } // checkOnline checks if MariaDB is accessible func (h *SyncHandler) checkOnline() bool { sqlDB, err := h.mariaDB.DB() if err != nil { return false } if err := sqlDB.Ping(); err != nil { return false } return true } // PushPendingChanges pushes all pending changes to the server // POST /api/sync/push func (h *SyncHandler) PushPendingChanges(c *gin.Context) { if !h.checkOnline() { c.JSON(http.StatusServiceUnavailable, gin.H{ "success": false, "error": "Database is offline", }) return } startTime := time.Now() pushed, err := h.syncService.PushPendingChanges() if err != nil { slog.Error("push pending changes failed", "error", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, SyncResultResponse{ Success: true, Message: "Pending changes pushed successfully", Synced: pushed, Duration: time.Since(startTime).String(), }) } // GetPendingCount returns the number of pending changes // GET /api/sync/pending/count func (h *SyncHandler) GetPendingCount(c *gin.Context) { count := h.localDB.GetPendingCount() c.JSON(http.StatusOK, gin.H{ "count": count, }) } // GetPendingChanges returns all pending changes // GET /api/sync/pending func (h *SyncHandler) GetPendingChanges(c *gin.Context) { changes, err := h.localDB.GetPendingChanges() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "changes": changes, }) } // SyncStatusPartial renders the sync status partial for htmx // GET /partials/sync-status func (h *SyncHandler) SyncStatusPartial(c *gin.Context) { // Check online status from middleware isOfflineValue, exists := c.Get("is_offline") isOffline := false if exists { isOffline = isOfflineValue.(bool) } else { // Fallback: check directly if middleware didn't set it isOffline = !h.checkOnline() slog.Warn("is_offline not found in context, checking directly") } // Get pending count pendingCount := h.localDB.GetPendingCount() slog.Debug("rendering sync status", "is_offline", isOffline, "pending_count", pendingCount) data := gin.H{ "IsOffline": isOffline, "PendingCount": pendingCount, } c.Header("Content-Type", "text/html; charset=utf-8") if err := h.tmpl.ExecuteTemplate(c.Writer, "sync_status", data); err != nil { slog.Error("failed to render sync_status template", "error", err) c.String(http.StatusInternalServerError, "Template error: "+err.Error()) } }