PRIMEIRO ENVIO
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
// Global state
|
||||
let currentResults = null;
|
||||
|
||||
// DOM Elements
|
||||
const crawlerForm = document.getElementById('crawlerForm');
|
||||
const urlInput = document.getElementById('urlInput');
|
||||
const crawlBtn = document.getElementById('crawlBtn');
|
||||
const loadingSection = document.getElementById('loadingSection');
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
const errorSection = document.getElementById('errorSection');
|
||||
const crawlerStatus = document.getElementById('crawlerStatus');
|
||||
const exportBtn = document.getElementById('exportBtn');
|
||||
|
||||
// Initialize app
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkCrawlerStatus();
|
||||
setupEventListeners();
|
||||
setupTabs();
|
||||
});
|
||||
|
||||
// Check crawler service status
|
||||
async function checkCrawlerStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/health');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'ok') {
|
||||
updateStatusIndicator('online', 'Crawler Online');
|
||||
} else {
|
||||
updateStatusIndicator('offline', 'Crawler Offline');
|
||||
}
|
||||
} catch (error) {
|
||||
updateStatusIndicator('offline', 'Crawler Offline');
|
||||
}
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
function updateStatusIndicator(status, text) {
|
||||
crawlerStatus.className = `status-indicator ${status}`;
|
||||
crawlerStatus.querySelector('span').textContent = text;
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
crawlerForm.addEventListener('submit', handleCrawlSubmit);
|
||||
exportBtn.addEventListener('click', exportResults);
|
||||
|
||||
// Auto-check status every 30 seconds
|
||||
setInterval(checkCrawlerStatus, 30000);
|
||||
}
|
||||
|
||||
// Setup tabs functionality
|
||||
function setupTabs() {
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
const tabPanes = document.querySelectorAll('.tab-pane');
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const targetTab = button.getAttribute('data-tab');
|
||||
|
||||
// Remove active class from all buttons and panes
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabPanes.forEach(pane => pane.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked button and corresponding pane
|
||||
button.classList.add('active');
|
||||
document.getElementById(targetTab).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle crawl form submission
|
||||
async function handleCrawlSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) {
|
||||
showError('Por favor, insira uma URL válida.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
showError('Por favor, insira uma URL válida (ex: https://example.com).');
|
||||
return;
|
||||
}
|
||||
|
||||
await startCrawling(url);
|
||||
}
|
||||
|
||||
// Start crawling process
|
||||
async function startCrawling(url) {
|
||||
showLoading();
|
||||
updateLoadingMessage('🚀 Iniciando clonagem profissional com WGET...');
|
||||
|
||||
try {
|
||||
// SEM TIMEOUT - permite clonagens longas
|
||||
// Removido AbortController para permitir clonagens que demoram mais de 10 minutos
|
||||
|
||||
// Atualizar mensagem periodicamente
|
||||
const messages = [
|
||||
'🔍 Analisando estrutura do site...',
|
||||
'📄 Baixando páginas HTML...',
|
||||
'📦 Baixando imagens e assets...',
|
||||
'🎨 Baixando CSS e fontes...',
|
||||
'⚡ Baixando JavaScript...',
|
||||
'🔗 Convertendo links para locais...',
|
||||
'⏳ Aguarde, clonagem profissional em andamento...'
|
||||
];
|
||||
let messageIndex = 0;
|
||||
const messageInterval = setInterval(() => {
|
||||
messageIndex = (messageIndex + 1) % messages.length;
|
||||
updateLoadingMessage(messages[messageIndex]);
|
||||
}, 4000);
|
||||
|
||||
// Tentar usar wget primeiro
|
||||
let response;
|
||||
try {
|
||||
updateLoadingMessage('🔧 Usando WGET para clonagem profissional...');
|
||||
response = await fetch('/api/wget-clone', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url })
|
||||
// SEM signal/timeout - permite clonagens longas
|
||||
});
|
||||
} catch (wgetError) {
|
||||
// Se wget falhar, usar crawler normal
|
||||
console.log('WGET não disponível, usando crawler padrão');
|
||||
updateLoadingMessage('🔄 Usando crawler alternativo...');
|
||||
response = await fetch('/api/crawl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url })
|
||||
// SEM signal/timeout - permite clonagens longas
|
||||
});
|
||||
}
|
||||
|
||||
// Removido clearTimeout pois não há mais timeout
|
||||
clearInterval(messageInterval);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
updateLoadingMessage('✅ Processando resultados...');
|
||||
setTimeout(() => {
|
||||
currentResults = data.data;
|
||||
showResults(data.data);
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(data.error || 'Erro desconhecido no crawling');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Crawling error:', error);
|
||||
if (error.name === 'AbortError') {
|
||||
showError('⏱️ Timeout: A clonagem demorou mais de 10 minutos. Tente novamente com um site menor.');
|
||||
} else {
|
||||
showError(error.message || 'Erro ao conectar com o serviço de crawling');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function showLoading() {
|
||||
hideAllSections();
|
||||
loadingSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update loading message
|
||||
function updateLoadingMessage(message) {
|
||||
const loadingMessage = document.getElementById('loadingMessage');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
// Show results
|
||||
function showResults(data) {
|
||||
hideAllSections();
|
||||
resultsSection.style.display = 'block';
|
||||
|
||||
// Populate stats
|
||||
populateStats(data);
|
||||
|
||||
// Populate overview
|
||||
populateOverview(data);
|
||||
|
||||
// Populate links
|
||||
populateLinks(data.links || []);
|
||||
|
||||
// Populate images
|
||||
populateImages(data.images || []);
|
||||
|
||||
// Populate raw data
|
||||
populateRawData(data);
|
||||
}
|
||||
|
||||
// Populate statistics
|
||||
function populateStats(data) {
|
||||
const statsGrid = document.getElementById('statsGrid');
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: 'Status Code',
|
||||
value: data.statusCode || 'N/A',
|
||||
icon: 'fas fa-check-circle'
|
||||
},
|
||||
{
|
||||
label: 'Links Encontrados',
|
||||
value: (data.links || []).length,
|
||||
icon: 'fas fa-link'
|
||||
},
|
||||
{
|
||||
label: 'Imagens Encontradas',
|
||||
value: (data.images || []).length,
|
||||
icon: 'fas fa-image'
|
||||
},
|
||||
{
|
||||
label: 'Tamanho do Conteúdo',
|
||||
value: formatBytes(data.contentLength || 0),
|
||||
icon: 'fas fa-file-alt'
|
||||
}
|
||||
];
|
||||
|
||||
statsGrid.innerHTML = stats.map(stat => `
|
||||
<div class="stat-card">
|
||||
<i class="${stat.icon}" style="font-size: 1.5rem; color: #667eea; margin-bottom: 0.5rem;"></i>
|
||||
<span class="stat-value">${stat.value}</span>
|
||||
<div class="stat-label">${stat.label}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Populate overview
|
||||
function populateOverview(data) {
|
||||
const overviewContent = document.querySelector('#overview .overview-content');
|
||||
|
||||
const overviewItems = [
|
||||
{ label: 'URL', value: data.url || 'N/A' },
|
||||
{ label: 'Título', value: data.title || 'N/A' },
|
||||
{ label: 'Status Code', value: data.statusCode || 'N/A' },
|
||||
{ label: 'Timestamp', value: formatDate(data.timestamp) },
|
||||
{ label: 'Tamanho do Conteúdo', value: formatBytes(data.contentLength || 0) },
|
||||
{ label: 'Total de Links', value: (data.links || []).length },
|
||||
{ label: 'Total de Imagens', value: (data.images || []).length }
|
||||
];
|
||||
|
||||
// Adicionar informações do clone se disponível
|
||||
if (data.cloneInfo) {
|
||||
overviewItems.push(
|
||||
{ label: 'Clone Status', value: data.cloneInfo.success ? '✅ Sucesso' : '❌ Falhou' },
|
||||
{ label: 'Assets Baixados', value: `${data.cloneInfo.assetsDownloaded}/${data.cloneInfo.totalAssets}` },
|
||||
{ label: 'Diretório Local', value: data.cloneInfo.directory }
|
||||
);
|
||||
}
|
||||
|
||||
overviewContent.innerHTML = overviewItems.map(item => `
|
||||
<div class="overview-item">
|
||||
<span class="label">${item.label}:</span>
|
||||
<span class="value">${item.value}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Adicionar botão para abrir pasta do clone
|
||||
if (data.cloneInfo && data.cloneInfo.success) {
|
||||
const cloneButton = document.createElement('div');
|
||||
cloneButton.className = 'clone-actions';
|
||||
cloneButton.innerHTML = `
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e8f5e8; border-radius: 8px; border-left: 4px solid #28a745;">
|
||||
<h4 style="color: #155724; margin-bottom: 0.5rem;">🎉 Site Clonado com Sucesso!</h4>
|
||||
<p style="color: #155724; margin-bottom: 1rem;">O site foi clonado e salvo localmente com todos os assets.</p>
|
||||
<p style="color: #155724; font-size: 0.9rem; margin-bottom: 1rem;"><strong>Localização:</strong> ${data.cloneInfo.directory}</p>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button class="btn btn-success btn-sm" onclick="openCloneFolder('${data.cloneInfo.directory}')">
|
||||
📁 Abrir Pasta
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="openClonedSite('${data.cloneInfo.directory}')">
|
||||
🌐 Abrir Site Local
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
overviewContent.appendChild(cloneButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate links
|
||||
function populateLinks(links) {
|
||||
const linksList = document.querySelector('#links .links-list');
|
||||
|
||||
if (links.length === 0) {
|
||||
linksList.innerHTML = '<p style="text-align: center; color: #666; padding: 2rem;">Nenhum link encontrado.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
linksList.innerHTML = links.map(link => `
|
||||
<div class="link-item">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<a href="${link}" target="_blank" rel="noopener noreferrer">${link}</a>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Populate images
|
||||
function populateImages(images) {
|
||||
const imagesGrid = document.querySelector('#images .images-grid');
|
||||
|
||||
if (images.length === 0) {
|
||||
imagesGrid.innerHTML = '<p style="text-align: center; color: #666; padding: 2rem; grid-column: 1 / -1;">Nenhuma imagem encontrada.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
imagesGrid.innerHTML = images.map(imageUrl => `
|
||||
<div class="image-item">
|
||||
<img src="${imageUrl}" alt="Website Image" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||||
<div style="display: none; padding: 2rem; color: #666;">
|
||||
<i class="fas fa-image" style="font-size: 2rem; margin-bottom: 1rem;"></i>
|
||||
<p>Imagem não pôde ser carregada</p>
|
||||
</div>
|
||||
<div class="image-url">${imageUrl}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Populate raw data
|
||||
function populateRawData(data) {
|
||||
const rawDataElement = document.getElementById('rawData');
|
||||
rawDataElement.textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
// Show error
|
||||
function showError(message) {
|
||||
hideAllSections();
|
||||
errorSection.style.display = 'block';
|
||||
document.getElementById('errorMessage').textContent = message;
|
||||
}
|
||||
|
||||
// Hide all sections
|
||||
function hideAllSections() {
|
||||
loadingSection.style.display = 'none';
|
||||
resultsSection.style.display = 'none';
|
||||
errorSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Reset form
|
||||
function resetForm() {
|
||||
hideAllSections();
|
||||
urlInput.value = '';
|
||||
urlInput.focus();
|
||||
currentResults = null;
|
||||
}
|
||||
|
||||
// Export results
|
||||
function exportResults() {
|
||||
if (!currentResults) {
|
||||
alert('Nenhum resultado para exportar.');
|
||||
return;
|
||||
}
|
||||
|
||||
const dataStr = JSON.stringify(currentResults, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `crawl-results-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('pt-BR');
|
||||
}
|
||||
|
||||
// Add some sample URLs for quick testing
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const urlInput = document.getElementById('urlInput');
|
||||
|
||||
// Add placeholder with sample URLs
|
||||
const sampleUrls = [
|
||||
'https://example.com',
|
||||
'https://github.com',
|
||||
'https://stackoverflow.com',
|
||||
'https://www.uol.com.br'
|
||||
];
|
||||
|
||||
let currentSample = 0;
|
||||
urlInput.placeholder = sampleUrls[currentSample];
|
||||
|
||||
// Cycle through sample URLs in placeholder
|
||||
setInterval(() => {
|
||||
currentSample = (currentSample + 1) % sampleUrls.length;
|
||||
if (!urlInput.value) {
|
||||
urlInput.placeholder = sampleUrls[currentSample];
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// Funções para gerenciar sites clonados
|
||||
function openCloneFolder(directory) {
|
||||
// Tentar abrir a pasta no explorador do Windows
|
||||
fetch('/api/open-folder', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ path: directory })
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
alert('Não foi possível abrir a pasta automaticamente.\nLocalização: ' + directory);
|
||||
}
|
||||
}).catch(error => {
|
||||
alert('Não foi possível abrir a pasta automaticamente.\nLocalização: ' + directory);
|
||||
});
|
||||
}
|
||||
|
||||
function openClonedSite(directory) {
|
||||
// Tentar abrir o index.html do site clonado
|
||||
fetch('/api/open-site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ path: directory })
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
alert('Não foi possível abrir o site automaticamente.\nAbra manualmente: ' + directory + '\\index.html');
|
||||
}
|
||||
}).catch(error => {
|
||||
alert('Não foi possível abrir o site automaticamente.\nAbra manualmente: ' + directory + '\\index.html');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user