siip-available-ips/public.php
DANYDHSV 6a0e15447c fix(v1.5.0): Correcciones críticas en configuración avanzada de IPs
- Corregida inicialización de editorData que impedía cargar rangos personalizados
- Corregidos errores de escape JS que rompían botones de la interfaz
- Corregida clasificación frontend que ignoraba rangos específicos por segmento
- Agregado indicador visual de modo y logs de depuración
- Eliminada columna redundante en tabla de resultados

Resuelve problemas donde los rangos admin personalizados eran ignorados.
2025-11-27 14:40:58 -06:00

2066 lines
74 KiB
PHP

<?php
// Configurar manejo de errores para devolver JSON
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);
});
// Handler global de excepciones no capturadas
set_exception_handler(function($exception) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Error fatal: ' . $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine()
]);
exit;
});
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/IpSearchService.php';
require_once __DIR__ . '/src/PingService.php';
require_once __DIR__ . '/src/ApiHandlers.php';
require_once __DIR__ . '/src/AdminRangeHelper.php';
use Ubnt\UcrmPluginSdk\Service\PluginLogManager;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use SiipAvailableIps\IpSearchService;
use SiipAvailableIps\PingService;
// Manejo de errores para el handler de configuración avanzada
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Handler para guardar configuración avanzada
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_advanced_config'])) {
try {
$jsonData = $_POST['save_advanced_config'];
// Validar JSON
$validation = \SiipAvailableIps\AdminRangeHelper::validateConfigJson($jsonData);
if (!$validation['valid']) {
die('Error: ' . $validation['error']);
}
// Guardar en config.json directamente (el SDK no tiene método save)
$configFile = __DIR__ . '/data/config.json';
if (!file_exists($configFile)) {
// Si no existe, intentar crearlo con estructura básica
$config = [];
} else {
$config = json_decode(file_get_contents($configFile), true);
if (!is_array($config)) {
$config = [];
}
}
$config['customAdminRangesJson'] = $jsonData;
if (file_put_contents($configFile, json_encode($config, JSON_PRETTY_PRINT)) === false) {
throw new Exception("No se pudo escribir en el archivo de configuración: $configFile");
}
// Redirigir con mensaje de éxito
header('Location: ?success=1');
exit;
} catch (Exception $e) {
die('Error al guardar configuración: ' . $e->getMessage());
}
}
// Get UCRM log manager
$log = PluginLogManager::create();
// Log TODAS las peticiones
$log->appendLog('=== NUEVA PETICIÓN ===' );
$log->appendLog('Método: ' . $_SERVER['REQUEST_METHOD']);
$log->appendLog('POST data: ' . json_encode($_POST));
$log->appendLog('GET data: ' . json_encode($_GET));
$log->appendLog('Content-Type: ' . ($_SERVER['CONTENT_TYPE'] ?? 'no definido'));
$log->appendLog('User Agent: ' . ($_SERVER['HTTP_USER_AGENT'] ?? 'no definido'));
// ============================================================================
// API REST - Manejar peticiones JSON (Postman, Webhooks, etc.)
// ============================================================================
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$isJsonRequest = stripos($contentType, 'application/json') !== false;
if ($isJsonRequest && $_SERVER['REQUEST_METHOD'] === 'POST') {
$log->appendLog('>>> Petición API JSON detectada');
$rawInput = file_get_contents('php://input');
$log->appendLog('Raw input length: ' . strlen($rawInput) . ' bytes');
$jsonData = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$log->appendLog('ERROR: JSON inválido - ' . json_last_error_msg());
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => 'Invalid JSON',
'message' => json_last_error_msg()
]);
exit;
}
$log->appendLog('JSON parseado correctamente: ' . json_encode($jsonData));
// Procesar petición API
handleApiRequest($jsonData, $log);
exit;
}
// ============================================================================
// FRONTEND HTML - Manejar peticiones AJAX del formulario
// ============================================================================
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'search') {
$log->appendLog('>>> Entrando al handler de búsqueda AJAX');
try {
// Obtener configuración del plugin desde data/config.json
$configManager = PluginConfigManager::create();
$config = $configManager->loadConfig();
$log->appendLog('Configuración cargada: ' . json_encode([
'ipserver' => $config['ipserver'] ?? 'NO CONFIGURADO',
'hasUnmsToken' => !empty($config['unmsApiToken']),
'hasApiToken' => !empty($config['apitoken'])
]));
// Validar que exista la configuración necesaria
if (empty($config['ipserver'])) {
$log->appendLog('ERROR: No se ha configurado ipserver');
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'El plugin no está configurado correctamente. Falta la dirección del servidor.'
]);
exit;
}
if (empty($config['unmsApiToken'])) {
$log->appendLog('ERROR: No se ha configurado unmsApiToken');
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'El plugin no está configurado correctamente. Falta el token de API de UNMS.'
]);
exit;
}
$segmento = $_POST['segment'] ?? '';
// 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';
$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";
$apiToken = $config['unmsApiToken'];
$log->appendLog("URL de API: $apiUrl");
// 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, false);
$log->appendLog('Resultado de búsqueda: ' . json_encode([
'success' => $resultado['success'],
'ipsDisponibles' => count($resultado['data'] ?? []),
'ipsEnUso' => count($resultado['used'] ?? [])
]));
header('Content-Type: application/json');
echo json_encode($resultado);
} catch (Exception $e) {
$log->appendLog('EXCEPCIÓN en búsqueda de IPs: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Error al procesar la solicitud: ' . $e->getMessage(),
'error_file' => $e->getFile(),
'error_line' => $e->getLine()
]);
} catch (Error $e) {
$log->appendLog('ERROR FATAL en búsqueda de IPs: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Error fatal: ' . $e->getMessage(),
'error_file' => $e->getFile(),
'error_line' => $e->getLine()
]);
}
$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 (Throwable $e) {
$log->appendLog('ERROR en verify_batch: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
} 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'));
}
// Log de acceso público
$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 = !empty($config['enablePingVerification']) && ($config['enablePingVerification'] === true || $config['enablePingVerification'] === '1');
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Buscador de IPs Disponibles - SIIP</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Tema Oscuro (por defecto) */
[data-theme="dark"] {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--dark-bg: #0f0f23;
--card-bg: rgba(255, 255, 255, 0.05);
--card-border: rgba(255, 255, 255, 0.1);
--text-primary: #ffffff;
--text-secondary: #a0aec0;
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.3);
--shadow-xl: 0 25px 80px rgba(0, 0, 0, 0.4);
}
/* Tema Claro */
[data-theme="light"] {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--dark-bg: #f7fafc;
--card-bg: rgba(255, 255, 255, 0.95);
--card-border: rgba(0, 0, 0, 0.1);
--text-primary: #1a202c;
--text-secondary: #4a5568;
--shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.08);
--shadow-xl: 0 15px 50px rgba(0, 0, 0, 0.12);
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--dark-bg);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background-image:
radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(245, 87, 108, 0.15) 0%, transparent 50%);
background-attachment: fixed;
transition: background 0.3s ease, color 0.3s ease;
}
.container {
width: 100%;
max-width: 1200px;
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
text-align: center;
margin-bottom: 40px;
}
.logo {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: var(--primary-gradient);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
box-shadow: var(--shadow-lg);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
h1 {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #f5576c 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
font-weight: 400;
}
.card {
background: var(--card-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--card-border);
border-radius: 24px;
padding: 40px;
box-shadow: var(--shadow-xl);
margin-bottom: 30px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.5);
}
.search-form {
display: flex;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.input-group {
flex: 1;
min-width: 250px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 0.9rem;
color: var(--text-secondary);
}
.input-wrapper {
position: relative;
}
.input-prefix {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
font-weight: 500;
pointer-events: none;
}
input[type="text"] {
width: 100%;
padding: 16px 20px 16px 90px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid var(--card-border);
border-radius: 12px;
color: var(--text-primary);
font-size: 1rem;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}
.btn {
padding: 16px 32px;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Inter', sans-serif;
display: inline-flex;
align-items: center;
gap: 10px;
align-self: flex-end;
}
.btn-primary {
background: var(--primary-gradient);
color: white;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 15px 40px rgba(102, 126, 234, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-cancel {
background: rgba(245, 87, 108, 0.2);
border: 1px solid rgba(245, 87, 108, 0.4);
color: #f5576c;
margin-top: 15px;
}
.btn-cancel:hover {
background: rgba(245, 87, 108, 0.3);
border-color: rgba(245, 87, 108, 0.6);
transform: translateY(-2px);
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.loading.active {
display: block;
}
.spinner {
width: 50px;
height: 50px;
margin: 0 auto 15px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.results {
display: none;
}
.results.active {
display: block;
animation: fadeInUp 0.5s ease-out;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 20px;
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
background: var(--success-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 5px;
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9rem;
}
.table-container {
overflow-x: auto;
border-radius: 16px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--card-border);
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: rgba(255, 255, 255, 0.05);
}
th {
padding: 16px;
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 16px;
border-top: 1px solid var(--card-border);
}
tbody tr {
transition: background 0.2s ease;
}
tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
.ip-address {
font-family: 'Courier New', monospace;
font-weight: 900;
font-size: 1.7rem;
color: #2095fbff;
}
.ip-cell-mobile {
display: flex;
align-items: center;
gap: 12px;
}
.btn-copy {
padding: 8px 16px;
background: rgba(102, 126, 234, 0.2);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
color: #667eea;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-copy:hover {
background: rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.5);
transform: scale(1.05);
}
.btn-copy.copied {
background: rgba(79, 172, 254, 0.2);
border-color: rgba(79, 172, 254, 0.3);
color: #4facfe;
}
.alert {
padding: 16px 20px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
animation: fadeInUp 0.4s ease-out;
}
.alert-error {
background: rgba(245, 87, 108, 0.1);
border: 1px solid rgba(245, 87, 108, 0.3);
color: #f5576c;
}
.alert-success {
background: rgba(79, 172, 254, 0.1);
border: 1px solid rgba(79, 172, 254, 0.3);
color: #4facfe;
}
.ip-type-badge {
padding: 6px 12px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
display: inline-block;
text-align: center;
}
.ip-type-admin {
background: rgba(245, 87, 108, 0.2);
border: 1px solid rgba(245, 87, 108, 0.4);
color: #ff6b6b;
}
.ip-type-client {
background: rgba(79, 209, 197, 0.2);
border: 1px solid rgba(79, 209, 197, 0.4);
color: #4fd1c5;
}
.footer {
text-align: center;
color: var(--text-secondary);
font-size: 0.9rem;
margin-top: 40px;
}
.footer a {
color: #667eea;
text-decoration: none;
transition: color 0.2s ease;
}
.footer a:hover {
color: #f5576c;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.card {
padding: 25px;
}
.search-form {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
.stats {
grid-template-columns: 1fr;
}
/* Optimizaciones para tabla en móvil */
th, td {
padding: 10px 6px;
font-size: 0.8rem;
}
/* Ocultar columnas # en móvil */
th:first-child,
td:first-child {
display: none;
}
/* Contenedor de IP con badge debajo */
.ip-cell-mobile {
flex-direction: column;
gap: 6px;
align-items: flex-start;
}
.ip-address {
font-size: 0.85rem;
}
.ip-type-badge {
font-size: 0.65rem;
padding: 3px 8px;
align-self: flex-start;
}
.status-badge {
font-size: 0.7rem;
padding: 4px 8px;
white-space: normal;
line-height: 1.3;
}
.btn-copy {
padding: 8px 12px;
font-size: 0.75rem;
}
/* Hacer la tabla más compacta */
.table-container {
font-size: 0.85rem;
}
/* Ajustar anchos de columnas en móvil (3 columnas) */
th:nth-child(2), td:nth-child(2) { /* Dirección IP + Tipo */
width: 40%;
}
th:nth-child(4), td:nth-child(4) { /* Estado */
width: 35%;
}
th:nth-child(5), td:nth-child(5) { /* Acción */
width: 25%;
}
}
.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);
}
.status-unknown {
background: rgba(160, 174, 192, 0.2);
color: #a0aec0;
border: 1px solid rgba(160, 174, 192, 0.4);
}
tr.hidden-row {
display: none;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
margin-bottom: 0;
padding-top: 28px;
}
@media (max-width: 768px) {
.checkbox-label {
padding-top: 0;
margin-top: 10px;
}
.input-group {
width: 100%;
}
}
/* Botón de cambio de tema */
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--card-bg);
border: 2px solid var(--card-border);
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: var(--shadow-lg);
z-index: 1000;
}
.theme-toggle:hover {
transform: scale(1.1) rotate(15deg);
box-shadow: var(--shadow-xl);
}
@media (max-width: 768px) {
.theme-toggle {
width: 45px;
height: 45px;
font-size: 1.3rem;
top: 15px;
right: 15px;
}
}
/* Botón de configuración avanzada */
.config-toggle {
position: fixed;
top: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--card-bg);
border: 2px solid var(--card-border);
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: var(--shadow-lg);
z-index: 1001;
}
.config-toggle:hover {
transform: scale(1.1) rotate(90deg);
box-shadow: var(--shadow-xl);
}
/* Ajustar posición del botón de tema */
.theme-toggle {
top: 80px; /* Debajo del botón de configuración */
}
/* Modal de contraseña */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 2000;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 16px;
padding: 40px;
max-width: 400px;
width: 90%;
box-shadow: var(--shadow-xl);
text-align: center;
}
.modal-content h3 {
margin-bottom: 10px;
color: var(--text-primary);
}
.modal-content p {
color: var(--text-secondary);
margin-bottom: 20px;
}
.modal-input {
width: 100%;
padding: 12px;
border: 2px solid var(--card-border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
font-size: 1rem;
margin-bottom: 20px;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: center;
}
/* Editor Visual */
.advanced-editor {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--dark-bg);
z-index: 1500;
overflow-y: auto;
padding: 20px;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
}
.editor-header h2 {
margin: 0;
color: var(--text-primary);
}
.editor-content {
max-width: 1200px;
margin: 0 auto;
}
.add-segment-form {
padding: 20px 0;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.range-input-group {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.range-input-group input {
width: 100px;
padding: 8px;
border: 2px solid var(--card-border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
}
.range-input-group span {
color: var(--text-secondary);
}
.btn-small {
padding: 6px 12px;
font-size: 0.85rem;
background: rgba(102, 126, 234, 0.2);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
color: #667eea;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-small:hover {
background: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
.editor-actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
@media (max-width: 768px) {
.config-toggle {
width: 45px;
height: 45px;
font-size: 1.3rem;
top: 15px;
right: 15px;
}
.theme-toggle {
top: 70px;
}
.modal-content {
padding: 30px 20px;
}
.editor-header {
flex-direction: column;
gap: 15px;
}
.form-row {
flex-direction: column;
}
.range-input-group {
flex-wrap: wrap;
}
}
</style>
</head>
<body data-theme="dark">
<!-- Botón de configuración avanzada -->
<button id="advancedConfigBtn" class="config-toggle" title="Configuración avanzada">
<span>⚙️</span>
</button>
<!-- Botón de cambio de tema -->
<button id="themeToggle" class="theme-toggle" title="Cambiar tema">
<span id="themeIcon">🌙</span>
</button>
<!-- Modal de contraseña -->
<div id="passwordModal" class="modal" style="display: none;">
<div class="modal-content">
<h3>🔒 Acceso Restringido</h3>
<p>Ingresa la contraseña de administrador para acceder a la configuración avanzada</p>
<input type="password" id="adminPasswordInput" class="modal-input" placeholder="Contraseña" />
<div class="modal-actions">
<button class="btn btn-primary" onclick="validatePassword()">Acceder</button>
<button class="btn" onclick="closePasswordModal()">Cancelar</button>
</div>
</div>
</div>
<!-- Editor Visual de Configuración Avanzada -->
<div id="advancedEditor" class="advanced-editor" style="display: none;">
<div class="editor-header">
<h2>⚙️ Editor de Rangos Administrativos por Segmento</h2>
<button class="btn" onclick="returnToMain()">← Volver</button>
</div>
<div class="editor-content">
<!-- Formulario para agregar segmento -->
<div class="card">
<h3>Agregar Nuevo Segmento</h3>
<div class="add-segment-form">
<div class="form-row">
<div class="input-group">
<label>Segmento</label>
<input type="number" id="newSegment" placeholder="ej: 18" min="0" max="255" />
</div>
</div>
<h4>Rangos Iniciales (IPs al inicio del segmento)</h4>
<div id="initialRangesContainer">
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="addInitialRange()">+ Agregar Rango</button>
</div>
</div>
<h4>Rangos Finales (IPs al final del segmento)</h4>
<div id="finalRangesContainer">
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" value="254" />
<button class="btn-small" onclick="addFinalRange()">+ Agregar Rango</button>
</div>
</div>
<button class="btn btn-primary" onclick="addSegmentToTable()">✅ Agregar Segmento</button>
</div>
</div>
<!-- Tabla de segmentos configurados -->
<div class="card">
<h3>Segmentos Configurados</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Segmento</th>
<th>Rangos Iniciales</th>
<th>Rangos Finales</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="segmentsTableBody">
<!-- Filas dinámicas -->
</tbody>
</table>
</div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="saveConfiguration()">💾 Guardar Configuración</button>
<button class="btn" onclick="returnToMain()">Cancelar</button>
</div>
</div>
</div>
<div class="container">
<div class="header">
<div class="logo">🌐</div>
<h1>Buscador de IPs Disponibles</h1>
<p class="subtitle">Sistema de gestión de direcciones IP para UISP</p>
<?php if (isset($_GET['success']) && $_GET['success'] == '1'): ?>
<div style="background: rgba(79, 209, 197, 0.2); border: 1px solid rgba(79, 209, 197, 0.4); color: #4fd1c5; padding: 15px; border-radius: 12px; margin-top: 20px; text-align: center;">
✅ Configuración avanzada guardada exitosamente
</div>
<?php endif; ?>
</div>
<div class="card">
<form class="search-form" id="searchForm">
<div class="input-group">
<label for="segment">Segmento de Red</label>
<div class="input-wrapper">
<span class="input-prefix">172.16.</span>
<input
type="text"
id="segment"
name="segment"
placeholder="0-255"
required
pattern="[0-9]{1,3}"
maxlength="3"
>
</div>
</div>
<?php if ($pingEnabled): ?>
<div class="input-group" style="flex: 0 0 auto; min-width: auto;">
<label class="checkbox-label">
<input type="checkbox" id="verifyPing" name="verify_ping" style="width: auto; padding: 0; margin: 0;" checked>
<span style="color: var(--text-secondary); font-size: 0.9rem;">🔍 Verificar con ping</span>
</label>
</div>
<div class="input-group" style="flex: 0 0 auto; min-width: auto;">
<label class="checkbox-label">
<input type="checkbox" id="hideAdmin" name="hide_admin" style="width: auto; padding: 0; margin: 0;">
<span style="color: var(--text-secondary); font-size: 0.9rem;">👁️ Ocultar IP's de Administración</span>
</label>
</div>
<div class="input-group" id="pingLimitContainer" style="flex: 0 0 auto; min-width: 150px;">
<label for="pingLimit">Límite de IPs</label>
<select id="pingLimit" name="ping_limit" style="width: 100%; padding: 16px; background: rgba(255, 255, 255, 0.05); border: 2px solid var(--card-border); border-radius: 12px; color: var(--text-primary); font-size: 1rem;">
<option value="0">Todas (Lento)</option>
<option value="5" selected>5 IPs (Rápido)</option>
<option value="10">10 IPs</option>
<option value="20">20 IPs</option>
</select>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary" id="searchBtn">
<span>🔍</span>
<span>Buscar IPs</span>
</button>
</form>
<button id="cancelBtn" class="btn btn-cancel" style="display: none;">
🛑 Cancelar Verificación
</button>
<div class="loading" id="loading">
<div class="spinner"></div>
<p id="loadingMessage">Consultando IPs disponibles...</p>
<p id="loadingPingWarning" style="display: none; color: var(--text-secondary); font-size: 0.9rem; margin-top: 10px;">
⏱️ Verificación por ping habilitada. Esto puede tardar varios minutos...
</p>
</div>
<div id="errorContainer"></div>
<div class="results" id="results">
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="availableCount">0</div>
<div class="stat-label">IPs Disponibles</div>
</div>
<div class="stat-card">
<div class="stat-value" id="usedCount">0</div>
<div class="stat-label">IPs en Uso</div>
</div>
<div class="stat-card">
<div class="stat-value" id="segmentDisplay">-</div>
<div class="stat-label">Segmento</div>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>#</th>
<th>Dirección IP</th>
<th>Estado</th>
<th>Acción</th>
</tr>
</thead>
<tbody id="ipTableBody">
</tbody>
</table>
</div>
</div>
</div>
<div class="footer">
<p>Desarrollado por <a href="https://siip.mx" target="_blank">SIIP Internet</a></p>
</div>
</div>
<script>
// Configuración de rangos de IPs administrativas
const adminConfig = {
rangeStart: <?php echo isset($config['adminRangeStart']) ? (int)$config['adminRangeStart'] : 1; ?>,
rangeEnd: <?php echo isset($config['adminRangeEnd']) ? (int)$config['adminRangeEnd'] : 30; ?>,
finalStart: <?php echo isset($config['adminRangeFinalStart']) ? (int)$config['adminRangeFinalStart'] : 254; ?>,
finalEnd: <?php echo isset($config['adminRangeFinalEnd']) ? (int)$config['adminRangeFinalEnd'] : 254; ?>
};
console.log('PHP Config Debug:', <?php echo json_encode($config); ?>);
const useCustomRanges = <?php echo !empty($config['useCustomAdminRanges']) ? 'true' : 'false'; ?>;
const searchForm = document.getElementById('searchForm');
// Mostrar indicador de modo
const header = document.querySelector('.header');
const modeBadge = document.createElement('div');
modeBadge.className = useCustomRanges ? 'mode-badge custom' : 'mode-badge global';
modeBadge.innerHTML = useCustomRanges ? '✨ Modo: Rangos Personalizados' : '🌐 Modo: Rangos Globales';
modeBadge.style.cssText = `
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
margin-top: 10px;
background: ${useCustomRanges ? 'rgba(102, 126, 234, 0.2)' : 'rgba(255, 255, 255, 0.1)'};
color: ${useCustomRanges ? '#667eea' : '#a0aec0'};
border: 1px solid ${useCustomRanges ? 'rgba(102, 126, 234, 0.3)' : 'rgba(255, 255, 255, 0.2)'};
`;
header.appendChild(modeBadge);
const searchBtn = document.getElementById('searchBtn');
const loading = document.getElementById('loading');
const results = document.getElementById('results');
const errorContainer = document.getElementById('errorContainer');
const ipTableBody = document.getElementById('ipTableBody');
const availableCount = document.getElementById('availableCount');
const usedCount = document.getElementById('usedCount');
const segmentDisplay = document.getElementById('segmentDisplay');
const cancelBtn = document.getElementById('cancelBtn');
let verificationCancelled = false;
// ========== ADVANCED CONFIGURATION EDITOR ==========
// Variables del editor
// Inicializar editorData con los rangos personalizados
let editorData;
const customRangesJson = <?php echo json_encode($config['customAdminRangesJson'] ?? '{"segmentos":[]}'); ?>;
try {
editorData = JSON.parse(customRangesJson);
if (!editorData.segmentos) {
editorData = { segmentos: [] };
}
console.log('editorData cargado al inicio:', editorData);
} catch (e) {
console.error('Error loading initial editorData:', e);
editorData = { segmentos: [] };
}
let hasUnsavedChanges = false;
const adminPassword = <?php echo json_encode($config['adminPassword'] ?? ''); ?>;
// Event listener para botón de configuración avanzada
document.getElementById('advancedConfigBtn').addEventListener('click', openAdvancedConfig);
function openAdvancedConfig() {
document.getElementById('passwordModal').style.display = 'flex';
document.getElementById('adminPasswordInput').value = '';
document.getElementById('adminPasswordInput').focus();
}
function closePasswordModal() {
document.getElementById('passwordModal').style.display = 'none';
}
function validatePassword() {
const inputPassword = document.getElementById('adminPasswordInput').value;
if (inputPassword === adminPassword) {
closePasswordModal();
showEditor();
} else {
alert('❌ Contraseña incorrecta');
document.getElementById('adminPasswordInput').value = '';
document.getElementById('adminPasswordInput').focus();
}
}
// Permitir Enter en el campo de contraseña
document.getElementById('adminPasswordInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
validatePassword();
}
});
function showEditor() {
document.querySelector('.container').style.display = 'none';
document.getElementById('advancedEditor').style.display = 'block';
loadEditorData();
hasUnsavedChanges = false;
}
function returnToMain() {
if (hasUnsavedChanges) {
if (!confirm('⚠️ Tienes cambios sin guardar. ¿Deseas salir de todos modos?')) {
return;
}
}
document.getElementById('advancedEditor').style.display = 'none';
document.querySelector('.container').style.display = 'block';
hasUnsavedChanges = false;
}
function loadEditorData() {
try {
editorData = JSON.parse(customRangesJson);
if (!editorData.segmentos) {
editorData = { segmentos: [] };
}
} catch (e) {
console.error('Error parsing JSON:', e);
editorData = { segmentos: [] };
}
renderSegmentsTable();
}
function renderSegmentsTable() {
const tbody = document.getElementById('segmentsTableBody');
tbody.innerHTML = '';
if (editorData.segmentos.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: var(--text-secondary);">No hay segmentos configurados</td></tr>';
return;
}
editorData.segmentos.forEach((seg, index) => {
const row = document.createElement('tr');
// Formatear rangos iniciales
const iniciales = seg.administrativas_iniciales.map(r => `${r.inicio}-${r.hasta}`).join(', ');
// Formatear rangos finales
const finales = seg.administrativas_finales.map(r => `${r.inicio}-${r.hasta}`).join(', ');
row.innerHTML = `
<td><strong>172.16.${seg.segmento}.x</strong></td>
<td>${iniciales || '-'}</td>
<td>${finales || '-'}</td>
<td>
<button class="btn-small" onclick="deleteSegment(${index})" style="background: rgba(245, 87, 108, 0.2); color: #ff6b6b; border-color: rgba(245, 87, 108, 0.3);">
🗑️ Eliminar
</button>
</td>
`;
tbody.appendChild(row);
});
}
function addInitialRange() {
const container = document.getElementById('initialRangesContainer');
const newRange = document.createElement('div');
newRange.className = 'range-input-group';
newRange.innerHTML = `
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="this.parentElement.remove()" style="background: rgba(245, 87, 108, 0.2); color: #ff6b6b;">
✖ Quitar
</button>
`;
container.appendChild(newRange);
}
function addFinalRange() {
const container = document.getElementById('finalRangesContainer');
const newRange = document.createElement('div');
newRange.className = 'range-input-group';
newRange.innerHTML = `
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" value="255" />
<button class="btn-small" onclick="this.parentElement.remove()" style="background: rgba(245, 87, 108, 0.2); color: #ff6b6b;">
✖ Quitar
</button>
`;
container.appendChild(newRange);
}
function addSegmentToTable() {
const segmentInput = document.getElementById('newSegment');
const segment = segmentInput.value;
if (!segment || segment < 0 || segment > 255) {
alert('❌ Por favor ingresa un segmento válido (0-255)');
return;
}
// Verificar si el segmento ya existe
if (editorData.segmentos.some(s => s.segmento == segment)) {
alert('⚠️ Este segmento ya está configurado');
return;
}
// Obtener rangos iniciales
const initialRanges = [];
const initialContainer = document.getElementById('initialRangesContainer');
const initialGroups = initialContainer.querySelectorAll('.range-input-group');
initialGroups.forEach(group => {
const start = parseInt(group.querySelector('.range-start').value);
const end = parseInt(group.querySelector('.range-end').value);
if (start && end && start <= end) {
initialRanges.push({ inicio: start, hasta: end });
}
});
// Obtener rangos finales
const finalRanges = [];
const finalContainer = document.getElementById('finalRangesContainer');
const finalGroups = finalContainer.querySelectorAll('.range-input-group');
finalGroups.forEach(group => {
const start = parseInt(group.querySelector('.range-start').value);
const end = parseInt(group.querySelector('.range-end').value);
if (start && end && start <= end) {
finalRanges.push({ inicio: start, hasta: end });
}
});
// Agregar segmento
editorData.segmentos.push({
segmento: segment,
administrativas_iniciales: initialRanges,
administrativas_finales: finalRanges
});
// Limpiar formulario
segmentInput.value = '';
initialContainer.innerHTML = `
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="addInitialRange()">+ Agregar Rango</button>
</div>
`;
finalContainer.innerHTML = `
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" value="255" />
<button class="btn-small" onclick="addFinalRange()">+ Agregar Rango</button>
</div>
`;
hasUnsavedChanges = true;
renderSegmentsTable();
alert('✅ Segmento agregado correctamente');
}
function deleteSegment(index) {
if (confirm('¿Estás seguro de eliminar este segmento?')) {
editorData.segmentos.splice(index, 1);
hasUnsavedChanges = true;
renderSegmentsTable();
}
}
function saveConfiguration() {
if (!confirm('¿Deseas guardar la configuración? Esto actualizará el archivo de configuración del plugin.')) {
return;
}
const jsonData = JSON.stringify(editorData);
// Crear formulario para enviar datos
const form = document.createElement('form');
form.method = 'POST';
form.action = '';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'save_advanced_config';
input.value = jsonData;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
// ========== FIN ADVANCED CONFIGURATION EDITOR ==========
// Theme toggle functionality
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const body = document.body;
// Load saved theme or default to dark
const savedTheme = localStorage.getItem('theme') || 'dark';
body.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
// Theme toggle event
themeToggle.addEventListener('click', () => {
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
body.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
function updateThemeIcon(theme) {
themeIcon.textContent = theme === 'dark' ? '🌙' : '☀️';
}
// Toggle visibility of ping limit select
<?php if ($pingEnabled): ?>
const verifyPingCheckbox = document.getElementById('verifyPing');
const pingLimitContainer = document.getElementById('pingLimitContainer');
if (verifyPingCheckbox && pingLimitContainer) {
verifyPingCheckbox.addEventListener('change', function() {
pingLimitContainer.style.display = this.checked ? 'block' : 'none';
});
}
<?php endif; ?>
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
const segment = document.getElementById('segment').value;
// Validar segmento
const segmentNum = parseInt(segment);
if (isNaN(segmentNum) || segmentNum < 0 || segmentNum > 255) {
showError('El segmento debe ser un número entre 0 y 255');
return;
}
// Mostrar loading
<?php if ($pingEnabled): ?>
const verifyPingCheckbox = document.getElementById('verifyPing');
const loadingPingWarning = document.getElementById('loadingPingWarning');
if (verifyPingCheckbox && verifyPingCheckbox.checked) {
loadingPingWarning.style.display = 'block';
}
<?php endif; ?>
loading.classList.add('active');
results.classList.remove('active');
errorContainer.innerHTML = '';
searchBtn.disabled = true;
try {
const formData = new FormData();
formData.append('action', 'search');
formData.append('segment', segment);
// Agregar verificación por ping si está habilitada y marcada
<?php if ($pingEnabled): ?>
const verifyPingCheckbox = document.getElementById('verifyPing');
const pingLimitSelect = document.getElementById('pingLimit');
const shouldVerifyPing = verifyPingCheckbox && verifyPingCheckbox.checked;
const pingLimit = pingLimitSelect ? parseInt(pingLimitSelect.value) : 0;
// NOTA: Ya no enviamos verify_ping=true al backend para la búsqueda inicial
// La verificación se hará progresivamente después de recibir la lista
// Solo lo enviamos como 'false' para que el backend sepa que no debe verificar
formData.append('verify_ping', 'false');
<?php endif; ?>
console.log('Enviando petición AJAX...');
console.log('Segmento:', segment);
const response = await fetch('', {
method: 'POST',
body: formData
});
console.log('Respuesta recibida:', response.status, response.statusText);
// Verificar si la respuesta es JSON
const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType);
if (!contentType || !contentType.includes('application/json')) {
const textResponse = await response.text();
console.error('Respuesta no es JSON:', textResponse.substring(0, 500));
throw new Error('El servidor no devolvió una respuesta JSON válida');
}
const data = await response.json();
console.log('Datos recibidos:', data);
loading.classList.remove('active');
<?php if ($pingEnabled): ?>
loadingPingWarning.style.display = 'none';
<?php endif; ?>
searchBtn.disabled = false;
if (data.success) {
const clientIps = displayResults(data);
// Iniciar verificación progresiva si corresponde
<?php if ($pingEnabled): ?>
if (shouldVerifyPing) {
runProgressiveVerification(clientIps, pingLimit);
} else {
// Si no se verifica con ping, mostrar todas las IPs de cliente de inmediato
clientIps.forEach(ip => renderRow(ip, 'Sólo validada con UISP', 'pending'));
}
<?php else: ?>
// Si ping no está habilitado en config, mostrar todas
clientIps.forEach(ip => renderRow(ip, 'Sólo validada con UISP', 'pending'));
<?php endif; ?>
} else {
showError(data.message || 'Error al buscar IPs disponibles');
}
} catch (error) {
loading.classList.remove('active');
<?php if ($pingEnabled): ?>
loadingPingWarning.style.display = 'none'; // Ocultar advertencia
<?php endif; ?>
searchBtn.disabled = false;
console.error('Error completo:', error);
console.error('Tipo de error:', error.name);
console.error('Mensaje:', error.message);
console.error('Stack:', error.stack);
showError('Error de conexión: ' + error.message + '. Revise la consola del navegador para más detalles.');
}
});
function displayResults(data) {
// Actualizar estadísticas
availableCount.textContent = data.data.length;
usedCount.textContent = data.used ? data.used.length : 0;
segmentDisplay.textContent = data.segment || '-';
// Limpiar tabla
ipTableBody.innerHTML = '';
// Mostrar mensaje si no hay IPs disponibles
if (data.data.length === 0) {
showError('No hay IPs disponibles en este segmento', 'warning');
return [];
}
const clientIps = [];
// Separar y renderizar solo IPs de administración
data.data.forEach((ip, index) => {
const ipTypeLabel = getIpType(ip);
if (ipTypeLabel === 'Administración') {
renderRow(ip, 'Sólo validada con UISP', 'pending');
} else {
clientIps.push(ip);
}
});
// Mostrar resultados container
results.classList.add('active');
// Mostrar mensaje de éxito
showError(data.message, 'success');
return clientIps;
}
function renderRow(ip, statusText, statusClass) {
const ipTypeLabel = getIpType(ip);
const hideAdminCheckbox = document.getElementById('hideAdmin');
const isHidden = hideAdminCheckbox && hideAdminCheckbox.checked && ipTypeLabel === 'Administración';
const row = document.createElement('tr');
row.id = `row-${ip.replace(/\./g, '-')}`;
if (isHidden) row.classList.add('hidden-row');
if (ipTypeLabel === 'Administración') row.classList.add('admin-row');
// Calcular índice visual
const index = ipTableBody.children.length + 1;
row.innerHTML = `
<td>${index}</td>
<td>
<div class="ip-cell-mobile">
<span class="ip-address">${ip}</span>
<span class="ip-type-badge ip-type-${ipTypeLabel === 'Administración' ? 'admin' : 'client'}">
${ipTypeLabel}
</span>
</div>
</td>
<td id="status-${ip.replace(/\./g, '-')}">
<span class="status-badge status-${statusClass}">${statusText}</span>
</td>
<td>
<button class="btn-copy" onclick="copyToClipboard('${ip}', this)">
📋 Copiar
</button>
</td>
`;
ipTableBody.appendChild(row);
}
// Listener para el filtro de Admin IPs
const hideAdminCheckbox = document.getElementById('hideAdmin');
if (hideAdminCheckbox) {
hideAdminCheckbox.addEventListener('change', function() {
const adminRows = document.querySelectorAll('.admin-row');
adminRows.forEach(row => {
if (this.checked) {
row.classList.add('hidden-row');
} else {
row.classList.remove('hidden-row');
}
});
});
}
// Función para clasificar el tipo de IP
function getIpType(ip) {
const parts = ip.split('.');
if (parts.length !== 4) return 'Cliente';
const segment = parts[2];
const lastOctet = parseInt(parts[3]);
// 1. Verificar rangos personalizados si existen
// Asegurarse de que editorData esté cargado
if (typeof editorData === 'undefined' || !editorData.segmentos) {
try {
if (typeof customRangesJson !== 'undefined') {
console.log('customRangesJson type:', typeof customRangesJson);
console.log('customRangesJson value:', customRangesJson);
editorData = JSON.parse(customRangesJson);
console.log('Parsed editorData:', editorData);
console.log('editorData.segmentos:', editorData.segmentos);
} else {
console.warn('customRangesJson is undefined');
editorData = { segmentos: [] };
}
} catch(e) {
console.error('Error parsing JSON in getIpType:', e);
console.error('Failed to parse:', customRangesJson);
editorData = { segmentos: [] };
}
}
// Buscar segmento actual en configuración personalizada
// Convertir ambos a string y limpiar espacios para asegurar coincidencia
const searchSeg = segment.toString().trim();
const customSegment = editorData.segmentos.find(s => s.segmento.toString().trim() === searchSeg);
if (useCustomRanges && customSegment) {
// Verificar rangos iniciales personalizados
if (customSegment.administrativas_iniciales) {
for (const rango of customSegment.administrativas_iniciales) {
if (lastOctet >= rango.inicio && lastOctet <= rango.hasta) {
console.log(`IP ${ip} es Admin (Custom Init: ${rango.inicio}-${rango.hasta})`);
return 'Administración';
}
}
}
// Verificar rangos finales personalizados
if (customSegment.administrativas_finales) {
for (const rango of customSegment.administrativas_finales) {
if (lastOctet >= rango.inicio && lastOctet <= rango.hasta) {
console.log(`IP ${ip} es Admin (Custom Final: ${rango.inicio}-${rango.hasta})`);
return 'Administración';
}
}
}
// Si hay configuración personalizada para este segmento y no cayó en los rangos admin, es Cliente
console.log(`IP ${ip} es Cliente (Custom Segment Found but not in ranges)`);
return 'Cliente';
} else if (useCustomRanges) {
console.log(`IP ${ip}: Segmento '${searchSeg}' no encontrado en custom ranges.`);
if (editorData.segmentos) {
console.log('Segmentos disponibles:', editorData.segmentos.map(s => `'${s.segmento}'`));
} else {
console.log('editorData.segmentos es undefined o null');
}
}
// 2. Fallback a configuración global si no hay segmento personalizado
// Verificar si está en alguno de los rangos administrativos configurados globalmente
if ((lastOctet >= adminConfig.rangeStart && lastOctet <= adminConfig.rangeEnd) ||
(lastOctet >= adminConfig.finalStart && lastOctet <= adminConfig.finalEnd)) {
return 'Administración';
}
return 'Cliente';
}
async function runProgressiveVerification(clientIps, limit) {
let ipsToVerify = [];
// 1. Filtrar IPs a verificar según el límite
if (limit > 0) {
// Si hay límite, solo tomamos las primeras N
ipsToVerify = clientIps.slice(0, limit);
} else {
// Si límite es 0 (Todas), verificamos todas
ipsToVerify = [...clientIps];
}
console.log(`Iniciando verificación progresiva de ${ipsToVerify.length} IPs`);
// Mostrar botón de cancelar
verificationCancelled = false;
if (cancelBtn) {
cancelBtn.style.display = 'inline-flex';
}
// 2. Procesar una por una (secuencialmente)
for (const ip of ipsToVerify) {
// Verificar si el usuario canceló
if (verificationCancelled) {
console.log('Verificación cancelada por el usuario');
showError('Verificación cancelada. Mostrando resultados parciales.', 'warning');
break;
}
// A. Renderizar fila con estado "Verificando"
renderRow(ip, '⏳ Verificando...', 'verifying');
// Scroll al final de la tabla para seguir el progreso
const tableContainer = document.querySelector('.table-container');
tableContainer.scrollTop = tableContainer.scrollHeight;
// B. Verificar (lote de 1)
await verifyBatch([ip]);
}
// Ocultar botón de cancelar al finalizar
if (cancelBtn) {
cancelBtn.style.display = 'none';
}
// NOTA: Las IPs que exceden el límite NO se renderizan, cumpliendo el requerimiento.
}
// Event listener para botón de cancelar
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
verificationCancelled = true;
cancelBtn.style.display = 'none';
});
}
async function verifyBatch(ips) {
try {
const formData = new FormData();
formData.append('action', 'verify_batch');
formData.append('ips', JSON.stringify(ips));
// Timeout de 30 segundos por lote
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const response = await fetch('', {
method: 'POST',
body: formData,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch (e) {
console.error('Respuesta no válida del servidor:', text);
throw new Error('Respuesta inválida del servidor');
}
if (data.success) {
// Actualizar UI
// Responding = Conflicto (Rojo)
if (data.responding) {
data.responding.forEach(ip => {
updateStatus(ip, 'conflict', '⚠️ En uso (Ping)');
});
}
// Not Responding = Disponible (Verde)
if (data.not_responding) {
data.not_responding.forEach(ip => {
updateStatus(ip, 'available', '✅ Disponible');
});
}
} else {
throw new Error(data.message || 'Error en verificación');
}
} catch (error) {
console.error('Error verificando lote:', error);
// Marcar como error visualmente para que no se quede cargando
ips.forEach(ip => {
updateStatus(ip, 'unknown', '❓ Error verificación');
});
}
}
function updateStatus(ip, type, text) {
const cellId = `status-${ip.replace(/\./g, '-')}`;
const cell = document.getElementById(cellId);
if (cell) {
cell.innerHTML = `<span class="status-badge status-${type}">${text}</span>`;
}
}
function showError(message, type = 'error') {
const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
const icon = type === 'success' ? '✓' : '⚠';
errorContainer.innerHTML = `
<div class="alert ${alertClass}">
<span style="font-size: 1.5rem;">${icon}</span>
<span>${message}</span>
</div>
`;
// Auto-ocultar mensajes de éxito después de 5 segundos
if (type === 'success') {
setTimeout(() => {
errorContainer.innerHTML = '';
}, 5000);
}
}
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
const originalText = button.innerHTML;
button.innerHTML = '✓ Copiado';
button.classList.add('copied');
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove('copied');
}, 2000);
} catch (error) {
console.error('Error al copiar:', error);
alert('Error al copiar al portapapeles');
}
}
// Auto-focus en el campo de segmento
document.getElementById('segment').focus();
</script>
</body>
</html>