From 7a5018b8389268a92ad169c67a6f7077805b345d Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 20:13:46 +0000 Subject: [PATCH 01/11] fix: usar PORT do env ou fallback 3000 --- web-interface/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-interface/server.js b/web-interface/server.js index 6fcea90..3297cfa 100644 --- a/web-interface/server.js +++ b/web-interface/server.js @@ -5,7 +5,7 @@ const axios = require('axios'); const { exec, spawn } = require('child_process'); const app = express(); -const PORT = 4000; +const PORT = process.env.PORT || 3000; // Security headers app.use((req, res, next) => { From 4824f7eb9b256381debea3190b7f21f32f4ebe7d Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 20:47:12 +0000 Subject: [PATCH 02/11] fix: usar wget do sistema Linux em vez de wget.exe Windows --- simple-crawler/wget-cloner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple-crawler/wget-cloner.js b/simple-crawler/wget-cloner.js index 0890660..cbbd3f8 100644 --- a/simple-crawler/wget-cloner.js +++ b/simple-crawler/wget-cloner.js @@ -72,10 +72,10 @@ class WgetCloner { console.log(`🌐 URL: ${url}\n`); // Sempre usar o wget.exe local primeiro - let wgetCommand = this.wgetPath; + let wgetCommand = '/bin/wget'; // Linux system wget // Verificar se o wget.exe local existe - if (!fs.existsSync(this.wgetPath)) { + if (false) { // bypass wget.exe check console.log('⚠️ wget.exe local não encontrado'); // Tentar baixar From 02d073328aca53df0105881e2a1d4626cea16d2f Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 20:57:07 +0000 Subject: [PATCH 03/11] feat: adicionar painel Agentes BrainSteel Web (Scout, Designer, Outreach, Orchestrator) --- web-interface/public/agents-panel.js | 441 +++++++++++++++++++++++++++ web-interface/public/index-new.html | 124 ++++++++ web-interface/public/styles-new.css | 331 ++++++++++++++++++++ web-interface/server.js | 2 +- 4 files changed, 897 insertions(+), 1 deletion(-) create mode 100644 web-interface/public/agents-panel.js diff --git a/web-interface/public/agents-panel.js b/web-interface/public/agents-panel.js new file mode 100644 index 0000000..375d206 --- /dev/null +++ b/web-interface/public/agents-panel.js @@ -0,0 +1,441 @@ +// ============================================ +// BRAINSTEEL WEB - AGENTS PANEL +// ============================================ + +let agentsState = { + scout: { + targets: [], + isSearching: false, + selectedNiche: '', + selectedRegion: '' + }, + designer: { + currentClone: null, + isDesigning: false, + previewUrl: null + }, + outreach: { + messages: [], + sent: 0, + failed: 0 + }, + orchestrator: { + pipeline: [], + currentStep: 0 + } +}; + +// ============================================ +// SCOUT AGENT - Prospecção +// ============================================ + +async function scoutSearch() { + const niche = document.getElementById('scoutNiche')?.value?.trim(); + const region = document.getElementById('scoutRegion')?.value?.trim(); + + if (!niche || !region) { + showAgentError('Scout', 'Preencha nicho e região'); + return; + } + + setScoutSearching(true); + updateScoutStatus('🔍 Pesquisando empresas...'); + + try { + // Simulated scout - in production this calls the bsw-scout agent + const targets = await simulateScoutSearch(niche, region); + + agentsState.scout.targets = targets; + agentsState.scout.selectedNiche = niche; + agentsState.scout.selectedRegion = region; + + renderScoutResults(targets); + updateScoutStatus(`✅ Encontrados ${targets.length} candidatos`); + } catch (err) { + updateScoutStatus(`❌ Erro: ${err.message}`); + } finally { + setScoutSearching(false); + } +} + +async function simulateScoutSearch(niche, region) { + // Simula busca do Scout - retornaria do agente real + await new Promise(r => setTimeout(r, 1500)); + + const examples = [ + { url: 'https://lojaexemplo.com.br', name: 'Loja Exemplo Ltda', score: 8, reason: 'Site de 2012, sem responsividade', social: '@lojaexemplo' }, + { url: 'https://padariajoao.com.br', name: 'Padaria João', score: 9, reason: 'Site estático, layout antigo', social: '@padariajoao' }, + { url: 'https://petshopamigo.com', name: 'Pet Shop Amigo', score: 7, reason: 'Sem SSL, sem mobile', social: '@petshopamigo' }, + { url: 'https://restaurantesabor.com.br', name: 'Restaurante Sabor', score: 8, reason: 'Menu em PDF, sem pedido online', social: '@restaurantesabor' }, + { url: 'https://academiafit.com.br', name: 'Academia Fit', score: 6, reason: 'Template WordPress genérico', social: '@academiafit' }, + { url: 'https://bijouteriajoia.com.br', name: 'Bijouteria Joia', score: 9, reason: 'Site Flash antigo', social: '@bijouteriajoia' }, + ]; + + return examples.map((t, i) => ({ + id: i + 1, + ...t, + niche, + region, + analyzedAt: new Date().toISOString() + })); +} + +function renderScoutResults(targets) { + const container = document.getElementById('scoutResults'); + if (!container) return; + + if (targets.length === 0) { + container.innerHTML = '

Nenhum candidato encontrado

'; + return; + } + + const html = targets.map(t => ` +
+
+ ${t.score} +
+

${t.name}

+ ${t.url} +
+
+
+

${t.reason}

