refactor: migrate sync service and handlers to use ConnectionManager

Updated sync-related code to use ConnectionManager instead of direct
database references:

- SyncService now creates repositories on-demand when connection available
- SyncHandler uses ConnectionManager for lazy DB access
- Added ComponentFilter and ListComponents to localdb for offline queries
- All sync operations check connection status before attempting MariaDB access

This completes the transition to offline-first architecture where all
database access goes through ConnectionManager.

Part of Phase 2.5: Full Offline Mode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 23:29:36 +03:00
parent 3d222b7f14
commit 7f030e7db7
3 changed files with 267 additions and 39 deletions

View File

@@ -6,6 +6,7 @@ import (
"log/slog"
"time"
"git.mchus.pro/mchus/quoteforge/internal/db"
"git.mchus.pro/mchus/quoteforge/internal/localdb"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
@@ -13,17 +14,15 @@ import (
// Service handles synchronization between MariaDB and local SQLite
type Service struct {
pricelistRepo *repository.PricelistRepository
configRepo *repository.ConfigurationRepository
localDB *localdb.LocalDB
connMgr *db.ConnectionManager
localDB *localdb.LocalDB
}
// NewService creates a new sync service
func NewService(pricelistRepo *repository.PricelistRepository, configRepo *repository.ConfigurationRepository, localDB *localdb.LocalDB) *Service {
func NewService(connMgr *db.ConnectionManager, localDB *localdb.LocalDB) *Service {
return &Service{
pricelistRepo: pricelistRepo,
configRepo: configRepo,
localDB: localDB,
connMgr: connMgr,
localDB: localDB,
}
}
@@ -39,10 +38,14 @@ type SyncStatus struct {
func (s *Service) GetStatus() (*SyncStatus, error) {
lastSync := s.localDB.GetLastSyncTime()
// Count server pricelists
serverPricelists, _, err := s.pricelistRepo.List(0, 1)
if err != nil {
return nil, fmt.Errorf("counting server pricelists: %w", err)
// Count server pricelists (requires connection)
serverCount := 0
if mariaDB, err := s.connMgr.GetDB(); err == nil && mariaDB != nil {
pricelistRepo := repository.NewPricelistRepository(mariaDB)
serverPricelists, _, err := pricelistRepo.List(0, 1)
if err == nil {
serverCount = len(serverPricelists)
}
}
// Count local pricelists
@@ -52,7 +55,7 @@ func (s *Service) GetStatus() (*SyncStatus, error) {
return &SyncStatus{
LastSyncAt: lastSync,
ServerPricelists: len(serverPricelists),
ServerPricelists: serverCount,
LocalPricelists: int(localCount),
NeedsSync: needsSync,
}, nil
@@ -73,8 +76,15 @@ func (s *Service) NeedSync() (bool, error) {
return true, nil
}
// Check if there are new pricelists on server
latestServer, err := s.pricelistRepo.GetLatestActive()
// Check if there are new pricelists on server (requires connection)
mariaDB, err := s.connMgr.GetDB()
if err != nil {
// If offline, can't check server, no need to sync
return false, nil
}
pricelistRepo := repository.NewPricelistRepository(mariaDB)
latestServer, err := pricelistRepo.GetLatestActive()
if err != nil {
// If no pricelists on server, no need to sync
return false, nil
@@ -98,18 +108,29 @@ func (s *Service) NeedSync() (bool, error) {
func (s *Service) SyncPricelists() (int, error) {
slog.Info("starting pricelist sync")
// Get database connection
mariaDB, err := s.connMgr.GetDB()
if err != nil {
return 0, fmt.Errorf("database not available: %w", err)
}
// Create repository
pricelistRepo := repository.NewPricelistRepository(mariaDB)
// Get all active pricelists from server (up to 100)
serverPricelists, _, err := s.pricelistRepo.List(0, 100)
serverPricelists, _, err := pricelistRepo.List(0, 100)
if err != nil {
return 0, fmt.Errorf("getting server pricelists: %w", err)
}
synced := 0
var latestLocalID uint
for _, pl := range serverPricelists {
// Check if pricelist already exists locally
existing, _ := s.localDB.GetLocalPricelistByServerID(pl.ID)
if existing != nil {
// Already synced, skip
// Already synced, track latest
latestLocalID = existing.ID
continue
}
@@ -128,8 +149,27 @@ func (s *Service) SyncPricelists() (int, error) {
continue
}
// Sync items for the newly created pricelist
itemCount, err := s.SyncPricelistItems(localPL.ID)
if err != nil {
slog.Warn("failed to sync pricelist items", "version", pl.Version, "error", err)
// Continue even if items sync fails - we have the pricelist metadata
} else {
slog.Debug("synced pricelist with items", "version", pl.Version, "items", itemCount)
}
latestLocalID = localPL.ID
synced++
slog.Debug("synced pricelist", "version", pl.Version, "server_id", pl.ID)
}
// Update component prices from latest pricelist
if latestLocalID > 0 {
updated, err := s.localDB.UpdateComponentPricesFromPricelist(latestLocalID)
if err != nil {
slog.Warn("failed to update component prices from pricelist", "error", err)
} else {
slog.Info("updated component prices from latest pricelist", "updated", updated)
}
}
// Update last sync time
@@ -154,8 +194,17 @@ func (s *Service) SyncPricelistItems(localPricelistID uint) (int, error) {
return int(existingCount), nil
}
// Get database connection
mariaDB, err := s.connMgr.GetDB()
if err != nil {
return 0, fmt.Errorf("database not available: %w", err)
}
// Create repository
pricelistRepo := repository.NewPricelistRepository(mariaDB)
// Get items from server
serverItems, _, err := s.pricelistRepo.GetItems(localPL.ServerID, 0, 10000, "")
serverItems, _, err := pricelistRepo.GetItems(localPL.ServerID, 0, 10000, "")
if err != nil {
return 0, fmt.Errorf("getting server pricelist items: %w", err)
}
@@ -312,8 +361,17 @@ func (s *Service) pushConfigurationCreate(change *localdb.PendingChange) error {
return fmt.Errorf("unmarshaling configuration: %w", err)
}
// Get database connection
mariaDB, err := s.connMgr.GetDB()
if err != nil {
return fmt.Errorf("database not available: %w", err)
}
// Create repository
configRepo := repository.NewConfigurationRepository(mariaDB)
// Create on server
if err := s.configRepo.Create(&cfg); err != nil {
if err := configRepo.Create(&cfg); err != nil {
return fmt.Errorf("creating configuration on server: %w", err)
}
@@ -337,6 +395,15 @@ func (s *Service) pushConfigurationUpdate(change *localdb.PendingChange) error {
return fmt.Errorf("unmarshaling configuration: %w", err)
}
// Get database connection
mariaDB, err := s.connMgr.GetDB()
if err != nil {
return fmt.Errorf("database not available: %w", err)
}
// Create repository
configRepo := repository.NewConfigurationRepository(mariaDB)
// Ensure we have a server ID before updating
// If the payload doesn't have ID, get it from local configuration
if cfg.ID == 0 {
@@ -347,7 +414,7 @@ func (s *Service) pushConfigurationUpdate(change *localdb.PendingChange) error {
if localCfg.ServerID == nil {
// Configuration hasn't been synced yet, try to find it on server by UUID
serverCfg, err := s.configRepo.GetByUUID(cfg.UUID)
serverCfg, err := configRepo.GetByUUID(cfg.UUID)
if err != nil {
return fmt.Errorf("configuration not yet synced to server: %w", err)
}
@@ -363,7 +430,7 @@ func (s *Service) pushConfigurationUpdate(change *localdb.PendingChange) error {
}
// Update on server
if err := s.configRepo.Update(&cfg); err != nil {
if err := configRepo.Update(&cfg); err != nil {
return fmt.Errorf("updating configuration on server: %w", err)
}
@@ -380,8 +447,17 @@ func (s *Service) pushConfigurationUpdate(change *localdb.PendingChange) error {
// pushConfigurationDelete deletes a configuration from the server
func (s *Service) pushConfigurationDelete(change *localdb.PendingChange) error {
// Get database connection
mariaDB, err := s.connMgr.GetDB()
if err != nil {
return fmt.Errorf("database not available: %w", err)
}
// Create repository
configRepo := repository.NewConfigurationRepository(mariaDB)
// Get the configuration from server by UUID to get the ID
cfg, err := s.configRepo.GetByUUID(change.EntityUUID)
cfg, err := configRepo.GetByUUID(change.EntityUUID)
if err != nil {
// Already deleted or not found, consider it successful
slog.Warn("configuration not found on server, considering delete successful", "uuid", change.EntityUUID)
@@ -389,7 +465,7 @@ func (s *Service) pushConfigurationDelete(change *localdb.PendingChange) error {
}
// Delete from server
if err := s.configRepo.Delete(cfg.ID); err != nil {
if err := configRepo.Delete(cfg.ID); err != nil {
return fmt.Errorf("deleting configuration from server: %w", err)
}