Add smart self-healing for sync errors
Implements automatic repair mechanism for pending changes with sync errors: - Projects: validates and fixes empty name/code fields - Configurations: ensures project references exist or assigns system project - Clears errors and resets attempts to give changes another sync chance Backend: - LocalDB.RepairPendingChanges() with smart validation logic - POST /api/sync/repair endpoint - Detailed repair results with remaining errors Frontend: - Auto-repair section in sync modal shown when errors exist - "ИСПРАВИТЬ" button with clear explanation of actions - Real-time feedback with result messages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -419,6 +419,26 @@ func (h *SyncHandler) GetPendingChanges(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// RepairPendingChanges attempts to repair errored pending changes
|
||||
// POST /api/sync/repair
|
||||
func (h *SyncHandler) RepairPendingChanges(c *gin.Context) {
|
||||
repaired, remainingErrors, err := h.localDB.RepairPendingChanges()
|
||||
if err != nil {
|
||||
slog.Error("repair pending changes failed", "error", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"repaired": repaired,
|
||||
"remaining_errors": remainingErrors,
|
||||
})
|
||||
}
|
||||
|
||||
// SyncInfoResponse represents sync information for the modal
|
||||
type SyncInfoResponse struct {
|
||||
// Connection
|
||||
|
||||
@@ -1041,6 +1041,129 @@ func (l *LocalDB) GetPendingCount() int64 {
|
||||
return l.CountPendingChanges()
|
||||
}
|
||||
|
||||
// RepairPendingChanges attempts to fix errored pending changes by validating and correcting data.
|
||||
// Returns the number of changes repaired and a list of errors that couldn't be fixed.
|
||||
func (l *LocalDB) RepairPendingChanges() (int, []string, error) {
|
||||
var erroredChanges []PendingChange
|
||||
if err := l.db.Where("last_error != ?", "").Find(&erroredChanges).Error; err != nil {
|
||||
return 0, nil, fmt.Errorf("fetching errored changes: %w", err)
|
||||
}
|
||||
|
||||
if len(erroredChanges) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
repaired := 0
|
||||
var remainingErrors []string
|
||||
|
||||
for _, change := range erroredChanges {
|
||||
var repairErr error
|
||||
switch change.EntityType {
|
||||
case "project":
|
||||
repairErr = l.repairProjectChange(&change)
|
||||
case "configuration":
|
||||
repairErr = l.repairConfigurationChange(&change)
|
||||
default:
|
||||
repairErr = fmt.Errorf("unknown entity type: %s", change.EntityType)
|
||||
}
|
||||
|
||||
if repairErr != nil {
|
||||
remainingErrors = append(remainingErrors, fmt.Sprintf("%s %s %s: %v",
|
||||
change.Operation, change.EntityType, change.EntityUUID[:8], repairErr))
|
||||
continue
|
||||
}
|
||||
|
||||
// Clear error and reset attempts
|
||||
if err := l.db.Model(&PendingChange{}).Where("id = ?", change.ID).Updates(map[string]interface{}{
|
||||
"last_error": "",
|
||||
"attempts": 0,
|
||||
}).Error; err != nil {
|
||||
remainingErrors = append(remainingErrors, fmt.Sprintf("clearing error for %s: %v", change.EntityUUID[:8], err))
|
||||
continue
|
||||
}
|
||||
|
||||
repaired++
|
||||
}
|
||||
|
||||
return repaired, remainingErrors, nil
|
||||
}
|
||||
|
||||
// repairProjectChange validates and fixes project data
|
||||
func (l *LocalDB) repairProjectChange(change *PendingChange) error {
|
||||
project, err := l.GetProjectByUUID(change.EntityUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("project not found locally: %w", err)
|
||||
}
|
||||
|
||||
modified := false
|
||||
|
||||
// Fix Code: must be non-empty
|
||||
if strings.TrimSpace(project.Code) == "" {
|
||||
if project.Name != nil && strings.TrimSpace(*project.Name) != "" {
|
||||
project.Code = strings.TrimSpace(*project.Name)
|
||||
} else {
|
||||
project.Code = project.UUID[:8]
|
||||
}
|
||||
modified = true
|
||||
}
|
||||
|
||||
// Fix Name: use Code if empty
|
||||
if project.Name == nil || strings.TrimSpace(*project.Name) == "" {
|
||||
name := project.Code
|
||||
project.Name = &name
|
||||
modified = true
|
||||
}
|
||||
|
||||
// Fix OwnerUsername: must be non-empty
|
||||
if strings.TrimSpace(project.OwnerUsername) == "" {
|
||||
project.OwnerUsername = l.GetDBUser()
|
||||
if project.OwnerUsername == "" {
|
||||
return fmt.Errorf("cannot determine owner username")
|
||||
}
|
||||
modified = true
|
||||
}
|
||||
|
||||
if modified {
|
||||
if err := l.SaveProject(project); err != nil {
|
||||
return fmt.Errorf("saving repaired project: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// repairConfigurationChange validates and fixes configuration data
|
||||
func (l *LocalDB) repairConfigurationChange(change *PendingChange) error {
|
||||
config, err := l.GetConfigurationByUUID(change.EntityUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuration not found locally: %w", err)
|
||||
}
|
||||
|
||||
modified := false
|
||||
|
||||
// Check if referenced project exists
|
||||
if config.ProjectUUID != nil && *config.ProjectUUID != "" {
|
||||
_, err := l.GetProjectByUUID(*config.ProjectUUID)
|
||||
if err != nil {
|
||||
// Project doesn't exist locally - use default system project
|
||||
systemProject, sysErr := l.EnsureDefaultProject(config.OriginalUsername)
|
||||
if sysErr != nil {
|
||||
return fmt.Errorf("getting system project: %w", sysErr)
|
||||
}
|
||||
config.ProjectUUID = &systemProject.UUID
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
if modified {
|
||||
if err := l.SaveConfiguration(config); err != nil {
|
||||
return fmt.Errorf("saving repaired configuration: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRemoteMigrationApplied returns a locally applied remote migration by ID.
|
||||
func (l *LocalDB) GetRemoteMigrationApplied(id string) (*LocalRemoteMigrationApplied, error) {
|
||||
var migration LocalRemoteMigrationApplied
|
||||
|
||||
Reference in New Issue
Block a user