3c6b62126e
- Checkbox "Pausar Envio de Imagens" na bandeja (tray) - Auto-retomada automática após 10 min de pausa (caso usuário esqueça) - client-monitor: comandos IPC pause/resume; processQueue e executarSincronizacaoLocal respeitam a pausa, mantendo imagens na fila sem perdê-las - Estado de pausa reaplicado se o monitor reiniciar - "Forçar Sincronização" retoma o envio automaticamente se estiver pausado - Bump 1.2.7 → 1.2.8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
544 lines
18 KiB
JavaScript
544 lines
18 KiB
JavaScript
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();
|
|
}
|
|
});
|