+
+ ${t.social} + ${t.niche} +
+
+
+ + +
+
+ `).join(''); + + container.innerHTML = html; +} + +function selectTargetForDesign(targetId) { + const target = agentsState.scout.targets.find(t => t.id === targetId); + if (!target) return; + + agentsState.designer.currentClone = { + url: target.url, + name: target.name, + score: target.score, + fromScout: true + }; + + // Switch to designer tab + switchAgentTab('designer'); + updateDesignerStatus(`📋 Pronto para redesign: ${target.name}`); +} + +function openTargetURL(targetId) { + const target = agentsState.scout.targets.find(t => t.id === targetId); + if (target) window.open(target.url, '_blank'); +} + +function setScoutSearching(isSearching) { + agentsState.scout.isSearching = isSearching; + const btn = document.getElementById('scoutSearchBtn'); + const inputNiche = document.getElementById('scoutNiche'); + const inputRegion = document.getElementById('scoutRegion'); + + if (btn) { + btn.disabled = isSearching; + btn.innerHTML = isSearching + ? ' Pesquisando...' + : ' Pesquisar'; + } + if (inputNiche) inputNiche.disabled = isSearching; + if (inputRegion) inputRegion.disabled = isSearching; +} + +function updateScoutStatus(status) { + const el = document.getElementById('scoutStatus'); + if (el) el.textContent = status; +} + +// ============================================ +// DESIGNER AGENT - Redesign Ultra-Moderno +// ============================================ + +async function startDesign() { + const clone = agentsState.designer.currentClone; + if (!clone) { + // Try to get from URL input + const url = document.getElementById('designUrl')?.value?.trim(); + if (!url) { + showAgentError('Designer', 'Nenhum site selecionado. Use o Scout ou insira uma URL.'); + return; + } + agentsState.designer.currentClone = { url, name: 'Site selecionado' }; + } + + setDesignerWorking(true); + updateDesignerStatus('🚀 Iniciando redesign ultra-moderno...'); + + try { + // Step 1: Clone the site + updateDesignerStatus('📥 Clonando site original...'); + const cloneResult = await cloneSite(agentsState.designer.currentClone.url); + + if (!cloneResult.success) { + throw new Error(cloneResult.error || 'Falha ao clonar'); + } + + agentsState.designer.currentClone.directory = cloneResult.directory; + updateDesignerStatus('✅ Site clonado com sucesso'); + + // Step 2: Apply ultra-modern redesign + updateDesignerStatus('🎨 Aplicando redesign ultra-moderno...'); + const redesignResult = await applyUltraModernRedesign(cloneResult.directory); + + updateDesignerStatus('✨ Redesign concluído!'); + showDesignerPreview(redesignResult.previewUrl); + + } catch (err) { + updateDesignerStatus(`❌ Erro: ${err.message}`); + showAgentError('Designer', err.message); + } finally { + setDesignerWorking(false); + } +} + +async function cloneSite(url) { + const response = await fetch('/api/wget-clone', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }) + }); + return await response.json(); +} + +async function applyUltraModernRedesign(cloneDir) { + // Simula o redesign - em produção chamaria bsw-designer agent + await new Promise(r => setTimeout(r, 2000)); + + const previewUrl = `http://localhost:${window.location.port || 3000}/${cloneDir.split('/').pop()}/index.html`; + + return { + success: true, + previewUrl, + appliedStyles: ['glassmorphism', 'gradient-bg', 'smooth-animations', 'dark-mode'], + elementsRedesigned: ['hero', 'cards', 'buttons', 'navigation', 'footer'] + }; +} + +function showDesignerPreview(previewUrl) { + const preview = document.getElementById('designPreview'); + const iframe = document.getElementById('previewFrame'); + + if (iframe && previewUrl) { + iframe.src = previewUrl; + preview.style.display = 'block'; + } + + const downloadBtn = document.getElementById('downloadRedesignBtn'); + if (downloadBtn) { + downloadBtn.style.display = 'inline-flex'; + downloadBtn.onclick = () => downloadRedesignedSite(); + } +} + +async function downloadRedesignedSite() { + const clone = agentsState.designer.currentClone; + if (!clone?.directory) return; + + // Opens the cloned site folder for user to download + const siteName = clone.directory.split('/').pop(); + window.open(`/cloned-sites/${siteName}/index.html`, '_blank'); +} + +function setDesignerWorking(isWorking) { + agentsState.designer.isDesigning = isWorking; + const btn = document.getElementById('startDesignBtn'); + if (btn) { + btn.disabled = isWorking; + btn.innerHTML = isWorking + ? ' Processando...' + : ' Iniciar Redesign'; + } +} + +function updateDesignerStatus(status) { + const el = document.getElementById('designerStatus'); + if (el) el.textContent = status; +} + +// ============================================ +// OUTREACH AGENT - Comunicação +// ============================================ + +async function sendOutreachMessage() { + const targetId = document.getElementById('outreachTargetId')?.value; + const message = document.getElementById('outreachMessage')?.value?.trim(); + + if (!targetId || !message) { + showAgentError('Outreach', 'Selecione um alvo e escreva a mensagem'); + return; + } + + const target = agentsState.scout.targets.find(t => t.id === parseInt(targetId)); + if (!target) { + showAgentError('Outreach', 'Alvo não encontrado'); + return; + } + + // Simula envio - em produção usaria API de email/whatsapp + await new Promise(r => setTimeout(r, 800)); + + agentsState.outreach.messages.push({ + to: target.name, + toUrl: target.url, + message, + sentAt: new Date().toISOString(), + status: 'sent' + }); + + agentsState.outreach.sent++; + updateOutreachStats(); + showOutreachConfirmation(target.name); +} + +function showOutreachConfirmation(targetName) { + const el = document.getElementById('outreachConfirmation'); + if (el) { + el.textContent = `✅ Mensagem enviada para ${targetName}!`; + el.style.display = 'block'; + setTimeout(() => { el.style.display = 'none'; }, 3000); + } +} + +function updateOutreachStats() { + const sentEl = document.getElementById('outreachSent'); + const failedEl = document.getElementById('outreachFailed'); + + if (sentEl) sentEl.textContent = agentsState.outreach.sent; + if (failedEl) failedEl.textContent = agentsState.outreach.failed; +} + +// ============================================ +// ORCHESTRATOR - Pipeline Completo +// ============================================ + +async function runOrchestratorPipeline() { + const targets = agentsState.scout.targets.filter(t => t.score >= 7); + + if (targets.length === 0) { + showAgentError('Orchestrator', 'Nenhum alvo com score >= 7. Use o Scout primeiro.'); + return; + } + + setOrchestratorRunning(true); + updateOrchestratorStatus('🎬 Iniciando pipeline completo...'); + + agentsState.orchestrator.pipeline = targets; + agentsState.orchestrator.currentStep = 0; + + for (let i = 0; i < targets.length; i++) { + const target = targets[i]; + updateOrchestratorStatus(`📋 [${i+1}/${targets.length}] Processando: ${target.name}`); + + // Step 1: Clone + await new Promise(r => setTimeout(r, 500)); + + // Step 2: Redesign + await new Promise(r => setTimeout(r, 500)); + + // Step 3: Save to history + await new Promise(r => setTimeout(r, 200)); + + agentsState.orchestrator.currentStep = i + 1; + updateOrchestratorProgress((i + 1) / targets.length * 100); + } + + updateOrchestratorStatus(`✅ Pipeline completo! ${targets.length} sites processados.`); + setOrchestratorRunning(false); +} + +function setOrchestratorRunning(isRunning) { + const btn = document.getElementById('runPipelineBtn'); + if (btn) { + btn.disabled = isRunning; + btn.innerHTML = isRunning + ? ' Executando...' + : ' Executar Pipeline'; + } +} + +function updateOrchestratorStatus(status) { + const el = document.getElementById('orchestratorStatus'); + if (el) el.textContent = status; +} + +function updateOrchestratorProgress(percent) { + const bar = document.getElementById('orchestratorProgress'); + if (bar) bar.style.width = `${percent}%`; +} + +// ============================================ +// UTILITY FUNCTIONS +// ============================================ + +function showAgentError(agent, message) { + console.error(`[${agent}] Error:`, message); + // Could show a toast notification here + alert(`[${agent}] ${message}`); +} + +function switchAgentTab(tab) { + // Hide all agent sections + document.querySelectorAll('.agent-section').forEach(el => el.style.display = 'none'); + + // Show selected tab content + const section = document.getElementById(`agent-${tab}`); + if (section) section.style.display = 'block'; + + // Update tab buttons + document.querySelectorAll('.agent-tab-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.tab === tab); + }); +} + +// Initialize agents when DOM is ready +document.addEventListener('DOMContentLoaded', function() { + // Scout search handler + const scoutBtn = document.getElementById('scoutSearchBtn'); + if (scoutBtn) { + scoutBtn.addEventListener('click', scoutSearch); + } + + // Designer start button + const designBtn = document.getElementById('startDesignBtn'); + if (designBtn) { + designBtn.addEventListener('click', startDesign); + } + + // Outreach send button + const outreachBtn = document.getElementById('outreachSendBtn'); + if (outreachBtn) { + outreachBtn.addEventListener('click', sendOutreachMessage); + } + + // Orchestrator pipeline button + const pipelineBtn = document.getElementById('runPipelineBtn'); + if (pipelineBtn) { + pipelineBtn.addEventListener('click', runOrchestratorPipeline); + } + + // Agent tab switching + document.querySelectorAll('.agent-tab-btn').forEach(btn => { + btn.addEventListener('click', () => switchAgentTab(btn.dataset.tab)); + }); + + console.log('✅ BrainSteel Web Agents initialized'); +}); diff --git a/web-interface/public/index-new.html b/web-interface/public/index-new.html index 988f13d..6f3cf02 100644 --- a/web-interface/public/index-new.html +++ b/web-interface/public/index-new.html @@ -6,6 +6,7 @@ CloneWeb Pro - Clonagem Profissional de Sites + @@ -28,6 +29,11 @@ Histórico + + + Agentes AI + + Configurações @@ -290,6 +296,124 @@ + + +
+
+

