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