- Implement RefreshPrices for local-first mode - Update prices from local_components.current_price cache - Graceful degradation when component not found - Add PriceUpdatedAt timestamp to LocalConfiguration model - Support both authenticated and no-auth price refresh - Fix sync duplicate entry bug - pushConfigurationUpdate now ensures server_id exists before update - Fetch from LocalConfiguration.ServerID or search on server if missing - Update local config with server_id after finding - Add application auto-restart after settings save - Implement restartProcess() using syscall.Exec - Setup handler signals restart via channel - Setup page polls /health endpoint and redirects when ready - Add "Back" button on setup page when settings exist - Fix setup handler password handling - Use PasswordEncrypted field consistently - Support empty password by using saved value - Improve sync status handling - Add fallback for is_offline check in SyncStatusPartial - Enhance background sync logging with prefixes - Update CLAUDE.md documentation - Mark Phase 2.5 tasks as complete - Add UI Improvements section with future tasks - Update SQLite tables documentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
94 lines
2.0 KiB
Go
94 lines
2.0 KiB
Go
package sync
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Worker performs background synchronization at regular intervals
|
|
type Worker struct {
|
|
service *Service
|
|
db *gorm.DB
|
|
interval time.Duration
|
|
logger *slog.Logger
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
// NewWorker creates a new background sync worker
|
|
func NewWorker(service *Service, db *gorm.DB, interval time.Duration) *Worker {
|
|
return &Worker{
|
|
service: service,
|
|
db: db,
|
|
interval: interval,
|
|
logger: slog.Default(),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// isOnline checks if the database connection is available
|
|
func (w *Worker) isOnline() bool {
|
|
sqlDB, err := w.db.DB()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return sqlDB.Ping() == nil
|
|
}
|
|
|
|
// Start begins the background sync loop in a goroutine
|
|
func (w *Worker) Start(ctx context.Context) {
|
|
w.logger.Info("starting background sync worker", "interval", w.interval)
|
|
|
|
ticker := time.NewTicker(w.interval)
|
|
defer ticker.Stop()
|
|
|
|
// Run once immediately
|
|
w.runSync()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
w.logger.Info("background sync worker stopped by context")
|
|
return
|
|
case <-w.stopCh:
|
|
w.logger.Info("background sync worker stopped")
|
|
return
|
|
case <-ticker.C:
|
|
w.runSync()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop gracefully stops the worker
|
|
func (w *Worker) Stop() {
|
|
w.logger.Info("stopping background sync worker")
|
|
close(w.stopCh)
|
|
}
|
|
|
|
// runSync performs a single sync iteration
|
|
func (w *Worker) runSync() {
|
|
// Check if online
|
|
if !w.isOnline() {
|
|
w.logger.Debug("offline, skipping background sync")
|
|
return
|
|
}
|
|
|
|
// Push pending changes first
|
|
pushed, err := w.service.PushPendingChanges()
|
|
if err != nil {
|
|
w.logger.Warn("background sync: failed to push pending changes", "error", err)
|
|
} else if pushed > 0 {
|
|
w.logger.Info("background sync: pushed pending changes", "count", pushed)
|
|
}
|
|
|
|
// Then check for new pricelists
|
|
err = w.service.SyncPricelistsIfNeeded()
|
|
if err != nil {
|
|
w.logger.Warn("background sync: failed to sync pricelists", "error", err)
|
|
}
|
|
|
|
w.logger.Info("background sync cycle completed")
|
|
}
|