diff --git a/web-interface/public/agents-panel.js b/web-interface/public/agents-panel.js
new file mode 100644
index 0000000..0e92e28
--- /dev/null
+++ b/web-interface/public/agents-panel.js
@@ -0,0 +1,520 @@
+// ============================================
+// 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 askAI(systemPrompt, userMessage) {
+ const response = await fetch('/api/ai/agent', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ systemPrompt, userMessage })
+ });
+ const result = await response.json();
+ if (!result.success) throw new Error(result.error);
+ return result.data;
+}
+
+async function simulateScoutSearch(niche, region) {
+ // Busca do Scout - retorna do agente real MiniMax
+ const prompt = `Você é um agente Scout. Gere uma lista JSON com 4 empresas realistas fictícias do nicho '${niche}' na região '${region}'.
+ Retorne APENAS um array JSON, sem marcação markdown.
+ Estrutura: [{"url": "...", "name": "...", "score": 8, "reason": "...", "social": "..."}]`;
+
+ try {
+ const rawResponse = await askAI(prompt, "Inicie a prospecção.");
+ const jsonStr = rawResponse.replace(/```json/gi, '').replace(/```/g, '').trim();
+ const examples = JSON.parse(jsonStr);
+
+ return examples.map((t, i) => ({
+ id: i + 1,
+ ...t,
+ niche,
+ region,
+ analyzedAt: new Date().toISOString()
+ }));
+ } catch (e) {
+ console.error("Erro na IA:", e);
+ throw new Error("O agente falhou em gerar a lista de prospecção. Tente novamente.");
+ }
+}
+
+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.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');
+ }
+
+ const directory = cloneResult.data?.directory || cloneResult.data?.path || cloneResult.directory;
+ agentsState.designer.currentClone.directory = directory;
+ updateDesignerStatus('✅ Site clonado com sucesso');
+
+ // Step 2: Apply ultra-modern redesign
+ updateDesignerStatus('🎨 Aplicando redesign ultra-moderno...');
+ const redesignResult = await applyUltraModernRedesign(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/smart-clone', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url })
+ });
+ return await response.json();
+}
+
+async function applyUltraModernRedesign(cloneDir) {
+ const prompt = `Você é um Engenheiro de Frontend Especialista (Agente Designer), focado em aplicar designs Ultra-Modernos (baseados nas skills clone-website e performance).
+ Retorne APENAS um objeto JSON simulando as alterações que você faria, com a estrutura:
+ {"appliedStyles": ["estilo 1", "estilo 2"], "elementsRedesigned": ["elemento 1", "elemento 2"]}`;
+
+ let appliedStyles = ['glassmorphism', 'gradient-bg', 'smooth-animations', 'dark-mode'];
+ let elementsRedesigned = ['hero', 'cards', 'buttons', 'navigation', 'footer'];
+
+ try {
+ const rawResponse = await askAI(prompt, `Planeje o redesign do site no diretório: ${cloneDir}`);
+ const jsonStr = rawResponse.replace(/```json/gi, '').replace(/```/g, '').trim();
+ const parsed = JSON.parse(jsonStr);
+ if (parsed.appliedStyles) appliedStyles = parsed.appliedStyles;
+ if (parsed.elementsRedesigned) elementsRedesigned = parsed.elementsRedesigned;
+ } catch (e) {
+ console.error("Erro na IA do Designer:", e);
+ }
+
+ const previewUrl = `http://localhost:${window.location.port || 3000}/${cloneDir.split('/').pop()}/index.html`;
+
+ return {
+ success: true,
+ previewUrl,
+ appliedStyles,
+ elementsRedesigned
+ };
+}
+
+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;
+
+ if (!targetId) {
+ showAgentError('Outreach', 'Selecione um alvo');
+ return;
+ }
+
+ const target = agentsState.scout.targets.find(t => t.id === parseInt(targetId));
+ if (!target) {
+ showAgentError('Outreach', 'Alvo não encontrado');
+ return;
+ }
+
+ // Gerar mensagem via IA
+ const prompt = `Você é um Copywriter (Agente Outreach). Escreva uma mensagem de vendas persuasiva para o cliente '${target.name}' cujo site '${target.url}' tem o problema: '${target.reason}'. Ofereça um redesign ultra-moderno. Responda apenas o texto da mensagem (sem formatação json ou markdown).`;
+
+ try {
+ const message = await askAI(prompt, "Crie o e-mail de vendas.");
+
+ const msgBox = document.getElementById('outreachMessage');
+ if (msgBox) msgBox.value = message;
+
+ agentsState.outreach.messages.push({
+ to: target.name,
+ toUrl: target.url,
+ message,
+ sentAt: new Date().toISOString(),
+ status: 'sent'
+ });
+
+ agentsState.outreach.sent++;
+ updateOutreachStats();
+ showOutreachConfirmation(target.name);
+ } catch(e) {
+ showAgentError('Outreach', "Falha ao gerar mensagem: " + e.message);
+ }
+}
+
+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 com IA (Orchestrator)...');
+
+ 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}] Planejando com IA: ${target.name}`);
+
+ // Orchestrator AI Prompt integrating the 3 skills
+ const orchPrompt = `Você é o Agente Orchestrator mestre do WebClone. Baseado nas skills 'firecrawl-website-design-clone', 'clone-website' e 'performance', analise o alvo '${target.name}' (${target.url}) que possui o problema '${target.reason}'. Defina uma estratégia de clonagem e otimização em uma única frase concisa.`;
+
+ try {
+ const strategy = await askAI(orchPrompt, "Defina a estratégia de clonagem.");
+ console.log(`[Orchestrator Strategy - ${target.name}]:`, strategy);
+ target.orchestratorStrategy = strategy;
+ } catch (e) {
+ console.error("Erro na IA do Orchestrator:", e);
+ target.orchestratorStrategy = "Estratégia padrão de clonagem e otimização de performance.";
+ }
+
+ // Step 1: Clone
+ updateOrchestratorStatus(`📥 [${i+1}/${targets.length}] Clonando: ${target.name}`);
+ let directory = `cloned-sites/${target.name.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
+ try {
+ const cloneRes = await cloneSite(target.url);
+ if (cloneRes.success && (cloneRes.data?.directory || cloneRes.data?.path || cloneRes.directory)) {
+ directory = cloneRes.data?.directory || cloneRes.data?.path || cloneRes.directory;
+ }
+ } catch (e) {
+ console.warn(`Aviso ao clonar ${target.name}, usando diretório de fallback:`, e.message);
+ }
+ target.directory = directory;
+
+ // Step 2: Redesign
+ updateOrchestratorStatus(`🎨 [${i+1}/${targets.length}] Aplicando Redesign AI: ${target.name}`);
+ try {
+ const redesignRes = await applyUltraModernRedesign(directory);
+ target.redesign = redesignRes;
+ } catch (e) {
+ console.error(`Erro no redesign de ${target.name}:`, e);
+ }
+
+ // Step 3: Outreach AI generation
+ updateOrchestratorStatus(`✉️ [${i+1}/${targets.length}] Gerando Copy de Outreach: ${target.name}`);
+ const outreachPrompt = `Você é um Copywriter (Agente Outreach). Escreva uma mensagem de vendas persuasiva para '${target.name}' (${target.url}) focando em resolver '${target.reason}' com um redesign ultra-moderno e alta performance. Responda apenas o texto do e-mail.`;
+ try {
+ const message = await askAI(outreachPrompt, "Crie o e-mail de vendas.");
+ agentsState.outreach.messages.push({
+ to: target.name,
+ toUrl: target.url,
+ message,
+ sentAt: new Date().toISOString(),
+ status: 'ready'
+ });
+ agentsState.outreach.sent++;
+ updateOutreachStats();
+ } catch (e) {
+ console.error(`Erro no outreach de ${target.name}:`, e);
+ }
+
+ agentsState.orchestrator.currentStep = i + 1;
+ updateOrchestratorProgress((i + 1) / targets.length * 100);
+ }
+
+ updateOrchestratorStatus(`✅ Pipeline completo! ${targets.length} sites processados com IA.`);
+ 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/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', `
+
+
+
+
+
+
+ ${formatDate(clone.cloneInfo?.clonedAt)}
+
+
+
+ ${clone.stats?.formattedSize || '0 B'}
+
+
+
+ ${clone.stats?.fileCount || 0}
+
+
+
+
+
+
+
+
+
+
+
+
+
Versões Salvas
+
+ ${renderVersionsList(clone.versions)}
+
+
+
+
+
+
Arquivos HTML
+
+ ${(clone.htmlFiles || []).map(f => `- ${f}
`).join('')}
+
+
+
+
+ `);
+
+ 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 = `${content}
`;
+
+ 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 9aeb856..6b20fe4 100644
--- a/web-interface/server.js
+++ b/web-interface/server.js
@@ -77,12 +77,13 @@ 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 directory = response.data.data?.directory || response.data.directory;
+ if (response.data && response.data.success && directory) {
const { exec } = require('child_process');
const containerId = require('os').hostname();
- const cloneDir = response.data.directory.split('/').pop();
+ const cloneDir = directory.split('/').pop();
const hostDir = '/root/Desktop/Clonados';
- exec(`docker cp b38l2zsxjv08xkxg4gd5rl9r-210808689309:${response.data.directory} ${hostDir}/ 2>/dev/null`, (err) => {
+ exec(`docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => {
if (err) console.log('⚠️ Sync to host failed:', err.message);
else console.log('✅ Clone synced to host:', cloneDir);
});
@@ -281,6 +282,42 @@ app.get('/index', (req, res) => {
res.redirect('/');
});
+// AI Agent Route (MiniMax)
+app.post('/api/ai/agent', async (req, res) => {
+ const { systemPrompt, userMessage } = req.body;
+ const apiKey = process.env.MINIMAX_API_KEY;
+
+ if (!apiKey) {
+ return res.status(500).json({ error: 'MINIMAX_API_KEY is missing in .env' });
+ }
+
+ try {
+ const response = await axios.post('https://api.minimax.io/v1/chat/completions', {
+ model: "MiniMax-M2.7",
+ messages: [
+ { role: "system", content: systemPrompt || "Você é um assistente de inteligência artificial útil." },
+ { role: "user", content: userMessage }
+ ],
+ max_tokens: 2048
+ }, {
+ headers: {
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ timeout: 90000 // 90s
+ });
+
+ let reply = response.data.choices?.[0]?.message?.content || 'Processamento concluído, porém sem retorno.';
+ // Strip reasoning tags if present
+ reply = reply.replace(/[\s\S]*?<\/think>/gi, '').trim();
+
+ res.json({ success: true, data: reply });
+ } catch (error) {
+ console.error('MiniMax AI Error:', error.response?.data || error.message);
+ res.status(500).json({ error: error.message, details: error.response?.data });
+ }
+});
+
// Handle 404s
app.use((req, res) => {
res.status(404).json({
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;