408 lines
12 KiB
JavaScript
408 lines
12 KiB
JavaScript
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();
|
|
}
|
|
});
|