Files
KiraTV/frontend/src/pages/EPG.tsx
T
KiraStream b969b7e5af Mejoras de estabilidad de stream IPTV y nuevo registro de fallos
- restream.py: eliminar -reconnect_streamed de ffmpeg para evitar rebobinados
  al reconectar (el proveedor re-enviaba desde keyframe anterior causando PTS
  backward). El loop de Python gestiona las reconexiones con conexión fresca.
- restream.py: eliminar _drain_queue del loop de reconexión para que el buffer
  de la cola cubra el tiempo de reconexión sin pantalla negra en el player.
- restream.py: añadir buffer_server_bytes/secs/capacity a stats() y método
  _log_stream_event() para persistir eventos de caída y recuperación en BD.
- models/stream_event.py: nuevo modelo StreamEvent para registro de fallos.
- database.py: registrar StreamEvent en init_db.
- api/admin/logs.py: nuevos endpoints GET/DELETE /logs/events.
- Dashboard.tsx: mostrar reserva de buffer del servidor (capacidad + estado).
- Logs.tsx: añadir pestaña "Fallos de stream" con tabla de eventos persistidos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:43:31 +00:00

119 lines
4.8 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Plus, Trash2, RefreshCw } from 'lucide-react'
import api from '../lib/api'
interface EpgSource { id: number; name: string; url: string; is_active: boolean; last_fetched_at: string | null }
export default function EPG() {
const [sources, setSources] = useState<EpgSource[]>([])
const [showForm, setShowForm] = useState(false)
const [form, setForm] = useState({ name: '', url: '' })
const [refreshing, setRefreshing] = useState(false)
async function load() {
const { data } = await api.get('/epg/')
setSources(data)
}
useEffect(() => { load() }, [])
async function submit(e: React.FormEvent) {
e.preventDefault()
await api.post('/epg/', form)
setForm({ name: '', url: '' })
setShowForm(false)
load()
}
async function del(id: number) {
if (!confirm('¿Eliminar fuente EPG?')) return
await api.delete(`/epg/${id}`)
load()
}
async function refresh() {
setRefreshing(true)
try {
await api.post('/epg/refresh')
alert('EPG actualización iniciada en segundo plano')
} finally {
setRefreshing(false)
}
}
return (
<div className="p-6 space-y-5">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold">Gestión EPG</h1>
<div className="flex gap-2">
<button
onClick={refresh}
disabled={refreshing}
className="flex items-center gap-2 bg-dark-700 hover:bg-dark-600 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
<RefreshCw size={15} className={refreshing ? 'animate-spin' : ''} />
Actualizar EPG
</button>
<button onClick={() => setShowForm(v => !v)} className="flex items-center gap-2 bg-primary-500 hover:bg-primary-600 px-4 py-2 rounded-lg text-sm font-medium">
<Plus size={16} /> Añadir fuente
</button>
</div>
</div>
<div className="bg-dark-800 border border-primary-500/20 rounded-xl p-4 text-sm text-gray-400">
<strong className="text-gray-200">Endpoint EPG para tus apps IPTV:</strong>
<div className="font-mono text-xs mt-1 bg-dark-700 rounded p-2 text-green-400">
http://10.10.10.234/xmltv.php?username=USUARIO&password=CONTRASEÑA
</div>
</div>
{showForm && (
<form onSubmit={submit} className="bg-dark-800 border border-dark-700 rounded-xl p-5 space-y-3">
<h3 className="font-semibold text-sm">Nueva fuente XMLTV</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-500 mb-1">Nombre</label>
<input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} required
className="w-full bg-dark-700 border border-dark-600 rounded-lg px-3 py-2 text-sm" />
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">URL XMLTV (o del proveedor)</label>
<input value={form.url} onChange={e => setForm(f => ({ ...f, url: e.target.value }))} required
placeholder="http://proveedor.com/xmltv.php?username=X&password=Y"
className="w-full bg-dark-700 border border-dark-600 rounded-lg px-3 py-2 text-sm" />
</div>
</div>
<div className="flex gap-2">
<button type="submit" className="bg-primary-500 hover:bg-primary-600 px-4 py-2 rounded-lg text-sm font-medium">Guardar</button>
<button type="button" onClick={() => setShowForm(false)} className="bg-dark-700 px-4 py-2 rounded-lg text-sm">Cancelar</button>
</div>
</form>
)}
<div className="space-y-3">
{sources.map(s => (
<div key={s.id} className="bg-dark-800 border border-dark-700 rounded-xl p-4 flex items-center justify-between gap-4">
<div>
<div className="font-semibold">{s.name}</div>
<div className="text-xs text-gray-500 truncate max-w-xl">{s.url}</div>
{s.last_fetched_at && (
<div className="text-xs text-gray-600 mt-0.5">
Última actualización: {new Date(s.last_fetched_at).toLocaleString('es-ES')}
</div>
)}
</div>
<button onClick={() => del(s.id)} className="p-2 rounded-lg bg-dark-700 hover:bg-red-900/40 text-gray-500 hover:text-red-400 transition-colors">
<Trash2 size={15} />
</button>
</div>
))}
{sources.length === 0 && (
<div className="bg-dark-800 border border-dark-700 border-dashed rounded-xl p-8 text-center text-gray-500 text-sm">
No hay fuentes EPG configuradas
</div>
)}
</div>
</div>
)
}