diff --git a/CHANGELOG.md b/CHANGELOG.md index 94882d1..274e392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,33 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/lang/es/). --- +## [1.3.0] - 2025-11-26 + +### ✨ Añadido +- **Carga Progresiva**: Los resultados de la búsqueda aparecen inmediatamente y la verificación por ping se realiza en segundo plano, actualizando el estado en tiempo real. +- **Feedback Visual Mejorado**: Nuevos iconos de estado (⏳ Verificando, ✅ Disponible, ⚠️ En uso) en la tabla de resultados. +- **Filtro de IPs Administrativas**: Nuevo checkbox para ocultar/mostrar IPs de administración (1-30, 254) al instante. + +### 🔄 Mejorado +- **Rendimiento**: La interfaz ya no se bloquea durante la verificación por ping. +- **Experiencia de Usuario**: Se puede ver el progreso de la verificación IP por IP. + +--- + +## [1.2.1] - 2025-11-26 + +### ✨ Añadido +- **Límite de IPs para verificación**: Nuevo selector para limitar la cantidad de IPs a verificar con ping (5, 10, 20 o Todas) + - Evita timeouts en segmentos grandes + - Prioriza IPs de cliente (las administrativas se muestran sin verificar cuando hay límite) +- **Defaults mejorados**: Checkbox de verificación activado por defecto (si está habilitado en config) + +### 🔄 Mejorado +- **Optimización de verificación**: Al usar un límite, las IPs administrativas se excluyen del ping para acelerar el proceso +- **Interfaz de usuario**: Feedback visual más claro sobre el límite aplicado + +--- + ## [1.2.0] - 2025-11-26 ### ✨ Añadido diff --git a/README.md b/README.md index 46dabdf..d0eb3b7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SIIP - Buscador de IP's Disponibles UISP -[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](manifest.json) +[![Version](https://img.shields.io/badge/version-1.3.0-blue.svg)](manifest.json) [![UCRM](https://img.shields.io/badge/UCRM-Compatible-green.svg)](https://uisp.com/) [![UNMS](https://img.shields.io/badge/UNMS-Compatible-green.svg)](https://uisp.com/) @@ -142,17 +142,19 @@ La interfaz incluye: ### Verificación por Ping (Opcional) -Si está habilitada en la configuración, aparecerá un checkbox "🔍 Verificar con ping" en el formulario. +Si está habilitada en la configuración, aparecerá un checkbox "🔍 Verificar con ping" (marcado por defecto) y un selector de límite. -**¿Qué hace?** -- Verifica que las IPs reportadas como disponibles realmente no respondan a ping -- Detecta dispositivos no registrados en UISP (computadoras, impresoras, cámaras, etc.) -- Filtra automáticamente IPs que responden (posibles conflictos) +**Opciones de Límite:** +- **Todas (Lento)**: Verifica todas las IPs del segmento. +- **5, 10, 20 IPs (Rápido)**: Verifica solo las primeras N IPs disponibles para clientes. -**¿Cuándo usarlo?** -- ✅ Antes de asignar IPs a clientes nuevos (máxima seguridad) -- ✅ En redes con dispositivos no gestionados -- ❌ Para búsquedas rápidas donde no importa la verificación +**Feedback Visual:** +- ⏳ **Pendiente/Verificando**: La IP está siendo analizada. +- ✅ **Disponible**: La IP está libre en UISP y no responde a ping. +- ⚠️ **En uso (Ping)**: La IP está libre en UISP pero responde a ping (posible conflicto). + +### Filtrado de IPs +- **Ocultar Admin IPs**: Checkbox para ocultar/mostrar instantáneamente las IPs reservadas para administración (x.x.x.1-30 y x.x.x.254). --- @@ -213,13 +215,15 @@ Busca todas las IPs disponibles en un segmento de red específico. - `type` (string, requerido): Tipo de evento, debe ser `"event.ip_request"` - `segment` (string, requerido): Tercer octeto del segmento (0-255) - `verify_ping` (boolean, opcional): Si es `true`, verifica IPs con ping antes de reportarlas +- `ping_limit` (int, opcional): Cantidad máxima de IPs a verificar (0 = todas, default: 0) -**Ejemplo con verificación por ping**: +**Ejemplo con límite**: ```json { "type": "event.ip_request", "segment": "5", - "verify_ping": true + "verify_ping": true, + "ping_limit": 5 } ``` @@ -235,12 +239,13 @@ Busca todas las IPs disponibles en un segmento de red específico. }, "ping_verified": true, "ping_stats": { - "total_checked": 223, - "responding": 1, - "not_responding": 222, - "execution_time": "2.3s" + "total_checked": 5, + "responding": 0, + "not_responding": 5, + "execution_time": "0.8s", + "limit_applied": 5 }, - "ping_responding": ["172.16.5.50"] + "ping_responding": [] } ``` @@ -580,5 +585,5 @@ Este plugin es propiedad de **SIIP Internet**. Todos los derechos reservados. --- -**Versión**: 1.2.0 +**Versión**: 1.3.0 **Última actualización**: 26 de noviembre de 2025 diff --git a/data/plugin.log b/data/plugin.log index 2f4116c..5c1e8e0 100644 --- a/data/plugin.log +++ b/data/plugin.log @@ -379,3 +379,194 @@ IPs obtenidas exitosamente: 3105 direcciones Búsqueda de IPs en segmento 172.16.100.x - Disponibles: 123, En uso: 131 Resultado de búsqueda: {"success":true,"ipsDisponibles":123,"ipsEnUso":131} <<< Finalizando handler de búsqueda AJAX +=== NUEVA PETICIÓN === +Método: GET +POST data: [] +GET data: [] +Content-Type: +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +Acceso a la interfaz pública de búsqueda de IPs +=== NUEVA PETICIÓN === +Método: POST +POST data: {"action":"search","segment":"13"} +GET data: [] +Content-Type: multipart/form-data; boundary=----geckoformboundary43e6452152194b7d2f9ee1cc1f4dcc5f +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +>>> Entrando al handler de búsqueda AJAX +Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true} +Buscando IPs en segmento: 13 (sin verificación por ping) +URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Respuesta HTTP: 200 +Longitud de respuesta: 48906 bytes +IPs obtenidas exitosamente: 3105 direcciones +Búsqueda de IPs en segmento 172.16.13.x - Disponibles: 153, En uso: 101 +Resultado de búsqueda: {"success":true,"ipsDisponibles":153,"ipsEnUso":101} +<<< Finalizando handler de búsqueda AJAX +=== NUEVA PETICIÓN === +Método: GET +POST data: [] +GET data: [] +Content-Type: +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +Acceso a la interfaz pública de búsqueda de IPs +=== NUEVA PETICIÓN === +Método: POST +POST data: {"action":"search","segment":"13","verify_ping":"true"} +GET data: [] +Content-Type: multipart/form-data; boundary=----geckoformboundarycaf9a5a4ec0cc39fcde382e2ad3254b5 +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +>>> Entrando al handler de búsqueda AJAX +Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true} +Buscando IPs en segmento: 13 (con verificación por ping) +URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Respuesta HTTP: 200 +Longitud de respuesta: 48906 bytes +IPs obtenidas exitosamente: 3105 direcciones +Verificación por ping habilitada, iniciando verificación... +Iniciando verificación por ping de 153 IPs (lotes de 30) +Procesando lote 1/6 (30 IPs) +Lote completado: 1/30 IPs responden (29.20s) +Procesando lote 2/6 (30 IPs) +Lote completado: 1/30 IPs responden (29.35s) +Procesando lote 3/6 (30 IPs) +Lote completado: 0/30 IPs responden (30.20s) +Procesando lote 4/6 (30 IPs) +Lote completado: 1/30 IPs responden (29.23s) +Procesando lote 5/6 (30 IPs) +Lote completado: 0/30 IPs responden (30.19s) +Procesando lote 6/6 (3 IPs) +Lote completado: 0/3 IPs responden (3.11s) +ADVERTENCIA: 3 IPs responden a ping pero no están en UISP: 172.16.13.45, 172.16.13.52, 172.16.13.188 +Verificación por ping completada: 153 IPs verificadas, 3 responden, 150 no responden (151.28s) +Búsqueda de IPs en segmento 172.16.13.x - Disponibles: 150, En uso: 101 (verificado con ping) +Resultado de búsqueda: {"success":true,"ipsDisponibles":150,"ipsEnUso":101} +<<< Finalizando handler de búsqueda AJAX +=== NUEVA PETICIÓN === +Método: GET +POST data: [] +GET data: [] +Content-Type: +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +Acceso a la interfaz pública de búsqueda de IPs +=== NUEVA PETICIÓN === +Método: POST +POST data: {"action":"search","segment":"13"} +GET data: [] +Content-Type: multipart/form-data; boundary=----geckoformboundarycb3b0d69f1f38554c073fe54b312e6d4 +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +>>> Entrando al handler de búsqueda AJAX +Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true} +Buscando IPs en segmento: 13 (sin verificación por ping) +URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Respuesta HTTP: 200 +Longitud de respuesta: 48906 bytes +IPs obtenidas exitosamente: 3105 direcciones +Búsqueda de IPs en segmento 172.16.13.x - Disponibles: 153, En uso: 101 +Resultado de búsqueda: {"success":true,"ipsDisponibles":153,"ipsEnUso":101} +<<< Finalizando handler de búsqueda AJAX +=== NUEVA PETICIÓN === +Método: POST +POST data: {"action":"search","segment":"13","verify_ping":"true"} +GET data: [] +Content-Type: multipart/form-data; boundary=----geckoformboundaryb216276793b3e68fe84b13e1d74f9f09 +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +>>> Entrando al handler de búsqueda AJAX +Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true} +Buscando IPs en segmento: 13 (con verificación por ping) +URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Respuesta HTTP: 200 +Longitud de respuesta: 48906 bytes +IPs obtenidas exitosamente: 3105 direcciones +Verificación por ping habilitada, iniciando verificación... +Iniciando verificación por ping de 153 IPs (lotes de 30) +Procesando lote 1/6 (30 IPs) +Lote completado: 1/30 IPs responden (29.21s) +Procesando lote 2/6 (30 IPs) +Lote completado: 1/30 IPs responden (29.23s) +Procesando lote 3/6 (30 IPs) +Lote completado: 0/30 IPs responden (30.20s) +Procesando lote 4/6 (30 IPs) +Lote completado: 1/30 IPs responden (29.20s) +Procesando lote 5/6 (30 IPs) +Lote completado: 0/30 IPs responden (30.20s) +Procesando lote 6/6 (3 IPs) +Lote completado: 0/3 IPs responden (3.11s) +ADVERTENCIA: 3 IPs responden a ping pero no están en UISP: 172.16.13.45, 172.16.13.52, 172.16.13.188 +Verificación por ping completada: 153 IPs verificadas, 3 responden, 150 no responden (151.15s) +Búsqueda de IPs en segmento 172.16.13.x - Disponibles: 150, En uso: 101 (verificado con ping) +Resultado de búsqueda: {"success":true,"ipsDisponibles":150,"ipsEnUso":101} +<<< Finalizando handler de búsqueda AJAX +=== NUEVA PETICIÓN === +Método: GET +POST data: [] +GET data: [] +Content-Type: +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +Acceso a la interfaz pública de búsqueda de IPs +=== NUEVA PETICIÓN === +Método: POST +POST data: {"action":"search","segment":"100","verify_ping":"true","ping_limit":"20"} +GET data: [] +Content-Type: multipart/form-data; boundary=----geckoformboundary2463648a22fbfc0977c6f46af73a71c2 +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +>>> Entrando al handler de búsqueda AJAX +Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true} +Buscando IPs en segmento: 100 (con verificación por ping, límite: 20 IPs) +URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Respuesta HTTP: 200 +Longitud de respuesta: 48906 bytes +IPs obtenidas exitosamente: 3105 direcciones +Verificación por ping habilitada, iniciando verificación... +Aplicando límite de ping: 20 IPs de cliente (Total disponibles: 105) +Iniciando verificación por ping de 20 IPs (lotes de 30) +Procesando lote 1/1 (20 IPs) +Lote completado: 0/20 IPs responden (20.18s) +Verificación por ping completada: 20 IPs verificadas, 0 responden, 20 no responden (20.18s) +Búsqueda de IPs en segmento 172.16.100.x - Disponibles: 123, En uso: 131 (verificado con ping) +Resultado de búsqueda: {"success":true,"ipsDisponibles":123,"ipsEnUso":131} +<<< Finalizando handler de búsqueda AJAX +=== NUEVA PETICIÓN === +Método: POST +POST data: {"action":"search","segment":"100"} +GET data: [] +Content-Type: multipart/form-data; boundary=----geckoformboundary5753dc1b36d69572108222d59aa55e38 +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +>>> Entrando al handler de búsqueda AJAX +Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true} +Buscando IPs en segmento: 100 (sin verificación por ping) +URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Respuesta HTTP: 200 +Longitud de respuesta: 48906 bytes +IPs obtenidas exitosamente: 3105 direcciones +Búsqueda de IPs en segmento 172.16.100.x - Disponibles: 123, En uso: 131 +Resultado de búsqueda: {"success":true,"ipsDisponibles":123,"ipsEnUso":131} +<<< Finalizando handler de búsqueda AJAX +=== NUEVA PETICIÓN === +Método: POST +POST data: {"action":"search","segment":"100","verify_ping":"true","ping_limit":"5"} +GET data: [] +Content-Type: multipart/form-data; boundary=----geckoformboundary3aee2ff80e73e922f743d236ac2c22c9 +User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0 +>>> Entrando al handler de búsqueda AJAX +Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true} +Buscando IPs en segmento: 100 (con verificación por ping, límite: 5 IPs) +URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true +Respuesta HTTP: 200 +Longitud de respuesta: 48906 bytes +IPs obtenidas exitosamente: 3105 direcciones +Verificación por ping habilitada, iniciando verificación... +Aplicando límite de ping: 5 IPs de cliente (Total disponibles: 105) +Iniciando verificación por ping de 5 IPs (lotes de 30) +Procesando lote 1/1 (5 IPs) +Lote completado: 0/5 IPs responden (5.14s) +Verificación por ping completada: 5 IPs verificadas, 0 responden, 5 no responden (5.15s) +Búsqueda de IPs en segmento 172.16.100.x - Disponibles: 123, En uso: 131 (verificado con ping) +Resultado de búsqueda: {"success":true,"ipsDisponibles":123,"ipsEnUso":131} +<<< Finalizando handler de búsqueda AJAX diff --git a/manifest.json b/manifest.json index 19d4e4b..d74338f 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "displayName": "SIIP - Buscador de IP's Disponibles UISP", "description": "Este plugin permite buscar IP's disponibles en UISP (UNMS) y asignarlas a los clientes en UCRM. Evitando así la asignación de IP's duplicadas y mejorando la gestión de direcciones IP en la red.", "url": "https://siip.mx", - "version": "1.2.0", + "version": "1.3.0", "ucrmVersionCompliancy": { "min": "1.0.0", "max": null diff --git a/public.php b/public.php index 42bd8dc..4135487 100644 --- a/public.php +++ b/public.php @@ -4,6 +4,9 @@ error_reporting(E_ALL); ini_set('display_errors', 0); // No mostrar errores en HTML +// Aumentar tiempo de ejecución para verificación por ping (puede tardar varios minutos) +set_time_limit(300); // 5 minutos + // Handler global de errores set_error_handler(function($errno, $errstr, $errfile, $errline) { throw new ErrorException($errstr, 0, $errno, $errfile, $errline); @@ -114,13 +117,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST[' } $segmento = $_POST['segment'] ?? ''; - $verifyPing = isset($_POST['verify_ping']) && $_POST['verify_ping'] === 'true'; + // NOTA: Ignoramos verify_ping aquí porque ahora se hace progresivamente desde el frontend + // Pero lo logueamos para saber la intención del usuario + $verifyPingIntention = isset($_POST['verify_ping']) && $_POST['verify_ping'] === 'true'; - if ($verifyPing) { - $log->appendLog("Buscando IPs en segmento: $segmento (con verificación por ping)"); - } else { - $log->appendLog("Buscando IPs en segmento: $segmento (sin verificación por ping)"); - } + $log->appendLog("Buscando IPs en segmento: $segmento (Búsqueda inicial rápida)"); + + // URL de la API de UISP - Usar HTTPS // URL de la API de UISP - Usar HTTPS $apiUrl = "https://{$config['ipserver']}/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true"; @@ -128,9 +131,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST[' $log->appendLog("URL de API: $apiUrl"); - // Crear instancia del servicio y buscar IPs + // Crear instancia del servicio y buscar IPs (SIN ping, el ping se hace después) $ipService = new IpSearchService($apiUrl, $apiToken, $log); - $resultado = $ipService->buscarIpsDisponibles($segmento, $verifyPing); + $resultado = $ipService->buscarIpsDisponibles($segmento, false); $log->appendLog('Resultado de búsqueda: ' . json_encode([ 'success' => $resultado['success'], @@ -163,6 +166,51 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST[' $log->appendLog('<<< Finalizando handler de búsqueda AJAX'); exit; +} else if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'verify_batch') { + $log->appendLog('>>> Entrando al handler de verificación por lotes (verify_batch)'); + + try { + $ips = $_POST['ips'] ?? []; + if (!is_array($ips)) { + $ips = json_decode($ips, true); + } + + if (empty($ips)) { + echo json_encode(['success' => true, 'results' => []]); + exit; + } + + $log->appendLog('Verificando lote de ' . count($ips) . ' IPs'); + + // Crear instancia de PingService + $pingService = new PingService($log, 1, count($ips)); + + if (!$pingService->isAvailable()) { + echo json_encode([ + 'success' => false, + 'message' => 'Comando ping no disponible' + ]); + exit; + } + + // Hacer ping + $pingResults = $pingService->pingMultipleIps($ips); + $processed = $pingService->processPingResults($pingResults); + + // Retornar resultados + // responding = IPs que responden (ocupadas/conflicto) + // not_responding = IPs que no responden (disponibles) + echo json_encode([ + 'success' => true, + 'responding' => $processed['responding'], + 'not_responding' => $processed['not_responding'] + ]); + + } catch (Exception $e) { + $log->appendLog('ERROR en verify_batch: ' . $e->getMessage()); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); + } + exit; } else if ($_SERVER['REQUEST_METHOD'] === 'POST') { $log->appendLog('Petición POST recibida pero sin action=search o action no válida'); $log->appendLog('Action recibida: ' . ($_POST['action'] ?? 'NO DEFINIDA')); @@ -174,7 +222,7 @@ $log->appendLog('Acceso a la interfaz pública de búsqueda de IPs'); // Cargar configuración para verificar si ping está habilitado $configManager = PluginConfigManager::create(); $config = $configManager->loadConfig(); -$pingEnabled = isset($config['enablePingVerification']) && $config['enablePingVerification'] === '1'; +$pingEnabled = !empty($config['enablePingVerification']) && ($config['enablePingVerification'] === true || $config['enablePingVerification'] === '1'); ?> @@ -602,6 +650,43 @@ $pingEnabled = isset($config['enablePingVerification']) && $config['enablePingVe grid-template-columns: 1fr; } } + + .status-badge { + padding: 6px 12px; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 6px; + } + + .status-pending { + background: rgba(255, 255, 255, 0.1); + color: var(--text-secondary); + } + + .status-verifying { + background: rgba(102, 126, 234, 0.2); + color: #667eea; + animation: pulse 1.5s infinite; + } + + .status-available { + background: rgba(79, 209, 197, 0.2); + color: #4fd1c5; + border: 1px solid rgba(79, 209, 197, 0.4); + } + + .status-conflict { + background: rgba(245, 87, 108, 0.2); + color: #ff6b6b; + border: 1px solid rgba(245, 87, 108, 0.4); + } + + tr.hidden-row { + display: none; + } @@ -632,10 +717,25 @@ $pingEnabled = isset($config['enablePingVerification']) && $config['enablePingVe
+
+ +
+
+ + +