Remove migration logic again
This commit is contained in:
parent
1a068e3217
commit
87da2a3861
6 changed files with 23 additions and 277 deletions
|
|
@ -123,56 +123,6 @@ WHERE a.user_id = :userId
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Retroactive Migration
|
|
||||||
|
|
||||||
### For Activities Uploaded BEFORE This Feature
|
|
||||||
|
|
||||||
**Run once** to populate indoor flags for existing data:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
POST /api/admin/migrate-indoor-flags
|
|
||||||
Authorization: Bearer <your-jwt-token>
|
|
||||||
```
|
|
||||||
|
|
||||||
This endpoint:
|
|
||||||
1. Fetches all FIT activities with stored raw files
|
|
||||||
2. Re-parses FIT files to extract SubSport
|
|
||||||
3. Updates `indoor`, `sub_sport`, and `indoor_detection_method`
|
|
||||||
4. Only saves if values changed (idempotent)
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Indoor activity flag migration complete",
|
|
||||||
"activitiesUpdated": 15
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### How to Run Migration
|
|
||||||
|
|
||||||
#### Option 1: Using Browser DevTools
|
|
||||||
1. Login to FitPub
|
|
||||||
2. Open DevTools (F12) → Application → Local Storage
|
|
||||||
3. Copy `jwt_token` value
|
|
||||||
4. Use browser fetch or Postman:
|
|
||||||
```javascript
|
|
||||||
fetch('http://localhost:8080/api/admin/migrate-indoor-flags', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer YOUR_TOKEN_HERE'
|
|
||||||
}
|
|
||||||
}).then(r => r.json()).then(console.log);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Option 2: Using curl
|
|
||||||
```bash
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
|
|
||||||
http://localhost:8080/api/admin/migrate-indoor-flags
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Structure
|
## Code Structure
|
||||||
|
|
||||||
### Key Files Modified
|
### Key Files Modified
|
||||||
|
|
@ -192,15 +142,9 @@ curl -X POST \
|
||||||
|
|
||||||
4. **Service**:
|
4. **Service**:
|
||||||
- `ActivityFileService.java` - Save SubSport & detection method to database
|
- `ActivityFileService.java` - Save SubSport & detection method to database
|
||||||
- `IndoorActivityMigrationService.java` - Retroactive migration logic
|
|
||||||
|
|
||||||
5. **Repository**:
|
5. **Repository**:
|
||||||
- `UserHeatmapGridRepository.java` - Exclude indoor activities from heatmap queries
|
- `UserHeatmapGridRepository.java` - Exclude indoor activities from heatmap queries
|
||||||
- `ActivityRepository.java` - Added query method for migration
|
|
||||||
|
|
||||||
6. **Controller**:
|
|
||||||
- `AdminController.java` - Migration endpoint
|
|
||||||
- `SecurityConfig.java` - Added `/api/admin/**` route (authenticated)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -241,7 +185,6 @@ Expected:
|
||||||
- **New uploads**: SubSport extracted during normal parsing (no overhead)
|
- **New uploads**: SubSport extracted during normal parsing (no overhead)
|
||||||
- **Timeline loading**: Simple column read (instant)
|
- **Timeline loading**: Simple column read (instant)
|
||||||
- **Heatmap queries**: Added `AND indoor = FALSE` filter (uses index)
|
- **Heatmap queries**: Added `AND indoor = FALSE` filter (uses index)
|
||||||
- **Migration**: One-time operation, only re-parses FIT files with raw data
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -286,7 +229,7 @@ GROUP BY indoor_detection_method;
|
||||||
|
|
||||||
✅ **Fully implemented** multi-format indoor detection
|
✅ **Fully implemented** multi-format indoor detection
|
||||||
✅ **Backward compatible** - existing activities default to outdoor
|
✅ **Backward compatible** - existing activities default to outdoor
|
||||||
✅ **Retroactive migration** endpoint for old data
|
✅ **Automatic detection** on upload for new activities
|
||||||
✅ **Heatmap exclusion** automatic via SQL filters
|
✅ **Heatmap exclusion** automatic via SQL filters
|
||||||
✅ **Timeline display** includes all activities
|
✅ **Timeline display** includes all activities
|
||||||
✅ **Works for FIT and GPX** files with different detection strategies
|
✅ **Works for FIT and GPX** files with different detection strategies
|
||||||
|
|
|
||||||
|
|
@ -152,9 +152,6 @@ public class SecurityConfig {
|
||||||
.requestMatchers(HttpMethod.POST, "/api/users/*/follow").authenticated() // Follow user
|
.requestMatchers(HttpMethod.POST, "/api/users/*/follow").authenticated() // Follow user
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/users/*/follow").authenticated() // Unfollow user
|
.requestMatchers(HttpMethod.DELETE, "/api/users/*/follow").authenticated() // Unfollow user
|
||||||
|
|
||||||
// Protected endpoints - Admin API (data migration, maintenance)
|
|
||||||
.requestMatchers("/api/admin/**").authenticated()
|
|
||||||
|
|
||||||
// All other requests require authentication
|
// All other requests require authentication
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
package org.operaton.fitpub.controller;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.operaton.fitpub.service.IndoorActivityMigrationService;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Admin endpoints for data migration and maintenance tasks.
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/admin")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class AdminController {
|
|
||||||
|
|
||||||
private final IndoorActivityMigrationService indoorActivityMigrationService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retroactively detect and update indoor activity flags for existing activities.
|
|
||||||
* Re-parses all FIT files to detect indoor activities based on SubSport field.
|
|
||||||
*
|
|
||||||
* This is a one-time migration endpoint to update existing data.
|
|
||||||
*
|
|
||||||
* @return number of activities updated
|
|
||||||
*/
|
|
||||||
@PostMapping("/migrate-indoor-flags")
|
|
||||||
public ResponseEntity<Map<String, Object>> migrateIndoorFlags() {
|
|
||||||
log.info("Admin: Starting indoor activity flag migration");
|
|
||||||
|
|
||||||
int updatedCount = indoorActivityMigrationService.updateIndoorFlagsForExistingActivities();
|
|
||||||
|
|
||||||
log.info("Admin: Indoor activity flag migration complete - {} activities updated", updatedCount);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"message", "Indoor activity flag migration complete",
|
|
||||||
"activitiesUpdated", updatedCount
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -91,7 +91,8 @@ public class AuthController {
|
||||||
*/
|
*/
|
||||||
@GetMapping("/registration-status")
|
@GetMapping("/registration-status")
|
||||||
public ResponseEntity<RegistrationStatusResponse> getRegistrationStatus() {
|
public ResponseEntity<RegistrationStatusResponse> getRegistrationStatus() {
|
||||||
return ResponseEntity.ok(new RegistrationStatusResponse(registrationEnabled));
|
boolean passwordRequired = configuredRegistrationPassword != null && !configuredRegistrationPassword.trim().isEmpty();
|
||||||
|
return ResponseEntity.ok(new RegistrationStatusResponse(registrationEnabled, passwordRequired));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,5 +184,5 @@ public class AuthController {
|
||||||
/**
|
/**
|
||||||
* Registration status response DTO.
|
* Registration status response DTO.
|
||||||
*/
|
*/
|
||||||
record RegistrationStatusResponse(boolean enabled) {}
|
record RegistrationStatusResponse(boolean enabled, boolean passwordRequired) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
package org.operaton.fitpub.service;
|
|
||||||
|
|
||||||
import com.garmin.fit.Decode;
|
|
||||||
import com.garmin.fit.MesgBroadcaster;
|
|
||||||
import com.garmin.fit.SessionMesg;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.operaton.fitpub.model.entity.Activity;
|
|
||||||
import org.operaton.fitpub.repository.ActivityRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for retroactively detecting and updating indoor activity flags.
|
|
||||||
* This is a data migration service to update existing activities in the database.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class IndoorActivityMigrationService {
|
|
||||||
|
|
||||||
private final ActivityRepository activityRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retroactively update indoor flags for all existing FIT activities.
|
|
||||||
* Re-parses stored FIT files to detect indoor activities based on SubSport field.
|
|
||||||
*
|
|
||||||
* @return number of activities updated
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public int updateIndoorFlagsForExistingActivities() {
|
|
||||||
log.info("Starting retroactive indoor activity detection for all FIT activities");
|
|
||||||
|
|
||||||
// Find all activities with FIT files
|
|
||||||
List<Activity> fitActivities = activityRepository.findBySourceFileFormatAndRawActivityFileNotNull("FIT");
|
|
||||||
log.info("Found {} FIT activities to analyze", fitActivities.size());
|
|
||||||
|
|
||||||
AtomicInteger updatedCount = new AtomicInteger(0);
|
|
||||||
AtomicInteger errorCount = new AtomicInteger(0);
|
|
||||||
|
|
||||||
fitActivities.forEach(activity -> {
|
|
||||||
try {
|
|
||||||
IndoorDetectionResult result = detectIndoorFromFitFile(activity.getRawActivityFile());
|
|
||||||
|
|
||||||
boolean changed = false;
|
|
||||||
|
|
||||||
if (result.isIndoor() != activity.getIndoor()) {
|
|
||||||
activity.setIndoor(result.isIndoor());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.getSubSport() != null && !result.getSubSport().equals(activity.getSubSport())) {
|
|
||||||
activity.setSubSport(result.getSubSport());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.getDetectionMethod() != null &&
|
|
||||||
!result.getDetectionMethod().name().equals(activity.getIndoorDetectionMethod())) {
|
|
||||||
activity.setIndoorDetectionMethod(result.getDetectionMethod().name());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
activityRepository.save(activity);
|
|
||||||
updatedCount.incrementAndGet();
|
|
||||||
log.info("Updated activity {} - indoor: {}, subSport: {}, method: {}",
|
|
||||||
activity.getId(), result.isIndoor(), result.getSubSport(),
|
|
||||||
result.getDetectionMethod());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
errorCount.incrementAndGet();
|
|
||||||
log.warn("Failed to process activity {}: {}", activity.getId(), e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info("Retroactive indoor detection complete: {} activities updated, {} errors",
|
|
||||||
updatedCount.get(), errorCount.get());
|
|
||||||
|
|
||||||
return updatedCount.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if a FIT file represents an indoor activity.
|
|
||||||
* Checks the SubSport field from the session message.
|
|
||||||
*
|
|
||||||
* @param fitFileBytes raw FIT file bytes
|
|
||||||
* @return detection result with indoor flag, SubSport, and detection method
|
|
||||||
*/
|
|
||||||
private IndoorDetectionResult detectIndoorFromFitFile(byte[] fitFileBytes) {
|
|
||||||
IndoorDetectionResult result = new IndoorDetectionResult();
|
|
||||||
result.setIndoor(false);
|
|
||||||
|
|
||||||
if (fitFileBytes == null || fitFileBytes.length == 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
AtomicBoolean isIndoor = new AtomicBoolean(false);
|
|
||||||
AtomicReference<String> subSport = new AtomicReference<>(null);
|
|
||||||
AtomicReference<Activity.IndoorDetectionMethod> method = new AtomicReference<>(null);
|
|
||||||
|
|
||||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(fitFileBytes)) {
|
|
||||||
Decode decode = new Decode();
|
|
||||||
MesgBroadcaster broadcaster = new MesgBroadcaster(decode);
|
|
||||||
|
|
||||||
// Listen for session messages to extract SubSport
|
|
||||||
broadcaster.addListener((SessionMesg session) -> {
|
|
||||||
if (session.getSubSport() != null) {
|
|
||||||
String subSportStr = session.getSubSport().toString();
|
|
||||||
subSport.set(subSportStr);
|
|
||||||
|
|
||||||
String subSportUpper = subSportStr.toUpperCase();
|
|
||||||
boolean detected = subSportUpper.contains("INDOOR") ||
|
|
||||||
subSportUpper.contains("TREADMILL") ||
|
|
||||||
subSportUpper.contains("VIRTUAL") ||
|
|
||||||
subSportUpper.contains("TRAINER");
|
|
||||||
if (detected) {
|
|
||||||
isIndoor.set(true);
|
|
||||||
method.set(Activity.IndoorDetectionMethod.FIT_SUBSPORT);
|
|
||||||
log.debug("Detected indoor activity from SubSport: {}", subSportStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Decode the FIT file
|
|
||||||
if (!decode.checkFileIntegrity(inputStream)) {
|
|
||||||
log.warn("FIT file integrity check failed");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset stream and read
|
|
||||||
inputStream.reset();
|
|
||||||
decode.read(inputStream, broadcaster);
|
|
||||||
|
|
||||||
result.setIndoor(isIndoor.get());
|
|
||||||
result.setSubSport(subSport.get());
|
|
||||||
result.setDetectionMethod(method.get());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to parse FIT file: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of indoor activity detection.
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
private static class IndoorDetectionResult {
|
|
||||||
private boolean indoor;
|
|
||||||
private String subSport;
|
|
||||||
private Activity.IndoorDetectionMethod detectionMethod;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -231,15 +231,26 @@
|
||||||
const registrationPasswordField = document.getElementById('registrationPasswordField');
|
const registrationPasswordField = document.getElementById('registrationPasswordField');
|
||||||
const registrationPasswordInput = document.getElementById('registrationPassword');
|
const registrationPasswordInput = document.getElementById('registrationPassword');
|
||||||
|
|
||||||
// Check if registration password is required by checking URL parameters
|
// Fetch registration status from API
|
||||||
// If ?invite=true or REGISTRATION_PASSWORD is set, show the field
|
try {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const response = await fetch('/api/auth/registration-status');
|
||||||
const showRegistrationPassword = urlParams.has('invite') || urlParams.has('code');
|
const data = await response.json();
|
||||||
|
|
||||||
// Always show the field and let backend validate
|
if (data.passwordRequired) {
|
||||||
// This simplifies the logic - if not required, backend will ignore it
|
// Show registration password field if required
|
||||||
registrationPasswordField.style.display = 'block';
|
registrationPasswordField.style.display = 'block';
|
||||||
registrationPasswordInput.required = true;
|
registrationPasswordInput.required = true;
|
||||||
|
} else {
|
||||||
|
// Hide registration password field if not required
|
||||||
|
registrationPasswordField.style.display = 'none';
|
||||||
|
registrationPasswordInput.required = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch registration status:', error);
|
||||||
|
// On error, assume password is not required
|
||||||
|
registrationPasswordField.style.display = 'none';
|
||||||
|
registrationPasswordInput.required = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Password confirmation validation
|
// Password confirmation validation
|
||||||
confirmPassword.addEventListener('input', function() {
|
confirmPassword.addEventListener('input', function() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue