import { useEffect, useRef, useState } from 'react'; import { RefreshCw, Download } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAsync, useAction } from '../../hooks/useAsync.js'; import { getUpdateStatus, applyUpdate } from '../../services/updater.js'; import { Button } from '../../components/Button.jsx'; import { Badge } from '../../components/Badge.jsx'; import { InfoBar } from '../../components/InfoBar.jsx'; const CRITICALITY_COLOR = { alta: 'danger', media: 'warning', baja: 'success' }; const POLL_INTERVAL_MS = 5000; const POLL_MAX_ATTEMPTS = 60; // ~5 minutos tope export default function UpdaterPanel() { const { t, openDialog, pushToast } = useUi(); const { data: status, loading, error, reload, setData } = useAsync(getUpdateStatus, []); const runAction = useAction(); const [applying, setApplying] = useState(false); const [progressPct, setProgressPct] = useState(0); const mountedRef = useRef(true); // Setup explicito (no solo el cleanup): con React.StrictMode en dev el efecto se dispara // mount->cleanup->mount de una, y si el setup no reactiva el flag queda en false para siempre // aunque el componente SI este montado -- pollUntilUpToDate nunca correria. useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); // El servidor se reinicia a mitad de la actualizacion (backend y frontend), asi que durante // ese rato las llamadas van a fallar por un momento -- se ignoran y se sigue insistiendo. El // porcentaje es una estimacion sobre el tope de espera (no hay progreso real del script), pero // alcanza para que la persona sepa que sigue avanzando y no quede mirando una pantalla muerta. // Al confirmar que ya esta al dia, se recarga la pestaña sola para traer el build nuevo. async function pollUntilUpToDate() { for (let i = 0; i < POLL_MAX_ATTEMPTS && mountedRef.current; i++) { await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); if (!mountedRef.current) return; setProgressPct(Math.min(95, Math.round(((i + 1) / POLL_MAX_ATTEMPTS) * 100))); try { const result = await getUpdateStatus(); if (!mountedRef.current) return; setData(result); if (result.upToDate) { setProgressPct(100); await new Promise((resolve) => setTimeout(resolve, 600)); window.location.reload(); return; } } catch { // backend reiniciandose -- se ignora y se reintenta en el proximo ciclo } } if (mountedRef.current) { setApplying(false); pushToast({ tone: 'warning', message: t('updater_apply_timeout') }); } } function confirmApply() { openDialog({ tone: 'warning', title: t('updater_apply_confirm_title'), message: `${t('updater_apply_confirm_msg')} v${status.currentVersion} → v${status.latestVersion}`, actions: [ { label: t('common_cancel'), variant: 'standard' }, { label: t('updater_apply_confirm_ok'), variant: 'danger', onClick: () => runAction(async () => { setProgressPct(0); setApplying(true); await applyUpdate(); pushToast({ tone: 'success', message: t('updater_apply_started') }); }).then(pollUntilUpToDate).catch(() => setApplying(false)), }, ], }); } return (