const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const fs = require('fs').promises; const path = require('path'); const cors = require('cors'); const cookieParser = require('cookie-parser'); const sharp = require('sharp'); require('dotenv').config(); // Importar módulos const imageProcessor = require('./image-processor'); const { authenticateToken, hashPassword, JWT_SECRET } = require('./auth'); const jwt = require('jsonwebtoken'); const db = require('./database'); const storage = require('./storage'); const authRoutes = require('./routes/auth'); const imageRoutes = require('./routes/images'); const patientsRouter = require('./routes/patients'); const gtosRouter = require('./routes/gtos'); const systemRoutes = require('./routes/system'); const { checkInstallation } = require('./installer/check-installation'); // ================================================================ // CONFIGURAÇÃO DO SERVIDOR // ================================================================ const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: process.env.ALLOWED_ORIGINS || "*", methods: ["GET", "POST"] }, transports: ['websocket', 'polling'], allowEIO3: true, pingTimeout: 60000, pingInterval: 25000, connectTimeout: 45000, upgradeTimeout: 30000, maxHttpBufferSize: 5e7 // 50MB para suportar raios-x grandes }); const connectedClients = []; const PORT = process.env.PORT || 3000; const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads'); const PROCESSED_DIR = process.env.PROCESSED_DIR || path.join(__dirname, 'processed'); // ================================================================ // MIDDLEWARES BÁSICOS // ================================================================ app.use(cors()); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); app.use(cookieParser()); // ================================================================ // ROTAS DA API // ================================================================ app.use('/api/patients', patientsRouter); // ================================================================ // ROTAS PÚBLICAS (SEMPRE ACESSÍVEIS) // ================================================================ // Health check app.get('/status', (req, res) => { res.json({ status: 'online', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // Verificar se o nome do computador/cliente já existe app.get('/api/clients/check-name', async (req, res) => { try { const name = (req.query.name || '').trim(); const token = req.query.token || req.headers['x-api-key'] || req.headers['authorization']?.split(' ')[1]; const clientId = req.query.clientId || ''; const serverApiKey = process.env.CLIENT_API_KEY || 'rf-dental-secure-key-2026'; if (token !== serverApiKey) { return res.status(401).json({ success: false, error: 'Chave de API inválida' }); } if (!name) { return res.status(400).json({ success: false, error: 'Nome não fornecido' }); } // 1. Verificar se existe no banco de dados na tabela images (busca case-insensitive) const row = await db.get('SELECT COUNT(*) as count FROM images WHERE LOWER(client_name) = LOWER(?)', [name]); const existsInDb = row && row.count > 0; // 2. Verificar se existe algum cliente atualmente conectado com esse nome (desconsiderando o próprio clientId) const existsConnected = connectedClients.some(c => c.name.toLowerCase() === name.toLowerCase() && (!clientId || c.clientId !== clientId) ); if (existsInDb || existsConnected) { return res.json({ success: true, available: false, exists: true }); } res.json({ success: true, available: true, exists: false }); } catch (error) { console.error('Erro ao verificar nome do cliente:', error); res.status(500).json({ success: false, error: error.message }); } }); // Debug endpoint for browser logs app.post('/api/debug-log', (req, res) => { console.log('=== BROWSER HTML ==='); console.log(req.body.html || 'No HTML provided'); console.log('===================='); res.json({ success: true }); }); // Status de conexões Socket.IO app.get('/api/socket/status', (req, res) => { const sockets = Array.from(io.sockets.sockets.values()); const clients = sockets.map(socket => ({ id: socket.id, connected: socket.connected, transport: socket.conn.transport.name, isIdentified: socket.isIdentified || false, handshake: socket.handshake.query, remoteAddress: socket.handshake.address })); const identifiedClients = connectedClients.filter(c => { return sockets.some(s => s.id === c.socketId); }); res.json({ totalConnections: sockets.length, identifiedClients: identifiedClients.length, clients: clients, identified: identifiedClients, timestamp: new Date().toISOString() }); }); // Endpoint para forçar identificação de um cliente (debug) app.post('/api/socket/request-identify/:socketId', (req, res) => { const { socketId } = req.params; const socket = io.sockets.sockets.get(socketId); if (!socket) { return res.status(404).json({ error: 'Socket não encontrado' }); } socket.emit('please-identify', { message: 'Solicitação manual de identificação' }); res.json({ success: true, message: 'Solicitação de identificação enviada', socketId }); }); // Endpoint para teste de conexão Socket.IO app.post('/api/socket/test-connection', async (req, res) => { try { const sockets = Array.from(io.sockets.sockets.values()); const totalConnections = sockets.length; const identifiedClients = connectedClients.filter(c => { return sockets.some(s => s.id === c.socketId); }); if (identifiedClients.length === 0) { return res.json({ success: true, totalConnections: totalConnections, identifiedClients: 0, testResults: [], message: 'Nenhum cliente identificado para testar', timestamp: new Date().toISOString() }); } // Enviar evento de teste para todos os clientes identificados const testPromises = identifiedClients.map((client) => { return new Promise((resolve) => { const socket = io.sockets.sockets.get(client.socketId); if (!socket || !socket.connected) { resolve({ socketId: client.socketId, name: client.name, success: false, error: 'Cliente não está conectado' }); return; } const testId = `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const testStartTime = Date.now(); let resolved = false; const timeout = setTimeout(() => { if (!resolved) { resolved = true; resolve({ socketId: client.socketId, name: client.name, success: false, error: 'Timeout - cliente não respondeu em 5 segundos' }); } }, 5000); const ackHandler = (data) => { if (!resolved && data.testId === testId) { resolved = true; clearTimeout(timeout); socket.off('test-connection-ack', ackHandler); resolve({ socketId: client.socketId, name: client.name, success: true, latency: data.latency || (Date.now() - testStartTime), timestamp: data.timestamp }); } }; socket.on('test-connection-ack', ackHandler); socket.emit('test-connection', { testId: testId, timestamp: testStartTime, message: 'Teste de conexão iniciado pelo servidor' }); }); }); // Aguardar todas as respostas const testResults = await Promise.all(testPromises); res.json({ success: true, totalConnections: totalConnections, identifiedClients: identifiedClients.length, testResults: testResults, timestamp: new Date().toISOString() }); } catch (error) { console.error('Erro no teste de conexão:', error); res.status(500).json({ success: false, error: error.message }); } }); // API de Instalação - Status app.get('/api/install/status', async (req, res) => { try { const status = await checkInstallation(); res.json(status); } catch (error) { res.json({ installed: false, needsInstall: true, databaseExists: false, userExists: false, error: error.message }); } }); // API de Instalação - Executar app.post('/api/install', async (req, res) => { try { const { db: dbConfig, admin } = req.body; // Validar dados if (!dbConfig || !admin) { return res.status(400).json({ success: false, error: 'Dados incompletos' }); } if (!admin.username || !admin.email || !admin.password) { return res.status(400).json({ success: false, error: 'Preencha todos os dados do administrador' }); } if (admin.password.length < 8) { return res.status(400).json({ success: false, error: 'A senha deve ter no mínimo 8 caracteres' }); } // Atualizar variáveis de ambiente process.env.DB_HOST = dbConfig.host || 'localhost'; process.env.DB_PORT = dbConfig.port || 3306; process.env.DB_NAME = dbConfig.name || 'dental_images'; process.env.DB_USER = dbConfig.user || 'root'; process.env.DB_PASSWORD = dbConfig.password || ''; // Salvar no arquivo .env await updateEnvFile({ DB_HOST: process.env.DB_HOST, DB_PORT: process.env.DB_PORT, DB_NAME: process.env.DB_NAME, DB_USER: process.env.DB_USER, DB_PASSWORD: process.env.DB_PASSWORD }); // Inicializar banco de dados await db.initDatabase(); console.log('✅ Banco de dados inicializado'); // Criar usuário admin const passwordHash = await hashPassword(admin.password); await db.run( `INSERT INTO users (username, email, password_hash, created_at) VALUES (?, ?, ?, NOW()) ON CONFLICT (username) DO UPDATE SET email = EXCLUDED.email, password_hash = EXCLUDED.password_hash`, [admin.username, admin.email, passwordHash] ); console.log('✅ Usuário admin criado:', admin.username); // Criar arquivo .installed const installedPath = path.join(__dirname, '.installed'); await fs.writeFile(installedPath, new Date().toISOString()); // Criar arquivo version.txt const pkg = require('./package.json'); const versionPath = path.join(__dirname, 'version.txt'); await fs.writeFile(versionPath, pkg.version || '2.0.0'); res.json({ success: true, message: 'Instalação concluída com sucesso!' }); } catch (error) { console.error('Erro na instalação:', error); res.status(500).json({ success: false, error: error.message || 'Erro na instalação' }); } }); // Função para atualizar arquivo .env async function updateEnvFile(updates) { const envPath = path.join(__dirname, '.env'); let envContent = ''; try { // Ler arquivo .env existente envContent = await fs.readFile(envPath, 'utf8'); } catch (error) { // Se não existe, criar conteúdo inicial envContent = `PORT=3000 JWT_SECRET=${process.env.JWT_SECRET || 'sua-chave-secreta-alterar-em-producao'} SESSION_SECRET=${process.env.SESSION_SECRET || 'outra-chave-secreta'} ALLOWED_ORIGINS=${process.env.ALLOWED_ORIGINS || '*'} TRUST_PROXY=true `; } // Atualizar valores Object.keys(updates).forEach(key => { const regex = new RegExp(`^${key}=.*$`, 'm'); const newLine = `${key}=${updates[key]}`; if (regex.test(envContent)) { envContent = envContent.replace(regex, newLine); } else { envContent += `${newLine}\n`; } }); // Adicionar configurações de diretórios se não existirem if (!envContent.includes('UPLOAD_DIR')) { envContent += `UPLOAD_DIR=${UPLOAD_DIR}\n`; } if (!envContent.includes('PROCESSED_DIR')) { envContent += `PROCESSED_DIR=${PROCESSED_DIR}\n`; } // Salvar arquivo await fs.writeFile(envPath, envContent); console.log('✅ Arquivo .env atualizado'); } // Página de instalação app.get('/install', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'install.html')); }); // ================================================================ // ROTA PRINCIPAL (Página web) - PRIMEIRA ROTA A SER DEFINIDA // ================================================================ // ROTA PRINCIPAL (Página web) app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Favicon redirect/fallback app.get('/favicon.ico', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'favicon.svg')); }); // Página de clientes conectados app.get('/clients', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'clients.html')); }); // Página de reset de fábrica app.get('/reset', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'reset.html')); }); // Rotas de autenticação (públicas) app.use('/api/auth', authRoutes); // Página de login app.get('/login', (req, res) => { res.sendFile(path.join(__dirname, 'auth', 'login.html')); }); app.use('/login', express.static('auth')); // Serve static files app.use(express.static('public')); app.get('/uploads/:filename', async (req, res) => { const filename = req.params.filename; // Enviar ETag e Cache-Control imutável res.setHeader('ETag', `"${filename}"`); res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); const clientEtag = req.headers['if-none-match']; if (clientEtag === `"${filename}"` || clientEtag === filename || clientEtag === `W/"${filename}"`) { return res.status(304).end(); } // Tentar primeiro obter da pasta de uploads (imagens originais) let buffer = await storage.getImageBuffer(filename, false); // Se não encontrar, tentar da pasta de processed (imagens rotacionadas/transformadas) if (!buffer) { buffer = await storage.getImageBuffer(filename, true); } if (!buffer) { return res.status(404).send('Imagem não encontrada'); } const ext = path.extname(filename).toLowerCase(); let contentType = 'image/png'; if (ext === '.jpg' || ext === '.jpeg') { contentType = 'image/jpeg'; } else if (ext === '.webp') { contentType = 'image/webp'; } else if (ext === '.svg') { contentType = 'image/svg+xml'; } res.setHeader('Content-Type', contentType); res.send(buffer); }); // ================================================================ // MIDDLEWARE DE VERIFICAÇÃO DE INSTALAÇÃO (APENAS PARA APIs) // ================================================================ async function checkInstallationMiddleware(req, res, next) { // SEMPRE permitir rotas públicas, static files e Socket.IO if (req.path === '/' || req.path.startsWith('/install') || req.path.startsWith('/login') || req.path.startsWith('/status') || req.path.startsWith('/socket.io') || req.path.startsWith('/api/auth') || req.path.startsWith('/api/install') || req.path.startsWith('/api/clients/check-name')) { return next(); } // Para APIs que não são de instalação ou auth, verificar instalação if (req.path.startsWith('/api')) { try { const installedFile = path.join(__dirname, '.installed'); const fsSync = require('fs'); if (!fsSync.existsSync(installedFile)) { return res.status(503).json({ error: 'Sistema não instalado. Acesse /install para configurar.' }); } } catch (error) { return res.status(503).json({ error: 'Erro ao verificar instalação.' }); } } next(); } // Aplicar middleware apenas para verificar APIs app.use(checkInstallationMiddleware); // ================================================================ // ROTAS PROTEGIDAS (REQUEREM AUTENTICAÇÃO) // ================================================================ // APIs de imagens - requerem autenticação app.use('/api/images', authenticateToken); app.use('/api/images', imageRoutes); // APIs de GTOs - requerem autenticação app.use('/api/gtos', authenticateToken); app.use('/api/gtos', gtosRouter); app.use('/api/system', authenticateToken); app.use('/api/system', systemRoutes); // Endpoint para solicitar a sincronização aos dispositivos conectados app.post('/api/system/trigger-sync', authenticateToken, (req, res) => { try { const windowsClients = connectedClients.filter(c => c.type === 'windows'); // Envia o evento 'sync-request' com timestamp para todos os dispositivos clientes identificados io.emit('sync-request', { timestamp: Date.now() }); console.log(`🔄 [Sync] Sinal de sincronização enviado para ${windowsClients.length} dispositivo(s) Windows.`); res.json({ success: true, devicesTriggered: windowsClients.length, message: `Sinal de sincronização enviado com sucesso para ${windowsClients.length} dispositivo(s) Windows.` }); } catch (error) { console.error('Erro ao solicitar sincronização:', error); res.status(500).json({ error: error.message }); } }); // ================================================================ // SOCKET.IO - CONEXÕES EM TEMPO REAL // ================================================================ // Middleware de Autenticação para o Socket.IO io.use((socket, next) => { const token = socket.handshake.auth?.token || socket.handshake.query?.token; // Obter a API KEY do ambiente ou usar uma padrão (fallback local temporário) const serverApiKey = process.env.CLIENT_API_KEY || 'rf-dental-secure-key-2026'; if (!token) { console.log(`❌ Conexão Socket.IO rejeitada (Sem token) - IP: ${socket.handshake.address}`); return next(new Error('Authentication error: Token required')); } // 1. Verificar se é a API_KEY do Sensor if (token === serverApiKey) { socket.clientType = 'sensor'; return next(); // Token válido, prosseguir } // 2. Se não for a API_KEY, tentar validar como JWT (Painel Web) try { const user = jwt.verify(token, JWT_SECRET); socket.clientType = 'web'; socket.user = user; return next(); // JWT válido, prosseguir } catch (error) { console.log(`❌ Conexão Socket.IO rejeitada (Token Inválido) - IP: ${socket.handshake.address}`); return next(new Error('Authentication error: Invalid token')); } }); io.on('connection', (socket) => { console.log('📱 Cliente conectado:', socket.id); console.log('📱 Socket transport:', socket.conn.transport.name); console.log('📱 Socket handshake:', JSON.stringify(socket.handshake.query)); console.log('📱 Remote address:', socket.handshake.address); // Marcar cliente como conectado (mesmo sem identificação) socket.isIdentified = false; // Enviar confirmação imediata de conexão socket.emit('connection-ack', { message: 'Conexão estabelecida com sucesso', socketId: socket.id, serverTime: new Date().toISOString() }); console.log('✅ Confirmação de conexão enviada para:', socket.id); // Tentar identificar automaticamente após 1 segundo setTimeout(() => { if (!socket.isIdentified && socket.connected) { console.log('⚠️ Cliente ainda não identificado após 1s. Socket ainda conectado:', socket.id); // Enviar solicitação de identificação socket.emit('please-identify', { message: 'Por favor, identifique-se enviando o evento client-identify' }); } }, 1000); // Identificação de clientes socket.on('client-identify', (data) => { console.log('👤 [client-identify] Evento recebido de:', socket.id); console.log('👤 Cliente identificado:', data?.type || 'SEM TYPE', data?.name || 'SEM NAME'); console.log('👤 Dados completos:', JSON.stringify(data, null, 2)); if (!data || !data.type || !data.name) { console.error('❌ [client-identify] Dados inválidos:', data); socket.emit('error', { message: 'Dados de identificação inválidos' }); return; } const clientName = data.name.trim(); const clientId = data.clientId || ''; // 1. Validar unicidade em relação aos clientes já conectados const duplicate = connectedClients.find(c => c.name.toLowerCase() === clientName.toLowerCase() && c.clientId !== clientId ); if (duplicate) { console.log(`❌ [client-identify] Conexão rejeitada: O nome "${clientName}" já está em uso por outro dispositivo.`); socket.emit('error', { message: `O nome "${clientName}" já está sendo usado por outro computador neste servidor. Escolha um nome exclusivo.` }); setTimeout(() => { socket.disconnect(true); }, 500); return; } // 2. Se for uma reconexão do mesmo clientId, desconectar e limpar os sockets antigos do mesmo cliente const oldConnections = connectedClients.filter(c => c.clientId === clientId && c.socketId !== socket.id ); for (const old of oldConnections) { console.log(`🔄 Desconectando socket antigo ${old.socketId} para o mesmo clientId ${clientId}`); const oldSocket = io.sockets.sockets.get(old.socketId); if (oldSocket) { oldSocket.emit('error', { message: 'Nova conexão estabelecida deste computador.' }); oldSocket.disconnect(true); } const idx = connectedClients.indexOf(old); if (idx > -1) { connectedClients.splice(idx, 1); } } const clientInfo = { socketId: socket.id, type: data.type || 'unknown', name: clientName, version: data.version || '1.0.0', system: data.system || 'unknown', clientId: clientId, connectedAt: new Date().toISOString() }; // Remover cliente anterior se existir const existingIndex = connectedClients.findIndex(c => c.socketId === socket.id); if (existingIndex > -1) { connectedClients.splice(existingIndex, 1); } connectedClients.push(clientInfo); socket.isIdentified = true; console.log('✅ Cliente adicionado à lista. Total conectados:', connectedClients.length); // Confirmar para o cliente socket.emit('server-identified', { success: true, message: 'Identificação confirmada', clientInfo: clientInfo }); console.log('✅ Confirmação enviada para cliente:', socket.id); // Atualizar lista para todos io.emit('clients-list-updated', connectedClients); }); // Receber imagem do cliente (aceitar mesmo sem identificação prévia) socket.on('send-image', async (data, callback) => { try { console.log('📤 [send-image] Evento recebido de:', socket.id); console.log('📤 Cliente identificado?', socket.isIdentified ? 'SIM' : 'NÃO (mas aceitando)'); console.log('📤 Metadata:', data?.metadata?.fileName || 'SEM METADATA'); console.log('📤 Patient Data:', data?.patientData?.name || 'SEM PATIENT DATA'); console.log('📤 Image Base64 length:', data?.imageBase64?.length || 'SEM IMAGEM'); // Validar dados mínimos if (!data || !data.imageBase64) { throw new Error('Dados de imagem incompletos. imageBase64 é obrigatório.'); } if (!data.metadata || !data.metadata.fileName) { throw new Error('Metadata incompleta. fileName é obrigatório.'); } // Verificar se a imagem já foi inserida no banco const existing = await db.get( "SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1", [data.metadata.fileName] ); if (existing) { console.log(`⚠️ Imagem ${data.metadata.fileName} já existe no servidor (ID: ${existing.id}). Ignorando upload.`); const ackData = { success: true, imageId: existing.id, message: 'Imagem já existe no servidor', filename: existing.filename, originalFilename: data.metadata.fileName, savedAt: new Date().toISOString() }; socket.emit('image-received', ackData); if (typeof callback === 'function') { callback(ackData); } return; } // Decodificar imagem base64 const imageBuffer = Buffer.from(data.imageBase64, 'base64'); // Validar imagem await imageProcessor.validateImage(imageBuffer); // Detectar formato da imagem e gerar extensão adequada const imgMetadata = await sharp(imageBuffer).metadata(); const format = imgMetadata.format || 'png'; const ext = format === 'jpeg' ? 'jpg' : format; // Salvar imagem original const timestamp = Date.now(); const filename = `${timestamp}_${data.metadata.guid}.${ext}`; const filePath = path.join(UPLOAD_DIR, filename); // Buscar nome do cliente se identificado const clientInfo = connectedClients.find(c => c.socketId === socket.id); const clientName = clientInfo ? clientInfo.name : 'Cliente não identificado'; const patientName = data.patientData.name || 'Paciente não identificado'; await storage.saveImage(filename, imageBuffer, clientName, patientName, false); // Gerar e salvar thumbnail WebP let thumbFilename = null; try { const thumbBuffer = await sharp(imageBuffer) .resize({ width: 400, height: 400, fit: 'inside', withoutEnlargement: true }) .webp({ quality: 80, effort: 4 }) .toBuffer(); thumbFilename = `thumb_${timestamp}_${data.metadata.guid}.webp`; await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientName, false); console.log(`✅ Thumbnail gerado e salvo no Wasabi: ${thumbFilename}`); } catch (thumbErr) { console.error('⚠️ Falha ao gerar thumbnail, prosseguindo sem ele:', thumbErr.message); } // Determinar data de criação const formatLocalDateTime = (date) => { const pad = (num) => String(num).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }; const createdAt = (data.patientData && data.patientData.photographTime) || formatLocalDateTime(new Date()); // Salvar no banco de dados const result = await db.run( `INSERT INTO images ( image_guid, patient_name, client_name, original_filename, filename, enabled, doctor, remark, thumb_filename, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ data.metadata.guid, data.patientData.name || 'Paciente não identificado', clientName, data.metadata.fileName, filename, 1, data.patientData.doctor || null, data.patientData.remark || null, thumbFilename, createdAt ] ); console.log('✅ Imagem salva no banco. ID:', result.lastID); console.log('✅ Arquivo salvo em:', filePath); // Confirmar para o cliente const ackData = { success: true, imageId: result.lastID, message: 'Imagem recebida com sucesso', filename: filename, originalFilename: data.metadata.fileName, savedAt: new Date().toISOString() }; socket.emit('image-received', ackData); if (typeof callback === 'function') { callback(ackData); } console.log('✅ Confirmação enviada para cliente:', socket.id); // Notificar web clients (apenas se não for backlog de sincronização inicial) if (!data.isBacklog) { io.emit('new-image', { id: result.lastID, image_guid: data.metadata.guid, patient_name: data.patientData.name, doctor: data.patientData.doctor || null, remark: data.patientData.remark || null, filename: filename, thumb_filename: thumbFilename, created_at: createdAt }); } } catch (error) { console.error('❌ Erro ao processar imagem:', error); const errorData = { success: false, message: 'Erro ao processar imagem', error: error.message }; socket.emit('error', errorData); if (typeof callback === 'function') { callback(errorData); } } }); socket.on('check-image-exists', async (data, callback) => { try { if (!data || !data.fileName) { if (typeof callback === 'function') callback({ success: false, error: 'fileName é obrigatório' }); return; } const existing = await db.get( "SELECT id, filename FROM images WHERE original_filename = ? AND enabled = 1 LIMIT 1", [data.fileName] ); if (typeof callback === 'function') { callback({ success: true, exists: !!existing, imageId: existing ? existing.id : null, filename: existing ? existing.filename : null }); } } catch (error) { console.error('❌ Erro ao verificar existência de imagem:', error); if (typeof callback === 'function') callback({ success: false, error: error.message }); } }); // Debug: Logar todos os eventos recebidos socket.onAny((event, ...args) => { if (event !== 'ping' && event !== 'pong') { console.log(`🔔 [${socket.id}] Evento recebido: ${event}`, args.length > 0 ? '(com dados)' : '(sem dados)'); if (args.length > 0 && event !== 'ping') { console.log(` Dados:`, JSON.stringify(args[0], null, 2).substring(0, 200)); } } }); // Monitorar upgrades de transport socket.conn.on('upgrade', () => { console.log(`🔄 [${socket.id}] Transport upgraded para: ${socket.conn.transport.name}`); }); // Monitorar erros de conexão socket.conn.on('error', (error) => { console.error(`❌ [${socket.id}] Erro de conexão:`, error.message); }); // Heartbeat - manter conexão viva socket.on('ping', () => { socket.emit('pong', { timestamp: new Date().toISOString() }); }); // Handler para teste de conexão socket.on('test-connection', (data) => { console.log('🧪 [test-connection] Evento recebido de:', socket.id); console.log('🧪 Test ID:', data?.testId); const startTime = data.timestamp || Date.now(); const latency = Date.now() - startTime; socket.emit('test-connection-ack', { testId: data.testId, socketId: socket.id, latency: latency, timestamp: new Date().toISOString(), message: 'Teste de conexão bem-sucedido' }); console.log(`✅ [test-connection] Resposta enviada para ${socket.id}. Latência: ${latency}ms`); }); // Desconexão socket.on('disconnect', (reason) => { console.log('📴 Cliente desconectado:', socket.id, '- Motivo:', reason); console.log('📴 Estava identificado?', socket.isIdentified ? 'SIM' : 'NÃO'); // Remover da lista const index = connectedClients.findIndex(c => c.socketId === socket.id); if (index > -1) { const removed = connectedClients.splice(index, 1); console.log('🗑️ Cliente removido da lista:', removed[0].name || socket.id); } // Atualizar lista apenas se havia clientes identificados if (connectedClients.length > 0) { io.emit('clients-list-updated', connectedClients); } }); // Tratamento de erros do socket socket.on('error', (error) => { console.error(`❌ [${socket.id}] Erro no socket:`, error.message); }); }); // ================================================================ // INICIALIZAÇÃO // ================================================================ async function initDirectories() { try { await fs.mkdir(UPLOAD_DIR, { recursive: true }); await fs.mkdir(PROCESSED_DIR, { recursive: true }); console.log('✅ Pastas criadas:', UPLOAD_DIR, PROCESSED_DIR); } catch (error) { console.error('Erro ao criar pastas:', error); } } async function start() { const fsSync = require('fs'); try { // Inicializar diretórios await initDirectories(); // Inicializar banco se as variáveis já existirem no ambiente if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) { try { await db.initDatabase(); console.log('✅ Banco de dados inicializado a partir do ambiente'); await storage.loadConfigFromDb(); } catch (error) { console.log('❌ Falha ao inicializar o banco de dados:', error.message); } } else { // Fallback para arquivo .env físico const envPath = path.join(__dirname, '.env'); const fsSync = require('fs'); if (fsSync.existsSync(envPath)) { try { require('dotenv').config(); if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) { await db.initDatabase(); console.log('✅ Banco de dados inicializado a partir do .env'); await storage.loadConfigFromDb(); } } catch (error) { console.log('⚠️ Banco de dados não configurado. Execute a instalação em /install'); } } } // Iniciar servidor server.listen(PORT, () => { console.log('═══════════════════════════════════════════════════════'); console.log('🚀 DENTAL IMAGE SERVER STARTED'); console.log('═══════════════════════════════════════════════════════'); console.log(`📡 Server running on port ${PORT}`); console.log(`🌐 Web interface: http://localhost:${PORT}`); console.log(`🔧 Installation: http://localhost:${PORT}/install`); console.log(`🔐 Login: http://localhost:${PORT}/login`); console.log(`📁 Uploads at: ${UPLOAD_DIR}`); console.log(`🖼️ Processed at: ${PROCESSED_DIR}`); console.log('═══════════════════════════════════════════════════════'); const installedFile = path.join(__dirname, '.installed'); if (!fsSync.existsSync(installedFile)) { console.log('⚠️ Sistema não instalado. Acesse /install para configurar.'); } }); } catch (error) { console.error('❌ Erro ao iniciar servidor:', error); process.exit(1); } } // ================================================================ // TRATAMENTO DE ERROS // ================================================================ process.on('uncaughtException', (error) => { console.error('❌ Erro não capturado:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('❌ Promise rejeitada:', reason); }); // ================================================================ // INICIAR // ================================================================ start();