From 26efe10ae6e5995bf47c041b6fc5dae01a91bc9b Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:36:33 -0500 Subject: [PATCH] Actividad: tarjeta de salud de la voz del televisor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La bitácora turnero_tv_log solo se podía consultar con SQL, así que en la práctica nadie la iba a mirar. La pantalla de Actividad (Reportes → Actividad) muestra ahora, en lenguaje llano: cuántos llamados con voz hubo en 7 días, qué porcentaje habló completo, cuántos salieron mudos y cuántos se recuperaron con reintento, más los últimos fallos con fecha, turno y voz usada. La tarjeta no aparece mientras no haya datos (recién desplegado), para no mostrar una sección vacía. De paso: el ancla del parche se comió el async de exportarCSV; restaurado y verificado bloque por bloque. Co-Authored-By: Claude Fable 5 --- api/lab/get_tv_log.php | 45 ++++++++++++++++++++++++++++++++ lab_actividad.php | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 api/lab/get_tv_log.php diff --git a/api/lab/get_tv_log.php b/api/lab/get_tv_log.php new file mode 100644 index 0000000..da17270 --- /dev/null +++ b/api/lab/get_tv_log.php @@ -0,0 +1,45 @@ +query( + "SELECT evento, COUNT(*) n FROM turnero_tv_log + WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) + GROUP BY evento" +)->fetchAll(PDO::FETCH_KEY_PAIR); + +// Últimos incidentes (lo normal —start/end— no se lista: solo lo que falló) +$stmt = $pdo->query( + "SELECT evento, detalle, created_at FROM turnero_tv_log + WHERE evento IN ('mudo', 'sin_start', 'error') + ORDER BY id DESC LIMIT 30" +); +$incidentes = []; +foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) { + $d = json_decode($r['detalle'] ?? '{}', true) ?: []; + $incidentes[] = [ + 'cuando' => $r['created_at'], + 'evento' => $r['evento'], + 'turno' => $d['cod'] ?? null, + 'voz' => $d['voz'] ?? null, + ]; +} + +$anuncios = (int)($resumen['start'] ?? 0); +$mudos = (int)($resumen['mudo'] ?? 0); +$avisos = (int)($resumen['sin_start'] ?? 0) + (int)($resumen['error'] ?? 0); + +jsonOk([ + 'anuncios_7d' => $anuncios, + 'completos_7d' => (int)($resumen['end'] ?? 0), + 'mudos_7d' => $mudos, + 'avisos_7d' => $avisos, + 'incidentes' => $incidentes, +]); diff --git a/lab_actividad.php b/lab_actividad.php index a466c28..224f31a 100644 --- a/lab_actividad.php +++ b/lab_actividad.php @@ -111,6 +111,18 @@ require_once __DIR__ . '/shared/components/sidebar.php'; + + +
@@ -277,6 +289,52 @@ function verDetalle(i) { new bootstrap.Modal(document.getElementById('modalDetalle')).show(); } +// ── Salud de la voz del televisor ──────────────────────────── +const VOZ_EVENTO_TXT = { + mudo: ['🔇 No sonó', 'text-danger', 'El llamado salió sin voz: ni el reintento habló'], + sin_start: ['⚠️ Arrancó con reintento','text-warning','La voz elegida no arrancó; habló la del navegador'], + error: ['⚠️ Error del motor', 'text-warning', 'El motor de voz falló a mitad; se reintentó'], +}; + +async function cargarVozTV() { + try { + const r = await fetch('api/lab/get_tv_log.php'); + const d = await r.json(); + if (!d.ok) return; + // Sin datos todavía (recién desplegado): la tarjeta no aparece + if (!d.anuncios_7d && !d.incidentes.length) return; + + document.getElementById('card-voz-tv').style.display = ''; + const pct = d.anuncios_7d ? Math.round(d.completos_7d / d.anuncios_7d * 100) : 0; + document.getElementById('voz-tv-resumen').innerHTML = ` +
${d.anuncios_7d} + llamados con voz
+
${pct}% + hablados completos
+
${d.mudos_7d} + salieron mudos
+
${d.avisos_7d} + se recuperaron con reintento
`; + + const inc = document.getElementById('voz-tv-incidentes'); + if (!d.incidentes.length) { + inc.innerHTML = 'Sin fallos registrados'; + return; + } + inc.innerHTML = '
Últimos fallos:
' + + d.incidentes.slice(0, 10).map(i => { + const [txt, cls, ayuda] = VOZ_EVENTO_TXT[i.evento] || [i.evento, '', '']; + return `
+ ${(i.cuando || '').slice(0, 16)} + · turno ${i.turno || '—'} + · ${txt} + ${i.voz ? ` (voz: ${i.voz})` : ''} +
`; + }).join(''); + } catch (_) {} +} +cargarVozTV(); + async function exportarCSV() { const params = new URLSearchParams({ desde: document.getElementById('act-desde').value,