const { app, BrowserWindow, Tray, Menu, ipcMain, shell, dialog } = require('electron'); const path = require('path'); const fs = require('fs'); const { fork } = require('child_process'); const { autoUpdater } = require('electron-updater'); // ================================================================ // AUTO-UPDATER CONFIG // ================================================================ autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = true; autoUpdater.logger = require('electron').app ? console : null; autoUpdater.on('checking-for-update', () => { console.log('🔍 Verificando atualizações...'); }); autoUpdater.on('update-available', (info) => { console.log('🔄 Nova versão disponível:', info.version); if (tray) { tray.displayBalloon({ title: 'Nova Atualização Encontrada', content: `A versão ${info.version} está sendo baixada silenciosamente em segundo plano...`, iconType: 'info' }); } }); autoUpdater.on('update-not-available', () => { console.log('✅ Aplicativo está na versão mais recente.'); }); autoUpdater.on('download-progress', (progress) => { console.log(`⬇️ Baixando atualização: ${Math.round(progress.percent)}%`); }); autoUpdater.on('update-downloaded', (info) => { console.log('✅ Atualização baixada:', info.version, '— será instalada ao fechar o app.'); // Notificar o usuário na tray if (tray) { tray.displayBalloon({ title: 'ScoreOdonto - Atualização Pronta', content: `Versão ${info.version} será instalada automaticamente ao fechar o aplicativo.`, iconType: 'info' }); } }); autoUpdater.on('error', (err) => { console.error('❌ Erro no auto-updater:', err.message); }); let tray = null; let configWindow = null; let monitorProcess = null; let currentStatus = { connected: false, identified: false }; const appDataFolder = app.getPath('userData'); const configPath = path.join(appDataFolder, 'config.json'); // Ensure directory exists if (!fs.existsSync(appDataFolder)) { fs.mkdirSync(appDataFolder, { recursive: true }); } // Default config template const defaultConfig = { serverUrl: "https://rx.scoreodonto.com", monitorPath: "C:\\ProgramData\\RF\\Dental Sensor\\Images", clientName: "DIGITE-O-NOME-DA-MAQUINA", clientType: "windows", apiKey: "rf-dental-secure-key-2026", serverEmail: "", serverPassword: "", token: "", adminUsername: "admin", adminPassword: "admin" }; function loadConfig() { try { if (!fs.existsSync(configPath)) { fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8'); return defaultConfig; } const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); // Ensure required fields exist return { ...defaultConfig, ...userConfig }; } catch (e) { console.error("Error loading config:", e); return defaultConfig; } } function saveConfig(newConfig) { try { fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8'); return true; } catch (e) { console.error("Error saving config:", e); return false; } } // Start background child process function startMonitorProcess() { if (monitorProcess) { monitorProcess.kill(); } const config = loadConfig(); // Don't start if it's default/placeholder if (!config.clientName || config.clientName === defaultConfig.clientName) { console.log("Monitor process not started: clientName not configured."); return; } console.log("Starting client-monitor.js as background process..."); monitorProcess = fork(path.join(__dirname, 'client-monitor.js'), [], { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env: { ...process.env, DENTAL_CONFIG_DIR: appDataFolder } }); monitorProcess.on('message', (message) => { if (message && message.event === 'status') { currentStatus.connected = message.connected; currentStatus.identified = message.identified; updateTrayMenu(); } else if (message && message.event === 'log') { if (configWindow && !configWindow.isDestroyed()) { configWindow.webContents.send('new-log', message.log); } } }); monitorProcess.on('exit', (code) => { console.log(`Monitor process exited with code ${code}`); currentStatus.connected = false; currentStatus.identified = false; updateTrayMenu(); }); } function createConfigWindow() { if (configWindow) { configWindow.focus(); return; } configWindow = new BrowserWindow({ width: 600, height: 680, minHeight: 520, title: "ScoreOdonto Dental Client - Configuração", icon: path.join(__dirname, 'favicon.png'), resizable: true, maximizable: true, autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false } }); configWindow.loadFile(path.join(__dirname, 'config-ui.html')); configWindow.on('closed', () => { configWindow = null; }); } function updateTrayMenu() { if (!tray) return; let statusText = "ScoreOdonto: Desconectado 🔴"; if (currentStatus.connected) { statusText = currentStatus.identified ? "ScoreOdonto: Identificado 🟢" : "ScoreOdonto: Conectado... 🟡"; } const contextMenu = Menu.buildFromTemplate([ { label: statusText, enabled: false }, { type: 'separator' }, { label: 'Forçar Sincronização 🔄', click: () => { if (monitorProcess) { monitorProcess.send({ command: 'sync' }); } else { startMonitorProcess(); } } }, { label: 'Verificar Atualizações 🔄', click: () => { autoUpdater.checkForUpdatesAndNotify(); } }, { label: 'Configurações ⚙️', click: () => { createConfigWindow(); } }, { label: 'Abrir Pasta de Imagens 📂', click: () => { const config = loadConfig(); if (fs.existsSync(config.monitorPath)) { shell.openPath(config.monitorPath); } else { shell.openPath(path.dirname(config.monitorPath)); } } }, { type: 'separator' }, { label: 'Sair 🚪', click: () => { app.isQuitting = true; app.quit(); } } ]); tray.setContextMenu(contextMenu); tray.setToolTip(`ScoreOdonto Dental Client\n${statusText}`); } function initTray() { const iconPath = path.join(__dirname, 'favicon.png'); tray = new Tray(iconPath); updateTrayMenu(); tray.on('double-click', () => { createConfigWindow(); }); } // Single instance lock const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); return; } app.on('second-instance', () => { if (configWindow) { if (configWindow.isMinimized()) configWindow.restore(); configWindow.focus(); } else { createConfigWindow(); } }); app.whenReady().then(() => { initTray(); // Verificar atualizações silenciosamente ao iniciar autoUpdater.checkForUpdatesAndNotify(); const config = loadConfig(); // If not configured, show config UI if (!config.clientName || config.clientName === defaultConfig.clientName) { createConfigWindow(); } else { // Start background process startMonitorProcess(); } }); app.on('window-all-closed', () => { // Keep running in tray on close if (process.platform !== 'darwin') { // do nothing, let it run in tray } }); app.on('before-quit', () => { if (monitorProcess) { monitorProcess.kill(); } }); // IPC communication ipcMain.handle('get-config', () => { return loadConfig(); }); ipcMain.handle('get-version', () => { return app.getVersion(); }); ipcMain.handle('save-config', (event, newConfig) => { const success = saveConfig(newConfig); if (success) { // Restart child process with new settings startMonitorProcess(); } return success; }); ipcMain.handle('test-server', async (event, url) => { return new Promise((resolve) => { try { const checkUrl = url.endsWith('/') ? `${url}status` : `${url}/status`; const clientModule = checkUrl.startsWith('https') ? require('https') : require('http'); const req = clientModule.get(checkUrl, { timeout: 4000 }, (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve({ success: true, message: `Conexão bem sucedida! Código: ${res.statusCode}` }); } else { resolve({ success: false, error: `Código de status inesperado: ${res.statusCode}` }); } }); req.on('error', (err) => { resolve({ success: false, error: err.message }); }); req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'Timeout de 4 segundos esgotado.' }); }); } catch (e) { resolve({ success: false, error: e.message }); } }); }); ipcMain.handle('verify-credentials', async (event, url, email, password, token) => { return new Promise((resolve) => { try { let base = url.trim(); if (base.endsWith('/')) { base = base.slice(0, -1); } const verifyUrl = `${base}/api/auth/verify-client-credentials`; const clientModule = verifyUrl.startsWith('https') ? require('https') : require('http'); const parsedUrl = new URL(verifyUrl); const postData = JSON.stringify({ email, password, token }); const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || (verifyUrl.startsWith('https') ? 443 : 80), path: parsedUrl.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }, timeout: 8000 }; const req = clientModule.request(options, (res) => { let body = ''; res.on('data', (chunk) => body += chunk); res.on('end', () => { try { const data = JSON.parse(body); if (res.statusCode >= 200 && res.statusCode < 300 && data.success) { resolve({ success: true, clinic: data.clinic }); } else { resolve({ success: false, error: data.error || `Erro de validação: ${res.statusCode}` }); } } catch (e) { resolve({ success: false, error: `Resposta inválida do servidor: ${e.message}` }); } }); }); req.on('error', (err) => { resolve({ success: false, error: `Falha de rede: ${err.message}` }); }); req.on('timeout', () => { req.destroy(); resolve({ success: false, error: 'Tempo limite esgotado ao tentar validar credenciais (8s).' }); }); req.write(postData); req.end(); } catch (e) { resolve({ success: false, error: `Erro interno: ${e.message}` }); } }); }); ipcMain.handle('open-folder', () => { const config = loadConfig(); const targetPath = config.monitorPath; if (!fs.existsSync(targetPath)) { fs.mkdirSync(targetPath, { recursive: true }); } shell.openPath(targetPath); return true; }); ipcMain.on('close-window', () => { if (configWindow) { configWindow.close(); } });