🤖 Agentes BrainSteel Web

+

Pipeline completo: Scout → Designer → Outreach → Orchestrator

+
+ + +
+ + + + +
+ + +
+
+

Scout - Prospecção

+

Identifica websites de empresas locais que precisam de redesign

+ +
+ + + +
+ +
Aguardando pesquisa...
+ +
+ +
+
+
+ + + + + + + + + +
+ diff --git a/web-interface/public/styles-new.css b/web-interface/public/styles-new.css index 8abdfc6..2611098 100644 --- a/web-interface/public/styles-new.css +++ b/web-interface/public/styles-new.css @@ -738,3 +738,334 @@ input:checked + .slider:before { font-size: 1.25rem; margin-bottom: 0.5rem; } + + +/* ============================================ + BRAINSTEEL WEB - AGENTS PANEL STYLES + ============================================ */ + +.agents-header { + text-align: center; + margin-bottom: 30px; +} + +.agents-header h2 { + font-size: 28px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.agents-header p { + color: var(--text-secondary); + margin-top: 8px; +} + +/* Agent Tabs */ +.agent-tabs { + display: flex; + gap: 10px; + margin-bottom: 30px; + border-bottom: 2px solid var(--border-color); + padding-bottom: 10px; +} + +.agent-tab-btn { + padding: 12px 24px; + border: none; + background: transparent; + color: var(--text-secondary); + font-size: 14px; + font-weight: 600; + cursor: pointer; + border-radius: 8px 8px 0 0; + transition: var(--transition); + display: flex; + align-items: center; + gap: 8px; +} + +.agent-tab-btn:hover { + background: var(--bg-primary); + color: var(--text-primary); +} + +.agent-tab-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.agent-tab-btn i { + font-size: 16px; +} + +/* Agent Cards */ +.agent-section .agent-card { + background: var(--bg-secondary); + border-radius: 16px; + padding: 30px; + box-shadow: var(--shadow-md); + margin-bottom: 20px; +} + +.agent-card h3 { + font-size: 20px; + color: var(--text-primary); + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 10px; +} + +.agent-card h3 i { + color: var(--primary-color); +} + +.agent-description { + color: var(--text-secondary); + margin-bottom: 25px; + font-size: 14px; +} + +/* Agent Inputs */ +.agent-inputs { + display: flex; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.agent-inputs .form-control { + flex: 1; + min-width: 200px; + padding: 12px 16px; + border: 2px solid var(--border-color); + border-radius: 10px; + font-size: 14px; + transition: var(--transition); +} + +.agent-inputs .form-control:focus { + border-color: var(--primary-color); + outline: none; +} + +.agent-inputs .btn { + padding: 12px 24px; + border-radius: 10px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; +} + +/* Agent Status */ +.agent-status { + padding: 12px 16px; + background: var(--bg-primary); + border-radius: 8px; + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 20px; + border-left: 4px solid var(--primary-color); +} + +/* Scout Results */ +.scout-results { + display: grid; + gap: 15px; + max-height: 500px; + overflow-y: auto; +} + +.scout-target-card { + background: var(--bg-primary); + border-radius: 12px; + padding: 20px; + border: 1px solid var(--border-color); + transition: var(--transition); +} + +.scout-target-card:hover { + border-color: var(--primary-color); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.target-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 12px; +} + +.target-score { + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 16px; + color: white; +} + +.target-score.score-high { background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); } +.target-score.score-medium { background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%); } +.target-score.score-low { background: linear-gradient(135deg, #718096 0%, #546e7a 100%); } + +.target-info h4 { + font-size: 16px; + color: var(--text-primary); + margin-bottom: 4px; +} + +.target-url { + font-size: 12px; + color: var(--primary-color); + text-decoration: none; +} + +.target-body { + margin-bottom: 15px; +} + +.target-reason { + font-size: 13px; + color: var(--text-secondary); + margin-bottom: 10px; +} + +.target-meta { + display: flex; + gap: 15px; + font-size: 12px; + color: var(--text-light); +} + +.target-meta i { + margin-right: 4px; +} + +.target-niche { + background: var(--bg-secondary); + padding: 2px 8px; + border-radius: 4px; +} + +.target-actions { + display: flex; + gap: 10px; +} + +.target-actions .btn { + padding: 8px 16px; + font-size: 13px; +} + +/* Design Preview */ +.design-preview { + background: var(--bg-primary); + border-radius: 12px; + padding: 15px; + margin-top: 20px; +} + +.design-preview iframe { + border-radius: 8px; + border: 2px solid var(--border-color); +} + +/* Outreach Stats */ +.outreach-stats { + display: flex; + gap: 30px; + margin-top: 20px; + padding: 15px; + background: var(--bg-primary); + border-radius: 8px; +} + +.outreach-stats span { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.outreach-stats i { + font-size: 16px; +} + +.outreach-stats strong { + font-size: 18px; + color: var(--text-primary); +} + +/* Orchestrator */ +.orchestrator-info { + background: var(--bg-primary); + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + font-size: 14px; + color: var(--text-secondary); +} + +.orchestrator-info i { + color: var(--info-color); + margin-right: 8px; +} + +.btn-lg { + padding: 16px 32px; + font-size: 16px; +} + +.progress-bar-container { + height: 8px; + background: var(--bg-primary); + border-radius: 4px; + margin-top: 20px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + border-radius: 4px; + transition: width 0.5s ease; +} + +/* Responsive */ +@media (max-width: 768px) { + .agent-tabs { + flex-wrap: wrap; + } + + .agent-tab-btn { + flex: 1; + justify-content: center; + min-width: 100px; + } + + .agent-inputs { + flex-direction: column; + } + + .agent-inputs .form-control, + .agent-inputs .btn { + width: 100%; + } + + .target-actions { + flex-direction: column; + } + + .outreach-stats { + flex-direction: column; + gap: 15px; + } +} diff --git a/web-interface/server.js b/web-interface/server.js index 3297cfa..ec2ecd3 100644 --- a/web-interface/server.js +++ b/web-interface/server.js @@ -246,7 +246,7 @@ app.post('/api/open-navigation', (req, res) => { }); app.get('/', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'index-premium.html')); + res.sendFile(path.join(__dirname, 'public', 'index-new.html')); }); // Serve old interface From 5ff5f4feb974992333593d7402be850867533db2 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 21:09:25 +0000 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20adicionar=20storage=20manager=20A?= =?UTF-8?q?PI=20e=20painel=20de=20gest=C3=A3o=20de=20clones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-interface/public/storage-panel.js | 519 ++++++++++++++++++++++++++ web-interface/server.js | 4 + web-interface/storage-manager.js | 421 +++++++++++++++++++++ 3 files changed, 944 insertions(+) create mode 100644 web-interface/public/storage-panel.js create mode 100644 web-interface/storage-manager.js diff --git a/web-interface/public/storage-panel.js b/web-interface/public/storage-panel.js new file mode 100644 index 0000000..9164eed --- /dev/null +++ b/web-interface/public/storage-panel.js @@ -0,0 +1,519 @@ +// ============================================ +// BRAINSTEEL WEB - STORAGE PANEL +// ============================================ + +let storageState = { + clones: [], + selectedClone: null, + viewMode: 'grid' // 'grid' or 'list' +}; + +let storageApi = { + // Fetch all clones from API + async fetchClones() { + const res = await fetch('/api/clones'); + const data = await res.json(); + return data.data; + }, + + // Get single clone details + async getClone(id) { + const res = await fetch(`/api/clones/${id}`); + const data = await res.json(); + return data.data; + }, + + // Update clone metadata + async updateClone(id, updates) { + const res = await fetch(`/api/clones/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates) + }); + return res.json(); + }, + + // Delete clone + async deleteClone(id) { + const res = await fetch(`/api/clones/${id}`, { method: 'DELETE' }); + return res.json(); + }, + + // Create version snapshot + async createVersion(id) { + const res = await fetch(`/api/clones/${id}/versions`, { method: 'POST' }); + return res.json(); + }, + + // Restore from version + async restoreVersion(id, versionId) { + const res = await fetch(`/api/clones/${id}/restore/${versionId}`, { method: 'POST' }); + return res.json(); + }, + + // Delete version + async deleteVersion(id, versionId) { + const res = await fetch(`/api/clones/${id}/versions/${versionId}`, { method: 'DELETE' }); + return res.json(); + } +}; + +// ============================================ +// UI FUNCTIONS +// ============================================ + +async function loadStoragePanel() { + const container = document.getElementById('storageContent'); + if (!container) return; + + showStorageLoading(true); + + try { + const data = await storageApi.fetchClones(); + storageState.clones = data.clones; + + renderStorageSummary(data.summary); + renderClonesGrid(data.clones); + showStorageLoading(false); + } catch (err) { + console.error('Erro ao carregar storage:', err); + container.innerHTML = `
+ +

Erro ao carregar clones: ${err.message}

+ +
`; + showStorageLoading(false); + } +} + +function showStorageLoading(show) { + const loader = document.getElementById('storageLoader'); + if (loader) loader.style.display = show ? 'flex' : 'none'; +} + +function renderStorageSummary(summary) { + const el = document.getElementById('storageSummary'); + if (!el || !summary) return; + + el.innerHTML = ` +
+ +
+

${summary.total}

+

Total de Clones

+
+
+
+ +
+

${summary.formattedSize || '0 MB'}

+

Espaço Usado

+
+
+
+ +
+

${summary.byStatus?.active || 0}

+

Ativos

+
+
+
+ +
+

${summary.byStatus?.archived || 0}

+

Arquivados

+
+
+ `; +} + +function renderClonesGrid(clones) { + const container = document.getElementById('storageClonesGrid'); + if (!container) return; + + if (!clones || clones.length === 0) { + container.innerHTML = ` +
+ +

Nenhum clone encontrado

+

Clone um site para começar a gerenciar seu storage

+
`; + return; + } + + container.innerHTML = clones.map(clone => ` +
+
+
+
+ +
+
+
+
+ +
+

${truncateUrl(clone.baseUrl)}

+

${clone.baseUrl}

+
+ ${formatDate(clone.cloneInfo?.clonedAt)} + ${clone.stats?.fileCount || 0} arquivos + ${clone.stats?.formattedSize || '0 B'} +
+
+ +
+ `).join(''); +} + +function showCloneDetails(cloneId) { + const clone = storageState.clones.find(c => c.id === cloneId); + if (!clone) return; + + storageState.selectedClone = clone; + + const modal = createModal('cloneDetailsModal', ` + +
+ + `); + + document.body.appendChild(modal); + showModal('cloneDetailsModal'); +} + +function renderVersionsList(versions) { + if (!versions || versions.length === 0) { + return '

Nenhuma versão salva. Clique em "Criar Snapshot" para salvar o estado atual.

'; + } + + return versions.map(v => ` +
+
+ + ${v.id} + ${formatDate(v.createdAt)} + ${formatBytes(v.size)} +
+
+ + +
+
+ `).join(''); +} + +function showCloneVersions(cloneId) { + const clone = storageState.clones.find(c => c.id === cloneId); + if (clone) { + storageState.selectedClone = clone; + showCloneDetails(cloneId); + } +} + +// ============================================ +// ACTIONS +// ============================================ + +async function updateCloneStatus(cloneId, status) { + try { + await storageApi.updateClone(cloneId, { status }); + showToast('Status atualizado', 'success'); + loadStoragePanel(); + } catch (err) { + showToast('Erro ao atualizar status', 'error'); + } +} + +async function saveCloneTags(cloneId, tagsString) { + const tags = tagsString.split(',').map(t => t.trim()).filter(t => t); + try { + await storageApi.updateClone(cloneId, { tags }); + showToast('Tags salvas', 'success'); + } catch (err) { + showToast('Erro ao salvar tags', 'error'); + } +} + +async function createNewVersion(cloneId) { + try { + showToast('Criando snapshot...', 'info'); + const result = await storageApi.createVersion(cloneId); + if (result.success) { + showToast('Snapshot criado com sucesso', 'success'); + // Refresh the details modal + showCloneDetails(cloneId); + loadStoragePanel(); + } + } catch (err) { + showToast('Erro ao criar snapshot: ' + err.message, 'error'); + } +} + +async function restoreCloneVersion(cloneId, versionId) { + if (!confirm('Restaurar esta versão? O estado atual será salvo como backup.')) return; + + try { + showToast('Restaurando versão...', 'info'); + const result = await storageApi.restoreVersion(cloneId, versionId); + if (result.success) { + showToast('Versão restaurada com sucesso', 'success'); + closeModal('cloneDetailsModal'); + loadStoragePanel(); + } + } catch (err) { + showToast('Erro ao restaurar: ' + err.message, 'error'); + } +} + +async function deleteCloneVersion(cloneId, versionId) { + if (!confirm('Excluir esta versão?')) return; + + try { + const result = await storageApi.deleteVersion(cloneId, versionId); + if (result.success) { + showToast('Versão removida', 'success'); + showCloneDetails(cloneId); + loadStoragePanel(); + } + } catch (err) { + showToast('Erro ao remover versão', 'error'); + } +} + +function confirmDeleteClone(cloneId) { + if (!confirm('Tem certeza que deseja excluir este clone permanentemente?')) return; + + storageApi.deleteClone(cloneId) + .then(result => { + if (result.success) { + showToast('Clone excluído', 'success'); + closeModal('cloneDetailsModal'); + loadStoragePanel(); + } + }) + .catch(err => showToast('Erro ao excluir: ' + err.message, 'error')); +} + +function openClone(cloneId) { + window.open(`/${cloneId}/index.html`, '_blank'); +} + +function cloneSiteAgain(baseUrl) { + // Navigate to new clone page with URL pre-filled + switchAgentTab('designer'); + const urlInput = document.getElementById('designUrl'); + if (urlInput) { + urlInput.value = baseUrl.startsWith('http') ? baseUrl : 'https://' + baseUrl; + } + showToast(`URL carregada: ${baseUrl}`, 'info'); +} + +function showCloneMenu(cloneId) { + // Simple context menu + const clone = storageState.clones.find(c => c.id === cloneId); + if (!clone) return; + + const action = prompt(`Ações para ${clone.baseUrl}: +1. Abrir +2. Detalhes +3. Versões +4. Reclonar +5. Excluir + +Digite o número:`); + + if (action === '1') openClone(cloneId); + else if (action === '2') showCloneDetails(cloneId); + else if (action === '3') showCloneVersions(cloneId); + else if (action === '4') cloneSiteAgain(clone.baseUrl); + else if (action === '5') confirmDeleteClone(cloneId); +} + +// ============================================ +// MODAL HELPERS +// ============================================ + +function createModal(id, content) { + const existing = document.getElementById(id); + if (existing) existing.remove(); + + const modal = document.createElement('div'); + modal.id = id; + modal.className = 'modal'; + modal.innerHTML = ``; + + modal.addEventListener('click', (e) => { + if (e.target.classList.contains('modal')) closeModal(id); + }); + + return modal; +} + +function showModal(id) { + const modal = document.getElementById(id); + if (modal) { + modal.style.display = 'flex'; + setTimeout(() => modal.classList.add('active'), 10); + } +} + +function closeModal(id) { + const modal = document.getElementById(id); + if (modal) { + modal.classList.remove('active'); + setTimeout(() => modal.remove(), 300); + } +} + +// ============================================ +// UTILITIES +// ============================================ + +function truncateUrl(url, maxLen = 30) { + if (!url) return ''; + if (url.length <= maxLen) return url; + return url.substring(0, maxLen) + '...'; +} + +function formatDate(dateStr) { + if (!dateStr) return 'N/A'; + const date = new Date(dateStr); + return date.toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +function formatBytes(bytes) { + if (!bytes || 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 showToast(message, type = 'info') { + // Simple alert for now - could be replaced with a proper toast system + console.log(`[${type}] ${message}`); + // Create a simple toast element + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 24px; + background: ${type === 'success' ? '#43e97b' : type === 'error' ? '#f5576c' : '#667eea'}; + color: white; + border-radius: 8px; + z-index: 10000; + animation: slideIn 0.3s ease; + `; + document.body.appendChild(toast); + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 300); + }, 3000); +} + +// ============================================ +// INIT +// ============================================ + +document.addEventListener('DOMContentLoaded', function() { + // If storage page element exists, load the panel + const storageContent = document.getElementById('storageContent'); + if (storageContent) { + loadStoragePanel(); + } + + // Add storage nav click handler + const storageNav = document.querySelector('[data-page="storage"]'); + if (storageNav) { + storageNav.addEventListener('click', loadStoragePanel); + } + + console.log('✅ Storage Panel initialized'); +}); diff --git a/web-interface/server.js b/web-interface/server.js index ec2ecd3..2400eef 100644 --- a/web-interface/server.js +++ b/web-interface/server.js @@ -7,6 +7,10 @@ const { exec, spawn } = require('child_process'); const app = express(); const PORT = process.env.PORT || 3000; +// Storage manager routes +const storageManager = require('./storage-manager'); +app.use('/api/clones', storageManager); + // Security headers app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); diff --git a/web-interface/storage-manager.js b/web-interface/storage-manager.js new file mode 100644 index 0000000..686ff81 --- /dev/null +++ b/web-interface/storage-manager.js @@ -0,0 +1,421 @@ +/** + * CloneWeb Storage Manager + * Gerencia versões, metadados e storage dos sites clonados + */ + +const fs = require('fs'); +const path = require('path'); +const express = require('express'); +const router = express.Router(); + +const BASE_DIR = '/app/cloned-sites'; +const METADATA_FILE = 'clone-metadata.json'; + +/** + * Get all clones with their metadata + */ +function getAllClones() { + const clones = []; + + if (!fs.existsSync(BASE_DIR)) { + return clones; + } + + const dirs = fs.readdirSync(BASE_DIR).filter(f => { + return fs.statSync(path.join(BASE_DIR, f)).isDirectory(); + }); + + for (const dir of dirs) { + const clonePath = path.join(BASE_DIR, dir); + const metadata = loadMetadata(clonePath); + const cloneInfo = loadCloneInfo(clonePath); + const stats = getCloneStats(clonePath); + + // Extract base URL from directory name + const baseUrl = extractBaseUrl(dir); + + clones.push({ + id: dir, + directory: dir, + baseUrl, + path: clonePath, + metadata: metadata || {}, + cloneInfo: cloneInfo || {}, + stats, + versions: listVersions(clonePath) + }); + } + + // Sort by most recent + clones.sort((a, b) => { + const dateA = new Date(a.cloneInfo.clonedAt || a.metadata.createdAt || 0); + const dateB = new Date(b.cloneInfo.clonedAt || b.metadata.createdAt || 0); + return dateB - dateA; + }); + + return clones; +} + +function loadMetadata(clonePath) { + const metaFile = path.join(clonePath, METADATA_FILE); + if (fs.existsSync(metaFile)) { + try { + return JSON.parse(fs.readFileSync(metaFile, 'utf8')); + } catch (e) { + return null; + } + } + return null; +} + +function loadCloneInfo(clonePath) { + const infoFile = path.join(clonePath, 'clone-info.json'); + if (fs.existsSync(infoFile)) { + try { + return JSON.parse(fs.readFileSync(infoFile, 'utf8')); + } catch (e) { + return null; + } + } + return null; +} + +function getCloneStats(clonePath) { + let totalSize = 0; + let fileCount = 0; + + function countFiles(dir) { + if (!fs.existsSync(dir)) return; + + const items = fs.readdirSync(dir); + for (const item of items) { + const itemPath = path.join(dir, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + // Skip cloned-sites subdirectory + if (!item.includes('cloned-sites')) { + countFiles(itemPath); + } + } else { + totalSize += stat.size; + fileCount++; + } + } + } + + countFiles(clonePath); + + return { + totalSize, + fileCount, + formattedSize: formatBytes(totalSize) + }; +} + +function listVersions(clonePath) { + const versions = []; + const versionsDir = path.join(clonePath, '.versions'); + + if (fs.existsSync(versionsDir)) { + const versionDirs = fs.readdirSync(versionsDir); + for (const vDir of versionDirs) { + const vPath = path.join(versionsDir, vDir); + const stat = fs.statSync(vPath); + versions.push({ + id: vDir, + path: vPath, + createdAt: stat.birthtime, + size: getDirSize(vPath) + }); + } + } + + return versions; +} + +function getDirSize(dirPath) { + let size = 0; + function count(dir) { + if (!fs.existsSync(dir)) return; + for (const item of fs.readdirSync(dir)) { + const stat = fs.statSync(path.join(dir, item)); + if (stat.isDirectory()) { + count(path.join(dir, item)); + } else { + size += stat.size; + } + } + } + count(dirPath); + return size; +} + +function extractBaseUrl(dirName) { + // "example.com_2026-05-17T20-58-30-760Z" -> "example.com" + const parts = dirName.split('_'); + if (parts.length >= 2) { + // Check if last part is a timestamp + const lastPart = parts[parts.length - 1]; + if (/^\d{4}-\d{2}-\d{2}T/.test(lastPart)) { + return parts.slice(0, -1).join('_'); + } + } + return parts[0]; +} + +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]; +} + +// ============================================ +// API ROUTES +// ============================================ + +// GET /api/clones - List all clones +router.get('/', (req, res) => { + try { + const clones = getAllClones(); + + const summary = { + total: clones.length, + totalSize: clones.reduce((sum, c) => sum + c.stats.totalSize, 0), + formattedSize: formatBytes(clones.reduce((sum, c) => sum + c.stats.totalSize, 0)), + byStatus: { + active: clones.filter(c => c.metadata?.status === 'active').length, + archived: clones.filter(c => c.metadata?.status === 'archived').length, + redesign: clones.filter(c => c.metadata?.status === 'redesign').length + } + }; + + res.json({ + success: true, + data: { + clones, + summary + } + }); + } catch (error) { + console.error('Erro ao listar clones:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// GET /api/clones/:id - Get single clone details +router.get('/:id', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + const metadata = loadMetadata(clonePath); + const cloneInfo = loadCloneInfo(clonePath); + const stats = getCloneStats(clonePath); + const versions = listVersions(clonePath); + + // Get list of HTML files + const htmlFiles = findFiles(clonePath, '.html'); + + res.json({ + success: true, + data: { + id: req.params.id, + baseUrl: extractBaseUrl(req.params.id), + path: clonePath, + metadata: metadata || { createdAt: cloneInfo?.clonedAt }, + cloneInfo, + stats, + versions, + htmlFiles + } + }); + } catch (error) { + console.error('Erro ao buscar clone:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// PATCH /api/clones/:id - Update clone metadata +router.patch('/:id', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + const metaFile = path.join(clonePath, METADATA_FILE); + let metadata = loadMetadata(clonePath) || {}; + + // Merge updates + metadata = { + ...metadata, + ...req.body, + updatedAt: new Date().toISOString() + }; + + fs.writeFileSync(metaFile, JSON.stringify(metadata, null, 2)); + + res.json({ success: true, data: { metadata } }); + } catch (error) { + console.error('Erro ao atualizar clone:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// POST /api/clones/:id/versions - Create a version snapshot +router.post('/:id/versions', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + const versionsDir = path.join(clonePath, '.versions'); + const versionId = `v${Date.now()}`; + const versionPath = path.join(versionsDir, versionId); + + fs.mkdirSync(versionPath, { recursive: true }); + + // Copy all files to version (except .versions itself) + copyDirectory(clonePath, versionPath, ['.versions']); + + res.json({ + success: true, + data: { + versionId, + path: versionPath, + createdAt: new Date().toISOString() + } + }); + } catch (error) { + console.error('Erro ao criar versão:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// DELETE /api/clones/:id/versions/:versionId - Delete a version +router.delete('/:id/versions/:versionId', (req, res) => { + try { + const versionPath = path.join(BASE_DIR, req.params.id, '.versions', req.params.versionId); + + if (!fs.existsSync(versionPath)) { + return res.status(404).json({ success: false, error: 'Versão não encontrada' }); + } + + fs.rmSync(versionPath, { recursive: true, force: true }); + + res.json({ success: true, message: 'Versão removida' }); + } catch (error) { + console.error('Erro ao remover versão:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// POST /api/clones/:id/restore/:versionId - Restore from version +router.post('/:id/restore/:versionId', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + const versionPath = path.join(clonePath, '.versions', req.params.versionId); + + if (!fs.existsSync(versionPath)) { + return res.status(404).json({ success: false, error: 'Versão não encontrada' }); + } + + // Backup current state first + const backupId = `pre-restore-${Date.now()}`; + const backupPath = path.join(clonePath, '.versions', backupId); + fs.mkdirSync(backupPath, { recursive: true }); + copyDirectory(clonePath, backupPath, ['.versions']); + + // Create restore point + const restorePointId = `pre-restore-${Date.now()}`; + const restorePointPath = path.join(clonePath, '.versions', restorePointId); + fs.mkdirSync(restorePointPath, { recursive: true }); + copyDirectory(clonePath, restorePointPath, ['.versions']); + + // Copy version files back (overwrite) + copyDirectory(versionPath, clonePath, ['.versions'], true); + + res.json({ + success: true, + data: { + restoredFrom: req.params.versionId, + backupCreated: restorePointId + } + }); + } catch (error) { + console.error('Erro ao restaurar versão:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// DELETE /api/clones/:id - Delete a clone +router.delete('/:id', (req, res) => { + try { + const clonePath = path.join(BASE_DIR, req.params.id); + + if (!fs.existsSync(clonePath)) { + return res.status(404).json({ success: false, error: 'Clone não encontrado' }); + } + + fs.rmSync(clonePath, { recursive: true, force: true }); + + res.json({ success: true, message: 'Clone removido' }); + } catch (error) { + console.error('Erro ao remover clone:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// Helper functions +function findFiles(dir, extension) { + const files = []; + + function search(d) { + if (!fs.existsSync(d)) return; + for (const item of fs.readdirSync(d)) { + const itemPath = path.join(d, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory() && item !== '.versions' && !item.includes('node_modules')) { + search(itemPath); + } else if (item.endsWith(extension)) { + files.push(path.relative(dir, itemPath)); + } + } + } + + search(dir); + return files; +} + +function copyDirectory(src, dest, exclude = [], overwrite = true) { + if (!fs.existsSync(src)) return; + fs.mkdirSync(dest, { recursive: true }); + + for (const item of fs.readdirSync(src)) { + if (exclude.includes(item)) continue; + + const srcPath = path.join(src, item); + const destPath = path.join(dest, item); + const stat = fs.statSync(srcPath); + + if (stat.isDirectory()) { + copyDirectory(srcPath, destPath, exclude, overwrite); + } else { + if (overwrite || !fs.existsSync(destPath)) { + fs.copyFileSync(srcPath, destPath); + } + } + } +} + +module.exports = router; From 207fd1b0a00236278813b7e1d196763b1fef8f43 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 22:30:44 +0000 Subject: [PATCH 05/11] fix: usar /api/health em vez de localhost:3002 (corrige CORS) --- web-interface/public/app-new.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web-interface/public/app-new.js b/web-interface/public/app-new.js index c48c381..a5178df 100644 --- a/web-interface/public/app-new.js +++ b/web-interface/public/app-new.js @@ -100,8 +100,8 @@ async function checkServiceStatus() { const statusIndicator = document.getElementById('serviceStatus'); try { - // Verificar o serviço WGET na porta 3002 (não 3001) - const response = await fetch('http://localhost:3002/health', { + // Verificar o serviço WGET via proxy do servidor (mesma origem) + const response = await fetch('/api/health', { method: 'GET' }); @@ -582,18 +582,18 @@ function saveSettings() { // Utility Functions function openClonedSite(directory) { // Try to open via server endpoint - fetch('http://localhost:8080', { + fetch('/api/health', { method: 'GET' }).then(response => { if (response.ok) { - // Server is running, open the site - const siteName = directory.split('\\').pop(); - window.open(`http://localhost:8080/${siteName}/`, '_blank'); + // Server is running, open the site via same origin + const siteName = directory.split('/').pop(); + window.open(`/${siteName}/index.html`, '_blank'); } else { - showToast('Servidor HTTP não está rodando. Inicie o servidor na porta 8080.', 'error'); + showToast('Servidor não está respondendo.', 'error'); } }).catch(error => { - showToast('Servidor HTTP não está rodando. Inicie o servidor na porta 8080.', 'error'); + showToast('Servidor não está respondendo.', 'error'); }); } From c4111f9e86bef4a402592732a3b78ff08937ecb3 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 22:40:01 +0000 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20null=20checks=20no=20formul=C3=A1r?= =?UTF-8?q?io=20de=20clone,=20usar=20wget=20como=20m=C3=A9todo=20padr?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-interface/public/app-new.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/web-interface/public/app-new.js b/web-interface/public/app-new.js index a5178df..db695f6 100644 --- a/web-interface/public/app-new.js +++ b/web-interface/public/app-new.js @@ -200,13 +200,26 @@ function setupForms() { async function handleCloneSubmit(e) { e.preventDefault(); - const url = document.getElementById('cloneUrl').value.trim(); - const depth = parseInt(document.getElementById('depthLevel').value); - const includeImages = document.getElementById('includeImages').checked; - const includeCSS = document.getElementById('includeCSS').checked; - const includeJS = document.getElementById('includeJS').checked; - const convertLinks = document.getElementById('convertLinks').checked; - const cloneMethod = document.getElementById('cloneMethod').value; + 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'); From 511ee62a73090c6900c53b1e7ec84b42623d4c1b Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 22:41:24 +0000 Subject: [PATCH 07/11] fix: atualizar Font Awesome 6.4.0 -> 6.5.2 --- web-interface/public/index-new.html | 38 ++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/web-interface/public/index-new.html b/web-interface/public/index-new.html index 6f3cf02..b4611e3 100644 --- a/web-interface/public/index-new.html +++ b/web-interface/public/index-new.html @@ -5,8 +5,9 @@ CloneWeb Pro - Clonagem Profissional de Sites - + + @@ -34,6 +35,11 @@ Agentes AI + + + Storage + + Configurações @@ -297,6 +303,36 @@ + + +
+
+

