Saltar a contenido

SYNC-CONFLICT-001: Resolucion de Conflictos de Sincronizacion

Identificador: TECH-SYNC-CONFLICT-001 Version: 1.0.0 Fecha: 2025-12-09 Autor: ArchitectureDrone (MTS-DRN-ARC-001) Refs: API-SYNC-001, MOB-IOS-001, MOB-AND-001 Estado: Aprobado (DV2-P2)


1. Introduccion

1.1. Proposito

Este documento especifica la UI y logica de resolucion de conflictos de sincronizacion para MedTime, cubriendo:

  • Deteccion y presentacion de conflictos al usuario
  • Estrategias de resolucion (automatica y manual)
  • UI/UX para merge interactivo
  • Flujo de datos durante la resolucion

1.2. Contexto Zero-Knowledge

IMPORTANTE - CONFLICTOS EN ARQUITECTURA ZERO-KNOWLEDGE:
+------------------------------------------------------------------+
|  Los conflictos se detectan en SERVIDOR pero se resuelven        |
|  COMPLETAMENTE en el CLIENTE.                                     |
|                                                                   |
|  - Servidor: Detecta conflicto, retorna ambas versiones (blobs)  |
|  - Cliente: Descifra, presenta al usuario, resuelve, re-cifra    |
|  - Servidor: NUNCA ve contenido descifrado durante resolucion    |
+------------------------------------------------------------------+

2. Tipos de Conflicto

2.1. Clasificacion

Tipo Descripcion Frecuencia Auto-Resolvable
update_update Mismo blob modificado en 2+ dispositivos Alta Parcial
update_delete Modificado en cliente, eliminado en servidor Baja No
delete_update Eliminado en cliente, modificado en servidor Baja No

2.2. Escenarios Comunes

sequenceDiagram
    participant D1 as Dispositivo 1
    participant S as Servidor
    participant D2 as Dispositivo 2

    Note over D1,D2: ESCENARIO: update_update
    D1->>D1: Usuario edita medicamento
    D2->>D2: Usuario edita mismo medicamento
    D1->>S: PUSH cambio (v1 -> v2)
    S->>S: Acepta (v2)
    D2->>S: PUSH cambio (v1 -> v2)
    S-->>D2: CONFLICT! (tu v1, servidor v2)
    D2->>D2: Resolver conflicto localmente

3. UI de Resolucion de Conflictos

3.1. Flujo de Usuario

flowchart TB
    A[Sync detecta conflicto] --> B{Tipo de conflicto}

    B -->|update_update| C[Mostrar diff visual]
    B -->|update_delete| D[Confirmar recuperacion]
    B -->|delete_update| E[Confirmar eliminacion]

    C --> F{Usuario elige}
    F -->|Mantener mi version| G[keep_client]
    F -->|Usar otra version| H[keep_server]
    F -->|Combinar| I[Merge interactivo]

    D --> J{Usuario elige}
    J -->|Recuperar| G
    J -->|Eliminar| H

    E --> K{Usuario elige}
    K -->|Mantener eliminado| G
    K -->|Recuperar| H

    G --> L[Cifrar y enviar resolucion]
    H --> L
    I --> M[UI de merge campo por campo]
    M --> L

    L --> N[Sync completado]

3.2. Pantalla de Conflictos Pendientes

+--------------------------------------------------+
|              Conflictos de Sync (3)               |
+--------------------------------------------------+
|                                                   |
|  [!] Medicamento: Metformina                      |
|      Modificado en iPhone y iPad                  |
|      Hace 2 horas                                 |
|      [Resolver]                                   |
|                                                   |
|  [!] Horario: Recordatorio manana                 |
|      Modificado aqui, eliminado en iPhone         |
|      Hace 1 dia                                   |
|      [Resolver]                                   |
|                                                   |
|  [!] Contacto: Dr. Garcia                         |
|      Eliminado aqui, modificado en iPad           |
|      Hace 3 dias                                  |
|      [Resolver]                                   |
|                                                   |
+--------------------------------------------------+
|  [Resolver todos automaticamente]                 |
+--------------------------------------------------+

3.3. Pantalla de Diff Visual (update_update)

+--------------------------------------------------+
|           Conflicto: Metformina                   |
+--------------------------------------------------+
|                                                   |
|  Campo        | Mi version    | Otra version     |
|  -------------|---------------|------------------|
|  Dosis        | 850mg         | 1000mg      [!]  |
|  Frecuencia   | Cada 12h      | Cada 12h         |
|  Instruccion  | Con comida    | Antes comida [!] |
|  Notas        | -             | Verificar con Dr |
|                                                   |
+--------------------------------------------------+
|  [Mantener mi version]                            |
|  [Usar otra version]                              |
|  [Combinar manualmente]                           |
+--------------------------------------------------+

