Files
webclone/web-interface/server.js
T

656 lines
27 KiB
JavaScript

const express = require('express');
const path = require('path');
const cors = require('cors');
const axios = require('axios');
const { exec, spawn } = require('child_process');
const fs = require('fs');
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');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
// Middleware
app.use(cors());
app.use(express.json());
// Helper para parsear cookies
function parseCookies(cookieHeader) {
const list = {};
if (!cookieHeader) return list;
cookieHeader.split(';').forEach(cookie => {
let [name, ...rest] = cookie.split('=');
name = name?.trim();
if (!name) return;
const value = rest.join('=').trim();
if (!value) return;
list[name] = decodeURIComponent(value);
});
return list;
}
// Rota para ativar o Virtual Root Preview (define o cookie e redireciona para a raiz)
app.get('/api/preview/:folder', (req, res) => {
const folderName = req.params.folder;
res.setHeader('Set-Cookie', `clone_preview_target=${encodeURIComponent(folderName)}; Path=/; HttpOnly; SameSite=Lax`);
res.redirect('/');
});
// Rota para sair do Virtual Root Preview (limpa o cookie e volta ao dashboard)
app.get('/api/exit-preview', (req, res) => {
res.setHeader('Set-Cookie', 'clone_preview_target=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
res.redirect('/');
});
// Middleware de Virtual Root Proxy (serve o site clonado diretamente na raiz / quando o cookie está ativo)
app.use((req, res, next) => {
const cookies = parseCookies(req.headers.cookie);
if (cookies.clone_preview_target && !req.path.startsWith('/api') && req.path !== '/old') {
const folderName = cookies.clone_preview_target;
const folderPath = path.join(__dirname, '..', 'cloned-sites', folderName);
if (fs.existsSync(folderPath)) {
let targetFile = req.path;
if (targetFile === '/') targetFile = '/index.html';
// Se o arquivo solicitado existir dentro da pasta do clone
const fullPath = path.join(folderPath, targetFile);
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
// Se for HTML, injeta a barra de preview flutuante
if (targetFile.endsWith('.html')) {
let html = fs.readFileSync(fullPath, 'utf8');
const previewBar = `
<div id="__cloneweb_preview_bar" style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1f2937; color: white; padding: 12px 24px; border-radius: 50px; box-shadow: 0 10px 25px rgba(0,0,0,0.3); display: flex; align-items: center; gap: 16px; z-index: 2147483647; font-family: 'Inter', sans-serif; font-size: 14px;">
<span style="display: flex; align-items: center; gap: 8px;"><i class="fas fa-eye" style="color: #10b981;"></i> Live Preview: <strong style="color: #60a5fa;">${folderName.split('_')[0]}</strong></span>
<a href="/api/exit-preview" style="background: #ef4444; color: white; padding: 6px 16px; border-radius: 30px; text-decoration: none; font-weight: 600; transition: all 0.2s; box-shadow: 0 4px 12px rgba(239,68,68,0.3); font-size: 13px;">Sair do Preview</a>
</div>
`;
if (html.includes('</body>')) {
html = html.replace('</body>', previewBar + '</body>');
} else {
html += previewBar;
}
res.setHeader('Content-Type', 'text/html');
return res.send(html);
}
// Para outros arquivos (CSS, JS, imagens), serve diretamente com o MIME type correto
if (targetFile.endsWith('.css')) res.setHeader('Content-Type', 'text/css');
if (targetFile.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript');
return res.sendFile(fullPath);
} else {
// Fallback de SPA: se a rota virtual não existir como arquivo no disco, serve o index.html do clone!!!
const indexPath = path.join(folderPath, 'index.html');
if (fs.existsSync(indexPath)) {
let html = fs.readFileSync(indexPath, 'utf8');
const previewBar = `
<div id="__cloneweb_preview_bar" style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1f2937; color: white; padding: 12px 24px; border-radius: 50px; box-shadow: 0 10px 25px rgba(0,0,0,0.3); display: flex; align-items: center; gap: 16px; z-index: 2147483647; font-family: 'Inter', sans-serif; font-size: 14px;">
<span style="display: flex; align-items: center; gap: 8px;"><i class="fas fa-eye" style="color: #10b981;"></i> Live Preview: <strong style="color: #60a5fa;">${folderName.split('_')[0]}</strong></span>
<a href="/api/exit-preview" style="background: #ef4444; color: white; padding: 6px 16px; border-radius: 30px; text-decoration: none; font-weight: 600; transition: all 0.2s; box-shadow: 0 4px 12px rgba(239,68,68,0.3); font-size: 13px;">Sair do Preview</a>
</div>
`;
if (html.includes('</body>')) html = html.replace('</body>', previewBar + '</body>');
else html += previewBar;
res.setHeader('Content-Type', 'text/html');
return res.send(html);
}
}
}
}
next();
});
// Serve static files with proper MIME types
app.use(express.static(path.join(__dirname, 'public'), {
setHeaders: (res, path) => {
if (path.endsWith('.css')) res.setHeader('Content-Type', 'text/css');
if (path.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript');
if (path.endsWith('.html')) res.setHeader('Content-Type', 'text/html');
}
}));
// Middleware para injetar fix de roteador em sites clonados (SPAs como React, Vue, Angular, Vite) e reescrever JS
app.use((req, res, next) => {
if (!req.path.startsWith('/api') && req.path !== '/' && req.path !== '/old') {
let urlPath = req.path;
// Remover prefixo /cloned-sites se existir para buscar corretamente no disco
if (urlPath.startsWith('/cloned-sites/')) {
urlPath = urlPath.replace('/cloned-sites/', '/');
}
if (!urlPath.match(/\.[a-zA-Z0-9]+$/)) {
if (!urlPath.endsWith('/')) urlPath += '/';
urlPath += 'index.html';
}
// Interceptação de arquivos HTML (Camada 1 - DOM Mock Universal)
if (urlPath.endsWith('.html')) {
const filePath = path.join(__dirname, '..', 'cloned-sites', urlPath);
if (fs.existsSync(filePath)) {
try {
let html = fs.readFileSync(filePath, 'utf8');
if (!html.includes('__CLONEWEB_ROUTER_FIX__')) {
const injectScript = `
<script id="__CLONEWEB_ROUTER_FIX__">
(function() {
try {
window.__mockLocationPathname = '/';
window.__mockLocationHref = window.location.protocol + '//' + window.location.host + '/';
let targetObj = window.location;
let proto = Object.getPrototypeOf(targetObj);
let pathDesc = null;
let hrefDesc = null;
while (proto && (!pathDesc || !hrefDesc)) {
if (!pathDesc) pathDesc = Object.getOwnPropertyDescriptor(proto, 'pathname');
if (!hrefDesc) hrefDesc = Object.getOwnPropertyDescriptor(proto, 'href');
proto = Object.getPrototypeOf(proto);
}
if (!pathDesc) pathDesc = Object.getOwnPropertyDescriptor(targetObj, 'pathname');
if (!hrefDesc) hrefDesc = Object.getOwnPropertyDescriptor(targetObj, 'href');
if (pathDesc && pathDesc.configurable) {
const origGet = pathDesc.get;
Object.defineProperty(proto || targetObj, 'pathname', {
get: function() {
const p = origGet ? origGet.call(this) : this.pathname;
if (p !== '/' && p !== '/index.html' && (p.includes('_202') || p.includes('/cloned-sites/'))) {
return '/';
}
return p;
},
set: pathDesc.set || function(v) { window.location.assign(v); }
});
}
if (hrefDesc && hrefDesc.configurable) {
const origHrefGet = hrefDesc.get;
Object.defineProperty(proto || targetObj, 'href', {
get: function() {
const h = origHrefGet ? origHrefGet.call(this) : this.href;
if (h.includes('_202') || h.includes('/cloned-sites/')) {
const u = new URL(h);
return u.protocol + '//' + u.host + '/';
}
return h;
},
set: hrefDesc.set || function(v) { window.location.assign(v); }
});
}
const OriginalURL = window.URL;
window.URL = function(url, base) {
if (typeof url === 'string' && (url.includes('_202') || url.includes('/cloned-sites/'))) {
url = url.replace(/\\/cloned-sites\\/[^/]+/gi, '').replace(/\\/[^/]+_202[0-9-_T:.]+(\\/index\\.html)?/gi, '/');
}
return new OriginalURL(url, base);
};
window.URL.prototype = OriginalURL.prototype;
Object.assign(window.URL, OriginalURL);
console.log('🚀 [CloneWeb] Ultimate SPA Router fix initialized successfully (Cross-browser Gecko/Blink support).');
} catch(e) {
console.error('⚠️ [CloneWeb] DOM mock fallback active. Using __mockLocationPathname. Error:', e);
}
})();
</script>
`;
if (html.includes('<head>')) {
html = html.replace('<head>', '<head>' + injectScript);
} else {
html = injectScript + html;
}
}
res.setHeader('Content-Type', 'text/html');
return res.send(html);
} catch (err) {
console.error('Erro ao injetar fix no HTML do clone:', err);
}
}
}
// Interceptação de arquivos JS de sites clonados (Camada 2 - JS Rewriter Supremo)
if (urlPath.endsWith('.js') && (urlPath.includes('_202') || urlPath.includes('/cloned-sites/'))) {
const filePath = path.join(__dirname, '..', 'cloned-sites', urlPath);
if (fs.existsSync(filePath)) {
try {
let jsContent = fs.readFileSync(filePath, 'utf8');
// Substitui window.location.pathname de forma segura
jsContent = jsContent.replace(/\bwindow\.location\.pathname\b/g, '(window.__mockLocationPathname || window.location.pathname)');
// Substitui location.pathname isolado (evita quebrar e.location.pathname ou history.location.pathname)
jsContent = jsContent.replace(/(?<!\w\.)\blocation\.pathname\b/g, '(window.__mockLocationPathname || window.location.pathname)');
// Substitui window.location.href de forma segura
jsContent = jsContent.replace(/\bwindow\.location\.href\b/g, '(window.__mockLocationHref || window.location.href)');
// Substitui location.href isolado (evita quebrar e.location.href ou history.location.href)
jsContent = jsContent.replace(/(?<!\w\.)\blocation\.href\b/g, '(window.__mockLocationHref || window.location.href)');
res.setHeader('Content-Type', 'application/javascript');
return res.send(jsContent);
} catch (err) {
console.error('Erro ao reescrever JS do clone:', err);
}
}
}
}
next();
});
// Serve cloned sites directory
app.use(express.static(path.join(__dirname, '..', 'cloned-sites'), {
setHeaders: (res, filePath) => {
if (filePath.endsWith('.css')) res.setHeader('Content-Type', 'text/css');
if (filePath.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript');
}
}));
// Proxy para o crawler service - SEM TIMEOUT
app.post('/api/crawl', async (req, res) => {
try {
console.log('🚀 Proxying crawl request to crawler service...');
console.log('⏳ Clonagem pode demorar vários minutos para sites grandes...');
// SEM TIMEOUT - permite clonagens longas
const response = await axios.post('http://localhost:3001/crawl', req.body, {
timeout: 0 // Sem limite de tempo
});
console.log('✅ Crawl completed successfully');
res.json(response.data);
} catch (error) {
console.error('❌ Crawl error:', error.message);
res.status(500).json({
success: false,
error: error.response?.data?.error || error.message
});
}
});
// Proxy para o wget cloner - SEM TIMEOUT para clonagens grandes
app.post('/api/wget-clone', async (req, res) => {
try {
console.log('🔧 Proxying wget clone request...');
console.log('⏳ Clonagem pode demorar vários minutos para sites grandes...');
// SEM TIMEOUT - permite clonagens que demoram mais de 10 minutos
const response = await axios.post('http://localhost:3002/wget-clone', req.body, {
timeout: 0 // Sem limite de tempo
});
console.log('✅ Wget clone completed successfully');
// Sync to host /root/Desktop/Clonados
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 = directory.split('/').pop();
const hostDir = '/root/Desktop/Clonados';
exec(`mkdir -p ${hostDir} && cp -r ${directory} ${hostDir}/ 2>/dev/null || 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 Desktop:', cloneDir);
});
}
res.json(response.data);
} catch (error) {
console.error('❌ Wget clone error:', error.message);
res.status(500).json({
success: false,
error: error.response?.data?.error || error.message
});
}
});
// Smart Clone Endpoint (Local Spawn)
app.post('/api/smart-clone', (req, res) => {
const { url } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL is required' });
}
console.log('🚀 Starting Smart Clone for:', url);
console.log('⏳ This uses Puppeteer and may take a while...');
// Spawn the smart cloner process
const scriptPath = path.join(__dirname, '..', 'simple-crawler', 'smart-cloner.js');
console.log(`[SmartClone] Executing: node "${scriptPath}" --url="${url}"`);
const child = spawn('node', [scriptPath, `--url=${url}`], {
cwd: path.join(__dirname, '..'),
shell: true
});
let output = '';
let errorOutput = '';
child.stdout.on('data', (data) => {
const text = data.toString();
output += text;
console.log(`[SmartClone STDOUT] ${text.trim()}`);
});
child.stderr.on('data', (data) => {
const text = data.toString();
errorOutput += text;
console.error(`[SmartClone STDERR] ${text.trim()}`);
});
child.on('error', (err) => {
console.error('[SmartClone SPAWN ERROR]', err);
if (!res.headersSent) {
res.status(500).json({ success: false, error: 'Failed to start cloning process', details: err.message });
}
});
child.on('close', (code) => {
console.log(`[SmartClone] Process exited with code ${code}`);
if (res.headersSent) return;
try {
// Find the last JSON object in output
const matches = output.match(/\{[\s\S]*\}/g);
if (matches && matches.length > 0) {
const lastMatch = matches[matches.length - 1];
const result = JSON.parse(lastMatch);
// Sync to host /root/Desktop/Clonados
const directory = result.data?.directory || result.directory || result.path;
if (result.success && directory) {
const { exec } = require('child_process');
const containerId = require('os').hostname();
const cloneDir = directory.split('/').pop();
const hostDir = '/root/Desktop/Clonados';
exec(`mkdir -p ${hostDir} && cp -r ${directory} ${hostDir}/ 2>/dev/null || docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => {
if (err) console.log('⚠️ SmartClone sync to host failed:', err.message);
else console.log('✅ SmartClone synced to host Desktop:', cloneDir);
});
}
res.json({
success: result.success,
data: result,
error: result.error
});
} else {
throw new Error('No valid JSON output found from cloner');
}
} catch (e) {
console.error('[SmartClone PARSE ERROR]', e.message);
if (code === 0) {
res.json({ success: true, message: 'Process finished but output parsing failed', fullOutput: output });
} else {
res.status(500).json({ success: false, error: 'Clone failed or output was invalid', details: errorOutput });
}
}
});
});
// Health check do wget cloner (porta 3002)
app.get('/api/health', async (req, res) => {
try {
const response = await axios.get('http://localhost:3002/health', {
timeout: 5000
});
res.json(response.data);
} catch (error) {
res.status(500).json({
status: 'error',
message: 'WGET Cloner service unavailable'
});
}
});
// Rota de visualização de arquivos da pasta clonada (Gerenciador de Arquivos Web Embutido)
app.get('/api/browse/:folder', (req, res) => {
try {
const folderName = req.params.folder;
const folderPath = path.join(__dirname, '..', 'cloned-sites', folderName);
if (!fs.existsSync(folderPath)) {
return res.status(404).send('<h1>Pasta não encontrada</h1><p>A pasta solicitada não existe no servidor.</p>');
}
const items = fs.readdirSync(folderPath);
let filesHtml = '';
items.forEach(item => {
const itemPath = path.join(folderPath, item);
const stat = fs.statSync(itemPath);
const isDir = stat.isDirectory();
const size = isDir ? '-' : (stat.size / 1024).toFixed(2) + ' KB';
const date = new Date(stat.mtime).toLocaleString('pt-BR');
const icon = isDir ? 'fa-folder' : (item.endsWith('.html') ? 'fa-file-code' : (item.endsWith('.json') ? 'fa-file-alt' : 'fa-file'));
const link = isDir ? `#` : `/${folderName}/${item}`;
filesHtml += `
<tr>
<td><i class="fas ${icon}" style="color: #4f46e5; margin-right: 10px;"></i> <a href="${link}" target="_blank" style="color: #1f2937; text-decoration: none; font-weight: 500;">${item}</a></td>
<td style="color: #6b7280;">${size}</td>
<td style="color: #6b7280;">${date}</td>
<td>
${!isDir ? `<a href="${link}" target="_blank" class="btn btn-secondary btn-sm" style="background: #e0e7ff; color: #4f46e5; padding: 5px 10px; border-radius: 6px; text-decoration: none; font-size: 14px;">Visualizar</a>` : ''}
${!isDir ? `<a href="${link}" download="${item}" class="btn btn-secondary btn-sm" style="background: #fee2e2; color: #ef4444; padding: 5px 10px; border-radius: 6px; text-decoration: none; font-size: 14px; margin-left: 5px;">Baixar</a>` : ''}
</td>
</tr>
`;
});
const html = `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arquivos Clonados - ${folderName}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #f3f4f6; margin: 0; padding: 40px 20px; color: #1f2937; }
.container { max-width: 1000px; margin: 0 auto; background: white; border-radius: 16px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); overflow: hidden; padding: 30px; }
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e5e7eb; padding-bottom: 20px; margin-bottom: 25px; }
.header h1 { font-size: 24px; margin: 0; color: #111827; display: flex; align-items: center; gap: 12px; }
.actions { display: flex; gap: 12px; }
.btn { padding: 10px 20px; border-radius: 8px; font-weight: 600; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 14px; transition: all 0.2s; }
.btn-primary { background: #4f46e5; color: white; }
.btn-primary:hover { background: #4338ca; }
.btn-secondary { background: #f3f4f6; color: #374151; }
.btn-secondary:hover { background: #e5e7eb; }
table { width: 100%; border-collapse: collapse; text-align: left; }
th { padding: 16px; background: #f9fafb; font-weight: 600; color: #374151; border-bottom: 1px solid #e5e7eb; font-size: 14px; }
td { padding: 16px; border-bottom: 1px solid #e5e7eb; font-size: 15px; }
tr:hover { background: #f9fafb; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-folder-open" style="color: #4f46e5;"></i> ${folderName}</h1>
<div class="actions">
<a href="/${folderName}/index.html" target="_blank" class="btn btn-primary"><i class="fas fa-external-link-alt"></i> Abrir Site Clonado</a>
<a href="/" class="btn btn-secondary"><i class="fas fa-arrow-left"></i> Voltar ao Dashboard</a>
</div>
</div>
<table>
<thead>
<tr>
<th>Nome do Ficheiro</th>
<th>Tamanho</th>
<th>Modificado em</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
${filesHtml}
</tbody>
</table>
</div>
</body>
</html>
`;
res.send(html);
} catch (error) {
console.error('Erro ao listar pasta:', error);
res.status(500).send('<h1>Erro interno do servidor</h1>');
}
});
// Abrir pasta do clone (mantém compatibilidade legada e retorna a URL do Web Browse)
app.post('/api/open-folder', (req, res) => {
try {
const { path: folderPath } = req.body;
if (!folderPath) {
return res.status(400).json({ error: 'Caminho da pasta é obrigatório' });
}
const folderName = path.basename(folderPath);
const browseUrl = `/api/browse/${folderName}`;
res.json({ success: true, message: 'Pasta acessível via Web', url: browseUrl });
} catch (error) {
console.error('Erro ao abrir pasta:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
}
});
// Abrir site clonado via HTTP para evitar problemas de origem/CORS
app.post('/api/open-site', (req, res) => {
try {
const { path: sitePath } = req.body;
if (!sitePath) {
return res.status(400).json({ error: 'Caminho do site é obrigatório' });
}
const folderName = path.basename(sitePath);
const siteUrl = `/${folderName}/index.html`;
res.json({ success: true, message: 'Site aberto com sucesso', url: siteUrl });
} catch (error) {
console.error('Erro ao abrir site:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
}
});
// Abrir página de navegação via HTTP
app.post('/api/open-navigation', (req, res) => {
try {
const { path: sitePath } = req.body;
if (!sitePath) {
return res.status(400).json({ error: 'Caminho do site é obrigatório' });
}
const folderName = path.basename(sitePath);
const navUrl = `http://localhost:${PORT}/${folderName}/navegacao.html`;
console.log(`🌍 Opening navigation via HTTP: ${navUrl}`);
// Abrir navegacao.html no navegador padrão
exec(`start "" "${navUrl}"`, (error) => {
if (error) {
console.error('Erro ao abrir navegação:', error);
return res.status(500).json({ error: 'Não foi possível abrir a navegação via HTTP' });
}
res.json({ success: true, message: 'Navegação aberta via HTTP com sucesso', url: navUrl });
});
} catch (error) {
console.error('Erro ao abrir navegação:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
}
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index-new.html'));
});
// Serve old interface
app.get('/old', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Serve favicon requests
app.get('/favicon.ico', (req, res) => {
res.redirect('/favicon.svg');
});
// Handle common missing resources
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 <think> reasoning tags if present
reply = reply.replace(/<think>[\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({
error: 'Not Found',
message: `Route ${req.path} not found`
});
});
// Error handler
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({
error: 'Internal Server Error',
message: err.message
});
});
app.listen(PORT, () => {
console.log(`🌐 CloneWeb Interface running on http://localhost:${PORT}`);
console.log(`🔗 Make sure crawler service is running on http://localhost:3001`);
console.log(`📁 Serving static files from: ${path.join(__dirname, 'public')}`);
});