Add background sync worker and complete local-first architecture
Implements automatic background synchronization every 5 minutes: - Worker pushes pending changes to server (PushPendingChanges) - Worker pulls new pricelists (SyncPricelistsIfNeeded) - Graceful shutdown with context cancellation - Automatic online/offline detection via DB ping New files: - internal/services/sync/worker.go - Background sync worker - internal/services/local_configuration.go - Local-first CRUD - internal/localdb/converters.go - MariaDB ↔ SQLite converters Extended sync infrastructure: - Pending changes queue (pending_changes table) - Push/pull sync endpoints (/api/sync/push, /pending) - ConfigurationGetter interface for handler compatibility - LocalConfigurationService replaces ConfigurationService All configuration operations now run through SQLite with automatic background sync to MariaDB when online. Phase 2.5 nearly complete. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
161
internal/localdb/converters.go
Normal file
161
internal/localdb/converters.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package localdb
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
)
|
||||
|
||||
// ConfigurationToLocal converts models.Configuration to LocalConfiguration
|
||||
func ConfigurationToLocal(cfg *models.Configuration) *LocalConfiguration {
|
||||
items := make(LocalConfigItems, len(cfg.Items))
|
||||
for i, item := range cfg.Items {
|
||||
items[i] = LocalConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPrice,
|
||||
}
|
||||
}
|
||||
|
||||
local := &LocalConfiguration{
|
||||
UUID: cfg.UUID,
|
||||
Name: cfg.Name,
|
||||
Items: items,
|
||||
TotalPrice: cfg.TotalPrice,
|
||||
CustomPrice: cfg.CustomPrice,
|
||||
Notes: cfg.Notes,
|
||||
IsTemplate: cfg.IsTemplate,
|
||||
ServerCount: cfg.ServerCount,
|
||||
CreatedAt: cfg.CreatedAt,
|
||||
UpdatedAt: time.Now(),
|
||||
SyncStatus: "pending",
|
||||
OriginalUserID: cfg.UserID,
|
||||
}
|
||||
|
||||
if cfg.ID > 0 {
|
||||
serverID := cfg.ID
|
||||
local.ServerID = &serverID
|
||||
}
|
||||
|
||||
return local
|
||||
}
|
||||
|
||||
// LocalToConfiguration converts LocalConfiguration to models.Configuration
|
||||
func LocalToConfiguration(local *LocalConfiguration) *models.Configuration {
|
||||
items := make(models.ConfigItems, len(local.Items))
|
||||
for i, item := range local.Items {
|
||||
items[i] = models.ConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPrice,
|
||||
}
|
||||
}
|
||||
|
||||
cfg := &models.Configuration{
|
||||
UUID: local.UUID,
|
||||
UserID: local.OriginalUserID,
|
||||
Name: local.Name,
|
||||
Items: items,
|
||||
TotalPrice: local.TotalPrice,
|
||||
CustomPrice: local.CustomPrice,
|
||||
Notes: local.Notes,
|
||||
IsTemplate: local.IsTemplate,
|
||||
ServerCount: local.ServerCount,
|
||||
CreatedAt: local.CreatedAt,
|
||||
}
|
||||
|
||||
if local.ServerID != nil {
|
||||
cfg.ID = *local.ServerID
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// PricelistToLocal converts models.Pricelist to LocalPricelist
|
||||
func PricelistToLocal(pl *models.Pricelist) *LocalPricelist {
|
||||
name := pl.Notification
|
||||
if name == "" {
|
||||
name = pl.Version
|
||||
}
|
||||
|
||||
return &LocalPricelist{
|
||||
ServerID: pl.ID,
|
||||
Version: pl.Version,
|
||||
Name: name,
|
||||
CreatedAt: pl.CreatedAt,
|
||||
SyncedAt: time.Now(),
|
||||
IsUsed: false,
|
||||
}
|
||||
}
|
||||
|
||||
// LocalToPricelist converts LocalPricelist to models.Pricelist
|
||||
func LocalToPricelist(local *LocalPricelist) *models.Pricelist {
|
||||
return &models.Pricelist{
|
||||
ID: local.ServerID,
|
||||
Version: local.Version,
|
||||
Notification: local.Name,
|
||||
CreatedAt: local.CreatedAt,
|
||||
IsActive: true,
|
||||
}
|
||||
}
|
||||
|
||||
// PricelistItemToLocal converts models.PricelistItem to LocalPricelistItem
|
||||
func PricelistItemToLocal(item *models.PricelistItem, localPricelistID uint) *LocalPricelistItem {
|
||||
return &LocalPricelistItem{
|
||||
PricelistID: localPricelistID,
|
||||
LotName: item.LotName,
|
||||
Price: item.Price,
|
||||
}
|
||||
}
|
||||
|
||||
// LocalToPricelistItem converts LocalPricelistItem to models.PricelistItem
|
||||
func LocalToPricelistItem(local *LocalPricelistItem, serverPricelistID uint) *models.PricelistItem {
|
||||
return &models.PricelistItem{
|
||||
ID: local.ID,
|
||||
PricelistID: serverPricelistID,
|
||||
LotName: local.LotName,
|
||||
Price: local.Price,
|
||||
}
|
||||
}
|
||||
|
||||
// ComponentToLocal converts models.LotMetadata to LocalComponent
|
||||
func ComponentToLocal(meta *models.LotMetadata) *LocalComponent {
|
||||
var lotDesc string
|
||||
var category string
|
||||
|
||||
if meta.Lot != nil {
|
||||
lotDesc = meta.Lot.LotDescription
|
||||
}
|
||||
|
||||
// Extract category from lot_name (e.g., "CPU_AMD_9654" -> "CPU")
|
||||
if len(meta.LotName) > 0 {
|
||||
for i, ch := range meta.LotName {
|
||||
if ch == '_' {
|
||||
category = meta.LotName[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &LocalComponent{
|
||||
LotName: meta.LotName,
|
||||
LotDescription: lotDesc,
|
||||
Category: category,
|
||||
Model: meta.Model,
|
||||
CurrentPrice: meta.CurrentPrice,
|
||||
SyncedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// LocalToComponent converts LocalComponent to models.LotMetadata
|
||||
func LocalToComponent(local *LocalComponent) *models.LotMetadata {
|
||||
return &models.LotMetadata{
|
||||
LotName: local.LotName,
|
||||
Model: local.Model,
|
||||
CurrentPrice: local.CurrentPrice,
|
||||
Lot: &models.Lot{
|
||||
LotName: local.LotName,
|
||||
LotDescription: local.LotDescription,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user