diff --git a/cmd/qfs/main.go b/cmd/qfs/main.go index c1f7d3f..d44c4cd 100644 --- a/cmd/qfs/main.go +++ b/cmd/qfs/main.go @@ -652,6 +652,19 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect }) }) + configs.POST("/import", func(c *gin.Context) { + result, err := configService.ImportFromServer() + if err != nil { + if errors.Is(err, sync.ErrOffline) { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Database is offline"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, result) + }) + configs.POST("", func(c *gin.Context) { var req services.CreateConfigRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/internal/services/local_configuration.go b/internal/services/local_configuration.go index 3b281dc..d9cf265 100644 --- a/internal/services/local_configuration.go +++ b/internal/services/local_configuration.go @@ -4,10 +4,10 @@ import ( "encoding/json" "time" - "github.com/google/uuid" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/services/sync" + "github.com/google/uuid" ) // LocalConfigurationService handles configurations in local-first mode @@ -621,3 +621,8 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co return cfg, nil } + +// ImportFromServer imports configurations from MariaDB to local SQLite cache. +func (s *LocalConfigurationService) ImportFromServer() (*sync.ConfigImportResult, error) { + return s.syncService.ImportConfigurationsToLocal() +} diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go index e995da4..d8267f5 100644 --- a/internal/services/sync/service.go +++ b/internal/services/sync/service.go @@ -2,6 +2,7 @@ package sync import ( "encoding/json" + "errors" "fmt" "log/slog" "time" @@ -10,8 +11,11 @@ import ( "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/repository" + "gorm.io/gorm" ) +var ErrOffline = errors.New("database is offline") + // Service handles synchronization between MariaDB and local SQLite type Service struct { connMgr *db.ConnectionManager @@ -34,6 +38,71 @@ type SyncStatus struct { NeedsSync bool `json:"needs_sync"` } +// ConfigImportResult represents server->local configuration import stats. +type ConfigImportResult struct { + Imported int `json:"imported"` + Updated int `json:"updated"` + Skipped int `json:"skipped"` +} + +// ImportConfigurationsToLocal imports configurations from MariaDB into local SQLite. +// Existing local configs with pending local changes are skipped to avoid data loss. +func (s *Service) ImportConfigurationsToLocal() (*ConfigImportResult, error) { + mariaDB, err := s.connMgr.GetDB() + if err != nil { + return nil, ErrOffline + } + + configRepo := repository.NewConfigurationRepository(mariaDB) + result := &ConfigImportResult{} + + offset := 0 + const limit = 200 + for { + serverConfigs, _, err := configRepo.ListAll(offset, limit) + if err != nil { + return nil, fmt.Errorf("listing server configurations: %w", err) + } + if len(serverConfigs) == 0 { + break + } + + for i := range serverConfigs { + cfg := serverConfigs[i] + existing, err := s.localDB.GetConfigurationByUUID(cfg.UUID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("getting local configuration %s: %w", cfg.UUID, err) + } + + if existing != nil && err == nil && existing.SyncStatus == "pending" { + result.Skipped++ + continue + } + + localCfg := localdb.ConfigurationToLocal(&cfg) + now := time.Now() + localCfg.SyncedAt = &now + localCfg.SyncStatus = "synced" + localCfg.UpdatedAt = now + + if existing != nil && err == nil { + localCfg.ID = existing.ID + result.Updated++ + } else { + result.Imported++ + } + + if err := s.localDB.SaveConfiguration(localCfg); err != nil { + return nil, fmt.Errorf("saving local configuration %s: %w", cfg.UUID, err) + } + } + + offset += len(serverConfigs) + } + + return result, nil +} + // GetStatus returns the current sync status func (s *Service) GetStatus() (*SyncStatus, error) { lastSync := s.localDB.GetLastSyncTime() diff --git a/web/templates/configs.html b/web/templates/configs.html index a91b754..a6a4498 100644 --- a/web/templates/configs.html +++ b/web/templates/configs.html @@ -4,10 +4,13 @@