3.4. Pantalla de Merge Interactivo

+--------------------------------------------------+
|           Combinar: Metformina                    |
+--------------------------------------------------+
|                                                   |
|  Dosis:                                           |
|  ( ) 850mg (mi version)                           |
|  (x) 1000mg (otra version)                        |
|                                                   |
|  Frecuencia:                                      |
|  (x) Cada 12h (ambas)                             |
|                                                   |
|  Instruccion:                                     |
|  ( ) Con comida (mi version)                      |
|  (x) Antes comida (otra version)                  |
|                                                   |
|  Notas:                                           |
|  [x] Incluir: "Verificar con Dr"                  |
|                                                   |
+--------------------------------------------------+
|              [Guardar combinacion]                |
+--------------------------------------------------+

4. Implementacion Cliente

4.1. iOS (Swift)

// ConflictResolutionView.swift
struct ConflictResolutionView: View {
    @StateObject private var viewModel: ConflictResolutionViewModel
    let conflict: SyncConflict

    var body: some View {
        VStack {
            // Header
            ConflictHeader(
                entityType: conflict.entityType,
                conflictType: conflict.conflictType
            )

            // Diff visual
            DiffView(
                clientVersion: viewModel.decryptedClient,
                serverVersion: viewModel.decryptedServer,
                differences: viewModel.differences
            )

            // Acciones
            HStack {
                Button("Mantener mi version") {
                    viewModel.resolve(strategy: .keepClient)
                }

                Button("Usar otra version") {
                    viewModel.resolve(strategy: .keepServer)
                }

                Button("Combinar") {
                    viewModel.showMergeUI()
                }
            }
        }
        .task {
            await viewModel.decryptVersions(conflict: conflict)
        }
    }
}

// ConflictResolutionViewModel.swift
@MainActor
class ConflictResolutionViewModel: ObservableObject {
    @Published var decryptedClient: DecodedEntity?
    @Published var decryptedServer: DecodedEntity?
    @Published var differences: [FieldDiff] = []
    @Published var isResolving = false

    private let cryptoService: CryptoServiceProtocol
    private let syncService: SyncServiceProtocol

    func decryptVersions(conflict: SyncConflict) async {
        // Descifrar ambas versiones localmente
        // Servidor NUNCA ve esto
        decryptedClient = try? await cryptoService.decrypt(
            blob: conflict.clientVersion.data
        )
        decryptedServer = try? await cryptoService.decrypt(
            blob: conflict.serverVersion.data
        )

        // Calcular diferencias
        differences = calculateDiff(
            client: decryptedClient,
            server: decryptedServer
        )
    }

    func resolve(strategy: ResolutionStrategy) {
        isResolving = true

        Task {
            switch strategy {
            case .keepClient:
                try await syncService.resolveConflict(
                    conflictId: conflict.id,
                    resolution: .keepClient
                )

            case .keepServer:
                try await syncService.resolveConflict(
                    conflictId: conflict.id,
                    resolution: .keepServer
                )

            case .merge(let mergedData):
                // Cifrar el merge antes de enviar
                let encryptedMerge = try await cryptoService.encrypt(
                    data: mergedData
                )
                try await syncService.resolveConflict(
                    conflictId: conflict.id,
                    resolution: .merge,
                    mergedBlob: encryptedMerge
                )
            }

            isResolving = false
        }
    }
}

4.2. Android (Kotlin)

// ConflictResolutionScreen.kt
@Composable
fun ConflictResolutionScreen(
    conflict: SyncConflict,
    viewModel: ConflictResolutionViewModel = hiltViewModel()
) {
    val state by viewModel.state.collectAsState()

    LaunchedEffect(conflict) {
        viewModel.decryptVersions(conflict)
    }

    Column(modifier = Modifier.fillMaxSize()) {
        // Header
        ConflictHeader(
            entityType = conflict.entityType,
            conflictType = conflict.conflictType
        )

        // Diff visual
        when (state) {
            is ConflictState.Loading -> CircularProgressIndicator()
            is ConflictState.Ready -> {
                DiffView(
                    clientVersion = state.decryptedClient,
                    serverVersion = state.decryptedServer,
                    differences = state.differences
                )

                // Acciones
                Row {
                    Button(onClick = { viewModel.resolve(ResolutionStrategy.KeepClient) }) {
                        Text("Mantener mi version")
                    }
                    Button(onClick = { viewModel.resolve(ResolutionStrategy.KeepServer) }) {
                        Text("Usar otra version")
                    }
                    Button(onClick = { viewModel.showMergeUI() }) {
                        Text("Combinar")
                    }
                }
            }
            is ConflictState.Error -> ErrorMessage(state.message)
        }
    }
}

