import { useEffect, useState, useCallback } from 'react' import api from '../lib/api' import { Search, Trash2, RefreshCw, Users, Tv, Database, Clock, AlertTriangle, CheckCircle } from 'lucide-react' import clsx from 'clsx' // ─── Connection log types ─────────────────────────────────────────────────── interface LogEntry { id: number user_id: number username: string channel_name: string | null content_type: string | null stream_id: string | null started_at: string | null ended_at: string | null duration_seconds: number | null bytes_transferred: number client_ip: string | null } interface Stats { total_sessions: number total_bytes: number avg_duration_seconds: number unique_users: number top_users: { username: string; sessions: number; bytes: number }[] top_channels: { channel_name: string; sessions: number }[] } // ─── Stream event types ────────────────────────────────────────────────────── interface StreamEvent { id: number channel_id: number | null channel_name: string | null provider_name: string | null event_type: 'caida' | 'recuperado' | string 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 } // ─── Helpers ───────────────────────────────────────────────────────────────── function fmtBytes(b: number): string { if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB' if (b >= 1e6) return (b / 1e6).toFixed(1) + ' MB' if (b >= 1e3) return (b / 1e3).toFixed(0) + ' KB' return b + ' B' } function fmtDuration(s: number | null): string { if (!s) return '—' if (s < 60) return `${s}s` if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s` return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m` } function fmtDate(iso: string | null): string { if (!iso) return '—' return new Date(iso).toLocaleString('es-ES', { dateStyle: 'short', timeStyle: 'short' }) } const TYPE_COLORS: Record = { live: 'bg-blue-500/20 text-blue-400', movie: 'bg-purple-500/20 text-purple-400', series: 'bg-orange-500/20 text-orange-400', } const TYPE_LABELS: Record = { live: 'Directo', movie: 'Película', series: 'Serie' } // ─── Sub-components ─────────────────────────────────────────────────────────── function StatCard({ icon: Icon, label, value, color }: { icon: typeof Clock; label: string; value: string; color: string }) { return (

{label}

