package main import ( "encoding/json" "fmt" "log" "net/http" "os" "sync" "time" ) var ( stepManagers = make(map[string]*StepManager) // userID -> StepManager smMutex sync.RWMutex ) // getOrCreateStepManager retrieves or creates a StepManager for the given user func getOrCreateStepManager(userID string) *StepManager { smMutex.RLock() sm, exists := stepManagers[userID] smMutex.RUnlock() if exists { return sm } // Create new StepManager for this user smMutex.Lock() defer smMutex.Unlock() // Double-check it wasn't created while we were waiting for the lock if sm, exists := stepManagers[userID]; exists { return sm } sm = NewStepManager(userID) stepManagers[userID] = sm return sm } func initTimezone() { tz := os.Getenv("TZ") if tz != "" { loc, err := time.LoadLocation(tz) if err != nil { log.Printf("Error loading timezone %s: %v. Using UTC.", tz, err) return } time.Local = loc log.Printf("Timezone set to %s", tz) } else { log.Printf("TZ environment variable not set, using system default (usually UTC)") } log.Printf("Current system time: %s", time.Now().Format(time.RFC1123Z)) } func main() { // Initialize components initTimezone() InitFitbit() InitUserRegistry() InitVoteRegistry() // 1. Serve Static Files (Frontend) fs := http.FileServer(http.Dir("frontend")) http.Handle("/", fs) // 2. API Endpoints (all require authentication) http.HandleFunc("/api/status", RequireAuth(func(w http.ResponseWriter, r *http.Request) { userID, _ := getUserID(r.Context()) sm := getOrCreateStepManager(userID) status := sm.GetStatus() // Add user info to status user, exists := userRegistry.GetUser(userID) if exists && user != nil { status["user"] = map[string]string{ "displayName": user.DisplayName, "avatarUrl": user.AvatarURL, } } else { fmt.Printf("[API Status] WARNING: User info not found for ID: %s (exists=%v)\n", userID, exists) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(status) })) http.HandleFunc("/api/refresh", RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID, _ := getUserID(r.Context()) sm := getOrCreateStepManager(userID) sm.Sync() w.WriteHeader(http.StatusOK) })) http.HandleFunc("/api/trip", RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID, _ := getUserID(r.Context()) sm := getOrCreateStepManager(userID) sm.StartNewTrip() w.WriteHeader(http.StatusOK) })) http.HandleFunc("/api/drain", RequireAuth(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID, _ := getUserID(r.Context()) sm := getOrCreateStepManager(userID) go sm.Drain() // Async so we don't block w.WriteHeader(http.StatusOK) })) // 3. KML Management Endpoints http.HandleFunc("/api/kml/upload", RequireAuth(HandleKMLUpload)) http.HandleFunc("/api/kml/list", RequireAuth(HandleKMLList)) http.HandleFunc("/api/kml/privacy", RequireAuth(HandleKMLPrivacyToggle)) http.HandleFunc("/api/kml/vote", RequireAuth(HandleKMLVote)) http.HandleFunc("/api/kml/delete", RequireAuth(HandleKMLDelete)) http.HandleFunc("/api/kml/download", RequireAuth(HandleKMLDownload)) // 4. Fitbit OAuth Endpoints http.HandleFunc("/auth/fitbit", HandleFitbitAuth) http.HandleFunc("/auth/callback", HandleFitbitCallback) // 5. Logout Endpoint http.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) { session, err := GetSessionFromRequest(r) if err == nil { DeleteSession(session.Token) } ClearSessionCookie(w) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) }) // 6. Start Server binding := "0.0.0.0:8080" fmt.Printf("Server starting on http://%s\n", binding) log.Fatal(http.ListenAndServe(binding, nil)) }