From aebd0f21a3e5c771ed41571140b83cbfd415469d Mon Sep 17 00:00:00 2001 From: Steven Polley Date: Sun, 18 Jan 2026 09:55:53 -0700 Subject: [PATCH] support for resume on multiple devices, fix camera freeze bug when opening setup overlay --- frontend/app.js | 101 +++++++++++++++++++++++++++++++++++++---- server/step_manager.go | 18 +++++++- 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 62bb08b..93485e5 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -824,6 +824,11 @@ function showSetupOverlay() { function hideSetupOverlay() { document.getElementById('setup-overlay').classList.remove('active'); + + // Resume animation if a trip is active + if (localStorage.getItem(LOCATION_STORAGE)) { + startCameraAnimation(); + } } function setupEventListeners() { @@ -989,6 +994,8 @@ async function calculateAndStartRoute(startAddress, endAddress, isRestoring = fa // Reset display steps for visual consistency displayTripSteps = 0; stateBuffer = []; + window.lastPositionUpdateDistance = 0; + window.lastPanoUpdate = 0; startFromLocation(leg.start_location, locationData.startAddress); hideSetupOverlay(); @@ -1169,7 +1176,7 @@ function resetLocation() { showSetupOverlay(); document.getElementById('startLocationInput').value = ''; document.getElementById('endLocationInput').value = ''; - stopAnimation(); + // stopAnimation(); // Don't stop animation just because we opened the setup overlay } // Rate limiting for refresh @@ -1230,14 +1237,64 @@ async function fetchStatus() { const data = await response.json(); const now = Date.now(); - // Update target steps from server - if (data.tripSteps !== undefined) { - // Add to buffer - stateBuffer.push({ t: now, s: data.tripSteps }); + // Server-Side Trip Synchronization + if (data.activeTrip) { + // A trip is active on the server - // Keep buffer size manageable (20 points is ~1.5 mins) - if (stateBuffer.length > 20) { - stateBuffer.shift(); + // Check if we need to sync/resume this trip locally + const localLocation = localStorage.getItem(LOCATION_STORAGE); + let needsResync = false; + + if (!localLocation) { + needsResync = true; + console.log("[Sync] No local trip found. Syncing from server."); + } else { + // We have a local trip, check if it's the SAME trip + try { + const localData = JSON.parse(localLocation); + if (localData.routeName !== data.activeTrip.route_name) { + needsResync = true; + console.log("[Sync] Local trip route mismatch. Syncing from server."); + } + } catch (e) { + needsResync = true; + } + } + + if (needsResync) { + syncActiveTrip(data.activeTrip); + } + + // Update target steps from server + if (data.tripSteps !== undefined) { + // Safety: Only accept step updates if they are for the current trip + if (needsResync) { + stateBuffer = []; + displayTripSteps = 0; + } + + stateBuffer.push({ t: now, s: data.tripSteps }); + + // Keep buffer size manageable (20 points is ~1.5 mins) + if (stateBuffer.length > 20) { + stateBuffer.shift(); + } + } + } else { + // No trip active on the server + if (localStorage.getItem(LOCATION_STORAGE)) { + console.log("[Sync] No trip on server. Clearing local trip."); + localStorage.removeItem(LOCATION_STORAGE); + + if (!document.getElementById('celebration-overlay').classList.contains('active')) { + document.getElementById('routeInfo').textContent = 'Not Started'; + document.getElementById('currentSteps').textContent = '0'; + document.getElementById('tripMeter').textContent = '0.0 / 0.0 km'; + masterPath = []; + routeTotalDistance = 0; + displayTripSteps = 0; + stateBuffer = []; + } } } @@ -1254,6 +1311,24 @@ async function fetchStatus() { } } +async function syncActiveTrip(activeTrip) { + console.log("[Sync] Restoring trip from server:", activeTrip.route_name); + + if (activeTrip.trip_type === 'address') { + calculateAndStartRoute(activeTrip.start_address, activeTrip.end_address, true); + } else if (activeTrip.trip_type === 'kml') { + try { + const response = await fetch(`/api/kml/download?owner_id=${activeTrip.kml_owner_id || currentUserID}&filename=${encodeURIComponent(activeTrip.route_name)}`); + if (response.ok) { + const kmlContent = await response.text(); + processKML(kmlContent, activeTrip.route_name); + } + } catch (err) { + console.error("[Sync] Failed to download KML for sync:", err); + } + } +} + function updateNextSyncCountdown(nextSyncTime) { const now = new Date(); const diff = nextSyncTime - now; @@ -2154,6 +2229,8 @@ function startFromCustomRoute(pathData, routeName, isRestoring = false, markers // Reset display steps displayTripSteps = 0; stateBuffer = []; + window.lastPositionUpdateDistance = 0; + window.lastPanoUpdate = 0; // Initialize Map View startFromLocation(startPoint, routeName); @@ -2301,6 +2378,14 @@ async function triggerCelebration(finalDistance) { currentTripDetails = null; masterPath = []; routeTotalDistance = 0; + displayTripSteps = 0; + stateBuffer = []; + + // Update UI to reflect reset + document.getElementById('routeInfo').textContent = 'Not Started'; + document.getElementById('currentSteps').textContent = '0'; + document.getElementById('tripMeter').textContent = '0.0 / 0.0 km'; + // We keep window.hasCelebrated = true to prevent immediate re-trigger // but starting a new trip will reset it. } diff --git a/server/step_manager.go b/server/step_manager.go index 65568b6..6a54b88 100644 --- a/server/step_manager.go +++ b/server/step_manager.go @@ -360,10 +360,26 @@ func (sm *StepManager) GetStatus() map[string]interface{} { go sm.Sync() // Async sync } - return map[string]interface{}{ + res := map[string]interface{}{ "tripSteps": currentSmoothed, "nextSyncTime": nextSyncMilli, } + + sm.mu.Lock() + if sm.tripState.StartDate != "" { + res["activeTrip"] = map[string]interface{}{ + "trip_type": sm.tripState.TripType, + "route_name": sm.tripState.RouteName, + "start_address": sm.tripState.StartAddress, + "end_address": sm.tripState.EndAddress, + "kml_id": sm.tripState.KmlID, + "total_distance": sm.tripState.TotalDistance, + "start_time": sm.tripState.StartTime, + } + } + sm.mu.Unlock() + + return res } // RecalculateTotalFromState sums up the steps from the DailyCache without making external API calls