Migration Tool
Copia gratis el código

Copia y pega el código para usarlo en tu propio proyecto

				
					SEO Migration Auditor - Interfaz Web
=====================================
Panel de configuración visual para ejecutar auditorías SEO entre entornos.

Ejecutar: python auditor_web.py
Abrir: http://localhost:5000


from flask import Flask, render_template_string, request, jsonify, send_file
from flask_socketio import SocketIO, emit
import threading
import os
import sys
import json
from datetime import datetime

# Importar el auditor original
from auditor_sitemaps import SEOAuditor, CONFIG, AuditResult
from tqdm import tqdm
import concurrent.futures

app = Flask(__name__)
app.config['SECRET_KEY'] = 'seo-auditor-secret-key'
socketio = SocketIO(app, cors_allowed_origins="*")

# Estado global del análisis
audit_state = {
    "running": False,
    "progress": 0,
    "total": 0,
    "current_url": "",
    "status": "idle",
    "report_path": None,
    "error": None
}

# ============================================================================
# PLANTILLA HTML - PANEL DE CONFIGURACIÓN
# ============================================================================

HTML_TEMPLATE = '''
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🔍 SEO Migration Auditor</title>
    <link data-optimized="1" href="https://soycarlosgonzalez.com/wp-content/litespeed/css/715b583662145a45d6cd72d7051cfd11.css?ver=cfd11" rel="stylesheet">
    <link data-optimized="1" href="https://soycarlosgonzalez.com/wp-content/litespeed/css/d576b5bc316c2375daac811392339867.css?ver=39867" rel="stylesheet">
    <link data-optimized="1" href="https://soycarlosgonzalez.com/wp-content/litespeed/css/2832c0ff631d47e143c3641d8717c1ab.css?ver=7c1ab" rel="stylesheet"> <script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script> <style>:root{--primary:#6366f1;--primary-dark:#4f46e5;--primary-light:#a5b4fc;--secondary:#0ea5e9;--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--bg-dark:#0f172a;--bg-card:#1e293b;--text-primary:#f8fafc;--text-secondary:#94a3b8;--border:#334155}*{box-sizing:border-box}body{font-family:'Inter',sans-serif;background:var(--bg-dark);background-image:radial-gradient(at 0% 0%,rgb(99 102 241 / .15) 0,transparent 50%),radial-gradient(at 100% 100%,rgb(14 165 233 / .15) 0,transparent 50%);min-height:100vh;color:var(--text-primary);padding:0;margin:0}.main-container{max-width:900px;margin:0 auto;padding:40px 20px}.header{text-align:center;margin-bottom:40px}.logo-icon{width:80px;height:80px;background:linear-gradient(135deg,var(--primary) 0%,var(--secondary) 100%);border-radius:20px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;font-size:2.5rem;box-shadow:0 20px 40px rgb(99 102 241 / .3)}.header h1{font-size:2.5rem;font-weight:800;margin-bottom:10px;background:linear-gradient(135deg,#f8fafc 0%,#cbd5e1 100%);-webkit-background-clip:text;-webkit-text-fill-color:#fff0;letter-spacing:-1px}.header p{color:var(--text-secondary);font-size:1.1rem}.version-badge{display:inline-block;background:rgb(99 102 241 / .2);color:var(--primary-light);padding:4px 12px;border-radius:20px;font-size:.75rem;font-weight:600;margin-top:10px}.config-card{background:var(--bg-card);border-radius:20px;padding:30px;margin-bottom:25px;border:1px solid var(--border);box-shadow:0 25px 50px -12px rgb(0 0 0 / .5)}.card-header-custom{display:flex;align-items:center;margin-bottom:25px;padding-bottom:15px;border-bottom:1px solid var(--border)}.card-header-custom i{font-size:1.5rem;margin-right:12px;color:var(--primary)}.card-header-custom h2{font-size:1.3rem;font-weight:600;margin:0}.form-label{color:var(--text-secondary);font-size:.85rem;font-weight:500;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}.form-control,.form-select{background:var(--bg-dark);border:1px solid var(--border);color:var(--text-primary);padding:12px 16px;border-radius:10px;font-size:.95rem;transition:all 0.2s}.form-control:focus,.form-select:focus{background:var(--bg-dark);border-color:var(--primary);color:var(--text-primary);box-shadow:0 0 0 3px rgb(79 70 229 / .2)}.form-control::placeholder{color:var(--text-secondary);opacity:.7}.input-group-text{background:var(--bg-dark);border:1px solid var(--border);color:var(--text-secondary);border-radius:10px 0 0 10px}.input-group .form-control{border-radius:0 10px 10px 0}.form-check-input{background-color:var(--bg-dark);border-color:var(--border);width:20px;height:20px}.form-check-input:checked{background-color:var(--primary);border-color:var(--primary)}.form-check-label{margin-left:8px;color:var(--text-secondary)}.btn-analyze{background:linear-gradient(135deg,var(--primary) 0%,#7c3aed 100%);border:none;padding:16px 40px;font-size:1.1rem;font-weight:600;border-radius:12px;width:100%;color:#fff;transition:all 0.3s;text-transform:uppercase;letter-spacing:1px}.btn-analyze:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 10px 40px rgb(79 70 229 / .4)}.btn-analyze:disabled{opacity:.6;cursor:not-allowed}.progress-section{display:none}.progress-section.active{display:block;animation:fadeIn 0.3s ease}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.progress-bar-custom{height:10px;background:var(--bg-dark);border-radius:10px;overflow:hidden;margin:20px 0;box-shadow:inset 0 2px 4px rgb(0 0 0 / .3)}.progress-fill{height:100%;background:linear-gradient(90deg,var(--primary) 0%,var(--success) 50%,var(--secondary) 100%);background-size:200% 100%;border-radius:10px;transition:width 0.3s ease;width:0%;animation:shimmer 2s linear infinite}@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}.progress-stats{display:flex;justify-content:space-between;color:var(--text-secondary);font-size:.9rem}.progress-percent{font-size:2rem;font-weight:800;background:linear-gradient(135deg,var(--primary) 0%,var(--secondary) 100%);-webkit-background-clip:text;-webkit-text-fill-color:#fff0;margin-bottom:10px}.current-url{background:var(--bg-dark);padding:12px 16px;border-radius:8px;font-family:'JetBrains Mono',Consolas,monospace;font-size:.85rem;color:var(--secondary);margin-top:15px;word-break:break-all;border:1px solid var(--border)}.status-badge{display:inline-flex;align-items:center;padding:8px 16px;border-radius:20px;font-size:.85rem;font-weight:600;gap:8px}.status-idle{background:var(--bg-dark);color:var(--text-secondary)}.status-running{background:rgb(99 102 241 / .2);color:var(--primary-light)}.status-complete{background:rgb(16 185 129 / .2);color:#34d399}.status-error{background:rgb(239 68 68 / .2);color:#f87171}.spinner-icon{animation:spin 1s linear infinite}@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.help-icon{color:var(--text-secondary);margin-left:6px;cursor:help;font-size:.85rem}.result-section{display:none;text-align:center;padding:30px}.result-section.active{display:block}.result-icon{font-size:4rem;margin-bottom:20px}.result-icon.success{color:var(--secondary)}.result-icon.error{color:#ef4444}.btn-view-report{background:var(--secondary);border:none;padding:14px 30px;font-size:1rem;font-weight:600;border-radius:10px;color:#fff;margin:10px;transition:all 0.2s}.btn-view-report:hover{background:#059669;transform:translateY(-2px)}.btn-new-audit{background:#fff0;border:2px solid var(--border);padding:14px 30px;font-size:1rem;font-weight:600;border-radius:10px;color:var(--text-secondary);margin:10px;transition:all 0.2s}.btn-new-audit:hover{border-color:var(--primary);color:var(--primary)}.presets-container{margin-bottom:20px}.preset-btn{background:var(--bg-dark);border:1px solid var(--border);color:var(--text-secondary);padding:8px 16px;border-radius:8px;font-size:.85rem;margin-right:8px;margin-bottom:8px;transition:all 0.2s;cursor:pointer}.preset-btn:hover{border-color:var(--primary);color:var(--primary)}@media (max-width:768px){.header h1{font-size:1.8rem}.config-card{padding:20px}}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.analyzing .progress-fill{animation:pulse 1.5s infinite}.log-area{background:var(--bg-dark);border-radius:10px;padding:15px;max-height:200px;overflow-y:auto;font-family:'Consolas',monospace;font-size:.8rem;margin-top:20px}.log-entry{padding:4px 0;border-bottom:1px solid var(--border)}.log-entry:last-child{border-bottom:none}.log-info{color:var(--text-secondary)}.log-success{color:var(--secondary)}.log-warning{color:#fbbf24}.log-error{color:#f87171}</style></head>
<body>
    <div class="main-container">
        
        <div class="header">
            <div class="logo-icon">
                <i class="bi bi-search text-white"></i>
            </div>
            <h1>SEO Migration Auditor</h1>
            <p>Compara entornos de Producción y Staging para detectar problemas SEO</p>
            <span class="version-badge">v2.0 Professional</span>
        </div>
        
        
        <form id="configForm">
            
            <div class="config-card">
                <div class="card-header-custom">
                    <i class="bi bi-globe"></i>
                    <h2>Configuración de URLs</h2>
                </div>
                
                <div class="row g-4">
                    <div class="col-md-6">
                        <label class="form-label">Dominio Producción</label>
                        <input type="url" class="form-control" id="prodDomain" 
                               placeholder="https://www.ejemplo.com" required>
                    </div>
                    <div class="col-md-6">
                        <label class="form-label">Dominio Staging</label>
                        <input type="url" class="form-control" id="stageDomain" 
                               placeholder="https://staging.ejemplo.com" required>
                    </div>
                    <div class="col-md-6">
                        <label class="form-label">
                            Sitemap Producción
                            <i class="bi bi-question-circle help-icon" 
                               title="URL completa del sitemap.xml de producción"></i>
                        </label>
                        <input type="url" class="form-control" id="prodSitemap" 
                               placeholder="https://www.ejemplo.com/sitemap.xml" required>
                    </div>
                    <div class="col-md-6">
                        <label class="form-label">
                            Sitemap Staging
                            <i class="bi bi-question-circle help-icon" 
                               title="URL del sitemap en staging (puede ser el mismo que prod si no existe)"></i>
                        </label>
                        <input type="url" class="form-control" id="stageSitemap" 
                               placeholder="https://staging.ejemplo.com/sitemap.xml" required>
                    </div>
                </div>
            </div>
            
            
            <div class="config-card">
                <div class="card-header-custom">
                    <i class="bi bi-shield-lock"></i>
                    <h2>Autenticación Staging (Opcional)</h2>
                </div>
                
                <div class="form-check mb-3">
                    <input class="form-check-input" type="checkbox" id="needsAuth">
                    <label class="form-check-label" for="needsAuth">
                        El entorno de Staging requiere autenticación HTTP Basic
                    </label>
                </div>
                
                <div class="row g-4" id="authFields" style="display: none;">
                    <div class="col-md-6">
                        <label class="form-label">Usuario</label>
                        <input type="text" class="form-control" id="authUser" placeholder="usuario">
                    </div>
                    <div class="col-md-6">
                        <label class="form-label">Contraseña</label>
                        <div class="input-group">
                            <input type="password" class="form-control" id="authPass" placeholder="••••••••">
                            <button class="btn btn-outline-secondary" type="button" id="togglePass">
                                <i class="bi bi-eye"></i>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            
            
            <div class="config-card">
                <div class="card-header-custom">
                    <i class="bi bi-diagram-3"></i>
                    <h2>Descubrimiento de URLs (Crawling)</h2>
                </div>
                
                <div class="form-check mb-3">
                    <input class="form-check-input" type="checkbox" id="enableCrawling">
                    <label class="form-check-label" for="enableCrawling">
                        <strong>Activar Crawling</strong> - Descubrir URLs adicionales no incluidas en el sitemap
                    </label>
                </div>
                
                <div id="crawlingFields" style="display: none;">
                    <div class="alert alert-info py-2 mb-3">
                        <i class="bi bi-info-circle me-1"></i>
                        <small>El crawler navegará por las páginas extrayendo enlaces internos para descubrir URLs ocultas.</small>
                    </div>
                    
                    <div class="row g-4">
                        <div class="col-md-4">
                            <label class="form-label">
                                Máximo URLs a Descubrir
                                <i class="bi bi-question-circle help-icon" 
                                   title="Límite de nuevas URLs a descubrir mediante crawling"></i>
                            </label>
                            <input type="number" class="form-control" id="crawlMaxUrls" 
                                   value="500" min="10" max="5000">
                        </div>
                        <div class="col-md-4">
                            <label class="form-label">
                                Profundidad de Crawling
                                <i class="bi bi-question-circle help-icon" 
                                   title="1 = Solo enlaces directos. 2+ = Sigue enlaces en páginas descubiertas"></i>
                            </label>
                            <select class="form-select" id="crawlMaxDepth">
                                <option value="1">1 nivel (Rápido)</option>
                                <option value="2">2 niveles</option>
                                <option value="3" selected>3 niveles (Recomendado)</option>
                                <option value="5">5 niveles (Profundo)</option>
                            </select>
                        </div>
                        <div class="col-md-4">
                            <label class="form-label">Excluir Patrones</label>
                            <input type="text" class="form-control" id="crawlExclude" 
                                   value="/wp-admin/,/feed/,/tag/,/page/" 
                                   placeholder="/admin/,/tag/">
                        </div>
                    </div>
                </div>
            </div>
            
            
            <div class="config-card">
                <div class="card-header-custom">
                    <i class="bi bi-sliders"></i>
                    <h2>Opciones Avanzadas</h2>
                </div>
                
                <div class="row g-4">
                    <div class="col-md-4">
                        <label class="form-label">
                            Selector de Contenido
                            <i class="bi bi-question-circle help-icon" 
                               title="Selector CSS para focalizar el análisis (ej: main, article, .content)"></i>
                        </label>
                        <input type="text" class="form-control" id="contentSelector" 
                               value="main" placeholder="main">
                    </div>
                    <div class="col-md-4">
                        <label class="form-label">Hilos Paralelos</label>
                        <select class="form-select" id="threads">
                            <option value="3">3 (Lento)</option>
                            <option value="5" selected>5 (Recomendado)</option>
                            <option value="10">10 (Rápido)</option>
                            <option value="15">15 (Muy Rápido)</option>
                        </select>
                    </div>
                    <div class="col-md-4">
                        <label class="form-label">
                            Límite de URLs
                            <i class="bi bi-question-circle help-icon" 
                               title="0 = Sin límite. Usa un número pequeño para pruebas"></i>
                        </label>
                        <input type="number" class="form-control" id="limitUrls" 
                               value="0" min="0" placeholder="0 = Sin límite">
                    </div>
                </div>
                
                <div class="row g-4 mt-2">
                    <div class="col-md-6">
                        <label class="form-label">Umbral Smart Match (%)</label>
                        <input type="range" class="form-range" id="smartMatchThreshold" 
                               min="50" max="95" value="75">
                        <small class="text-muted">Actual: <span id="smartMatchValue">75</span>%</small>
                    </div>
                    <div class="col-md-6">
                        <label class="form-label">Trigger Similitud Baja (%)</label>
                        <input type="range" class="form-range" id="lowSimTrigger" 
                               min="20" max="70" value="50">
                        <small class="text-muted">Actual: <span id="lowSimValue">50</span>%</small>
                    </div>
                </div>
            </div>
            
            
            <button type="submit" class="btn btn-analyze" id="btnAnalyze">
                <i class="bi bi-search me-2"></i> Iniciar Auditoría SEO
            </button>
        </form>
        
        
        <div class="config-card progress-section" id="progressSection">
            <div class="card-header-custom">
                <i class="bi bi-hourglass-split"></i>
                <h2>Análisis en Progreso</h2>
                <span class="status-badge status-running ms-auto" id="statusBadge">
                    <i class="bi bi-arrow-repeat me-1"></i> Analizando...
                </span>
            </div>
            
            <div class="progress-bar-custom analyzing">
                <div class="progress-fill" id="progressFill"></div>
            </div>
            
            <div class="progress-stats">
                <span id="progressText">0 / 0 URLs</span>
                <span id="progressPercent">0%</span>
            </div>
            
            <div class="current-url" id="currentUrl">Preparando análisis...</div>
            
            <div class="log-area" id="logArea">
                <div class="log-entry log-info">🚀 Iniciando auditoría...</div>
            </div>
        </div>
        
        
        <div class="config-card result-section" id="resultSection">
            <div class="result-icon success" id="resultIcon">
                <i class="bi bi-check-circle-fill"></i>
            </div>
            <h2 id="resultTitle">¡Auditoría Completada!</h2>
            <p class="text-muted" id="resultMessage">Se analizaron X URLs correctamente.</p>
            
            <div class="mt-4">
                <button class="btn btn-view-report" id="btnViewReport">
                    <i class="bi bi-file-earmark-bar-graph me-2"></i> Ver Reporte
                </button>
                <button class="btn btn-new-audit" id="btnNewAudit">
                    <i class="bi bi-arrow-repeat me-2"></i> Nueva Auditoría
                </button>
            </div>
        </div>
    </div> <script>// Socket.IO connection
        const socket = io();
        
        // DOM Elements
        const configForm = document.getElementById('configForm');
        const progressSection = document.getElementById('progressSection');
        const resultSection = document.getElementById('resultSection');
        const btnAnalyze = document.getElementById('btnAnalyze');
        
        // Toggle auth fields
        document.getElementById('needsAuth').addEventListener('change', function() {
            document.getElementById('authFields').style.display = this.checked ? 'flex' : 'none';
        });
        
        // Toggle crawling fields
        document.getElementById('enableCrawling').addEventListener('change', function() {
            document.getElementById('crawlingFields').style.display = this.checked ? 'block' : 'none';
        });
        
        // Debug: Log cuando se hace click en el botón
        document.getElementById('btnAnalyze').addEventListener('click', function(e) {
            console.log('🖱️ Click en botón Analizar');
            // Verificar validación del formulario
            if(!configForm.checkValidity()) {
                console.log('❌ Formulario no válido - mostrando errores');
                configForm.reportValidity();
            }
        });
        
        // Toggle password visibility
        document.getElementById('togglePass').addEventListener('click', function() {
            const passField = document.getElementById('authPass');
            const icon = this.querySelector('i');
            if (passField.type === 'password') {
                passField.type = 'text';
                icon.className = 'bi bi-eye-slash';
            } else {
                passField.type = 'password';
                icon.className = 'bi bi-eye';
            }
        });
        
        // Range sliders
        document.getElementById('smartMatchThreshold').addEventListener('input', function() {
            document.getElementById('smartMatchValue').textContent = this.value;
        });
        
        document.getElementById('lowSimTrigger').addEventListener('input', function() {
            document.getElementById('lowSimValue').textContent = this.value;
        });
        
        // Form submission
        configForm.addEventListener('submit', async function(e) {
            e.preventDefault();
            
            console.log('✅ Formulario enviado - submit event triggered');
            
            // Verificar que los campos están llenos
            const prodDomain = document.getElementById('prodDomain').value;
            const stageDomain = document.getElementById('stageDomain').value;
            
            if(!prodDomain || !stageDomain) {
                alert('Por favor, completa todos los campos obligatorios');
                return;
            }
            
            console.log('Dominios:', prodDomain, stageDomain);
            
            const config = {
                prod_domain: document.getElementById('prodDomain').value,
                stage_domain: document.getElementById('stageDomain').value,
                prod_sitemap: document.getElementById('prodSitemap').value,
                stage_sitemap: document.getElementById('stageSitemap').value,
                needs_auth: document.getElementById('needsAuth').checked,
                auth_user: document.getElementById('authUser').value,
                auth_pass: document.getElementById('authPass').value,
                content_selector: document.getElementById('contentSelector').value,
                threads: parseInt(document.getElementById('threads').value),
                limit_urls: parseInt(document.getElementById('limitUrls').value),
                smart_match_threshold: parseInt(document.getElementById('smartMatchThreshold').value) / 100,
                low_sim_trigger: parseInt(document.getElementById('lowSimTrigger').value) / 100,
                // Crawling options
                enable_crawling: document.getElementById('enableCrawling').checked,
                crawl_max_urls: parseInt(document.getElementById('crawlMaxUrls').value) || 500,
                crawl_max_depth: parseInt(document.getElementById('crawlMaxDepth').value) || 3,
                crawl_exclude: document.getElementById('crawlExclude').value || ''
            };
            
            console.log('Config:', config);
            
            // Mostrar sección de progreso
            configForm.style.display = 'none';
            progressSection.classList.add('active');
            resultSection.classList.remove('active');
            
            // Limpiar log
            document.getElementById('logArea').innerHTML = '<div class="log-entry log-info">🚀 Iniciando auditoría...</div>';
            
            // Enviar configuración al servidor
            try {
                console.log('Enviando solicitud al servidor...');
                const response = await fetch('/start-audit', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(config)
                });
                
                console.log('Respuesta recibida:', response.status);
                const result = await response.json();
                console.log('Resultado:', result);
                
                if (!result.success) {
                    addLog('error', '❌ Error: ' + result.error);
                    // Mostrar botón para volver
                    setTimeout(() => {
                        if (!audit_state_running) {
                            progressSection.classList.remove('active');
                            configForm.style.display = 'block';
                        }
                    }, 3000);
                } else {
                    addLog('success', '✅ Auditoría iniciada correctamente');
                }
            } catch (err) {
                console.error('Error:', err);
                addLog('error', '❌ Error de conexión: ' + err.message);
                // Volver al formulario después de error
                setTimeout(() => {
                    progressSection.classList.remove('active');
                    configForm.style.display = 'block';
                }, 3000);
            }
        });
        
        let audit_state_running = false;
        
        // Socket events
        socket.on('progress', function(data) {
            document.getElementById('progressFill').style.width = data.percent + '%';
            document.getElementById('progressText').textContent = data.current + ' / ' + data.total + ' URLs';
            document.getElementById('progressPercent').textContent = data.percent + '%';
            document.getElementById('currentUrl').textContent = data.url || 'Procesando...';
        });
        
        socket.on('log', function(data) {
            addLog(data.type, data.message);
        });
        
        socket.on('complete', function(data) {
            progressSection.classList.remove('active');
            resultSection.classList.add('active');
            
            if (data.success) {
                document.getElementById('resultIcon').innerHTML = '<i class="bi bi-check-circle-fill"></i>';
                document.getElementById('resultIcon').className = 'result-icon success';
                document.getElementById('resultTitle').textContent = '¡Auditoría Completada!';
                document.getElementById('resultMessage').textContent = 
                    'Se analizaron ' + data.total_urls + ' URLs. ' +
                    'Smart Matches: ' + data.smart_matches + '. ' +
                    'Errores: ' + data.errors + '.';
                document.getElementById('btnViewReport').onclick = function() {
                    window.open('/report/' + data.report_file, '_blank');
                };
            } else {
                document.getElementById('resultIcon').innerHTML = '<i class="bi bi-x-circle-fill"></i>';
                document.getElementById('resultIcon').className = 'result-icon error';
                document.getElementById('resultTitle').textContent = 'Error en la Auditoría';
                document.getElementById('resultMessage').textContent = data.error;
            }
        });
        
        // Helper function
        function addLog(type, message) {
            const logArea = document.getElementById('logArea');
            const entry = document.createElement('div');
            entry.className = 'log-entry log-' + type;
            entry.textContent = message;
            logArea.appendChild(entry);
            logArea.scrollTop = logArea.scrollHeight;
        }
        
        // New audit button
        document.getElementById('btnNewAudit').addEventListener('click', function() {
            resultSection.classList.remove('active');
            configForm.style.display = 'block';
            document.getElementById('progressFill').style.width = '0%';
        });
        
        // Auto-fill sitemaps based on domains
        document.getElementById('prodDomain').addEventListener('blur', function() {
            const sitemapField = document.getElementById('prodSitemap');
            if (!sitemapField.value && this.value) {
                let domain = this.value.endsWith('/') ? this.value.slice(0, -1) : this.value;
                sitemapField.value = domain + '/sitemap.xml';
            }
        });
        
        document.getElementById('stageDomain').addEventListener('blur', function() {
            const sitemapField = document.getElementById('stageSitemap');
            if (!sitemapField.value && this.value) {
                let domain = this.value.endsWith('/') ? this.value.slice(0, -1) : this.value;
                sitemapField.value = domain + '/sitemap.xml';
            }
        });</script> <script data-no-optimize="1">window.lazyLoadOptions=Object.assign({},{threshold:300},window.lazyLoadOptions||{});!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).LazyLoad=e()}(this,function(){"use strict";function e(){return(e=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,a=arguments[e];for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(t[n]=a[n])}return t}).apply(this,arguments)}function o(t){return e({},at,t)}function l(t,e){return t.getAttribute(gt+e)}function c(t){return l(t,vt)}function s(t,e){return function(t,e,n){e=gt+e;null!==n?t.setAttribute(e,n):t.removeAttribute(e)}(t,vt,e)}function i(t){return s(t,null),0}function r(t){return null===c(t)}function u(t){return c(t)===_t}function d(t,e,n,a){t&&(void 0===a?void 0===n?t(e):t(e,n):t(e,n,a))}function f(t,e){et?t.classList.add(e):t.className+=(t.className?" ":"")+e}function _(t,e){et?t.classList.remove(e):t.className=t.className.replace(new RegExp("(^|\\s+)"+e+"(\\s+|$)")," ").replace(/^\s+/,"").replace(/\s+$/,"")}function g(t){return t.llTempImage}function v(t,e){!e||(e=e._observer)&&e.unobserve(t)}function b(t,e){t&&(t.loadingCount+=e)}function p(t,e){t&&(t.toLoadCount=e)}function n(t){for(var e,n=[],a=0;e=t.children[a];a+=1)"SOURCE"===e.tagName&&n.push(e);return n}function h(t,e){(t=t.parentNode)&&"PICTURE"===t.tagName&&n(t).forEach(e)}function a(t,e){n(t).forEach(e)}function m(t){return!!t[lt]}function E(t){return t[lt]}function I(t){return delete t[lt]}function y(e,t){var n;m(e)||(n={},t.forEach(function(t){n[t]=e.getAttribute(t)}),e[lt]=n)}function L(a,t){var o;m(a)&&(o=E(a),t.forEach(function(t){var e,n;e=a,(t=o[n=t])?e.setAttribute(n,t):e.removeAttribute(n)}))}function k(t,e,n){f(t,e.class_loading),s(t,st),n&&(b(n,1),d(e.callback_loading,t,n))}function A(t,e,n){n&&t.setAttribute(e,n)}function O(t,e){A(t,rt,l(t,e.data_sizes)),A(t,it,l(t,e.data_srcset)),A(t,ot,l(t,e.data_src))}function w(t,e,n){var a=l(t,e.data_bg_multi),o=l(t,e.data_bg_multi_hidpi);(a=nt&&o?o:a)&&(t.style.backgroundImage=a,n=n,f(t=t,(e=e).class_applied),s(t,dt),n&&(e.unobserve_completed&&v(t,e),d(e.callback_applied,t,n)))}function x(t,e){!e||0<e.loadingCount||0<e.toLoadCount||d(t.callback_finish,e)}function M(t,e,n){t.addEventListener(e,n),t.llEvLisnrs[e]=n}function N(t){return!!t.llEvLisnrs}function z(t){if(N(t)){var e,n,a=t.llEvLisnrs;for(e in a){var o=a[e];n=e,o=o,t.removeEventListener(n,o)}delete t.llEvLisnrs}}function C(t,e,n){var a;delete t.llTempImage,b(n,-1),(a=n)&&--a.toLoadCount,_(t,e.class_loading),e.unobserve_completed&&v(t,n)}function R(i,r,c){var l=g(i)||i;N(l)||function(t,e,n){N(t)||(t.llEvLisnrs={});var a="VIDEO"===t.tagName?"loadeddata":"load";M(t,a,e),M(t,"error",n)}(l,function(t){var e,n,a,o;n=r,a=c,o=u(e=i),C(e,n,a),f(e,n.class_loaded),s(e,ut),d(n.callback_loaded,e,a),o||x(n,a),z(l)},function(t){var e,n,a,o;n=r,a=c,o=u(e=i),C(e,n,a),f(e,n.class_error),s(e,ft),d(n.callback_error,e,a),o||x(n,a),z(l)})}function T(t,e,n){var a,o,i,r,c;t.llTempImage=document.createElement("IMG"),R(t,e,n),m(c=t)||(c[lt]={backgroundImage:c.style.backgroundImage}),i=n,r=l(a=t,(o=e).data_bg),c=l(a,o.data_bg_hidpi),(r=nt&&c?c:r)&&(a.style.backgroundImage='url("'.concat(r,'")'),g(a).setAttribute(ot,r),k(a,o,i)),w(t,e,n)}function G(t,e,n){var a;R(t,e,n),a=e,e=n,(t=Et[(n=t).tagName])&&(t(n,a),k(n,a,e))}function D(t,e,n){var a;a=t,(-1<It.indexOf(a.tagName)?G:T)(t,e,n)}function S(t,e,n){var a;t.setAttribute("loading","lazy"),R(t,e,n),a=e,(e=Et[(n=t).tagName])&&e(n,a),s(t,_t)}function V(t){t.removeAttribute(ot),t.removeAttribute(it),t.removeAttribute(rt)}function j(t){h(t,function(t){L(t,mt)}),L(t,mt)}function F(t){var e;(e=yt[t.tagName])?e(t):m(e=t)&&(t=E(e),e.style.backgroundImage=t.backgroundImage)}function P(t,e){var n;F(t),n=e,r(e=t)||u(e)||(_(e,n.class_entered),_(e,n.class_exited),_(e,n.class_applied),_(e,n.class_loading),_(e,n.class_loaded),_(e,n.class_error)),i(t),I(t)}function U(t,e,n,a){var o;n.cancel_on_exit&&(c(t)!==st||"IMG"===t.tagName&&(z(t),h(o=t,function(t){V(t)}),V(o),j(t),_(t,n.class_loading),b(a,-1),i(t),d(n.callback_cancel,t,e,a)))}function $(t,e,n,a){var o,i,r=(i=t,0<=bt.indexOf(c(i)));s(t,"entered"),f(t,n.class_entered),_(t,n.class_exited),o=t,i=a,n.unobserve_entered&&v(o,i),d(n.callback_enter,t,e,a),r||D(t,n,a)}function q(t){return t.use_native&&"loading"in HTMLImageElement.prototype}function H(t,o,i){t.forEach(function(t){return(a=t).isIntersecting||0<a.intersectionRatio?$(t.target,t,o,i):(e=t.target,n=t,a=o,t=i,void(r(e)||(f(e,a.class_exited),U(e,n,a,t),d(a.callback_exit,e,n,t))));var e,n,a})}function B(e,n){var t;tt&&!q(e)&&(n._observer=new IntersectionObserver(function(t){H(t,e,n)},{root:(t=e).container===document?null:t.container,rootMargin:t.thresholds||t.threshold+"px"}))}function J(t){return Array.prototype.slice.call(t)}function K(t){return t.container.querySelectorAll(t.elements_selector)}function Q(t){return c(t)===ft}function W(t,e){return e=t||K(e),J(e).filter(r)}function X(e,t){var n;(n=K(e),J(n).filter(Q)).forEach(function(t){_(t,e.class_error),i(t)}),t.update()}function t(t,e){var n,a,t=o(t);this._settings=t,this.loadingCount=0,B(t,this),n=t,a=this,Y&&window.addEventListener("online",function(){X(n,a)}),this.update(e)}var Y="undefined"!=typeof window,Z=Y&&!("onscroll"in window)||"undefined"!=typeof navigator&&/(gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent),tt=Y&&"IntersectionObserver"in window,et=Y&&"classList"in document.createElement("p"),nt=Y&&1<window.devicePixelRatio,at={elements_selector:".lazy",container:Z||Y?document:null,threshold:300,thresholds:null,data_src:"src",data_srcset:"srcset",data_sizes:"sizes",data_bg:"bg",data_bg_hidpi:"bg-hidpi",data_bg_multi:"bg-multi",data_bg_multi_hidpi:"bg-multi-hidpi",data_poster:"poster",class_applied:"applied",class_loading:"litespeed-loading",class_loaded:"litespeed-loaded",class_error:"error",class_entered:"entered",class_exited:"exited",unobserve_completed:!0,unobserve_entered:!1,cancel_on_exit:!0,callback_enter:null,callback_exit:null,callback_applied:null,callback_loading:null,callback_loaded:null,callback_error:null,callback_finish:null,callback_cancel:null,use_native:!1},ot="src",it="srcset",rt="sizes",ct="poster",lt="llOriginalAttrs",st="loading",ut="loaded",dt="applied",ft="error",_t="native",gt="data-",vt="ll-status",bt=[st,ut,dt,ft],pt=[ot],ht=[ot,ct],mt=[ot,it,rt],Et={IMG:function(t,e){h(t,function(t){y(t,mt),O(t,e)}),y(t,mt),O(t,e)},IFRAME:function(t,e){y(t,pt),A(t,ot,l(t,e.data_src))},VIDEO:function(t,e){a(t,function(t){y(t,pt),A(t,ot,l(t,e.data_src))}),y(t,ht),A(t,ct,l(t,e.data_poster)),A(t,ot,l(t,e.data_src)),t.load()}},It=["IMG","IFRAME","VIDEO"],yt={IMG:j,IFRAME:function(t){L(t,pt)},VIDEO:function(t){a(t,function(t){L(t,pt)}),L(t,ht),t.load()}},Lt=["IMG","IFRAME","VIDEO"];return t.prototype={update:function(t){var e,n,a,o=this._settings,i=W(t,o);{if(p(this,i.length),!Z&&tt)return q(o)?(e=o,n=this,i.forEach(function(t){-1!==Lt.indexOf(t.tagName)&&S(t,e,n)}),void p(n,0)):(t=this._observer,o=i,t.disconnect(),a=t,void o.forEach(function(t){a.observe(t)}));this.loadAll(i)}},destroy:function(){this._observer&&this._observer.disconnect(),K(this._settings).forEach(function(t){I(t)}),delete this._observer,delete this._settings,delete this.loadingCount,delete this.toLoadCount},loadAll:function(t){var e=this,n=this._settings;W(t,n).forEach(function(t){v(t,e),D(t,n,e)})},restoreAll:function(){var e=this._settings;K(e).forEach(function(t){P(t,e)})}},t.load=function(t,e){e=o(e);D(t,e)},t.resetStatus=function(t){i(t)},t}),function(t,e){"use strict";function n(){e.body.classList.add("litespeed_lazyloaded")}function a(){console.log("[LiteSpeed] Start Lazy Load"),o=new LazyLoad(Object.assign({},t.lazyLoadOptions||{},{elements_selector:"[data-lazyloaded]",callback_finish:n})),i=function(){o.update()},t.MutationObserver&&new MutationObserver(i).observe(e.documentElement,{childList:!0,subtree:!0,attributes:!0})}var o,i;t.addEventListener?t.addEventListener("load",a,!1):t.attachEvent("onload",a)}(window,document);</script></body>
</html>
'''