{value}

) } // ─── Connection Logs tab ────────────────────────────────────────────────────── function ConnectionsTab() { const [logs, setLogs] = useState([]) const [stats, setStats] = useState(null) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [loading, setLoading] = useState(false) const [filterUsername, setFilterUsername] = useState('') const [filterChannel, setFilterChannel] = useState('') const [filterType, setFilterType] = useState('') const [filterDateFrom, setFilterDateFrom] = useState('') const [filterDateTo, setFilterDateTo] = useState('') const limit = 50 const loadStats = useCallback(async () => { try { const { data } = await api.get('/logs/stats') setStats(data) } catch {} }, []) const loadLogs = useCallback(async (p = 1) => { setLoading(true) try { const params: Record = { page: p, limit } if (filterUsername) params.username = filterUsername if (filterChannel) params.channel = filterChannel if (filterType) params.content_type = filterType if (filterDateFrom) params.date_from = filterDateFrom if (filterDateTo) params.date_to = filterDateTo const { data } = await api.get('/logs', { params }) setLogs(data.items) setTotal(data.total) setPage(p) } finally { setLoading(false) } }, [filterUsername, filterChannel, filterType, filterDateFrom, filterDateTo]) useEffect(() => { loadStats(); loadLogs(1) }, []) // eslint-disable-line const doSearch = () => { loadStats(); loadLogs(1) } const clearLogs = async () => { if (!confirm('¿Eliminar todos los registros de conexión?')) return await api.delete('/logs') setLogs([]) setTotal(0) loadStats() } const totalPages = Math.ceil(total / limit) return (
{stats && (
)} {stats && (

Top usuarios

{stats.top_users.map((u, i) => (
{i + 1} {u.username} {u.sessions} sesiones {fmtBytes(u.bytes)}
))} {stats.top_users.length === 0 &&

Sin datos

}

Top canales/contenido

{stats.top_channels.map((c, i) => (
{i + 1} {c.channel_name} {c.sessions}
))} {stats.top_channels.length === 0 &&

Sin datos

}
)}
setFilterUsername(e.target.value)} onKeyDown={e => e.key === 'Enter' && doSearch()} placeholder="Usuario" className="bg-dark-700 border border-dark-600 rounded-lg pl-8 pr-3 py-1.5 text-sm w-36 focus:outline-none focus:border-primary-500" />
setFilterChannel(e.target.value)} onKeyDown={e => e.key === 'Enter' && doSearch()} placeholder="Canal / película" className="bg-dark-700 border border-dark-600 rounded-lg pl-8 pr-3 py-1.5 text-sm w-48 focus:outline-none focus:border-primary-500" />
setFilterDateFrom(e.target.value)} className="bg-dark-700 border border-dark-600 rounded-lg px-3 py-1.5 text-sm focus:outline-none" /> → setFilterDateTo(e.target.value)} className="bg-dark-700 border border-dark-600 rounded-lg px-3 py-1.5 text-sm focus:outline-none" />
{total.toLocaleString()} registros {totalPages > 1 && (
{page} / {totalPages}
)}
{loading && } {!loading && logs.length === 0 && } {logs.map(log => ( ))}
Usuario IP Canal / Contenido Tipo Inicio Duración Datos
Cargando...
No hay registros
{log.username} {log.client_ip || '—'} {log.channel_name || log.stream_id || '—'} {log.content_type ? ( {TYPE_LABELS[log.content_type] || log.content_type} ) : '—'} {fmtDate(log.started_at)} {fmtDuration(log.duration_seconds)} {fmtBytes(log.bytes_transferred)}
) } // ─── Stream Events tab ──────────────────────────────────────────────────────── function EventsTab() { const [events, setEvents] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [loading, setLoading] = useState(false) const [filterChannel, setFilterChannel] = useState('') const [filterType, setFilterType] = useState('') const [filterDateFrom, setFilterDateFrom] = useState('') const [filterDateTo, setFilterDateTo] = useState('') const limit = 50 const loadEvents = useCallback(async (p = 1) => { setLoading(true) try { const params: Record = { page: p, limit } if (filterChannel) params.channel = filterChannel if (filterType) params.event_type = filterType if (filterDateFrom) params.date_from = filterDateFrom if (filterDateTo) params.date_to = filterDateTo const { data } = await api.get('/logs/events', { params }) setEvents(data.items) setTotal(data.total) setPage(p) } finally { setLoading(false) } }, [filterChannel, filterType, filterDateFrom, filterDateTo]) useEffect(() => { loadEvents(1) }, []) // eslint-disable-line const doSearch = () => loadEvents(1) const clearEvents = async () => { if (!confirm('¿Eliminar todos los registros de fallos?')) return await api.delete('/logs/events') setEvents([]) setTotal(0) } const totalPages = Math.ceil(total / limit) const caidas = events.filter(e => e.event_type === 'caida').length const recuperados = events.filter(e => e.event_type === 'recuperado').length return (
{/* Summary cards */}
{/* Filters */}
setFilterChannel(e.target.value)} onKeyDown={e => e.key === 'Enter' && doSearch()} placeholder="Canal / proveedor" className="bg-dark-700 border border-dark-600 rounded-lg pl-8 pr-3 py-1.5 text-sm w-48 focus:outline-none focus:border-primary-500" />
setFilterDateFrom(e.target.value)} className="bg-dark-700 border border-dark-600 rounded-lg px-3 py-1.5 text-sm focus:outline-none" /> → setFilterDateTo(e.target.value)} className="bg-dark-700 border border-dark-600 rounded-lg px-3 py-1.5 text-sm focus:outline-none" />
{/* Events table */}
{total.toLocaleString()} eventos {totalPages > 1 && (
{page} / {totalPages}
)}
{loading && } {!loading && events.length === 0 && ( )} {events.map(ev => ( ))}
Fecha Tipo Canal Proveedor · Dominio Causa Duración fallo Intentos
Cargando...
Sin fallos registrados — los errores reales del proveedor aparecerán aquí automáticamente
{fmtDate(ev.created_at)} {ev.event_type === 'caida' ? ( Caída ) : ev.event_type === 'recuperado' ? ( Recuperado ) : ( {ev.event_type} )} {ev.channel_name || (ev.channel_id ? `#${ev.channel_id}` : '—')} {ev.provider_name || '—'} {ev.domain && {ev.domain}} {ev.error_message || '—'} {ev.pump_duration_secs != null ? ( {fmtDuration(ev.pump_duration_secs)} ) : '—'} {ev.attempt_number ?? '—'}
) } // ─── Main page ──────────────────────────────────────────────────────────────── type Tab = 'connections' | 'events' export default function Logs() { const [activeTab, setActiveTab] = useState('connections') return (

Registro

{/* Tab bar */}
{activeTab === 'connections' && } {activeTab === 'events' && }
) }