// ============ AcademIA8 · Consola — Usuarios de prueba (reset por curso) ============ // Vive DENTRO de cada curso (pestaña "Pruebas", junto a "Alumnos"): rearma la // experiencia de un número de prueba EN ESE CURSO puntual, sin tocar sus // inscripciones a otros cursos. Sin CLI ni credenciales AWS (C4bis-reset). // // El front nunca ve ni manda un teléfono completo para el REARMADO: cada // número trae un `id` opaco (ver A8Api.listPruebasAllowlist); el backend // resuelve id->teléfono contra la allowlist fresca de SSM en cada llamada y // rechaza cualquier `id` que no resuelva. El ALTA es la única excepción // necesaria: quien agrega un número lo tipea, así que ese teléfono SÍ viaja // (igual que en `admin_reset allowlist add --phone`). // // La allowlist es GLOBAL del entorno (dev/prod) -- los mismos números // habilitan el rearmado en TODOS los cursos, aunque el alta/baja se gestione // desde acá adentro de uno puntual. Tope de 5 números y formato E.164 // validados server-side (console_service.py); el front solo refleja. // Quitar un número de la allowlist NUNCA borra datos de ningún alumno: solo // deshabilita el rearmado para ese número (ver texto del botón). function RearmarModal({ course, id, telefonoEnmascarado, onClose, onDone }){ const [step,setStep] = useState('cargando'); // cargando | listo | ejecutando | resultado const [inv,setInv] = useState(null); const [error,setError] = useState(null); const [resultado,setResultado] = useState(null); useEffect(()=>{ let alive = true; (async ()=>{ try { const inventario = await A8Api.pruebasInventario(course.id, id); if (alive){ setInv(inventario); setStep('listo'); } } catch(e){ if (alive) setError((e&&e.message)||String(e)); } })(); return ()=>{ alive = false; }; }, [course.id, id]); async function confirmar(){ setStep('ejecutando'); setError(null); try { const r = await A8Api.pruebasReset(course.id, id); setResultado(r); setStep('resultado'); onDone && onDone(); } catch(e){ setError((e&&e.message)||String(e)); setStep('listo'); } } return (
Listo : <>
Cancelar {step==='ejecutando' ? 'Rearmando…' : 'Confirmar y rearmar'} }> {error && (
{error}
)} {step==='cargando' && (

Trayendo el inventario…

)} {(step==='listo' || step==='ejecutando') && inv && ( inv.encontrado ? ( <>
Esto borra tu avance, conversación e intentos de este curso y te reinscribe desde cero. Tus otros cursos no se tocan. Es definitivo — no hay forma de deshacerlo.
Se va a borrar
{inv.inscripcion.curso_titulo || inv.inscripcion.curso_id} {inv.inscripcion.estado}
Unidad {inv.inscripcion.unidad_actual ?? '—'} {inv.inscripcion.intentos_recall} intentos recall {inv.inscripcion.intentos_quiz_libre} quiz libre {inv.inscripcion.repasos} repasos {inv.inscripcion.tiene_memoria && con memoria de conversación}
) : (
Sin inscripción en este curso — se creará una nueva.
) )} {step==='resultado' && resultado && (

Experiencia rearmada en este curso

{resultado.borrado ? <>Se borraron {resultado.borrado.items_borrados} registros de la inscripción anterior.
: 'No había inscripción previa en este curso.'} Reinscripto como {resultado.reinscripcion.alumno_id}.

)}
); } const MAX_ALLOWLIST_SIZE = 5; function AgregarNumeroForm({ count, onAdded }){ const [phone,setPhone] = useState(''); const [busy,setBusy] = useState(false); const [err,setErr] = useState(null); const atTope = count >= MAX_ALLOWLIST_SIZE; const ok = !atTope && /^\+[1-9]\d{7,14}$/.test(phone.trim()); async function submit(){ if (!ok || busy) return; setBusy(true); setErr(null); try { const numeros = await A8Api.pruebasAllowlistAdd(phone.trim()); setPhone(''); onAdded(numeros); } catch(e){ setErr((e&&e.message)||String(e)); } finally { setBusy(false); } } return (
setPhone(e.target.value)} placeholder="+5491112345678" onKeyDown={e=>{ if(e.key==='Enter'&&ok) submit(); }} style={{flex:1}} /> {busy ? 'Agregando…' : 'Agregar'}
{err && (
{err}
)}
); } function PruebasCursoPanel({ course }){ const [numeros,setNumeros] = useState(null); // null = cargando const [error,setError] = useState(null); const [openId,setOpenId] = useState(null); const [quitandoId,setQuitandoId] = useState(null); async function load(){ setError(null); try { setNumeros(await A8Api.listPruebasAllowlist()); } catch(e){ setError((e&&e.message)||String(e)); } } useEffect(()=>{ load(); }, []); async function quitar(id){ setQuitandoId(id); setError(null); try { setNumeros(await A8Api.pruebasAllowlistRemove(id)); } catch(e){ setError((e&&e.message)||String(e)); } finally { setQuitandoId(null); } } const abierto = numeros && numeros.find(n=>n.id===openId); return (

Los números de prueba habilitados valen para todos los cursos, no solo este — vive acá por comodidad, porque "Rearmar experiencia en este curso" borra tu avance en{' '} {course.meta_draft?.name || course.id} y te reinscribe desde cero, sin tocar tus otros cursos.

Usuarios de prueba
{numeros ? numeros.length : '—'}
{error && (
{error}
)} {numeros===null && !error ? (

Cargando…

) : ( <> {numeros && numeros.length>0 ? (
{numeros.map(n=>(
{n.telefono_enmascarado}
setOpenId(n.id)}>Rearmar experiencia en este curso quitar(n.id)}> {quitandoId===n.id ? 'Quitando…' : 'Quitar'}
Quitar no borra datos del alumno — solo deshabilita el rearmado para este número.
))}
) : (
No hay números habilitados. Agregá uno arriba.
)} )} {abierto && ( setOpenId(null)} onDone={load} /> )}
); } window.PruebasCursoPanel = PruebasCursoPanel;