Add on-demand heatmap rebuild and remove nightly scheduler

- Added rebuild button to heatmap page with loading states
- Added POST /api/heatmap/me/rebuild endpoint for on-demand recalculation
- Removed HeatmapRecalculationScheduler (nightly 3 AM cron job)
- Removed @EnableScheduling annotation since no schedulers remain
- Users can now rebuild their heatmap manually instead of relying on automatic nightly recalculation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Tim Zöller 2026-01-02 09:21:52 +01:00
parent f391028061
commit 66b14ebf7f
6 changed files with 90 additions and 60 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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) {}
}

View file

@ -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<User> 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);
}
}

View file

@ -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 = '<span class="spinner-border spinner-border-sm me-2"></span>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;
}
}

View file

@ -52,8 +52,8 @@
<!-- Stats Card -->
<div class="heatmap-stats mb-4" id="statsCard" style="display: none;">
<div class="row">
<div class="col-md-4 mb-3 mb-md-0">
<div class="row align-items-center">
<div class="col-md-3 mb-3 mb-md-0">
<div class="stat-item">
<i class="bi bi-grid-3x3"></i>
<div>
@ -62,7 +62,7 @@
</div>
</div>
</div>
<div class="col-md-4 mb-3 mb-md-0">
<div class="col-md-3 mb-3 mb-md-0">
<div class="stat-item">
<i class="bi bi-fire"></i>
<div>
@ -71,7 +71,7 @@
</div>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3 mb-3 mb-md-0">
<div class="stat-item">
<i class="bi bi-clock-history"></i>
<div>
@ -80,6 +80,12 @@
</div>
</div>
</div>
<div class="col-md-3 text-md-end">
<button id="rebuildBtn" class="btn btn-outline-primary">
<i class="bi bi-arrow-clockwise"></i>
Rebuild Heatmap
</button>
</div>
</div>
</div>