From e41892ef2d4056c57ac154c92ff68a7cdf68103f Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 29 Oct 2025 14:16:57 -0300 Subject: [PATCH] Feat Skeleton Table de Carga - Fix Freno del Proceso al Cerrar Web --- .../src/Titulares.Api/Hubs/TitularesHub.cs | 24 +++++++-- .../Services/EstadoProcesoService.cs | 33 ++++++++++-- frontend/src/components/Dashboard.tsx | 42 ++++++++++----- frontend/src/components/TableSkeleton.tsx | 51 +++++++++++++++++++ 4 files changed, 128 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/TableSkeleton.tsx diff --git a/backend/src/Titulares.Api/Hubs/TitularesHub.cs b/backend/src/Titulares.Api/Hubs/TitularesHub.cs index d16dea6..261f187 100644 --- a/backend/src/Titulares.Api/Hubs/TitularesHub.cs +++ b/backend/src/Titulares.Api/Hubs/TitularesHub.cs @@ -1,12 +1,30 @@ // backend/src/Titulares.Api/Hubs/TitularesHub.cs using Microsoft.AspNetCore.SignalR; +using Titulares.Api.Services; namespace Titulares.Api.Hubs; -// Esta clase es el punto de conexión para los clientes de SignalR. -// No necesitamos añadirle métodos personalizados porque solo enviaremos -// mensajes desde el servidor hacia los clientes. public class TitularesHub : Hub { + private readonly EstadoProcesoService _estadoService; + + public TitularesHub(EstadoProcesoService estadoService) + { + _estadoService = estadoService; + } + + // Este método se ejecuta CADA VEZ que un nuevo cliente (pestaña) se conecta. + public override Task OnConnectedAsync() + { + _estadoService.RegistrarConexion(); + return base.OnConnectedAsync(); + } + + // Este método se ejecuta CADA VEZ que un cliente (pestaña) se desconecta. + public override Task OnDisconnectedAsync(Exception? exception) + { + _estadoService.RegistrarDesconexionYApagarSiEsElUltimo(); + return base.OnDisconnectedAsync(exception); + } } \ No newline at end of file diff --git a/backend/src/Titulares.Api/Services/EstadoProcesoService.cs b/backend/src/Titulares.Api/Services/EstadoProcesoService.cs index 1a5152a..b68e479 100644 --- a/backend/src/Titulares.Api/Services/EstadoProcesoService.cs +++ b/backend/src/Titulares.Api/Services/EstadoProcesoService.cs @@ -1,12 +1,11 @@ -// backend/src/Titulares.Api/Services/EstadoProcesoService.cs - namespace Titulares.Api.Services; public class EstadoProcesoService { private volatile bool _estaActivo = false; + private volatile int _connectionCount = 0; + private readonly object _lock = new object(); - // 1. Definimos un evento público al que otros servicios pueden suscribirse. public event Action? OnStateChanged; public bool EstaActivo() => _estaActivo; @@ -14,14 +13,38 @@ public class EstadoProcesoService public void Activar() { _estaActivo = true; - // 2. Disparamos el evento para notificar a los suscriptores. OnStateChanged?.Invoke(); } public void Desactivar() { _estaActivo = false; - // 3. Disparamos el evento también al desactivar. OnStateChanged?.Invoke(); } + + public void RegistrarConexion() + { + lock (_lock) + { + _connectionCount++; + } + } + + public void RegistrarDesconexionYApagarSiEsElUltimo() + { + lock (_lock) + { + _connectionCount--; + // Si el contador llega a 0, significa que no hay más clientes conectados. + // Apagamos el proceso de forma segura. + if (_connectionCount <= 0) + { + _connectionCount = 0; // Prevenir números negativos + if (_estaActivo) + { + Desactivar(); + } + } + } + } } \ No newline at end of file diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 17b87f4..9590ae8 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,8 +1,7 @@ +// frontend/src/components/Dashboard.tsx + import { useEffect, useState, useCallback } from 'react'; -import { - Box, Button, Stack, Chip, CircularProgress, - Accordion, AccordionSummary, AccordionDetails, Typography -} from '@mui/material'; +import { Box, Button, Stack, Chip, CircularProgress, Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import SyncIcon from '@mui/icons-material/Sync'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; @@ -18,6 +17,7 @@ import EditarTitularModal from './EditarTitularModal'; import { PowerSwitch } from './PowerSwitch'; import ConfirmationModal from './ConfirmationModal'; import type { ActualizarTitularPayload } from '../services/apiService'; +import { TableSkeleton } from './TableSkeleton'; const Dashboard = () => { const [titulares, setTitulares] = useState([]); @@ -26,9 +26,10 @@ const Dashboard = () => { const [isGeneratingCsv, setIsGeneratingCsv] = useState(false); const [confirmState, setConfirmState] = useState<{ open: boolean; onConfirm: (() => void) | null }>({ open: false, onConfirm: null }); const { showNotification } = useNotification(); - const [titularAEditar, setTitularAEditar] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => { setTitulares(titularesActualizados); }, []); @@ -38,11 +39,15 @@ const Dashboard = () => { ]); useEffect(() => { + // 1. Cargamos la configuración persistente const fetchConfig = api.obtenerConfiguracion(); + + // 2. Preguntamos al servidor por el estado ACTUAL del proceso const fetchEstado = api.getEstadoProceso(); Promise.all([fetchConfig, fetchEstado]) .then(([configData, estadoData]) => { + // Construimos el estado de la UI para que REFLEJE el estado real del servidor setConfig({ ...configData, scrapingActivo: estadoData.activo @@ -50,8 +55,13 @@ const Dashboard = () => { }) .catch(error => console.error("Error al cargar datos iniciales:", error)); - api.obtenerTitulares().then(setTitulares); - }, []); + // La carga de titulares sigue igual + api.obtenerTitulares() + .then(setTitulares) + .catch(error => console.error("Error al cargar titulares:", error)) + .finally(() => setIsLoading(false)); + + }, []); // El array vacío asegura que esto solo se ejecute una vez const handleDelete = (id: number) => { const onConfirm = async () => { @@ -188,13 +198,17 @@ const Dashboard = () => { - setTitularAEditar(titular)} - onSave={handleSaveEdit} - /> + {isLoading ? ( + + ) : ( + setTitularAEditar(titular)} + onSave={handleSaveEdit} + /> + )} ( + + + + + + + + + + + + + + + + + +); + +// El componente principal que renderiza la tabla fantasma +export const TableSkeleton = () => { + return ( + + + + + + + Texto del Titular + Tipo + Fuente + Acciones + + + + {/* Creamos un array de 5 elementos para renderizar 5 filas de esqueleto */} + {[...Array(5)].map((_, index) => ( + + ))} + +
+
+
+ ); +}; \ No newline at end of file