Files
webclone/web-interface/public/app-new.js
T

712 lines
24 KiB
JavaScript

// Global state
let currentPage = 'dashboard';
let cloneHistory = [];
let settings = {
outputDir: 'M:\\CLONEWEB\\cloned-sites',
timeout: 30,
retries: 3,
excludedDomains: 'facebook.com\ntwitter.com\ninstagram.com\ngoogle-analytics.com\ngoogletagmanager.com'
};
// Initialize app
document.addEventListener('DOMContentLoaded', function () {
initializeApp();
});
function initializeApp() {
setupNavigation();
setupThemeToggle();
setupForms();
checkServiceStatus();
loadDashboard();
loadHistory();
loadSettings();
// Auto-refresh status every 30 seconds
setInterval(checkServiceStatus, 30000);
}
// Navigation
function setupNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const page = item.getAttribute('data-page');
navigateToPage(page);
});
});
}
function navigateToPage(page) {
// Update nav items
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-page') === page) {
item.classList.add('active');
}
});
// Update pages
document.querySelectorAll('.page').forEach(p => {
p.classList.remove('active');
});
document.getElementById(page).classList.add('active');
// Update page title
const titles = {
'dashboard': 'Dashboard',
'new-clone': 'Nova Clonagem',
'history': 'Histórico',
'settings': 'Configurações'
};
document.getElementById('pageTitle').textContent = titles[page] || page;
currentPage = page;
// Refresh data when navigating
if (page === 'dashboard') {
loadDashboard();
} else if (page === 'history') {
loadHistory();
}
}
// Theme Toggle
function setupThemeToggle() {
const themeToggle = document.getElementById('themeToggle');
// Load saved theme
const savedTheme = localStorage.getItem('theme') || 'light';
if (savedTheme === 'dark') {
document.body.classList.add('dark-theme');
themeToggle.checked = true;
}
themeToggle.addEventListener('change', () => {
if (themeToggle.checked) {
document.body.classList.add('dark-theme');
localStorage.setItem('theme', 'dark');
} else {
document.body.classList.remove('dark-theme');
localStorage.setItem('theme', 'light');
}
});
}
// Service Status - verifica o serviço WGET na porta 3002
async function checkServiceStatus() {
const statusIndicator = document.getElementById('serviceStatus');
try {
// Verificar o serviço WGET via proxy do servidor (mesma origem)
const response = await fetch('/api/health', {
method: 'GET'
});
if (response.ok) {
statusIndicator.className = 'status-indicator online';
statusIndicator.querySelector('.status-text').textContent = 'Serviço Online';
} else {
statusIndicator.className = 'status-indicator offline';
statusIndicator.querySelector('.status-text').textContent = 'Serviço Offline';
}
} catch (error) {
statusIndicator.className = 'status-indicator offline';
statusIndicator.querySelector('.status-text').textContent = 'Serviço Offline';
}
}
// Dashboard
function loadDashboard() {
// Load from localStorage
const history = JSON.parse(localStorage.getItem('cloneHistory') || '[]');
cloneHistory = history;
// Calculate stats
const totalSites = history.length;
const totalSize = history.reduce((sum, item) => sum + (item.size || 0), 0);
const lastClone = history.length > 0 ? history[0].timestamp : null;
const successCount = history.filter(item => item.success).length;
const successRate = totalSites > 0 ? Math.round((successCount / totalSites) * 100) : 100;
// Update stats
document.getElementById('totalSites').textContent = totalSites;
document.getElementById('totalSize').textContent = formatBytes(totalSize);
document.getElementById('lastClone').textContent = lastClone ? formatRelativeTime(lastClone) : 'Nunca';
document.getElementById('successRate').textContent = successRate + '%';
// Update recent clones list
const recentClonesList = document.getElementById('recentClonesList');
if (history.length === 0) {
recentClonesList.innerHTML = `
<div class="empty-state">
<i class="fas fa-inbox"></i>
<h3>Nenhuma clonagem ainda</h3>
<p>Comece clonando seu primeiro site!</p>
</div>
`;
return;
}
const recentClones = history.slice(0, 5);
recentClonesList.innerHTML = recentClones.map(clone => `
<div class="clone-item">
<div class="clone-info">
<div class="clone-icon">
<i class="fas fa-${clone.success ? 'check' : 'times'}"></i>
</div>
<div class="clone-details">
<h4>${clone.url}</h4>
<p>${formatRelativeTime(clone.timestamp)}${formatBytes(clone.size || 0)}${clone.files || 0} arquivos</p>
</div>
</div>
<div class="clone-actions">
${clone.success ? `
<button class="btn btn-secondary btn-sm" onclick="openClonedSite('${clone.directory}')">
<i class="fas fa-external-link-alt"></i>
</button>
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder('${clone.directory}')">
<i class="fas fa-folder-open"></i>
</button>
` : ''}
</div>
</div>
`).join('');
}
function refreshDashboard() {
loadDashboard();
showToast('Dashboard atualizado!', 'success');
}
// Clone Form
function setupForms() {
// Clone form
const cloneForm = document.getElementById('cloneForm');
cloneForm.addEventListener('submit', handleCloneSubmit);
// Depth slider
const depthSlider = document.getElementById('depthLevel');
const depthValue = document.getElementById('depthValue');
depthSlider.addEventListener('input', () => {
depthValue.textContent = depthSlider.value + ' níveis';
});
}
async function handleCloneSubmit(e) {
e.preventDefault();
const cloneUrlEl = document.getElementById('cloneUrl');
const depthLevelEl = document.getElementById('depthLevel');
const includeImagesEl = document.getElementById('includeImages');
const includeCSSEl = document.getElementById('includeCSS');
const includeJSEl = document.getElementById('includeJS');
const convertLinksEl = document.getElementById('convertLinks');
if (!cloneUrlEl || !depthLevelEl || !includeImagesEl || !includeCSSEl || !includeJSEl || !convertLinksEl) {
console.error('Form elements not found');
showToast('Erro: Elementos do formulário não encontrados', 'error');
return;
}
const url = cloneUrlEl.value.trim();
const depth = parseInt(depthLevelEl.value);
const includeImages = includeImagesEl.checked;
const includeCSS = includeCSSEl.checked;
const includeJS = includeJSEl.checked;
const convertLinks = convertLinksEl.checked;
const cloneMethod = 'wget';
if (!url) {
showToast('Por favor, insira uma URL válida', 'error');
return;
}
// Validate URL
try {
new URL(url);
} catch {
showToast('URL inválida. Use o formato: https://exemplo.com', 'error');
return;
}
await startCloning(url, {
depth,
includeImages,
includeCSS,
includeJS,
convertLinks,
method: cloneMethod
});
}
async function startCloning(url, options) {
const progressCard = document.getElementById('progressCard');
const progressBar = document.getElementById('progressBar');
const progressMessage = document.getElementById('progressMessage');
const filesDownloaded = document.getElementById('filesDownloaded');
const sizeDownloaded = document.getElementById('sizeDownloaded');
const timeElapsed = document.getElementById('timeElapsed');
// Show progress card
progressCard.style.display = 'block';
progressBar.style.width = '0%';
const startTime = Date.now();
let progress = 0;
// Simulate progress
const progressInterval = setInterval(() => {
progress = Math.min(progress + Math.random() * 10, 90);
progressBar.style.width = progress + '%';
const elapsed = Math.floor((Date.now() - startTime) / 1000);
timeElapsed.textContent = elapsed + 's';
}, 1000);
const messages = options.method === 'smart' ? [
'🚀 Iniciando Smart Clone (Puppeteer)...',
'⏳ Renderizando página completa...',
'🖱️ Simulando interação do usuário...',
'📜 Carregando conteúdo sob demanda...',
'📦 Capturando recursos da rede...',
'🔄 Reescrevendo links dinâmicos...',
'✨ Otimizando clone final...'
] : [
'🚀 Iniciando clonagem com WGET...',
'⏳ Sem limite de tempo - pode demorar vários minutos...',
'🔍 Analisando estrutura do site...',
'📄 Baixando páginas HTML...',
'🎨 Baixando CSS e estilos...',
'⚡ Baixando JavaScript...',
'📦 Baixando imagens e assets...',
'🔗 Convertendo links para locais...',
'💪 Clonagem profissional em andamento...',
'✨ Finalizando clonagem...'
];
let messageIndex = 0;
progressMessage.textContent = messages[0];
const messageInterval = setInterval(() => {
messageIndex = (messageIndex + 1) % messages.length;
progressMessage.textContent = messages[messageIndex];
}, 4000);
try {
let response;
if (options.method === 'smart') {
response = await fetch('/api/smart-clone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
outputDir: settings.outputDir
})
});
} else {
// WGET logic
response = await fetch('/api/wget-clone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
depth: options.depth,
outputDir: settings.outputDir
})
});
}
clearInterval(progressInterval);
clearInterval(messageInterval);
const data = await response.json();
if (data.success) {
// Complete progress
progressBar.style.width = '100%';
progressMessage.textContent = '✅ Clonagem concluída com sucesso!';
// Update stats
const files = data.data?.files || data.data?.cloneInfo?.fileCount || data.data?.resources || data.files || 0;
const size = data.data?.size || data.data?.cloneInfo?.totalSize || data.size || 0;
filesDownloaded.textContent = files;
sizeDownloaded.textContent = formatBytes(size);
// Save to history
const cloneRecord = {
url,
timestamp: new Date().toISOString(),
success: true,
files: files,
size: size,
directory: data.data.path || data.data.directory,
depth: options.depth,
method: options.method
};
saveToHistory(cloneRecord);
// Show success message
setTimeout(() => {
showToast('Site clonado com sucesso!', 'success');
progressCard.style.display = 'none';
// Show success dialog
showSuccessDialog(cloneRecord);
}, 2000);
} else {
throw new Error(data.error || 'Erro desconhecido');
}
} catch (error) {
clearInterval(progressInterval);
clearInterval(messageInterval);
console.error('Clone error:', error);
progressCard.style.display = 'none';
// Save failed attempt to history
saveToHistory({
url,
timestamp: new Date().toISOString(),
success: false,
error: error.message
});
showToast('Erro ao clonar site: ' + error.message, 'error');
}
}
function showSuccessDialog(cloneRecord) {
const dialog = document.createElement('div');
dialog.className = 'success-dialog';
dialog.innerHTML = `
<div class="success-dialog-content">
<div class="success-icon">
<i class="fas fa-check-circle"></i>
</div>
<h3>Site Clonado com Sucesso!</h3>
<p>${cloneRecord.url}</p>
<div class="success-stats">
<span><i class="fas fa-file"></i> ${cloneRecord.files} arquivos</span>
<span><i class="fas fa-hdd"></i> ${formatBytes(cloneRecord.size)}</span>
</div>
<div class="success-actions">
<button class="btn btn-primary" onclick="openClonedSite('${cloneRecord.directory}'); closeSuccessDialog()">
<i class="fas fa-external-link-alt"></i> Abrir Site
</button>
<button class="btn btn-secondary" onclick="openCloneFolder('${cloneRecord.directory}'); closeSuccessDialog()">
<i class="fas fa-folder-open"></i> Abrir Pasta
</button>
<button class="btn btn-secondary" onclick="closeSuccessDialog()">
Fechar
</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// Add styles
const style = document.createElement('style');
style.textContent = `
.success-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.3s;
}
.success-dialog-content {
background: white;
border-radius: 16px;
padding: 2rem;
max-width: 500px;
text-align: center;
animation: slideUp 0.3s;
}
.success-icon {
font-size: 4rem;
color: var(--success-color);
margin-bottom: 1rem;
}
.success-dialog-content h3 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.success-dialog-content p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.success-stats {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
padding: 1rem;
background: var(--bg-primary);
border-radius: 8px;
}
.success-stats span {
display: flex;
align-items: center;
gap: 0.5rem;
}
.success-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`;
document.head.appendChild(style);
}
function closeSuccessDialog() {
const dialog = document.querySelector('.success-dialog');
if (dialog) {
dialog.remove();
}
}
// History
function loadHistory() {
const history = JSON.parse(localStorage.getItem('cloneHistory') || '[]');
cloneHistory = history;
const historyList = document.getElementById('historyList');
if (history.length === 0) {
historyList.innerHTML = `
<div class="empty-state">
<i class="fas fa-history"></i>
<h3>Nenhum histórico</h3>
<p>Seus sites clonados aparecerão aqui</p>
</div>
`;
return;
}
historyList.innerHTML = history.map((item, index) => `
<div class="history-item">
<div class="history-info">
<h4>${item.url}</h4>
<div class="history-meta">
<span><i class="fas fa-calendar"></i> ${formatDate(item.timestamp)}</span>
<span><i class="fas fa-${item.success ? 'check-circle' : 'times-circle'}"></i> ${item.success ? 'Sucesso' : 'Falhou'}</span>
${item.files ? `<span><i class="fas fa-file"></i> ${item.files} arquivos</span>` : ''}
${item.size ? `<span><i class="fas fa-hdd"></i> ${formatBytes(item.size)}</span>` : ''}
</div>
</div>
<div class="clone-actions">
${item.success ? `
<button class="btn btn-secondary btn-sm" onclick="openClonedSite('${item.directory}')" title="Abrir Site">
<i class="fas fa-external-link-alt"></i>
</button>
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder('${item.directory}')" title="Abrir Pasta">
<i class="fas fa-folder-open"></i>
</button>
<button class="btn btn-primary btn-sm" onclick="recloneSite('${item.url}')" title="Clonar Novamente">
<i class="fas fa-redo"></i>
</button>
` : ''}
<button class="btn btn-danger btn-sm" onclick="deleteHistoryItem(${index})" title="Remover">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
}
function refreshHistory() {
loadHistory();
showToast('Histórico atualizado!', 'success');
}
function clearHistory() {
if (confirm('Tem certeza que deseja limpar todo o histórico? Esta ação não pode ser desfeita.')) {
localStorage.removeItem('cloneHistory');
cloneHistory = [];
loadHistory();
loadDashboard();
showToast('Histórico limpo com sucesso!', 'success');
}
}
function deleteHistoryItem(index) {
if (confirm('Remover este item do histórico?')) {
cloneHistory.splice(index, 1);
localStorage.setItem('cloneHistory', JSON.stringify(cloneHistory));
loadHistory();
loadDashboard();
showToast('Item removido!', 'success');
}
}
function recloneSite(url) {
document.getElementById('cloneUrl').value = url;
navigateToPage('new-clone');
showToast('URL carregada! Configure as opções e clique em "Iniciar Clonagem"', 'info');
}
function saveToHistory(record) {
cloneHistory.unshift(record);
// Keep only last 100 records
if (cloneHistory.length > 100) {
cloneHistory = cloneHistory.slice(0, 100);
}
localStorage.setItem('cloneHistory', JSON.stringify(cloneHistory));
}
// Settings
function loadSettings() {
const savedSettings = localStorage.getItem('cloneSettings');
if (savedSettings) {
settings = JSON.parse(savedSettings);
}
// Populate settings form (if needed)
const outDirInput = document.getElementById('settingsOutputDir');
if (outDirInput) outDirInput.value = settings.outputDir || '/root/Desktop/Clonados';
}
function saveSettings() {
const outDirInput = document.getElementById('settingsOutputDir');
if (outDirInput) settings.outputDir = outDirInput.value;
localStorage.setItem('cloneSettings', JSON.stringify(settings));
showToast('Configurações salvas!', 'success');
}
// Utility Functions
function openClonedSite(directory) {
const siteName = directory.replace(/\\/g, '/').split('/').pop();
window.open(`/api/preview/${siteName}`, '_blank');
}
function openCloneFolder(directory) {
showToast('Abrindo gerenciador de arquivos web...', 'info');
fetch('/api/open-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: directory })
}).then(res => res.json())
.then(data => {
if (!data.success) {
showToast('Erro ao abrir pasta: ' + (data.error || ''), 'error');
} else if (data.url) {
window.open(data.url, '_blank');
}
})
.catch(err => console.error('Erro ao chamar open-folder:', err));
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${message}</span>
`;
document.body.appendChild(toast);
// Add styles if not already added
if (!document.getElementById('toast-styles')) {
const style = document.createElement('style');
style.id = 'toast-styles';
style.textContent = `
.toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 0.75rem;
z-index: 10000;
animation: slideInRight 0.3s, slideOutRight 0.3s 2.7s;
}
.toast-success { border-left: 4px solid var(--success-color); }
.toast-error { border-left: 4px solid var(--danger-color); }
.toast-info { border-left: 4px solid var(--info-color); }
.toast i { font-size: 1.2rem; }
.toast-success i { color: var(--success-color); }
.toast-error i { color: var(--danger-color); }
.toast-info i { color: var(--info-color); }
@keyframes slideInRight {
from { transform: translateX(400px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(400px); opacity: 0; }
}
`;
document.head.appendChild(style);
}
setTimeout(() => {
toast.remove();
}, 3000);
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', '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) {
const date = new Date(dateString);
return date.toLocaleString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function formatRelativeTime(dateString) {
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return days === 1 ? 'Há 1 dia' : `Há ${days} dias`;
if (hours > 0) return hours === 1 ? 'Há 1 hora' : `Há ${hours} horas`;
if (minutes > 0) return minutes === 1 ? 'Há 1 minuto' : `Há ${minutes} minutos`;
return 'Agora mesmo';
}