diff --git a/server/main.go b/server/main.go index bcb9c32..d0facaa 100644 --- a/server/main.go +++ b/server/main.go @@ -11,6 +11,19 @@ import ( _ "github.com/go-sql-driver/mysql" ) +// RecoveryMiddleware catches panics and returns 500 +func RecoveryMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("[PANIC] %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} + // getStepManager creates a new StepManager for the given user, loading state from DB func getStepManager(userID string) *StepManager { return NewStepManager(userID) @@ -125,5 +138,5 @@ func main() { // 6. Start Server binding := "0.0.0.0:8080" fmt.Printf("Server starting on http://%s\n", binding) - log.Fatal(http.ListenAndServe(binding, nil)) + log.Fatal(http.ListenAndServe(binding, RecoveryMiddleware(http.DefaultServeMux))) } diff --git a/server/step_manager.go b/server/step_manager.go index b019367..7d42d8b 100644 --- a/server/step_manager.go +++ b/server/step_manager.go @@ -28,15 +28,24 @@ type StepManager struct { } func NewStepManager(userID string) *StepManager { + now := time.Now() sm := &StepManager{ userID: userID, syncInterval: 15 * time.Minute, + lastSyncTime: now.Add(-15 * time.Minute), + nextSyncTime: now.Add(24 * time.Hour), // Don't sync unless a trip is started or loaded } sm.LoadTripState() return sm } func (sm *StepManager) LoadTripState() error { + sm.mu.Lock() + defer sm.mu.Unlock() + return sm.loadTripStateLocked() +} + +func (sm *StepManager) loadTripStateLocked() error { var startTime time.Time err := db.QueryRow(` SELECT start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time @@ -47,7 +56,11 @@ func (sm *StepManager) LoadTripState() error { ) if err != nil { if err == sql.ErrNoRows { - return nil // Normal for first run + // Initialize with defaults if no trip exists + now := time.Now() + sm.tripState.StartDate = "" // No active trip + sm.nextSyncTime = now.Add(24 * time.Hour) + return nil } return err } @@ -74,6 +87,12 @@ func (sm *StepManager) LoadTripState() error { } func (sm *StepManager) SaveTripState() { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.saveTripStateLocked() +} + +func (sm *StepManager) saveTripStateLocked() { _, err := db.Exec(` INSERT INTO trips (user_id, start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?) @@ -122,7 +141,7 @@ func (sm *StepManager) StartNewTrip() { // On new trip, previous total is 0 sm.previousTotalSteps = 0 sm.targetTotalSteps = 0 - sm.SaveTripState() + sm.saveTripStateLocked() // Trigger immediate sync to set baseline go sm.Sync() @@ -153,7 +172,19 @@ func (sm *StepManager) performSync(interval time.Duration) { today := time.Now().Format("2006-01-02") // Parse start date in local time - start, _ := time.ParseInLocation("2006-01-02", tripStateCopy.StartDate, time.Local) + if tripStateCopy.StartDate == "" { + fmt.Println("[StepManager] Skipping sync: No active trip start date") + sm.mu.Lock() + sm.nextSyncTime = time.Now().Add(1 * time.Hour) // Check again in an hour + sm.mu.Unlock() + return + } + + start, err := time.ParseInLocation("2006-01-02", tripStateCopy.StartDate, time.Local) + if err != nil { + fmt.Printf("[StepManager] ERROR: Invalid start date '%s': %v\n", tripStateCopy.StartDate, err) + return + } end, _ := time.ParseInLocation("2006-01-02", today, time.Local) // Iterate from Start Date to Today @@ -204,7 +235,7 @@ func (sm *StepManager) performSync(interval time.Duration) { // Update State sm.tripState.DailyCache = newDailyCache - sm.SaveTripState() + sm.saveTripStateLocked() // Update Smoothing Targets sm.previousTotalSteps = sm.calculateSmoothedTokenAt(time.Now()) // Snapshot current interpolated value as new start @@ -239,22 +270,23 @@ func (sm *StepManager) calculateSmoothedTokenAt(t time.Time) int { } func (sm *StepManager) GetStatus() map[string]interface{} { - sm.mu.Lock() - defer sm.mu.Unlock() - // Reload from DB to get latest sync results from other instances sm.LoadTripState() - // Auto-trigger sync if needed - if time.Now().After(sm.nextSyncTime) { - sm.Sync() // Sync and save to DB - } - + sm.mu.Lock() + shouldSync := time.Now().After(sm.nextSyncTime) + nextSyncMilli := sm.nextSyncTime.UnixMilli() currentSmoothed := sm.calculateSmoothedTokenAt(time.Now()) + sm.mu.Unlock() + + // Auto-trigger sync if needed (out of initial lock to avoid deadlock) + if shouldSync { + go sm.Sync() // Async sync + } return map[string]interface{}{ "tripSteps": currentSmoothed, - "nextSyncTime": sm.nextSyncTime.UnixMilli(), + "nextSyncTime": nextSyncMilli, } }