Files
QuoteForge/internal/services/sync/worker.go
Mikhail Chusavitin 9bd2acd4f7 Add offline RefreshPrices, fix sync bugs, implement auto-restart
- 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>
2026-02-02 11:03:41 +03:00

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")
}