diff --git a/src/main/java/org/operaton/fitpub/FitPubApplication.java b/src/main/java/org/operaton/fitpub/FitPubApplication.java index d8ba851..344432d 100644 --- a/src/main/java/org/operaton/fitpub/FitPubApplication.java +++ b/src/main/java/org/operaton/fitpub/FitPubApplication.java @@ -10,7 +10,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.client.RestTemplate; /** @@ -20,7 +19,6 @@ import org.springframework.web.client.RestTemplate; */ @SpringBootApplication @EnableAsync -@EnableScheduling @Slf4j public class FitPubApplication { diff --git a/src/main/java/org/operaton/fitpub/config/SecurityConfig.java b/src/main/java/org/operaton/fitpub/config/SecurityConfig.java index 2a7f144..b24125a 100644 --- a/src/main/java/org/operaton/fitpub/config/SecurityConfig.java +++ b/src/main/java/org/operaton/fitpub/config/SecurityConfig.java @@ -108,6 +108,7 @@ public class SecurityConfig { // Protected endpoints - Heatmap API .requestMatchers("/api/heatmap/me").authenticated() + .requestMatchers(HttpMethod.POST, "/api/heatmap/me/rebuild").authenticated() .requestMatchers(HttpMethod.GET, "/api/heatmap/user/*").permitAll() // Protected endpoints - Activities API (upload, edit, delete) diff --git a/src/main/java/org/operaton/fitpub/controller/HeatmapController.java b/src/main/java/org/operaton/fitpub/controller/HeatmapController.java index 025f3e6..1bcb8cf 100644 --- a/src/main/java/org/operaton/fitpub/controller/HeatmapController.java +++ b/src/main/java/org/operaton/fitpub/controller/HeatmapController.java @@ -98,4 +98,34 @@ public class HeatmapController { return ResponseEntity.ok(heatmapData); } + + /** + * Rebuild heatmap for the authenticated user. + * This triggers a full recalculation of the user's heatmap grid from all their activities. + * + * @param userDetails authenticated user + * @return success message + */ + @PostMapping("/me/rebuild") + public ResponseEntity rebuildMyHeatmap(@AuthenticationPrincipal UserDetails userDetails) { + log.info("User {} requested heatmap rebuild", userDetails.getUsername()); + + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + try { + heatmapGridService.recalculateUserHeatmap(user); + log.info("Heatmap rebuild completed successfully for user {}", userDetails.getUsername()); + return ResponseEntity.ok().body(new RebuildResponse("Heatmap rebuilt successfully")); + } catch (Exception e) { + log.error("Failed to rebuild heatmap for user {}", userDetails.getUsername(), e); + return ResponseEntity.internalServerError() + .body(new RebuildResponse("Failed to rebuild heatmap: " + e.getMessage())); + } + } + + /** + * Simple response DTO for rebuild endpoint + */ + private record RebuildResponse(String message) {} } diff --git a/src/main/java/org/operaton/fitpub/scheduler/HeatmapRecalculationScheduler.java b/src/main/java/org/operaton/fitpub/scheduler/HeatmapRecalculationScheduler.java deleted file mode 100644 index 01b0a38..0000000 --- a/src/main/java/org/operaton/fitpub/scheduler/HeatmapRecalculationScheduler.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.operaton.fitpub.scheduler; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.operaton.fitpub.model.entity.User; -import org.operaton.fitpub.repository.UserRepository; -import org.operaton.fitpub.service.HeatmapGridService; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.util.List; - -/** - * Scheduled task to recalculate user heatmaps nightly. - * Ensures heatmap data stays in sync with activities even if incremental updates fail. - */ -@Component -@RequiredArgsConstructor -@Slf4j -public class HeatmapRecalculationScheduler { - - private final HeatmapGridService heatmapGridService; - private final UserRepository userRepository; - - /** - * Recalculate heatmaps for all users. - * Runs daily at 3 AM server time. - */ - @Scheduled(cron = "0 0 3 * * *") - public void recalculateAllUserHeatmaps() { - log.info("Starting nightly heatmap recalculation for all users"); - long startTime = System.currentTimeMillis(); - - List users = userRepository.findAll(); - log.info("Found {} users to process", users.size()); - - int successCount = 0; - int errorCount = 0; - - for (User user : users) { - try { - heatmapGridService.recalculateUserHeatmap(user); - successCount++; - } catch (Exception e) { - log.error("Failed to recalculate heatmap for user {}", user.getUsername(), e); - errorCount++; - } - } - - long duration = System.currentTimeMillis() - startTime; - log.info("Heatmap recalculation completed in {}ms. Success: {}, Errors: {}", - duration, successCount, errorCount); - } -} diff --git a/src/main/resources/static/js/heatmap.js b/src/main/resources/static/js/heatmap.js index 252dda6..dab0bf1 100644 --- a/src/main/resources/static/js/heatmap.js +++ b/src/main/resources/static/js/heatmap.js @@ -17,6 +17,12 @@ document.addEventListener('DOMContentLoaded', async function() { } await loadHeatmap(); + + // Attach rebuild button handler + const rebuildBtn = document.getElementById('rebuildBtn'); + if (rebuildBtn) { + rebuildBtn.addEventListener('click', rebuildHeatmap); + } }); /** @@ -175,3 +181,46 @@ function fitMapToBounds(features) { heatmapMap.fitBounds(bounds); } + +/** + * Rebuild the heatmap by triggering a full recalculation + */ +async function rebuildHeatmap() { + const rebuildBtn = document.getElementById('rebuildBtn'); + const originalContent = rebuildBtn.innerHTML; + const errorAlert = document.getElementById('errorAlert'); + const errorMessage = document.getElementById('errorMessage'); + + // Disable button and show loading state + rebuildBtn.disabled = true; + rebuildBtn.innerHTML = 'Rebuilding...'; + errorAlert.classList.add('d-none'); + + try { + // Call rebuild endpoint + const response = await FitPubAuth.authenticatedFetch('/api/heatmap/me/rebuild', { + method: 'POST' + }); + + if (!response.ok) { + throw new Error('Failed to rebuild heatmap'); + } + + const result = await response.json(); + + // Show success message + FitPub.showAlert('success', result.message || 'Heatmap rebuilt successfully!'); + + // Reload the heatmap + await loadHeatmap(); + + } catch (error) { + console.error('Error rebuilding heatmap:', error); + errorAlert.classList.remove('d-none'); + errorMessage.textContent = 'Failed to rebuild heatmap. Please try again later.'; + } finally { + // Restore button state + rebuildBtn.disabled = false; + rebuildBtn.innerHTML = originalContent; + } +} diff --git a/src/main/resources/templates/heatmap.html b/src/main/resources/templates/heatmap.html index b0a4a70..feae716 100644 --- a/src/main/resources/templates/heatmap.html +++ b/src/main/resources/templates/heatmap.html @@ -52,8 +52,8 @@