diff --git a/frontend/app.js b/frontend/app.js index c102481..62bb08b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -969,9 +969,21 @@ async function calculateAndStartRoute(startAddress, endAddress, isRestoring = fa }; // Initialize step reference + // Initialize/Reset Server Trip // Only start a new trip on server if this is a fresh start, not a restore if (!isRestoring) { - fetch('/api/trip', { method: 'POST' }).catch(console.error); + const tripMetadata = { + trip_type: 'address', + route_name: `${leg.start_address} to ${leg.end_address}`, + start_address: leg.start_address, + end_address: leg.end_address, + total_distance: routeTotalDistance / 1000 // In km + }; + fetch('/api/trip', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(tripMetadata) + }).catch(console.error); } // Reset display steps for visual consistency @@ -2126,7 +2138,17 @@ function startFromCustomRoute(pathData, routeName, isRestoring = false, markers // Initialize/Reset Server Trip if (!isRestoring) { - fetch('/api/trip', { method: 'POST' }).catch(console.error); + const tripMetadata = { + trip_type: 'kml', + route_name: routeName, + kml_id: currentKmlFile ? currentKmlFile.id : null, + total_distance: routeTotalDistance / 1000 // In km + }; + fetch('/api/trip', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(tripMetadata) + }).catch(console.error); } // Reset display steps @@ -2274,26 +2296,13 @@ async function triggerCelebration(finalDistance) { overlay.classList.add('active'); createConfetti(); - // Report to backend - if (currentTripDetails) { - try { - await fetch('/api/trip/complete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: currentTripDetails.type, - route_name: currentTripDetails.route_name, - start_address: currentTripDetails.start_address || '', - end_address: currentTripDetails.end_address || '', - kml_filename: currentTripDetails.kml_filename || '', - kml_owner_id: currentTripDetails.kml_owner_id || '', - distance: currentTripDetails.distance - }) - }); - } catch (err) { - console.error("Failed to record trip completion:", err); - } - } + // Clean up local state + localStorage.removeItem(LOCATION_STORAGE); + currentTripDetails = null; + masterPath = []; + routeTotalDistance = 0; + // We keep window.hasCelebrated = true to prevent immediate re-trigger + // but starting a new trip will reset it. } function createConfetti() { diff --git a/server/db.go b/server/db.go index 9ae6a07..908c412 100644 --- a/server/db.go +++ b/server/db.go @@ -134,5 +134,13 @@ func createTables() { // Migration for KML description db.Exec("ALTER TABLE kml_metadata ADD COLUMN IF NOT EXISTS description TEXT") + // Migration for trips metadata + db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS trip_type ENUM('address', 'kml') DEFAULT 'address'") + db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS route_name TEXT") + db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS start_address TEXT") + db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS end_address TEXT") + db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS kml_id INT") + db.Exec("ALTER TABLE trips ADD COLUMN IF NOT EXISTS total_distance DOUBLE") + log.Println("Database tables initialized") } diff --git a/server/kml.go b/server/kml.go index 79df41a..181cf44 100644 --- a/server/kml.go +++ b/server/kml.go @@ -687,7 +687,10 @@ func HandleUserProfile(w http.ResponseWriter, r *http.Request) { // Fetch completed trips rows, err := db.Query(` - SELECT ct.id, ct.trip_type, ct.route_name, ct.start_address, ct.end_address, ct.kml_id, ct.distance, ct.completed_at, + SELECT ct.id, ct.trip_type, ct.route_name, + COALESCE(ct.start_address, '') as start_address, + COALESCE(ct.end_address, '') as end_address, + ct.kml_id, ct.distance, ct.completed_at, m.filename, m.user_id, u.display_name, m.description, COALESCE((SELECT SUM(vote) FROM kml_votes WHERE kml_id = m.id), 0) as votes FROM completed_trips ct @@ -731,51 +734,3 @@ func HandleUserProfile(w http.ResponseWriter, r *http.Request) { } // HandleTripComplete records a completed trip -func HandleTripComplete(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - userID, ok := getUserID(r.Context()) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - var req struct { - Type string `json:"type"` - RouteName string `json:"route_name"` - StartAddress string `json:"start_address"` - EndAddress string `json:"end_address"` - KmlFilename string `json:"kml_filename"` - KmlOwnerID string `json:"kml_owner_id"` - Distance float64 `json:"distance"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - var kmlID interface{} = nil - if req.Type == "kml" { - var id int - err := db.QueryRow("SELECT id FROM kml_metadata WHERE user_id = ? AND filename = ?", req.KmlOwnerID, req.KmlFilename).Scan(&id) - if err == nil { - kmlID = id - } - } - - _, err := db.Exec(` - INSERT INTO completed_trips (user_id, trip_type, route_name, start_address, end_address, kml_id, distance) - VALUES (?, ?, ?, ?, ?, ?, ?) - `, userID, req.Type, req.RouteName, req.StartAddress, req.EndAddress, kmlID, req.Distance) - - if err != nil { - http.Error(w, fmt.Sprintf("Failed to save completed trip: %v", err), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} diff --git a/server/main.go b/server/main.go index 11a61bf..96a9d2c 100644 --- a/server/main.go +++ b/server/main.go @@ -96,9 +96,16 @@ func main() { return } + var metadata TripState + if err := json.NewDecoder(r.Body).Decode(&metadata); err != nil { + // Fallback for legacy calls or if no metadata is sent + // But we expect metadata now + fmt.Printf("[API Trip] Warning: Failed to decode metadata: %v\n", err) + } + userID, _ := getUserID(r.Context()) sm := getStepManager(userID) - sm.StartNewTrip() + sm.StartNewTrip(metadata) w.WriteHeader(http.StatusOK) })) @@ -140,9 +147,6 @@ func main() { // 7. User Profile Endpoint http.HandleFunc("/api/user/profile", RequireAuth(HandleUserProfile)) - // 8. Trip Completion Endpoint - http.HandleFunc("/api/trip/complete", RequireAuth(HandleTripComplete)) - // 9. Start Server binding := "0.0.0.0:8080" fmt.Printf("Server starting on http://%s\n", binding) diff --git a/server/step_manager.go b/server/step_manager.go index ec4ee92..65568b6 100644 --- a/server/step_manager.go +++ b/server/step_manager.go @@ -7,11 +7,21 @@ import ( "time" ) +const METERS_PER_STEP = 0.762 // Match frontend + type TripState struct { StartDate string `json:"start_date"` // YYYY-MM-DD StartTime time.Time `json:"start_time"` // Exact start time StartDayInitialSteps int `json:"start_day_initial_steps"` // Steps on the tracker when trip started DailyCache map[string]int `json:"daily_cache"` // Cache of steps for past days + + // Trip Metadata + TripType string `json:"trip_type"` + RouteName string `json:"route_name"` + StartAddress string `json:"start_address"` + EndAddress string `json:"end_address"` + KmlID *int `json:"kml_id"` + TotalDistance float64 `json:"total_distance"` // in km } type StepManager struct { @@ -47,13 +57,20 @@ func (sm *StepManager) LoadTripState() error { func (sm *StepManager) loadTripStateLocked() error { var startTime time.Time + var kmlID sql.NullInt64 err := db.QueryRow(` - SELECT start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time + SELECT start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time, + trip_type, route_name, start_address, end_address, kml_id, total_distance FROM trips WHERE user_id = ? `, sm.userID).Scan( &sm.tripState.StartDate, &startTime, &sm.tripState.StartDayInitialSteps, &sm.previousTotalSteps, &sm.targetTotalSteps, &sm.lastSyncTime, &sm.nextSyncTime, + &sm.tripState.TripType, &sm.tripState.RouteName, &sm.tripState.StartAddress, &sm.tripState.EndAddress, &kmlID, &sm.tripState.TotalDistance, ) + if err == nil && kmlID.Valid { + id := int(kmlID.Int64) + sm.tripState.KmlID = &id + } if err != nil { if err == sql.ErrNoRows { // Initialize with defaults if no trip exists @@ -94,8 +111,9 @@ func (sm *StepManager) SaveTripState() { 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 (?, ?, ?, ?, ?, ?, ?, ?) + 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, + trip_type, route_name, start_address, end_address, kml_id, total_distance) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE start_date = VALUES(start_date), start_time = VALUES(start_time), @@ -103,9 +121,16 @@ func (sm *StepManager) saveTripStateLocked() { previous_total_steps = VALUES(previous_total_steps), target_total_steps = VALUES(target_total_steps), last_sync_time = VALUES(last_sync_time), - next_sync_time = VALUES(next_sync_time) + next_sync_time = VALUES(next_sync_time), + trip_type = VALUES(trip_type), + route_name = VALUES(route_name), + start_address = VALUES(start_address), + end_address = VALUES(end_address), + kml_id = VALUES(kml_id), + total_distance = VALUES(total_distance) `, sm.userID, sm.tripState.StartDate, sm.tripState.StartTime, sm.tripState.StartDayInitialSteps, - sm.previousTotalSteps, sm.targetTotalSteps, sm.lastSyncTime, sm.nextSyncTime) + sm.previousTotalSteps, sm.targetTotalSteps, sm.lastSyncTime, sm.nextSyncTime, + sm.tripState.TripType, sm.tripState.RouteName, sm.tripState.StartAddress, sm.tripState.EndAddress, sm.tripState.KmlID, sm.tripState.TotalDistance) if err != nil { fmt.Printf("Error saving trip state: %v\n", err) } @@ -123,7 +148,7 @@ func (sm *StepManager) saveTripStateLocked() { } } -func (sm *StepManager) StartNewTrip() { +func (sm *StepManager) StartNewTrip(metadata TripState) { sm.mu.Lock() defer sm.mu.Unlock() now := time.Now() @@ -137,6 +162,12 @@ func (sm *StepManager) StartNewTrip() { StartTime: now, StartDayInitialSteps: initialSteps, DailyCache: make(map[string]int), + TripType: metadata.TripType, + RouteName: metadata.RouteName, + StartAddress: metadata.StartAddress, + EndAddress: metadata.EndAddress, + KmlID: metadata.KmlID, + TotalDistance: metadata.TotalDistance, } // On new trip, previous total is 0 sm.previousTotalSteps = 0 @@ -244,8 +275,6 @@ func (sm *StepManager) performSync(interval time.Duration) { sm.lastSyncTime = time.Now() sm.nextSyncTime = time.Now().Add(interval) - // Save final state with explicit NOW() for last_sync_time to ensure DB consistency - // next_sync_time is also calculated in SQL relative to the same NOW() _, err = db.Exec(` UPDATE trips SET last_sync_time = NOW(), @@ -258,9 +287,42 @@ func (sm *StepManager) performSync(interval time.Duration) { fmt.Printf("Error saving sync completion: %v\n", err) } + // Check for Trip Completion + if sm.tripState.TotalDistance > 0 { + currentDistance := float64(sm.targetTotalSteps) * METERS_PER_STEP / 1000.0 + if currentDistance >= sm.tripState.TotalDistance { + fmt.Printf("[StepManager] Trip Fulfillment Detected! %.2f / %.2f km\n", currentDistance, sm.tripState.TotalDistance) + sm.recordTripCompletionLocked() + } + } + fmt.Printf("Sync Complete. Total Trip Steps: %d\n", sm.targetTotalSteps) } +func (sm *StepManager) recordTripCompletionLocked() { + // 1. Insert into completed_trips + _, err := db.Exec(` + INSERT INTO completed_trips (user_id, trip_type, route_name, start_address, end_address, kml_id, distance) + VALUES (?, ?, ?, ?, ?, ?, ?) + `, sm.userID, sm.tripState.TripType, sm.tripState.RouteName, sm.tripState.StartAddress, sm.tripState.EndAddress, sm.tripState.KmlID, sm.tripState.TotalDistance) + + if err != nil { + fmt.Printf("[StepManager] Error recording completion: %v\n", err) + return + } + + // 2. Delete from active trips + _, err = db.Exec("DELETE FROM trips WHERE user_id = ?", sm.userID) + if err != nil { + fmt.Printf("[StepManager] Error clearing active trip: %v\n", err) + } + + // 3. Reset local state + sm.tripState.StartDate = "" + sm.previousTotalSteps = 0 + sm.targetTotalSteps = 0 +} + // calculateSmoothedTokenAt returns the interpolated step count at a given time func (sm *StepManager) calculateSmoothedTokenAt(t time.Time) int { totalDuration := sm.nextSyncTime.Sub(sm.lastSyncTime)