# ============================================================================
# RUTAS DE LA API
# ============================================================================

@app.route('/')
def index():
    """Página principal con el panel de configuración."""
    return render_template_string(HTML_TEMPLATE)

@app.route('/start-audit', methods=['POST'])
def start_audit():
    """Inicia una nueva auditoría con la configuración proporcionada."""
    global audit_state
    
    print("\n📥 Solicitud de auditoría recibida")
    
    if audit_state['running']:
        print("⚠️ Ya hay una auditoría en progreso")
        return jsonify({'success': False, 'error': 'Ya hay una auditoría en progreso'})
    
    config = request.json
    print(f"📋 Configuración recibida: {config.get('prod_domain', 'N/A')}")
    
    # Actualizar CONFIG global con los valores del usuario
    CONFIG['DOMAINS']['prod'] = config['prod_domain'].rstrip('/')
    CONFIG['DOMAINS']['stage'] = config['stage_domain'].rstrip('/')
    CONFIG['SITEMAP_PROD'] = config['prod_sitemap']
    CONFIG['SITEMAP_STAGE'] = config['stage_sitemap']
    CONFIG['STAGING_USER'] = config['auth_user'] if config['needs_auth'] else ''
    CONFIG['STAGING_PASS'] = config['auth_pass'] if config['needs_auth'] else ''
    CONFIG['CONTENT_SELECTOR'] = config['content_selector'] or 'main'
    CONFIG['THREADS'] = config['threads']
    CONFIG['LIMIT_URLS'] = config['limit_urls']
    CONFIG['SMART_MATCH_THRESHOLD'] = config['smart_match_threshold']
    CONFIG['LOW_SIMILARITY_TRIGGER'] = config['low_sim_trigger']
    
    # Crawling options
    CONFIG['ENABLE_CRAWLING'] = config.get('enable_crawling', False)
    CONFIG['CRAWL_MAX_URLS'] = config.get('crawl_max_urls', 500)
    CONFIG['CRAWL_MAX_DEPTH'] = config.get('crawl_max_depth', 3)
    
    # Parse exclude patterns
    exclude_str = config.get('crawl_exclude', '')
    if exclude_str:
        CONFIG['CRAWL_EXCLUDE_PATTERNS'] = [p.strip() for p in exclude_str.split(',') if p.strip()]
    
    print(f"✅ CONFIG actualizado:")
    print(f"   - Prod: {CONFIG['DOMAINS']['prod']}")
    print(f"   - Stage: {CONFIG['DOMAINS']['stage']}")
    print(f"   - Crawling: {CONFIG['ENABLE_CRAWLING']}")
    
    # Iniciar auditoría en un hilo separado
    print("🚀 Iniciando hilo de auditoría...")
    thread = threading.Thread(target=run_audit_thread)
    thread.daemon = True  # Para que el thread se cierre cuando se cierra la app
    thread.start()
    
    print("✅ Hilo iniciado correctamente")
    return jsonify({'success': True})

