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:
2026-02-01 22:17:00 +03:00
parent 143d217397
commit be77256d4e
11 changed files with 1131 additions and 27 deletions

View File

@@ -12,13 +12,13 @@ import (
type ExportHandler struct {
exportService *services.ExportService
configService *services.ConfigurationService
configService services.ConfigurationGetter
componentService *services.ComponentService
}
func NewExportHandler(
exportService *services.ExportService,
configService *services.ConfigurationService,
configService services.ConfigurationGetter,
componentService *services.ComponentService,
) *ExportHandler {
return &ExportHandler{

View File

@@ -215,3 +215,58 @@ func (h *SyncHandler) checkOnline() bool {
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,
})
}