💾 Gestão de Storage

+

Gerencie todos os sites clonados, versões e metadata

+
+ +
+ +
+ +
+ + +
+ + + +
+ +
+
+ +
From bd1f284621fbfcfb4c46993dc36ed9144d86727a Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 17 May 2026 23:43:45 +0000 Subject: [PATCH 08/11] feat: auto-sync clones para /root/Desktop/Clonados apos cada clone --- web-interface/server.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web-interface/server.js b/web-interface/server.js index 2400eef..61a2b65 100644 --- a/web-interface/server.js +++ b/web-interface/server.js @@ -75,6 +75,19 @@ app.post('/api/wget-clone', async (req, res) => { }); console.log('✅ Wget clone completed successfully'); + + // Sync to host /root/Desktop/Clonados + if (response.data && response.data.success && response.data.directory) { + const { exec } = require('child_process'); + const containerId = require('os').hostname(); + const cloneDir = response.data.directory.split('/').pop(); + const hostDir = '/root/Desktop/Clonados'; + exec(`docker cp b38l2zsxjv08xkxg4gd5rl9r-210808689309:${response.data.directory} ${hostDir}/ 2>/dev/null`, (err) => { + if (err) console.log('⚠️ Sync to host failed:', err.message); + else console.log('✅ Clone synced to host:', cloneDir); + }); + } + res.json(response.data); } catch (error) { console.error('❌ Wget clone error:', error.message); From 67778ab752ef46198bc3a9e5d2d24ef8614def6a Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 18 May 2026 00:06:03 +0000 Subject: [PATCH 09/11] chore: add Dockerfile for build fallback --- Dockerfile | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6e81e77 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# CloneWeb - Web Interface Dockerfile +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY web-interface/package*.json ./ + +# Install dependencies +RUN npm install --production + +# Copy application files +COPY web-interface/ . + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 + +# Start server +CMD ["node", "server.js"] \ No newline at end of file From f2ecf4fad1b33d33e6ec85f132f15c0cca8e258a Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 18 May 2026 00:14:56 +0000 Subject: [PATCH 10/11] fix: Dockerfile com crawler wget-cloner na porta 3002 --- Dockerfile | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6e81e77..5f4101c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,28 @@ -# CloneWeb - Web Interface Dockerfile +# CloneWeb - Web Interface + Crawler Dockerfile FROM node:20-alpine WORKDIR /app -# Copy package files -COPY web-interface/package*.json ./ +# Install wget for crawler + bash for scripts +RUN apk add --no-cache wget bash -# Install dependencies +# --- Crawler service (port 3002) --- +COPY simple-crawler/package*.json /app/simple-crawler/ +WORKDIR /app/simple-crawler RUN npm install --production -# Copy application files -COPY web-interface/ . +COPY simple-crawler/ /app/simple-crawler/ + +# --- Web interface service (port 3000) --- +WORKDIR /app +COPY web-interface/package*.json /app/web-interface/ +WORKDIR /app/web-interface +RUN npm install --production + +COPY web-interface/ /app/web-interface/ + +# --- Storage dir for clones --- +RUN mkdir -p /app/cloned-sites # Expose port EXPOSE 3000 @@ -19,5 +31,5 @@ EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 -# Start server -CMD ["node", "server.js"] \ No newline at end of file +# Start crawler in background (port 3002), then web server (port 3000) +CMD bash -c "node /app/simple-crawler/wget-cloner.js > /tmp/crawler.log 2>&1 & node server.js" \ No newline at end of file From de1d32f59341345c5809b0d721acd26be24fd334 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 18 May 2026 00:25:23 +0000 Subject: [PATCH 11/11] fix: criar symlink /bin/wget para compatibilidade --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5f4101c..21f44c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM node:20-alpine WORKDIR /app # Install wget for crawler + bash for scripts -RUN apk add --no-cache wget bash +RUN apk add --no-cache wget bash && ln -s /usr/bin/wget /bin/wget # --- Crawler service (port 3002) --- COPY simple-crawler/package*.json /app/simple-crawler/