769 lines
24 KiB
PHP
769 lines
24 KiB
PHP
<?php
|
|
|
|
// Configurar manejo de errores para devolver JSON
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0); // No mostrar errores en HTML
|
|
|
|
// 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';
|
|
|
|
use Ubnt\UcrmPluginSdk\Service\PluginLogManager;
|
|
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
|
|
use SiipAvailableIps\IpSearchService;
|
|
|
|
// 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'));
|
|
|
|
// Manejar peticiones AJAX
|
|
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'] ?? '';
|
|
$log->appendLog("Buscando IPs en segmento: $segmento");
|
|
|
|
// 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
|
|
$ipService = new IpSearchService($apiUrl, $apiToken, $log);
|
|
$resultado = $ipService->buscarIpsDisponibles($segmento);
|
|
|
|
$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') {
|
|
$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');
|
|
|
|
?>
|
|
<!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;
|
|
}
|
|
|
|
:root {
|
|
--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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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: 600;
|
|
font-size: 1rem;
|
|
color: #4facfe;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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>
|
|
</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>
|
|
<button type="submit" class="btn btn-primary" id="searchBtn">
|
|
<span>🔍</span>
|
|
<span>Buscar IPs</span>
|
|
</button>
|
|
</form>
|
|
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
<p>Consultando IPs disponibles...</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>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>
|
|
const searchForm = document.getElementById('searchForm');
|
|
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');
|
|
|
|
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
|
|
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);
|
|
|
|
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');
|
|
searchBtn.disabled = false;
|
|
|
|
if (data.success) {
|
|
displayResults(data);
|
|
} else {
|
|
showError(data.message || 'Error al buscar IPs disponibles');
|
|
}
|
|
} catch (error) {
|
|
loading.classList.remove('active');
|
|
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;
|
|
}
|
|
|
|
// Llenar tabla
|
|
data.data.forEach((ip, index) => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${index + 1}</td>
|
|
<td><span class="ip-address">${ip}</span></td>
|
|
<td>
|
|
<button class="btn-copy" onclick="copyToClipboard('${ip}', this)">
|
|
📋 Copiar
|
|
</button>
|
|
</td>
|
|
`;
|
|
ipTableBody.appendChild(row);
|
|
});
|
|
|
|
// Mostrar resultados
|
|
results.classList.add('active');
|
|
|
|
// Mostrar mensaje de éxito
|
|
showError(data.message, 'success');
|
|
}
|
|
|
|
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>
|