const { app, BrowserWindow, Tray, Menu, ipcMain, shell, dialog, nativeImage } = require('electron'); const path = require('path'); const fs = require('fs'); const { fork } = require('child_process'); const { autoUpdater } = require('electron-updater'); let tray = null; let configWindow = null; let monitorProcess = null; let currentStatus = { connected: false, identified: false }; let downloadProgress = null; // Variável para controlar o progresso de download const recentLogs = []; // ================================================================ // CONTROLE DE PAUSA DE ENVIO // ================================================================ let isSendingPaused = false; // true = envio de imagens pausado let autoResumeTimer = null; // timer que retoma o envio sozinho const AUTO_RESUME_MS = 10 * 60 * 1000; // 10 minutos function pauseSending() { isSendingPaused = true; if (monitorProcess) monitorProcess.send({ command: 'pause' }); // Agenda retomada automática após 10 min, caso o usuário esqueça de continuar if (autoResumeTimer) clearTimeout(autoResumeTimer); autoResumeTimer = setTimeout(() => { addLog('▶️ Auto-retomada: 10 minutos se passaram, retomando envio automaticamente.'); resumeSending(true); }, AUTO_RESUME_MS); if (tray) { tray.displayBalloon({ title: 'ScoreOdonto - Envio Pausado', content: 'O envio de imagens foi pausado. Ele será retomado automaticamente em 10 minutos se você não continuar antes.', iconType: 'warning' }); } updateTrayMenu(); } function resumeSending(isAuto = false) { isSendingPaused = false; if (autoResumeTimer) { clearTimeout(autoResumeTimer); autoResumeTimer = null; } if (monitorProcess) monitorProcess.send({ command: 'resume' }); if (tray && !isAuto) { tray.displayBalloon({ title: 'ScoreOdonto - Envio Retomado', content: 'O envio de imagens foi retomado.', iconType: 'info' }); } updateTrayMenu(); } function toggleSending() { if (isSendingPaused) resumeSending(false); else pauseSending(); } // ================================================================ // 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); downloadProgress = 0; // Inicializa o progresso updateTrayMenu(); // Atualiza o menu para mostrar a aba "Baixando..." if (tray) { tray.displayBalloon({ title: 'Nova Atualização Encontrada', content: `A versão ${info.version} está sendo baixada. Clique com botão direito no ícone para acompanhar o progresso!`, iconType: 'info' }); } }); autoUpdater.on('update-not-available', () => { console.log('✅ Aplicativo está na versão mais recente.'); }); autoUpdater.on('download-progress', (progress) => { const p = Math.round(progress.percent); console.log(`⬇️ Baixando atualização: ${p}%`); if (downloadProgress !== p) { downloadProgress = p; // Atualiza a bandeja com throttling para não piscar demais if (p % 5 === 0 || p === 100) { updateTrayMenu(); } } }); autoUpdater.on('update-downloaded', (info) => { console.log('✅ Atualização baixada:', info.version, '— reiniciando em 15 segundos...'); downloadProgress = "Pronto"; if (tray) { tray.displayBalloon({ title: 'ScoreOdonto - Atualização Pronta', content: `Versão ${info.version} baixada. O aplicativo será reiniciado automaticamente em 15 segundos.`, iconType: 'info' }); } // Contagem regressiva no tray tooltip let countdown = 15; const countdownInterval = setInterval(() => { countdown--; if (tray) tray.setToolTip(`ScoreOdonto — Reiniciando em ${countdown}s para instalar v${info.version}...`); if (countdown <= 0) { clearInterval(countdownInterval); // isSilent=true (sem janela do instalador), isForceRunAfter=true (reabre após instalar) autoUpdater.quitAndInstall(true, true); } }, 1000); // Permite cancelar o restart manual pelo menu updateTrayMenu(countdown, countdownInterval, info.version); }); autoUpdater.on('error', (err) => { console.error('❌ Erro no auto-updater:', err.message); downloadProgress = null; updateTrayMenu(); }); 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') { recentLogs.push(message.log); if (recentLogs.length > 100) recentLogs.shift(); if (configWindow && !configWindow.isDestroyed()) { configWindow.webContents.send('new-log', message.log); } } else if (message && message.event === 'resize-image') { try { const img = nativeImage.createFromBuffer(Buffer.from(message.base64, 'base64')); const thumb = img.resize({ width: 300, quality: "good" }).toJPEG(80); monitorProcess.send({ event: 'resize-result', id: message.id, success: true, thumbBase64: thumb.toString('base64') }); } catch (e) { monitorProcess.send({ event: 'resize-result', id: message.id, success: false, error: e.message }); } } }); 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; }); } let pendingCountdownInterval = null; function updateTrayMenu(countdown = null, countdownInterval = null, updateVersion = null) { if (!tray) return; // Salva o interval ativo para poder cancelar if (countdownInterval !== null) pendingCountdownInterval = countdownInterval; let statusText = "ScoreOdonto: Desconectado 🔴"; if (currentStatus.connected) { statusText = currentStatus.identified ? "ScoreOdonto: Identificado 🟢" : "ScoreOdonto: Conectado... 🟡"; } const template = [ { label: statusText, enabled: false }, { type: 'separator' } ]; // Se houver um download em andamento, mostra a barra no menu if (downloadProgress !== null) { if (downloadProgress === "Pronto") { const countdownLabel = countdown !== null ? `⏱️ Reiniciando em ${countdown}s para instalar v${updateVersion}...` : '📦 Atualização pronta para instalar'; template.push({ label: countdownLabel, enabled: false }); template.push({ label: '🚀 Instalar Agora (Reiniciar)', click: () => { if (pendingCountdownInterval) clearInterval(pendingCountdownInterval); autoUpdater.quitAndInstall(true, true); } }); } else { template.push({ label: `⬇️ Baixando atualização: ${downloadProgress}%`, enabled: false }); } template.push({ type: 'separator' }); } template.push( { label: isSendingPaused ? '⏸️ Envio PAUSADO (retoma em ~10 min)' : '▶️ Envio Ativo', enabled: false }, { label: 'Pausar Envio de Imagens', type: 'checkbox', checked: isSendingPaused, click: () => toggleSending() }, { type: 'separator' }, { label: 'Forçar Sincronização 🔄', click: () => { if (isSendingPaused) { resumeSending(false); } 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(); } } ); const contextMenu = Menu.buildFromTemplate(template); 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('get-logs', () => { return recentLogs; }); 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(); } });