fix: distinguir sesiones normales del proveedor de fallos reales en logs y dashboard

- Solo registra "caída" cuando ffmpeg sale en <25s (error real: DNS, 403, auth, stall)
- Timeouts de sesión del proveedor (~2 min) NO se registran como fallos — son normales
- "Recuperado" se registra cuando el stream vuelve a funcionar tras un fallo real
- Añade clasificación automática de causa: conexión rechazada, DNS, 403, timeout...
- Dashboard: "reinicios" ahora distingue sesiones del proveedor (gris, normal) vs fallos (rojo)
- Registro: nueva columna "Duración fallo", proveedor+dominio juntos, filas con color de fondo
- StreamEvent: campo pump_duration_secs para persistir duración del fallo o interrupción

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
KiraStream
2026-05-19 15:12:04 +00:00
parent f965ccafa8
commit 4bfc7592a6
12 changed files with 474 additions and 362 deletions
+26 -16
View File
@@ -39,6 +39,7 @@ interface StreamEvent {
domain: string | null
error_message: string | null
attempt_number: number | null
pump_duration_secs: number | null // for caída: seconds ffmpeg ran; for recuperado: outage length
created_at: string | null
}
@@ -375,42 +376,51 @@ function EventsTab() {
<th className="text-left px-4 py-2">Fecha</th>
<th className="text-left px-4 py-2">Tipo</th>
<th className="text-left px-4 py-2">Canal</th>
<th className="text-left px-4 py-2">Proveedor</th>
<th className="text-left px-4 py-2">Dominio</th>
<th className="text-left px-4 py-2">Detalle</th>
<th className="text-right px-4 py-2">Intento</th>
<th className="text-left px-4 py-2">Proveedor · Dominio</th>
<th className="text-left px-4 py-2">Causa</th>
<th className="text-left px-4 py-2 whitespace-nowrap" title="Para caídas: tiempo que duró ffmpeg antes de fallar. Para recuperados: duración total de la interrupción.">Duración fallo</th>
<th className="text-right px-4 py-2" title="Número de intentos fallidos antes de recuperar (solo en recuperados)">Intentos</th>
</tr>
</thead>
<tbody>
{loading && <tr><td colSpan={7} className="text-center py-8 text-gray-500">Cargando...</td></tr>}
{!loading && events.length === 0 && (
<tr><td colSpan={7} className="text-center py-8 text-gray-500">No hay eventos registrados — los fallos aparecerán aquí automáticamente</td></tr>
<tr><td colSpan={7} className="text-center py-8 text-gray-500">Sin fallos registrados — los errores reales del proveedor aparecerán aquí automáticamente</td></tr>
)}
{events.map(ev => (
<tr key={ev.id} className="border-b border-dark-700/50 hover:bg-dark-700/30 transition-colors">
<td className="px-4 py-2 text-gray-400 text-xs whitespace-nowrap">{fmtDate(ev.created_at)}</td>
<td className="px-4 py-2">
<tr key={ev.id} className={`border-b border-dark-700/50 hover:bg-dark-700/30 transition-colors ${ev.event_type === 'caida' ? 'bg-red-950/10' : ev.event_type === 'recuperado' ? 'bg-emerald-950/10' : ''}`}>
<td className="px-4 py-2.5 text-gray-400 text-xs whitespace-nowrap">{fmtDate(ev.created_at)}</td>
<td className="px-4 py-2.5">
{ev.event_type === 'caida' ? (
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-500/20 text-red-400">
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-red-500/20 text-red-400 border border-red-800/50">
<AlertTriangle size={10} /> Caída
</span>
) : ev.event_type === 'recuperado' ? (
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400">
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400 border border-emerald-800/50">
<CheckCircle size={10} /> Recuperado
</span>
) : (
<span className="text-xs text-gray-400">{ev.event_type}</span>
)}
</td>
<td className="px-4 py-2 text-gray-200 max-w-[180px] truncate" title={ev.channel_name ?? ''}>
{ev.channel_name || `#${ev.channel_id}` || '—'}
<td className="px-4 py-2.5 text-gray-200 max-w-[180px] truncate text-xs" title={ev.channel_name ?? ''}>
{ev.channel_name || (ev.channel_id ? `#${ev.channel_id}` : '—')}
</td>
<td className="px-4 py-2 text-gray-400 text-xs">{ev.provider_name || '—'}</td>
<td className="px-4 py-2 text-gray-400 font-mono text-xs">{ev.domain || '—'}</td>
<td className="px-4 py-2 text-gray-500 text-xs max-w-[240px] truncate" title={ev.error_message ?? ''}>
<td className="px-4 py-2.5 text-xs">
<span className="text-gray-400">{ev.provider_name || '—'}</span>
{ev.domain && <span className="block text-gray-600 font-mono text-[10px]">{ev.domain}</span>}
</td>
<td className="px-4 py-2.5 text-gray-300 text-xs max-w-[260px]" title={ev.error_message ?? ''}>
{ev.error_message || '—'}
</td>
<td className="px-4 py-2 text-right text-gray-500 text-xs tabular-nums">
<td className="px-4 py-2.5 text-xs tabular-nums">
{ev.pump_duration_secs != null ? (
<span className={ev.event_type === 'caida' ? 'text-red-400' : 'text-emerald-400'}>
{fmtDuration(ev.pump_duration_secs)}
</span>
) : '—'}
</td>
<td className="px-4 py-2.5 text-right text-gray-500 text-xs tabular-nums">
{ev.attempt_number ?? '—'}
</td>
</tr>