@app.route('/report/<filename>')
def serve_report(filename):
    """Sirve el archivo de reporte HTML."""
    report_path = os.path.join(os.getcwd(), filename)
    if os.path.exists(report_path):
        return send_file(report_path)
    return "Reporte no encontrado", 404

# ============================================================================
# LÓGICA DE AUDITORÍA
# ============================================================================

def run_audit_thread():
    """Ejecuta la auditoría en un hilo separado."""
    global audit_state
    
    print("\n🔄 run_audit_thread() iniciado")
    
    audit_state['running'] = True
    audit_state['status'] = 'running'
    audit_state['error'] = None
    
    try:
        print("📡 Emitiendo mensaje de inicio...")
        socketio.emit('log', {'type': 'info', 'message': '📡 Conectando con los sitemaps...'})
        
        # Crear auditor
        auditor = SEOAuditor()
        
        # Obtener URLs
        socketio.emit('log', {'type': 'info', 'message': '🔄 Descubriendo URLs desde sitemaps...'})
        
        # Si crawling está activo, notificar
        if CONFIG.get('ENABLE_CRAWLING', False):
            socketio.emit('log', {'type': 'info', 'message': f'🕷️ Crawling activado (max: {CONFIG["CRAWL_MAX_URLS"]} URLs, profundidad: {CONFIG["CRAWL_MAX_DEPTH"]})'})
        
        urls = auditor.get_master_list()
        
        if not urls:
            raise Exception("No se encontraron URLs en los sitemaps")
        
        # Mostrar estadísticas de fuentes de URLs
        if hasattr(auditor, 'url_sources'):
            sources = auditor.url_sources
            socketio.emit('log', {'type': 'success', 'message': f'📋 Sitemap Prod: {sources.get("sitemap_prod", 0)} URLs'})
            socketio.emit('log', {'type': 'success', 'message': f'📋 Sitemap Stage: {sources.get("sitemap_stage", 0)} URLs'})
            if sources.get('crawled_prod', 0) > 0:
                socketio.emit('log', {'type': 'warning', 'message': f'🕷️ Crawling Prod: +{sources["crawled_prod"]} URLs adicionales'})
            if sources.get('crawled_stage', 0) > 0:
                socketio.emit('log', {'type': 'warning', 'message': f'🕷️ Crawling Stage: +{sources["crawled_stage"]} URLs adicionales'})
        
        socketio.emit('log', {'type': 'success', 'message': f'✅ Total: {len(urls)} URLs a analizar'})
        
        # Pre-cachear staging
        socketio.emit('log', {'type': 'info', 'message': '🧠 Pre-cacheando contenido de Staging...'})
        
        staging_urls = [f"{CONFIG['DOMAINS']['stage']}{p}" for p in auditor.staging_paths]
        cached = 0
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG['THREADS']) as executor:
            future_map = {executor.submit(auditor.analyze_page, url, True): url for url in staging_urls}
            
            for i, future in enumerate(concurrent.futures.as_completed(future_map)):
                try:
                    from urllib.parse import urlparse
                    result = future.result()
                    if result.status == 200 and result.raw_text:
                        parsed = urlparse(result.url)
                        path = parsed.path
                        if parsed.query:
                            path += f"?{parsed.query}"
                        auditor.staging_content_cache[path] = result
                        cached += 1
                except:
                    pass
                
                # Emitir progreso de caché
                if i % 5 == 0:
                    socketio.emit('progress', {
                        'current': i + 1,
                        'total': len(staging_urls),
                        'percent': int((i + 1) / len(staging_urls) * 100),
                        'url': f'Cacheando staging... ({cached} páginas)'
                    })
        
        socketio.emit('log', {'type': 'success', 'message': f'✅ {cached} páginas de staging cacheadas'})
        
        # Procesar URLs
        socketio.emit('log', {'type': 'info', 'message': '📊 Iniciando comparación de entornos...'})
        
        audit_state['total'] = len(urls)
        results = []
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=CONFIG['THREADS']) as executor:
            future_map = {executor.submit(auditor.compare_environments, url): url for url in urls}
            
            for i, future in enumerate(concurrent.futures.as_completed(future_map)):
                try:
                    result = future.result()
                    results.append(result)
                    
                    # Log para smart matches
                    if result.is_smart_match:
                        socketio.emit('log', {
                            'type': 'warning',
                            'message': f'🔀 Smart Match: {result.rel_uri} → {result.smart_match_uri}'
                        })
                    
                except Exception as e:
                    socketio.emit('log', {'type': 'error', 'message': f'❌ Error: {str(e)[:50]}'})
                
                # Emitir progreso
                progress = int((i + 1) / len(urls) * 100)
                socketio.emit('progress', {
                    'current': i + 1,
                    'total': len(urls),
                    'percent': progress,
                    'url': urls[i] if i < len(urls) else 'Finalizando...'
                })
                
                audit_state['progress'] = i + 1
        
        # Generar reporte
        socketio.emit('log', {'type': 'info', 'message': '📝 Generando reporte HTML...'})
        
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        report_filename = f'Reporte_SEO_{timestamp}.html'
        auditor.generate_report(results, report_filename)
        
        audit_state['report_path'] = report_filename
        
        # Estadísticas finales
        smart_matches = sum(1 for r in results if r.is_smart_match)
        errors = sum(1 for r in results if r.stage.status >= 400)
        
        socketio.emit('log', {'type': 'success', 'message': f'✨ Reporte generado: {report_filename}'})
        
        socketio.emit('complete', {
            'success': True,
            'total_urls': len(results),
            'smart_matches': smart_matches,
            'errors': errors,
            'report_file': report_filename
        })
        
    except Exception as e:
        import traceback
        error_msg = str(e)
        traceback_str = traceback.format_exc()
        print(f"\n❌ ERROR EN AUDITORÍA:")
        print(traceback_str)
        
        socketio.emit('log', {'type': 'error', 'message': f'❌ Error fatal: {error_msg}'})
        socketio.emit('complete', {
            'success': False,
            'error': error_msg
        })
        
        audit_state['error'] = error_msg
    
    finally:
        audit_state['running'] = False
        audit_state['status'] = 'idle'

# ============================================================================
# MAIN
# ============================================================================

if __name__ == '__main__':
    PORT = 5050  # Cambiado de 5000 porque AirPlay usa ese puerto en macOS
    
    print("\n" + "="*60)
    print("🔍 SEO MIGRATION AUDITOR - Interfaz Web")
    print("="*60)
    print(f"\n📌 Abre tu navegador en: http://localhost:{PORT}\n")
    print("   Presiona Ctrl+C para detener el servidor\n")
    print("="*60 + "\n")
    
    socketio.run(app, host='0.0.0.0', port=PORT, debug=False, allow_unsafe_werkzeug=True)