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
|
||||
|
||||
### Key Files Modified
|
||||
|
|
@ -192,15 +142,9 @@ curl -X POST \
|
|||
|
||||
4. **Service**:
|
||||
- `ActivityFileService.java` - Save SubSport & detection method to database
|
||||
- `IndoorActivityMigrationService.java` - Retroactive migration logic
|
||||
|
||||
5. **Repository**:
|
||||
- `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)
|
||||
- **Timeline loading**: Simple column read (instant)
|
||||
- **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
|
||||
✅ **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
|
||||
✅ **Timeline display** includes all activities
|
||||
✅ **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.DELETE, "/api/users/*/follow").authenticated() // Unfollow user
|
||||
|
||||
// Protected endpoints - Admin API (data migration, maintenance)
|
||||
.requestMatchers("/api/admin/**").authenticated()
|
||||
|
||||
// All other requests require authentication
|
||||
.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")
|
||||
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.
|
||||
*/
|
||||
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 registrationPasswordInput = document.getElementById('registrationPassword');
|
||||
|
||||
// Check if registration password is required by checking URL parameters
|
||||
// If ?invite=true or REGISTRATION_PASSWORD is set, show the field
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const showRegistrationPassword = urlParams.has('invite') || urlParams.has('code');
|
||||
// Fetch registration status from API
|
||||
try {
|
||||
const response = await fetch('/api/auth/registration-status');
|
||||
const data = await response.json();
|
||||
|
||||
// Always show the field and let backend validate
|
||||
// This simplifies the logic - if not required, backend will ignore it
|
||||
registrationPasswordField.style.display = 'block';
|
||||
registrationPasswordInput.required = true;
|
||||
if (data.passwordRequired) {
|
||||
// Show registration password field if required
|
||||
registrationPasswordField.style.display = 'block';
|
||||
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
|
||||
confirmPassword.addEventListener('input', function() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue