diff --git a/dental-server/Dockerfile b/dental-server/Dockerfile
index f2456f5..e0681f3 100644
--- a/dental-server/Dockerfile
+++ b/dental-server/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:18-alpine
+FROM node:22-alpine
WORKDIR /app
diff --git a/dental-server/app.js b/dental-server/app.js
new file mode 100644
index 0000000..9218c65
--- /dev/null
+++ b/dental-server/app.js
@@ -0,0 +1,1753 @@
+// ================================================================
+// DENTAL IMAGE MANAGER - Main JavaScript
+// Two-Level Navigation: Patients → Images → Transform
+// ================================================================
+
+const API_URL = '/api';
+
+// ================================================================
+// ERROR LOGGER — Logs to console AND shows on screen
+// ================================================================
+const _log = {
+ info: (tag, ...args) => console.log(`%c[${tag}]`, 'color:#667eea;font-weight:bold', ...args),
+ warn: (tag, ...args) => console.warn(`%c[${tag}]`, 'color:#ffa500;font-weight:bold', ...args),
+ error: (tag, ...args) => console.error(`%c[${tag}]`, 'color:#ff4444;font-weight:bold', ...args),
+ group: (tag) => console.group(`%c[${tag}]`, 'color:#667eea;font-weight:bold'),
+ end: () => console.groupEnd()
+};
+
+function showErrorBanner(msg, detail = '') {
+ let banner = document.getElementById('_errBanner');
+ if (!banner) {
+ banner = document.createElement('div');
+ banner.id = '_errBanner';
+ banner.style.cssText = [
+ 'position:fixed','bottom:70px','left:50%','transform:translateX(-50%)',
+ 'background:#c0392b','color:#fff','padding:12px 20px','border-radius:8px',
+ 'z-index:9999','font-size:13px','max-width:90vw','text-align:center',
+ 'box-shadow:0 4px 20px rgba(0,0,0,0.3)','cursor:pointer'
+ ].join(';');
+ banner.onclick = () => banner.remove();
+ document.body.appendChild(banner);
+ }
+ banner.innerHTML = `⚠️ ${msg} ${detail ? `${detail} ` : ''}`;
+ setTimeout(() => banner && banner.remove(), 8000);
+}
+
+function getAuthToken() {
+ return localStorage.getItem('auth_token');
+}
+
+let authVerificationInProgress = false;
+
+async function verifyAuth() {
+ if (authVerificationInProgress) return false;
+ const token = getAuthToken();
+ if (!token) { window.location.href = '/login'; return false; }
+ authVerificationInProgress = true;
+ try {
+ const response = await fetch(`${API_URL}/auth/verify`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ if (!response.ok) { localStorage.removeItem('auth_token'); window.location.href = '/login'; return false; }
+ const data = await response.json();
+ authVerificationInProgress = false;
+ if (data.valid) return true;
+ localStorage.removeItem('auth_token'); window.location.href = '/login'; return false;
+ } catch {
+ localStorage.removeItem('auth_token');
+ authVerificationInProgress = false;
+ window.location.href = '/login'; return false;
+ }
+}
+
+async function fetchWithAuth(url, options = {}) {
+ const token = getAuthToken();
+ _log.info('FETCH', `${options.method || 'GET'} ${url}`);
+ const t0 = Date.now();
+ try {
+ const res = await fetch(url, {
+ ...options,
+ headers: { ...options.headers, 'Authorization': `Bearer ${token}` }
+ });
+ const ms = Date.now() - t0;
+ if (!res.ok) {
+ _log.warn('FETCH', `${options.method || 'GET'} ${url} → ${res.status} (${ms}ms)`);
+ } else {
+ _log.info('FETCH', `${options.method || 'GET'} ${url} → ${res.status} OK (${ms}ms)`);
+ }
+ return res;
+ } catch (err) {
+ const ms = Date.now() - t0;
+ _log.error('FETCH', `${options.method || 'GET'} ${url} → FALHOU após ${ms}ms`, err.message);
+ throw err;
+ }
+}
+
+// ================================================================
+// STATE
+// ================================================================
+
+let socket = null;
+let view = 'patients'; // 'patients' | 'patient-images'
+let patients = [];
+let patientImages = [];
+let selectedPatient = null; // { patient_name, first_name, last_name, ... }
+let selectedImageId = null;
+let clientsList = [];
+let selectedClient = '';
+let showDisabled = false;
+let currentTransformations = [];
+let fineTuneAngle = 0;
+
+// ================================================================
+// DOM REFS
+// ================================================================
+
+const searchInput = document.getElementById('searchInput');
+const clientFilter = document.getElementById('clientFilter');
+const toggleDisabledBtn = document.getElementById('toggleDisabledBtn');
+const toggleText = document.getElementById('toggleText');
+const imagesGrid = document.getElementById('imagesGrid');
+const loadingState = document.getElementById('loadingState');
+const emptyState = document.getElementById('emptyState');
+const transformModal = document.getElementById('transformModal');
+const backBtn = document.getElementById('backBtn');
+const patientHeader = document.getElementById('patientHeader');
+const patientTitle = document.getElementById('patientTitle');
+const patientMeta = document.getElementById('patientMeta');
+
+// ================================================================
+// INIT
+// ================================================================
+
+document.addEventListener('DOMContentLoaded', async () => {
+ _log.info('INIT', 'DOMContentLoaded — iniciando aplicação');
+ try {
+ const ok = await verifyAuth();
+ if (!ok) { _log.warn('AUTH', 'Verificação de auth falhou, redirecionando...'); return; }
+ _log.info('AUTH', 'Autenticação OK');
+
+ const token = getAuthToken();
+ if (token) {
+ _log.info('SOCKET', 'Conectando Socket.IO...');
+ socket = io({ auth: { token } });
+ const debouncedReload = debounce(() => {
+ if (view === 'patients') loadPatients();
+ else loadPatientImages(selectedPatient.patient_name);
+ }, 1200);
+ socket.on('new-image', () => {
+ _log.info('SOCKET', 'Nova imagem recebida via socket');
+ showToast('Nova imagem recebida!', 'info');
+ debouncedReload();
+ });
+ socket.on('clients-list-updated', () => loadClientsList());
+ socket.on('connect', () => _log.info('SOCKET', '✅ Socket.IO conectado, ID:', socket.id));
+ socket.on('disconnect', (r) => _log.warn('SOCKET', '❌ Socket.IO desconectado. Motivo:', r));
+ socket.on('connect_error', (e) => _log.error('SOCKET', 'Erro de conexão:', e.message));
+ }
+
+ await loadClientsList();
+ loadPatients();
+ setupEventListeners();
+ initDragAndDrop();
+ setInterval(loadClientsList, 5000);
+ } catch (error) {
+ _log.error('INIT', 'Erro fatal na inicialização:', error.message, error);
+ showErrorBanner('Erro na inicialização', error.message);
+ localStorage.removeItem('auth_token');
+ window.location.replace('/login');
+ }
+});
+
+// ================================================================
+// EVENT LISTENERS
+// ================================================================
+
+function setupEventListeners() {
+ searchInput.addEventListener('input', debounce(() => {
+ if (view === 'patients') loadPatients();
+ else loadPatientImages(selectedPatient.patient_name);
+ }, 400));
+
+ clientFilter.addEventListener('change', e => {
+ selectedClient = e.target.value;
+ if (view === 'patients') loadPatients();
+ });
+
+ toggleDisabledBtn.addEventListener('click', () => {
+ showDisabled = !showDisabled;
+ toggleText.textContent = showDisabled ? 'Ver Ativas' : 'Ver Ocultas';
+ if (view === 'patients') loadPatients();
+ else loadPatientImages(selectedPatient.patient_name);
+ });
+
+ backBtn.addEventListener('click', () => showPatientsView());
+
+ // Transform modal close on backdrop
+ transformModal.addEventListener('click', e => {
+ if (e.target === transformModal) transformModal.classList.remove('active');
+ });
+ document.getElementById('closeTransformModal').addEventListener('click', () => {
+ transformModal.classList.remove('active');
+ });
+}
+
+// ================================================================
+// CLIENTS LIST
+// ================================================================
+
+async function loadClientsList() {
+ try {
+ const r = await fetch('/api/socket/status');
+ const data = await r.json();
+ clientsList = (data.identified || []).map(c => ({
+ ...c, name: c.name || 'Unknown', displayName: c.name || 'Unknown',
+ dbName: c.name || 'Unknown', status: 'identified'
+ }));
+
+ const imagesRes = await fetchWithAuth('/api/images/patients?disabled=false&search=');
+ if (imagesRes.ok) {
+ const pts = await imagesRes.json();
+ const uniqueClients = [...new Set(pts.map(p => p.client_name).filter(Boolean))];
+ uniqueClients.forEach(cn => {
+ if (!clientsList.find(c => c.dbName === cn || c.name === cn)) {
+ clientsList.push({ name: cn, displayName: cn, dbName: cn, type: 'historic', socketId: null, status: 'historic' });
+ }
+ });
+ }
+ updateClientsDropdown();
+ } catch (e) { console.error('Erro ao carregar clientes:', e); }
+}
+
+function updateClientsDropdown() {
+ const current = clientFilter.value;
+ clientFilter.innerHTML = '📋 Todos os Clientes ';
+ const sorted = [...clientsList].sort((a, b) => {
+ const o = { identified: 1, connected: 2, historic: 3 };
+ return (o[a.status] || 99) - (o[b.status] || 99);
+ });
+ sorted.forEach(c => {
+ const opt = document.createElement('option');
+ opt.value = c.name;
+ let lbl = c.name;
+ 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);
+ });
+ clientFilter.value = current || selectedClient;
+}
+
+// ================================================================
+// VIEW: PATIENTS LIST
+// ================================================================
+
+function showPatientsView() {
+ view = 'patients';
+ selectedPatient = null;
+ backBtn.style.display = 'none';
+ patientHeader.style.display = 'none';
+ loadPatients();
+}
+
+async function loadPatients(silent = false) {
+ _log.group('LOAD_PATIENTS');
+ _log.info('LOAD_PATIENTS', `Iniciando carga. silent=${silent}, showDisabled=${showDisabled}, client="${selectedClient}"`);
+
+ if (!silent) {
+ loadingState.style.display = 'block';
+ imagesGrid.style.display = 'none';
+ emptyState.style.display = 'none';
+ }
+
+ try {
+ const search = searchInput.value.trim();
+ const clientParam = selectedClient ? `&client=${encodeURIComponent(selectedClient)}` : '';
+ const url = `/api/images/patients?disabled=${showDisabled}&search=${encodeURIComponent(search)}${clientParam}`;
+
+ const res = await fetchWithAuth(url);
+
+ if (!res.ok) {
+ let errBody = '';
+ try { errBody = await res.text(); } catch (_) {}
+ _log.error('LOAD_PATIENTS', `HTTP ${res.status} — body:`, errBody);
+ if (res.status === 401 || res.status === 403) {
+ _log.warn('AUTH', 'Token inválido/expirado, redirecionando para login');
+ localStorage.removeItem('auth_token');
+ window.location.href = '/login';
+ return;
+ }
+ throw new Error(`HTTP ${res.status}: ${errBody || 'Erro ao carregar pacientes'}`);
+ }
+
+ patients = await res.json();
+ _log.info('LOAD_PATIENTS', `✅ ${patients.length} paciente(s) recebido(s)`);
+ if (patients.length > 0) {
+ _log.info('LOAD_PATIENTS', 'Primeiro paciente:', {
+ name: patients[0].patient_name,
+ thumb: patients[0].thumb_filename,
+ count: patients[0].image_count
+ });
+ }
+
+ loadingState.style.display = 'none';
+
+ if (patients.length === 0) {
+ _log.warn('LOAD_PATIENTS', 'Nenhum paciente encontrado — exibindo empty state');
+ emptyState.style.display = 'block';
+ imagesGrid.style.display = 'none';
+ } else {
+ emptyState.style.display = 'none';
+ imagesGrid.style.display = 'grid';
+ _log.info('LOAD_PATIENTS', `imagesGrid display=${imagesGrid.style.display}`);
+ renderPatientCards();
+ }
+ } catch (e) {
+ _log.error('LOAD_PATIENTS', '❌ Exceção:', e.message, e);
+ loadingState.style.display = 'none';
+ emptyState.style.display = 'block';
+ showErrorBanner('Erro ao carregar pacientes', e.message);
+ showToast('Erro ao carregar pacientes', 'error');
+ } finally {
+ _log.end();
+ }
+}
+
+function renderPatientCards() {
+ _log.group('RENDER_CARDS');
+ _log.info('RENDER_CARDS', `Renderizando ${patients.length} card(s)...`);
+
+ // Inspecionar o grid antes de renderizar
+ const gridStyle = window.getComputedStyle(imagesGrid);
+ _log.info('RENDER_CARDS', 'imagesGrid computed style:', {
+ display: gridStyle.display,
+ gridTemplateColumns: gridStyle.gridTemplateColumns,
+ width: gridStyle.width,
+ height: gridStyle.height,
+ overflow: gridStyle.overflow
+ });
+
+ imagesGrid.innerHTML = patients.map((p, index) => {
+ const fullName = p.patient_name || 'Sem nome';
+ const thumb = p.thumb_filename ? `/uploads/${p.thumb_filename}` : '';
+ const count = p.image_count || 0;
+ const date = formatDate(p.last_date);
+ const doctor = p.doctor || '—';
+ const remark = p.remark || '—';
+
+ if (index === 0) {
+ _log.info('RENDER_CARDS', 'Card #0 dados:', { fullName, thumb, count, date, doctor });
+ }
+
+ return `
+
+
+
🗑️
+
${count} imagem${count !== 1 ? 'ns' : ''}
+
+
+
${escapeHtml(fullName)}
+
📅 ${date}
+
+
+
`;
+ }).join('');
+
+ // Inspecionar o primeiro card renderizado
+ requestAnimationFrame(() => {
+ const firstCard = imagesGrid.querySelector('.image-card');
+ if (firstCard) {
+ const cs = window.getComputedStyle(firstCard);
+ const preview = firstCard.querySelector('.image-preview');
+ const pcs = preview ? window.getComputedStyle(preview) : null;
+ _log.info('RENDER_CARDS', 'Primeiro card computado:', {
+ 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!');
+ }
+ _log.info('RENDER_CARDS', `Total de cards no DOM: ${imagesGrid.querySelectorAll('.image-card').length}`);
+ _log.end();
+ });
+}
+
+// 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
+// ================================================================
+
+function openPatient(patient) {
+ selectedPatient = patient;
+ view = 'patient-images';
+ backBtn.style.display = 'inline-flex';
+ patientHeader.style.display = 'block';
+
+ const fullName = patient.patient_name || 'Sem nome';
+ patientTitle.textContent = fullName;
+ patientMeta.textContent = `${patient.image_count || 0} imagem(ns) · ${patient.doctor || '—'}`;
+
+ loadPatientImages(patient.patient_name);
+}
+
+async function loadPatientImages(patientName) {
+ _log.group('LOAD_IMAGES');
+ _log.info('LOAD_IMAGES', `Carregando imagens de: "${patientName}"`);
+
+ loadingState.style.display = 'block';
+ imagesGrid.style.display = 'none';
+ emptyState.style.display = 'none';
+
+ try {
+ const url = `/api/images/by-patient?name=${encodeURIComponent(patientName)}&disabled=${showDisabled}`;
+ const res = await fetchWithAuth(url);
+
+ if (!res.ok) {
+ let errBody = '';
+ try { errBody = await res.text(); } catch (_) {}
+ _log.error('LOAD_IMAGES', `HTTP ${res.status}:`, errBody);
+ throw new Error(`HTTP ${res.status}: ${errBody}`);
+ }
+
+ patientImages = await res.json();
+ _log.info('LOAD_IMAGES', `✅ ${patientImages.length} imagem(ns) recebida(s)`);
+ loadingState.style.display = 'none';
+
+ if (patientImages.length === 0) {
+ _log.warn('LOAD_IMAGES', 'Nenhuma imagem encontrada');
+ emptyState.style.display = 'block';
+ imagesGrid.style.display = 'none';
+ } else {
+ emptyState.style.display = 'none';
+ imagesGrid.style.display = 'grid';
+ renderPatientImages();
+ }
+ } catch (e) {
+ _log.error('LOAD_IMAGES', '❌ Exceção:', e.message, e);
+ loadingState.style.display = 'none';
+ emptyState.style.display = 'block';
+ showErrorBanner('Erro ao carregar imagens', e.message);
+ showToast('Erro ao carregar imagens', 'error');
+ } finally {
+ _log.end();
+ }
+}
+
+function renderPatientImages() {
+ 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)}
+
+
+ 🔄 Orientar
+
+
+ ${image.enabled ? '🚫' : '✅'}
+
+
+
+ ${!image.enabled ? '
Desabilitada
' : ''}
+
`;
+ }).join('');
+}
+
+// ================================================================
+// IMAGE CLICK → TRANSFORM MODAL
+// ================================================================
+
+async function handleImageClick(imageId) {
+ selectedImageId = imageId;
+ await showTransformModal(imageId);
+}
+
+async function showTransformModal(imageId) {
+ transformModal.classList.add('active');
+
+ // Default view is 'transform'
+ toggleModalView('transform');
+
+ // Find image to show details
+ const image = patientImages.find(img => img.id === imageId);
+
+ // Check if the image has been modified (has original_image_id)
+ const btnResetImage = document.getElementById('btnResetImage');
+ if (image && image.original_image_id) {
+ btnResetImage.style.display = 'flex';
+ btnResetImage.setAttribute('data-modified', 'true');
+ } else {
+ btnResetImage.style.display = 'none';
+ btnResetImage.setAttribute('data-modified', 'false');
+ }
+
+ if (image) {
+ document.getElementById('transformModalTitle').innerText = `${escapeHtml(image.patient_name || 'Desconhecido')} - ${formatDate(image.created_at)}`;
+
+ document.getElementById('transformExtraInfo').innerHTML = `
+ 👨⚕️ Dentista: ${escapeHtml(image.doctor || 'Não informado')}
+ 📝 Obs: ${escapeHtml(image.remark || 'Nenhuma observação')}
+ `;
+ document.getElementById('transformExtraInfo').style.display = 'none'; // Keep hidden by default
+
+ loadGtosForPatient(image.patient_name);
+ } else {
+ document.getElementById('transformModalTitle').innerText = `Opções da Imagem`;
+ document.getElementById('transformExtraInfo').innerHTML = '';
+ document.getElementById('transformExtraInfo').style.display = 'none';
+ }
+
+ document.getElementById('transformOptions').innerHTML = `
+
+
+
Gerando variações...
+
`;
+
+ try {
+ const image = patientImages.find(img => img.id === imageId);
+ if (!image) {
+ throw new Error('Imagem não encontrada localmente');
+ }
+
+ // 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) => `
+
+ `).join('');
+
+ // ---------------------------------------------------------------
+ // Detectar orientação da imagem original → aplicar layout correto
+ // ---------------------------------------------------------------
+ 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 carregar variações', 'error');
+ transformModal.classList.remove('active');
+ }
+}
+
+
+function selectTransform(index) {
+ const el = document.querySelector(`.transform-option[data-index="${index}"]`);
+ if (el.classList.contains('selected')) {
+ // If clicking the already selected one, clear selection
+ clearTransformSelection();
+ return;
+ }
+
+ const container = document.getElementById('transformOptions');
+ container.classList.add('selection-mode');
+
+ document.querySelectorAll('.transform-option').forEach(option => {
+ option.classList.remove('selected');
+ const optIndex = parseInt(option.getAttribute('data-index'));
+ if (optIndex === 0 || optIndex === index) {
+ option.style.display = 'block';
+ } else {
+ option.style.display = 'none';
+ }
+ });
+
+ el.classList.add('selected');
+
+ // Reset fine tune angle when selecting a new transform
+ fineTuneAngle = 0;
+ updateFineTuneDisplay();
+
+ // Show action area
+ document.getElementById('transformActionArea').style.display = 'block';
+
+ // Pre-fill remark if we have an image
+ const image = patientImages.find(img => img.id === selectedImageId);
+ if (image && !document.getElementById('newImageRemark').value) {
+ document.getElementById('newImageRemark').value = image.remark || '';
+ }
+}
+
+function clearTransformSelection() {
+ const container = document.getElementById('transformOptions');
+ container.classList.remove('selection-mode');
+
+ // 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 = '';
+}
+
+async function saveSelectedTransform() {
+ const selectedEl = document.querySelector('.transform-option.selected');
+ if (!selectedEl) {
+ showToast('Selecione uma orientação primeiro.', 'error');
+ return;
+ }
+
+ const index = parseInt(selectedEl.getAttribute('data-index'));
+ const transform = currentTransformations[index];
+ const remark = document.getElementById('newImageRemark').value.trim();
+
+ const btn = document.getElementById('btnSaveTransform');
+ const originalText = btn.innerText;
+ btn.innerText = 'Salvando...';
+ btn.disabled = true;
+
+ try {
+ const payload = {
+ rotation: transform.rotation + fineTuneAngle,
+ flipH: transform.flipH,
+ flipV: transform.flipV,
+ remark: remark
+ };
+
+ const res = await fetchWithAuth(`/api/images/${selectedImageId}/transform`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ if (!res.ok) throw new Error('Erro ao salvar');
+
+ transformModal.classList.remove('active');
+ showToast('Orientação salva!', 'success');
+ if (selectedPatient) loadPatientImages(selectedPatient.patient_name);
+ } catch (e) {
+ console.error(e);
+ showToast('Erro ao salvar orientação', 'error');
+ }
+}
+
+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' });
+ if (!res.ok) throw new Error();
+ const result = await res.json();
+ showToast(result.enabled ? 'Imagem habilitada!' : 'Imagem desabilitada!', 'success');
+ if (selectedPatient) loadPatientImages(selectedPatient.patient_name);
+ } catch {
+ showToast('Erro ao alterar status', 'error');
+ }
+}
+
+// ================================================================
+// UTILITY FUNCTIONS
+// ================================================================
+
+function escapeHtml(text) {
+ if (!text) return '';
+ const d = document.createElement('div');
+ d.textContent = text;
+ return d.innerHTML;
+}
+
+function formatDate(dateString) {
+ if (!dateString) return '—';
+ const date = new Date(dateString);
+ return date.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
+}
+
+function debounce(func, wait) {
+ let timeout;
+ return function (...args) {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func(...args), wait);
+ };
+}
+
+function showToast(message, type = 'info') {
+ const toast = document.getElementById('toast');
+ toast.textContent = message;
+ toast.className = `toast show ${type}`;
+ setTimeout(() => toast.classList.remove('show'), 3000);
+}
+
+function openPatientByIndex(index) {
+ if (patients[index]) {
+ openPatient(patients[index]);
+ }
+}
+
+window.openPatient = openPatient;
+window.openPatientByIndex = openPatientByIndex;
+window.handleImageClick = handleImageClick;
+window.selectTransform = selectTransform;
+window.toggleImageEnabled = toggleImageEnabled;
+
+// ================================================================
+// PATIENT CREATION (DENTAL SENSOR)
+// ================================================================
+
+function openCreatePatientModal() {
+ document.getElementById('create-patient-modal').classList.add('active');
+}
+
+function closeCreatePatientModal() {
+ document.getElementById('create-patient-modal').classList.remove('active');
+ document.getElementById('create-patient-form').reset();
+}
+
+async function submitCreatePatient(event) {
+ event.preventDefault();
+
+ const submitBtn = event.target.querySelector('button[type="submit"]');
+ const originalText = submitBtn.textContent;
+ submitBtn.textContent = 'Salvando...';
+ submitBtn.disabled = true;
+
+ const payload = {
+ firstName: document.getElementById('patient-first-name').value.trim(),
+ lastName: document.getElementById('patient-last-name').value.trim(),
+ doctor: document.getElementById('patient-doctor').value.trim(),
+ birthday: document.getElementById('patient-birthday').value || null,
+ gender: document.getElementById('patient-gender').value,
+ phone: document.getElementById('patient-phone').value.trim(),
+ remark: document.getElementById('patient-remark').value.trim()
+ };
+
+ try {
+ const response = await fetchWithAuth(`${API_URL}/patients`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+
+ if (response.ok) {
+ showToast('Paciente criado com sucesso no Dental Sensor!', 'success');
+ closeCreatePatientModal();
+ } else {
+ const err = await response.json();
+ alert('Erro ao criar paciente: ' + (err.error || 'Desconhecido'));
+ }
+ } catch (err) {
+ console.error(err);
+ alert('Erro ao conectar com o servidor para criar paciente.');
+ } finally {
+ submitBtn.textContent = originalText;
+ submitBtn.disabled = false;
+ }
+}
+
+// ================================================================
+// GTO LOGIC
+// ================================================================
+
+function toggleModalView(view) {
+ const transformViewWrapper = document.getElementById('transformViewWrapper');
+ const gtoView = document.getElementById('gtoView');
+ const btnShowTransform = document.getElementById('btnShowTransform');
+ const btnShowGto = document.getElementById('btnShowGto');
+ const btnResetImage = document.getElementById('btnResetImage');
+
+ if (view === 'transform') {
+ transformViewWrapper.style.display = 'flex';
+ gtoView.style.display = 'none';
+ btnShowTransform.className = 'btn btn-primary';
+ btnShowGto.className = 'btn btn-secondary';
+
+ // Restore reset button visibility if image is modified
+ if (btnResetImage.getAttribute('data-modified') === 'true') {
+ btnResetImage.style.display = 'flex';
+ }
+ } else {
+ transformViewWrapper.style.display = 'none';
+ gtoView.style.display = 'block';
+ btnShowTransform.className = 'btn btn-secondary';
+ btnShowGto.className = 'btn btn-primary';
+
+ // Hide reset button in GTO view
+ btnResetImage.style.display = 'none';
+ }
+}
+
+function toggleModalInfo() {
+ const infoDiv = document.getElementById('transformExtraInfo');
+ const btnShowInfo = document.getElementById('btnShowInfo');
+ if (infoDiv.style.display === 'none') {
+ infoDiv.style.display = 'block';
+ btnShowInfo.className = 'btn btn-primary';
+ } else {
+ infoDiv.style.display = 'none';
+ btnShowInfo.className = 'btn btn-secondary';
+ }
+}
+
+async function loadGtosForPatient(patientName) {
+ const container = document.getElementById('gtoListContainer');
+ container.innerHTML = 'Carregando GTOs...
';
+
+ try {
+ const res = await fetchWithAuth(`/api/gtos?patient=${encodeURIComponent(patientName)}`);
+ if (!res.ok) throw new Error('Erro ao carregar GTOs');
+ const gtos = await res.json();
+
+ if (gtos.length === 0) {
+ container.innerHTML = 'Nenhuma GTO cadastrada para este paciente.
';
+ return;
+ }
+
+ let html = '';
+ gtos.forEach((gto, idx) => {
+ const bg = idx % 2 === 0 ? '#fff' : '#f9f9f9';
+ html += `
+
+
+
${escapeHtml(gto.gto_number)}
+
${escapeHtml(gto.description || 'Sem descrição')}
+
📅 ${formatDate(gto.created_at)}
+
+
+ ✅ Vincular
+
+
+ `;
+ });
+ html += ' ';
+ container.innerHTML = html;
+
+ } catch (e) {
+ console.error(e);
+ container.innerHTML = 'Erro ao carregar GTOs
';
+ }
+}
+
+async function inlineCreateGto() {
+ const numberInput = document.getElementById('inlineGtoNumber');
+ const descInput = document.getElementById('inlineGtoDesc');
+
+ const number = numberInput.value.trim();
+ if (!number) {
+ showToast('O Número da GTO é obrigatório.', 'error');
+ return;
+ }
+
+ const image = patientImages.find(img => img.id === selectedImageId);
+ if (!image || !image.patient_name) {
+ showToast('Erro: Paciente não identificado.', 'error');
+ return;
+ }
+
+ const payload = {
+ gto_number: number,
+ description: descInput.value.trim(),
+ patient_name: image.patient_name
+ };
+
+ try {
+ const res = await fetchWithAuth('/api/gtos', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+
+ if (!res.ok) throw new Error('Erro ao criar GTO');
+
+ showToast('GTO criada com sucesso!', 'success');
+
+ // Limpar inputs
+ numberInput.value = '';
+ descInput.value = '';
+
+ // Recarregar lista
+ await loadGtosForPatient(image.patient_name);
+
+ } catch (e) {
+ console.error(e);
+ showToast('Erro ao criar GTO.', 'error');
+ }
+}
+
+async function linkImageToGto(gtoId, btnElement) {
+ if (!gtoId) return;
+
+ const originalText = btnElement.innerText;
+ btnElement.innerText = 'Vinculando...';
+ btnElement.disabled = true;
+
+ try {
+ const res = await fetchWithAuth(`/api/gtos/${gtoId}/images`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ image_id: selectedImageId })
+ });
+
+ if (!res.ok) {
+ const err = await res.json();
+ throw new Error(err.error || 'Erro ao vincular');
+ }
+
+ showToast('Imagem vinculada à GTO!', 'success');
+ btnElement.innerText = 'Vinculada!';
+ btnElement.classList.replace('btn-primary', 'btn-success');
+ } catch (e) {
+ console.error(e);
+ showToast(e.message, 'error');
+ btnElement.innerText = originalText;
+ btnElement.disabled = false;
+ }
+}
+
+async function resetImageToOriginal() {
+ if (!confirm('Tem certeza que deseja restaurar esta imagem para o estado original em que foi capturada pelo sensor? Rotações, espelhamentos e observações serão descartados.')) {
+ return;
+ }
+
+ const btn = document.getElementById('btnResetImage');
+ const originalContent = btn.innerHTML;
+ btn.innerHTML = '
';
+ btn.disabled = true;
+
+ try {
+ const payload = {
+ rotation: 0,
+ flipH: false,
+ flipV: false,
+ remark: ''
+ };
+
+ const res = await fetchWithAuth(`/api/images/${selectedImageId}/transform`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+
+ if (!res.ok) throw new Error('Erro ao resetar imagem');
+
+ showToast('Imagem restaurada para o original!', 'success');
+
+ transformModal.classList.remove('active');
+ clearTransformSelection();
+
+ const patientName = patientImages.find(img => img.id === selectedImageId)?.patient_name;
+ await loadPatients();
+ if (patientName) {
+ await loadPatientImages(patientName);
+ }
+
+ } catch (e) {
+ console.error(e);
+ showToast('Erro ao restaurar a imagem.', 'error');
+ } finally {
+ btn.innerHTML = originalContent;
+ btn.disabled = false;
+ }
+}
+
+// Global scope exposures
+window.handleImageClick = handleImageClick;
+window.toggleImageEnabled = toggleImageEnabled;
+window.selectTransform = selectTransform;
+window.openCreatePatientModal = openCreatePatientModal;
+window.closeCreatePatientModal = closeCreatePatientModal;
+window.submitCreatePatient = submitCreatePatient;
+window.toggleModalView = toggleModalView;
+window.toggleModalInfo = toggleModalInfo;
+window.inlineCreateGto = inlineCreateGto;
+window.saveSelectedTransform = saveSelectedTransform;
+window.clearTransformSelection = clearTransformSelection;
+window.resetImageToOriginal = resetImageToOriginal;
+
+// ================================================================
+// CONFIGURAÇÕES (Trocar Usuário/Senha)
+// ================================================================
+
+function openSettingsModal() {
+ const modal = document.getElementById('settingsModal');
+ if (modal) modal.style.display = 'block';
+
+ // Limpar form
+ document.getElementById('currentPassword').value = '';
+ document.getElementById('newUsername').value = '';
+ document.getElementById('newPassword').value = '';
+}
+
+function closeSettingsModal() {
+ const modal = document.getElementById('settingsModal');
+ if (modal) modal.style.display = 'none';
+}
+
+async function updateCredentials(event) {
+ event.preventDefault();
+
+ const currentPassword = document.getElementById('currentPassword').value;
+ const newUsername = document.getElementById('newUsername').value.trim();
+ const newPassword = document.getElementById('newPassword').value;
+
+ if (!currentPassword) {
+ showToast('A senha atual é obrigatória.', 'error');
+ return;
+ }
+
+ if (!newUsername && !newPassword) {
+ showToast('Preencha um novo usuário ou nova senha.', 'error');
+ return;
+ }
+
+ const btn = document.getElementById('btnSaveSettings');
+ const originalText = btn.innerHTML;
+ btn.innerHTML = ' Salvando...';
+ btn.disabled = true;
+
+ try {
+ const token = getAuthToken();
+ const res = await fetch(`${API_URL}/auth/update-credentials`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify({ currentPassword, newUsername, newPassword })
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.error || 'Erro ao atualizar credenciais.');
+ }
+
+ // Sucesso
+ showToast('Credenciais atualizadas com sucesso!', 'success');
+
+ // Atualizar token no localStorage se tiver um novo
+ if (data.token) {
+ localStorage.setItem('auth_token', data.token);
+ }
+
+ closeSettingsModal();
+
+ } catch (e) {
+ console.error(e);
+ showToast(e.message, 'error');
+ } finally {
+ btn.innerHTML = originalText;
+ btn.disabled = false;
+ }
+}
+
+// Global scope
+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/auth\\login.html" "b/dental-server/auth\\login.html"
new file mode 100644
index 0000000..c3c6b52
--- /dev/null
+++ "b/dental-server/auth\\login.html"
@@ -0,0 +1,221 @@
+
+
+
+
+
+ Login - Dental Server
+
+
+
+
+
🦷 Dental Server
+
Área Administrativa
+
+
+
+
+
+
+
+
+
diff --git a/dental-server/database.js b/dental-server/database.js
index 8da67c8..be67ab9 100644
--- a/dental-server/database.js
+++ b/dental-server/database.js
@@ -32,7 +32,7 @@ function rewriteQuery(sql) {
async function initSQLite() {
const { DatabaseSync } = require('node:sqlite');
- const dbPath = path.join(__dirname, 'dental_images.db');
+ const dbPath = process.env.DB_PATH || path.join(__dirname, 'dental_images.db');
sqliteDb = new DatabaseSync(dbPath);
console.log(`✅ Banco de dados SQLite criado/inicializado em: ${dbPath}`);
diff --git a/dental-server/index.html b/dental-server/index.html
new file mode 100644
index 0000000..a36c5ea
--- /dev/null
+++ b/dental-server/index.html
@@ -0,0 +1,466 @@
+
+
+
+
+
+ Gerenciador de Imagens Dentais
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📭
+
Nenhum resultado encontrado
+
As imagens aparecerão aqui quando forem enviadas pelo cliente Windows
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Operação realizada com sucesso!
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 💻 Dispositivos Conectados
+ 📤 Envio Manual
+
+
+
+
+
+ Os computadores clientes com Windows instalados e ativos enviam imagens em tempo real. Você pode solicitar que eles façam uma sincronização manual forçada com o servidor enviando um sinal.
+
+
+
+
+
Buscando dispositivos online...
+
+
+
+
📭
+
Nenhum dispositivo Windows conectado no momento.
+
+
+
+
+
+
+
+ Fechar
+
+ 🔄 Enviar Sinal de Sincronização
+
+
+
+
+
+
+
+
+
+
+
+
Dados do Novo Paciente
+
+
+ Dentista Responsável
+
+
+
+
+
+ Observações / Detalhes (Opcional)
+
+
+
+
+
+
📸
+
Arraste as imagens aqui ou clique para selecionar
+
Suporta arquivos JPG, JPEG e PNG (máx. 10MB por imagem)
+
+
+
+
+
+
+
+
+
+ Enviando imagens...
+ 0%
+
+
+
+
+
+ Cancelar
+ Enviar Imagens
+
+
+
+
+
+
+
+
+
+
+
+
diff --git "a/dental-server/mysql\\database_full.sql" "b/dental-server/mysql\\database_full.sql"
new file mode 100644
index 0000000..7ac3ed6
--- /dev/null
+++ "b/dental-server/mysql\\database_full.sql"
@@ -0,0 +1,104 @@
+/*M!999999\- enable the sandbox mode */
+-- MariaDB dump 10.19 Distrib 10.11.10-MariaDB, for Linux (x86_64)
+--
+-- Host: localhost Database: dental_images
+-- ------------------------------------------------------
+-- Server version 10.11.10-MariaDB-log
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `images`
+--
+
+DROP TABLE IF EXISTS `images`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `images` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `image_guid` varchar(255) NOT NULL,
+ `patient_name` text DEFAULT NULL,
+ `client_name` varchar(100) DEFAULT NULL,
+ `original_filename` text DEFAULT NULL,
+ `filename` text NOT NULL,
+ `enabled` tinyint(1) DEFAULT 1,
+ `tooth_number` varchar(10) DEFAULT NULL,
+ `tooth_side` varchar(10) DEFAULT NULL,
+ `patient_name_resumed` varchar(50) DEFAULT NULL,
+ `original_image_id` int(11) DEFAULT NULL,
+ `rotation` int(11) DEFAULT 0,
+ `flip_horizontal` int(11) DEFAULT 0,
+ `flip_vertical` int(11) DEFAULT 0,
+ `created_at` datetime DEFAULT current_timestamp(),
+ PRIMARY KEY (`id`),
+ KEY `idx_images_enabled` (`enabled`),
+ KEY `idx_images_original` (`original_image_id`),
+ KEY `idx_patient_name` (`patient_name`(100)),
+ KEY `idx_image_guid` (`image_guid`),
+ KEY `idx_created_at` (`created_at`),
+ KEY `idx_client_name` (`client_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `images`
+--
+
+LOCK TABLES `images` WRITE;
+/*!40000 ALTER TABLE `images` DISABLE KEYS */;
+/*!40000 ALTER TABLE `images` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `users`
+--
+
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `users` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `username` varchar(50) NOT NULL,
+ `email` varchar(100) NOT NULL,
+ `password_hash` text NOT NULL,
+ `created_at` datetime DEFAULT current_timestamp(),
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `username` (`username`),
+ UNIQUE KEY `email` (`email`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `users`
+--
+
+LOCK TABLES `users` WRITE;
+/*!40000 ALTER TABLE `users` DISABLE KEYS */;
+INSERT INTO `users` VALUES
+(1,'rcesar','rcesar@rcesar.com','$2b$10$i0zu9BGACsu6TDTsrU1EaOGZkKnvp8zwjOek1W/3m9mP4mzbN8HkC','2025-10-31 07:16:50');
+/*!40000 ALTER TABLE `users` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Dumping routines for database 'dental_images'
+--
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2025-10-31 21:24:39
diff --git "a/dental-server/mysql\\schema.sql" "b/dental-server/mysql\\schema.sql"
new file mode 100644
index 0000000..9ae4caf
--- /dev/null
+++ "b/dental-server/mysql\\schema.sql"
@@ -0,0 +1,84 @@
+/*M!999999\- enable the sandbox mode */
+-- MariaDB dump 10.19 Distrib 10.11.10-MariaDB, for Linux (x86_64)
+--
+-- Host: localhost Database: dental_images
+-- ------------------------------------------------------
+-- Server version 10.11.10-MariaDB-log
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `images`
+--
+
+DROP TABLE IF EXISTS `images`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `images` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `image_guid` varchar(255) NOT NULL,
+ `patient_name` text DEFAULT NULL,
+ `client_name` varchar(100) DEFAULT NULL,
+ `original_filename` text DEFAULT NULL,
+ `filename` text NOT NULL,
+ `enabled` tinyint(1) DEFAULT 1,
+ `tooth_number` varchar(10) DEFAULT NULL,
+ `tooth_side` varchar(10) DEFAULT NULL,
+ `patient_name_resumed` varchar(50) DEFAULT NULL,
+ `original_image_id` int(11) DEFAULT NULL,
+ `rotation` int(11) DEFAULT 0,
+ `flip_horizontal` int(11) DEFAULT 0,
+ `flip_vertical` int(11) DEFAULT 0,
+ `created_at` datetime DEFAULT current_timestamp(),
+ PRIMARY KEY (`id`),
+ KEY `idx_images_enabled` (`enabled`),
+ KEY `idx_images_original` (`original_image_id`),
+ KEY `idx_patient_name` (`patient_name`(100)),
+ KEY `idx_image_guid` (`image_guid`),
+ KEY `idx_created_at` (`created_at`),
+ KEY `idx_client_name` (`client_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Table structure for table `users`
+--
+
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `users` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `username` varchar(50) NOT NULL,
+ `email` varchar(100) NOT NULL,
+ `password_hash` text NOT NULL,
+ `created_at` datetime DEFAULT current_timestamp(),
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `username` (`username`),
+ UNIQUE KEY `email` (`email`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping routines for database 'dental_images'
+--
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2025-10-31 21:24:36
diff --git a/dental-server/server.js b/dental-server/server.js
index 68ae94a..0a6decc 100644
--- a/dental-server/server.js
+++ b/dental-server/server.js
@@ -892,23 +892,23 @@ async function start() {
// Inicializar diretórios
await initDirectories();
- // Tentar inicializar banco apenas se .env já existe
+ // Recarregar .env se existir
const envPath = path.join(__dirname, '.env');
const fsSync = require('fs');
-
if (fsSync.existsSync(envPath)) {
+ require('dotenv').config();
+ }
+
+ // Inicializar banco de dados se configurações existirem
+ 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');
} catch (error) {
- console.log('⚠️ Banco de dados não configurado. Execute a instalação em /install');
+ console.log('⚠️ Erro ao inicializar o banco:', error.message);
}
+ } else {
+ console.log('⚠️ Banco de dados não configurado. Execute a instalação em /install');
}
// Iniciar servidor
diff --git a/docker-compose.yml b/docker-compose.yml
index df943c2..871125e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,8 +12,8 @@ services:
environment:
- NODE_ENV=production
- PORT=3000
- - DB_TYPE=postgres
- - DB_HOST=10.99.0.3
+ - DB_TYPE=sqlite
+ - DB_PATH=/app/data/dental_images.db
- DB_PORT=5432
- DB_USER=clube67
- DB_PASSWORD=clube67_db_pass_9903
@@ -23,6 +23,7 @@ services:
volumes:
- ./uploads:/app/uploads
- ./processed:/app/processed
+ - ./data:/app/data
networks:
default: