feat(sync): media en paralelo + visibilidad proactiva de problemas

- pushMedia sube en lotes de 3 en paralelo (Promise.all por chunk) en vez
  de una a una; contadores del report seguros al ser el runtime monohilo.
- SyncStatusBar: fondo rojo cuando hay conflictos/errores (prioridad sobre
  el estado de conexión), para que el problema salte a la vista.
- ProjectDetail: el auto-sync silencioso ahora avisa una vez con Alert
  cuando un ciclo produce conflictos/errores, con acceso directo a la cola.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 12:00:29 +02:00
co-authored by Claude Sonnet 4.6
parent 5d1a4db42f
commit eee15819ac
3 changed files with 31 additions and 3 deletions
+4 -1
View File
@@ -16,8 +16,11 @@ export function SyncStatusBar({
const pendientes = counts.pending + counts.mediaPending; const pendientes = counts.pending + counts.mediaPending;
const hasProblems = counts.conflict > 0 || counts.error > 0; const hasProblems = counts.conflict > 0 || counts.error > 0;
// Prioridad de color: problemas (rojo) > sin conexión (ámbar) > al día (verde).
const bg = hasProblems ? '#b00020' : online ? '#1f6f43' : '#8a6d00';
const content = ( const content = (
<View style={[styles.bar, { backgroundColor: online ? '#1f6f43' : '#8a6d00' }]}> <View style={[styles.bar, { backgroundColor: bg }]}>
{syncing && <ActivityIndicator color="#fff" size="small" />} {syncing && <ActivityIndicator color="#fff" size="small" />}
<Text style={styles.text}> <Text style={styles.text}>
{online ? 'En línea' : 'Sin conexión'} {online ? 'En línea' : 'Sin conexión'}
+15 -1
View File
@@ -45,8 +45,22 @@ export function ProjectDetailScreen({ route, navigation }: Props) {
}, [projectId, refreshCounts]); }, [projectId, refreshCounts]);
// Auto-sync (silencioso) al recuperar red / volver a primer plano / por intervalo. // Auto-sync (silencioso) al recuperar red / volver a primer plano / por intervalo.
// Aunque es silencioso, si un ciclo produce conflictos o errores nuevos avisamos
// proactivamente una vez (cada conflicto/error solo se reporta en su ciclo).
useAutoSync(true, async () => { useAutoSync(true, async () => {
await doSync(); const report = await doSync();
const problems = report.conflicts + report.errors;
if (problems > 0) {
Alert.alert(
'Sincronización con incidencias',
`${report.conflicts} conflicto(s) y ${report.errors} error(es) al sincronizar. ` +
'Revisa la cola para resolverlos.',
[
{ text: 'Ahora no', style: 'cancel' },
{ text: 'Revisar', onPress: () => navigation.navigate('Outbox') },
],
);
}
}); });
// Sync manual (con resumen). // Sync manual (con resumen).
+12 -1
View File
@@ -173,10 +173,14 @@ async function processResult(
return false; return false;
} }
/** Nº de ficheros que se suben en paralelo por lote. */
const MEDIA_CONCURRENCY = 3;
/** Sube los ficheros pendientes a /media (solo los de padres ya sincronizados). */ /** Sube los ficheros pendientes a /media (solo los de padres ya sincronizados). */
async function pushMedia(report: SyncReport): Promise<void> { async function pushMedia(report: SyncReport): Promise<void> {
const pending = await getPendingMedia(); const pending = await getPendingMedia();
for (const m of pending) {
const uploadOne = async (m: (typeof pending)[number]): Promise<void> => {
try { try {
const res = await uploadMedia({ const res = await uploadMedia({
uuid: m.uuid, uuid: m.uuid,
@@ -194,6 +198,13 @@ async function pushMedia(report: SyncReport): Promise<void> {
await markMediaError(m.uuid, e instanceof Error ? e.message : String(e)); await markMediaError(m.uuid, e instanceof Error ? e.message : String(e));
report.mediaErrors += 1; report.mediaErrors += 1;
} }
};
// Lotes de MEDIA_CONCURRENCY en paralelo (el runtime es monohilo: los
// contadores del report se incrementan sin condición de carrera).
for (let i = 0; i < pending.length; i += MEDIA_CONCURRENCY) {
const batch = pending.slice(i, i + MEDIA_CONCURRENCY);
await Promise.all(batch.map(uploadOne));
} }
} }