Mejoras de UX/UI: - Mensajes de progreso persistentes durante la validación - Estadísticas reales de 'IPs Disponibles' actualizadas post-validación - Badge de estado muestra 'No disponible' en rojo para conflictos de ping - Mejor distribución del formulario (checkboxes agrupados) Correcciones: - Corregido bug que ignoraba el límite de IPs seleccionado - Corregido ocultamiento prematuro de indicadores de carga - Corregido desaparición rápida de mensajes de éxito Docs: - Actualizado manifest a v1.6.1 - Actualizado CHANGELOG.md y README.md
2448 lines
91 KiB
PHP
2448 lines
91 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/CrmService.php';
|
|
require_once __DIR__ . '/src/IpValidator.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=true&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)
|
|
// IMPORTANTE: Pasar $config completo para que tenga acceso a apitoken e ipserver para CRM
|
|
$ipService = new IpSearchService($apiUrl, $apiToken, $log, $config);
|
|
$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' && isset($_POST['action']) && $_POST['action'] === 'validate') {
|
|
// Handler para validación individual de IP con búsqueda de sitios
|
|
header('Content-Type: application/json');
|
|
|
|
try {
|
|
$ip = $_POST['ip'] ?? '';
|
|
|
|
if (empty($ip)) {
|
|
echo json_encode(['success' => false, 'error' => 'Missing IP']);
|
|
exit;
|
|
}
|
|
|
|
// Cargar configuración
|
|
$configManager = \Ubnt\UcrmPluginSdk\Service\PluginConfigManager::create();
|
|
$config = $configManager->loadConfig();
|
|
|
|
// Crear instancia del validador
|
|
$baseUrl = 'https://' . $config['ipserver'];
|
|
$ipValidator = new \SiipAvailableIps\IpValidator(
|
|
$baseUrl,
|
|
$config['unmsApiToken'],
|
|
$log
|
|
);
|
|
|
|
// Validar si la IP está en uso
|
|
$isInUse = $ipValidator->isIpInUse($ip);
|
|
|
|
echo json_encode([
|
|
'success' => true,
|
|
'ip' => $ip,
|
|
'in_use' => $isInUse,
|
|
'available' => !$isInUse
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$log->appendLog('Error en validación: ' . $e->getMessage());
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => $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;
|
|
}
|
|
|
|
.alert-info {
|
|
background: rgba(79, 209, 197, 0.1);
|
|
border: 1px solid rgba(79, 209, 197, 0.3);
|
|
color: #4fd1c5;
|
|
}
|
|
|
|
.alert-warning {
|
|
background: rgba(255, 193, 7, 0.1);
|
|
border: 1px solid rgba(255, 193, 7, 0.3);
|
|
color: #ffc107;
|
|
}
|
|
|
|
.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;" checked>
|
|
<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;
|
|
let editingIndex = null; // Track which segment is being edited
|
|
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="editSegment(${index})" style="background: rgba(79, 172, 254, 0.2); color: #4facfe; border-color: rgba(79, 172, 254, 0.3); margin-right: 8px;">
|
|
✏️ Editar
|
|
</button>
|
|
<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);
|
|
}
|
|
|
|
// Helper function: Clear all range containers
|
|
function clearRangeContainers() {
|
|
document.getElementById('initialRangesContainer').innerHTML = '';
|
|
document.getElementById('finalRangesContainer').innerHTML = '';
|
|
}
|
|
|
|
// Helper function: Add initial range with data
|
|
function addInitialRangeWithData(start, end) {
|
|
const container = document.getElementById('initialRangesContainer');
|
|
const div = document.createElement('div');
|
|
div.className = 'range-input-group';
|
|
div.innerHTML = `
|
|
<input type="number" class="range-start" value="${start}" min="1" max="254" />
|
|
<span>hasta</span>
|
|
<input type="number" class="range-end" value="${end}" 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(div);
|
|
}
|
|
|
|
// Helper function: Add final range with data
|
|
function addFinalRangeWithData(start, end) {
|
|
const container = document.getElementById('finalRangesContainer');
|
|
const div = document.createElement('div');
|
|
div.className = 'range-input-group';
|
|
div.innerHTML = `
|
|
<input type="number" class="range-start" value="${start}" min="1" max="254" />
|
|
<span>hasta</span>
|
|
<input type="number" class="range-end" value="${end}" 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(div);
|
|
}
|
|
|
|
// Reset form to add mode
|
|
function resetForm() {
|
|
const segmentInput = document.getElementById('newSegment');
|
|
segmentInput.value = '';
|
|
segmentInput.disabled = false;
|
|
|
|
clearRangeContainers();
|
|
|
|
// Add default empty ranges
|
|
document.getElementById('initialRangesContainer').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>
|
|
`;
|
|
|
|
document.getElementById('finalRangesContainer').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="254" />
|
|
<button class="btn-small" onclick="addFinalRange()">+ Agregar Rango</button>
|
|
</div>
|
|
`;
|
|
|
|
// Reset UI text
|
|
document.querySelector('#advancedEditor .card h3').textContent = 'Agregar Nuevo Segmento';
|
|
const submitBtn = document.querySelector('button[onclick="addSegmentToTable()"]');
|
|
if (submitBtn) {
|
|
submitBtn.textContent = '✅ Agregar Segmento';
|
|
}
|
|
|
|
editingIndex = null;
|
|
}
|
|
|
|
// Edit existing segment
|
|
function editSegment(index) {
|
|
editingIndex = index;
|
|
const segment = editorData.segmentos[index];
|
|
|
|
// Populate segment number
|
|
const segmentInput = document.getElementById('newSegment');
|
|
segmentInput.value = segment.segmento;
|
|
segmentInput.disabled = true; // Can't change segment number when editing
|
|
|
|
// Clear and populate ranges
|
|
clearRangeContainers();
|
|
|
|
// Populate initial ranges
|
|
if (segment.administrativas_iniciales && segment.administrativas_iniciales.length > 0) {
|
|
segment.administrativas_iniciales.forEach(rango => {
|
|
addInitialRangeWithData(rango.inicio, rango.hasta);
|
|
});
|
|
} else {
|
|
// Add empty range if none exist
|
|
addInitialRange();
|
|
}
|
|
|
|
// Populate final ranges
|
|
if (segment.administrativas_finales && segment.administrativas_finales.length > 0) {
|
|
segment.administrativas_finales.forEach(rango => {
|
|
addFinalRangeWithData(rango.inicio, rango.hasta);
|
|
});
|
|
} else {
|
|
// Add empty range if none exist
|
|
addFinalRange();
|
|
}
|
|
|
|
// Update UI
|
|
document.querySelector('#advancedEditor .card h3').textContent = `Editar Segmento ${segment.segmento}`;
|
|
const submitBtn = document.querySelector('button[onclick="addSegmentToTable()"]');
|
|
if (submitBtn) {
|
|
submitBtn.textContent = '💾 Actualizar Segmento';
|
|
}
|
|
|
|
// Scroll to top of editor
|
|
document.getElementById('advancedEditor').scrollTop = 0;
|
|
}
|
|
|
|
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 (solo en modo CREATE)
|
|
if (editingIndex === null && 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 });
|
|
}
|
|
});
|
|
|
|
const newSegment = {
|
|
segmento: segment,
|
|
administrativas_iniciales: initialRanges,
|
|
administrativas_finales: finalRanges
|
|
};
|
|
|
|
if (editingIndex !== null) {
|
|
// UPDATE mode
|
|
editorData.segmentos[editingIndex] = newSegment;
|
|
alert('✅ Segmento actualizado correctamente');
|
|
} else {
|
|
// CREATE mode
|
|
editorData.segmentos.push(newSegment);
|
|
alert('✅ Segmento agregado correctamente');
|
|
}
|
|
|
|
// Reset form and update table
|
|
resetForm();
|
|
hasUnsavedChanges = true;
|
|
renderSegmentsTable();
|
|
}
|
|
|
|
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);
|
|
|
|
if (data.success) {
|
|
const clientIps = displayResults(data);
|
|
|
|
// Validación progresiva con búsqueda de sitios
|
|
<?php if ($pingEnabled): ?>
|
|
if (shouldVerifyPing) {
|
|
// Si ping está marcado, validar con site search y luego con ping
|
|
// NO ocultar loading, se encargará runProgressiveValidation
|
|
runProgressiveValidation(clientIps, pingLimit, shouldVerifyPing);
|
|
} else {
|
|
// Si ping NO está marcado, validar solo con site search
|
|
// NO ocultar loading, se encargará runProgressiveValidation
|
|
runProgressiveValidation(clientIps, 0, false);
|
|
}
|
|
<?php else: ?>
|
|
// Si ping no está habilitado globalmente, solo site search
|
|
runProgressiveValidation(clientIps, 0, false);
|
|
<?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;
|
|
|
|
// Determinar el badge de tipo de IP
|
|
let ipTypeBadge = '';
|
|
if (ipTypeLabel === 'Administración') {
|
|
ipTypeBadge = '<span class="ip-type-badge ip-type-admin">Administración</span>';
|
|
} else {
|
|
// Si la IP está en uso (status contiene "En uso" o statusClass es "used"), mostrar "No disponible" en rojo
|
|
if (statusClass === 'used' || statusText.includes('En uso')) {
|
|
ipTypeBadge = '<span class="ip-type-badge ip-type-admin">No disponible</span>';
|
|
} else {
|
|
ipTypeBadge = '<span class="ip-type-badge ip-type-client">Cliente</span>';
|
|
}
|
|
}
|
|
|
|
row.innerHTML = `
|
|
<td>${index}</td>
|
|
<td>
|
|
<div class="ip-cell-mobile">
|
|
<span class="ip-address">${ip}</span>
|
|
${ipTypeBadge}
|
|
</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';
|
|
}
|
|
|
|
|
|
/**
|
|
* Validación progresiva con búsqueda de sitios (event.ip_validate)
|
|
* Valida cada IP antes de mostrarla en la tabla
|
|
*/
|
|
async function runProgressiveValidation(clientIps, pingLimit, shouldVerifyPing) {
|
|
console.log(`Iniciando validación progresiva de ${clientIps.length} IPs`);
|
|
console.log(`Ping habilitado: ${shouldVerifyPing}, Límite: ${pingLimit}`);
|
|
|
|
// Determinar cuántas IPs validar según el límite
|
|
let ipsToValidate = [];
|
|
if (pingLimit > 0 && shouldVerifyPing) {
|
|
// Si hay límite y ping está habilitado, solo validar las primeras N
|
|
ipsToValidate = clientIps.slice(0, pingLimit);
|
|
} else {
|
|
// Si no hay límite o ping no está habilitado, validar todas
|
|
ipsToValidate = [...clientIps];
|
|
}
|
|
|
|
console.log(`Validando ${ipsToValidate.length} IPs de ${clientIps.length} disponibles`);
|
|
|
|
// Mostrar mensaje de progreso según cantidad de IPs
|
|
let progressMessage = '';
|
|
if (shouldVerifyPing && (pingLimit === 0 || pingLimit >= 20)) {
|
|
progressMessage = 'Validando todas las IP\'s por ping, esto puede llevar varios minutos. Espere por favor...';
|
|
} else if (shouldVerifyPing) {
|
|
progressMessage = 'Validando IP\'s por ping. Espere por favor...';
|
|
} else {
|
|
progressMessage = 'Validando IP\'s con búsqueda de sitios. Espere por favor...';
|
|
}
|
|
|
|
// Usar autohide = false explícitamente y agregar spinner HTML al mensaje
|
|
const spinnerHtml = '<span class="loading-spinner-small"></span> ';
|
|
showError(spinnerHtml + progressMessage, 'info', false);
|
|
|
|
// Mostrar botón de cancelar
|
|
verificationCancelled = false;
|
|
if (cancelBtn) {
|
|
cancelBtn.style.display = 'inline-flex';
|
|
}
|
|
|
|
let validatedIps = [];
|
|
|
|
// Procesar cada IP secuencialmente
|
|
for (const ip of ipsToValidate) {
|
|
// Verificar si el usuario canceló
|
|
if (verificationCancelled) {
|
|
console.log('Validación cancelada por el usuario');
|
|
showError('Validación cancelada. Mostrando resultados parciales.', 'warning');
|
|
break;
|
|
}
|
|
|
|
// NO renderizar aún, primero validar
|
|
|
|
// Scroll al final de la tabla
|
|
const tableContainer = document.querySelector('.table-container');
|
|
if (tableContainer) {
|
|
tableContainer.scrollTop = tableContainer.scrollHeight;
|
|
}
|
|
|
|
try {
|
|
// Llamar a validación de IP con búsqueda de sitios
|
|
const formData = new FormData();
|
|
formData.append('action', 'validate');
|
|
formData.append('ip', ip);
|
|
|
|
const response = await fetch('', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && !data.in_use) {
|
|
// IP disponible según site search
|
|
validatedIps.push(ip);
|
|
|
|
if (shouldVerifyPing) {
|
|
// Si ping está habilitado, renderizar y verificar con ping
|
|
renderRow(ip, '⏳ Verificando ping...', 'verifying');
|
|
await verifyBatch([ip]);
|
|
} else {
|
|
// Si ping NO está habilitado, renderizar como disponible
|
|
renderRow(ip, '✅ Disponible', 'available');
|
|
}
|
|
} else {
|
|
// IP en uso según site search - NO RENDERIZAR
|
|
// Solo agregar log en consola
|
|
console.log(`IP ${ip} filtrada (en uso en UISP)`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error validando IP ${ip}:`, error);
|
|
// En caso de error, renderizar con error
|
|
renderRow(ip, '❌ Error validación', 'error');
|
|
}
|
|
}
|
|
|
|
// Ocultar botón de cancelar
|
|
if (cancelBtn) {
|
|
cancelBtn.style.display = 'none';
|
|
}
|
|
|
|
// Actualizar estadísticas con los números reales
|
|
// Contar IPs disponibles (status-available) y en uso (status-used o status-conflict)
|
|
const allRows = ipTableBody.querySelectorAll('tr');
|
|
let availableIpsCount = 0;
|
|
let usedIpsCount = 0;
|
|
|
|
allRows.forEach(row => {
|
|
const statusBadge = row.querySelector('.status-badge');
|
|
if (statusBadge) {
|
|
if (statusBadge.classList.contains('status-available')) {
|
|
availableIpsCount++;
|
|
} else if (statusBadge.classList.contains('status-used') ||
|
|
statusBadge.classList.contains('status-conflict')) {
|
|
usedIpsCount++;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Actualizar contadores en la UI
|
|
availableCount.textContent = availableIpsCount;
|
|
usedCount.textContent = usedIpsCount;
|
|
|
|
// Ocultar indicadores de carga y reactivar botón
|
|
loading.classList.remove('active');
|
|
<?php if ($pingEnabled): ?>
|
|
if (loadingPingWarning) loadingPingWarning.style.display = 'none';
|
|
<?php endif; ?>
|
|
if (searchBtn) searchBtn.disabled = false;
|
|
|
|
console.log(`Validación completada. ${availableIpsCount} IPs disponibles, ${usedIpsCount} IPs en uso`);
|
|
showError(`Validación completada. ${availableIpsCount} IPs disponibles encontradas.`, 'success', false);
|
|
}
|
|
|
|
/**
|
|
* Actualizar el estado de una fila existente
|
|
*/
|
|
function updateRowStatus(ip, statusText, statusClass) {
|
|
const statusCell = document.getElementById(`status-${ip.replace(/\./g, '-')}`);
|
|
if (statusCell) {
|
|
statusCell.innerHTML = `<span class="status-badge status-${statusClass}">${statusText}</span>`;
|
|
}
|
|
|
|
// Si el estado es "En uso", también actualizar el badge de tipo de IP a "No disponible"
|
|
if (statusClass === 'used' || statusText.includes('En uso')) {
|
|
const row = document.getElementById(`row-${ip.replace(/\./g, '-')}`);
|
|
if (row) {
|
|
const ipTypeBadge = row.querySelector('.ip-type-badge');
|
|
if (ipTypeBadge && !ipTypeBadge.classList.contains('ip-type-admin')) {
|
|
// Cambiar de "Cliente" a "No disponible" con color rojo
|
|
ipTypeBadge.className = 'ip-type-badge ip-type-admin';
|
|
ipTypeBadge.textContent = 'No disponible';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
// Actualizar estadísticas con los números reales
|
|
const allRows = ipTableBody.querySelectorAll('tr');
|
|
let availableIpsCount = 0;
|
|
let usedIpsCount = 0;
|
|
|
|
allRows.forEach(row => {
|
|
const statusBadge = row.querySelector('.status-badge');
|
|
if (statusBadge) {
|
|
if (statusBadge.classList.contains('status-available')) {
|
|
availableIpsCount++;
|
|
} else if (statusBadge.classList.contains('status-used') ||
|
|
statusBadge.classList.contains('status-conflict')) {
|
|
usedIpsCount++;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Actualizar contadores en la UI
|
|
availableCount.textContent = availableIpsCount;
|
|
usedCount.textContent = usedIpsCount;
|
|
|
|
// 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>`;
|
|
}
|
|
|
|
// Si el tipo es "conflict" (En uso por ping), actualizar el badge de tipo de IP
|
|
if (type === 'conflict') {
|
|
const row = document.getElementById(`row-${ip.replace(/\./g, '-')}`);
|
|
if (row) {
|
|
const ipTypeBadge = row.querySelector('.ip-type-badge');
|
|
if (ipTypeBadge && !ipTypeBadge.classList.contains('ip-type-admin')) {
|
|
// Cambiar de "Cliente" a "No disponible" con color rojo
|
|
ipTypeBadge.className = 'ip-type-badge ip-type-admin';
|
|
ipTypeBadge.textContent = 'No disponible';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function showError(message, type = 'error', autohide = true) {
|
|
let alertClass = 'alert-error';
|
|
let icon = '⚠';
|
|
|
|
if (type === 'success') {
|
|
alertClass = 'alert-success';
|
|
icon = '✓';
|
|
} else if (type === 'info') {
|
|
alertClass = 'alert-info';
|
|
icon = '⏳';
|
|
} else if (type === 'warning') {
|
|
alertClass = 'alert-warning';
|
|
icon = '⚠';
|
|
}
|
|
|
|
errorContainer.innerHTML = `
|
|
<div class="alert ${alertClass}">
|
|
<span style="font-size: 1.5rem;">${icon}</span>
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
// Auto-ocultar solo si autohide es true
|
|
if (autohide && 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>
|