// ConflictResolutionViewModel.kt
@HiltViewModel
class ConflictResolutionViewModel @Inject constructor(
    private val cryptoService: CryptoService,
    private val syncService: SyncService
) : ViewModel() {

    private val _state = MutableStateFlow<ConflictState>(ConflictState.Loading)
    val state: StateFlow<ConflictState> = _state.asStateFlow()

    fun decryptVersions(conflict: SyncConflict) {
        viewModelScope.launch {
            try {
                // Descifrar ambas versiones localmente
                val clientDecrypted = cryptoService.decrypt(conflict.clientVersion.data)
                val serverDecrypted = cryptoService.decrypt(conflict.serverVersion.data)

                val differences = calculateDiff(clientDecrypted, serverDecrypted)

                _state.value = ConflictState.Ready(
                    conflict = conflict,
                    decryptedClient = clientDecrypted,
                    decryptedServer = serverDecrypted,
                    differences = differences
                )
            } catch (e: Exception) {
                _state.value = ConflictState.Error(e.message ?: "Error descifrando")
            }
        }
    }

    fun resolve(strategy: ResolutionStrategy) {
        viewModelScope.launch {
            val currentState = _state.value as? ConflictState.Ready ?: return@launch

            when (strategy) {
                ResolutionStrategy.KeepClient -> {
                    syncService.resolveConflict(
                        conflictId = currentState.conflict.id,
                        resolution = "keep_client"
                    )
                }
                ResolutionStrategy.KeepServer -> {
                    syncService.resolveConflict(
                        conflictId = currentState.conflict.id,
                        resolution = "keep_server"
                    )
                }
                is ResolutionStrategy.Merge -> {
                    // Cifrar merge antes de enviar
                    val encryptedMerge = cryptoService.encrypt(strategy.mergedData)
                    syncService.resolveConflict(
                        conflictId = currentState.conflict.id,
                        resolution = "merge",
                        mergedBlob = encryptedMerge
                    )
                }
            }
        }
    }
}

5. Auto-Resolucion

5.1. Reglas de Auto-Merge

Para ciertos tipos de conflictos, el sistema puede resolver automaticamente:

Escenario Regla de Auto-Resolucion Notificar Usuario
Timestamp mas reciente Si diferencia < 5 min, usar mas reciente No
Campos no conflictivos Merge campos diferentes automaticamente Si
Solo metadata Merge metadata, preservar PHI del cliente No
Append-only (logs) Combinar ambas entradas Si

5.2. Configuracion de Usuario

// Settings
struct SyncSettings {
    /// Resolver conflictos automaticamente cuando sea posible
    var autoResolveConflicts: Bool = true

    /// Preferencia por defecto si auto-resolucion no aplica
    var defaultPreference: ConflictPreference = .askUser

    enum ConflictPreference {
        case askUser        // Siempre preguntar
        case preferLocal    // Preferir version local
        case preferRemote   // Preferir version remota
        case preferNewest   // Preferir la mas reciente
    }
}

6. Notificaciones de Conflicto

6.1. Badge en App

  • Mostrar badge numerico en icono de Sync si hay conflictos pendientes
  • Color amarillo para conflictos pendientes
  • Color rojo si conflictos > 24h sin resolver

6.2. Push Notification (Opcional)

{
  "notification": {
    "title": "Conflicto de sincronizacion",
    "body": "Tienes 2 cambios que requieren tu atencion"
  },
  "data": {
    "type": "sync_conflict",
    "count": 2
  }
}

7. Metricas y Logging

7.1. Eventos a Registrar

Evento Datos (No PHI) Proposito
conflict_detected entity_type, conflict_type Analytics
conflict_resolved resolution_strategy, time_to_resolve UX mejora
conflict_auto_resolved rule_applied Validar reglas
conflict_merge_ui_opened entity_type UI analytics

7.2. Metricas de Exito

  • Tasa de auto-resolucion: Target > 60%
  • Tiempo promedio de resolucion manual: Target < 30s
  • Conflictos abandonados (> 7 dias): Target < 5%

8. Testing

8.1. Casos de Prueba

Test Descripcion Prioridad
TC-CONFLICT-001 Detectar update_update correctamente P0
TC-CONFLICT-002 UI muestra diff correctamente P0
TC-CONFLICT-003 Resolucion keep_client funciona P0
TC-CONFLICT-004 Resolucion keep_server funciona P0
TC-CONFLICT-005 Merge interactivo preserva cifrado P0
TC-CONFLICT-006 Auto-resolucion aplica reglas P1
TC-CONFLICT-007 Notificacion de conflictos funciona P1
TC-CONFLICT-008 Multiples conflictos simultaneos P1

9. Referencias

  • API-SYNC-001: Sync API Specification
  • MOB-IOS-001: iOS Architecture
  • MOB-AND-001: Android Architecture
  • 04-seguridad-cliente.md: Zero-Knowledge principles