diff --git a/dental-client/client-monitor.js b/dental-client/client-monitor.js new file mode 100644 index 0000000..b3acfed --- /dev/null +++ b/dental-client/client-monitor.js @@ -0,0 +1,1323 @@ +const io = require('socket.io-client'); +const chokidar = require('chokidar'); +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); +const http = require('http'); +const { exec } = require('child_process'); + +// Carregar configuração dinamicamente para poder atualizá-la +const configPath = path.join(__dirname, 'config.json'); +let config = {}; +try { + config = JSON.parse(fs.readFileSync(configPath, 'utf8')); +} catch (e) { + config = { + serverUrl: "http://localhost:3000", + monitorPath: "C:\\ProgramData\\RF\\Dental Sensor\\Images", + clientName: "COMPUTADOR-LOCAL", + clientType: "windows", + apiKey: "rf-dental-secure-key-2026", + adminPassword: "admin" + }; +} + +// Variáveis de estado global +let socket; +let isConnected = false; +let isIdentified = false; +let watcher; +let isSyncing = false; +const recentLogs = []; + +// Instancia única global do banco local para evitar sobrecarga de I/O +let localDbInstance = null; +function getLocalDb() { + if (!localDbInstance) { + try { + const { DatabaseSync } = require('node:sqlite'); + const dbPath = path.join('C:', 'ProgramData', 'RF', 'Dental Sensor', 'data'); + if (fs.existsSync(dbPath)) { + localDbInstance = new DatabaseSync(dbPath); + } + } catch (e) { + console.error('Erro ao abrir banco local:', e.message); + } + } + return localDbInstance; +} + +// Cache local de uploads concluídos para evitar reenvio de arquivos no scan inicial +const uploadCachePath = path.join(__dirname, 'uploaded_cache.json'); +let uploadedCache = new Set(); +try { + if (fs.existsSync(uploadCachePath)) { + const cachedFiles = JSON.parse(fs.readFileSync(uploadCachePath, 'utf8')); + uploadedCache = new Set(cachedFiles); + console.log(`💾 Cache de uploads carregado com ${uploadedCache.size} arquivos.`); + } +} catch (e) { + console.error('Erro ao carregar cache de uploads:', e.message); +} + +function saveUploadCache() { + try { + fs.writeFileSync(uploadCachePath, JSON.stringify([...uploadedCache], null, 2)); + } catch (e) { + console.error('Erro ao salvar cache de uploads:', e.message); + } +} + +// Função auxiliar para registrar logs locais e expor no painel +function addLog(msg) { + const timestamp = new Date().toLocaleTimeString(); + const formatted = `[${timestamp}] ${msg}`; + console.log(formatted); + recentLogs.push(formatted); + if (recentLogs.length > 50) { + recentLogs.shift(); + } +} + +// Interface Web de Administração em HTML/CSS (zero dependências) +function getAdminHtml() { + return ` + + + + + Dental Client - Painel + + + + + +
+
+ + +
+
+ +
+ + Carregando... +
+
+
+ + +
+
⚙️ Configurações do Cliente
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
📊 Status do Sistema
+
+
+ Sistema + - +
+
+ Uptime + - +
+
+ Socket ID + - +
+
+ +
🛠️ Atalhos
+
+ + +
+
+ + +
+
📜 Atividades Recentes
+
+
Carregando logs...
+
+
+ +
+ + + +`; +} + +function startAdminServer() { + const server = http.createServer((req, res) => { + // Habilitar CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Parse da URL + const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`); + + // Rota pública: Favicon + if (parsedUrl.pathname === '/favicon.png' && req.method === 'GET') { + const faviconPath = path.join(__dirname, 'favicon.png'); + if (fs.existsSync(faviconPath)) { + res.writeHead(200, { 'Content-Type': 'image/png' }); + res.end(fs.readFileSync(faviconPath)); + } else { + res.writeHead(404); + res.end(); + } + return; + } + + // --- PROTEÇÃO DO PAINEL LOCAL (Basic Auth) --- + const authHeader = req.headers['authorization']; + if (config.adminPassword && config.adminPassword.trim() !== '') { + if (!authHeader) { + res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Dental Client Admin"' }); + res.end('Acesso Negado'); + return; + } + + const authStr = Buffer.from(authHeader.split(' ')[1], 'base64').toString(); + const [user, pass] = authStr.split(':'); + + // Autenticamos apenas pela senha, o usuário pode ser admin ou vazio + if (pass !== config.adminPassword) { + res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Dental Client Admin"' }); + res.end('Acesso Negado'); + return; + } + } + // --------------------------------------------- + + if (parsedUrl.pathname === '/' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(getAdminHtml()); + } + else if (parsedUrl.pathname === '/api/status' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + connected: isConnected, + identified: isIdentified, + socketId: socket ? socket.id : null, + config: config, + logs: recentLogs, + platform: process.platform, + uptime: process.uptime() + })); + } + else if (parsedUrl.pathname === '/api/config' && req.method === 'POST') { + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', () => { + try { + const newConfig = JSON.parse(body); + if (!newConfig.serverUrl || !newConfig.monitorPath || !newConfig.clientName || !newConfig.apiKey || !newConfig.adminPassword) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Campos obrigatórios ausentes.' })); + return; + } + + // Gravar no config.json + config.serverUrl = newConfig.serverUrl; + config.monitorPath = newConfig.monitorPath; + config.clientName = newConfig.clientName; + config.apiKey = newConfig.apiKey; + config.adminPassword = newConfig.adminPassword; + + try { + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + addLog(`💾 Novas configurações persistidas em config.json`); + } catch (writeErr) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Erro ao salvar config.json: ' + writeErr.message })); + return; + } + + // Aplicar dinamicamente + reconnectAndRestart(); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true })); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: e.message })); + } + }); + } + else if (parsedUrl.pathname === '/api/test-server' && req.method === 'POST') { + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', () => { + try { + const data = JSON.parse(body); + const testUrl = data.url; + if (!testUrl) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'URL inválida.' })); + return; + } + + const checkUrl = testUrl.endsWith('/') ? `${testUrl}status` : `${testUrl}/status`; + const clientModule = checkUrl.startsWith('https') ? require('https') : require('http'); + + addLog(`⚡ Testando conectividade com: ${checkUrl}`); + + const testReq = clientModule.get(checkUrl, { timeout: 4000 }, (testRes) => { + let resBody = ''; + testRes.on('data', chunk => resBody += chunk); + testRes.on('end', () => { + if (testRes.statusCode >= 200 && testRes.statusCode < 300) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, message: `Conexão bem sucedida! Código: ${testRes.statusCode}` })); + } else { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: `Código de status inesperado: ${testRes.statusCode}` })); + } + }); + }); + + testReq.on('error', (err) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err.message })); + }); + + testReq.on('timeout', () => { + testReq.destroy(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Timeout de 4 segundos esgotado.' })); + }); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: e.message })); + } + }); + } + else if (parsedUrl.pathname === '/api/open-folder' && req.method === 'POST') { + try { + const normalizedPath = path.normalize(config.monitorPath); + if (!fs.existsSync(normalizedPath)) { + fs.mkdirSync(normalizedPath, { recursive: true }); + } + exec(`explorer.exe "${normalizedPath}"`); + addLog(`📁 Pasta de imagens aberta no Explorer: ${normalizedPath}`); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true })); + } catch (e) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: e.message })); + } + } + else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } + }); + + server.listen(3001, '127.0.0.1', () => { + addLog('🌐 Painel de Administração iniciado em: http://localhost:3001'); + }); +} + +// Aplicar alterações do painel dinamicamente +function reconnectAndRestart() { + addLog('🔄 Aplicando novas configurações dinamicamente...'); + + // 1. Fechar conexão socket anterior + if (socket) { + addLog('🔌 Desconectando socket ativo...'); + socket.disconnect(); + socket.removeAllListeners(); + } + isConnected = false; + isIdentified = false; + + // 2. Parar watcher antigo + if (watcher) { + addLog('👀 Interrompendo monitoramento anterior...'); + watcher.close(); + } + + // 3. Conectar novamente com novas definições + connectToServer(); + + // 4. Reiniciar monitoramento + setTimeout(() => { + startMonitoring(); + }, 1000); +} + +// Função de Conexão com o Servidor Dental (Socket.IO) +function connectToServer() { + const serverUrl = process.env.SERVER_URL || config.serverUrl; + const apiKey = process.env.CLIENT_API_KEY || config.apiKey; + + addLog(`🔄 Conectando ao servidor: ${serverUrl}...`); + socket = io(serverUrl, { + transports: ['websocket', 'polling'], + upgrade: true, + timeout: 20000, + reconnection: true, + reconnectionDelay: 2000, + reconnectionAttempts: Infinity, + forceNew: true, + auth: { + token: apiKey || '' + } + }); + + socket.on('connect_error', (err) => { + addLog(`❌ Erro de Conexão: ${err.message}`); + if (err.message.includes('Authentication error')) { + addLog(`🛑 VERIFIQUE A CHAVE DA API! Acesso negado pelo servidor.`); + } + }); + + socket.on('connect', () => { + addLog('✅ Conectado ao servidor Socket.IO com sucesso!'); + isConnected = true; + setTimeout(() => identifyClient(), 500); + }); + + socket.on('disconnect', (reason) => { + addLog(`❌ Desconectado do servidor. Motivo: ${reason}`); + isConnected = false; + isIdentified = false; + }); + + socket.on('connection-ack', (data) => { + addLog(`✅ Confirmação de socket recebida. Socket ID: ${data.socketId}`); + }); + + socket.on('please-identify', () => { + addLog('⚠️ Servidor solicitou identificação do cliente.'); + identifyClient(); + }); + + socket.on('server-identified', (data) => { + addLog(`✅ Identificação confirmada pelo servidor para o cliente: ${data.clientInfo?.name}`); + isIdentified = true; + }); + + // Ouvinte para o Sinal de Sincronização Remota (Enviado pelo Painel Web) + socket.on('sync-request', async (data) => { + addLog(`🔄 [Sincronização] Sinal recebido do servidor. Timestamp do sinal: ${data?.timestamp || 'N/A'}`); + try { + addLog('🚀 Iniciando varredura local de imagens do sensor...'); + await executarSincronizacaoLocal(); + } catch (error) { + addLog(`❌ Erro durante o processamento da sincronização remota: ${error.message}`); + } + }); + + socket.on('connect_error', (error) => { + addLog(`❌ Erro de conexão Socket.IO: ${error.message}`); + }); + + socket.on('error', (error) => { + addLog(`❌ Erro no socket: ${error.message}`); + }); + + socket.on('image-received', (data) => { + addLog(`✅ Imagem processada e salva no servidor! ID: ${data.imageId}, Arquivo: ${data.filename}`); + if (data.originalFilename) { + uploadedCache.add(data.originalFilename); + saveUploadCache(); + } + }); + + socket.on('test-connection', (data) => { + addLog(`🧪 Recebido teste de conexão do servidor. 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 respondido pelo cliente' + }); + }); +} + +// Função para Identificar o Cliente +function identifyClient() { + if (!isConnected || isIdentified) return; + + addLog(`👤 Enviando identificação: Nome=${config.clientName || 'Notebook Consultório Principal'}, Tipo=windows`); + socket.emit('client-identify', { + type: 'windows', // MANDATÓRIO: Identifica o notebook na Área de Sincronização + name: config.clientName || 'Notebook Consultório Principal', // Nome amigável que aparecerá no painel + version: '1.1.0', + system: 'windows', + path: config.monitorPath + }); + + // Reenviar identificação em 3 segundos se não confirmado + setTimeout(() => { + if (!isIdentified && isConnected) { + addLog('⚠️ Confirmação de identificação pendente. Tentando novamente...'); + identifyClient(); + } + }, 3000); +} + +// Função para executar a varredura local e sincronização remota +async function executarSincronizacaoLocal() { + if (isSyncing) { + addLog('⚠️ Sincronização local já está em execução. Ignorando nova chamada concorrente.'); + return; + } + + isSyncing = true; + const pastaImagensSensor = config.monitorPath; + if (!fs.existsSync(pastaImagensSensor)) { + addLog(`⚠️ Pasta de imagens não encontrada no caminho: ${pastaImagensSensor}`); + isSyncing = false; + return; + } + + try { + const arquivos = fs.readdirSync(pastaImagensSensor); + const imagens = arquivos.filter(file => /\.(png|jpg|jpeg)$/i.test(file)); + addLog(`🔎 Encontradas ${imagens.length} imagens locais no total. Verificando pendências...`); + + let enviadas = 0; + let puladas = 0; + + for (const file of imagens) { + const filePath = path.join(pastaImagensSensor, file); + + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + continue; + } + + // Pular se já estiver no cache + if (uploadedCache.has(file)) { + puladas++; + continue; + } + + try { + // sendImageToServer envia o arquivo e valida no servidor via check-image-exists + await sendImageToServer(filePath, true); // true = isBacklog + enviadas++; + } catch (err) { + addLog(`❌ Falha ao enviar imagem ${file} na sincronização: ${err.message}`); + } + } + + addLog(`✅ Sincronização concluída: ${enviadas} enviadas/verificadas, ${puladas} ignoradas pelo cache.`); + } catch (e) { + addLog(`❌ Erro durante a varredura local de imagens: ${e.message}`); + } finally { + isSyncing = false; + } +} + +// Fila para processar uploads de forma sequencial +const uploadQueue = []; +let isProcessingQueue = false; + +async function processQueue() { + if (isProcessingQueue) return; + isProcessingQueue = true; + + while (uploadQueue.length > 0) { + const item = uploadQueue.shift(); + const nextPath = item.filePath; + const isBacklog = item.isBacklog; + try { + await sendImageToServer(nextPath, isBacklog); + } catch (e) { + addLog(`⚠️ Falha ao processar upload de ${path.basename(nextPath)}: ${e.message}`); + } + } + + isProcessingQueue = false; +} + +// Função de Monitoramento da Pasta de Imagens (Chokidar) +function startMonitoring() { + const targetDir = config.monitorPath; + if (!fs.existsSync(targetDir)) { + addLog(`💡 Criando pasta de monitoramento: ${targetDir}`); + fs.mkdirSync(targetDir, { recursive: true }); + } + + addLog(`👀 Iniciando monitoramento de arquivos PNG em: ${targetDir}`); + + let isReady = false; + + watcher = chokidar.watch(path.join(targetDir, '**/*.png'), { + persistent: true, + ignoreInitial: false, + awaitWriteFinish: { + stabilityThreshold: 1500, // aumentar estabilidade para raios-x grandes + pollInterval: 500 + } + }); + + watcher.on('add', (filePath) => { + // Ignorar pastas internas do servidor se estiver na raiz errada + if (filePath.includes('dental-server')) return; + + uploadQueue.push({ filePath, isBacklog: !isReady }); + processQueue(); + }); + + watcher.on('ready', () => { + isReady = true; + addLog('✅ Monitoramento Chokidar pronto e aguardando novos raios-X!'); + }); + + watcher.on('error', (error) => { + addLog(`❌ Erro no Chokidar: ${error.message}`); + }); +} + +// Função para buscar dados do paciente no banco SQLite local do Dental Sensor +function lookupPatientInfo(fileName) { + try { + const db = getLocalDb(); + if (!db) { + return null; + } + + // Tentar busca exata pelo nome do arquivo + let img = db.prepare("SELECT * FROM Images WHERE LOWER(FileName) = LOWER(?)").get(fileName); + + // Se não encontrar, tentar busca por prefixo (primeiros 10 caracteres do GUID) + if (!img) { + const baseName = fileName.replace(/\.png$/i, '').toLowerCase(); + if (baseName.length >= 10) { + const prefix = baseName.substring(0, 10); + img = db.prepare("SELECT * FROM Images WHERE FileName LIKE ? LIMIT 1").get(`%${prefix}%`); + } + } + + if (img) { + let photographTime = img.PhotographTime; + if (photographTime && photographTime.includes('.')) { + photographTime = photographTime.split('.')[0]; + } + const patient = db.prepare("SELECT * FROM Patients WHERE Id = ?").get(img.PatientId); + const firstName = patient ? (patient.FirstName || '').trim() : ''; + const lastName = patient ? (patient.LastName || '').trim() : ''; + const fullName = `${firstName} ${lastName}`.trim() || 'Sem nome'; + const doctor = patient ? (patient.Doctor || '').trim() || 'Sem médico' : 'Sem médico'; + const remark = patient ? (patient.Remark || '').trim() || 'Sem observação' : 'Sem observação'; + return { name: fullName, doctor, remark, photographTime }; + } + } catch (e) { + addLog(`⚠️ Erro ao buscar dados do paciente no banco local: ${e.message}`); + } + return null; +} + +// Enviar imagem para o servidor via Socket.IO e aguardar confirmação +function sendImageToServer(filePath, isBacklog) { + return new Promise(async (resolve, reject) => { + try { + if (!isConnected) { + addLog(`⚠️ Falha ao enviar: Sem conexão com o servidor.`); + resolve(); + return; + } + if (!isIdentified) { + identifyClient(); + await new Promise(r => setTimeout(r, 1200)); + } + + const fileName = path.basename(filePath); + if (uploadedCache.has(fileName)) { + resolve(); + return; + } + + // Verificar no servidor se a imagem já existe antes de ler e codificar em base64 + const existsOnServer = await new Promise((res) => { + socket.emit('check-image-exists', { fileName }, (resData) => { + if (resData && resData.success && resData.exists) { + res(true); + } else { + res(false); + } + }); + // Timeout de segurança para a verificação + setTimeout(() => res(false), 5000); + }); + + if (existsOnServer) { + addLog(`ℹ️ Imagem ${fileName} já existe no servidor. Adicionando ao cache local.`); + uploadedCache.add(fileName); + saveUploadCache(); + resolve(); + return; + } + + const fileBuffer = fs.readFileSync(filePath); + + let patientName = "Cliente não identificado"; + let doctor = 'Sem médico'; + let remark = 'Sem observação'; + let photographTime = null; + + const localInfo = lookupPatientInfo(fileName); + if (localInfo) { + patientName = localInfo.name; + doctor = localInfo.doctor; + remark = localInfo.remark; + photographTime = localInfo.photographTime; + addLog(`🔍 Dados do paciente encontrados localmente: "${patientName}", Dentista: "${doctor}", Obs: "${remark}", Data: "${photographTime}"`); + } else { + addLog(`🔍 Nenhum registro no banco local para o arquivo: ${fileName}`); + try { + const stat = fs.statSync(filePath); + const fileDate = stat.mtime || stat.birthtime || new Date(); + const pad = (num) => String(num).padStart(2, '0'); + photographTime = `${fileDate.getFullYear()}-${pad(fileDate.getMonth() + 1)}-${pad(fileDate.getDate())} ${pad(fileDate.getHours())}:${pad(fileDate.getMinutes())}:${pad(fileDate.getSeconds())}`; + addLog(`ℹ️ Usando data de modificação do arquivo como fallback: "${photographTime}"`); + } catch (e) { + addLog(`⚠️ Erro ao ler metadados do arquivo: ${e.message}`); + } + } + + addLog(`📤 Enviando: ${fileName} (${Math.round(fileBuffer.length / 1024)} KB) ${isBacklog ? '[Backlog]' : '[Novo]'}`); + + // Definir timeout de 45 segundos para segurança + let didTimeout = false; + const timeoutId = setTimeout(() => { + didTimeout = true; + reject(new Error(`Timeout de 45 segundos aguardando resposta do servidor para: ${fileName}`)); + }, 45000); + + socket.emit('send-image', { + imageBase64: fileBuffer.toString('base64'), + isBacklog: isBacklog, + metadata: { + fileName: fileName, + guid: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + fileSize: fileBuffer.length + }, + patientData: { + name: patientName, + doctor: doctor, + remark: remark, + photographTime: photographTime + } + }, (response) => { + if (didTimeout) return; + clearTimeout(timeoutId); + + if (response && response.success) { + addLog(`✅ Confirmação do servidor recebida: ${response.message || 'sucesso'}`); + if (response.originalFilename) { + uploadedCache.add(response.originalFilename); + saveUploadCache(); + } + resolve(); + } else { + reject(new Error(response ? response.error : 'Erro desconhecido do servidor')); + } + }); + + } catch (error) { + addLog(`❌ Erro ao ler/enviar arquivo: ${error.message}`); + reject(error); + } + }); +} + +// Configuração interativa via terminal CLI (fallback) +function runSetupWizard() { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + console.log('\n======================================================='); + console.log('⚙️ CONFIGURAÇÃO DO DENTAL CLIENT'); + console.log('======================================================='); + console.log('Pressione ENTER para manter o valor atual entre colchetes.\n'); + + rl.question(`🌐 IP/URL do Servidor Destino [${config.serverUrl}]: `, (urlInput) => { + config.serverUrl = urlInput.trim() || config.serverUrl; + + rl.question(`📁 Pasta para Monitorar Imagens [${config.monitorPath}]: `, (pathInput) => { + config.monitorPath = pathInput.trim() || config.monitorPath; + + rl.question(`🖥️ Nome deste Computador [${config.clientName}]: `, (nameInput) => { + config.clientName = nameInput.trim() || config.clientName; + + try { + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + console.log('═══════════════════════════════════════════════════════'); + console.log('✅ CONFIGURAÇÃO SALVA COM SUCESSO EM config.json!'); + console.log(` Servidor Destino: ${config.serverUrl}`); + console.log(` Pasta Monitorada: ${config.monitorPath}`); + console.log(` Nome do Cliente : ${config.clientName}`); + console.log('═══════════════════════════════════════════════════════\n'); + } catch (err) { + console.error('❌ Erro ao salvar config.json:', err.message); + } + + rl.close(); + resolve(); + }); + }); + }); + }); +} + +// Função Principal +async function main() { + const args = process.argv.slice(2); + const needsSetup = args.includes('--setup') || args.includes('--config') || !config.serverUrl; + + if (needsSetup) { + await runSetupWizard(); + } + + addLog('🦷 DENTAL CLIENT - Monitor de Imagens'); + addLog('═══════════════════════════════════════════════════════'); + addLog(`📁 Monitorando: ${config.monitorPath}`); + addLog(`🌐 Servidor : ${config.serverUrl}`); + addLog(`🖥️ Cliente : ${config.clientName}`); + addLog('═══════════════════════════════════════════════════════'); + + // Iniciar servidor HTTP de administração local + startAdminServer(); + + // Conectar ao Socket.IO + connectToServer(); + + // Iniciar monitor de arquivos após 2 segundos + setTimeout(startMonitoring, 2000); +} + +main(); + +// Encerrar processos de forma limpa +process.on('SIGINT', () => { + addLog('🛑 Sinal de encerramento recebido. Desconectando e saindo...'); + if (socket) socket.disconnect(); + if (watcher) watcher.close(); + process.exit(0); +}); + +process.on('uncaughtException', (error) => { + addLog(`❌ Erro não capturado no processo: ${error.message}`); +}); diff --git a/dental-client/config.json b/dental-client/config.json new file mode 100644 index 0000000..46af802 --- /dev/null +++ b/dental-client/config.json @@ -0,0 +1,8 @@ +{ + "serverUrl": "https://rx.scoreodonto.com", + "monitorPath": "C:\\ProgramData\\RF\\Dental Sensor\\Images", + "clientName": "COMPUTADOR-LOCAL", + "clientType": "windows", + "apiKey": "rf-dental-secure-key-2026", + "adminPassword": "admin" +} \ No newline at end of file diff --git a/dental-client/package-lock.json b/dental-client/package-lock.json new file mode 100644 index 0000000..e358500 --- /dev/null +++ b/dental-client/package-lock.json @@ -0,0 +1,2628 @@ +{ + "name": "dental-client", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dental-client", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "electron": "^28.0.0", + "socket.io-client": "^4.7.5" + }, + "devDependencies": { + "pkg": "^5.8.1" + } + }, + "node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron": { + "version": "28.3.3", + "resolved": "https://registry.npmmirror.com/electron/-/electron-28.3.3.tgz", + "integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmmirror.com/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/multistream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.80.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz", + "integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-notifier": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz", + "integrity": "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.5", + "shellwords": "^0.1.1", + "uuid": "^8.3.2", + "which": "^2.0.2" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve/node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmmirror.com/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/dental-client/package.json b/dental-client/package.json new file mode 100644 index 0000000..ce81874 --- /dev/null +++ b/dental-client/package.json @@ -0,0 +1,13 @@ +{ + "name": "dental-client", + "version": "1.0.0", + "description": "Cliente Windows para envio de imagens dentais", + "main": "client-monitor.js", + "scripts": { + "start": "node client-monitor.js" + }, + "dependencies": { + "socket.io-client": "^4.7.5", + "chokidar": "^3.5.3" + } +} diff --git a/dental-client/uploaded_cache.json b/dental-client/uploaded_cache.json new file mode 100644 index 0000000..4abf3f0 --- /dev/null +++ b/dental-client/uploaded_cache.json @@ -0,0 +1,2514 @@ +[ + "00281353327345d499e27393bdabe423.png", + "005314d3e27c438a96174826720f731f.png", + "0058a58bd9df4128befefce3d4d55f26.png", + "00643c222803447eaecaf6d8e1461f46.png", + "00cf4b59a2f846d980c8e914e35c539a.png", + "00d7db1a987a41ef82cda145cc889795.png", + "00f14d2a3d6446bfbf55ec44cbd28666.png", + "012d8f6f1faa4ed6a15617074038e277.png", + "0135dc9d4d514b58ba358fe8301bd35a.png", + "015810d777af4245b301753a74ad7556.png", + "0172ad79c7214e2995aeaeb9a62d15c5.png", + "017ed1b735db4b059d8bfe8fb6cebfc4.png", + "0193dc90d50a43048daa083c9dc9b47f.png", + "01997d48350143889a80076d9ea08cf6.png", + "01af6adf8ae4478e8ee72563cc943039.png", + "01b4fc152e8f4e40b59a0e3ea0aa2052.png", + "01d5f3205e2447e2ae6dedfe0eae6d01.png", + "01f6c539ce19424ab149a0fdf14b2c16.png", + "023ef7a7a76a495bbbec824345f85e48.png", + "02a2b66a448e47b581b6cbb51efa1c6b.png", + "02a9d41ec88949538c24dc6b5a4b86bd.png", + "02d532f755154b798ececec1292d0cff.png", + "02dcaed298d644328c8eb2474779f7c7.png", + "02e4389dae9d46a1a310f6cd1ecefb5f.png", + "02eaa40ef906401fb1f882ea0fa75f93.png", + "02ecbdb7e069455abda4107ebd6ef62b.png", + "032a9e936daa4004ba73b421a4a0403d.png", + "033046d2eb03465a92e165740a3e01d6.png", + "03514bd0703c456d8e507fbd4775547d.png", + "035dd66bbe8e400cbce4d521aab91404.png", + "0361e22a8ed04bc39cb34751438ddfd8.png", + "03871a9e411f46939e902dca6dd2c6cf.png", + "03a1bb85a1d14f318fbe2eacd91b0cff.png", + "03aa6b501b9f4b6caff0b2b7484dbbef.png", + "03b113d77dfc43ffb571623777f05687.png", + "03be788eda3d411f8ed0baa7524c5401.png", + "03d75288ff7c4b16a37ec236d3d30ad2.png", + "03d7e2a1947547d1a79166666b5f923f.png", + "03d83c8b8cff412c823ba34ddd4fcbff.png", + "03edf0017cfa4b65835d06d18e7a0a56.png", + "03f911b34ab74e8ca8db39ce6c83345c.png", + "0444e8f668dc42c2b322fabc3f153414.png", + "046ead7a63564f16936017818c3ef398.png", + "0486cc3cbc854ba0833e79230ff29787.png", + "04884c919e20446280a2a9ba46d55e5a.png", + "04930573e4f5449aa5738698a927ec48.png", + "0498124f697d4cbfb92618ade839e06f.png", + "04b163d6eeab4497b3e8b4e7f4e3bae9.png", + "04b5a11a3e63486ba7149a1632438385.png", + "04d56a2be39e44cca1085fef3b8f86a4.png", + "04d90a48136c46be9a907d2b54091ccf.png", + "04dd6d136a704d9eb6cb182f781ed203.png", + "04e1fa035d534802b60b35da46ef8823.png", + "04fb4cd815b64041b94e952d32fba628.png", + "050d72eac2a5437d8e0d5e3bb72cf2dd.png", + "0514aad3d08a4bc4a1c9071acb7c897f.png", + "05155aa34b2742b09b64a289af1506ba.png", + "0515719dfbb545c1b29cd6a939f56e60.png", + "052fd09df22d43f7bd5bd9a596d380cb.png", + "0548f2a257244971a28b5a7c57cc7662.png", + "05491ef0a296406fa6023713a0cd8ccf.png", + "05497d3251604e4e951cf3aa6427a2cf.png", + "05622e0491ae41dcb7668728ceb8bc6d.png", + "05b24e975c7a4dcfbf42370cdf371bd8.png", + "05be4a13540b40e0993059dcb4c892eb.png", + "05c16342af1740cfa1c1fd78cf2e8ab3.png", + "05c28c8654d2464d93d7cf1055b42dda.png", + "05c32aaa35274ac2aa72a517e9b99858.png", + "05c73821c0824c41b70fcc39891ed77c.png", + "05cb7ccfd26f445cb13457e4f23e5813.png", + "05cbf130345944f187b9134d83fa3d8e.png", + "05ce4d690fb344d69e2cfeb53e4fd447.png", + "05f7f7c708c14d09b488640058cfaee6.png", + "0605c31ff5c44e50944f552454648c88.png", + "0605e0f869cb4cc7b58923b52aa07e70.png", + "060c1e5e85fe458392ebd3a9e7b9a44a.png", + "06635bdcae8b4720ab879bd989ff4cb2.png", + "06a38695f9d7424a8c354f8e3a6885b6.png", + "06a3cee77c7145b995b819da3d7a1fdf.png", + "06a8f55dea134e75a8083b95f90a0923.png", + "06da7a2080a7402a8782e66034501a95.png", + "06e2980c29784bb188dd7aee87187e78.png", + "070c4ad0fd354ba0b4f3a641236e497c.png", + "071adfbf4ada48fc81f6703f7e79e181.png", + "0728921d66094f41adadd20bee66f99d.png", + "072c919cd796466bbd9aad5de3b368a9.png", + "0734df53b74849acb99a0ed207fe9ff0.png", + "073a115f95e14b6bb33f28e941c87cba.png", + "0756b5b9ea9a4def9cf902614570f20b.png", + "076958a5a6264217954352a77cecdf23.png", + "077509ae634b4d9e8bb6af4fa4aeabd7.png", + "077aa3fa525e4e04a91e164796e880b1.png", + "078344496c454d69a5a7c84b599d7fc5.png", + "0790b25fbcc44bbd82bcf70e8472b840.png", + "07a003801e7e4afb8fea660666742350.png", + "07bab580bcbf4bb49e389965ad4fe0e2.png", + "07de95cd2abb4129aa53beb374359d30.png", + "07df504801e54985ad50468e1dfe33d8.png", + "083d6f71a4ba46f1963830b334e0d1e2.png", + "084e6a48b29d4e3c971230b2dd3beded.png", + "08542197606e4f6ebec1d1c0d8396781.png", + "08578a35d5224da6b0be5f52876bb3b5.png", + "087f498cba1447edb9b1e797411cca06.png", + "0899084330c8445e97a4e188a508595f.png", + "08b53d093fee4b1a8ba6962670e0741e.png", + "08bedf2d2f3c496680f9d3abd922412f.png", + "08cfd0b4615d4aceb29343038b496fef.png", + "08e26fb334c2475d98d4dbb0facbaab4.png", + "08e54b59a6524dc6953013667bd0d4c1.png", + "09010820f5cc486682044aae0493f7d9.png", + "0904fe271fce4f0cab90d0ae2201a50e.png", + "092179a29bd74650b9d93604f1db43ac.png", + "0947d53f17dc4975b094a34e7fc80532.png", + "09504ada25a14334ad9f0ec0d8ea41ea.png", + "0962514369ef47e499ec88c0f5ead168.png", + "0970e73396b948dc858b81ed941df10a.png", + "097697ad71924be7a879c1e3598703c0.png", + "097b79b25dd0422094634de2747694ca.png", + "097bf4c7e069493fb5639f81d65830c1.png", + "097e5af70f9e4f7196f1e1e4665815c9.png", + "09bfe7147fb64f9284972b0167297f57.png", + "09d3a82846ef4bdf97b16493d71723af.png", + "09f9f4f613764df1ab71ee537efaceaa.png", + "0a0cd27b6bc54c7088ca3c849570db22.png", + "0a209ce579f74903b6881253595924bc.png", + "0a2c5d1a947344f986c5c596dfa6bf22.png", + "0a5bc3dcfba44ed2938caea63694c0bf.png", + "0a7456bc92124641bc43c0975a78abef.png", + "0a81fb3cb4bc4d35a3e66f7cc2f54e71.png", + "0a92688459044147b5d914c290ab60d1.png", + "0a9caae978ff4977a0c8c78b4f10e3f0.png", + "0aa0707f112a4804904b1893a6d2f79a.png", + "0aed18b3bf0440aab1077b03a5e79453.png", + "0b03f2112d6b4f36839c4c8d45d4fa17.png", + "0b2f44f45a064971a49d3b07836e8c4b.png", + "0b3ae105fbc84d28857ea432b76c535a.png", + "0b73f94afdbd4217a25a1401fe5b264d.png", + "0b85443450cb4c63ad937e2a68be2680.png", + "0b94a56013594f098f6ddfbc78b960fa.png", + "0ba10d47e40449baa17ae987806c0ca9.png", + "0bc53694fc2a4cc7925f434fcc90e9c6.png", + "0bfd0fb03c2f4277bef74aa8ed08153a.png", + "0c4904bb658e4d148adbbb21d50cf6b1.png", + "0c60b604bbe140f6942afd82e86423ec.png", + "0c7783da23494cbd89249063cf2e716d.png", + "0c85367c54804cba9216b04057976d18.png", + "0ca1e1ef4ffe49e5ab2f368520a761dc.png", + "0ca5a992349746d0a250478ea4a10f1b.png", + "0cb895815f4f4edeb2878b864c2d4c3e.png", + "0cf4e5d0eed1433da569ea172e8127c1.png", + "0d04c662957a454292fb95c940304a97.png", + "0d5969a583a34ec1a5e5005050c5423a.png", + "0d885139a4e445ab8a0f3021b32446a1.png", + "0da3817af3ad40f4bb0d2d37e091aacf.png", + "0db053dddccb45659ddd48e50d600a3f.png", + "0db8f4e755264eee99759b01abb9a365.png", + "0dfed8b013b941ac97b0272d214f0cfe.png", + "0e61923d4d964f2ea9bf44bb820c6b86.png", + "0e70235967ba49daa7f79c1f1d4a89c8.png", + "0e74f804ba814820ab15e1112bf263fe.png", + "0ec541c97f454522ba36c0601c216603.png", + "0ec6f54ac58e463aa6153fe02f636bb4.png", + "0eeca54856d244e88c891f243342ee70.png", + "0f0e0302d17b4cc68b0d2294d522e25e.png", + "0f2e13b10a4d4b05a816645d024ab1db.png", + "0f55ceae0a42405ab2340875483891d8.png", + "0f5c5cd95f9d44f2a3ac65640ee104a0.png", + "0f5ef9e18f2f4f6da4818424cc4527bc.png", + "0f7e67b71cdb4e9b88634ad9e70f0a25.png", + "0f82365aefdf4321be64f7f3fbf466c9.png", + "0f8a25d28a43449f860d53970a8bfd48.png", + "0fa73f86420f43a7b131a7296c8c81fa.png", + "0fa8f55b715e4f6f85c4cca34b6bb07a.png", + "0fb70b213f3c41c6a9f4e929a5a62938.png", + "0fc454cc12d14c72a0f0d3d2644fef96.png", + "0fe8536fbb3e45fcbff055347eede002.png", + "0ff4a183af174f06831b468e25a1ec28.png", + "0ff4da10359d4240bb6b3cc4feb003ef.png", + "0ff83ca6af07419f97af87bcb2a807a7.png", + "0ff8b0b241db4aaa863106b38ad9426c.png", + "0ffc5c814cc04e61a53e4a273906c6cc.png", + "1037747f7d344ad298090b3e6442f196.png", + "105105cac88a4d7e9748c65e0d428ecd.png", + "1069665f79ee46b2824949d9b2b1fb3f.png", + "1084370897ed49029a91c498aee73094.png", + "10bc4e0698c8443a80189f1cec307074.png", + "10cf4670c79c440fba09bb1323702546.png", + "10d205516063481b890a0c94996c60ee.png", + "10eac61f12a54b0d8d668348fb250ea3.png", + "10fca3e5cb5043beb3354314309c2efa.png", + "1133a7ab280448a1ab4d381adb9e690c.png", + "115f78027cf24c6493dd6c245b9ccc85.png", + "1171fc08982a42128a5b29b91a330959.png", + "1174718106ec4891b3413645a0ec53a8.png", + "11a267b9dd3743bfa0ca67057aae10a7.png", + "11bc9d95e8da4070971ab926e9d1140d.png", + "11d6b397c6164e36a09a6fb05c8f5ea1.png", + "120703c448d14827a9f1306af28a8d87.png", + "12289861941f4cca824963ecd314a20b.png", + "12536fe610944da3a79ce6788fdd58e6.png", + "12885091c04f4b239af261c26e8b76e2.png", + "128c2315f4c248d4b65603dd1ebeb696.png", + "1291f0baeb044657bba5334b09bc6a66.png", + "12be9c9f785d4bfc898d666b3b9e5e2f.png", + "12bfc4d42db54eeeb46791c6465286e0.png", + "12d3f39d92254191b517604f2f7665bc.png", + "12e884c155844906a5b834f0c87b3cb4.png", + "13543942c53c41f8af284c2f7dc3a5db.png", + "137c2d6f3fad47b6874bf2985bfac4ef.png", + "137d4c9e24ae4946981f8c9d3951b6e4.png", + "137e08b5bb5745179585cd2bbd6ede63.png", + "137f715155864100b3daad031d6ddd8f.png", + "138e74fe82ea4c73bb27c26a9aa41bc5.png", + "1398cd2f0295433d980a687b883a5cff.png", + "13b5717199124749ac027785b33efb82.png", + "13b8cb6c1df74ddcbc5dc8d05622c2d0.png", + "13d7cee18bd84cf2ad3efbd18c42adf6.png", + "1408468e0a724127acf87d0ede471c40.png", + "14175f0281a148ce8feba8a218b84230.png", + "141955d41951429b824f185a9adda580.png", + "141b631c8dba44158f12dc9016e50573.png", + "146575b9e2c04af59d7a0053040a697a.png", + "146ef3f941974559b53a993394baa840.png", + "147683304b444768a8edda3647c8597b.png", + "1478523042a9435f9b1d8779c0938608.png", + "1494dad960644935801d44aa497b5183.png", + "14d4aea6a19744ed8795d36b50f3f379.png", + "14db6ed0f8854d16954fe96786c6d401.png", + "14db8b3a19324c9dbf1704cadfd044b2.png", + "1509c14f85b44b69ba91646ce3566b86.png", + "151105a5742246a8a1625e1f412366c2.png", + "152b9839a9bb451682334286d3b3c690.png", + "154ae34c41b94960b7684ea66e77dc61.png", + "158d50c1a534480b8efab8719b263972.png", + "15c3ab8f642446489f09ce7b70697bf0.png", + "15dd33ecd66a40e89b262d6e8b7b2287.png", + "16142432d7ee48299335ecfa888cf69a.png", + "161b3cd80a8649f7b279dd8820d4f36b.png", + "161ef3cacfb042b4828ca22f9ffcad0e.png", + "1656b546e1124ffbb9e1b8fd2a10427c.png", + "16635791f5784796ae4eba1e55bcdcd4.png", + "16843c444dff48179f20261f4d1e49d0.png", + "1688bdfa0c414884a7200da373403126.png", + "168deae30fd740e7b47ebf15c879a016.png", + "16b11472f493481a86bd1c37e3b53b0d.png", + "16bc8dff2efd4e9e87063980ede35375.png", + "16c59b8f49984aec9b9c501dda8f5636.png", + "16c7fd41677a4122ad15475156b9c177.png", + "170e9eb99a23452a8e4ccddde8738b79.png", + "1722cac3415942568a128c075a1cc57b.png", + "1723be24e3994cbba9cf32fa329116ce.png", + "1750a56c06e14b1da2e8ffea4d0f4814.png", + "175cf713b4e7442f9e8f0b6cdd83b774.png", + "1791c74ee2cc42d3a5b8a869499d2cd1.png", + "17dc7368468c449b95e722c5128ab3e8.png", + "185698e89b4a44c6940b3e87fe3745a0.png", + "185fa76cdd36406683f6670e0d95a4cf.png", + "187e6eb8718946d796d6a093afc69104.png", + "18b33771f2b3467285251a5401750b9e.png", + "18c845d352af4c6a90221c2f20051a86.png", + "18cd645f603e430db306421456eb507b.png", + "18fc518d847e4a88a348520582fa5782.png", + "1931d33f531c49c482433a328681b3ed.png", + "194a3d95703c4a81bff175dbbba5725e.png", + "199a7391b4e9488fb6fea62bf05c9e0e.png", + "199e4a538bdd474a9040329a7d6b8206.png", + "19aed4c5f38c4d078da9ad3d89773c10.png", + "1a0bfebf098f45d2a47fbf1d81d14599.png", + "1a1aef81b912468f8a4aaa60e8d9aa3e.png", + "1a29154f701c49d69a08940f8338b4c9.png", + "1a302b83af3f43f38867b2fdbb33dd9c.png", + "1a61cf7a7efd4a28b20f4f494135605b.png", + "1a791ffa5d3145d6a67630f2b1ec5787.png", + "1a841ed55f5841dab92b99a214c2c1d2.png", + "1a85c308311449e29d822fdba230bc13.png", + "1aa3e3890dfb49f7aa5cc3503f0168b4.png", + "1aae775432b34f90a1a83074a38f6090.png", + "1aaf3faf55db4de0ab0a4e8b3e8ba87d.png", + "1aafedcb26264fcdae7a441f4832fede.png", + "1acae052ac394cde80cdf3b597b2051e.png", + "1acffdaa567e403b9133f12a0b6683c6.png", + "1b08bde9406c433ea217fc163e185bde.png", + "1b2c858f9d06419e9fd3a4c4fc37f245.png", + "1ba37ff0382345f698b047ae0c7bdc31.png", + "1be88ab6aa644d74a9c2c5336c840b7a.png", + "1bff1efa104d478bbe1cf1851f1aec3a.png", + "1c082bb1585840e1a887104c7413b374.png", + "1c16dd7e265045edbe2ee03b48efe86b.png", + "1c27367e128d4662a642d4ae823a770b.png", + "1c59b4248d4140c1834f046b338a169b.png", + "1c7cf778f4894856ab99497a301dba52.png", + "1cc3d4081a4f4e73919bd85c1026c03d.png", + "1d0aea23fb7c42b2a324f619fd11b506.png", + "1d3ce4e75fb74b509d45b07eb55dde71.png", + "1d3d7cedd58648babb5ca2c155f644f0.png", + "1d5a76cdcbf041cd9b57ec5116b4bef4.png", + "1d66a888bbfd442b9845412d5766b869.png", + "1d7f0e9e1e0048af994706591894fe8c.png", + "1d809ef67fad4a9e88fe63aeca4a55c0.png", + "1d8b487f8d824d89a815ba4df624b43a.png", + "1d9b81e3238c4fa78cb7832b94aa4493.png", + "1db94a3be9d8499a9841178d78187c36.png", + "1ddbf90fd6a94176b38c0950be4250b3.png", + "1ddff5c734dc46b6bd52f2b53e55cb61.png", + "1dffa3f4834e47c0a286d1bc9cc44480.png", + "1e077bc44e1546a4b5b9e9aab03f12f9.png", + "1e0d046680ac4caa9ba24c8d317795b7.png", + "1e15c5ab14bb4f7d89fa739c16d97c02.png", + "1e34fe281b7a40018c5d780329a6e05b.png", + "1e5f0f462d6244e8a595f1e1afc90397.png", + "1e78c72461d546f5ae81b0fd0b124533.png", + "1e80b0a1ab17414daf17fdb2dbc3bbd0.png", + "1e8eaf16f9d24b28b42b1d12e467de38.png", + "1e9440d2b7cd4477aff8a298553a0830.png", + "1e9d70f0e130415da254598a56a63ff8.png", + "1ec22a9c5a0e4b109c9f44be0ed9d3f3.png", + "1ec7cc9b04e04eaab4e65b3b09d3c24e.png", + "1ee0828fc5774d7d8ff6ec541fe0a1a9.png", + "1ee2b2a0c4f7407e8b3adb47164982f5.png", + "1ef048551a3f4aab9a61b8573f508e84.png", + "1ef7d16b2aaf4b8ea0b75bc9f9f00f0a.png", + "1f0e315d18154060a3497617fb39735a.png", + "1f2fe794cb8744d093ac385bf7f3fee3.png", + "1f39f365bda446348785e79abb6977d2.png", + "1f3c0dfa3a764a21b53f51e5a1879478.png", + "1f4fd3e84bc14bc286506ba56f430f7d.png", + "1f6d1a7e6ece48b8997a39b674fc9d47.png", + "1f9b1dd1d0384304b64c68a3de0fabb2.png", + "1f9fe197d36d40768873dbe148133765.png", + "2034395595c94c88bd2bcf5f89b00d9b.png", + "204d826b329d41beb10444b43466ac15.png", + "206865a089df47fd9791bc76c6f45440.png", + "206b3b5b72854b5eb3b103283294406b.png", + "209112fc3cdd4366b184e2b3cc9714ed.png", + "20a0729186ba4a16b965ead25096b248.png", + "20bb9ea2bb814d31a196c638fdd5e779.png", + "20ed55ee72d64d34b35715ad2f615ce9.png", + "2120a16dbb344eebbec8eba07bf1e6a0.png", + "213b94d4ac234be895c496f3ac48fa7f.png", + "213d9e9c2ff542d9987b655702b90a2f.png", + "21413c871aa0476585f86020157307f4.png", + "215c55ad0f2440a18ef4377527998b6a.png", + "217786897a7c43e780e401a30c2502af.png", + "218f58715a7646e3bf9fa073a2b898ab.png", + "21a96171405247d1aee52c222807e793.png", + "21ce6fb40d2843188c2e3b089a3944c5.png", + "22172b865e1446a4b3d318e6ff2b42fa.png", + "2221989990f548758cf2e1cfdd5ad7ae.png", + "2259d89293a34f548120fb1baa3874ec.png", + "225b569bb7864469bf4209b6505b4b95.png", + "2287ac57da0247559dfc01a13750ef71.png", + "228a48e4566b41f0b3df46f3db759c31.png", + "2298bd67cbb64128bb99bf293d4591c3.png", + "22c60f70e3044450a9eda7960fadc6c5.png", + "22df465dd98340ec8561cd501d651520.png", + "22e6b4c6ffe94c40b5f7487c25365808.png", + "23084757e30e48f4a918874d7770fd03.png", + "230c76e378fc46d897ba7deb3f757af3.png", + "232dda70f6e84c48be9e436bc246beeb.png", + "2351b42774f24a58ad50d42c321d37fa.png", + "237ebec6e9304a72874cab3922b911f8.png", + "2387ce6acc7e402b8bff5265e0d29772.png", + "238884571d9946d5bc7da5c2bc6c5ca4.png", + "23d8c3cd805346e0bfbcc48179f3950f.png", + "240ddf2f58594ad084df0808ccceed8c.png", + "241307acd8ab4a81858e757b12d9e2ac.png", + "24610703ad1b4de7b4cd0707b5350925.png", + "247ca50fdbeb49e88aac273752e52e8f.png", + "24917a823daa40ed83295aedf48605d5.png", + "24ab11d526a8415b9084f8041519f3bc.png", + "24bd13a6ce564bab84d94f58b45cf48c.png", + "24e2564084b74c3b9a73b0c440b28220.png", + "24fa44c6dfbe49f180cf9e03baf382e6.png", + "25075293a6494d47bd86436a6b962268.png", + "2515dd6e48dc43b0a56f9e0e35e697c5.png", + "2525947fba184306a89068723a93172f.png", + "252bb58905d34a858c5f5b77680e2467.png", + "256f7666da264d30b4339219cce23440.png", + "258ed01bf29643eebfaa7ffcd7d3f9fe.png", + "25a44067ac4d4e76ac2f5dbd240a954a.png", + "25b0bba49a2a4fa0b74489b986660eaa.png", + "25b9da094bc24669b7c058ad4575ccc9.png", + "25bd1d39b8084cc8b610e969083f22e6.png", + "25c4c44165c9470d868acd1ddef5990d.png", + "25e6b983f4824215af30cafade809456.png", + "260a3657ee214a0c8d126ce408d7fa14.png", + "260f8107690741d9ac5eea9e148533d7.png", + "26105ae683ca46ceb8d2e90f9c068ea2.png", + "261e745743fb462b9970dcfcca0c1b26.png", + "2628ecf39f024cb4b8df5248220e9706.png", + "26410ed6224448a7bcf8ed928a90afaa.png", + "2677e14b47d74442b9ac20b10a0623cb.png", + "26b7dd056dc7490cb40f4a2dfc2e1043.png", + "26b8dcde41bc444cb6c99a69c37edc88.png", + "26bc46adbad54be8ba2d24bde6046827.png", + "26e94d7c2eca4822b5e8f056192b6968.png", + "270ed0bb72b84063900ca2dbf66c3b4d.png", + "27240378dc8c426f8fbdb3be49c6a0a5.png", + "273969e957a64312a234b4bb0fda05d0.png", + "27479de20234412b8a1c9fe0b240e831.png", + "2750636f0865470ebcfd1f3ff4337fe0.png", + "275b88b33b6f46be8f2bdaefb7ffb65e.png", + "276aec22bdfa4d8d839d69b5288eedf2.png", + "27714623949b4397a572d0eb863a2b25.png", + "277e1e49a25f4c29978474500032cbee.png", + "277f73bfcc2a4786af0633120bd992cc.png", + "27b9ccc2af2643a28f08cf84e6bdaa57.png", + "27cf99bb6e38429e89d6d1c779bac60e.png", + "27d7161d016b4a6281f985b243a81f85.png", + "27e038f9292449da907ff39d432ac4d9.png", + "27e2ed36b0af48cbad2f941b7a0ea75d.png", + "27eb43611b2e43c9b941cef8c8406a80.png", + "27fbbbd178b44a58a4f49dbc1f143ca8.png", + "27fd6bf831894f5cb9e464893e0c65be.png", + "2802ef766f83414aa31368d9d488e34e.png", + "28136eb9ea574b10b6ff95a85675c5e4.png", + "28497937f8da434e95e338edea970022.png", + "284f429a0d5147dc8718b32c6a3f0409.png", + "286ec99d185843868c3bd3876a968a36.png", + "2871a9fecc37485e8ebd6e471a3c0654.png", + "2877008ecaf942f28e04028644ae8518.png", + "287d7e665c87436ea9b3b43b1131ccc8.png", + "28879f6b1b3d4ebf8bca945fdc45aca8.png", + "288b7b9252f549a49b80334448e82b61.png", + "289d4890530448d89577b0fdc9feab6a.png", + "28a9c3a8a8c2430e9e26e1edc2ff1d78.png", + "28e3433dbfcb4fd1a143e43ff309b37f.png", + "28f964c93a414410aeafd9a42832c2d2.png", + "294c78ae4f824f7fba4961c1be912484.png", + "294db55298f14799829e9aef083a81e5.png", + "2957b9888c0d4e19a6b1d6e7d1182d36.png", + "2976542a005b4fc3970dae9b0c0ff9e8.png", + "2986a6abc16f4c649064e8c771d2732f.png", + "298deded70e941d5a8e97c49cae0c600.png", + "2990194d6a1d40bab41bfefcef5e8672.png", + "29b54c02f30d4cc0893da73154979323.png", + "29b65635a4034233a32227fd512ed114.png", + "29d2ad6239e74b219d2dda5d7082b55e.png", + "2a06346401da4a8184a1d1ab8fd101e8.png", + "2a2b1e6a0fca4a48b158d68b2a80328d.png", + "2a3abf77415d434c9bc819b5695955c8.png", + "2aa1fe899741434c9a61ca3a09d325f7.png", + "2aa4330bacad4341b3cb36e7e04b8802.png", + "2ac8ceb00e464bdc92d76e1828169310.png", + "2acfeaf297474f9eb7d1d374ed5b4ee5.png", + "2b08bb8e90244e1db0c151fe6a3fd671.png", + "2b15f68f3ea64cba8fb2ea2b9e50abd2.png", + "2b1656d6667145c499c5b00e8920ee75.png", + "2b2d29513a6c4e72aa2719653f9f8747.png", + "2b384a4a4a4d41fea71e3838fdf22707.png", + "2b4b21eca993439381034e6a811f9239.png", + "2b94c7dd135540ebb9aa303d63e70eeb.png", + "2ba4ec25cdb94c88a44ddc4fa75700a7.png", + "2bd36181dc5f41d7badf30ec56bfc07f.png", + "2bed597276d144fb9ccdb8ce26aa848e.png", + "2bfe1dffeb2d4b438f07e59b3f621051.png", + "2c1b2509e59647b6bc14dd032740374a.png", + "2c48c132c8d74ea0a8a672bc8b305d6d.png", + "2c771a7cd6a4404ca4bf64f3a1a29f6a.png", + "2cbc2364e5d942e0987809d2fd9fa90e.png", + "2cc6c70c4e8e4d5f8dde431b17d942ac.png", + "2cd3ce613cbe4bf58709be94b13fed78.png", + "2cd7447531484b9f9b052cd2cd179969.png", + "2d14e07f5baf4d8e8d688e9caa5959c2.png", + "2d3798c503964da9a90add0049a4986e.png", + "2d586ac9028e4c5e9be9584d22ae75e2.png", + "2d695f13ccd5486797e4f5d3b449adc7.png", + "2d79790e5dbc4461a78e65090a8d9372.png", + "2db9da7f28974a8f8f27442253dff388.png", + "2dbdcb40612c4ebd9e5771ae699f1153.png", + "2dc238fa46a14cca9ebc30acde867d64.png", + "2dda6eb21598468f93c24ca0b099578f.png", + "2dede2d0b98541d5a1d45426d43bee28.png", + "2df29f9d302d4e47b4c920486d688f17.png", + "2df4879531ae48caac6f3a126ddf6fe5.png", + "2e1f982fc4f14b5bbdddb715d1ff62fa.png", + "2e20f96df58a4bcb9c2994e349d62d31.png", + "2e2ae704c297423faaade1b3b39f3a78.png", + "2e5a622ce6df405eb0d612c2d9d364e5.png", + "2e5e3740f0c04abdac94364db354993f.png", + "2e7732f0154d462d8fec789cb65b0744.png", + "2e7abdd4ce624e47a689fc440ba04d51.png", + "2e9f157999ab49478a57669f5bd59eea.png", + "2ee2b3ac3f7b44b08f4816dde251911d.png", + "2ee4608d472743ee934127564668aaf7.png", + "2f0a0b2f01c14da5a39d7214e43b28d6.png", + "2f0c285e07004a918917e3b4c9ff3d9e.png", + "2f18b018f90d494dae663b67e3c0e84c.png", + "2f239ebb594e4a10b02d5cba1fd497ad.png", + "2f7b63b2c5074ca685bdf05dd6975783.png", + "2f8103f620f441039981aaf60cc6c8be.png", + "2f85f1a7d4f643d0954015d240c7f3b7.png", + "2fafc370b0d04689b511eb28d399d103.png", + "2fba9f20fa8d443b9eac7e14badf1a79.png", + "2fbf2aff613040d4a4cc3827d97cb952.png", + "2fc85a9e3fb74fb9a4dc0a0ede70998b.png", + "2fcb6927e7234d4f999378c84157c10f.png", + "2ffaff361c004652bb9439d8af0237d0.png", + "300b3be9715e44b0add35c287f7b6f73.png", + "302c72f88e7a4e3e8190f791be7ba300.png", + "302f095e91ad42ae92a0baadf27b02ed.png", + "3048101de65c4610989b19f7620a5dc7.png", + "3048736514aa400f8fa850c80a69f85a.png", + "3054b6e87f0f413d9760fdbe4a4a29e8.png", + "306413f2f9284bd4ad28f7620d607a7e.png", + "30cf9623f98642019df09669f7eb0fc0.png", + "30eaf1409ae94ce19c80511ef1a841a0.png", + "313b086ab00c440eb51f44e1af5f2ec0.png", + "3103896178c04718b92c28830829edf6.png", + "3140e6f6ebe548ab87629614f75ddc79.png", + "3142c4390d7542dab2ded6dcfff15b53.png", + "3171c9659d1e4177885d7b4e4bebec56.png", + "31804d381f0a49a68fbf0294a983c6cc.png", + "318dc8ac9a59472d9a090d28572fc2aa.png", + "3230a3d81f354cc9a568202f429b6c41.png", + "32b908072efc47a6aa5094e7214510aa.png", + "331f827682ef442eae4297b19c3f874f.png", + "3343ed0458f2458b8ba0ff3013786df5.png", + "3361573e1a9145da9b965361d07cb87d.png", + "3369b841586f49ef9304fa3c790cba97.png", + "3375afd7d44549b5a3545ea7b189443b.png", + "33783a07c4aa42089404a1bc986ea353.png", + "33a6e05b080f454fb52c652c4f43ac0f.png", + "339f46cc067f48f1961851769d6db7b1.png", + "33a938ce28a744278e7ceb564b3a64b3.png", + "33c04e15ee0d442d8b557110b8bc1627.png", + "33c983245846401cbd2e3f7c5ebc7957.png", + "33e78dfb77a44539a6232f89a77b1091.png", + "343e4550f4794afc95bbe0b11ea69de9.png", + "34478a0e0ce544b193a98df3fa02866a.png", + "3451a31cb49c46e3829687a79b275317.png", + "3455b2b0b1864d12b57ef962dae3ffe0.png", + "34646133659946ce8514a68eff683b84.png", + "34c482db19a24d2680e244f6dd7b2b7e.png", + "34ce1f463de24cdfa62688b7132bba85.png", + "34e871fc35194b9eb4e8dd74a92b3265.png", + "34e980ccef53420a929e955de0efb9dc.png", + "34e9cccddf3944ccb083ddaf3d560331.png", + "34fd3310a47547e3b785e5fdea6f891f.png", + "3522bbe310764f2a8c81a17319994fcc.png", + "350ab6e046634af4b37cad74de522da8.png", + "3547dd5c7299417cb0e5aa060788cd9b.png", + "3554efe04566405dbd27ff27e8a5ba61.png", + "3566b6a3e8334f22bda00b68b6f879c8.png", + "3569394affc24869aec4b38390898f12.png", + "358e70909bfd42de9f8353606c954561.png", + "35d90ee5322149d59b1bad05ae62cb1b.png", + "35e1a21f34a0447ea5ae4ec29cfb5355.png", + "3601d70020b1421694c10a846b484242.png", + "3610aa70453b4c8389d36c0182596cd6.png", + "362bb460197947b59f66e221fd62420a.png", + "366afac270d74039899245d030fd3a28.png", + "365ede162df7432db4e896ba451dc829.png", + "368719c471bd473cb7daa4fe84b624f2.png", + "36c79f143dfe41f59bc47af03fa580f1.png", + "36af20199705410cbfa05d544a6d904e.png", + "36e56bfe06dd409a9e6c8dbc2a77c6ce.png", + "375399e26bc9448a85aa98f461d5c70f.png", + "3775c2fc652745d588275cc5696c4305.png", + "3777f96269ee42f688bd3962347cbab6.png", + "374af5ac93c44f568fe40b49784b23e5.png", + "37b8393ed4024ec4b6786af967467244.png", + "37c3cfcab6da4c099b81b864fa06df37.png", + "37bc3ef8bdd444b693dde771f21c98fd.png", + "37eb906806064030b45b32c44500d061.png", + "3800c21b596b45bd9014b45fc6425e53.png", + "3801b900a82b4fb4869ec251d4fa6737.png", + "38173a1d46ae4a49a43c734bd7b65796.png", + "384d8e5c66b646e99c1c181d4404c522.png", + "37f3a1d85d3e4e4ba0c7d68fd99c6721.png", + "3854743281ed459f9646d2d954d76889.png", + "3854a93b302945278810d6c125f0a463.png", + "38741f32b27c4a469f36a03c2aad84b9.png", + "38ae1bb91ffb48e2b2f64e55c810efe8.png", + "38b9539e29a34af088a78598c403296d.png", + "38e16a94a48d45ebb70d19637cec9351.png", + "38e80752153746168b107fb867a20f34.png", + "38f1dd8c79a24cfbb45bd50cadeed16e.png", + "38ff2cc2a48b475e8641b3b977fd3ff4.png", + "391f0bc70fb241fbaf0bf601f332ad35.png", + "39308da4ec0640fdab06b1d00b6b9724.png", + "395f393ac50a4f588aabad5d48395549.png", + "398f94ce135c4d42999df2e3e05f8804.png", + "39db3977ae1443d8b0bbc312ad92dbd8.png", + "3a581b917d73494e9f821f3426a33c79.png", + "3a3af94731124a4e8ae9d82e09f3caba.png", + "3a6b8bbd0a1145e29e313e4727da0dd5.png", + "3a78b5549ecd4f65b55aadd79d7c0428.png", + "3a93395f7fb344eca40597645fce2bed.png", + "3a9aad770e5e4e0b8a30938ea4e0ee86.png", + "3acafa25019f4069b6c4facbe7cb0e92.png", + "3abe0ed0864d450baf798ffdd4316a6a.png", + "3acfacf31d344d169e09709da2415304.png", + "3afeb192b09248c5b8c3c99356ffe5d1.png", + "3af25a94397c4ecebea713a0448d81ca.png", + "306c4d12a3f7451c85df6969cdc9e889.png", + "3b03c5b3ab5d42e7a968e27fae4e486b.png", + "3ae9826c224247c186bfb35ef0b5809d.png", + "3b347bd503c0466c908d993b12e1c08c.png", + "3b46cfb94b5249a2b915aa7e49686657.png", + "3b4edb88e5644748a6e27e0dd2f38db5.png", + "3b6003ccf7af485a806e14df13a0944f.png", + "3b606c22fd3c47f7b83bd4fe5be5064e.png", + "3b993188f7374622ad3ded31ad5cd75a.png", + "3b6912dd4ffa4326aa0707376aa57ab1.png", + "3ba130b210e248e1974940ebb60506d5.png", + "3bb70623cd7640f8bf4d7a3d5f9482a3.png", + "3bca978b5af4483ab64a709692e856cb.png", + "3be21e8e7a994be5a420bae4c9ea1901.png", + "3c2decfa102447d398a48245709ae4ea.png", + "3c096d531c2346d4a59f5c7385399f9a.png", + "3c11b35f936d4c1ba9f5f64a5afa62a6.png", + "3c64f3d7efeb41209cc9a36e427df69b.png", + "3c7f6327cea54aa3adfb40c0912563c8.png", + "3c892404a97b4a61b68e5f2bdf63ca22.png", + "3c90dea312694f488efd4f1f6fe342c3.png", + "3ca1b3d088cf449abc763de2f96ec44a.png", + "3cd1fdfb0fb14b4081c3a6f5111c0d39.png", + "3d24a54934fa40619401b70bea19b86b.png", + "3d3f8f909cc4450a9ec54ae20ba99e59.png", + "3d3ec94f897641bd9601fb222261c471.png", + "3b4a914f394d4b86be7adca5750b36b7.png", + "3d4b7710123e4efa858726ec6a88d759.png", + "3d5e0fb1d9fe46e7b415f3018e2398f5.png", + "3d7352bde88c40babe62629025376119.png", + "3d79cd9855b44811977893ea80207f40.png", + "3da2f2a58f2242939e4ac01cdd0a08bc.png", + "3dc6b4888dea4b078e782790b66b02ae.png", + "3dc7c29c14c741cc9fe6d27339d0d477.png", + "3df3f6c368ae4e5f9b3180a3b316d976.png", + "3df484a477734ec39bc5a2c05fca78b9.png", + "3e01687ebc5c45bc8ff250a7757e65e4.png", + "3e17bffa80d84bf985c8644be996b1d7.png", + "3e1d01bcb868412089168b4259b41f32.png", + "3e5a43924f5c45629343c6f8d2df1a4e.png", + "3e65be5d9b434518b8c456489101d8c4.png", + "3e6b6aaabf6b4d60bac87ef6d4763019.png", + "3e9516ebcf094aacb09bce1fb4bc80c3.png", + "3ebf99fad99c4f4ca1a08ec21469e7e8.png", + "3f29c1a87f1842faa3d295e56de6e7e8.png", + "3f3a0a1842e0433c8df84c7f0392a54d.png", + "3f647c19437f4edfbfcd7b90b30f50f6.png", + "3fb31643f61b4685b46f1e6e71f2d6d0.png", + "4002e1b6f37145dca141c9eff6706abc.png", + "401f5bb1c7d14bbe85a672d7f3797b53.png", + "402fca8a9e6442428a3779b6d2de3e85.png", + "4036e74ce7da4368b1dd37b9bbee68f8.png", + "404da4d6a184495db2bc5ddde1965f25.png", + "4084a3bdfa2140bab87af70810903491.png", + "40884bf327674e0d8e80af7f77aa4584.png", + "409bde4165fc4fcc833fc45f0b7d1e71.png", + "40bffc99956642c39c733f428555f639.png", + "40e4a862d6294420a03bab1978bf43b0.png", + "40ea6629f76a451bb60e8cee89ed68fa.png", + "41070e2dae98482985074e3b1b130a23.png", + "4110119f79554009bbdb32d60e869ec3.png", + "4115003aaf864687bc996f5290d7cc56.png", + "413c74989dc545c0823ca6ac8fb52024.png", + "413f9a7dad4d403fafac04e7702a57f5.png", + "41a9974ad47040e0b245b322adbc5f4d.png", + "41ba3df197c94b359439cd2bcdc5504c.png", + "41e23f96778b47188681de92d3e06711.png", + "41e650f65f12435285e792231cfe09f3.png", + "4230727e6830402597fcdf27c69d32f7.png", + "4256d7ca1c514475a0d2096abaef1d3f.png", + "425b8f2f79044bdc90d0a7e8adf7a436.png", + "42988b57ed1a4a519fe5c763256e2562.png", + "42adf201a4024496bd898681bd3bd15b.png", + "42dc0906215f428c8ba8e17ca6831148.png", + "4313ba7879b44280b9a6f039c1263b1a.png", + "432470b2a4874a6294d558bcb1e5adc7.png", + "4352b9b0a9d44e7f954446d69c5454c5.png", + "43684b317b0e46b1862f96c6ddcc09b0.png", + "4379af89f7554b4c9f54e4abdd37f42e.png", + "43bff3786eb949c8806aa2705a4530ca.png", + "43da44ce7202432dbe94e2280269ba79.png", + "43efe0ea2c9040ffa3e2b1d487edf89e.png", + "440bc1167ac448d199daeacf443e866d.png", + "441a25a07005479da52ea639f593875e.png", + "4424966903a04e47be8646193d4d20a9.png", + "444193510669452e8bea2943bc7c45f5.png", + "445f29ace6fa4da5af67411ccc822501.png", + "446c8435af92459887b9a93ace1f5790.png", + "44722c23f20043d291bbc9954b7697a4.png", + "44789327ff864b9596c9721a36e45985.png", + "447ce6bfaf4e43059066529a50663340.png", + "44cc8d75b6e24886af32f59b04c6eaa3.png", + "4519504fa9234924af15707fbc33c1a7.png", + "454f60dfe4db4bfa9a59e85b696097e9.png", + "4551f698bca243e38c68f8a1298c7274.png", + "455a9f4cc966459fa901f26cd7ba41cf.png", + "45aabab374b74d05919f2c27d2bc28a8.png", + "45fc404f2ad14b7087ae6693ce04c6c0.png", + "4605bd3256b14db1a978ecf2916d392a.png", + "46080372d46a4597a22e211fa24c4eca.png", + "460d17ca831b4a3abe8fe566708ea059.png", + "461c526c324b43a0b1f732fe2fea3350.png", + "4657e413f653421eb1d8428b692d146e.png", + "4668147008a34ae180c189db136d1c7f.png", + "4684df6d27254a54a2d51858e880b131.png", + "4696f562b7354b85aa450d81004d7e59.png", + "46975a5959364936b3b3ccb15fac1dc0.png", + "469bba1a11a142019133c504ca894db0.png", + "46c31230bae441688b724a82c3d97617.png", + "46c3b8a3ba7a4e98a4ea1d47322dc628.png", + "46c49edb56204b7dba90b683a6a2c2a8.png", + "46d9d339a7b745598364fb47284291ee.png", + "46db2973f83443439eaadd8fd30aeadc.png", + "46e2c172d1af46b8bd0619d93d84e3a6.png", + "46e64ee5d33b4eb695431d987a918da6.png", + "46e6532d64644fd3bda23d5dc62fcd2b.png", + "46eb956a5d9240018bd6ff103a5fb7c2.png", + "47010dd6794647d4a345918077b2edd2.png", + "4710dc5986e542f99d3f8eb9e30dc77a.png", + "4711d0e286bd492b85d440ced757d8ab.png", + "473e654f622548e98b9dd8b1903f3b20.png", + "47b17757224643c4a4e43f1abe728051.png", + "47c9fccb40034abba7d96db45f42e508.png", + "47e9cc421622443c9ef2178eea04eea9.png", + "48289d6793e641f39ef7ad3dfb2ec9b7.png", + "483b3340f3054355ae76b96107749aab.png", + "484610b4be4f4f54ac932cf19539821e.png", + "484c4c12673942a0b492c69916570eaa.png", + "485384be04914a71911e396b673989f1.png", + "485cb5852461445c89fb162c3c612278.png", + "4889b00ec8a44992b615cac9460f087b.png", + "48a107e8db28436cbcce4765f49ac649.png", + "48abeadab58d4cda8f740e43ae1bd0ab.png", + "48afda34487b4333b54db356e6162f06.png", + "48b10d2d18824a97b894143f9e56e232.png", + "48bb52214c0742d18518f5e0ff51ab56.png", + "48c5c219d79c46b18ebdb2df9e618043.png", + "48ec84368bda4feab290b0394a4b6bcd.png", + "49100d2cd92e4dbba9f107c07dca3775.png", + "492c480870094ba78fca0c1127da3532.png", + "493af7af98a4456498c61ab817ae96ff.png", + "49663aa9bb904ffeb09d1ce546823f55.png", + "4966a9ae98fd45d9aa2e30be3d49cc2e.png", + "496b86f4831a447bac5c14ea0caa9238.png", + "4973c7bb8f1441b4ba6fbf0de04511b4.png", + "4983ce70630d4ae3a6d1327af9ac0c9c.png", + "498d18f58dbe46fd98bd149d1464be3a.png", + "499c9f4d366942ffaef561ce91b1d07e.png", + "49a1fdd3445b461d837906cc2a443544.png", + "49d061184e224efb9da6b51479cca2f0.png", + "49d4e98ae947416195a8e721620f2543.png", + "49e67fd57a664c87a176aa06b834e628.png", + "49eb0004085e4254ac3621806dbc1be2.png", + "49ffbf4cd913402bb2653e118f4b1ff7.png", + "4a2b3b3a35ad4c93a3c1ecd0393f7ae7.png", + "4a42f9ac90174c8fa9e37ebb26cb09fd.png", + "4a9965f6fc4145a291c23d8dc574aa06.png", + "4aa5e6b1af8c43878163f2e414324d15.png", + "4ab500a0b8cb453d99397cbae029228e.png", + "4af6da12e7d540afad80879f9eeb9389.png", + "4af7e337d9874068b377ac33758e263c.png", + "4b275bc65eb449f7ab183e8c359dbefa.png", + "4b56d267458c491a8c501d23496a15c8.png", + "4b61fc4005ec45f59a49d4fa1741318d.png", + "4b68cb7eca4d410aadbda068f3dcadd3.png", + "4bce998f40fd45bd8eca2780cc9ca1a5.png", + "4bfed8e96f3849d5bb6a1de4eae6efc6.png", + "4c0ec25646f444d084f56ccdcfb76cc0.png", + "4c338ec422624d25a1ce6f7f211fe933.png", + "4c3edcacdd1c4be2a55fd477e1ffd66a.png", + "4c54f2715eee4e788bc22e7da09a6dcc.png", + "4c90245edaf9435e91f74e85c2e9fdb7.png", + "4cb439ffe79840b0b6ab1a2cf2485e64.png", + "4ccb1335db5b4c048ede0429fbde6ea9.png", + "4cd316f8ce424266a8ee6e2efef325b4.png", + "4d5d4dc714e1477c9d9c2cebdf3f6f37.png", + "4d6aa4e4ed474d2db0ba05cf13ccadf1.png", + "4d71119b634e45799e257a7ca11db087.png", + "4d7cd698298846b9a5094965bdc366ca.png", + "4d96ade1abca48b89730b035e5463172.png", + "4e0a3415b15d451494db299b645e0852.png", + "4e190a8a8b234dcdbe3522989298a0e8.png", + "4e2c3d959de94e2dac786a3ccbf676c0.png", + "4e2ed25543c64b948eb2c51281356d74.png", + "4e686897b9ab413b9cbc99b0f46220f0.png", + "4e79c7523cba447ba9b0d51d28aefc9b.png", + "4ea10e9613434abfb98ad7477837d883.png", + "4ea7037fd2494c88baae6a89fa1d851f.png", + "4ec7784bc64b439f93a5a678722cf1a4.png", + "4f077bda107a40e99bfa87c18c5fd056.png", + "4f2fed6ce95e42a98fb7ed2079d1abf3.png", + "4f420949d7304ed1a9fd82b2ad2623dc.png", + "4f744f65dfc849f09f1f7d3d248918a4.png", + "4f9641b77897437784c23a626734bcf5.png", + "4f9eea6bbf3b40f09bc3142f50683953.png", + "4fc440ca5aed4123818faf3737d13bc7.png", + "4fe247586ce34caab7d876cab6ac97dd.png", + "4fe7dc295da7449ca1805f1529ada611.png", + "4fe9e2c7cd524f79a976661de869ef04.png", + "4ffa7aa343b94877aba6ff605ff4733c.png", + "5048dc11412b4ae7aa8534d74cb8da3b.png", + "5052f89092a24236a397d7d06761cf08.png", + "506d6cf74a4a4cd9a981f316850a35ef.png", + "509fb66cdf504662bb7d9053fb2d1911.png", + "50d46f0380e44b328e5a3ec558440f5c.png", + "50de0eca8b184f3fb7058f0fa57ca12b.png", + "50de169ca62043ecad092aae6a487bb0.png", + "50e26cf911f34af086e7ebdc72191cab.png", + "50f5673cb13842a7a014b6edc2d617e2.png", + "5106a2cb65b24c0ca99d42780f77b0e1.png", + "513fa84b52564173a897a82343e49692.png", + "5193ce4a01224e418d3287e82702a449.png", + "51bb86b5c3ba477b922d5f1151d9a0c7.png", + "51e3aa24bfae42e38216c61d6d12b275.png", + "520680cd81054c6cb7bf954e9897d633.png", + "520dd413bba94040b696e8803833b088.png", + "522e3f0b602c456db335940224ba379a.png", + "523731b3f7b9492d8e4564bb19ea960b.png", + "525fc4a45e4c44509e0beb9f9095ae77.png", + "528bcf0989a742c3bb3845ada2904a1e.png", + "52adc5701258445aaafc14d62bab6fce.png", + "52b03d89787544199c629f1d83da7450.png", + "52b66a13e03f4e20b0a5c7eda9c31f69.png", + "52f45bb00ea14089b84a22bda3c170a1.png", + "5336d3c96f604f75a05320ce549dc237.png", + "534092b89daf4bf2a4c783726703c2b9.png", + "53423fc7fe3041db85a3dccf2f284a21.png", + "535a9c432344498db170d6d440b1df36.png", + "5382d95daf0c4b93b15980ef050477d8.png", + "53a0cf73047241e2abf86193e3b65fa4.png", + "53c33f06bb7147e4831ea03185fde2bb.png", + "53c61874ef634160a23477f69527ec82.png", + "53edf984f5274e0ba53c471ced29a318.png", + "5418a55e4b904f30820d0cd6cc5dc49a.png", + "5440c5d7b22340f1aecaeaa8f29bcbd6.png", + "544ee4b2fbcb48be9c9c67831f2f6b78.png", + "5450d9a93c5f49d4ad049188b6e1a024.png", + "54705618f00d48bd9bcb69dc14fe059a.png", + "54782b244f9f42e59e7e6babb63fe030.png", + "54cc2a64387f4d9e871b2a351311f9c2.png", + "5533755d6e8c4364ada7eab772b04845.png", + "553a8d9e580e4facb7b602bc0ae893ed.png", + "553af8b54e974d6480179d74d0777ec4.png", + "554137ecaaea4efd9f92de91a0a1e28f.png", + "55433f20a38642ef87f454f6c1a3765c.png", + "557538523d1c4a5fa68e83e8a4fee65d.png", + "558f316f8b204a35b478e0b2ae078fa2.png", + "55a0eb0723854e319e24d8481fd8b872.png", + "55a1fb46d81646bc9bfb3d1f667faefe.png", + "55add47b13084d1f9c93935547c0a792.png", + "55d9c77d5c6d4b4c8a89ccd12b30e37a.png", + "560ff0946a424a01b9de0ff84ea495c5.png", + "56280a321d9a4264ba05c2c7c3b06226.png", + "5661a69dcad5403c86822c32a3943bab.png", + "56696aeb67434b01845b08b1a5a6afb1.png", + "5671ea37f2f54b6f8781cf9323df2b9e.png", + "569519e8aca2427dbadf34303e3e8b53.png", + "569830211fdb4e0f8d65a5e690da5a64.png", + "56a9c6658ecf4e13a22409184d2f9817.png", + "56b5e2c3db8a47e48f4f2e908b930f1d.png", + "56fac67b82994dda96d698167692b40a.png", + "56fe19ccbb7a4cd4b399db76d350449e.png", + "572219668e844d5788bf849f325583b1.png", + "573cfc879642484288e81f93dfb39f2f.png", + "57464c4a14f644d589a6f47278fd6210.png", + "57652416c3a7438bb7265d6e3129c455.png", + "577ff4e6f4864345b8ba0f961c527de0.png", + "5797f04b68254fc78b04a1c6e056be71.png", + "57d810bbaaf74f89a98a976ced29f5c7.png", + "57f6b7591e504caba95ddaba2887cfb7.png", + "583030ec71b84d7b8399ab0614373141.png", + "584d14f7a4ed4dd38bd60758cd7fab49.png", + "5850aae7cadb4de5ba0674d47642d07a.png", + "586a5f94e87949b69e53fc381e0eaf18.png", + "587082440471434088bca825d12bbc3a.png", + "587738276169474b98a0079069aa0174.png", + "58affd8a51584604bdc69134a574271e.png", + "58b5de6b212d45cda579069f65230c62.png", + "58e1b27bbdfa4acf9d098f1e93f1b953.png", + "58e4bd7642634dbfa59f3be04b496e6c.png", + "58eff89d722a4c97807bd59b7974afad.png", + "58f48b8ec25849c1afad322a00112c1f.png", + "590cf0d7469a4288b5813213c318b741.png", + "5910c32b5c92435e964e398bdc1ccc31.png", + "59110604183a4e409da3f04acb56d736.png", + "591495e712f347fa92d6633028efcb1c.png", + "591756b4b85c445ba84e8959edfa6d73.png", + "59253681f81c43e99fe77f884ce039f1.png", + "592e63bcffa44e3386994f8147239dee.png", + "594bb27cee0b4dfa83a90fbcb9f306bf.png", + "598062b2db594c6b9ad796516476aa61.png", + "5986af3ba79f4b138535764a822cd0f9.png", + "59a0a7a3512f478c841d5538091c35eb.png", + "59a31d087ec14c4ab1fb9c14fef8d340.png", + "59aa96e3b8af42649d3bb693cce7b2c6.png", + "59c6a0f01f78479abf7e45e0cdeaf460.png", + "59e795bdcb2949eea3e95c9154649318.png", + "59f5c64fc3264fa38cbbbba1795ec441.png", + "59fb5d2da344460a9ad36498afce4c9c.png", + "5a1b532ebe0044339083b5c2f217713c.png", + "5a203dc10e924b05b74ff58e0a8e2310.png", + "5a20b654d8c847009db387672444daf0.png", + "5a214740fc4345e593833de90a9f7cd8.png", + "5a8c43d892f649388651012af40d2e80.png", + "5a8fcc13d503443296ca718a2b3b092c.png", + "5ab9a8b266db4f9a9ad3f670590cca72.png", + "5ad0ef7b0bed4a7c8184426898a1e813.png", + "5ad5c7debae24e07b13333d35d475432.png", + "5ae46c14c11b4ee3830a4971447fc35f.png", + "5aeb416565b243b09916897b06c32313.png", + "5b13bed8204246749f001ef01fd19675.png", + "5b1a2afbe78f4b4882c82868834efb8c.png", + "5b2627ab22804230b4631be052df9780.png", + "5b2b7e98e4e34593992340fb7cba1694.png", + "5b367117ad184e9e88618fa0e704d6ca.png", + "5b5e18dbf3084c4c9530d95e4834f517.png", + "5b8102c666fb4b36ac4719f7ba322924.png", + "5b8939ea37534b0bb8ebb7b00b351861.png", + "5b8ad352f5e549dbae49cc3119b2dab3.png", + "5b986df14ffd4a0482a54dca582bc19e.png", + "5ba1107ebcf14a4993690b4953c5a013.png", + "5ba3634a27ff44049f30fd5113fed1c6.png", + "5bb33fe96a7d400ca1b97ed9e574ea7a.png", + "5befff9fd3964209b1823e15cb6ebbe1.png", + "5bfbc77e127246db86f2fb0d98e76e9b.png", + "5bfef2836ae64b76a68f2cb981055451.png", + "5c05cea8306f4f8e9ee6cc0ad36ba272.png", + "5c1bcfa76d824860b8d08de659deb112.png", + "5c25f34560c4412f96726dc08b5216b5.png", + "5c64a82467894c2a911551a192176a1b.png", + "5c726bb8ea11484db272039f0b0aa3d2.png", + "5ca14589a9474f468a1ad8b0c5bddb98.png", + "5c7c7446e93142b2b2e0e3a0b7412936.png", + "5cbae25ca95c4fbebbc798918a8ec0d9.png", + "5cf3afe9c34b42268cc3feb591771e6a.png", + "5d416975b4924d97bf3ef4a3bfcfd3f9.png", + "5d53e1c2250a4e9f83e1c160a99600e2.png", + "5d5fe52cd6e642e5a0b0285fa1408675.png", + "5d6be0e78a4141ec93679972615268ec.png", + "5d76abda7c124fb1860bb8af4f74b997.png", + "5d93bdd4427849e9b5e38a8f73792072.png", + "5d9fbef803434d42b3ce20aa06481084.png", + "5db094752edd4aedb04f622f95b00544.png", + "5e1586b571294526848e6f0d8b9abe2a.png", + "5e3b3619f6e44cd9859a64cdf9fb25b5.png", + "5e40b98541494c40a839c90293ba8fb9.png", + "5e5553105e41465e9b127bcc48b091d3.png", + "5e7170b94768452babe294ea314da92f.png", + "5e969be19f8d4be6be5891eeeac0a47c.png", + "5edf6bfcc8394c78a1b0d83ccf39d6bf.png", + "5ef69d99c7924540aacfdd2d7c64e7e2.png", + "5ef772c427d34f829d8bc79d05b33772.png", + "5f439e15652d41df8fffdc1bc9baeee5.png", + "5f54d05b19634efd88db84c1a7cc15c7.png", + "5f5ebb4e26874a678288c2f6ab05f409.png", + "5f874626cc0f479b8a7a8a3732ef6a83.png", + "5f9baa10115e455fb4a1defb030d84b0.png", + "5fa78f302c03436d8a3d5986dc0348ef.png", + "5fb1afedf493495ead1da210adbc3383.png", + "5fb97cabae624a38b2a5c65977bbc73d.png", + "5fc2c7098da9444ba055038f78fadfe9.png", + "5fc52652d2404eb0b79fb86b37b95ce6.png", + "5fd90e586ef84dc3b9524fbd5ebcd967.png", + "5fea962e42de4940b0f255d94ecc8750.png", + "5fec55f3790b480d8d28538d73b0baba.png", + "602e58bca31141b4bb99a61a60a8b091.png", + "6034eb2335ec491fa937ddf632cdfeac.png", + "6039d7488cfe452f80b63b50a3d986d1.png", + "603d163b2b524de6bb753485f11b36c2.png", + "60513da8fbb44686a6e63fb948750c17.png", + "608b49df9aaa42ed922c1b50f7f01f5c.png", + "6090d3f0166b4ac5a102c18258617520.png", + "60a04419e3bf4a209766a7bc6dad0b24.png", + "610a561c72d64db89ca7dbb16b260bda.png", + "611534aed0a1472fa524e822af2b5d1f.png", + "612283a53a894547a8b1ae94c4950357.png", + "613f2b0d17d647feb446f29a7f9dc98b.png", + "61520285e1404764a376e125408df490.png", + "6174a5cc0cef4ba8be4c98ce37280b4b.png", + "6187441ed6a143db8034b09c8ebe7aa7.png", + "619f6acb1041401fa781d1ce4566ad64.png", + "61bae9feb6214255a095d90e5837ca90.png", + "61c798f9b60b4b1aa547ba8dd46d95f9.png", + "61cd9399ed1b4ca8b3d8c13a77746e60.png", + "621647909f3a4e66bd0e4f2b28bf07e2.png", + "6245d6d389414251aa687ff8855f32d2.png", + "626884b1563244f4ae38ac1358e5785f.png", + "62816264ee644230908e2b5e48481740.png", + "62827cf0c8e04dadae071f217f16979c.png", + "62a1e2f1109648f0b24053c7d8ae4d07.png", + "62a3cba178594b9b8c437673bab39cfc.png", + "62e84e99ae7d40bda127174ff9f5b430.png", + "6301dcc8feb545abb3ae6af2e993ded1.png", + "630ae842898b46adbbf2a0cf7b4edd35.png", + "630e4b7748854e409c2d161a2ab3af17.png", + "633a549c96e040be98b1771f9e0f643c.png", + "6357723433a84e62b0700995e47d7ad8.png", + "635f0bd7f0f249c3b5c9672b2536e905.png", + "63640312aaa549dfb1fd36bbfcce3de3.png", + "63b6229def8643eb81c629e2507b1360.png", + "63bedca2533e46a4882da0a42b6431ab.png", + "63c0da64f83945c39fc0af96afced163.png", + "63d002952e1b42188133909ad1d88541.png", + "63d2c7e751f54baf9fdbee8a365cfc9c.png", + "63e524a4fb4148ada6e3e63fc2fc61b2.png", + "63ee8ac94c154d9985060e646950e9f6.png", + "641601c11b2d421fb432b9bf36ae31c4.png", + "642137fb81ab468ab6489d85cf2550b7.png", + "644ba85eac1c48c2a16ae5738ea49625.png", + "64a0345a39384a02b37112ba95457391.png", + "64a91cbc790143e1bbea64dbc9978b58.png", + "64b8e50d76c142d0996e50a34492a6dc.png", + "64d6efc4af8549dc9a6e4c61137e1cca.png", + "64fbff49ae034f9eaf3d245f97686b16.png", + "65068f4228604cef94c7390dec52f312.png", + "650add51487d4cfb8f3353b7aec65ee2.png", + "650ee65fdc49473bbe73e809a7aa5fbc.png", + "653c3f2fb98941aa9757574ea9930b78.png", + "654af140e3a9405bac52f5026e157ecb.png", + "656345a8e30f46aea4fe28acc675a203.png", + "65668b9cfe4443848f4043fcd5a09527.png", + "656e9b4e162b4f6f9f50401a7d11fddb.png", + "65837a6e08264ec1bfb43717e1901bba.png", + "65a584f2eaf64fbab8ad156afc420f5b.png", + "65b14cba1b2849ef9a1e5ebe3c724ae9.png", + "65b2218f1cc04a6aad3f22bd4616dd88.png", + "65ca762abc5b4d7da975ed0d04cba624.png", + "65cfb3c8af2d42e6b62682b47159e401.png", + "65d3b8aae10f4fd8a87e76849bc8aa77.png", + "6633d18cbb294047ae44203d8bad8186.png", + "6645cac7db4b46fc89b81fb285f31876.png", + "664bf7965630465daece7f11352ced02.png", + "664d7662bf18418a96c0b212fcdee2a4.png", + "66596ceceeef40aa85b749580452af01.png", + "666be8d28eb64570ba356c342c97a89a.png", + "66844c3fdafb46c8abaefbb3b462dcd7.png", + "66b81301c3e3433280bdee0897f380a5.png", + "66fd4a78a6f7451e9350b5d5a01dbae3.png", + "672587184ab2425a9153702d6038bf55.png", + "677d2a967b4444bb971a3202b0c31c38.png", + "67a13ae3735e43959df4ccb4d05577f0.png", + "67a2b8bbac2549bdab43e8f6ad4db06e.png", + "67bb6cb1627c47058c07af7e4e8139e0.png", + "67d3508ad1be468c9e650c9ae666722b.png", + "67f2a40fcb7a4031a2e584eb4279de96.png", + "681eb1a8c2a0472691eb233dc8f46afc.png", + "6836fed554134f41a6c7b669595f867f.png", + "683a65060ef446ebbdb5dd25e4db14c2.png", + "68827c6a4eb744ef92f9b19c02e9165a.png", + "68c353e6989047e4a5071290ac77b411.png", + "68ddbb8067bd4f20be2758052c14ae04.png", + "68e4e1b340ce49f68489c9efefbddd3a.png", + "68ee22509c234cd9aaf036e3054587e0.png", + "690dbdac1c6149849e176a411117ed68.png", + "690e7b88793a49f29be220de2d896a86.png", + "69721c26e3e442948e60499478f51925.png", + "697dcac8aaf24b2c8cee5473207ca363.png", + "69847a3fadfd4fb0ac173fce518cf70a.png", + "699b91db63264c2d911639e0c9188de5.png", + "699f3072ddfd482fa969fb5f1aea7938.png", + "69a3c619985b43bc8c0959e226a5ca0c.png", + "69c82d87d9d44b9baed17d992f8135a2.png", + "69e00f2341c14c62bef114dca110aff7.png", + "69e35db151db4263986ed61bdd191227.png", + "69e35f6824e5402091ce16ff0f7e55b5.png", + "69f296a366d54b028a586bea90c8c386.png", + "69fc58f088f44d25b6381fa007a00ab1.png", + "69ff7294daec45dba6c39e555ade3a9b.png", + "6a735a8033c54e319055960a5eff5abc.png", + "6acdd747843643b5b95d23dd157e7bb7.png", + "6af157e5bd1a4fbda5480c6b6f0c64cb.png", + "6af2bff4a95f42df895aecbed2b49784.png", + "6afd431e3d5146e8aa66055755746625.png", + "6b19c22e177b434ab80bd87bb854e6f1.png", + "6b328a9247fc4ad9a376c7a12d256d01.png", + "6b3a739c20554ae49562b4bea7b26361.png", + "6b8fca837d7f43b19603635a318350a0.png", + "6b90137961c34c98813f72295feb5728.png", + "6bca4094c5094d66ae8395225c0ad053.png", + "6beb60c27d6e4669b5d1b6f7f9590745.png", + "6bf69594046343268437e76296670825.png", + "6c0a8a340aa443b6ac5fa510af19a403.png", + "6c136b196285449cb8ce3a47e9784483.png", + "6c282450684c4e818162e7abe0a87e36.png", + "6c5af5b1eb9d499ca70381e8fb2c47d8.png", + "6c769091f20c429192a41e6adf96d9be.png", + "6c921d6442cb4b12bd2261f09231d0c8.png", + "6cbedc8a575c466fb85fcc0d672e12c2.png", + "6d0abd2c416a49b88fa5dc0fed2996fa.png", + "6d1764965d734a6f81c9d57bf7f4d783.png", + "6d2ddbf78ec1471283e6a6a1c5a5af7f.png", + "6d2e7c5a2a574bfba43cb921d5b2eac0.png", + "6d496ca200b845119a54bff699b9f7c3.png", + "6d718457a7544f6eaf55c6f1a6dee41c.png", + "6da5c1e30d484b7184834a048a7fc44b.png", + "6db4674cde244c208496690da7694cc6.png", + "6db827ea2e4345ea95bc31cc2605494f.png", + "6dd6f0f63b464a0ba1c3433b4b773387.png", + "6ddaf71cb790449abbbd0589aaacc094.png", + "6e1774ecddf543dc847c584b9bc76a92.png", + "6e25ac8f48bf4d19b80d708603d7988e.png", + "6e4135b7a0eb4d489ff65f2e9a3d5acb.png", + "6e550c4dc51f45d3b4a591d95375bda8.png", + "6e8d538ba8014c66997bf6d289b6da99.png", + "6eb79de8c2f648f8942e459cca519021.png", + "6f01f6210d3c48408962bf3ed5e1bf17.png", + "6f2c1edcc44240e9a305d4f387ea039f.png", + "6f59ec916c184256868e8a5442bebcc4.png", + "6f5a2ee2e1354321b0636ed810d34349.png", + "6f655c4ae95d44eaad6e2149421032b3.png", + "6f6df7217fe64557a2833efb0f7dd564.png", + "6f71e41bb2914676bf5484ab559b9dc2.png", + "6fa9d0228a0e4b31b0100bd179cb74aa.png", + "6fcfa8396b4c4feea59fcaa13f7a13ec.png", + "6fe401b4b3b748c4ba751c8e184f0d39.png", + "6fee6b6b332948978fd3ecd65a0b1298.png", + "6ff77aea600840c99e8e1a15d8ee3f84.png", + "706de3eb8e5742e08db0cdf4f4b894ce.png", + "70bd87855a62498ab5fe483aec93dfc9.png", + "70eec73b6ce746eba69ce29e439c0a19.png", + "70fe576b38ef4227bc97e336eaa43b22.png", + "71202b42800b4b53ae1cc572ad5fd0a7.png", + "71588d86cca547ef93b81ac5443839c2.png", + "7170a4f018c941d2ae02e4368d31a81e.png", + "71daf0b27dc043919070b64d4c129962.png", + "71dd8d3975a74b06b8b196b02bc35410.png", + "71f6917a4cf4452aa29082ca48627ca6.png", + "722e25cddc3643e592590ca531ca66b1.png", + "722e8ab4f56747ae8ede9cc112960ef3.png", + "72c0010ddb7241878d732de2ba743710.png", + "72cd28fa2ff54e6e9ff3207cf08364ca.png", + "72f741bc43964fd89fa0ef35cba2fcf2.png", + "731a057c409a48fdb12d6d622fc08757.png", + "73495be35fbf4efe83706455718b3282.png", + "734fee4532294e5f9fcc8e8b01e17cf2.png", + "736039a577694b1e8e4afc96f1ee5e1f.png", + "7379acdfc88b429fb383c5b11b7387b6.png", + "738ee1832c3d46fb94d69c7b099594b0.png", + "739bc90cae55480399cc04f448556eba.png", + "73b0b492559e4e9685e2d8f12ca56d9b.png", + "73ba74b8796744a09dd4a3e568891832.png", + "73c51756bf134b41837ae64727b87ca5.png", + "73d210b01cce4ebba0df4e94f85d65d7.png", + "73ddf106d06c4c34b103501e271d7970.png", + "73f35b10334a4545b10d1aecc96709c2.png", + "741a8f8573674322af76e4a58b36f0d4.png", + "741d691eb3824787a3833e439f225119.png", + "742fcf7e416f4191a602130accf683d2.png", + "743e87cbb45d4ad7a6b85086c86c7ef3.png", + "745e7fe1441d423e8ca8a1327e131bd9.png", + "749224c6c6fd491997b0945f497f934a.png", + "749ea372aa014475a5d6d9f0a0c8146f.png", + "74d520feaba841fb8d9a1b612f4b27dc.png", + "74da4d38fa20420bb20bce1cad219d26.png", + "750899d0153c4601965398afa7f30e3e.png", + "751dc8dcdc1748408e5838f8d93b12d1.png", + "752c7d55cfdf4778961c9677b19180b2.png", + "7531f9b4be9241cdb198ed2f82e1a47e.png", + "7541a240e8224e7db4dd66262ab9dba7.png", + "75ea29bc46ba44ceb674c69f42fe7fdf.png", + "75f1066ff8d749e1b22c6886028d4137.png", + "75ffcdb26d7d46a88e3a9a8d4c6f20bc.png", + "760d129964a74c18b99b38da9c40ac5a.png", + "76127badcbe5499bb110c13ec8bc1c49.png", + "765fb8b1c3c94b8c94430f3943c60773.png", + "7678a9b4885b48068215cf775ad68ae2.png", + "770b050c43314f14ba14bc7bce82ca10.png", + "770c3c308ece491389fdaa48e06f0e64.png", + "773c95de7d7c498594dd6bc45ef83c57.png", + "774e5841e2964ea3a6a365e9fd6cc0a4.png", + "777132bb7d6044f7ba8f51611284b7ec.png", + "777eb0b4d9c54664885c306e165a3069.png", + "7784f6b8f0f143358bc8b0a44da71a2b.png", + "77af88ead08c4d90b82107a0dacb9281.png", + "77c08186c2c84232b9007f7f88a53a38.png", + "77c1a8ab747d4b8ba8627faefc4e6f61.png", + "77c566abf556431eba9b57308c11cfd5.png", + "77df9e03833d47e5ba9d4f17a46ccd25.png", + "77ef9103bc8b462aaf8c522035ed15c6.png", + "77f65fad120644a99b965371708d9bfd.png", + "77fa5cbf2464462fb24e8d731da8a302.png", + "781f94b7e931427c856cc36547018c8a.png", + "78536baeb8bf401db286ca77dd89b3c2.png", + "789a8a99741249b38b2c6ee601f0f1b0.png", + "78b721a0d0ea477e9e0a1c4c374808c3.png", + "78c3b4e49220463883e7c1855df04670.png", + "78e50fe00b7348bfba208deaecfb9dc9.png", + "78ec4b29e727475983622e8e3b4b637a.png", + "790a1a028e4e4eaab5f67cc983057bc4.png", + "7910bf1541384dd9990dafcab7723741.png", + "79396ab62f7245fd956f2946258de5aa.png", + "793f4e2ebb0c4598b8b2d0b465b29133.png", + "7946259dca0b49eca22b510bea3d22bb.png", + "79a7d97644754fc58606b8b70e385f11.png", + "79ada2c46a344a73873cb99c9d1ca1b4.png", + "79c51b40a5524527a958576d619ea093.png", + "79f5106ab0a64bdf90e868794b304088.png", + "7a09f27b1cb74391b18cd3a9e10ee176.png", + "7a2dcabd14db45d3ab4e4afd030eda1e.png", + "7a4921122d014d52b7621eea1babe7c8.png", + "7a4a7a645594495f8599dc330c81446d.png", + "7a501027ac184a8d973c7f16c29d717a.png", + "7a523673ea194f4e9791a7fcca0ae587.png", + "7a6985e4d2f0462eb6b6dfbf1d38f13d.png", + "7a6cc0026500450abf6687d806763bc9.png", + "7a6f2f31f1a74ffe929f3499f8c04ac0.png", + "7a7bb5c2bc5040368640ce74591f5ccc.png", + "7a7ed1a8784143cfb05de934199b6ced.png", + "7a8fdb18dd2944c88e9c21c8785e42a3.png", + "7a94fdc2616c459584a05f684dff5f5e.png", + "7abb0e3dcc9a4c1ba1a976306d83c1d0.png", + "7ad2c9235bb749de87c6ce4aa1e3a99b.png", + "7adc402e1adb4db4880715559ddcdf3c.png", + "7af430bb7bcf4ec893777f823e6311ff.png", + "7b992f83310a435eb6746ef245b5f2e7.png", + "7bc24e7e6462470b848d60b722e160b3.png", + "7be664a8b5a64b43b454f61c48f08d7a.png", + "7c16c3701b9d4d74aa5ba53a95c336a0.png", + "7c47a8381c744ebbb5c836cd532960e0.png", + "7c54c04c85a9484db58d938e9542cbe8.png", + "7c641b4902ae4a469937e3a5e8f5c16b.png", + "7c890af930d340b6893a7bc12b9c4f93.png", + "7c98a213614c4109a81761ece0f50302.png", + "7c994134d6e24cb8ab939d3cda551fa6.png", + "7c9e4b11edc14c1e859172953accdacb.png", + "7cb704cad87e4a8cb88047999362bd56.png", + "7cbf18d3335645f2a83127962962d0ce.png", + "7ccfe13f42c840fd86c0b80ec5aa3e05.png", + "7d31e7697f8445fa91345d215d768ccb.png", + "7d3a05cb0ac04663b16252b95f71f6c3.png", + "7d48fd4940ad4fbbb89792f8cc62d2a3.png", + "7d4cec210f9247f78d2a977fdbeb69ad.png", + "7d60bb5e0ec140748dd0e903bc281530.png", + "7d7500a57e70420c9a92a1a3a81c70de.png", + "7d77b50d5ef547c8b775c472c9de8ae9.png", + "7d7a60a04e8a46d39e614837717201ba.png", + "7de277b0df3845ae884d935e0f55d48f.png", + "7e02d864a5a945ca9074c7905ace1b6e.png", + "7e2dc1c7cbe644a98da06c3f65d315e5.png", + "7e318cf815cc47a3aba6a31ea2f24f16.png", + "7e4824be71e54c54a3a49f4bab939679.png", + "7e830746a7de4b46bcd2f9ada753a5b0.png", + "7e89c0807ef9456eacbc7b43915083b0.png", + "7ea9ec0c974548edaca84a4416b664e7.png", + "7eb07780a5ed494986ae4433e8aba449.png", + "7eb2ecd3f8c54f86b388f0ba5be67c1c.png", + "7ed700aa44db439fbb2395b59390b383.png", + "7ee096cb12fe41fc966c2e5dfc0ca087.png", + "7ee69ffd3af94d3ebfb27f3932c247f5.png", + "7f4189fb23cb4048a88064b2fc6758c1.png", + "7f648fde8f6c4873865754525be1a01a.png", + "7f6be9fa8ff845caa4e9e9eb6b77be67.png", + "7f793b71db814bfe8c67dcca1c1c572f.png", + "7f82f467433d42c8942e7c4f8c3525bd.png", + "7fa08592f24b442b85cc9ad85df110f1.png", + "7fb5d5bda19c47b38aa203fb9759a4ef.png", + "7fc0de5d390d4d4f9e225becb72f4890.png", + "7fd2fc4876e64348983ab45023091781.png", + "7fd4ce99a0934ea7b9eb70efefb4a29c.png", + "7ff32cd51f664dd3a66fd95c0d530019.png", + "801e816b7b2c45cc80bb254a7c94738f.png", + "80207c5659e74d9dbf472071923bea22.png", + "8025a68b5b604ebead9a94dce68e93df.png", + "8033d4b59ea84b30bd254354636d2a81.png", + "80806c4d188947ffb02271a6e6ddc71b.png", + "80a5517c5bc04c589f02d8b286299be7.png", + "80b8f9c7dbfd4eebac4c2464273f7060.png", + "80b9de1810b34fa59a920850fcb26a66.png", + "80df77b2e4964861be9a6448c99f0639.png", + "80ebfc0c5f03442280e4f32822f44b70.png", + "80f48ee46d9b4532892caa804a35f28f.png", + "8103e8a02c904ac99ad426539153ff85.png", + "813cedb5db53469a980e1dec81daa949.png", + "815b2f88694f44dfa6e0de7994de11d2.png", + "8164c629a3e64862b6d9743f5039de96.png", + "816ad705374f4e939adebfcd6649778a.png", + "81a3f9b1a11e43dab32c0642d08dbacc.png", + "81acf45b8e084972a1c0bfdbd2caaa6b.png", + "81c38de29b984c6ab9b7b58b61288b95.png", + "81f09fc5ca7441bc804ec4a7215f2a86.png", + "81f72833959c4cbba6861284a3d2e4cc.png", + "82064cd38fa7446c970641afd273e1fe.png", + "826a712799a04ab0a3322fb127efbdb6.png", + "827a055ace884dbd8cafd21ee0a65eae.png", + "829229a3fbdf41d3a8cf6b9684e66e17.png", + "82a43656a75f4d9fb4496ceebdfd1b36.png", + "82b8f5266b3c4245bb97ca911787a473.png", + "831d36a2a4de46fea5e6f1e056b842db.png", + "832bb0080b934dc59aeefdca82ad8715.png", + "832bcafd6e4443579cf4c6a38e84ca3e.png", + "832dad67e4f94096bec306fb8f58948d.png", + "8370e36ea52c4eaca500e08fc218a07f.png", + "83ca3644c9b14d549a564b59fc40ee19.png", + "83d4fe1153b7463d98f29082522b388e.png", + "83e59b8365fc4028855c09d5dac810d4.png", + "83fa74d42f544b6fb406228a6fcd4814.png", + "8408c2430fc94f8b85966840d92c26b3.png", + "841126a29f0b4ac3a8af353484ecb922.png", + "8435749c85464bdfbb25cb1b56e29855.png", + "84436d7206a8403cb9d42f0bc7c1c779.png", + "84446dd7bfb648adadff89b677473086.png", + "8452beaa8f334654aa5020fe673effde.png", + "845a6f369c104a3aaea201fb731e3d5d.png", + "84795667aa7c409f9436dc35b648f245.png", + "84860cece1e346e9bb49ad0b209003a5.png", + "84938a30e3df42a79a36ba505dba63ff.png", + "84a4a1d9bfa04dfe932fa6a99746533f.png", + "84a65b64153e4d68976a33d4840420d5.png", + "84acba7f269b42df8da3ae1fc145d607.png", + "84d4803647564c18b70d446b2428b8a5.png", + "84e34c0a4f9e40c988a05daec63b0a22.png", + "8512954c4a9041b0be0ae27125c47193.png", + "851b7c10fc2441fb9a2c3d5cad305c57.png", + "8530ca9d76bd488799abe573b4168ab2.png", + "858740aef4744cc2b54b6754655b138f.png", + "859a09b800384a1b93cfd4b9c1ad47b9.png", + "859f096bda454eee8e7e90e37e51030c.png", + "85c029433ba1403ba8de8be6acc05068.png", + "85ca7facd5cb4f9da25013e6245996da.png", + "8608cfcb4b804ad488cd1760072fa1d1.png", + "8628c5f1cf2d484da291a99017b21a02.png", + "86366e1ba4a94b289576192bb3234783.png", + "8651f7b463694fcaa238cc57c0716744.png", + "8666b6e51b2142ec8f4c6f5a82c625c7.png", + "86aba81d657f4b068578e47323f65573.png", + "86b5299327b54cb0b0cfa54cff96da37.png", + "86b72c4e281c44529475d749e1a06c42.png", + "86d63e15e03641ecbb30c9ead640d23b.png", + "87143ea801dc41b8b079d2713fd69171.png", + "871d3c710a914532b69b45ba99bb8c70.png", + "873c85539f6c4d9d87ece15e6f11ca32.png", + "876d884f413441c99354f73fd210400e.png", + "876e406d9fa44157a5f0101ba58a1258.png", + "879e65c28db14facae938be8f4a09a1d.png", + "879ed9efb7c64dfbb35efe1478aaf9dc.png", + "87a1a9c1bd5c40079cc978dd2de48ea4.png", + "87c8b8198b8f4961b9e2c64731effb35.png", + "87f18231fed94fcab365e35f87845654.png", + "880b7ed63cb14706bfefd2b73de461ad.png", + "8818f94fe0ac4838b62c4bd90d5a6d53.png", + "881ae3d555844479845cbd5a55dc097b.png", + "883347f4df1749bf99711a9fe0d3077a.png", + "883d695f68104f8297c0fe42b64611a6.png", + "88e0b58d4f0d42ea9ac4018ced6cb131.png", + "88e0f0ac2d7f43bc826704bc9c641071.png", + "88e2abe945e84255811bf4bde1f3ef6c.png", + "88e440f750e8450bbe1d719bb58b9b1a.png", + "88e70d05e4004e7abdaa8c844b0ac803.png", + "88f264606b6a46568545ca27ab99e99d.png", + "88f4eadf35724b7a9b0aee4a00c3eaca.png", + "88ffa76e71b14c48a674fe544115757e.png", + "89224b54539449859e1e7da91f3bced6.png", + "895151e0bf3849399f45bbf2f225b4be.png", + "89787823f726434ca02aa5b96194975c.png", + "897fce744d0a4d4abc2498776e176c41.png", + "89938f5b78414236acee534079ab11cd.png", + "8993a589de694fe2b1a6593920fca76b.png", + "89a4d76ffd624f4b8d1abbd755280f32.png", + "89c3a9a9b31e484e813e65d95661f65d.png", + "89f54f4a33b345c5ad85c80c06b6f682.png", + "89fa4b7af3104043a22d8512d08d9e18.png", + "8a0c6ade9a5541b3ad99b9a435e34310.png", + "8a2541b7c3914fa48c9704d1c9f78501.png", + "8a3e058f7a2c4f289df97616136dd529.png", + "8a480232d91047a093793ea9b4397fe6.png", + "8a64be918c2b45368331bd8bf708bc81.png", + "8a8ed69389c44c2bbc12ddc93dabd486.png", + "8a92fb34b19d4446aed136fbd26c578c.png", + "8b14fb9cf5d64911ab786f62b3ee97f2.png", + "8b22ff3292924fe89b69815d09dcdd2d.png", + "8b34135f89a848a3b7bd6743cbd399a8.png", + "8b4aab72b22b485ea719ab14f85cb151.png", + "8b6617980ef3479983c44413d6ff9b3d.png", + "8b9c1147f9764916b804fd1a13f46f6a.png", + "8b9ec22568d84cdb823d1440c6c748ad.png", + "8bd6d9fc9dda47de9e6ee4873a0ee315.png", + "8c084c8285534718ab8afdd0469a7243.png", + "8c08a37886f6474f9f8f7cc36c94b53d.png", + "8c12dfdccfb84f2da4be04268694e2e0.png", + "8c786483577049d489cd14771a16c4e7.png", + "8c947ce47c2d42a3afeefac592968f38.png", + "8c9943459d8640f8ade1fec0a2e1b387.png", + "8cdf8fdec344465bbae4addb4b9ccc2b.png", + "8d0626c1e41841a7a97dd8cadbbb4ba4.png", + "8d0a5cfef18f43e6a060eeb13b10578b.png", + "8d1713c04c2940128dfecafde4756186.png", + "8d6b7761aaf542a0a9c0eab33ac8ec97.png", + "8d88e0f4ece04521b183d6771eccf330.png", + "8dcc644a047b4ae2b4fa2b34b32bf8b9.png", + "8dfffec0bdaf44d5880c36376540f090.png", + "8e11dab6c55b45559da2e3a5048fe4c0.png", + "8e14c9ef0b00472ea018f9b17d024ce2.png", + "8e1eddadb7404f2982a0bfb71d2cc6be.png", + "8e414c8d70b54c1aadfa86b696b92ff9.png", + "8e945fffe3e04a26b44d33115041046f.png", + "8e99414d62a7408ab42bbd36d14606a4.png", + "8eca169d69914c6187d3078347d72d0b.png", + "8ed09ab0ff9f42809ac985c2cadb9f81.png", + "8ef8e1e20fe542e497881f133fc8860f.png", + "8f00f0d169d84e32b7a90068b4fb8831.png", + "8f1751943c694e7386542ca9fb0118de.png", + "8f39ffd544264f30b534d4ab58e1f823.png", + "8f4d212afbe94862b16816341094884c.png", + "8f777bae74a349a09eaddb36af480bec.png", + "8fd9bb49b9634aa690fe403aa47e3880.png", + "8ff669487c1942038e008fbc2ad57cd1.png", + "9019567549cf4f24bda763795e2d2b5b.png", + "901d4726bb2544578a58a26c2322984e.png", + "9020d02b960a42d396b23c13cdc3ae8a.png", + "9021241a42c641a8bcc2193170ff4779.png", + "90368bba30374e4484c66dfed1e8f801.png", + "90558fd5de954d74a513d4fc70a520bd.png", + "9066a810bfb64fcd914835b794e62962.png", + "9072b29949a642ee9b70cea7dbb12e99.png", + "907537c466d6416ca255a55d0efa1408.png", + "90989f4c1a884c39aee82718b7a6a66b.png", + "90b1e82c1726401fac8968c9e9a0ae49.png", + "90dc68a3791f4a348dac4ea6d21936e4.png", + "90f9f31d53724c979d3847ab98a52f39.png", + "911a680836f44dffb5eecc6aa3588241.png", + "9145095498ec4e74838f611962a87121.png", + "914eb1cd883c471ba5c8076ee714838b.png", + "9158ef79c65d4bdcac59872f1e93e99b.png", + "915964048760479c856a18d38bf4c010.png", + "915a06da503247fbad10fa8b9522ae1b.png", + "9176c2483d1b487897e54d01642cb1f4.png", + "918c4ab4a01f48f18b5778942ab87518.png", + "91920b4d321a49fd94c50385c5356401.png", + "919c4cd4bd514b0480407619b7b85c07.png", + "91a79cc6d8cd4de6a647886c4223ea95.png", + "91a9002045f646baa3d1b448176d7af9.png", + "91bfb67373294defb883fad9772339ea.png", + "91d4a9f207d74ddf9ae1374047f6aa2c.png", + "924e6ec8317a463e8f7f21ec4e8fa9d2.png", + "926a7e38bae840d896371bc0e67c2fc7.png", + "92a6bf82de7d4d4582e4fd003b895345.png", + "92b2752736834b628effbbd7656571f3.png", + "92c12acabb0c4ef381ac8a5b04c0b84f.png", + "92cb72eea9bb403eaa09bddaf556201d.png", + "92dc1ad17072469281368e0ef8e85263.png", + "92eab84c630d4dc0b42d879810f4d122.png", + "92ef743f517b45a5ad489647f66deea5.png", + "930ad47d6e7e47ac874875fcb2c9fb05.png", + "933560ef25a24b239c2e5f46cc50c033.png", + "9339d1e79c2a48d7903d0464ac045b18.png", + "933c48e945004c25a454811e3d0e212b.png", + "93432bcd24794f298214c139feeeba97.png", + "9354de0df1bb42ba8888749b27444360.png", + "935a72e7324047009ab87a19a1c20b33.png", + "9365842d4fdd4f5abd360128383ac990.png", + "936aa40a98fd43718355e9b060fab5b7.png", + "9370acdce1a64127ac02525110e2058e.png", + "937294cb5a4045669abb1a4502886eee.png", + "937e0aa8b8434a78a5462a5dd9608b24.png", + "938f2c6b6bf54a60aa0b25ee5e4dd033.png", + "939444db544d4adfafb59ebf7f3cb306.png", + "93b9a51abeab4832b857d939ad214740.png", + "940b70d999154c36bd546a67d632f880.png", + "941770b1d3664c65a3dd7a125125395c.png", + "941992bedb7345549da2a1b7caa4f1c5.png", + "9426d907506c4f1fae01b9119d0b91a4.png", + "9462cc96fb404b7e9136f4e9cc1a6c96.png", + "947c27d5dbaf4b81b2f5075159d09b00.png", + "948067b87a32475c852c0fac61a5da01.png", + "94963b18ba27428db12dea37f7eb0789.png", + "94aab986e920434db3efdab1fd7da49c.png", + "94ba70d740204f5b8f9ec9357caa0599.png", + "94c05de78acc45569ea25ba42f65aa7e.png", + "94c1924963ab4be88c199321dcc9b00c.png", + "94cbfc1300564131ac2224e2e8bf25cd.png", + "94cda9a34ada441198b5b2b0099b243b.png", + "94eb6830a8244156b5382cc3bda5729f.png", + "94f5c8ba8dd84a0db12f45f5f0847254.png", + "951168ad8fb74fc08a31b05be01b8a24.png", + "952c2923d92e4db6914337b962212833.png", + "953d25d3abe1401aa223b73efdf2d102.png", + "954b5f53a06545f0985b69cac4900556.png", + "95565fe6934f45c48462b6816f599739.png", + "95771f9fe0084b65bd3076929fd55e50.png", + "95a628af63d64977883fef0a91bd553d.png", + "95a8ec0a2c2a4e2d852165f90daf4337.png", + "95c1a393d18c4505a165201eea35cb57.png", + "95d23d0afd2347e9bcd8db6b6edf5248.png", + "95d32e72eb1c4c459674e615529fc42d.png", + "95e88f28ae594d669ac1e085f1b9c9be.png", + "95ee186a8ae44d578f6978bb1fa03694.png", + "95f24fad14e64735afbfe68b9b30c987.png", + "95fdc63d6a554bd8a3b84e47bcca9963.png", + "961397f4011540c3b56929cd3faeccf3.png", + "961d185ec1054989b303fae5fa30e414.png", + "9625cd8b40424f2bb8b26de9d583d84e.png", + "965eb23adc454e11b1d2bc55d3af69d0.png", + "9686f86919444cc59730698ca3bf8448.png", + "96ab796311eb44fb9ab55440f1e8dcee.png", + "96b20e12b1024c688e5e7b7a72aa15fc.png", + "96bfda0f353e4c609c5f3b469a8be8bf.png", + "96c5054c73df405e8d299702d30d1f19.png", + "96c84e2587d841eb8de899803f6d0a6b.png", + "96dcc41b2ad545a2bced50fd53423601.png", + "96ff7055eb3d4b4992da36b561ec6595.png", + "972b253e60984e408769157e3de14e14.png", + "979df64495dc476e8fdf747e386cca1b.png", + "979ff9816d894f23818c1d84e551eede.png", + "97a48a6c8730410db0da7da0e7fdf930.png", + "97d9b817cfdc43e59bcf885d4a35bdc5.png", + "97da7e8e7a0c4253b5838af572271141.png", + "980e62e637874999b415d68ee91d203c.png", + "981a76338c13449683b29d548040e2be.png", + "9825011857dc40eb8699d623dc1d6bac.png", + "982cfca3484741e4958e9c24b1588f06.png", + "983c760204fb40b387b40ac0e37e8bfb.png", + "986ca9173ea64fac962782d6b30456c2.png", + "98d157bebe3f422cb339d7bcb6c4c4d4.png", + "98d903c8aff84811b284586424a3928a.png", + "98f60afeede64351a80ce9fe7ef0cb39.png", + "99043d25271144278f3a0bbb8dab1339.png", + "99075cc3f8e746cfb10c5eebe21a3447.png", + "9908d1314210483b93da8e119fce2545.png", + "99187442f97d4c57a4665796d0591bf6.png", + "9920fc09123d4a8b9acaa8297ea9f3b6.png", + "9934685e7f934762a7c5e9389b65ee86.png", + "99821b0890e642479d94b1c27500b198.png", + "99982ae1f599402989219c00d465235d.png", + "99a5d098870d424084965e5f154b0c2e.png", + "99f5cc027f7a4ecba08d15263240b325.png", + "9a08b7b205f24dce94394343c2a0ad2d.png", + "9a2d9e01b5544a1aa6646188cbaa6720.png", + "9a3a5f46087d4d8985a71e85f6570b01.png", + "9a40898fb995486d9464a83350962c9c.png", + "9a43a44ef71e4bebba7f9e7a96d70af2.png", + "9a53f873aa3d47c89d16edd2c636f904.png", + "9a73c187eea749f29b26f1936177bf5c.png", + "9a7bd13e25584b7183cc54c4dde19f60.png", + "9aa7d887e9664f60920374b90d24707c.png", + "9b1b5be2d21f422d839d1da1e121f7fe.png", + "9b27aa5bff744b84851e699b5168a93c.png", + "9b6915ebb7d9401ca42ee6e9ea3c623d.png", + "9b76ebaadc3642298f58a35d3f9c5930.png", + "9b931730ef69448ca4a8419ebf9651fa.png", + "9bae913db18b42d6993dbd7d2942cc12.png", + "9bcdcd974b254e359de8be53e41bf938.png", + "9c0bf6a375fd4c09a1c1893e074723bf.png", + "9c14c4281f91490ead809164a2ef9e43.png", + "9c306a36595a44b6836a846e648d3274.png", + "9c3fae5e39e342f2ad26922adaa0d74b.png", + "9c4ce6bea2d9423ab4a15574dc39881e.png", + "9c51f0bd94ed4553ade28267d7e1bc3b.png", + "9c55cbf02b744009a9dccae412b441fb.png", + "9c81deb2ea6c466ca83a48b9a42362d7.png", + "9cc4d50cc55941ab8b33d83de31409c2.png", + "9ccae322cde84d8096d4a6f914e05559.png", + "9ce19804e91d4c2ca03d5665ea06df69.png", + "9ce98bf2023a4c5b852479e24a9fec12.png", + "9cf93e3a1859449fb19ec450c5c90400.png", + "9cfe025e277b4d62ae8ed16780a28a47.png", + "9d1f792d499e4811889dc459f2cb5dd3.png", + "9d622690002c47f8ad31ad4eea41f502.png", + "9d8e49167a744927879ebeb459862c25.png", + "9d96ba92a2f946dfbe1a309d1ad3e876.png", + "9db9e5a8077342479f7fd981ffa9eb8f.png", + "9dcb76ba27c74a778203818688e60b85.png", + "9dd526da6a0b411eae7f003638b3810b.png", + "9de2103742844e92956785be06ccdec8.png", + "9e0994a815ad488f8db783d10046ff59.png", + "9e16930579aa425fbccb4252a323109e.png", + "9e2263cc98bd492985b1949ce0fdaa2e.png", + "9e23d567b0d74ba2af0ce92051d54c7d.png", + "9e2a40352a6a47eb9dd5b979df0e5e7b.png", + "9e6ddf47e7a747c1a325469bcea54cd1.png", + "9e5c6b75d33e46e1827154a409660826.png", + "9e94fa47cb4a430c87bc9df57c4f416a.png", + "9e9c0e396c744c5e9ada4e1fc5fbe336.png", + "9ecfb0fa61534c45a204ff99b0ca3c72.png", + "9eda3fbfa6c845838a72b4b572b52acb.png", + "9ee44de0042e4318a041c3b0f46c6cfb.png", + "9ee2bed42b444de987df487fda40b7b6.png", + "9ee578a8af3040ca9741949ef2dc6577.png", + "9f17ed2460584068b14680f983ff4081.png", + "9f1ceaffb25643eeb30fdef1264dcea0.png", + "9f0350b9d70e4e27ba8583320408c57e.png", + "9f327e0cda8549bdab1aaf5fd286cca4.png", + "9f7ad818e3594fbab4de21f29c005721.png", + "9fbe920ce330450185cfd9d817ead660.png", + "a000b9e230b7425d8787721d86b2018d.png", + "a023086e0c4e4704ac91c23341770e9d.png", + "a04bf8dda5234155a450030ca4e5fc97.png", + "a0698134d9bf4cf7afbefcee790c2707.png", + "a0ab746cba764f1ea1c0c4827541210d.png", + "a0c0a34d787348168ae092106c54fe84.png", + "a0c90610e0974302ab1bc8107a06958b.png", + "a0d06dd8dc234c289c5b533b0fbb7260.png", + "a12d69e321fd4bcf8b02202b5c233e2f.png", + "a12e6f07ad2b4a7889630c3f3b181cbd.png", + "a13f7566b02b4403be3833329727b019.png", + "a142692fdce142d0b6949dd6e1a879a7.png", + "a142d75b0c73486db80dfe5ae6653b72.png", + "a151d12e1c0c43b9bc8c01f79d84c944.png", + "a170aa0dd4c24e189c5f4c018a376ba4.png", + "a18f2a58fbe1422b9d7be036afb17d2f.png", + "a192dba889e14abcbf8a94d3fbcdf01b.png", + "a1b0345f2a5e443e80a5cf650808a708.png", + "a1c986850cba4e8b8313932488a5d663.png", + "a1b9969e70974f9dbe3b1853d2a4d0ac.png", + "a1cf5ea39bad42bd975ec0d8d318a3d1.png", + "a20ba91429664048a795e55534debe80.png", + "a22286b4f54d49748a4b98937381b0b7.png", + "a2398e55a2c444459c84235915ad747e.png", + "a2424fa0f717438385b2910000bde8db.png", + "a25ba8a798834ae799dba8641920fc91.png", + "a27cb6d1d88447e7a29fc6a1b0e39c32.png", + "a26a25e83c294500bdea90b877e9ccab.png", + "a28bf1bd756c40bd8099c15509ce0c0f.png", + "a28c8037f9d44bbdb42e8735f63e972a.png", + "a319d008baf948729a3e0baca7fbef0a.png", + "a2beefbe39084e6d80fd24353199f61b.png", + "a3755d12f38c474fa476eb050ce32813.png", + "a387466123a04b6f9248b9ceaafd5340.png", + "a37d75fe107645b2ad927afceb0fb35f.png", + "a38983d44d2a42fc8449f766f46860c3.png", + "a39602eb649041e4aa0d22a9761da500.png", + "a3b700737bab496fba31fe12ac8bbb1f.png", + "a3b714bd81d74b6ca7e8d7ee204be277.png", + "a475e5024d5e49f18fde77153102fdcf.png", + "a493ea191e7d41ae881af8cc7dcd5e9c.png", + "a4974bc72cd845a9bbed6c4b6923f520.png", + "a44a5be3a63b42c1b2667c926be28c26.png", + "a497febe878743da86de25ae5d4b4a2d.png", + "a49931cc67be400ca5dba1814d5ebcee.png", + "a4d1524ee63246b48259ef8a47472d33.png", + "a4f964eb29484b718ef5b799edd45eee.png", + "a53e4a4dffca4db7a4cbc4563ba996f0.png", + "a5bfd8f44c33478fa456a1a9e1d68654.png", + "a58bb3befa3749e3afcd5e0f3a49c1d1.png", + "a5c84d1ee2b9489eaf83852bcef83317.png", + "a5cbdafa5ff7400783d4246e5abfe96c.png", + "a5d08060f62e456c8d4249a9781789d5.png", + "a5e3439ea0ff447fbf51db9f9137d7a5.png", + "a5f1812646214369a6126c13c79ef2e3.png", + "a5f6eb9d97ae438d9f2d04ebceb88df0.png", + "a5ede46908c843d182de757dcdb7d084.png", + "a5ff014942e44e9cac9e0ce9c70e66e0.png", + "a630ef37a81d4150b08d1f8f658230a6.png", + "a6338b8be299414b9de575122ef755ab.png", + "a6a5db75a06e46e3a67f5894ae6ddd1f.png", + "a7051194db704c8ab05fdb1858fccecb.png", + "a7108550370d46a78bb875e6797cc21f.png", + "a73f0931fb5743048e6d3a247ffc2cb4.png", + "a74926cd23ef4133af094fce053ea163.png", + "a781bb40efd04cfd86dd8b1efb509336.png", + "a79c4efec0734e729ef6333218e0e77b.png", + "a7b884da1e8244b28b460ef6039d3ece.png", + "a8352eb025e245a99d6edf0dd98e7618.png", + "a852c5f8ce894da2b09ea95086ccad47.png", + "a85b7840057c494e9882b9e8d817a808.png", + "a88cb8f189504346b97335eacad3fd97.png", + "a88e83366d404141b819b0a9c21245f3.png", + "a8b7f25a97a540b3883376ebfd719e84.png", + "a90622f9906d4dbe971de5b4aebb1dc3.png", + "a90b3c87700c4372930aad93d50f4f00.png", + "a918ef59613d42ee85dc85e686ddeaea.png", + "a95a71d8352b4ee09918b3c84414dc57.png", + "a96f34d8f292420186da2e155118ec15.png", + "a974544f33ba42c8a9dc9cbc362cbdc7.png", + "a98a0a6b19c04c0ca41ab05193742808.png", + "a9b37a38dce341bd967aba0ebf7c5b25.png", + "a9b926ad95e343fb8a667ed2e976d526.png", + "aa12db79d42c4fee8ddd8547e4522dfe.png", + "aa1ba2637da2419bbcd3286e80fcd094.png", + "aa2c237103054368a741bbdbf50dcd2e.png", + "aa2e2a450d9c403c9c9718f3f367783e.png", + "aa4275b98a534cee8dcce76790404dda.png", + "aa45cf8cf74a45028e84eb7689672c1b.png", + "aa496297ba4e44c2b95bc22365bd10b7.png", + "aa4cd898b15747a4a564583943040e79.png", + "aa6a60841c27434b852bf0ca9cf889bd.png", + "aa857d0d86b44b3d931d933db497736a.png", + "aa8d98236ace49c9a163dddd34c63f9b.png", + "aaa0c025c7c14a03ab6df454da20f69b.png", + "aac3d211bd414cf2843c4a26debc3569.png", + "aac70b1edf5d403c87f1696d7aa3ed41.png", + "aaed0297a2ec463981eaef0c12c3f069.png", + "ab138a45e4d0442480d378c9c7f80537.png", + "ab543aaefbe04a2cb2265ad508ed1059.png", + "ab56fad17c744a2687ab3aa48f9ca95f.png", + "ab57c954154e4d9491bd39a8f9ea036b.png", + "ab788c980dd5406ea27c48ea3819fc2e.png", + "ab93fc4e609b4c0aab375b743e0047ef.png", + "abb83c48fc0b4c03988bdec35e932b98.png", + "ac095632de834507beae63445c3fc21f.png", + "ac308632cdc046b887fa1feb8357849f.png", + "ac4c8a14b90a469f817cf30216f0cf33.png", + "ac5cba5940d1465cb1c3cd16f570f596.png", + "ac61f115bfac477bb9b4d996314f1168.png", + "ac6d869b0d3d4f50a12bd62439ac56d4.png", + "ac8f91fff46243fbbcbc6e5326ebe81d.png", + "ac91fb3ba2c74a21921fd520f112b068.png", + "acb12101bc964efab281f4c5853dafb4.png", + "ace3e21a82994eedbf1b54610d5c34ca.png", + "acf37f2910c84ff68484aa82382908bd.png", + "acffa23edd3544a59101e9094554bb86.png", + "ad59e845891d4182ac771eac424e4734.png", + "ada95965b6ca4eb4a202e9d348e42a31.png", + "adabd0a8b4b84db09829bc3653db6f75.png", + "add68e92c165402793665a07340db719.png", + "adf6d4c9dbc24242abaacbbbdd768527.png", + "ae23ca4762f8420d90f3499c9edd7f22.png", + "ae2c788c342541ceae1e46786e1226a7.png", + "ae36441f71444efba5dfa2a6b683f3fc.png", + "ae3a22db80884b13897e44cd12a5f42e.png", + "ae5047161e7249319f64e4100c7a17e7.png", + "ae83e57db0e24d4eb5cd36a41582d810.png", + "ae97a9ce5a454ea39c2f8481fe3d00df.png", + "ae9e90cf50f44a4883087be6ed8665cb.png", + "aebe706a29cb4ab99459f63c471c24d3.png", + "aee87f8a5d884b429a33b2fdf9651db8.png", + "aed4de2f588049e4a04e1aa3b759d824.png", + "af30e4a5ea6c45d293f5dda2d5479a68.png", + "af4c267fcdaf48f99ff7c91470f4bfb3.png", + "af4de035885741b3a2349523d8ec4224.png", + "af75ed0ac1aa4265af71b324567e40fb.png", + "af7683028bec4546bf6fd7a7fae941b5.png", + "af91c066babc4e4e92d4d7c6fe9a4e71.png", + "af9264c406b34ca486d6fe31a6eaeffc.png", + "afd3a26d771d40a5be3e401e874fd77f.png", + "b01653023c9b4ea0b0fca7986155a205.png", + "b02b44223d2c4d9b8bb9a2988e1a0998.png", + "b03d0529b2bc47a99937812f1abe4852.png", + "b0853863a97e4c1fb899438e3ec6c12e.png", + "b088b4d5a8f749f48f63538322b66d7f.png", + "b0ef8c9d7ae74f2c87089cab49a49875.png", + "b10d373c9d404522904e75bc06ff0ccf.png", + "b118e7a9b80545d4b9202755186c21c3.png", + "b118edd6007e469999c1be66821d06ed.png", + "b11d3c4d01f941d0b1537965897fdd8a.png", + "b11d41580552482e91e3726e3687557a.png", + "b13fb00c11404789aada69d6c9b05e95.png", + "b1524042fe584db4b38bdc36fb5d659a.png", + "b16696a77a444baf99f35d6782d223f3.png", + "b19fc9b4217a4e84a4a5bd0c4472824e.png", + "b1af13ef454540a59677f24e54d07128.png", + "b1d0e221ad0246ea8260a15546bd34a0.png", + "b1f8776a24894b6595169db683652740.png", + "b1fe4c1bbe2e445895f61ce9ffd77986.png", + "b22b9c3e645d4c378c333b77fa2fc158.png", + "b22c43e6684545c7a3272fe7ab62cd58.png", + "b24336ff530c441da95da3d0bdd93a46.png", + "b24da68daca04a9cac394a49c9a6c09c.png", + "b25d45cb32fa4dd58ddea61e9eb9f901.png", + "b2609d4399b0458c964c1e582e520020.png", + "b271982ad2eb4975a721751717204fe5.png", + "b27bff1d63594db596e6e01d798c59ab.png", + "b29961634aa24b1bab607f8eaa9f9179.png", + "b2a158e504e04abd918950cbbd35a573.png", + "b2d961c7b4034b2982f3ebe84558043e.png", + "b2ec3995118a4d1295c97c4d1888f4f9.png", + "b2f642f3e3bf421089bf0cfb6dfc950b.png", + "b306ab6e37c649c59f71aaff948a572b.png", + "b315726cd31e4e708638384d9b191b9c.png", + "b3490038b0f6449982e5c823132750c3.png", + "b363523399634f28b9328ddbf791d537.png", + "b36ce83d0cf54a2893d46394a2a5e722.png", + "b373000cb8484c13be1c65c18c295c11.png", + "b374ffab2b694c85ae927be38f662d65.png", + "b38bc2c9886a4ff38c6b0d923e5d3304.png", + "b38cc5f1d758403583ad54d751139fcc.png", + "b396e13762d4419d987ad6a287786fa3.png", + "b3c77e424c5a42f796bcf915ff091153.png", + "b4110128261b41f49be497225c0c75f5.png", + "b47b323e1d8948018206b2c99ca50e2e.png", + "b483584cd81b45f4abd026165dd0b4b6.png", + "b4bcf2454be247be9693563ceab115eb.png", + "b4c3e0a9d2b249d89d239be647f6a07d.png", + "b4e5d4f5805445109ce55a7acee6f0cc.png", + "b4eacb041f734950897e4f404802df53.png", + "b51df04c97c74456ab92d0deccfcd2bf.png", + "b54cbf5bc73e46f5b5915894b4b88709.png", + "b559822c7923496b987368bfd219216a.png", + "b567c4a05c3b41ffb69b503f0d2717b3.png", + "b56ab8a11b3a4173b50bc23875ea2ac1.png", + "b571d278e2624991bf21cf6625b355c8.png", + "b576f22b09b14fe8ab5424e96aec038e.png", + "b59894600dee4d0da6bab75edc21238a.png", + "b59ee2f65404457fae0c01a817a0d45e.png", + "b59febba4278469f84082d4cb2025083.png", + "b5be7980340c4440ad2e218e3d50bd5f.png", + "b5c1ea79eebf4a368911339cb74fb9e1.png", + "b5c502e1aa33451bb6be9f65e7ed63e6.png", + "b5c71172b5a544fbaf557ee4543a9462.png", + "b5cb5080f77440509c3fea8db4ea2d82.png", + "b5e8b1457d754477bbad4d67aec90104.png", + "b62328bb41564701a0664326c6d358d8.png", + "b64600d7b9e54b14b3eaea010fc02c24.png", + "b657e664adbb4019b4cbf4861bab641c.png", + "b6a02102f6b640d19eb90370df4c3148.png", + "b6c555f293534edbbfa876456c7c302f.png", + "b6ca5259766a4eaaaf0fef25c77e0879.png", + "b6dc5159e86d4c4e95188f3507c455f6.png", + "b6decedab7c44a82bab100733f3f0e48.png", + "b74cb3b8688b4d618e1d7d8bc2e074c0.png", + "b76763761f3d4fdea09a8406f7c56e9f.png", + "b781626cb3f6478ca653e95bccc4c7fb.png", + "b7902972d2664eb993eb25193fffeb25.png", + "b7ea41fc7e864d54a7f7474135ed158d.png", + "b7fec1f978dd42e692131176e4770b2a.png", + "b80ef55833cf45f39fef2650072cc88b.png", + "b81022606eb24d4f9515f9a4d748efc1.png", + "b8116fe57bd84a5e98f14f0a0c25e6d0.png", + "b81bfd67e5a346748339cd5ca2806c5b.png", + "b863e030c2c447faae19b39315093711.png", + "b86af2c519f14cb6b968606039bd6eaf.png", + "b86b13e7020f4c51b4ce9248f29376de.png", + "b8710298c8ea474e8ffc6bb83431e8d0.png", + "b8925bffa944483c9c09e454b5c240e3.png", + "b8c08146973b4dc0bd6ee9e45d573791.png", + "b8c685a148154c0a8782760f558e632f.png", + "b8d920ed953944d99d4ad2be2321e3aa.png", + "b90e4b13948041749a51df4711ce30fd.png", + "b910e5bb28f74b5598a96d7be44fd0ba.png", + "b94543962965451797c056d98b506322.png", + "b96919401e7a467e98a80d45a4096163.png", + "b989a702e038458e876fa2e1e6220a28.png", + "b9ac17c4da3b4f929fc50834bd351bca.png", + "ba228b1faa2e4fb99888003720e26ffa.png", + "ba2cd559d79a46e19fa6615147cda3ee.png", + "ba529d7363a04275911e152d686a3b77.png", + "ba76ed56d8804d37ab275899dd0c7db0.png", + "ba8634f9a94f4be2a8d3ea3d7d946b43.png", + "bac60295ce2c4b0d846ac5b5a9b8722f.png", + "bac637bce9c84ae1a48c1bd3561806cb.png", + "badb18fb1dcc4495b3061bb0100db63a.png", + "baf18f733f6048c8b491f5ccea43b8df.png", + "bb15f43cd93745709508246abdcc4b40.png", + "bb3767d446094ff99067a04d0c812dfc.png", + "bb43438cf2be441183b39b87ccea62b9.png", + "bb5d9ccabf284e929ac7a505e08febc9.png", + "bb67e8f7631848b2a4a6bfcd757a5d2d.png", + "bb737137e8004581bd537eda0b53112c.png", + "bb79c438a44747aaa88ef0f8cf9397d4.png", + "bb86ca6c36ee4357beb32fa904932aeb.png", + "bb9340ca65054a829bdac5a8cc170928.png", + "bb97f46947464a9994de87af6ddb5770.png", + "bba79f3528454ada905b2f3a992f213f.png", + "bbb61ecfde1c45f79ce9becb3c5489cb.png", + "bbce8b74c6884fefaa8551a307fbb82c.png", + "bbff5df0a84a49978aa504f350c8564c.png", + "bc094d809cf24aefb8697fc6fdc248f0.png", + "bc300c2d328e4fa38da566f17f3c8355.png", + "bc32ba2b47c949569c519fe57b6c9cc8.png", + "bc4389c3e1af41c1a7890368a94f6e46.png", + "bc51d98820e3495e8d4911e1f598a946.png", + "bc5f7cc7317b478d963f628c7134e54f.png", + "bc65e43c103a43329e99a54cf4acede6.png", + "bc8127b08c884960a0014c57e50a4f92.png", + "bc83a58e014448b09c925881419436b1.png", + "bc929fe27ed34ebda0de760c028976cb.png", + "bccafb2be678488eb232b7063ae1fd10.png", + "bd349e19b9024e58b16fdc5fb6e1f64d.png", + "bd7d0ce130c646f6b1e61046b4a65b03.png", + "bd88c1ed37a04d64b247f5cc907dea13.png", + "bd8c29ee4b59446784af1995ff4fe2e9.png", + "bd90cffc4e8b4de4ae7f662cfe3804c3.png", + "bda5a309f4ab4dddb2e10f4174ceda4c.png", + "bdd84cb102e4425482ce731b7638db13.png", + "bdda66abe8db4fb6947e00aac3362195.png", + "be05ac43e24f46a9b574712cae55c587.png", + "be2319e528544df5bf2615da04f573ad.png", + "be310a405d7e47099074256c0cebb5e8.png", + "be62ee0e57bc4fa198af2320d9f7c492.png", + "be8231cd7709417888eaf01e5581bd2e.png", + "beb697e13d024b72a6927a7a2ff000cb.png", + "beb95d02aad34937a42565f6250ded9b.png", + "becfb4d3df644914a0578d82b4d17cd4.png", + "bee5098be83a4d0e9a2dcc167b0ff2b7.png", + "bf07c0faad0345b9b72edb4f0f78a5db.png", + "bf12a250bd444fad9b2f6d2a0b27e392.png", + "bf2f6c1dd0c043b784d7a557ef2eb81c.png", + "bf361a5c835c4a09a5464f7f7819b571.png", + "bf4a350249cb414495adf7309d8f3e4d.png", + "bf61032472a44a51bbfe1bc6fed6647d.png", + "bf61545770de48f19b4e6c91307d41c3.png", + "bf633a54df0846b093c90db686aa5c30.png", + "bf8602bb7b5e4a2fa3e9fbbc8086c79e.png", + "bf8847ccfff44b63aed9c39c79ad6c1f.png", + "bf8cf156c1d74243bcffbccc0be574ce.png", + "bf98c569b9aa4208b591e9ff200c911b.png", + "bfaf4176cf304e54951575a8e2b418f9.png", + "bfc54eb9204d4539ad436620d744038e.png", + "bfcdccaf59aa4f0794a8d642177ff382.png", + "bfed7d3b958648daa1dad1358dd81877.png", + "c008e6b804c1436d89d7ffb3fedd4e45.png", + "c009e7ac302f436c9a26fae00a4ce2d4.png", + "c01e9712f9544336ba7942e4d986df4d.png", + "c089a4c8401446c5b4644bd203206a6c.png", + "c08dc0d2c8af4506a724ac5f9fbd3003.png", + "c0ad4f41bba54c17bf76a7417d6a24f1.png", + "c0d06ff4f0de4a59884bc0f4b4786528.png", + "c10bb6ae75c7476c8a672ab2c8b6dd9e.png", + "c110a648d3e64c2e9824d552bdc0b5e8.png", + "c12194a54207489a940a478573959ebc.png", + "c124dba7fef447f2bee2f2f9e8f76d74.png", + "c132812d7811432bb7ab1a957e2e73e7.png", + "c13995dc0c53401e8dfb3c6da81e6703.png", + "c1569fe0ec734f41a1c359085d76b669.png", + "c17302737fff41e68759e546efa58e60.png", + "c181cb9c6932445b83ea5b0e1348b7b3.png", + "c184a97560d34b1db3476d79833b3ff2.png", + "c1a153e14c244aed91e53fbf4da31d4b.png", + "c1ac7415a2ed42c8bccd2c85cd1c011b.png", + "c1b2155b2afc4329b598c27372f1e6ce.png", + "c1c19933f22a40e7ba48fd950beb2221.png", + "c1c74e0388a349819b3d4d55a1ebf13e.png", + "c1f4de69c7934fa6a3b561d9d73ae25d.png", + "c20fe8c868684a33b0c8ab98ec6c3e9d.png", + "c24a6e049d8f4af49314ff967f055258.png", + "c250d0bc295d4b2cad07efefbcf3322a.png", + "c25dd4fc5ac44a90852420cda5f5a3ba.png", + "c28bf8f196f2412a80e3f02f14f50c68.png", + "c2a56331f3a241b996e0aa4ac0df84ec.png", + "c2c4edf6411041baa0c882b1235b33cc.png", + "c2d61d7b9a6a4e4ba3ad77a351fa8154.png", + "c30c8e6aa46e486eb4c348ac7f3f6faa.png", + "c31243ddba724e7b8a7095961616f25c.png", + "c31d66208b2243729a65c15ef2869e43.png", + "c387c6fa65414486988312c739c38859.png", + "c3d8ff83c4064ef59f5d6c0264b69304.png", + "c3df27ddf37a453fb3112c88ae31b58c.png", + "c419c3c4f13143c2b6de455a4f9a9128.png", + "c4230ce815204bc09bacd444924009d9.png", + "c42f9add56d34d149856966dfff9c80f.png", + "c4359eb294e2431b84ecc5550ea4c1a9.png", + "c43c9eca974b448da8f99eaea0a08555.png", + "c4490f61d79b47768287f99cf6c66b1b.png", + "c46c99add1484a37b8d67c7f5dea41c6.png", + "c4729d102be6461c87922162d73bd179.png", + "c48f3e18a08d4dcfbb57e9c5d9d57987.png", + "c4a6ac6153eb4422a296f5758ea0edd2.png", + "c4af68ef2fd54b48b3c8f76eea875feb.png", + "c4b5dc45e0394c4ea75a645f971a1418.png", + "c4b62705578747928873d4c0a5c580dd.png", + "c4c9b2cc3d30403b9b966c394ac975cb.png", + "c51be1f3a9cd4cc78636a69e016eed42.png", + "c52d7a7eb4a344a7af2872e3dd0e48ea.png", + "c54c472fdd3f406d8020934f3748c2f5.png", + "c54d4057d09042d188396bb6ddedab3b.png", + "c555ffe6d1af4e468c665e6bb4c6b9c3.png", + "c55b5481b1844d5bad4306cd78b46458.png", + "c5808fe63f214fff909490003edbee22.png", + "c59452a9dab64158aef451c277dd468f.png", + "c5b46415981d4fe6b0c58f6801d99230.png", + "c5bfdd0635954e96ac33d463d566f2af.png", + "c5d78345214540a9abeae0ad35b18359.png", + "c6103b6767664010883a9bca17d95b29.png", + "c640f779b9e545fdae9389ab899f69c8.png", + "c669ac13cbaa4f7dbbfb5025ed4e860d.png", + "c6788c505eb44b8da924277f3ad3d297.png", + "c6b758ce9ba94feca0302caafdc4403a.png", + "c6d666aee0d444aaa16ad04cc887627b.png", + "c6f8ff0f5b7a496ab030eef1ef911450.png", + "c6fedf43f8494896b40fdccf2fc5e5ef.png", + "c7250a2d168541ccab58e815013ea861.png", + "c72a78c802844402add7db825b5cbbd1.png", + "c744aba395464669b10d444285960441.png", + "c7544795e8854f0e84b064ed844ec14c.png", + "c7d6f00635cd4e2eb86822c3ca3a2bf4.png", + "c7db4861bfee4a1c8869b2ddaa072633.png", + "c7e70cacb3c24b58ac79e38153666f7d.png", + "c81860c4a784481ab8ed83ab262d117e.png", + "c8335f5a4d5d47e9aaeec8ca9de211a0.png", + "c83a95062f0f4b1d8043707c840f6a1b.png", + "c89c5cb69d7249f895ae040b97afcfdf.png", + "c8c39a519f274fe484cf190f4e3e67c5.png", + "c8d026b0920a4446b89aca9b4f595e70.png", + "c8d13a25fd6c4823be7b9d4b8b7ee686.png", + "c8f348dc97cf42a28ffb745c4781fb33.png", + "c9104b1c9e71468fab46775ef168b698.png", + "c94125443d064567b234b9d9fc7332d5.png", + "c9638872f71849269d3dddb966ec1156.png", + "c96bdec2099e49b99d49fd8a312ebb2f.png", + "c995edc9037c4a07a1b103b1dfe6e87f.png", + "c9c70505741744ada1e9e113609b333d.png", + "c9d42062c40c4fc595fc46674d6e433f.png", + "ca18fe0922f94c2fafdd09769414a23e.png", + "ca59ea0fbeb94ebbb7e822b01c3521e0.png", + "ca74efd9012d4a4596166b29e112e800.png", + "cabd0ec1c10144b7a9bde356e161901d.png", + "cb01acc3c2e544d3aa4ca7db6a767c3d.png", + "cb1ee21715b1484bb7bb656dc456c514.png", + "cb206e893b5649cb8e746a8dbc9af0a8.png", + "cb2cc25031c04446b0c37d07014cb8fb.png", + "cb4dd1577fa5412f909884e85e42d94b.png", + "cb766b40ca324693bfe0a7f5d2937fea.png", + "cb8ab44dc4e044b391f77909aa9dfae9.png", + "cb96cfbb43504305b65747d0406b3b28.png", + "cbb242396c9146bdb2879311b5bd8682.png", + "cbbea8594532414196e53863141f1fc7.png", + "cbc8492773ca443b8ce2760a52235c74.png", + "cc05c21a208b4dd58da1dd54888f46ff.png", + "cc1764f48c8f43e2a36e2e7724c19aca.png", + "cc3ca53d2bb545079dc096a7ba6f8d4f.png", + "cc4c7321e9dc4a6f8befa032c44ad8b9.png", + "cc529590e9aa4f0e91a432559c2c6085.png", + "cc6fe266068c43a3afe9e97b2aa30b6b.png", + "cc95ccc9f3754b9f821eb8761545b5a0.png", + "cc97d1a063ef455aa9e9daab48b658e4.png", + "ccca5e321f4b40908a8a483b8ada2473.png", + "cd04ad0efda445718e89e18d2a314c26.png", + "cd3189197058468a97546a62c927f90e.png", + "cd3d04f672124381a5b3ed8067614a0b.png", + "cd41aa5879c146ba908eb91e27c2ce52.png", + "cd81ade3a26a4ca992152577880e0405.png", + "cd842aa0034042bdb4160a1897925b67.png", + "cdcbed8368a44f39b0a01c2e8a622659.png", + "cdd703c179b94468b6a0ca7d1d48d204.png", + "cdf6b6d491c041b89c50a2cd2c94694c.png", + "ce3d169c8afc4b87bb72acdbb8c9a0c5.png", + "ce5d08bb0484412f86bae8e5651219fe.png", + "ceb14fdeaf734396a08b22306f10c38f.png", + "ceb3fde3dd94495c85e0051b5ce41e97.png", + "cee966c4ff9e454487934836ea43a874.png", + "cf2363445ac1419595e41ff5a401b791.png", + "cf47555964ca4d8a84662d6151fbbeda.png", + "cf4e3a8cc75347a3831e3f1946ab237a.png", + "cf569ca851da4e7ab595da586bbd2d02.png", + "cf5e0e8ff7124e09884d57f27de5d2e9.png", + "cf6270cb227a431ab9c0bb14758c08b7.png", + "cf7e1f74df27400098465dec25355c7b.png", + "cf805a4f9091463297505752de61bb86.png", + "cf926183b7984910bcd85a0291f65c7c.png", + "cfbaf0eaa9ef4841aaa155cb6176f5f5.png", + "cfd2793411104d9cafd6e988e695b068.png", + "d0238d7647f047d18619f1dab02a85c7.png", + "d03eb4ff6636479a8c9827e60758476c.png", + "d04e1bbc799c4d22af4aee64a747843b.png", + "d05d08646a1f46b5aaa04f998a400a5f.png", + "d0845da535bf44c1a4456413d58f859b.png", + "d0928c55c4b64cc5baa7ef23fc5d69c3.png", + "d0b18b119ae147c08934764ceeaec848.png", + "d0e6d410c65049f8a618180caa4e6cfb.png", + "d0fe02b6bad04b5887d355af9c39bc4d.png", + "d1061e386ed84ebf8eebfce565bc2958.png", + "d10d21eb098042fea965917c2415c489.png", + "d110ccc24e3a477fb5cdb08cb29b5c15.png", + "d116047fb5b449e5a62a5c6aac0cd48c.png", + "d12c2facf367475b8c126a983d3e2b98.png", + "d14effbd14fb47b7aee01600a8f8af34.png", + "d15952cd07d6468da91f3fc470e7a2b2.png", + "d1842233b3f34c219a05892466031c7c.png", + "d1a1a55a6e4746cca00652dcd743f8f5.png", + "d1d36174a341450b80d9abdddf86b9e6.png", + "d20911c3041045cbb4e64f983959ac95.png", + "d265eaefa3e84226b94eb7a6a079daa3.png", + "d27304c2eeca44c985989ae6b3b8480f.png", + "d2744a7a5c5146b8ad5ee737244a26e9.png", + "d2949511aae24eef9aed755fe47a77fc.png", + "d29c9bc390284c1eb4a943a6bdd1a98a.png", + "d29dd3a6364d42f985e903e0902e7534.png", + "d2a15089e50a44c4a64308edd1181796.png", + "d2b21635cf5046e0a7b3bbb69d584f55.png", + "d2f1c69c09a44cb1b3fc5620e3a44976.png", + "d2fa3df5ad1d4e8293c2e0a39131de50.png", + "d30437f2f3b14f05bb1cfe39643ba44b.png", + "d315c7de9bc14c2c8dece800a6d0dcaf.png", + "d3716d1782a04952a47829f1c84de315.png", + "d37b84da1a3c40d09bee21bcc00112c3.png", + "d397e2d85ca6462c864466c8d2e7f384.png", + "d3ac4b03ef884d19a68036672007e942.png", + "d3afeacac62b43c1990c1790adc6c08d.png", + "d3c366ccc708493b87a6e1eb695eb635.png", + "d40b19edb58c41ab97a532d3af26442a.png", + "d4126f00a68f4fcf8a33e81fce5e0f02.png", + "d43c82c3dd624f80afcc9fbb9b0f463f.png", + "d46b1ce340e2480d9d588f5f821f056d.png", + "d4907ed4037a49678b656a7a0ec42cdf.png", + "d4be042371ac4200bf690453c8f0fbb2.png", + "d4db3ea173a24872b79ff5ecc069d4ba.png", + "d5017e44191b4a02938b10235244b34a.png", + "d51cfe1209cb49b3a25e19e515a6ab86.png", + "d527226f227b411b9b93b8d77ad2b203.png", + "d582f03899ef4dee98adaffb514dc9e8.png", + "d5d4f0f99fa74e038380efb29fd42681.png", + "d5d5e9acf29849d4917b979ba7a6e0c7.png", + "d5df4a10bda642688553039b1fa339c6.png", + "d5e0059c44fb42c5946ea8f3b15cdd63.png", + "d5e8e83fb17147dead08e9a03040fde0.png", + "d5f6224206c14d24a1d2a0f8ac828f8e.png", + "d5fe9ca8e1014546beafd9204996c4df.png", + "d613a483d8b04ca18efc095801ab3b8b.png", + "d634af5157b448b0b62f578d17db55c5.png", + "d6422a9d8a264fa1943acca6507cee7b.png", + "d652f0b4c3664bf2872d36c830eb1807.png", + "d6586365208e4192b4e104a3e37269be.png", + "d663284ad9414be592d04384ffe989d1.png", + "d67b30fd69b64452b092be2174d592a9.png", + "d68a20e63d8549159348cffa18e78aa1.png", + "d6a16f02b99043fab022872b7d09b5df.png", + "d6ade5536de74332b5fef98130345bf2.png", + "d6c8ccd1ab4f46bc85fa2ae37a2e9c1c.png", + "d6ce25961d6c4d52a2042bde6450637d.png", + "d6cf1b83118b4882ab49389d1dd44071.png", + "d6d1f8e33a2b453ea70a5e11b331d2f5.png", + "d70aa7a1de8f4d778adb3e53f1ccf916.png", + "d71c0259d0fa4be6a721eca8cc933299.png", + "d721628d42394362ba970a3d73efe51e.png", + "d72ba525485b4c52b5b6becc837adde2.png", + "d72dd74cb8ef47acaa83d7fe5ee04d6f.png", + "d730cb10da1c464f8f0436090937c262.png", + "d766766834b04618be2cc8ec4c0c82bd.png", + "d771ae387d314a89a6cdb16980d0f234.png", + "d7785d05cdce4c8dba142e276291ba99.png", + "d7815af8bc464ff489c2fb410d9a7754.png", + "d7941af9b48248538ceb27ed5667d543.png", + "d7c5b46de73e45fab8fff21a90020c3d.png", + "d7d344ce00e4454c8b76bd2c5ee99e62.png", + "d8039ef69995483b9c41e39603d467c8.png", + "d82834c68ef14467b27165b021d61a29.png", + "d851833cab904b1684b68fd12daa64d2.png", + "d85ed85892c1442bb48127aa86a79419.png", + "d86851fc60fc40df8b9a41c68c54e8b3.png", + "d87f1dd8bcd649ed9d5d27861df4d1c5.png", + "d88b0efdc0224694a2090cd3eed0f5c1.png", + "d895ed737c2c4c448a2f2a375ef01c43.png", + "d89ae57b1257444fb853691ef716f2c8.png", + "d8aeb08d41264ef28a94cbb5e41f895a.png", + "d92a7b10ee824120a3fb4d09b32ec7b2.png", + "d934413c4e1f4e3ea1e9954ffebef74a.png", + "d935f6a0180a478fb5230f8d7b78bf91.png", + "d9363fba84d448909e094a851a8b4723.png", + "d93c373d10a54c1bbe2dceb8729bd013.png", + "d962ae3f192d4f52931a1faedfb5c0bb.png", + "d9a30a88dca64c4ab16ef99ed1d75d1d.png", + "d9c08b70194e43928e049672be6c1c74.png", + "d9d4a534192748fd9805b63c8f1ee7ff.png", + "d9dd0ff0877141e7b80085d6b4c577d3.png", + "d9e0404ffe6e475b90ea3a6c8ac497ca.png", + "d9f5fa261c0249a3b53458c4a4355a07.png", + "da0dd4671efd4bbfa21cfda20786bb1c.png", + "da1fd4c0a83b4adf8057f983491ddf9b.png", + "da2d7db2f72d4b809cc8f183282c16cc.png", + "da6687cc59bd4c94b8cb3026b7e935db.png", + "da677dfa18d9488f803240ce98712858.png", + "da952f71ca6a45d3aecbdc40e231a67b.png", + "da9a5d23d0994e04bbcdfd3efaaf14d4.png", + "da9de4e38d2f4115b060eff93f6264ed.png", + "daa3af6e732b4798bbfee6c57a45a282.png", + "dad9556a53dc443e9f9dd67234baccae.png", + "dadaae3ecdfe474e9c65a940833f3603.png", + "dae310f50ccc482d8363ee563ec73e26.png", + "daeb4033b8314a9086b64ab3e21883ed.png", + "daf7abd733624e68aa5cd98bd99e466a.png", + "db1294398d0d454a8516755602a223f8.png", + "db13cd5d93314885b6af702736a26ab7.png", + "db82ade699084b9cb8fd8d2248262303.png", + "db95039db30346acb2b910a7c0001234.png", + "db98506cb6974e5f8f8f163364c507e0.png", + "dbbe5e9306da42e79f914f86dc633aaf.png", + "dbc75ad6dadd4c409594b7e473884b61.png", + "dbdb10a07caa43ffaba3324eda4693bf.png", + "dbfc0d2871934147b03b50b866a5743e.png", + "dc150f2e12014f66b9467ad12c7818fc.png", + "dc2cf97973c540de88e3c519bbe2f8c5.png", + "dc2f7bd503a94781be099eaed3d36b05.png", + "dc45fa58ff0644b19099e1d81645445e.png", + "dc5a7500d0df4d228897b90cc361aad5.png", + "dc78fcaf87d04bbaa13068f68f550fdf.png", + "dc93609202094571b09a4752f9cc692e.png", + "dc999168ac124e2f87f895bbf28c13ce.png", + "dc9b65a8e80b431ba27e1169a35c1800.png", + "dcc972711fa24d55b47fa6bdbab9b759.png", + "dce18e8628634433a1dbbebf7a4753fc.png", + "dce911f3ba4442929c4819a2afad84bf.png", + "dd1414fb66c94cfcb58cc7f4483a7779.png", + "dd3aa17d7e6f4b22aba4171169fac443.png", + "dd930013a7eb44bf84cd3895bb99faa3.png", + "ddbbfe9c8cee4a06a864f20aa3f7648e.png", + "ddc055a401bb4f9d8a23f3eed21c8e5e.png", + "ddc0f15f116a41aeb86606ce69151e5c.png", + "dde771f2c0b646208a556619db75f112.png", + "de0295112fc945278b5a1c5f2afd9ff1.png", + "de34ab040e0e49eaa3a878936c754897.png", + "de3aefd05e6b42fb82894ad8d1a45168.png", + "de50194dcfcf4459b3af175ac211ad9d.png", + "de578b4793b54c98829b4eec9340effc.png", + "de593e417d274ab1945782a53837907b.png", + "de6b4aafeb2245e580b7475e55049f2f.png", + "de78a847ad5d4533bd8c19309bee9fa4.png", + "de85e92ca13541128af02b57e5fe8afd.png", + "de98fa0cb135456bbbdf4fe20f54c65b.png", + "deb70b343d87492caba72acb35f7ad70.png", + "df24d5ff77a74ab0a765c9b75ee66805.png", + "df47163e34e4492ca0bfa42e23e41683.png", + "df54d96c8a534f5498cc1219f7b9ffca.png", + "df6b95d2ef0341dc8167ef932c348c41.png", + "dfb133d8a7744ab3b3219a9a95986e72.png", + "dff7d41d4e8c4ec887e20a9c84f51a53.png", + "e0123bad22884463bc7a91aafdd658c1.png", + "e03abda7284248d68956411545fe9b0a.png", + "e056253062f04165bdd2758f1e43bdc9.png", + "e062218f699946c3a1533eba4b80dcc8.png", + "e0764dca26fe459c85a177f14062da6e.png", + "e07978d711c045dd95d18d590f6d8123.png", + "e0b8d209c2a14e88a072b43bb636ed1d.png", + "e0db009400c046ae9703f6541d7e2374.png", + "e0dc053d50964e1a946eff647baf8bd2.png", + "e0dd43ef6494415a8d6f56535cee305e.png", + "e0ef9cc9710b4ba4af487864f466c391.png", + "e0fabb55beba42e789ae5ef491381b2f.png", + "e12b263c4980473682bbc77f4c314008.png", + "e11dab3fc3b3440a88e05a1bbc41c06b.png", + "e151b96df1b64cfebfbc0ea3938f659a.png", + "e1599289815f49d8aa616e5f98983aaf.png", + "e1577fa5c2d8474c9cb546b0b29ce6d4.png", + "e176d7b3668848f89502d717496f654a.png", + "e194eee2f1d74181859b7e7ad893095b.png", + "e1801f727c30422dbfdb561a5eb44086.png", + "e19785b2188e4925a702df012ac03cea.png", + "e1ac9acc101f43b6984459a15174e975.png", + "e1d4593a30f84e89b10b7db58fd0fedb.png", + "e1f75a0736a4408aac67532426fb3af6.png", + "e1fb869773104d5a9672ae8c0feb2b8f.png", + "e1fd5c560227415e8c645c69e3862d86.png", + "e214e74f846d4624ac5d240334ad102c.png", + "e2086bfa615c449aa6572a4200b601d6.png", + "e21ca7a08e934c3d8e03c88793fef7e2.png", + "e27c20867cac4455b1b12ad3e0a6f2cb.png", + "e25fb8e7a34e4e419afb718128e5796e.png", + "e2953e4266cc4b07a582c89558b6effd.png", + "e2ce313ac64244468fae7baf5b21a0ee.png", + "e2e269d58d174a728b8804eadcd3d1a3.png", + "e2e6bc64e397454990e4bfb6b4f6ee9f.png", + "e306cf5b17d842228c752f17c9fdc29e.png", + "e30b1e49f68f46298fc675782631ee33.png", + "e3149945a02a480290edfa6af045b1a6.png", + "e31df2730ec24c45a2bf5d15fb6e2a5b.png", + "e382e34f4a6b40ed8b770266032ff019.png", + "e3c5afece37f40749716845e70ceac49.png", + "e3d13aada9834d84a4c0bc829f49ecca.png", + "e3fc9a36baf445ea947cc68882e3c1b1.png", + "e4155f45d59f4fe18c1fd1f341a29a6f.png", + "e41c4dae8c404a87960506bc8a008d2c.png", + "e42a1b12418e4fb299a53a38a0851939.png", + "e46ab9efbf60439b9e99074f00b1e5e3.png", + "e474789862e04c7391c0b71e6441df9b.png", + "e4967b4bfee744368600d577a08149fa.png", + "e49c8db2a7254fc1a82a33ee92855e74.png", + "e51b81d675f845959c088a45db3c4222.png", + "e53a97efea9c474cb6cfd0f6f8776134.png", + "e55d2628d2cf4b72b1a52b8bb3feaf3e.png", + "e57c4bd2823c4c068a8b72a0d3157a26.png", + "e59972d445a043cfa866b796c2ac7b1e.png", + "e5d79ed8ac484de4a1bed74d4c016217.png", + "e5def9d9c69945e18abd3627ee1902d2.png", + "e626c6cffede4b52b89ff583c2eebce6.png", + "e65e99a09dc94ff994cf37e0afdadcea.png", + "e6a6807907c741a7b806d1ea1471f49d.png", + "e6c7e90a27474e6a8b16ef8c48dcc3b1.png", + "e6e2518bdefe48f1a5f6deb2d2cd94c1.png", + "e6e9fc9c8fdc4d8e923a62d0ad80a22d.png", + "e70f54b08fdf4eda8564cb8eedb61de7.png", + "e71766df67ca4d98b939e7206faf58ec.png", + "e74a0a71740f48f0945ca91f3a019ece.png", + "e7647f83f96646c39dfd3d867a4fb811.png", + "e777a78d31e34699b678bc93bcc5844d.png", + "e79f8d81a4e14017aab068ffdd412267.png", + "e7bb43b176d14466b973dc95b634084e.png", + "e7bf18f50233433893ae6da1c71b0314.png", + "e7dff6218c0345a08771015c2927a183.png", + "e7f02016d51d4c8496f4b6123d8d8794.png", + "e846258250d54298b14e320f35bd3dce.png", + "e862b9e0928649e3ae6a0f953e8d510a.png", + "e86a1144f04c45e3b8cb6abfeefd0470.png", + "e88043ccd32f409b9c048d39cec28668.png", + "e890c3789e314511b8f748aae4b9f45f.png", + "e8cc2bb17f63471e9311013f8b00e581.png", + "e8ce2e4c67044b30bf14671eddc8b509.png", + "e8d5a629fdb4433d8f2de0f2b6a99ce8.png", + "e8e2216ce0ef44aba411a31ea76a3eb1.png", + "e90313607f45495086fc243369981e1c.png", + "e90abc20a3604d5da96df8e30dc74bbc.png", + "e93e688c91da47de808797912ca3c7d8.png", + "e992cd8d9a57419792f1d98514081524.png", + "e99a6c7ff321461b891ec8c067d32211.png", + "e9ba23f0eb424da38b1c788c3a6929f4.png", + "e9c7c20a2a1a4b75b9929afe6b2839a4.png", + "e9ebef74f8d04a0c9ec77501e263fec0.png", + "e9f46a5c1f7d46e1aebcecfcc622a2cb.png", + "ea052a8b8b664135a890c5d6058cc68c.png", + "ea0b6052092f4286bfc72a0553079fef.png", + "ea3900971d544fdba59a7596f620d6fd.png", + "ea41dd2c955344d799c27c05eb3f4ce7.png", + "ea45a0b3ecd644dfb6635f5b0af437cd.png", + "ea6a40d52c574af7a8d8fee9a0243e54.png", + "ea74dcc82a1045348f23db4610e270e0.png", + "ea90c146c4cc48be949b98a95ffe5cb1.png", + "ea92ece5f2474757b38f11effa8ff4d5.png", + "eaad445d314a40c38c3c4cf0777647f5.png", + "eab80a804b53411aa0d63e756712c3c0.png", + "eb01c303f19c4d57836ad735709a3245.png", + "eb14b52e1f3d44d9acad1869b1e4123b.png", + "eb252c5784d94d9cb38e37146658b24c.png", + "eb25730ed9914e0eacb4feee9b5bf4d9.png", + "eb3883e8af33434b8a1dc018c106d17d.png", + "eb3cf2dad85a4c77b57ead6757a7faa8.png", + "eb3ed3dee2e5452eb7a82443b739ddd1.png", + "eb4178ef8f7f42e2b3d934deeff14361.png", + "eb9e62dfb0e64c10b4757cadd5b1d54d.png", + "ebeb054afc814e1c94d3c2f4896c9ce4.png", + "ec045cc97fea4c78ab49daa9aff97964.png", + "ec1556742ae54c9a90b9491250956217.png", + "ec178c9b844b404db7443708d1742126.png", + "ec314e4f10b74d5bbc91f072153d6344.png", + "ec336b157abd47408bf7d2a3d953716a.png", + "ec35231d39414cb099ec919dec6d0688.png", + "ec55c772a5e8417ea1b745c338aaa632.png", + "ec59c17577be48148e993251e372dcda.png", + "ec68d6d327854de3bba5879d594f5650.png", + "ec6b33791e4a47a5a251f796f53c04da.png", + "ec842d0dcacf4de58ef561b72a554a33.png", + "ec8570568443416cb71b2ba6791f4630.png", + "ec91a662b43a43b7ae9d3ed869e65a48.png", + "ed10f5a868d84f4b89dcc69d80a228f2.png", + "ed1fcbecc704438e9aa85ea1a9c3882b.png", + "ed35214b0a694e4384a0bd23011e9a04.png", + "eda6de4c42484e1ca8e7fce32689e45a.png", + "edf8d0d370a64b299cc8a5a5e6ca2195.png", + "ee146f03d46e410ab2e12c14b0df9fa9.png", + "ee20b17173bd42a19b88923f3c8e76ba.png", + "ee3a09e850a044d6b11576e6b7d1ad7d.png", + "ee4b2defcf94450889d477ca43341d3f.png", + "ee4e32f5ad13437791c3fe398adb1dad.png", + "ee647b51213143988b5cfeee75eb1a66.png", + "ee8aba22e29744d08982eb58d6f18450.png", + "eea2b9ac6c8e4542849d20adbb38405c.png", + "eeadbac0e81f47fd84aacc0707cfd3bc.png", + "eec614d69c9f4ac2a7a0e605e5475b38.png", + "eed421a36ada4b69b86b4ff0c6cbadbc.png", + "eedcb1410e514e6db23eab2d0d1895b4.png", + "eee9faa7083641e181975e19526e1b12.png", + "ef19742e6e8f4f5dad75c3418c079a96.png", + "ef59fc43ca1a4e909407f2aae41b61af.png", + "ef5ff2873ee040db9ef4f7b6cef79d7f.png", + "ef7ddc48972e42fb978c8cafcea6254d.png", + "efa034229212464f8b3dde9e54f85f73.png", + "efdbc33f992c408c99ffeab03e7b13cb.png", + "efe2aefbd96a4e0f88497fe81bee4934.png", + "f00368c18c184223a231c9ba5c29add2.png", + "f029b941cedd415e9e43fbcf1aefc764.png", + "f02b8ec20afb4f8d88c3c2071e1b99e9.png", + "f03842608d85473eb6559c8a63a25aa3.png", + "f048bcfd4a0b48898ce977435eec6810.png", + "f06bf4554ced4b199d0d32fa647f87de.png", + "f091a52e8a4048a1b05c169e4bd2e784.png", + "f094e007868c46e8a08f98d587aa60a8.png", + "f0a4958ba1f24828a46b2792dff7cbda.png", + "f0ac860a08db4b57967180b060fc968f.png", + "f0cf6e9d52a7404cb15d740abf6e5694.png", + "f10e9cdb8d1f4461a562c30f9f7bdf95.png", + "f128a01837984a6883b10ad40e80df7a.png", + "f14a511a9de445f7b0ad46a2580fb5bf.png", + "f1522b6151c14396a7d467933cde12b5.png", + "f167f3567b1f4537a67e9edb92a9c44e.png", + "f18c1050c45748b09c95df6a4dea5e9a.png", + "f195942944ff4f3da05d8512b8495bce.png", + "f1a665ed634b4ae08415f99260b291b4.png", + "f1b4f9a1534e4978b383ad3536b6c279.png", + "f1c5421d7dc34103a85d5e4dff13ac0a.png", + "f1d385769c34474680a792b4366e6f2e.png", + "f1e8ac16a12d496d95e0ea15b3ebb19b.png", + "f1eae3b2e44b44949db7dd0f69ca5882.png", + "f2297b75afb54c5e8dabbbcde1dab021.png", + "f233cedf33f544fbb399997c883f0dfe.png", + "f2456d3eb72b4ec4b4a7b7589bebeb58.png", + "f293e66189634338a8abe2a5c239f787.png", + "f2b0f303d4e44560b9a73db78c010b37.png", + "f2deb5bb2ccb483092a6e3f024e2c61d.png", + "f339f324983846bdb0a55e0184860781.png", + "f33c81b993d44919a54c19de093cc25b.png", + "f3476b84210c4f77b9d0b68ce3063c6c.png", + "f355aa8f4dd74982b2ba424e58b23636.png", + "f35bc82d60ba4cc1b3af72a38e2c0076.png", + "f36071612d944e92a14e653b47f0f817.png", + "f37e74b8b3d247b0bb06f28934539cf2.png", + "f38b08e919a34f30b875fe77c24cb3f1.png", + "f39f999138c749ebaaa84c673c3299b2.png", + "f3a3077ca3df4fd9871bd19c870d718f.png", + "f3dab084041740df8a6dd8d4dd9dd7c7.png", + "f3dc991fdad04ac08a37e2621f9858dc.png", + "f3e5c338ae5040afa80ac0e1c445fce7.png", + "f3e7286d49a242d993f337582a943671.png", + "f454e6b5d5f746aa9262cef1b0b14a29.png", + "f45ea3d796e4463fbcde1c523acd58b9.png", + "f4a462ba2b2049b0985e144adb9e3ed3.png", + "f4ffc9474a6a465eb5e0c68933860d3e.png", + "f5282ac338cc4a8ca7f5138868f5e9d0.png", + "f529cad0fd184fae820d8d7514e047a4.png", + "f535b4caa5824753b1ffebdb1bf4273f.png", + "f53c989397e544a9b2c7a88030e370e5.png", + "f575e7ae7bf8466299dfd0873639a22e.png", + "f57ef18eb5c24ea39a0b15e8ab18b9ad.png", + "f58c76d0eae44437b87cf78e29ab27ed.png", + "f593c79a4aa540f394fa8e0b46cb95e4.png", + "f5a2ea3321a2436bbd0f066b5f448621.png", + "f5a9f0f8d27b4958a2c2460c5099deb6.png", + "f5c2d67f070943a0af8700e0f1ed3ade.png", + "f5cae688a78c4afa89ab489c11cfa5be.png", + "f604238da5b04d38974f8019a2741732.png", + "f621b718638e4333a93a27fdbb69bb21.png", + "f632b38e93c14602882916a8177c5c5a.png", + "f6493e6673614f88aaf2c3998b02559a.png", + "f65744a6472445978ec1c6d95298611f.png", + "f65c84bd7e1348fa875a985f88f20913.png", + "f693f3fc0d3a412b95312a8f482bcd80.png", + "f69b5862087d4403858dfaba17cce1e2.png", + "f6b42201f4914461ad876a7dc8796b37.png", + "f6bb647b467d46cf824db952d4538094.png", + "f6c15060145f496391a635617254b0d6.png", + "f6d43bae34c14ed08313b1f13c7be603.png", + "f718270193ed42c78848919f42bdbbdb.png", + "f720ce3ff8434785a8c10e481d8b8aa7.png", + "f723b157fe134d9ab522d57537808b87.png", + "f73699f7afb44a7b99cf553e36872a16.png", + "f758fd53c2c94507a8e076027834c2d6.png", + "f75f125f3d2f42269c53321504f04010.png", + "f7612e034ccb412a874025b34bd8f326.png", + "f789dce6a3444dc5b662d340b8289cae.png", + "f78a6130223e4c31a17bd367f88699ea.png", + "f78fd0286e6a40f5a81b491d656e6b68.png", + "f791e08fab004e2cb84f29ac73af323d.png", + "f7d89ff9b3af4e47b096115413e5026a.png", + "f7e63a3d46a74663af757e006e6822f6.png", + "f82b8d8705ce43fb930b2ffbf7264b09.png", + "f84acfce4e26495c94f4f0564ade5d74.png", + "f84ef233f583473dae0dc173f2cd5184.png", + "f868942bc5074350a1dba1a8a5e76119.png", + "f87ca9adf1654bf38f2b119e6a41861f.png", + "f8c42c087c8a41c68fcb53d7357bbac6.png", + "f8e5da30cb0c4ea58e34b56a2cbb7ea5.png", + "f8e74b05d4e94056885042fcd624a335.png", + "f915ff1e3d64465e86216352a781f8e4.png", + "f91df849cea343e59d398760258d270b.png", + "f91e0d7d8cb942a69cdf036a92b3aa57.png", + "f93b4c74608546528bc4554179854b35.png", + "f947d46c8c20438e903904cfcf23256b.png", + "f94a402fb18d47a7bae0248d215fec94.png", + "f94bbd90e490446a9c41175fdb79587f.png", + "f951b8ca474a4f0d9802bd58cede4b40.png", + "f975bda6b2264f898cffa80057bef55e.png", + "f983d7ffd2b241c08ddb84b647a76d6d.png", + "f9ad37ed90a344ddb2d56de17ddf0f25.png", + "f9d838bc368c4cda99bbd7ec675e47c5.png", + "f9dea73090ba4f5eb84c1a026aa711f5.png", + "fa4be301f44044db9ae5ee534caed45e.png", + "fa5d663bb8db452fb0967b101764dc8c.png", + "fa7569de7ea741e9a384f8a2d4dd9fb9.png", + "faa2b17311d14e26a0a28e5822cfb2c7.png", + "faba9f2e236c4c698d656adbbfcb80cc.png", + "fabf312f0337475399727437518b991e.png", + "fac769879f5247869096109dcabc3d54.png", + "faf0f298acef498fa7c54576547b25ab.png", + "fb26233eba1041be96a6a35c41a6c99f.png", + "fb338f5537fd4cc4a3917ffdb58acacd.png", + "fb4fcae400c24d25a24b176f7d7aa0a6.png", + "fb68a11511c04ae08a21565eebda3c83.png", + "fb7fbd7830e24bd4b4cc880c09b91d14.png", + "fb869fa1043b4b749375b1b7c2dfbb31.png", + "fb99b24e9a504046bc8accc23e6a8e07.png", + "fba0696065434b2e98cbbe32809ee3db.png", + "fbdeb5b26f6445d783a72789bed79cf7.png", + "fbc83fc0a30e42738958464348833b44.png", + "fbe6e12e2e5a4219856acc91501be386.png", + "fbfad3b4fa134730a9548c53463b6782.png", + "fc3673cd42f7494cad7caad177aded0c.png", + "fbebbc1cdd044aeb9e8a34763b1983ba.png", + "fc3b0372b3d745a2888a4db8886b41a3.png", + "fc3d14b0727346a7981e66cb0163e0ef.png", + "fc47bf28992f4cbbb114d2d0fb6d51d9.png", + "fc48a297f11249e790c73fbb61acec33.png", + "fc593a28b60c49fc955ea60330bdab05.png", + "fc60c2c490844bf7bbed4e1298767282.png", + "fc6e085b0ae14e56a400a22a9c69a161.png", + "fc8abeb5250d4cafaea153f85a85f43e.png", + "fc93a9d72b6a43ae8cbf8ce51a8a2b09.png", + "fc99eec78c1440a1abc2065531fcbeed.png", + "fcaacc33e0ff4386bab80522803010b7.png", + "fcd4908ab6c246f193c46417764614b7.png", + "fcdf995332f141efababff9846d8e86e.png", + "fd02154daf4c49b58cb6e6fc798d1ecb.png", + "fd04312e1fa94224a444ab564445c598.png", + "fd08f34106db4dbf916c6d6a841dfe69.png", + "fd0eefc5d4d94603b35521d230de7069.png", + "fd17bc0b2baf4188aba6fae6efba71ac.png", + "fd2072a12e144464808037baa08d6d00.png", + "fd452468999e4d7ba0d4c2742b5d9e72.png", + "fd65515c77a542a6a549dd8d566f4785.png", + "fd6d8a219cbf425898c050bf8245f40b.png", + "fd81697f36fa4c35afdd051fd85d372c.png", + "fd97480a9d614da996238d801c4896e2.png", + "fd99752207e6439fae4467b83dc6dd60.png", + "fdba3df92dc64cf3966210d1e03c645a.png", + "fdf1813abf4445e3883ebff632526e93.png", + "fe1ce1daee31452eaf5fb7667d57c1ff.png", + "fe34aa9fccbe4e3c95d5e6ff25c948bb.png", + "fe7bd9239c05436183afec9a01e69118.png", + "fea4e3d81bef45e3a5be91d8d69fcfee.png", + "fef6b101f6d649f8843c2b1529ea1e24.png", + "fef7e95ccdd64d56870a844da3e5e93a.png", + "fefad2180b80419c9124971a3537d31c.png", + "ff45cc770b214210b6b89b87ea3c47e1.png", + "ff78d9c3901a43c0a9f629d4aa3026ac.png", + "ff4f7c5835de4f088d33f4b08629d406.png", + "ff801f2b68cd4c5082357960147fd7e4.png", + "ffa1f5ee7536420f9641f50c9cb75922.png", + "ffb0062ade8b4f68a952d5ed00a709f4.png", + "ffb99a82f41946bfb94a53d929f6ead8.png", + "ffd079e7fd454a1284c6bbd5a13addf8.png", + "ffe5003845cb457282102803a2cd574a.png", + "fffc709fa590478f9e76a6e966c2b47e.png", + "test-upload-antigravity.png" +] \ No newline at end of file diff --git a/dental-server/.dockerignore b/dental-server/.dockerignore new file mode 100644 index 0000000..768fb39 --- /dev/null +++ b/dental-server/.dockerignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +.env +dental_images.db +uploads +processed diff --git a/dental-server/backfill-thumbs.js b/dental-server/backfill-thumbs.js new file mode 100644 index 0000000..98edc87 --- /dev/null +++ b/dental-server/backfill-thumbs.js @@ -0,0 +1,89 @@ +const db = require('./database'); +const storage = require('./storage'); +const sharp = require('sharp'); + +// Limit of concurrency +const CONCURRENCY = 15; + +async function runBackfill() { + console.log('🚀 Iniciando backfill de thumbnails concorrido...'); + + // 1. Inicializar banco de dados + await db.initDatabase(); + console.log('✅ Banco de dados inicializado.'); + + // 2. Carregar configurações do Wasabi a partir do banco + await storage.loadConfigFromDb(); + console.log('✅ Configuração do Wasabi carregada.'); + + // 3. Buscar todas as imagens que não possuem thumb_filename + const query = "SELECT id, filename, client_name, patient_name FROM images WHERE (thumb_filename IS NULL OR thumb_filename = '') AND enabled = 1"; + const images = await db.all(query); + console.log(`🔎 Encontradas ${images.length} imagens pendentes de thumbnail.`); + + let successCount = 0; + let failCount = 0; + + // Worker function to process a single image + async function processImage(img) { + try { + // a. Baixar imagem original do Wasabi + const buffer = await storage.getImageBuffer(img.filename, img.client_name, img.patient_name, false); + if (!buffer) { + throw new Error(`Não foi possível obter o buffer para ${img.filename}`); + } + + // b. Gerar thumbnail WebP + const thumbBuffer = await sharp(buffer) + .resize({ width: 400, height: 400, fit: 'inside', withoutEnlargement: true }) + .webp({ quality: 80, effort: 4 }) + .toBuffer(); + + // c. Salvar thumbnail no Wasabi + const thumbFilename = `thumb_${Date.now()}_${img.id}.webp`; + await storage.saveImage(thumbFilename, thumbBuffer, img.client_name, img.patient_name, false); + + // d. Atualizar no banco de dados + await db.run('UPDATE images SET thumb_filename = ? WHERE id = ?', [thumbFilename, img.id]); + + console.log(`✅ [ID ${img.id}] Thumbnail salvo com sucesso: ${thumbFilename} (Paciente: ${img.patient_name})`); + successCount++; + } catch (err) { + console.error(`❌ [ID ${img.id}] Erro no processamento da imagem:`, err.message); + failCount++; + } + } + + // Concurrency queue + const queue = [...images]; + const workers = []; + + async function worker() { + while (queue.length > 0) { + const img = queue.shift(); + if (!img) break; + await processImage(img); + } + } + + // Start workers + for (let i = 0; i < Math.min(CONCURRENCY, images.length); i++) { + workers.push(worker()); + } + + // Wait for all to complete + await Promise.all(workers); + + console.log('\n========================================='); + console.log('📊 Resumo do Backfill Concorrido:'); + console.log(`- Sucesso: ${successCount}`); + console.log(`- Falhas : ${failCount}`); + console.log('========================================='); + + process.exit(0); +} + +runBackfill().catch(err => { + console.error('❌ Erro fatal no script de backfill:', err); + process.exit(1); +}); diff --git a/dental-server/database.js b/dental-server/database.js index d0e3f83..9a4235c 100644 --- a/dental-server/database.js +++ b/dental-server/database.js @@ -45,6 +45,14 @@ async function initSQLite() { ); `); + sqliteDb.exec(` + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + `); + console.log('✅ Tabela settings SQLite criada/verificada'); + sqliteDb.exec(` CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -63,6 +71,7 @@ async function initSQLite() { flip_vertical INTEGER DEFAULT 0, doctor TEXT, remark TEXT, + thumb_filename TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); @@ -95,6 +104,9 @@ async function initSQLite() { try { sqliteDb.exec("ALTER TABLE images ADD COLUMN remark TEXT"); } catch (e) {} + try { + sqliteDb.exec("ALTER TABLE images ADD COLUMN thumb_filename TEXT"); + } catch (e) {} console.log('✅ Tabelas SQLite users e images criadas/verificadas'); @@ -187,6 +199,15 @@ async function createTables() { created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); + + // Tabela de configurações + await client.query(` + CREATE TABLE IF NOT EXISTS settings ( + key VARCHAR(100) PRIMARY KEY, + value TEXT NOT NULL + ) + `); + console.log('✅ Tabela settings PostgreSQL criada/verificada'); console.log('✅ Tabela users criada/verificada'); // Tabela de imagens @@ -208,6 +229,7 @@ async function createTables() { flip_vertical SMALLINT DEFAULT 0, doctor VARCHAR(255) NULL, remark TEXT NULL, + thumb_filename VARCHAR(500) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); @@ -262,6 +284,35 @@ async function createTables() { await client.query(`ALTER TABLE images ADD COLUMN remark TEXT`); } catch (e) { /* já existe */ } + try { + await client.query(`ALTER TABLE images ADD COLUMN thumb_filename VARCHAR(500)`); + console.log('✅ Coluna thumb_filename adicionada'); + } catch (e) { /* já existe */ } + + // Inserir usuários padrões se a tabela estiver vazia + try { + const userCountRes = await client.query('SELECT COUNT(*) as count FROM users'); + const userCount = parseInt(userCountRes.rows[0].count, 10); + if (userCount === 0) { + console.log('⚠️ Nenhum usuário encontrado no PostgreSQL. Criando usuários padrões...'); + const bcrypt = require('bcrypt'); + const hash1 = await bcrypt.hash('Rc362514', 10); + const hash2 = await bcrypt.hash('admin1234', 10); + + await client.query( + `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, + ['rcesar', 'rcesar@rcesar.com', hash1] + ); + await client.query( + `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, + ['admin', 'admin@dental.local', hash2] + ); + console.log('✅ Usuários padrões (rcesar e admin) criados com sucesso no PostgreSQL!'); + } + } catch (e) { + console.error('⚠️ Erro ao inicializar usuários padrões no PostgreSQL:', e.message); + } + } catch (error) { console.error('Erro ao criar tabelas:', error); } finally { @@ -341,8 +392,10 @@ function run(query, params = []) { try { let pgQuery = convertSqlForPg(query); - // Auto-append RETURNING id for INSERT if not present - if (pgQuery.trim().toUpperCase().startsWith('INSERT') && !pgQuery.toUpperCase().includes('RETURNING ID')) { + // Auto-append RETURNING id for INSERT if not present (except for settings table) + if (pgQuery.trim().toUpperCase().startsWith('INSERT') && + !pgQuery.toUpperCase().includes('RETURNING') && + !pgQuery.toUpperCase().includes('SETTINGS')) { pgQuery += ' RETURNING id'; } diff --git a/dental-server/package.json b/dental-server/package.json index 2dd6c07..921def9 100644 --- a/dental-server/package.json +++ b/dental-server/package.json @@ -25,7 +25,9 @@ "bcrypt": "^5.1.1", "jsonwebtoken": "^9.0.2", "cookie-parser": "^1.4.6", - "express-session": "^1.17.3" + "express-session": "^1.17.3", + "@aws-sdk/client-s3": "^3.500.0", + "@aws-sdk/lib-storage": "^3.500.0" }, "engines": { "node": ">=16.0.0", diff --git a/dental-server/plugins/SmartVision/backend/Optimizer.js b/dental-server/plugins/SmartVision/backend/Optimizer.js new file mode 100644 index 0000000..95491f9 --- /dev/null +++ b/dental-server/plugins/SmartVision/backend/Optimizer.js @@ -0,0 +1,60 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ImageOptimizer = void 0; +const sharp_1 = __importDefault(require("sharp")); +class ImageOptimizer { + /** + * Optimizes an image: converts to WebP, strips metadata, + * and applies a 1:1 Smart Crop if needed. + */ + static async optimize(buffer, category = 'media', options = {}) { + try { + let pipeline = (0, sharp_1.default)(buffer); + const metadata = await pipeline.metadata(); + if (!metadata.format) + return { buffer, mimetype: 'application/octet-stream' }; + // 1. Identify Target Size + // Default sizes based on category + let targetSize = options.size || 1024; + if (category === 'partners' || category === 'users') + targetSize = 400; + if (category === 'benefits') + targetSize = 800; + // 2. Smart Crop 1:1 if aspect=1:1 is requested or implied by category + const shouldCrop = options.aspect === '1:1' || ['partners', 'users'].includes(category); + if (shouldCrop) { + pipeline = pipeline.resize({ + width: targetSize, + height: targetSize, + fit: sharp_1.default.fit.cover, + position: sharp_1.default.strategy.entropy // Smart focal point detection + }); + } + else { + // Resize without cropping if it's too large + pipeline = pipeline.resize({ + width: targetSize, + withoutEnlargement: true, + fit: sharp_1.default.fit.inside + }); + } + // 3. Convert to WebP & Strip Metadata + const optimizedBuffer = await pipeline + .webp({ quality: 85, effort: 4 }) + .toBuffer(); + return { + buffer: optimizedBuffer, + mimetype: 'image/webp' + }; + } + catch (err) { + console.error('[SmartVision] Optimization failed:', err); + return { buffer, mimetype: 'image/jpeg' }; // Fallback + } + } +} +exports.ImageOptimizer = ImageOptimizer; +//# sourceMappingURL=Optimizer.js.map \ No newline at end of file diff --git a/dental-server/plugins/SmartVision/backend/Optimizer.js.map b/dental-server/plugins/SmartVision/backend/Optimizer.js.map new file mode 100644 index 0000000..f613bb5 --- /dev/null +++ b/dental-server/plugins/SmartVision/backend/Optimizer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Optimizer.js","sourceRoot":"","sources":["Optimizer.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA0B;AAO1B,MAAa,cAAc;IACvB;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,QAAQ,CACjB,MAAc,EACd,WAAmB,OAAO,EAC1B,UAA8C,EAAE;QAEhD,IAAI,CAAC;YACD,IAAI,QAAQ,GAAG,IAAA,eAAK,EAAC,MAAM,CAAC,CAAC;YAC7B,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;YAE3C,IAAI,CAAC,QAAQ,CAAC,MAAM;gBAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,0BAA0B,EAAE,CAAC;YAE9E,0BAA0B;YAC1B,kCAAkC;YAClC,IAAI,UAAU,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC;YACtC,IAAI,QAAQ,KAAK,UAAU,IAAI,QAAQ,KAAK,OAAO;gBAAE,UAAU,GAAG,GAAG,CAAC;YACtE,IAAI,QAAQ,KAAK,UAAU;gBAAE,UAAU,GAAG,GAAG,CAAC;YAE9C,sEAAsE;YACtE,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAExF,IAAI,UAAU,EAAE,CAAC;gBACb,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC;oBACvB,KAAK,EAAE,UAAU;oBACjB,MAAM,EAAE,UAAU;oBAClB,GAAG,EAAE,eAAK,CAAC,GAAG,CAAC,KAAK;oBACpB,QAAQ,EAAE,eAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,8BAA8B;iBAClE,CAAC,CAAC;YACP,CAAC;iBAAM,CAAC;gBACJ,4CAA4C;gBAC5C,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC;oBACvB,KAAK,EAAE,UAAU;oBACjB,kBAAkB,EAAE,IAAI;oBACxB,GAAG,EAAE,eAAK,CAAC,GAAG,CAAC,MAAM;iBACxB,CAAC,CAAC;YACP,CAAC;YAED,sCAAsC;YACtC,MAAM,eAAe,GAAG,MAAM,QAAQ;iBACjC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;iBAChC,QAAQ,EAAE,CAAC;YAEhB,OAAO;gBACH,MAAM,EAAE,eAAe;gBACvB,QAAQ,EAAE,YAAY;aACzB,CAAC;QACN,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;YACzD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC,WAAW;QAC1D,CAAC;IACL,CAAC;CACJ;AAvDD,wCAuDC"} \ No newline at end of file diff --git a/dental-server/plugins/SmartVision/backend/Optimizer.ts b/dental-server/plugins/SmartVision/backend/Optimizer.ts new file mode 100644 index 0000000..f072bf1 --- /dev/null +++ b/dental-server/plugins/SmartVision/backend/Optimizer.ts @@ -0,0 +1,63 @@ +import sharp from 'sharp'; + +export interface OptimizationResult { + buffer: Buffer; + mimetype: string; +} + +export class ImageOptimizer { + /** + * Optimizes an image: converts to WebP, strips metadata, + * and applies a 1:1 Smart Crop if needed. + */ + static async optimize( + buffer: Buffer, + category: string = 'media', + options: { aspect?: string; size?: number } = {} + ): Promise { + try { + let pipeline = sharp(buffer); + const metadata = await pipeline.metadata(); + + if (!metadata.format) return { buffer, mimetype: 'application/octet-stream' }; + + // 1. Identify Target Size + // Default sizes based on category + let targetSize = options.size || 1024; + if (category === 'partners' || category === 'users') targetSize = 400; + if (category === 'benefits') targetSize = 800; + + // 2. Smart Crop 1:1 if aspect=1:1 is requested or implied by category + const shouldCrop = options.aspect === '1:1' || ['partners', 'users'].includes(category); + + if (shouldCrop) { + pipeline = pipeline.resize({ + width: targetSize, + height: targetSize, + fit: sharp.fit.cover, + position: sharp.strategy.entropy // Smart focal point detection + }); + } else { + // Resize without cropping if it's too large + pipeline = pipeline.resize({ + width: targetSize, + withoutEnlargement: true, + fit: sharp.fit.inside + }); + } + + // 3. Convert to WebP & Strip Metadata + const optimizedBuffer = await pipeline + .webp({ quality: 85, effort: 4 }) + .toBuffer(); + + return { + buffer: optimizedBuffer, + mimetype: 'image/webp' + }; + } catch (err) { + console.error('[SmartVision] Optimization failed:', err); + return { buffer, mimetype: 'image/jpeg' }; // Fallback + } + } +} diff --git a/dental-server/plugins/SmartVision/index.js b/dental-server/plugins/SmartVision/index.js new file mode 100644 index 0000000..26431e1 --- /dev/null +++ b/dental-server/plugins/SmartVision/index.js @@ -0,0 +1,33 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const Optimizer_1 = require("./backend/Optimizer"); +const manifest_json_1 = __importDefault(require("./manifest.json")); +const plugin = { + manifest: manifest_json_1.default, + async activate(ctx) { + // Register for the upload:transform hook + ctx.hooks.register('upload:transform', async (payload) => { + const { buffer, mimetype, category } = payload; + // Only process common image formats + const imageTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/avif']; + if (!imageTypes.includes(mimetype)) { + return null; // Skip non-images + } + ctx.logger.info(`[SmartVision] Optimizing image for category: ${category}`); + const result = await Optimizer_1.ImageOptimizer.optimize(buffer, category); + return { + buffer: result.buffer, + mimetype: result.mimetype + }; + }); + ctx.logger.info('SmartVision Optimizer activated'); + }, + async deactivate(ctx) { + ctx.logger.info('SmartVision Optimizer deactivated'); + } +}; +exports.default = plugin; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dental-server/plugins/SmartVision/index.js.map b/dental-server/plugins/SmartVision/index.js.map new file mode 100644 index 0000000..a973c6e --- /dev/null +++ b/dental-server/plugins/SmartVision/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;AACA,mDAAqD;AACrD,oEAAuC;AAEvC,MAAM,MAAM,GAAmB;IAC3B,QAAQ,EAAE,uBAAe;IACzB,KAAK,CAAC,QAAQ,CAAC,GAAkB;QAC7B,yCAAyC;QACzC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,kBAAkB,EAAE,KAAK,EAAE,OAAY,EAAE,EAAE;YAC1D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;YAE/C,oCAAoC;YACpC,MAAM,UAAU,GAAG,CAAC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC;YAC3E,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,OAAO,IAAI,CAAC,CAAC,kBAAkB;YACnC,CAAC;YAED,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,gDAAgD,QAAQ,EAAE,CAAC,CAAC;YAE5E,MAAM,MAAM,GAAG,MAAM,0BAAc,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAE/D,OAAO;gBACH,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;aAC5B,CAAC;QACN,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IACvD,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,GAAkB;QAC/B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IACzD,CAAC;CACJ,CAAC;AAEF,kBAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/dental-server/plugins/SmartVision/index.ts b/dental-server/plugins/SmartVision/index.ts new file mode 100644 index 0000000..12f97c5 --- /dev/null +++ b/dental-server/plugins/SmartVision/index.ts @@ -0,0 +1,35 @@ +import { PluginInstance, PluginContext } from '../../backend/src/core/types'; +import { ImageOptimizer } from './backend/Optimizer'; +import manifest from './manifest.json'; + +const plugin: PluginInstance = { + manifest: manifest as any, + async activate(ctx: PluginContext): Promise { + // Register for the upload:transform hook + ctx.hooks.register('upload:transform', async (payload: any) => { + const { buffer, mimetype, category } = payload; + + // Only process common image formats + const imageTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/avif']; + if (!imageTypes.includes(mimetype)) { + return null; // Skip non-images + } + + ctx.logger.info(`[SmartVision] Optimizing image for category: ${category}`); + + const result = await ImageOptimizer.optimize(buffer, category); + + return { + buffer: result.buffer, + mimetype: result.mimetype + }; + }); + + ctx.logger.info('SmartVision Optimizer activated'); + }, + async deactivate(ctx: PluginContext): Promise { + ctx.logger.info('SmartVision Optimizer deactivated'); + } +}; + +export default plugin; diff --git a/dental-server/plugins/SmartVision/manifest.json b/dental-server/plugins/SmartVision/manifest.json new file mode 100644 index 0000000..267d598 --- /dev/null +++ b/dental-server/plugins/SmartVision/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "SmartVision", + "displayName": "SmartVision Optimizer", + "version": "1.0.0", + "description": "Otimização inteligente de imagens com WebP e Smart Crop 1:1.", + "author": "Clube67 Team", + "category": "integration", + "enabled": true, + "canDisable": true, + "dependencies": [ + "uploads" + ], + "hooks": { + "subscribes": [ + "upload:transform" + ], + "emits": [] + } +} \ No newline at end of file diff --git a/dental-server/plugins/UnifiedStorageProvider/index.js b/dental-server/plugins/UnifiedStorageProvider/index.js new file mode 100644 index 0000000..3d60088 --- /dev/null +++ b/dental-server/plugins/UnifiedStorageProvider/index.js @@ -0,0 +1,388 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const client_s3_1 = require("@aws-sdk/client-s3"); +const lib_storage_1 = require("@aws-sdk/lib-storage"); +let storageProvider; +if (process.env.NODE_ENV === 'production') { + storageProvider = require('../../dist/core/StorageProvider').storageProvider; +} +else { + storageProvider = require('../../backend/src/core/StorageProvider').storageProvider; +} +const sharp_1 = __importDefault(require("sharp")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const VALID_CATEGORIES = ['documents', 'exports', 'posts', 'cards', 'partners', 'whatsapp', 'media']; +// Mapa de extensão → mimetype para o proxy +const MIME_MAP = { + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', + gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', + pdf: 'application/pdf', mp4: 'video/mp4', ogg: 'audio/ogg', + mp3: 'audio/mpeg', doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', +}; +class UnifiedStorageProviderPlugin { + constructor() { + this.s3 = null; + this.bucket = ''; + this.region = 'us-east-1'; + this.localFallbackDir = path_1.default.resolve(process.cwd(), 'storage', 'local_fallback'); + this.routesRegistered = false; + } + async activate(ctx) { + this.ctx = ctx; + const cfg = ctx.config.get('UnifiedStorageProvider') || {}; + if (!fs_1.default.existsSync(this.localFallbackDir)) { + fs_1.default.mkdirSync(this.localFallbackDir, { recursive: true }); + } + const accessKey = cfg.wasabiAccessKey || process.env.WASABI_ACCESS_KEY; + const secretKey = cfg.wasabiSecretKey || process.env.WASABI_SECRET_KEY; + this.region = (cfg.wasabiRegion || process.env.WASABI_REGION || 'us-east-1'); + this.bucket = (cfg.wasabiBucket || process.env.WASABI_BUCKET || ''); + const rawEndpoint = (cfg.wasabiEndpoint || process.env.WASABI_ENDPOINT || 's3.wasabisys.com'); + const endpoint = rawEndpoint.startsWith('http') ? rawEndpoint : `https://${rawEndpoint}`; + ctx.logger.info({ cfgKeys: Object.keys(cfg), hasKey: !!accessKey, hasBucket: !!this.bucket }, '[UnifiedStorage] activate debug'); + if (!accessKey || !secretKey || !this.bucket) { + ctx.logger.warn('UnifiedStorageProvider: credenciais ou bucket não configurados — configure via admin'); + } + else { + this.s3 = new client_s3_1.S3Client({ + region: this.region, + endpoint, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + storageProvider.register(this); + ctx.logger.info(`UnifiedStorageProvider ativo — bucket: ${this.bucket} endpoint: ${endpoint}`); + this.startSyncWorker(); + } + if (!this.routesRegistered) { + // ── Proxy: serve arquivos do Wasabi (ou fallback local) ──────────────── + ctx.app.get('/api/storage/view/*', async (req, res) => { + const filePath = req.params[0]; + if (!filePath) + return res.status(400).send('Path missing'); + try { + // 1. Fallback local + const localPath = path_1.default.join(this.localFallbackDir, filePath); + if (fs_1.default.existsSync(localPath)) { + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'local_fallback'); + return res.send(fs_1.default.readFileSync(localPath)); + } + // 2. Wasabi + if (!this.s3 || !this.bucket) { + return res.status(503).send('Storage não inicializado'); + } + const result = await this.s3.send(new client_s3_1.GetObjectCommand({ + Bucket: this.bucket, + Key: filePath, + })); + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', result.ContentType || MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'wasabi'); + if (result.Body) { + const bytes = await result.Body.transformToByteArray(); + return res.send(Buffer.from(bytes)); + } + return res.status(404).send('Body vazio'); + } + catch { + return res.status(404).send('Arquivo não encontrado'); + } + }); + // ── POST /api/storage/wasabi/list-buckets ────────────────────────────── + // Lista buckets usando credenciais do body (sem exigir S3 pré-configurado) + ctx.app.post('/api/storage/wasabi/list-buckets', async (req, res) => { + const { accessKey, secretKey, region, endpoint } = req.body; + if (!accessKey || !secretKey) { + return res.status(400).json({ error: 'Access Key e Secret Key são obrigatórios' }); + } + try { + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + const s3t = new client_s3_1.S3Client({ + region: region || 'us-east-1', + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const data = await s3t.send(new client_s3_1.ListBucketsCommand({})); + const buckets = (data.Buckets ?? []).map(b => ({ name: b.Name, createdAt: b.CreationDate })); + return res.json({ buckets }); + } + catch (err) { + return res.status(400).json({ error: err.message }); + } + }); + // ── POST /api/storage/wasabi/create-bucket ───────────────────────────── + ctx.app.post('/api/storage/wasabi/create-bucket', async (req, res) => { + const { accessKey, secretKey, region, endpoint, name } = req.body; + if (!accessKey || !secretKey) + return res.status(400).json({ error: 'Credenciais obrigatórias' }); + if (!name) + return res.status(400).json({ error: 'Campo "name" obrigatório' }); + const safeName = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'); + const effectiveRegion = region || 'us-east-1'; + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + try { + const s3t = new client_s3_1.S3Client({ + region: effectiveRegion, + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const params = { Bucket: safeName }; + if (effectiveRegion !== 'us-east-1') { + params.CreateBucketConfiguration = { LocationConstraint: effectiveRegion }; + } + await s3t.send(new client_s3_1.CreateBucketCommand(params)); + ctx.logger.info(`[Wasabi] Bucket criado: ${safeName}`); + return res.status(201).json({ ok: true, name: safeName }); + } + catch (err) { + return res.status(500).json({ error: err.message }); + } + }); + this.routesRegistered = true; + } + setInterval(() => { storageProvider.updateHealth(); }, 30000); + } + async deactivate(ctx) { + this.s3 = null; + ctx.logger.info('UnifiedStorageProvider desativado'); + } + async checkHealth() { + return (await this.checkDetailedHealth()).healthy; + } + async checkDetailedHealth() { + if (!this.s3 || !this.bucket) { + return { + healthy: false, + configured: false, + reason: 'not_configured', + message: 'Credenciais Wasabi não configuradas. Acesse Admin > Plugins > UnifiedStorageProvider.', + }; + } + try { + await this.s3.send(new client_s3_1.HeadBucketCommand({ Bucket: this.bucket })); + return { healthy: true, configured: true, reason: 'ok', message: `Wasabi OK — bucket: ${this.bucket}` }; + } + catch (err) { + const code = err?.Code || err?.code || err?.name || ''; + const httpStatus = err?.$metadata?.httpStatusCode ?? 0; + if (code === 'InvalidAccessKeyId' || code === 'SignatureDoesNotMatch') { + return { + healthy: false, configured: true, reason: 'auth_error', + message: 'Chaves de acesso inválidas. Verifique Access Key e Secret Key no painel do Wasabi.', + }; + } + if (httpStatus === 403 || code === 'AccessDenied') { + // 403 pode ser pagamento atrasado ou permissão negada + return { + healthy: false, configured: true, reason: 'billing_error', + message: 'Acesso negado ao Wasabi (HTTP 403). Verifique se a conta está ativa e o pagamento em dia.', + }; + } + if (httpStatus === 404 || code === 'NoSuchBucket') { + return { + healthy: false, configured: true, reason: 'bucket_not_found', + message: `Bucket "${this.bucket}" não encontrado. Verifique o nome do bucket no painel Wasabi.`, + }; + } + if (code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'NetworkingError') { + return { + healthy: false, configured: true, reason: 'network_error', + message: 'Sem conectividade com o Wasabi. Verifique a rede e o endpoint configurado.', + }; + } + return { + healthy: false, configured: true, reason: 'unknown', + message: `Erro ao verificar Wasabi: ${err?.message ?? code}`, + }; + } + } + async deletePrefix(prefix) { + let deletedCount = 0; + // 1. Limpa do Wasabi + if (this.s3 && this.bucket) { + try { + let isTruncated = true; + let continuationToken; + while (isTruncated) { + const listParams = { + Bucket: this.bucket, + Prefix: prefix, + MaxKeys: 1000, + }; + if (continuationToken) { + listParams.ContinuationToken = continuationToken; + } + const listResult = await this.s3.send(new client_s3_1.ListObjectsV2Command(listParams)); + const objects = listResult.Contents ?? []; + if (objects.length > 0) { + const deleteParams = { + Bucket: this.bucket, + Delete: { + Objects: objects.map((obj) => ({ Key: obj.Key })), + Quiet: true, + }, + }; + await this.s3.send(new client_s3_1.DeleteObjectsCommand(deleteParams)); + deletedCount += objects.length; + this.ctx.logger.info(`[UnifiedStorageProvider] ${objects.length} objetos deletados do Wasabi com prefixo ${prefix}`); + } + isTruncated = listResult.IsTruncated ?? false; + continuationToken = listResult.NextContinuationToken; + } + } + catch (err) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao deletar prefixo ${prefix} do Wasabi: ${err.message}`); + } + } + // 2. Limpa do fallback local + const localPath = path_1.default.join(this.localFallbackDir, prefix); + if (fs_1.default.existsSync(localPath)) { + try { + const stat = fs_1.default.statSync(localPath); + if (stat.isDirectory()) { + fs_1.default.rmSync(localPath, { recursive: true, force: true }); + this.ctx.logger.info(`[UnifiedStorageProvider] Pasta local do fallback removida: ${localPath}`); + } + else { + fs_1.default.unlinkSync(localPath); + this.ctx.logger.info(`[UnifiedStorageProvider] Arquivo local do fallback removido: ${localPath}`); + } + } + catch (err) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao remover fallback local para prefixo ${prefix}: ${err.message}`); + } + } + return { deleted: deletedCount }; + } + async upload(payload) { + const { category, file } = payload; + if (!VALID_CATEGORIES.includes(category)) + throw new Error(`Categoria inválida: ${category}`); + let buffer = file.buffer; + let originalName = file.originalname; + let mimetype = file.mimetype; + // Otimiza imagens grandes (>150 KB) para WebP + if (mimetype.startsWith('image/') && !mimetype.includes('webp') && buffer.length > 150 * 1024) { + try { + buffer = await (0, sharp_1.default)(buffer).resize(1200, 1200, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75 }).toBuffer(); + mimetype = 'image/webp'; + originalName = originalName.replace(/\.[^.]+$/, '.webp'); + this.ctx.logger.debug(`[Optimizer] ${originalName} convertida para webp`); + } + catch (err) { + this.ctx.logger.warn(`[Optimizer] falha: ${err.message}`); + } + } + const safeBase = originalName.replace(/[^a-zA-Z0-9._-]/g, '_'); + const fileName = `${Date.now()}-${safeBase}`; + // Path no bucket: customPath (quando fornecido) > whatsapp/{t}/{i}/{file} > {cat}/{file} + const relativePath = payload.customPath + ?? ((category === 'whatsapp' && payload.usuarioSis && payload.sessionJID) + ? `whatsapp/${payload.usuarioSis}/${payload.sessionJID}/${fileName}` + : `${category}/${fileName}`); + if (!this.s3 || !this.bucket) { + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + try { + await new lib_storage_1.Upload({ + client: this.s3, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: buffer, + ContentType: mimetype, + }, + }).done(); + this.ctx.logger.debug(`[Wasabi] upload ok: ${relativePath}`); + return { provider: 'wasabi', path: relativePath }; + } + catch (err) { + this.ctx.logger.error(`[Wasabi] upload falhou (${err.message}) — salvando local`); + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + } + async getBuffer(relativePath) { + // 1. Fallback local + const localPath = path_1.default.join(this.localFallbackDir, relativePath); + if (fs_1.default.existsSync(localPath)) { + return fs_1.default.readFileSync(localPath); + } + // 2. Wasabi + if (!this.s3 || !this.bucket) + return null; + try { + const result = await this.s3.send(new client_s3_1.GetObjectCommand({ Bucket: this.bucket, Key: relativePath })); + if (!result.Body) + return null; + const bytes = await result.Body.transformToByteArray(); + return Buffer.from(bytes); + } + catch { + return null; + } + } + /** Salva buffer no diretório de fallback local, criando subpastas se necessário */ + saveToLocalFallback(relativePath, buffer, originalName, _category) { + const dest = path_1.default.join(this.localFallbackDir, relativePath); + fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true }); + fs_1.default.writeFileSync(dest, buffer); + this.ctx.logger.debug(`[Fallback] salvo local: ${dest}`); + return { provider: 'local_fallback', path: relativePath }; + } + /** Sincroniza arquivos do fallback local para o Wasabi a cada 2 minutos */ + startSyncWorker() { + setInterval(async () => { + if (!this.s3 || !this.bucket) + return; + await this.syncDir(this.localFallbackDir); + }, 120000); + } + async syncDir(dir) { + if (!fs_1.default.existsSync(dir)) + return; + for (const entry of fs_1.default.readdirSync(dir)) { + const fullPath = path_1.default.join(dir, entry); + const stat = fs_1.default.statSync(fullPath); + if (stat.isDirectory()) { + await this.syncDir(fullPath); + continue; + } + const relativePath = path_1.default.relative(this.localFallbackDir, fullPath).replace(/\\/g, '/'); + try { + await new lib_storage_1.Upload({ + client: this.s3, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: fs_1.default.readFileSync(fullPath), + }, + }).done(); + fs_1.default.unlinkSync(fullPath); + this.ctx.logger.info(`[SyncWorker] enviado e removido: ${relativePath}`); + } + catch (err) { + this.ctx.logger.warn(`[SyncWorker] falha em ${relativePath}: ${err.message}`); + } + } + } +} +const instance = new UnifiedStorageProviderPlugin(); +exports.default = instance; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dental-server/plugins/UnifiedStorageProvider/index.js.map b/dental-server/plugins/UnifiedStorageProvider/index.js.map new file mode 100644 index 0000000..7b93e19 --- /dev/null +++ b/dental-server/plugins/UnifiedStorageProvider/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dental-server/plugins/UnifiedStorageProvider/index.ts b/dental-server/plugins/UnifiedStorageProvider/index.ts new file mode 100644 index 0000000..bf39be5 --- /dev/null +++ b/dental-server/plugins/UnifiedStorageProvider/index.ts @@ -0,0 +1,426 @@ +import { + S3Client, + GetObjectCommand, + HeadBucketCommand, + ListObjectsV2Command, + ListBucketsCommand, + CreateBucketCommand, + DeleteObjectsCommand, +} from '@aws-sdk/client-s3'; +import { Upload } from '@aws-sdk/lib-storage'; +import { PluginInstance, PluginContext } from '../../backend/src/core/types'; +import type { StorageUploadPayload, StorageProviderInterface } from '../../backend/src/core/StorageProvider'; + +let storageProvider: any; +if (process.env.NODE_ENV === 'production') { + storageProvider = require('../../dist/core/StorageProvider').storageProvider; +} else { + storageProvider = require('../../backend/src/core/StorageProvider').storageProvider; +} +import sharp from 'sharp'; +import fs from 'fs'; +import path from 'path'; + +const VALID_CATEGORIES = ['documents', 'exports', 'posts', 'cards', 'partners', 'whatsapp', 'media']; + +// Mapa de extensão → mimetype para o proxy +const MIME_MAP: Record = { + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', + gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', + pdf: 'application/pdf', mp4: 'video/mp4', ogg: 'audio/ogg', + mp3: 'audio/mpeg', doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', +}; + +class UnifiedStorageProviderPlugin implements PluginInstance, StorageProviderInterface { + manifest: any; + private s3: S3Client | null = null; + private bucket: string = ''; + private region: string = 'us-east-1'; + private localFallbackDir: string = path.resolve(process.cwd(), 'storage', 'local_fallback'); + private ctx!: PluginContext; + private routesRegistered = false; + + async activate(ctx: PluginContext): Promise { + this.ctx = ctx; + const cfg = ctx.config.get('UnifiedStorageProvider') || {}; + + if (!fs.existsSync(this.localFallbackDir)) { + fs.mkdirSync(this.localFallbackDir, { recursive: true }); + } + + const accessKey = cfg.wasabiAccessKey || process.env.WASABI_ACCESS_KEY; + const secretKey = cfg.wasabiSecretKey || process.env.WASABI_SECRET_KEY; + this.region = (cfg.wasabiRegion || process.env.WASABI_REGION || 'us-east-1') as string; + this.bucket = (cfg.wasabiBucket || process.env.WASABI_BUCKET || '') as string; + + const rawEndpoint = (cfg.wasabiEndpoint || process.env.WASABI_ENDPOINT || 's3.wasabisys.com') as string; + const endpoint = rawEndpoint.startsWith('http') ? rawEndpoint : `https://${rawEndpoint}`; + + ctx.logger.info({ cfgKeys: Object.keys(cfg), hasKey: !!accessKey, hasBucket: !!this.bucket }, '[UnifiedStorage] activate debug'); + + if (!accessKey || !secretKey || !this.bucket) { + ctx.logger.warn('UnifiedStorageProvider: credenciais ou bucket não configurados — configure via admin'); + } else { + this.s3 = new S3Client({ + region: this.region, + endpoint, + credentials: { accessKeyId: accessKey as string, secretAccessKey: secretKey as string }, + forcePathStyle: true, + }); + + storageProvider.register(this); + ctx.logger.info(`UnifiedStorageProvider ativo — bucket: ${this.bucket} endpoint: ${endpoint}`); + + this.startSyncWorker(); + } + + if (!this.routesRegistered) { + // ── Proxy: serve arquivos do Wasabi (ou fallback local) ──────────────── + ctx.app.get('/api/storage/view/*', async (req, res) => { + const filePath = req.params[0]; + if (!filePath) return res.status(400).send('Path missing'); + + try { + // 1. Fallback local + const localPath = path.join(this.localFallbackDir, filePath); + if (fs.existsSync(localPath)) { + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'local_fallback'); + return res.send(fs.readFileSync(localPath)); + } + + // 2. Wasabi + if (!this.s3 || !this.bucket) { + return res.status(503).send('Storage não inicializado'); + } + + const result = await this.s3.send(new GetObjectCommand({ + Bucket: this.bucket, + Key: filePath, + })); + + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', result.ContentType || MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'wasabi'); + + if (result.Body) { + const bytes = await (result.Body as any).transformToByteArray(); + return res.send(Buffer.from(bytes)); + } + return res.status(404).send('Body vazio'); + } catch { + return res.status(404).send('Arquivo não encontrado'); + } + }); + + // ── POST /api/storage/wasabi/list-buckets ────────────────────────────── + // Lista buckets usando credenciais do body (sem exigir S3 pré-configurado) + ctx.app.post('/api/storage/wasabi/list-buckets', async (req, res) => { + const { accessKey, secretKey, region, endpoint } = req.body as Record; + if (!accessKey || !secretKey) { + return res.status(400).json({ error: 'Access Key e Secret Key são obrigatórios' }); + } + try { + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + const s3t = new S3Client({ + region: region || 'us-east-1', + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const data = await s3t.send(new ListBucketsCommand({})); + const buckets = (data.Buckets ?? []).map(b => ({ name: b.Name, createdAt: b.CreationDate })); + return res.json({ buckets }); + } catch (err: any) { + return res.status(400).json({ error: err.message }); + } + }); + + // ── POST /api/storage/wasabi/create-bucket ───────────────────────────── + ctx.app.post('/api/storage/wasabi/create-bucket', async (req, res) => { + const { accessKey, secretKey, region, endpoint, name } = req.body as Record; + if (!accessKey || !secretKey) return res.status(400).json({ error: 'Credenciais obrigatórias' }); + if (!name) return res.status(400).json({ error: 'Campo "name" obrigatório' }); + + const safeName = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'); + const effectiveRegion = region || 'us-east-1'; + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + + try { + const s3t = new S3Client({ + region: effectiveRegion, + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const params: any = { Bucket: safeName }; + if (effectiveRegion !== 'us-east-1') { + params.CreateBucketConfiguration = { LocationConstraint: effectiveRegion }; + } + await s3t.send(new CreateBucketCommand(params)); + ctx.logger.info(`[Wasabi] Bucket criado: ${safeName}`); + return res.status(201).json({ ok: true, name: safeName }); + } catch (err: any) { + return res.status(500).json({ error: err.message }); + } + }); + + this.routesRegistered = true; + } + + setInterval(() => { storageProvider.updateHealth(); }, 30000); + } + + async deactivate(ctx: PluginContext): Promise { + this.s3 = null; + ctx.logger.info('UnifiedStorageProvider desativado'); + } + + async checkHealth(): Promise { + return (await this.checkDetailedHealth()).healthy; + } + + async checkDetailedHealth(): Promise { + if (!this.s3 || !this.bucket) { + return { + healthy: false, + configured: false, + reason: 'not_configured', + message: 'Credenciais Wasabi não configuradas. Acesse Admin > Plugins > UnifiedStorageProvider.', + }; + } + try { + await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket })); + return { healthy: true, configured: true, reason: 'ok', message: `Wasabi OK — bucket: ${this.bucket}` }; + } catch (err: any) { + const code: string = err?.Code || err?.code || err?.name || ''; + const httpStatus: number = err?.$metadata?.httpStatusCode ?? 0; + + if (code === 'InvalidAccessKeyId' || code === 'SignatureDoesNotMatch') { + return { + healthy: false, configured: true, reason: 'auth_error', + message: 'Chaves de acesso inválidas. Verifique Access Key e Secret Key no painel do Wasabi.', + }; + } + if (httpStatus === 403 || code === 'AccessDenied') { + // 403 pode ser pagamento atrasado ou permissão negada + return { + healthy: false, configured: true, reason: 'billing_error', + message: 'Acesso negado ao Wasabi (HTTP 403). Verifique se a conta está ativa e o pagamento em dia.', + }; + } + if (httpStatus === 404 || code === 'NoSuchBucket') { + return { + healthy: false, configured: true, reason: 'bucket_not_found', + message: `Bucket "${this.bucket}" não encontrado. Verifique o nome do bucket no painel Wasabi.`, + }; + } + if (code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'NetworkingError') { + return { + healthy: false, configured: true, reason: 'network_error', + message: 'Sem conectividade com o Wasabi. Verifique a rede e o endpoint configurado.', + }; + } + return { + healthy: false, configured: true, reason: 'unknown', + message: `Erro ao verificar Wasabi: ${err?.message ?? code}`, + }; + } + } + + async deletePrefix(prefix: string): Promise<{ deleted: number }> { + let deletedCount = 0; + + // 1. Limpa do Wasabi + if (this.s3 && this.bucket) { + try { + let isTruncated = true; + let continuationToken: string | undefined; + + while (isTruncated) { + const listParams: any = { + Bucket: this.bucket, + Prefix: prefix, + MaxKeys: 1000, + }; + if (continuationToken) { + listParams.ContinuationToken = continuationToken; + } + + const listResult = await this.s3.send(new ListObjectsV2Command(listParams)); + const objects = listResult.Contents ?? []; + + if (objects.length > 0) { + const deleteParams = { + Bucket: this.bucket, + Delete: { + Objects: objects.map((obj) => ({ Key: obj.Key! })), + Quiet: true, + }, + }; + await this.s3.send(new DeleteObjectsCommand(deleteParams)); + deletedCount += objects.length; + this.ctx.logger.info(`[UnifiedStorageProvider] ${objects.length} objetos deletados do Wasabi com prefixo ${prefix}`); + } + + isTruncated = listResult.IsTruncated ?? false; + continuationToken = listResult.NextContinuationToken; + } + } catch (err: any) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao deletar prefixo ${prefix} do Wasabi: ${err.message}`); + } + } + + // 2. Limpa do fallback local + const localPath = path.join(this.localFallbackDir, prefix); + if (fs.existsSync(localPath)) { + try { + const stat = fs.statSync(localPath); + if (stat.isDirectory()) { + fs.rmSync(localPath, { recursive: true, force: true }); + this.ctx.logger.info(`[UnifiedStorageProvider] Pasta local do fallback removida: ${localPath}`); + } else { + fs.unlinkSync(localPath); + this.ctx.logger.info(`[UnifiedStorageProvider] Arquivo local do fallback removido: ${localPath}`); + } + } catch (err: any) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao remover fallback local para prefixo ${prefix}: ${err.message}`); + } + } + + return { deleted: deletedCount }; + } + + async upload(payload: StorageUploadPayload): Promise<{ provider: string; path: string }> { + const { category, file } = payload; + if (!VALID_CATEGORIES.includes(category)) throw new Error(`Categoria inválida: ${category}`); + + let buffer = file.buffer; + let originalName = file.originalname; + let mimetype = file.mimetype; + + // Otimiza imagens grandes (>150 KB) para WebP + if (mimetype.startsWith('image/') && !mimetype.includes('webp') && buffer.length > 150 * 1024) { + try { + buffer = await sharp(buffer).resize(1200, 1200, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75 }).toBuffer(); + mimetype = 'image/webp'; + originalName = originalName.replace(/\.[^.]+$/, '.webp'); + this.ctx.logger.debug(`[Optimizer] ${originalName} convertida para webp`); + } catch (err: any) { + this.ctx.logger.warn(`[Optimizer] falha: ${err.message}`); + } + } + + const safeBase = originalName.replace(/[^a-zA-Z0-9._-]/g, '_'); + const fileName = `${Date.now()}-${safeBase}`; + + // Path no bucket: customPath (quando fornecido) > whatsapp/{t}/{i}/{file} > {cat}/{file} + const relativePath = payload.customPath + ?? ((category === 'whatsapp' && payload.usuarioSis && payload.sessionJID) + ? `whatsapp/${payload.usuarioSis}/${payload.sessionJID}/${fileName}` + : `${category}/${fileName}`); + + if (!this.s3 || !this.bucket) { + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + + try { + await new Upload({ + client: this.s3, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: buffer, + ContentType: mimetype, + }, + }).done(); + + this.ctx.logger.debug(`[Wasabi] upload ok: ${relativePath}`); + return { provider: 'wasabi', path: relativePath }; + + } catch (err: any) { + this.ctx.logger.error(`[Wasabi] upload falhou (${err.message}) — salvando local`); + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + } + + async getBuffer(relativePath: string): Promise { + // 1. Fallback local + const localPath = path.join(this.localFallbackDir, relativePath); + if (fs.existsSync(localPath)) { + return fs.readFileSync(localPath); + } + // 2. Wasabi + if (!this.s3 || !this.bucket) return null; + try { + const result = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: relativePath })); + if (!result.Body) return null; + const bytes = await (result.Body as any).transformToByteArray(); + return Buffer.from(bytes); + } catch { + return null; + } + } + + /** Salva buffer no diretório de fallback local, criando subpastas se necessário */ + private saveToLocalFallback(relativePath: string, buffer: Buffer, originalName: string, _category: string): { provider: string; path: string } { + const dest = path.join(this.localFallbackDir, relativePath); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, buffer); + this.ctx.logger.debug(`[Fallback] salvo local: ${dest}`); + return { provider: 'local_fallback', path: relativePath }; + } + + /** Sincroniza arquivos do fallback local para o Wasabi a cada 2 minutos */ + private startSyncWorker(): void { + setInterval(async () => { + if (!this.s3 || !this.bucket) return; + await this.syncDir(this.localFallbackDir); + }, 120_000); + } + + private async syncDir(dir: string): Promise { + if (!fs.existsSync(dir)) return; + + for (const entry of fs.readdirSync(dir)) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + await this.syncDir(fullPath); + continue; + } + + const relativePath = path.relative(this.localFallbackDir, fullPath).replace(/\\/g, '/'); + + try { + await new Upload({ + client: this.s3!, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: fs.readFileSync(fullPath), + }, + }).done(); + + fs.unlinkSync(fullPath); + this.ctx.logger.info(`[SyncWorker] enviado e removido: ${relativePath}`); + } catch (err: any) { + this.ctx.logger.warn(`[SyncWorker] falha em ${relativePath}: ${err.message}`); + } + } + } +} + +const instance = new UnifiedStorageProviderPlugin(); +export default instance; diff --git a/dental-server/plugins/UnifiedStorageProvider/manifest.json b/dental-server/plugins/UnifiedStorageProvider/manifest.json new file mode 100644 index 0000000..50d9af6 --- /dev/null +++ b/dental-server/plugins/UnifiedStorageProvider/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "UnifiedStorageProvider", + "displayName": "Wasabi Storage", + "version": "1.1.0", + "description": "Armazenamento de mídias do WhatsApp via Wasabi (S3 compatível)", + "author": "NewWhats", + "category": "core", + "enabled": true, + "canDisable": false, + "dependencies": [], + "backend": { + "routePrefix": "/api/storage", + "hasMigrations": false + }, + "configSchema": [ + { "key": "wasabiAccessKey", "label": "Access Key", "type": "password", "required": true, "group": "Credenciais Wasabi", "tooltip": "Chave de acesso gerada em Account → Access Keys no painel Wasabi." }, + { "key": "wasabiSecretKey", "label": "Secret Key", "type": "password", "required": true, "group": "Credenciais Wasabi", "tooltip": "Chave secreta correspondente à Access Key — exibida só uma vez no momento da criação." }, + { "key": "wasabiBucket", "label": "Bucket Ativo", "type": "text", "required": true, "group": "Bucket", "tooltip": "Nome do bucket onde as mídias do WhatsApp serão armazenadas." }, + { "key": "wasabiRegion", "label": "Region", "type": "text", "required": false, "group": "Avançado", "tooltip": "Região do bucket. Padrão: us-east-1. Exemplos: us-east-2, eu-central-1." }, + { "key": "wasabiEndpoint", "label": "Endpoint", "type": "text", "required": false, "group": "Avançado", "tooltip": "Endpoint S3. Padrão: s3.wasabisys.com. Só altere se usar região diferente." } + ] +} diff --git a/dental-server/public/app.js b/dental-server/public/app.js index 5ff3eb8..a9622a8 100644 --- a/dental-server/public/app.js +++ b/dental-server/public/app.js @@ -98,6 +98,7 @@ let clientsList = []; let selectedClient = ''; let showDisabled = false; let currentTransformations = []; +let fineTuneAngle = 0; // ================================================================ // DOM REFS @@ -149,6 +150,7 @@ document.addEventListener('DOMContentLoaded', async () => { await loadClientsList(); loadPatients(); setupEventListeners(); + initDragAndDrop(); setInterval(loadClientsList, 5000); } catch (error) { _log.error('INIT', 'Erro fatal na inicialização:', error.message, error); @@ -229,8 +231,12 @@ function updateClientsDropdown() { const opt = document.createElement('option'); opt.value = c.name; let lbl = c.name; - if (c.status === 'identified') lbl += ' ✅'; - else if (c.status === 'historic') lbl += ' 📚'; + if (c.name === 'Upload Web') { + lbl = 'Upload Web 💻 (Imagens do Painel)'; + } else { + if (c.status === 'identified') lbl += ' ✅'; + else if (c.status === 'historic') lbl += ' 📚'; + } opt.textContent = lbl; clientFilter.appendChild(opt); }); @@ -341,6 +347,7 @@ function renderPatientCards() { return `
+
${count} imagem${count !== 1 ? 'ns' : ''}
@@ -365,7 +372,7 @@ function renderPatientCards() { cardW: cs.width, cardH: cs.height, previewH: pcs ? pcs.height : 'n/a', previewBg: pcs ? pcs.backgroundImage.substring(0, 60) : 'n/a' - }); + }); } else { _log.error('RENDER_CARDS', '❌ Nenhum card encontrado no DOM após render!'); } @@ -374,6 +381,38 @@ function renderPatientCards() { }); } +// Exclui todas as imagens de um determinado paciente (S3 e Local) +async function deletePatientImages(patientName) { + if (!patientName) return; + + // TODO(security): Usando confirm() para manter coerência com o restante das confirmações do painel do projeto + const ok = confirm(`⚠️ ATENÇÃO: Tem certeza que deseja excluir permanentemente todas as imagens do paciente "${patientName}"?\n\nEsta ação apagará os arquivos locais, do Wasabi S3 e do banco de dados, e NÃO poderá ser desfeita.`); + if (!ok) return; + + showToast('Excluindo imagens do paciente...', 'info'); + + try { + const res = await fetchWithAuth(`/api/images/by-patient?name=${encodeURIComponent(patientName)}`, { + method: 'DELETE' + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Erro ao excluir imagens do paciente.'); + } + + showToast('✅ Todas as imagens do paciente foram excluídas!', 'success'); + + // Atualizar listagens + await loadPatients(); + await loadClientsList(); + + } catch (e) { + console.error('Erro na exclusão do paciente:', e); + showToast(`Falha ao excluir imagens: ${e.message}`, 'error'); + } +} + // ================================================================ // VIEW: PATIENT IMAGES // ================================================================ @@ -435,9 +474,11 @@ async function loadPatientImages(patientName) { } function renderPatientImages() { - imagesGrid.innerHTML = patientImages.map(image => ` + imagesGrid.innerHTML = patientImages.map(image => { + const thumbUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; + return `
-
+
📅 ${formatDate(image.created_at)}
${escapeHtml(image.image_guid || image.filename)}
@@ -451,8 +492,8 @@ function renderPatientImages() {
${!image.enabled ? '
Desabilitada
' : ''} -
- `).join(''); +
`; + }).join(''); } // ================================================================ @@ -506,19 +547,30 @@ async function showTransformModal(imageId) { `; try { - const res = await fetchWithAuth(`/api/images/${imageId}/transformations`); - if (!res.ok) { - if (res.status === 401 || res.status === 403) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return; } - throw new Error('Erro ao gerar transformações'); + const image = patientImages.find(img => img.id === imageId); + if (!image) { + throw new Error('Imagem não encontrada localmente'); } - const transformations = await res.json(); + // Se existir thumb_filename, usar ele; caso contrário, fallback para a imagem cheia + const imageUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`; + + const transformations = [ + { name: 'Original', rotation: 0, flipH: false, flipV: false, css: 'transform: none;' }, + { name: 'Flip Horizontal', rotation: 0, flipH: true, flipV: false, css: 'transform: scaleX(-1);' }, + { name: 'Flip Vertical', rotation: 0, flipH: false, flipV: true, css: 'transform: scaleY(-1);' }, + { name: 'Rotação +90°', rotation: 90, flipH: false, flipV: false, css: 'transform: rotate(90deg);' }, + { name: 'Rotação -90°', rotation: -90, flipH: false, flipV: false, css: 'transform: rotate(-90deg);' } + ]; + currentTransformations = transformations; const container = document.getElementById('transformOptions'); container.innerHTML = transformations.map((t, i) => `
- ${escapeHtml(t.name)} +
+ ${escapeHtml(t.name)} +
${escapeHtml(t.name)}
`).join(''); @@ -526,23 +578,21 @@ async function showTransformModal(imageId) { // --------------------------------------------------------------- // Detectar orientação da imagem original → aplicar layout correto // --------------------------------------------------------------- - if (transformations.length > 0 && transformations[0].base64) { - const probe = new Image(); - probe.onload = () => { - const orientation = probe.naturalWidth >= probe.naturalHeight ? 'landscape' : 'portrait'; - container.classList.remove('grid-landscape', 'grid-portrait'); - container.classList.add(`grid-${orientation}`); - _log.info('TRANSFORM', `Orientação detectada: ${orientation} (${probe.naturalWidth}×${probe.naturalHeight})`); - }; - probe.onerror = () => { - _log.warn('TRANSFORM', 'Não foi possível detectar orientação — usando fallback'); - }; - probe.src = transformations[0].base64; - } + const probe = new Image(); + probe.onload = () => { + const orientation = probe.naturalWidth >= probe.naturalHeight ? 'landscape' : 'portrait'; + container.classList.remove('grid-landscape', 'grid-portrait'); + container.classList.add(`grid-${orientation}`); + _log.info('TRANSFORM', `Orientação detectada: ${orientation} (${probe.naturalWidth}×${probe.naturalHeight})`); + }; + probe.onerror = () => { + _log.warn('TRANSFORM', 'Não foi possível detectar orientação — usando fallback'); + }; + probe.src = imageUrl; } catch (e) { _log.error('TRANSFORM', '❌ Exceção:', e.message, e); - showToast('Erro ao gerar variações', 'error'); + showToast('Erro ao carregar variações', 'error'); transformModal.classList.remove('active'); } } @@ -571,6 +621,17 @@ function selectTransform(index) { el.classList.add('selected'); + // Reset fine tune angle when selecting a new transform + fineTuneAngle = 0; + updateFineTuneDisplay(); + + // Mover o contêiner de ajuste fino para dentro do card selecionado + const ftContainer = document.getElementById('fineTuneContainer'); + if (ftContainer) { + el.appendChild(ftContainer); + ftContainer.style.display = 'flex'; + } + // Show action area document.getElementById('transformActionArea').style.display = 'block'; @@ -585,11 +646,38 @@ function clearTransformSelection() { const container = document.getElementById('transformOptions'); container.classList.remove('selection-mode'); + // Mover o contêiner de ajuste fino de volta para o Wrapper e esconder + const ftContainer = document.getElementById('fineTuneContainer'); + if (ftContainer) { + const viewWrapper = document.getElementById('transformViewWrapper'); + if (viewWrapper) { + viewWrapper.appendChild(ftContainer); + } else { + document.body.appendChild(ftContainer); + } + ftContainer.style.display = 'none'; + } + + // Restore original transform styles on all images document.querySelectorAll('.transform-option').forEach(option => { option.classList.remove('selected'); option.style.display = 'block'; + + const optIndex = parseInt(option.getAttribute('data-index')); + const transform = currentTransformations[optIndex]; + const imgEl = option.querySelector('img'); + if (imgEl && transform) { + imgEl.style.transform = transform.css; + } + const nameEl = option.querySelector('.transform-name'); + if (nameEl && transform) { + nameEl.innerText = transform.name; + } }); + fineTuneAngle = 0; + updateFineTuneDisplay(); + document.getElementById('transformActionArea').style.display = 'none'; document.getElementById('newImageRemark').value = ''; } @@ -612,7 +700,7 @@ async function saveSelectedTransform() { try { const payload = { - rotation: transform.rotation, + rotation: transform.rotation + fineTuneAngle, flipH: transform.flipH, flipV: transform.flipV, remark: remark @@ -621,7 +709,7 @@ async function saveSelectedTransform() { const res = await fetchWithAuth(`/api/images/${selectedImageId}/transform`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ rotation: transform.rotation, flipH: transform.flipH, flipV: transform.flipV }) + body: JSON.stringify(payload) }); if (!res.ok) throw new Error('Erro ao salvar'); @@ -634,6 +722,49 @@ async function saveSelectedTransform() { } } +function adjustFineTune(offset) { + const selectedEl = document.querySelector('.transform-option.selected'); + if (!selectedEl) return; + + const index = parseInt(selectedEl.getAttribute('data-index')); + const transform = currentTransformations[index]; + + fineTuneAngle += offset; + updateFineTuneDisplay(); + + // Update the image style using the combined transform + const imgEl = selectedEl.querySelector('img'); + if (imgEl) { + let totalRotation = transform.rotation + fineTuneAngle; + let transformStr = `rotate(${totalRotation}deg)`; + if (transform.flipH) transformStr += ` scaleX(-1)`; + if (transform.flipV) transformStr += ` scaleY(-1)`; + imgEl.style.transform = transformStr; + } + + // Update display name inside the card + const nameEl = selectedEl.querySelector('.transform-name'); + if (nameEl) { + nameEl.innerText = `${transform.name} (${fineTuneAngle > 0 ? '+' : ''}${fineTuneAngle}°)`; + } +} + +function resetFineTune() { + adjustFineTune(-fineTuneAngle); +} + +function updateFineTuneDisplay() { + const displayEl = document.getElementById('fineTuneValDisplay'); + if (displayEl) { + displayEl.textContent = `${fineTuneAngle > 0 ? '+' : ''}${fineTuneAngle}°`; + } + + const resetEl = document.getElementById('btnResetFineTune'); + if (resetEl) { + resetEl.style.display = fineTuneAngle !== 0 ? 'inline-block' : 'none'; + } +} + async function toggleImageEnabled(imageId) { try { const res = await fetchWithAuth(`/api/images/${imageId}/toggle-enabled`, { method: 'PUT' }); @@ -1044,4 +1175,600 @@ async function updateCredentials(event) { window.openSettingsModal = openSettingsModal; window.closeSettingsModal = closeSettingsModal; window.updateCredentials = updateCredentials; + +// ================================================================ +// CONFIGURAÇÕES DE PLUGINS (Wasabi Storage) +// ================================================================ + +function openPluginsModal() { + const modal = document.getElementById('pluginsModal'); + if (modal) modal.style.display = 'block'; + + // Limpar campos + document.getElementById('wasabiAccessKey').value = ''; + document.getElementById('wasabiSecretKey').value = ''; + document.getElementById('wasabiBucket').value = ''; + document.getElementById('wasabiRegion').value = ''; + document.getElementById('wasabiEndpoint').value = ''; + + const alertBox = document.getElementById('pluginsStatusAlert'); + alertBox.style.display = 'none'; + + // Carregar configurações atuais + loadPluginsConfig(); +} + +function closePluginsModal() { + const modal = document.getElementById('pluginsModal'); + if (modal) modal.style.display = 'none'; +} + +async function loadPluginsConfig() { + try { + const res = await fetchWithAuth('/api/system/storage-config'); + if (!res.ok) throw new Error('Falha ao carregar configurações'); + + const config = await res.json(); + + document.getElementById('wasabiAccessKey').value = config.wasabiAccessKey || ''; + document.getElementById('wasabiSecretKey').value = config.wasabiSecretKey || ''; + document.getElementById('wasabiBucket').value = config.wasabiBucket || ''; + document.getElementById('wasabiRegion').value = config.wasabiRegion || ''; + document.getElementById('wasabiEndpoint').value = config.wasabiEndpoint || ''; + + const alertBox = document.getElementById('pluginsStatusAlert'); + if (config.enabled) { + alertBox.className = 'alert-success'; + alertBox.style.background = '#eafaf1'; + alertBox.style.color = '#27ae60'; + alertBox.style.border = '1px solid #2ecc7133'; + alertBox.innerHTML = '🟢 Status: Wasabi Storage está ATIVO e conectado.'; + } else { + alertBox.className = 'alert-warning'; + alertBox.style.background = '#fffbeb'; + alertBox.style.color = '#d97706'; + alertBox.style.border = '1px solid #f59e0b33'; + alertBox.innerHTML = '🟡 Status: Wasabi Storage está DESATIVADO (utilizando armazenamento local).'; + } + alertBox.style.display = 'block'; + } catch (e) { + console.error(e); + showToast('Erro ao carregar configurações do Wasabi.', 'error'); + } +} + +async function updatePluginsConfig(event) { + event.preventDefault(); + + const wasabiAccessKey = document.getElementById('wasabiAccessKey').value.trim(); + const wasabiSecretKey = document.getElementById('wasabiSecretKey').value; + const wasabiBucket = document.getElementById('wasabiBucket').value.trim(); + const wasabiRegion = document.getElementById('wasabiRegion').value.trim(); + const wasabiEndpoint = document.getElementById('wasabiEndpoint').value.trim(); + + const btn = document.getElementById('btnSavePlugins'); + const originalText = btn.innerHTML; + btn.innerHTML = ' Salvando...'; + btn.disabled = true; + + try { + const res = await fetchWithAuth('/api/system/storage-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + wasabiAccessKey, + wasabiSecretKey, + wasabiBucket, + wasabiRegion, + wasabiEndpoint + }) + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Erro ao salvar configurações.'); + } + + showToast('Configurações do Wasabi salvas com sucesso!', 'success'); + + // Recarregar o status + await loadPluginsConfig(); + + // Fechar após 1 segundo + setTimeout(closePluginsModal, 1200); + + } catch (e) { + console.error(e); + showToast(e.message, 'error'); + } finally { + btn.innerHTML = originalText; + btn.disabled = false; + } +} + +async function triggerSync() { + showToast('🔄 Enviando sinal de sincronização para dispositivos...', 'info'); + try { + const res = await fetchWithAuth('/api/system/trigger-sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Erro ao acionar sincronização.'); + } + + const data = await res.json(); + if (data.devicesTriggered > 0) { + showToast(`✅ Sinal enviado para ${data.devicesTriggered} dispositivo(s) conectado(s)!`, 'success'); + } else { + showToast('⚠️ Nenhum dispositivo Windows conectado no momento.', 'warning'); + } + } catch (e) { + console.error(e); + showToast(e.message, 'error'); + } +} + +// Global scope +window.openPluginsModal = openPluginsModal; +window.closePluginsModal = closePluginsModal; +window.updatePluginsConfig = updatePluginsConfig; +window.triggerSync = triggerSync; + +// ================================================================ +// ÁREA DE SINCRONIZAÇÃO E UPLOAD MANUAL +// ================================================================ + +let selectedUploadFiles = []; + +function openSyncModal() { + const modal = document.getElementById('syncModal'); + if (!modal) return; + + modal.classList.add('active'); + modal.style.display = 'flex'; + + // Resetar abas + switchSyncTab('devices'); + + // Limpar formulário de upload + resetManualUploadForm(); + + // Carregar dados + loadSyncDevices(); + refreshUploadPatients(); + + // Escuta de clique fora para fechar modal + modal.onclick = (e) => { + if (e.target === modal) closeSyncModal(); + }; +} + +function closeSyncModal() { + const modal = document.getElementById('syncModal'); + if (!modal) return; + + modal.classList.remove('active'); + modal.style.display = 'none'; +} + +function switchSyncTab(tabName) { + const btnDevices = document.getElementById('tabBtnDevices'); + const btnUpload = document.getElementById('tabBtnUpload'); + const tabDevices = document.getElementById('tab-devices'); + const tabUpload = document.getElementById('tab-upload'); + + if (!btnDevices || !btnUpload || !tabDevices || !tabUpload) return; + + if (tabName === 'devices') { + btnDevices.classList.add('active'); + btnUpload.classList.remove('active'); + tabDevices.classList.add('active'); + tabUpload.classList.remove('active'); + loadSyncDevices(); + } else { + btnDevices.classList.remove('active'); + btnUpload.classList.add('active'); + tabDevices.classList.remove('active'); + tabUpload.classList.add('active'); + } +} + +async function loadSyncDevices() { + const loader = document.getElementById('syncDevicesLoading'); + const empty = document.getElementById('syncDevicesEmpty'); + const list = document.getElementById('syncDeviceList'); + + if (!loader || !empty || !list) return; + + loader.style.display = 'block'; + empty.style.display = 'none'; + list.style.display = 'none'; + + try { + const res = await fetchWithAuth('/api/socket/status'); + if (!res.ok) throw new Error('Falha ao obter status dos dispositivos.'); + const data = await res.json(); + + list.replaceChildren(); + + const windowsClients = (data.identified || []).filter(c => c.type === 'client' || c.type === 'windows'); + + if (windowsClients.length === 0) { + loader.style.display = 'none'; + empty.style.display = 'block'; + list.style.display = 'none'; + return; + } + + windowsClients.forEach(client => { + const card = document.createElement('div'); + card.className = 'sync-device-card'; + + const left = document.createElement('div'); + left.className = 'device-card-left'; + + const title = document.createElement('div'); + title.className = 'device-card-title'; + title.textContent = `💻 ${client.name || 'Dispositivo Sem Nome'}`; + + const subtitle = document.createElement('div'); + subtitle.className = 'device-card-subtitle'; + subtitle.textContent = `SO: ${client.system || 'Windows'} | Versão: ${client.version || 'N/A'} | ID: ${client.socketId}`; + + left.appendChild(title); + left.appendChild(subtitle); + + const badge = document.createElement('span'); + badge.className = 'device-card-badge badge-connected'; + badge.textContent = 'ONLINE'; + + card.appendChild(left); + card.appendChild(badge); + + list.appendChild(card); + }); + + loader.style.display = 'none'; + empty.style.display = 'none'; + list.style.display = 'flex'; + + } catch (e) { + console.error(e); + loader.style.display = 'none'; + empty.style.display = 'block'; + const p = empty.querySelector('p'); + if (p) p.textContent = 'Erro ao carregar dispositivos: ' + e.message; + } +} + +async function triggerSyncFromModal() { + const btn = document.getElementById('btnTriggerSyncModal'); + if (!btn) return; + + const originalText = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = '🔄 Enviando sinal...'; + + try { + const res = await fetchWithAuth('/api/system/trigger-sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Erro ao acionar sincronização.'); + } + + const data = await res.json(); + if (data.devicesTriggered > 0) { + showToast(`✅ Sinal enviado para ${data.devicesTriggered} dispositivo(s) conectado(s)!`, 'success'); + } else { + showToast('⚠️ Nenhum dispositivo Windows conectado para sincronizar.', 'warning'); + } + } catch (e) { + console.error(e); + showToast(e.message, 'error'); + } finally { + btn.disabled = false; + btn.innerHTML = originalText; + } +} + +async function refreshUploadPatients() { + const select = document.getElementById('uploadPatientSelect'); + if (!select) return; + + // Manter as primeiras duas opções + while (select.options.length > 2) { + select.remove(2); + } + + try { + const res = await fetchWithAuth('/api/images/patients?disabled=false&search='); + if (!res.ok) throw new Error('Erro ao listar pacientes.'); + + const patients = await res.json(); + patients.forEach(p => { + if (!p || !p.patient_name) return; + const opt = document.createElement('option'); + opt.value = p.patient_name; + opt.textContent = p.patient_name; + select.appendChild(opt); + }); + } catch (e) { + console.error(e); + showToast('Erro ao carregar lista de pacientes.', 'error'); + } +} + +function toggleNewPatientFields() { + const select = document.getElementById('uploadPatientSelect'); + const fields = document.getElementById('newPatientFields'); + + if (!select || !fields) return; + + if (select.value === 'NEW_PATIENT') { + fields.style.display = 'block'; + document.getElementById('uploadNewPatientFirstName').required = true; + document.getElementById('uploadNewPatientLastName').required = true; + } else { + fields.style.display = 'none'; + document.getElementById('uploadNewPatientFirstName').required = false; + document.getElementById('uploadNewPatientLastName').required = false; + } +} + +function resetManualUploadForm() { + selectedUploadFiles = []; + + const form = document.getElementById('manualUploadForm'); + if (form) form.reset(); + + const previews = document.getElementById('uploadPreviewsContainer'); + if (previews) previews.replaceChildren(); + + const progressContainer = document.getElementById('uploadProgressContainer'); + if (progressContainer) progressContainer.style.display = 'none'; + + const fields = document.getElementById('newPatientFields'); + if (fields) fields.style.display = 'none'; + + const btnSubmit = document.getElementById('btnSubmitManualUpload'); + if (btnSubmit) btnSubmit.disabled = true; +} + +// Inicializar Drag and Drop +function initDragAndDrop() { + const zone = document.getElementById('uploadDragDropZone'); + const fileInput = document.getElementById('uploadFileInput'); + + if (!zone || !fileInput) return; + + // Evento de clique na zona abre o seletor de arquivos + zone.addEventListener('click', () => fileInput.click()); + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + zone.addEventListener(eventName, e => { + e.preventDefault(); + e.stopPropagation(); + }, false); + }); + + ['dragenter', 'dragover'].forEach(eventName => { + zone.addEventListener(eventName, () => { + zone.classList.add('drag-active'); + }, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + zone.addEventListener(eventName, () => { + zone.classList.remove('drag-active'); + }, false); + }); + + zone.addEventListener('drop', e => { + const dt = e.dataTransfer; + if (dt) handleUploadFiles(dt.files); + }); + + fileInput.addEventListener('change', e => { + if (e.target && e.target.files) handleUploadFiles(e.target.files); + }); +} + +function handleUploadFiles(files) { + if (!files || files.length === 0) return; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + if (!file.type.match('image.*')) { + showToast(`O arquivo "${file.name}" não é uma imagem válida!`, 'warning'); + continue; + } + + if (file.size > 10 * 1024 * 1024) { + showToast(`A imagem "${file.name}" excede o limite de tamanho de 10MB!`, 'warning'); + continue; + } + + if (selectedUploadFiles.some(f => f.name === file.name && f.size === file.size)) { + continue; + } + + selectedUploadFiles.push(file); + } + + renderUploadPreviews(); +} + +function renderUploadPreviews() { + const container = document.getElementById('uploadPreviewsContainer'); + const btnSubmit = document.getElementById('btnSubmitManualUpload'); + if (!container) return; + + container.replaceChildren(); + + selectedUploadFiles.forEach((file, index) => { + const item = document.createElement('div'); + item.className = 'preview-item'; + + const img = document.createElement('img'); + img.alt = file.name; + + const reader = new FileReader(); + reader.onload = e => { + if (e.target && e.target.result) img.src = e.target.result; + }; + reader.readAsDataURL(file); + + const btnRemove = document.createElement('button'); + btnRemove.type = 'button'; + btnRemove.className = 'preview-remove'; + btnRemove.innerHTML = '×'; + btnRemove.title = 'Remover Imagem'; + btnRemove.onclick = (e) => { + e.stopPropagation(); + removeUploadPreview(index); + }; + + item.appendChild(img); + item.appendChild(btnRemove); + container.appendChild(item); + }); + + if (btnSubmit) { + btnSubmit.disabled = selectedUploadFiles.length === 0; + } +} + +function removeUploadPreview(index) { + selectedUploadFiles.splice(index, 1); + renderUploadPreviews(); +} + +function readFileAsBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + reader.readAsDataURL(file); + }); +} + +async function handleManualUploadSubmit(event) { + event.preventDefault(); + + if (selectedUploadFiles.length === 0) { + showToast('Selecione pelo menos uma imagem para enviar.', 'warning'); + return; + } + + const select = document.getElementById('uploadPatientSelect'); + const remarkInput = document.getElementById('uploadRemark'); + const btnSubmit = document.getElementById('btnSubmitManualUpload'); + + if (!select || !btnSubmit) return; + + let patientName = ''; + let doctor = ''; + + if (select.value === 'NEW_PATIENT') { + const firstName = document.getElementById('uploadNewPatientFirstName').value.trim(); + const lastName = document.getElementById('uploadNewPatientLastName').value.trim(); + doctor = document.getElementById('uploadNewPatientDoctor').value.trim(); + + if (!firstName || !lastName) { + showToast('Nome e sobrenome são obrigatórios para novo paciente.', 'warning'); + return; + } + + patientName = `${firstName} ${lastName}`; + } else { + patientName = select.value; + } + + const remark = remarkInput ? remarkInput.value.trim() : ''; + + const progressContainer = document.getElementById('uploadProgressContainer'); + const progressText = document.getElementById('uploadProgressText'); + const progressBarFill = document.getElementById('uploadProgressBarFill'); + const progressPercent = document.getElementById('uploadProgressPercent'); + + if (progressContainer) progressContainer.style.display = 'block'; + btnSubmit.disabled = true; + + const totalFiles = selectedUploadFiles.length; + let successCount = 0; + + for (let i = 0; i < totalFiles; i++) { + const file = selectedUploadFiles[i]; + + if (progressText) progressText.textContent = `Enviando imagem ${i + 1} de ${totalFiles}...`; + if (progressBarFill) { + const pct = Math.round((i / totalFiles) * 100); + progressBarFill.style.width = `${pct}%`; + if (progressPercent) progressPercent.textContent = `${pct}%`; + } + + try { + const base64 = await readFileAsBase64(file); + + const response = await fetchWithAuth('/api/images/upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + imageBase64: base64, + filename: file.name, + patientName: patientName, + doctor: doctor || null, + remark: remark || null + }) + }); + + if (!response.ok) { + const err = await response.json(); + throw new Error(err.error || `Erro HTTP ${response.status}`); + } + + successCount++; + } catch (e) { + console.error(`Erro ao enviar arquivo ${file.name}:`, e); + showToast(`Falha ao enviar a imagem ${file.name}: ${e.message}`, 'error'); + } + } + + if (progressBarFill) { + progressBarFill.style.width = '100%'; + if (progressPercent) progressPercent.textContent = '100%'; + } + if (progressText) progressText.textContent = `Finalizado! ${successCount} de ${totalFiles} imagens enviadas.`; + + setTimeout(async () => { + closeSyncModal(); + showToast(`✅ ${successCount} imagem(ns) enviada(s) com sucesso!`, 'success'); + + await loadPatients(); + if (window.loadClientsList) { + await window.loadClientsList(); + } + }, 1000); +} + +// Vinculando ao window +window.openSyncModal = openSyncModal; +window.closeSyncModal = closeSyncModal; +window.switchSyncTab = switchSyncTab; +window.triggerSyncFromModal = triggerSyncFromModal; +window.refreshUploadPatients = refreshUploadPatients; +window.toggleNewPatientFields = toggleNewPatientFields; +window.handleManualUploadSubmit = handleManualUploadSubmit; +window.initDragAndDrop = initDragAndDrop; +window.deletePatientImages = deletePatientImages; +window.adjustFineTune = adjustFineTune; +window.resetFineTune = resetFineTune; // A aplicação é inicializada no evento DOMContentLoaded acima diff --git a/dental-server/public/index.html b/dental-server/public/index.html index 4279aca..8d28c55 100644 --- a/dental-server/public/index.html +++ b/dental-server/public/index.html @@ -28,6 +28,12 @@ ⚙️ Configurações + + 🔌 Plugins / Wasabi + + + 🔄 Sincronização + ⚠️ Zerar Sistema @@ -122,7 +128,7 @@
- @@ -270,6 +286,169 @@
+ + + + + + diff --git a/dental-server/public/style.css b/dental-server/public/style.css index 633a52d..f95d708 100644 --- a/dental-server/public/style.css +++ b/dental-server/public/style.css @@ -754,10 +754,9 @@ body { grid-column: auto !important; grid-row: auto !important; } -.selection-mode .transform-option img { - height: 35vh !important; - max-height: 400px; - object-fit: contain; +.selection-mode .transform-image-wrapper { + height: 45vh !important; + max-height: 500px; } /* -90°: col 4, linha 2 (empilhado sob +90°) */ .transform-grid.grid-portrait .transform-option:nth-child(5) { grid-column: 4; grid-row: 2; } @@ -786,6 +785,7 @@ body { align-items: center; min-height: 0; overflow: hidden; + position: relative; /* Para posicionar o ajuste fino absoluto */ } .transform-option:hover { @@ -798,13 +798,28 @@ body { background: rgba(81, 207, 102, 0.08); } -.transform-option img { +/* Novo Wrapper para a Imagem */ +.transform-image-wrapper { + position: relative; + width: 100%; + height: 15vh; + min-height: 120px; + max-height: 250px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: 4px; + background: rgba(0,0,0,0.02); + margin-bottom: 10px; +} + +.transform-image-wrapper img { max-width: 100%; - flex: 1; - min-height: 0; + max-height: 100%; object-fit: contain; border-radius: 4px; - margin-bottom: 10px; + transition: transform 0.15s ease-out; /* Transição para ajuste fino suave */ } .transform-name { @@ -814,6 +829,64 @@ body { flex-shrink: 0; } +/* Estilo do Ajuste Fino Sobreposto como Marca d'Água */ +#fineTuneContainer { + position: absolute; + bottom: 45px; /* Logo acima do transform-name */ + left: 50%; + transform: translateX(-50%); + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 30px; + padding: 6px 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.18); + z-index: 100; + display: flex; + align-items: center; + gap: 8px; + width: max-content; + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateX(-50%) translateY(5px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +} + +#fineTuneContainer button { + background: #f1f3f5; + border: 1px solid #ced4da; + border-radius: 50%; + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: 700; + color: #495057; + cursor: pointer; + transition: all 0.15s ease; +} + +#fineTuneContainer button:hover { + background: #e9ecef; + border-color: #adb5bd; + color: #212529; +} + +#fineTuneContainer .fine-tune-val-display { + min-width: 48px; + text-align: center; + font-family: monospace; + font-weight: 700; + font-size: 1rem; + color: var(--primary-color); +} + /* ================================================================ @@ -1072,3 +1145,240 @@ body { #toggleDisabledBtn { font-size: 0; padding: 10px; } #toggleDisabledBtn::before { content: '👁'; font-size: 1.1rem; } } + +/* ================================================================ + SYNC & UPLOAD MODAL STYLES + ================================================================ */ + +.sync-tabs { + display: flex; + border-bottom: 2px solid var(--border-color); + margin-bottom: 20px; + gap: 10px; +} + +.sync-tab-btn { + padding: 12px 20px; + background: none; + border: none; + border-bottom: 3px solid transparent; + font-size: 1rem; + font-weight: 600; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.25s ease; +} + +.sync-tab-btn:hover { + color: var(--primary-color); +} + +.sync-tab-btn.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); +} + +.sync-tab-content { + display: none; + animation: slideDown 0.25s ease; +} + +.sync-tab-content.active { + display: block; +} + +/* Área de Drag & Drop */ +.drag-drop-zone { + border: 3px dashed var(--border-color); + border-radius: var(--radius); + padding: 40px 20px; + text-align: center; + background: #fdfdfd; + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: 20px; +} + +.drag-drop-zone:hover, +.drag-drop-zone.drag-active { + border-color: var(--primary-color); + background: rgba(102, 126, 234, 0.04); +} + +.drag-drop-icon { + font-size: 3rem; + margin-bottom: 12px; +} + +.drag-drop-text { + font-size: 1rem; + color: var(--text-color); + font-weight: 500; + margin-bottom: 6px; +} + +.drag-drop-subtext { + font-size: 0.82rem; + color: var(--text-secondary); +} + +/* Preview de arquivos selecionados */ +.upload-previews { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 10px; + margin-bottom: 20px; +} + +.preview-item { + position: relative; + aspect-ratio: 1; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--border-color); +} + +.preview-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.preview-remove { + position: absolute; + top: 4px; + right: 4px; + background: rgba(220, 53, 69, 0.85); + color: white; + border: none; + border-radius: 50%; + width: 18px; + height: 18px; + font-size: 10px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.preview-remove:hover { + background: #dc3545; +} + +/* Barra de Progresso */ +.upload-progress-container { + background: #f8f9fa; + border-radius: 8px; + padding: 15px; + margin-bottom: 20px; + border: 1px solid var(--border-color); +} + +.progress-bar-label { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + font-weight: 600; + margin-bottom: 8px; + color: var(--dark-color); +} + +.progress-bar-track { + background: #e2e8f0; + border-radius: 10px; + height: 8px; + overflow: hidden; +} + +.progress-bar-fill { + background: linear-gradient(90deg, var(--primary-color) 0%, #764ba2 100%); + height: 100%; + width: 0%; + transition: width 0.2s ease; +} + +/* Lista de dispositivos online */ +.sync-device-list { + display: flex; + flex-direction: column; + gap: 12px; + margin: 20px 0; +} + +.sync-device-card { + background: #f8f9fa; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 15px; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.2s ease; +} + +.sync-device-card:hover { + border-color: var(--primary-color); + background: #fff; + box-shadow: 0 4px 12px rgba(0,0,0,0.03); +} + +.device-card-left { + display: flex; + flex-direction: column; + gap: 4px; +} + +.device-card-title { + font-weight: 700; + font-size: 0.95rem; + color: var(--dark-color); +} + +.device-card-subtitle { + font-size: 0.78rem; + color: var(--text-secondary); +} + +.device-card-badge { + padding: 5px 12px; + border-radius: 20px; + font-size: 0.72rem; + font-weight: 700; +} + +.badge-connected { + background: rgba(81, 207, 102, 0.15); + color: var(--success-color); +} + +.badge-disconnected { + background: rgba(220, 53, 69, 0.1); + color: var(--danger-color); +} + +/* Botão de Excluir Paciente nos cards iniciais */ +.delete-patient-btn { + position: absolute; + top: 10px; + right: 10px; + background: rgba(220, 53, 69, 0.85); + color: white; + border: none; + border-radius: 50%; + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 5; + transition: all 0.2s ease; + font-size: 1.05rem; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); +} + +.delete-patient-btn:hover { + background: #dc3545; + transform: scale(1.1); +} diff --git a/dental-server/routes/images.js b/dental-server/routes/images.js index b35aca6..51ed393 100644 --- a/dental-server/routes/images.js +++ b/dental-server/routes/images.js @@ -4,6 +4,8 @@ const db = require('../database'); const imageProcessor = require('../image-processor'); const path = require('path'); const fs = require('fs').promises; +const storage = require('../storage'); +const sharp = require('sharp'); // ================================================================ // GET /api/images - Listar imagens @@ -64,12 +66,12 @@ router.get('/patients', async (req, res) => { const rows = await db.all( `SELECT patient_name, - doctor, - remark, - client_name, + MAX(doctor) AS doctor, + MAX(remark) AS remark, + MAX(client_name) AS client_name, COUNT(*) AS image_count, MAX(created_at) AS last_date, - (SELECT filename FROM images i2 + (SELECT COALESCE(thumb_filename, filename) FROM images i2 WHERE i2.patient_name = images.patient_name AND i2.enabled = ${enabledVal} ORDER BY i2.created_at DESC LIMIT 1) AS thumb_filename FROM images @@ -114,6 +116,107 @@ router.get('/by-patient', async (req, res) => { } }); +// ================================================================ +// POST /api/images/upload - Fazer upload manual de imagem +// ================================================================ +router.post('/upload', async (req, res) => { + try { + const { imageBase64, filename, patientName, doctor, remark } = req.body; + + if (!imageBase64) { + return res.status(400).json({ error: 'imageBase64 é obrigatório' }); + } + + // Decodificar imagem base64 + let base64Data = imageBase64; + if (base64Data.includes(';base64,')) { + base64Data = base64Data.split(';base64,')[1]; + } + const imageBuffer = Buffer.from(base64Data, 'base64'); + + // TODO(security): Adicionado limite de 10MB para prevenir Buffer Overflow/DoS + if (imageBuffer.length > 10 * 1024 * 1024) { + return res.status(400).json({ error: 'A imagem excede o limite de tamanho permitido de 10MB.' }); + } + + // Validar imagem (garante integridade do arquivo através do sharp) + await imageProcessor.validateImage(imageBuffer); + + // Salvar imagem + const imageGuid = 'web_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + // Detectar formato + const imgMetadata = await sharp(imageBuffer).metadata(); + const format = imgMetadata.format || 'png'; + const ext = format === 'jpeg' ? 'jpg' : format; + const savedFilename = `${Date.now()}_${imageGuid}.${ext}`; + + const clientName = 'Upload Web'; + const patientVal = patientName ? patientName.trim() : 'Paciente não identificado'; + + await storage.saveImage(savedFilename, imageBuffer, clientName, patientVal, false); + + // Gerar 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_${Date.now()}_${imageGuid}.webp`; + await storage.saveImage(thumbFilename, thumbBuffer, clientName, patientVal, false); + console.log(`✅ Thumbnail gerado e salvo para upload manual: ${thumbFilename}`); + } catch (thumbErr) { + console.error('⚠️ Falha ao gerar thumbnail para upload manual:', thumbErr.message); + } + + // Inserir no banco de dados + 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 = formatLocalDateTime(new Date()); + + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + imageGuid, + patientVal, + clientName, + filename || 'upload_manual.png', + savedFilename, + 1, + doctor || null, + remark || null, + thumbFilename, + createdAt + ] + ); + + res.json({ + success: true, + imageId: result.lastID, + filename: savedFilename, + message: 'Imagem enviada com sucesso!' + }); + } catch (error) { + console.error('Erro no upload de imagem manual:', error); + res.status(500).json({ error: error.message }); + } +}); + // ================================================================ // GET /api/images/:id - Buscar imagem por ID // ================================================================ @@ -164,19 +267,13 @@ router.get('/:id/transformations', async (req, res) => { const originalId = image.original_image_id || image.id; const originalImage = await db.get('SELECT * FROM images WHERE id = ?', [originalId]); - const originalPath = path.join(__dirname, '../uploads', originalImage.filename); - let originalBuffer; + let originalBuffer = await storage.getImageBuffer(originalImage.filename, false); + if (!originalBuffer) { + originalBuffer = await storage.getImageBuffer(originalImage.filename, true); + } - try { - originalBuffer = await fs.readFile(originalPath); - } catch (e) { - if (e.code === 'ENOENT') { - // Fallback for images corrupted by previous bug - const fallbackPath = path.join(__dirname, '../processed', originalImage.filename); - originalBuffer = await fs.readFile(fallbackPath); - } else { - throw e; - } + if (!originalBuffer) { + return res.status(404).json({ error: 'Arquivo de imagem original não encontrado' }); } const transformations = [ @@ -228,17 +325,13 @@ router.post('/:id/transform', async (req, res) => { const realOriginal = await db.get('SELECT * FROM images WHERE id = ?', [realOriginalId]); // Processar a imagem - const originalPath = path.join(__dirname, '../uploads', realOriginal.filename); - let originalBuffer; - try { - originalBuffer = await fs.readFile(originalPath); - } catch (e) { - if (e.code === 'ENOENT') { - const fallbackPath = path.join(__dirname, '../processed', realOriginal.filename); - originalBuffer = await fs.readFile(fallbackPath); - } else { - throw e; - } + let originalBuffer = await storage.getImageBuffer(realOriginal.filename, false); + if (!originalBuffer) { + originalBuffer = await storage.getImageBuffer(realOriginal.filename, true); + } + + if (!originalBuffer) { + return res.status(404).json({ error: 'Arquivo de imagem original não encontrado' }); } let processed = await imageProcessor.rotate(originalBuffer, rotation); @@ -247,8 +340,7 @@ router.post('/:id/transform', async (req, res) => { // Salvar nova versão em processed const timestamp = Date.now(); const processedFilename = `${realOriginal.image_guid}_${timestamp}.png`; - const processedPath = path.join(__dirname, '../processed', processedFilename); - await fs.writeFile(processedPath, processed); + await storage.saveImage(processedFilename, processed, realOriginal.client_name, realOriginal.patient_name, true); // Desabilitar a imagem atual para que não fique duplicada na interface await db.run('UPDATE images SET enabled = 0 WHERE id = ?', [req.params.id]); @@ -339,6 +431,37 @@ router.get('/:id/download', async (req, res) => { } }); +// ================================================================ +// DELETE /api/images/by-patient - Apagar todas as imagens de um paciente +// ================================================================ +router.delete('/by-patient', async (req, res) => { + try { + const patientName = req.query.name || ''; + if (!patientName) { + return res.status(400).json({ error: 'Nome do paciente é obrigatório' }); + } + + // Buscar todas as imagens do paciente no banco de dados + const images = await db.all('SELECT filename, client_name, patient_name FROM images WHERE patient_name = ?', [patientName]); + + // Apagar cada imagem do storage (Wasabi S3 + Local) + for (const img of images) { + await storage.deleteImage(img.filename, img.client_name, img.patient_name); + } + + // Apagar os registros do banco de dados + await db.run('DELETE FROM images WHERE patient_name = ?', [patientName]); + + res.json({ + success: true, + message: `Todas as imagens do paciente "${patientName}" foram excluídas com sucesso do servidor e do Wasabi.` + }); + } catch (error) { + console.error('Erro ao excluir imagens do paciente em lote:', error); + res.status(500).json({ error: error.message }); + } +}); + // ================================================================ // DELETE /api/images/:id - Apagar versão da imagem // ================================================================ @@ -357,13 +480,8 @@ router.delete('/:id', async (req, res) => { }); } - // Apagar arquivo - const filePath = path.join(__dirname, '../processed', image.filename); - try { - await fs.unlink(filePath); - } catch (e) { - console.warn('Arquivo não encontrado para deletar:', filePath); - } + // Apagar arquivo do storage (Wasabi S3 + Local) + await storage.deleteImage(image.filename, image.client_name, image.patient_name); // Apagar do banco await db.run('DELETE FROM images WHERE id = ?', [req.params.id]); diff --git a/dental-server/routes/system.js b/dental-server/routes/system.js index 5ad145c..61e728a 100644 --- a/dental-server/routes/system.js +++ b/dental-server/routes/system.js @@ -68,4 +68,29 @@ router.delete('/factory-reset', async (req, res) => { } }); +// ================================================================ +// GET /api/system/storage-config - Obter configurações de storage +// ================================================================ +router.get('/storage-config', (req, res) => { + try { + const storage = require('../storage'); + res.json(storage.getSafeConfig()); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ================================================================ +// POST /api/system/storage-config - Atualizar configurações de storage +// ================================================================ +router.post('/storage-config', async (req, res) => { + try { + const storage = require('../storage'); + const result = await storage.updateConfig(req.body); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/dental-server/server.js b/dental-server/server.js index c9adb94..b370ded 100644 --- a/dental-server/server.js +++ b/dental-server/server.js @@ -5,6 +5,7 @@ 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 @@ -12,6 +13,7 @@ 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'); @@ -401,8 +403,44 @@ app.use('/login', express.static('auth')); // Serve static files app.use(express.static('public')); -app.use('/uploads', express.static(UPLOAD_DIR)); -app.use('/uploads', express.static(PROCESSED_DIR)); + +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) @@ -456,10 +494,29 @@ app.use('/api/images', imageRoutes); app.use('/api/gtos', authenticateToken); app.use('/api/gtos', gtosRouter); -// APIs do Sistema - requerem autenticação 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 // ================================================================ @@ -613,16 +670,37 @@ io.on('connection', (socket) => { // 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}.png`; + const filename = `${timestamp}_${data.metadata.guid}.${ext}`; const filePath = path.join(UPLOAD_DIR, filename); - await fs.writeFile(filePath, imageBuffer); - // 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) => { @@ -643,8 +721,9 @@ io.on('connection', (socket) => { enabled, doctor, remark, + thumb_filename, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ data.metadata.guid, data.patientData.name || 'Paciente não identificado', @@ -654,6 +733,7 @@ io.on('connection', (socket) => { 1, data.patientData.doctor || null, data.patientData.remark || null, + thumbFilename, createdAt ] ); @@ -685,6 +765,7 @@ io.on('connection', (socket) => { doctor: data.patientData.doctor || null, remark: data.patientData.remark || null, filename: filename, + thumb_filename: thumbFilename, created_at: createdAt }); } @@ -812,26 +893,35 @@ async function initDirectories() { } async function start() { + const fsSync = require('fs'); try { // Inicializar diretórios await initDirectories(); - // Tentar inicializar banco apenas se .env já existe - const envPath = path.join(__dirname, '.env'); - const fsSync = require('fs'); - - if (fsSync.existsSync(envPath)) { + // 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 { - // Recarregar .env se foi atualizado - require('dotenv').config(); - - // Inicializar banco de dados apenas se configurações existem - if (process.env.DB_TYPE === 'sqlite' || (process.env.DB_HOST && process.env.DB_NAME)) { - await db.initDatabase(); - console.log('✅ Banco de dados inicializado'); - } + await db.initDatabase(); + console.log('✅ Banco de dados inicializado a partir do ambiente'); + await storage.loadConfigFromDb(); } catch (error) { - console.log('⚠️ Banco de dados não configurado. Execute a instalação em /install'); + 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'); + } } } diff --git a/dental-server/storage.js b/dental-server/storage.js new file mode 100644 index 0000000..50e93a1 --- /dev/null +++ b/dental-server/storage.js @@ -0,0 +1,338 @@ +const { S3Client } = require('@aws-sdk/client-s3'); +const { Upload } = require('@aws-sdk/lib-storage'); +const fs = require('fs').promises; +const fsSync = require('fs'); +const path = require('path'); + +// Configurações das pastas (resolvendo caminhos corretos) +const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads'); +const PROCESSED_DIR = process.env.PROCESSED_DIR || path.join(__dirname, 'processed'); +const CONFIG_FILE = path.join(UPLOAD_DIR, 'wasabi_config.json'); + +let s3 = null; +let currentBucket = null; +let currentConfig = {}; + +// Função para inicializar/reconfigurar o S3 Client +function initS3(cfg = {}) { + const accessKey = cfg.wasabiAccessKey || process.env.WASABI_ACCESS_KEY; + const secretKey = cfg.wasabiSecretKey || process.env.WASABI_SECRET_KEY; + const region = cfg.wasabiRegion || process.env.WASABI_REGION || 'us-east-1'; + const bucket = cfg.wasabiBucket || process.env.WASABI_BUCKET; + const rawEndpoint = cfg.wasabiEndpoint || process.env.WASABI_ENDPOINT || 's3.wasabisys.com'; + const endpoint = rawEndpoint.startsWith('http') ? rawEndpoint : `https://${rawEndpoint}`; + + currentConfig = { + wasabiAccessKey: accessKey || '', + wasabiSecretKey: secretKey || '', + wasabiBucket: bucket || '', + wasabiRegion: region || 'us-east-1', + wasabiEndpoint: rawEndpoint || 's3.wasabisys.com' + }; + + if (accessKey && secretKey && bucket) { + s3 = new S3Client({ + region, + endpoint, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + currentBucket = bucket; + console.log(`✅ [Wasabi] Inicializado com sucesso — Bucket: ${bucket}, Endpoint: ${endpoint}`); + } else { + s3 = null; + currentBucket = null; + console.log('⚠️ [Wasabi] Chaves WASABI_ACCESS_KEY, WASABI_SECRET_KEY ou WASABI_BUCKET não definidas no JSON/Banco/Ambiente. Wasabi desativado.'); + } +} + +// Inicializa a partir do arquivo JSON (se houver) ou das variáveis de ambiente (como fallback inicial síncrono) +function loadConfigSync() { + try { + if (fsSync.existsSync(CONFIG_FILE)) { + const data = fsSync.readFileSync(CONFIG_FILE, 'utf8'); + const cfg = JSON.parse(data); + console.log('📖 [Wasabi] Configuração dinâmica lida de wasabi_config.json'); + initS3(cfg); + } else { + initS3({}); + } + } catch (error) { + console.error('❌ [Wasabi] Erro ao carregar arquivo de config:', error.message); + initS3({}); + } +} + +// Inicializa a partir do Banco de Dados (Chamado de forma assíncrona após db.initDatabase) +async function loadConfigFromDb() { + try { + const db = require('./database'); + const keys = [ + 'wasabi_access_key', + 'wasabi_secret_key', + 'wasabi_bucket', + 'wasabi_region', + 'wasabi_endpoint' + ]; + + const cfg = {}; + let hasSettings = false; + + for (const k of keys) { + const row = await db.get('SELECT value FROM settings WHERE key = ?', [k]); + if (row) { + // Mapear de snake_case do banco para camelCase do config + const camelKey = k.replace(/_([a-z])/g, (g) => g[1].toUpperCase()); + cfg[camelKey] = row.value; + hasSettings = true; + } + } + + if (hasSettings) { + console.log('📖 [Wasabi] Configuração carregada com sucesso do Banco de Dados.'); + initS3(cfg); + } else { + // Se não há settings no banco, tenta carregar do fallback JSON + loadConfigSync(); + } + } catch (error) { + console.error('❌ [Wasabi] Erro ao carregar do banco, usando fallback:', error.message); + loadConfigSync(); + } +} + +// Inicialização imediata de fallback (síncrona) +loadConfigSync(); + +// Retorna a configuração atual de forma segura (mascarando a senha/secret key) +function getSafeConfig() { + return { + wasabiAccessKey: currentConfig.wasabiAccessKey, + wasabiSecretKey: currentConfig.wasabiSecretKey ? '********' : '', + wasabiBucket: currentConfig.wasabiBucket, + wasabiRegion: currentConfig.wasabiRegion, + wasabiEndpoint: currentConfig.wasabiEndpoint, + enabled: !!(s3 && currentBucket) + }; +} + +// Salva e atualiza a configuração (no banco de dados e no arquivo de redundância local) +async function updateConfig(newConfig) { + try { + const db = require('./database'); + + // Se a secretKey vier como asteriscos, mantém a antiga + const finalSecretKey = newConfig.wasabiSecretKey === '********' + ? currentConfig.wasabiSecretKey + : newConfig.wasabiSecretKey; + + const configToSave = { + wasabiAccessKey: newConfig.wasabiAccessKey || '', + wasabiSecretKey: finalSecretKey || '', + wasabiBucket: newConfig.wasabiBucket || '', + wasabiRegion: newConfig.wasabiRegion || 'us-east-1', + wasabiEndpoint: newConfig.wasabiEndpoint || 's3.wasabisys.com' + }; + + // 1. Salva no banco de dados (com suporte a ON CONFLICT UPSERT do PostgreSQL e SQLite) + const settingsMap = { + 'wasabi_access_key': configToSave.wasabiAccessKey, + 'wasabi_secret_key': configToSave.wasabiSecretKey, + 'wasabi_bucket': configToSave.wasabiBucket, + 'wasabi_region': configToSave.wasabiRegion, + 'wasabi_endpoint': configToSave.wasabiEndpoint + }; + + for (const [key, val] of Object.entries(settingsMap)) { + await db.run( + `INSERT INTO settings (key, value) + VALUES (?, ?) + ON CONFLICT (key) + DO UPDATE SET value = EXCLUDED.value`, + [key, val] + ); + } + console.log('💾 [Wasabi] Configurações persistidas no Banco de Dados.'); + + // 2. Salva localmente no arquivo de redundância JSON + try { + await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true }); + await fs.writeFile(CONFIG_FILE, JSON.stringify(configToSave, null, 2), 'utf8'); + console.log('💾 [Wasabi] Cópia de segurança salva em wasabi_config.json'); + } catch (fsErr) { + console.warn('⚠️ [Wasabi] Falha ao gravar cópia em arquivo de config:', fsErr.message); + } + + initS3(configToSave); + return { success: true }; + } catch (error) { + console.error('❌ [Wasabi] Erro ao salvar configurações:', error.message); + throw error; + } +} + +// Higieniza nomes para caminhos do S3 +function sanitizePathSegment(segment) { + if (!segment) return 'desconhecido'; + return segment.trim().replace(/[\/\\?#%*:"<>|]/g, '_'); +} + +// Salva uma imagem (original ou processada) no disco local e envia para o Wasabi +async function saveImage(filename, buffer, clientName, patientName, isProcessed = false) { + const targetDir = isProcessed ? PROCESSED_DIR : UPLOAD_DIR; + const destPath = path.join(targetDir, filename); + + // 1. Salva localmente de forma temporária + await fs.mkdir(path.dirname(destPath), { recursive: true }); + await fs.writeFile(destPath, buffer); + console.log(`💾 [Storage] Imagem salva localmente de forma temporária: ${destPath}`); + + // 2. Envia para o Wasabi se ativo + if (s3 && currentBucket) { + try { + const cleanClient = sanitizePathSegment(clientName); + const cleanPatient = sanitizePathSegment(patientName); + const key = `${cleanClient}/${cleanPatient}/${filename}`; + + 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'; + } + + const upload = new Upload({ + client: s3, + params: { + Bucket: currentBucket, + Key: key, + Body: buffer, + ContentType: contentType, + }, + }); + await upload.done(); + console.log(`☁️ [Wasabi] Upload concluído na pasta do cliente/paciente: ${key}`); + + // Se o upload foi bem-sucedido e o Wasabi está ativo, removemos o arquivo local imediatamente + try { + await fs.unlink(destPath); + console.log(`🗑️ [Storage] Arquivo local temporário removido após upload no Wasabi: ${destPath}`); + } catch (unlinkErr) { + console.warn(`⚠️ [Storage] Falha ao remover arquivo local temporário: ${unlinkErr.message}`); + } + } catch (error) { + console.error(`❌ [Wasabi] Falha no upload de ${filename}:`, error.message); + // Mantemos o arquivo local se o Wasabi falhar como fallback resiliente + } + } +} + +// Obtém o buffer da imagem (lê localmente, e se não existir, tenta baixar do Wasabi) +async function getImageBuffer(filename, clientName = null, patientName = null, isProcessed = false) { + const targetDir = isProcessed ? PROCESSED_DIR : UPLOAD_DIR; + const localPath = path.join(targetDir, filename); + + // 1. Tenta ler local primeiro (para compatibilidade caso o Wasabi não estivesse ativo no salvamento inicial) + if (fsSync.existsSync(localPath)) { + return await fs.readFile(localPath); + } + + // 2. Se não existir localmente, tenta obter do Wasabi + if (s3 && currentBucket) { + try { + let cName = clientName; + let pName = patientName; + + // Se não foram passados, busca metadados no banco + if (!cName || !pName) { + try { + const db = require('./database'); + const row = await db.get('SELECT client_name, patient_name FROM images WHERE filename = ? OR thumb_filename = ? LIMIT 1', [filename, filename]); + if (row) { + cName = row.client_name; + pName = row.patient_name; + } + } catch (dbErr) { + console.error('⚠️ [Storage] Erro ao buscar metadados do banco:', dbErr.message); + } + } + + const cleanClient = sanitizePathSegment(cName); + const cleanPatient = sanitizePathSegment(pName); + const key = `${cleanClient}/${cleanPatient}/${filename}`; + + console.log(`☁️ [Wasabi] Baixando do S3: ${key}`); + const { GetObjectCommand } = require('@aws-sdk/client-s3'); + const response = await s3.send(new GetObjectCommand({ + Bucket: currentBucket, + Key: key + })); + + if (response.Body) { + const bytes = await response.Body.transformToByteArray(); + const buffer = Buffer.from(bytes); + + // TODO(security): Não salvamos no disco rígido local da VPS em cache para preservar privacidade e espaço. + // Apenas retornamos o buffer lido na memória diretamente para o servidor. + console.log(`☁️ [Wasabi] Servindo imagem diretamente do S3 sem gravar em cache local`); + return buffer; + } + } catch (error) { + console.error(`❌ [Wasabi] Falha ao recuperar do S3 para ${filename}:`, error.message); + } + } + + return null; +} + +// Exclui uma imagem (do disco local e do Wasabi S3) +async function deleteImage(filename, clientName, patientName) { + // 1. Excluir localmente da pasta de uploads (imagens originais) e processed (imagens rotacionadas) + const localPaths = [ + path.join(UPLOAD_DIR, filename), + path.join(PROCESSED_DIR, filename) + ]; + + for (const localPath of localPaths) { + try { + // Usando fsSync.existsSync para checagem síncrona segura + if (fsSync.existsSync(localPath)) { + await fs.unlink(localPath); + console.log(`🗑️ [Storage] Arquivo local excluído: ${localPath}`); + } + } catch (err) { + console.warn(`⚠️ [Storage] Falha ao excluir arquivo local ${localPath}:`, err.message); + } + } + + // 2. Excluir do Wasabi S3 se ativo + if (s3 && currentBucket) { + try { + const cleanClient = sanitizePathSegment(clientName); + const cleanPatient = sanitizePathSegment(patientName); + const key = `${cleanClient}/${cleanPatient}/${filename}`; + + const { DeleteObjectCommand } = require('@aws-sdk/client-s3'); + await s3.send(new DeleteObjectCommand({ + Bucket: currentBucket, + Key: key + })); + console.log(`🗑️ [Wasabi] Objeto excluído do S3: ${key}`); + } catch (err) { + console.error(`❌ [Wasabi] Falha ao excluir objeto ${filename} do S3:`, err.message); + } + } +} + +module.exports = { + saveImage, + getImageBuffer, + deleteImage, + getSafeConfig, + updateConfig, + loadConfigFromDb, + isWasabiEnabled: () => !!(s3 && currentBucket) +}; diff --git a/dental-server/test-db-query.js b/dental-server/test-db-query.js new file mode 100644 index 0000000..8ac6055 --- /dev/null +++ b/dental-server/test-db-query.js @@ -0,0 +1,6 @@ +const db = require('./database'); +db.initDatabase().then(async () => { + const row = await db.get('SELECT id, filename, thumb_filename, client_name, patient_name FROM images WHERE id = 533'); + console.log('Result:', row); + process.exit(0); +}); diff --git a/docker-compose.yml b/docker-compose.yml index bd55d4c..df943c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,15 +8,15 @@ services: container_name: rx_scoreodonto_app restart: unless-stopped ports: - - "80:3000" + - "8083:3000" environment: - NODE_ENV=production - PORT=3000 - DB_TYPE=postgres - DB_HOST=10.99.0.3 - DB_PORT=5432 - - DB_USER=postgres - - DB_PASSWORD=sua_senha_segura # Altere no .env ou arquivo local se necessário + - DB_USER=clube67 + - DB_PASSWORD=clube67_db_pass_9903 - DB_NAME=dental_images - UPLOAD_DIR=/app/uploads - PROCESSED_DIR=/app/processed