From a30935bf2d00903ce080b8032eddf5b8670b2bce Mon Sep 17 00:00:00 2001 From: Jesus rodolfo gil Farias Date: Sun, 19 Jul 2026 13:17:51 -0400 Subject: [PATCH] feat: implement Google Drive synchronization with OAuth flow and background sync service --- public/_redirects | 1 + src/infrastructure/sync/google-drive-sync.ts | 27 ++++++++++++++++---- src/ui/components/SyncPanel.tsx | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 public/_redirects diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..7797f7c --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/src/infrastructure/sync/google-drive-sync.ts b/src/infrastructure/sync/google-drive-sync.ts index da7bdaf..274f376 100644 --- a/src/infrastructure/sync/google-drive-sync.ts +++ b/src/infrastructure/sync/google-drive-sync.ts @@ -75,13 +75,30 @@ export class GoogleDriveSync implements SyncPort { if (!res.ok) return; const remote = await res.json(); + if (remote?.app !== 'capmark' || remote.version !== 1 || !Array.isArray(remote.obras)) { + return; // El archivo remoto no es un backup válido de CapMark + } - // LWW: solo importamos si el remoto es más reciente que nuestro último export + // LWW por obra: importamos solo si la obra remota fue actualizada después que la local const localBackup = await this.backup.exportar(); - const remoteTs = new Date(remote.exportadoEn ?? 0).getTime(); - const localTs = new Date(localBackup.exportadoEn ?? 0).getTime(); - if (remoteTs > localTs) { - await this.backup.importar(remote, /* reemplazar= */ true); + const localObrasMap = new Map(localBackup.obras.map((o) => [o.obra.id, o])); + + const obrasParaImportar = []; + for (const remoteDetalle of remote.obras) { + const localDetalle = localObrasMap.get(remoteDetalle.obra.id); + if (!localDetalle) { + obrasParaImportar.push(remoteDetalle); + } else { + const remoteActualizada = new Date(remoteDetalle.obra.actualizadaEn || 0).getTime(); + const localActualizada = new Date(localDetalle.obra.actualizadaEn || 0).getTime(); + if (remoteActualizada > localActualizada) { + obrasParaImportar.push(remoteDetalle); + } + } + } + + if (obrasParaImportar.length > 0) { + await this.backup.importar({ ...remote, obras: obrasParaImportar }, /* reemplazar= */ true); } } diff --git a/src/ui/components/SyncPanel.tsx b/src/ui/components/SyncPanel.tsx index 76abc8d..7cf3470 100644 --- a/src/ui/components/SyncPanel.tsx +++ b/src/ui/components/SyncPanel.tsx @@ -54,8 +54,8 @@ export default function SyncPanel({ isOpen, onClose, onSyncCompleto }: Props) { setEstado('sincronizando'); setMensaje(''); try { - await container.sync.push(); await container.sync.pull(); + await container.sync.push(); setEstado('conectado'); setMensaje(`Sincronizado · ${new Date().toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' })}`); onSyncCompleto?.();