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;