Files
rx.scoreodonto.com/dental-server/public_legacy/clients.html
T
VPS 4 Deploy Agent 9786b96fd7
continuous-integration/webhook Deploy concluído (VPS4)
fix(frontend): exibe clínica antes do nome do PC na lista de dispositivos
Item 1 — Clínica antes do PC:
- public_legacy/clients.html: nome exibido como "Clínica — PC" na lista
  de clientes identificados no painel de monitoramento.
- public_legacy/app.js: displayName dos clientes ativos agora inclui
  clinicName como prefixo ("Clínica — PC"); propagado automaticamente
  para o dropdown de filtro e demais usos que iteram clientsList.
  Card de sincronização de dispositivos atualizado da mesma forma.
  Clínicas sem identificação ("Clínica não identificada") exibem só o
  nome do PC (sem prefixo vazio).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 05:12:55 +02:00

544 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clientes Conectados - Dental Server</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap">
<link rel="stylesheet" href="/style.css">
<style>
.clients-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.clients-header {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
padding: 24px 32px;
border-radius: var(--radius-lg);
margin-bottom: 32px;
box-shadow: var(--shadow-md);
display: flex;
justify-content: space-between;
align-items: center;
}
.clients-header h1 {
color: var(--dark-color);
margin-bottom: 4px;
font-size: 1.8rem;
font-weight: 700;
}
.header-actions {
display: flex;
gap: 12px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: var(--bg-surface);
padding: 24px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
border-color: var(--primary-light);
}
.stat-card .label {
color: var(--text-secondary);
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.clients-list-card {
background: var(--bg-surface);
border-radius: var(--radius-lg);
padding: 32px;
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
}
.client-item {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: var(--radius);
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s ease;
background: #fdfdfd;
}
.client-item:hover {
border-color: var(--primary-light);
background: var(--bg-surface);
box-shadow: var(--shadow-sm);
}
.client-info {
flex: 1;
}
.client-name {
font-weight: 700;
font-size: 1.1rem;
color: var(--dark-color);
margin-bottom: 6px;
}
.client-details {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.5;
}
.client-status {
padding: 8px 16px;
border-radius: var(--radius-full);
font-size: 0.85rem;
font-weight: 700;
margin-left: 20px;
box-shadow: var(--shadow-sm);
}
.status-connected {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.status-identified {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
}
.status-unknown {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
}
.status-testing {
background: #e2e8f0;
color: var(--text-primary);
}
.timestamp {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 20px;
text-align: right;
font-weight: 500;
}
.test-result {
margin-top: 12px;
padding: 10px 16px;
border-radius: var(--radius-sm);
font-size: 0.85rem;
font-weight: 600;
}
.test-result.success {
background: rgba(16, 185, 129, 0.1);
color: var(--success-color);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.test-result.error {
background: rgba(239, 68, 68, 0.1);
color: var(--danger-color);
border: 1px solid rgba(239, 68, 68, 0.2);
}
</style>
<script>
// Redirecionar se não estiver autenticado ou não for administrador
if (!localStorage.getItem('auth_token')) {
window.location.replace('/login');
} else if (localStorage.getItem('is_admin') !== 'true') {
window.location.replace('/');
} else {
// Adiciona classe de admin no body para que os estilos de admin sejam exibidos
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('is-admin');
});
}
</script>
<body class="app-container">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo-icon">
<i class="fa-solid fa-tooth"></i>
</div>
<h2>RF Dental <span style="font-size: 10px; opacity: 0.5; font-weight: 500; margin-left: 4px; vertical-align: middle;">v2.1.2</span></h2>
</div>
<nav class="sidebar-nav">
<div class="nav-section-title">Menu Principal</div>
<a href="/" class="nav-item" id="menu-dashboard">
<i class="fa-solid fa-images"></i> Galeria
</a>
<a href="/?action=new-patient" class="nav-item" id="menu-new-patient" onclick="if(window.location.pathname === '/') { openCreatePatientModal(); return false; }">
<i class="fa-solid fa-user-plus"></i> Novo Paciente
</a>
<a href="/?action=sync-upload" class="nav-item" id="menu-upload" onclick="if(window.location.pathname === '/') { openSyncModal('upload'); return false; }">
<i class="fa-solid fa-upload"></i> Enviar Imagens
</a>
<a href="/?action=settings" class="nav-item" id="menu-settings" onclick="if(window.location.pathname === '/') { openSettingsModal(); return false; }">
<i class="fa-solid fa-user-gear"></i> Minha Conta
</a>
<div class="admin-only">
<div class="nav-section-title">Área Administrativa</div>
<a href="/clients" class="nav-item active" id="menu-clients">
<i class="fa-solid fa-network-wired"></i> Conexões
</a>
<a href="/admin-clinics" class="nav-item" id="menu-clinics">
<i class="fa-solid fa-hospital"></i> Clínicas e Acessos
</a>
<a href="/?action=users" class="nav-item" id="menu-users" onclick="if(window.location.pathname === '/') { openUserManagementModal(); return false; }">
<i class="fa-solid fa-users"></i> Gerenciar Usuários
</a>
<a href="/?action=sync-devices" class="nav-item" id="menu-sync" onclick="if(window.location.pathname === '/') { openSyncModal('devices'); return false; }">
<i class="fa-solid fa-rotate"></i> Sincronização
</a>
<a href="/?action=plugins" class="nav-item" id="menu-plugins" onclick="if(window.location.pathname === '/') { openPluginsModal(); return false; }">
<i class="fa-solid fa-plug"></i> Plugins / Wasabi
</a>
<a href="/reset" class="nav-item" id="menu-reset" style="color: #dc2626;">
<i class="fa-solid fa-triangle-exclamation"></i> Zerar Sistema
</a>
</div>
</nav>
<div class="sidebar-footer">
<button id="logoutBtn" class="logout-btn" onclick="logout(); return false;">
<i class="fa-solid fa-arrow-right-from-bracket"></i> Sair
</button>
</div>
</aside>
<main class="main-content">
<div class="clients-container">
<div class="clients-header">
<div>
<h1 style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
🖥️ Clientes Conectados
<span style="font-size: 0.85rem; padding: 6px 12px; border-radius: 20px; background: rgba(79, 70, 229, 0.15); color: var(--primary-color); font-weight: 600;">
Conexões: <span id="stat-total">0</span>
</span>
<span style="font-size: 0.85rem; padding: 6px 12px; border-radius: 20px; background: rgba(16, 185, 129, 0.15); color: var(--success-color); font-weight: 600;">
Identificados: <span id="stat-identified">0</span>
</span>
</h1>
</div>
<div class="header-actions">
<a href="/" class="btn btn-secondary">← Voltar</a>
<button class="btn btn-primary" onclick="loadClients()">🔄 Atualizar</button>
<button class="btn btn-success" onclick="testConnection()" id="testBtn">🧪 Testar Conexão</button>
</div>
</div>
<div id="stats" class="stats"></div>
<div class="clients-list-card" style="margin-top: 24px;">
<div id="clientsList">
<div class="empty-state">
<div class="spinner" style="margin: 0 auto 16px auto;"></div>
Carregando...
</div>
</div>
<div class="timestamp" id="timestamp"></div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
let socket = null;
let testInProgress = false;
let testResults = {};
// Conectar ao Socket.IO
function initSocket() {
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/login';
return;
}
socket = io({ auth: { token } });
socket.on('connect', () => {
console.log('✅ Conectado ao servidor');
});
socket.on('disconnect', () => {
console.log('❌ Desconectado do servidor');
});
// Listener para atualização de lista de clientes
socket.on('clients-list-updated', (clients) => {
console.log('📋 Lista de clientes atualizada:', clients);
loadClients();
});
// Listener para resposta de teste de conexão
socket.on('test-connection-ack', (data) => {
console.log('✅ Teste de conexão respondido:', data);
if (data.testId) {
testResults[data.socketId] = {
success: true,
latency: data.latency || 'N/A',
timestamp: data.timestamp
};
updateTestResults();
}
});
}
async function loadClients() {
const token = localStorage.getItem('auth_token') || '';
try {
const response = await fetch('/api/socket/status', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/login';
return;
}
if (response.status === 403) {
alert('Acesso negado. Esta área é restrita a administradores.');
window.location.href = '/';
return;
}
const data = await response.json();
// Atualizar stats
document.getElementById('stats').innerHTML = `
<div class="stat-card">
<div class="label">Total de Conexões</div>
<div class="value">${data.totalConnections}</div>
</div>
<div class="stat-card">
<div class="label">Clientes Identificados</div>
<div class="value">${data.identifiedClients}</div>
</div>
<div class="stat-card">
<div class="label">Não Identificados</div>
<div class="value">${data.totalConnections - data.identifiedClients}</div>
</div>
`;
// Atualizar lista
if (data.clients.length === 0) {
document.getElementById('clientsList').innerHTML = `
<div class="empty-state">
<h2>📭 Nenhum cliente conectado</h2>
<p>Aguardando conexões...</p>
</div>
`;
} else {
let html = '';
// Mostrar identificados primeiro
data.identified.forEach(client => {
const testResult = testResults[client.socketId];
html += `
<div class="client-item">
<div class="client-info">
<div class="client-name">${client.clinicName && client.clinicName !== 'Clínica não identificada' ? client.clinicName + ' — ' : ''}${client.name || 'Sem nome'}</div>
<div class="client-details">
Tipo: ${client.type} |
Sistema: ${client.system || 'N/A'} |
Versão: ${client.version || 'N/A'} |
ID: ${client.socketId}
</div>
<div class="client-details" style="margin-top: 5px;">
Conectado em: ${new Date(client.connectedAt).toLocaleString('pt-BR')}
</div>
${testResult ? `
<div class="test-result ${testResult.success ? 'success' : 'error'}">
${testResult.success ?
`✅ Teste OK - Latência: ${testResult.latency}ms` :
`${testResult.error || 'Falha no teste'}`
}
</div>
` : ''}
</div>
<div class="client-status ${testInProgress ? 'status-testing' : 'status-identified'}">
${testInProgress ? '🔄 Testando...' : '✅ Identificado'}
</div>
</div>
`;
});
// Mostrar não identificados (como abas do navegador visualizando o painel)
data.clients.filter(c => !c.isIdentified).forEach(client => {
const isLocal = client.remoteAddress === '::1' || client.remoteAddress === '127.0.0.1' || client.remoteAddress === '::ffff:127.0.0.1';
const displayName = isLocal ? 'Painel Web (Navegador Local)' : 'Conexão Web (Painel de Monitoramento)';
const displayStatus = '🌐 Apenas Visualização';
html += `
<div class="client-item">
<div class="client-info">
<div class="client-name">${displayName}</div>
<div class="client-details">
Transport: ${client.transport} |
ID: ${client.id}
</div>
<div class="client-details" style="margin-top: 5px;">
IP: ${client.remoteAddress || 'N/A'}
</div>
</div>
<div class="client-status" style="background: #e2e8f0; color: #4a5568;">
${displayStatus}
</div>
</div>
`;
});
document.getElementById('clientsList').innerHTML = html;
}
// Atualizar timestamp
document.getElementById('timestamp').textContent =
`Última atualização: ${new Date(data.timestamp).toLocaleString('pt-BR')}`;
} catch (error) {
console.error('Erro ao carregar clientes:', error);
document.getElementById('clientsList').innerHTML = `
<div class="empty-state">
<h2>❌ Erro ao carregar</h2>
<p>${error.message}</p>
</div>
`;
}
}
async function testConnection() {
if (testInProgress) {
return;
}
testInProgress = true;
testResults = {};
const testBtn = document.getElementById('testBtn');
testBtn.disabled = true;
testBtn.innerHTML = '<span class="loading"></span> Testando...';
const token = localStorage.getItem('auth_token') || '';
try {
// Limpar resultados anteriores
testResults = {};
loadClients(); // Atualizar UI para mostrar estado de teste
// Chamar endpoint de teste
const response = await fetch('/api/socket/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/login';
return;
}
if (response.status === 403) {
alert('Acesso negado. Esta área é restrita a administradores.');
window.location.href = '/';
return;
}
const result = await response.json();
// Processar resultados
if (result.testResults && result.testResults.length > 0) {
result.testResults.forEach(test => {
testResults[test.socketId] = test;
});
}
// Atualizar interface
updateTestResults();
loadClients();
// Mostrar resumo
const successCount = result.testResults ? result.testResults.filter(r => r.success).length : 0;
const totalCount = result.testResults ? result.testResults.length : 0;
if (totalCount > 0) {
alert(`✅ Teste concluído!\n${successCount} de ${totalCount} clientes responderam com sucesso.`);
} else {
alert('⚠️ Nenhum cliente identificado para testar!');
}
} catch (error) {
console.error('Erro no teste de conexão:', error);
alert('❌ Erro ao executar teste: ' + error.message);
} finally {
testInProgress = false;
testBtn.disabled = false;
testBtn.innerHTML = '🧪 Testar Conexão';
}
}
function updateTestResults() {
loadClients();
}
function logout() {
localStorage.removeItem('auth_token');
window.location.replace('/login');
}
// Inicializar Socket.IO e carregar clientes
initSocket();
loadClients();
// Atualizar a cada 3 segundos
setInterval(loadClients, 3000);
</script>
</main>
</body>
</html>