08-envio
Úsala cuando el estudiante pida la skill 08 o el requisito R9 (envío automático de lecturas pendientes al recuperar la conexión, sin duplicados, y banner de conexión y pendientes en todas las pantalla
SKILL.md.github/skills/08-envio/SKILL.md
GitHubSkill 08: Envío automático (R9)
Lee antes
docs/CONTRATO.md secciones 1, 3, 4, 5 y 6 (R4, R9, R10 e idempotencia). Antes de modificarlos, lee src/db/readings.ts, src/services/api.ts, src/hooks/useNetworkStatus.ts, src/domain/consumption.ts, src/app/_layout.tsx, src/app/(tecnico)/index.tsx y lectura/paso-3.tsx. Si existe ADR-008, léelo; si no, manda el contrato.
Instala
Nada (expo-network ya está). Verifica en la documentación del SDK 57 el uso de useNetworkState y la API de AppState de React Native.
Crea o modifica
- src/db/readings.ts (todas con parámetros preparados):
listPending(db): filas con sync_status = 'pending', más antiguas primero.claimReading(db, clientId): UPDATE a 'sending' WHERE client_id = ? AND sync_status = 'pending'. Devuelve true solo si cambió 1 fila.markSent(db, clientId): sync_status = 'sent', sent_at y updated_at ahora, WHERE sync_status = 'sending'.releaseReading(db, clientId): 'sending' → 'pending'.resetSendingToPending(db): todas las 'sending' → 'pending'.applyVerification(db, clientId, { previousReading, averageMonthly, consumption, isAnomalous }): actualiza esos campos y pone verification = 'verified', WHERE sync_status = 'sending'.countUnsent(db): cuántas tienen sync_status distinto de 'sent'.
- src/services/api.ts: agrega
findReadingByClientId(clientId)(GET /readings?clientId=<id>; devuelve el elemento con clientId IGUAL exacto, o null) ypostReading(dto)(POST /readings con JSON). Ambas lanzan error si falla la red o la respuesta no es OK. El DTO sale de la fila según ReadingDTO del contrato, con status "sent". - src/domain/consumption.ts:
verifyWithMeter(row, meter)pura: devuelve { previousReading: meter.lastReading, averageMonthly: meter.averageMonthlyConsumption, consumption (readingValue - lastReading, o null si no hay lectura), isAnomalous (consumption > 3 * average, estrictamente mayor, false si consumption es null) }. - src/store/sync.ts (Zustand, SIN persistir): { pendingCount, isSyncing, version } y acciones para actualizarlos.
- src/services/sync.ts:
syncPending(db):- Exclusión: si ya hay una ejecución en curso, marca que se pidió otra y vuelve; al terminar, si se pidió otra, corre una vez más.
- Para cada fila de listPending: (a) claimReading; si devuelve false, saltar. (b) Si verification = 'pending_verification': fetchMeterByCode(meterCode); con verifyWithMeter aplica applyVerification. Si el medidor no se puede consultar, releaseReading y seguir con la siguiente. (c) findReadingByClientId; si ya existe, markSent sin enviar. (d) Si no existe, postReading y luego markSent. (e) Ante cualquier error en (b), (c) o (d): releaseReading y DETENER el recorrido (probablemente no hay internet real).
- Al terminar o fallar: actualiza pendingCount con countUnsent y suma 1 a
version. refreshPendingCount(db).
- src/components/SyncManager.tsx (sin interfaz), dentro de los providers del layout raíz: al montar, resetSendingToPending y luego syncPending; vuelve a llamar syncPending cuando isOnline pasa a true (hook useNetworkStatus), cuando la app vuelve a primer plano (AppState) y cada 30 s mientras haya pendientes y la app esté activa (limpia el temporizador al desmontar).
- src/components/ConnectionBanner.tsx: franja fija arriba, visible en TODAS las pantallas, con "Sin conexión" o "Conectado", "Pendientes: N" y "Enviando…" si isSyncing. Colores solo desde tokens.
- src/app/_layout.tsx: renderiza <ConnectionBanner /> sobre el Stack y <SyncManager />. Conserva SQLiteProvider, QueryClientProvider y la protección de sesión.
- src/app/(tecnico)/lectura/paso-3.tsx: tras guardar o corregir una lectura, llama refreshPendingCount y syncPending(db) sin esperar.
- src/app/(tecnico)/index.tsx: vuelve a cargar la lista cuando cambie
versiondel store, además de al enfocar. - docs/CONTRATO.md: agrega sync.ts, store/sync.ts, SyncManager, ConnectionBanner y las funciones nuevas de readings.ts. Avísame.
Reglas
- Nunca enviar una lectura sin pasar por claimReading y por findReadingByClientId.
- Una lectura 'sending' o 'sent' no se corrige.
- Si el POST falla, la lectura vuelve a 'pending' y nada se pierde.
- No usar setInterval para detectar la red; el temporizador de 30 s es solo para reintentar el envío.
- Solo api.ts llama a MockAPI. Estilos solo desde src/theme/tokens.ts.
Checklist
- npx tsc --noEmit sin errores.
- Ninguna dependencia nueva.
- Con señal, guardo una lectura y en segundos aparece "Enviada", Pendientes: 0, y en el panel de MockAPI (readings) hay UN registro con ese clientId.
- En modo avión guardo una lectura: banner "Sin conexión" y Pendientes: 1. Apago el modo avión sin tocar la app: se envía sola y queda Pendientes: 0.
- Una lectura registrada sin conexión como "pendiente de verificar" pasa a verificada tras enviarse, con la lectura anterior del servidor.
- Guardo sin conexión, cierro la app desde recientes, recupero la señal y la abro: se envía.
- Enciendo y apago el modo avión varias veces seguidas: en MockAPI sigue habiendo UN solo registro por lectura.
- El banner se ve en login, lista, buscar y los tres pasos.
- Una lectura enviada ya no muestra "Corregir".
- Pídeme probar los casos anteriores.
Al terminar
Actualiza MEMORY.md, escribe la explicación para la defensa y sugiere el mensaje de commit.