Files
rx.scoreodonto.com/dental-server/public/clients.html
T
VPS 4 Builder 6df952acae
continuous-integration/webhook Deploy concluido (rx.scoreodonto.com)
feat: update to v2.0.3, premium redesign and fix navigation bugs
2026-05-26 19:41:32 +02:00

487 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="icon" href="/favicon.svg" type="image/svg+xml">
<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
if (!localStorage.getItem('auth_token')) {
window.location.href = '/login';
}
</script>
</head>
<body class="app-container">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>🦷 DentalSys</h2>
</div>
<nav class="sidebar-nav">
<a href="/" class="nav-item">
<span class="icon">🖼️</span> Pacientes
</a>
<a href="/?action=new-patient" class="nav-item">
<span class="icon"></span> Novo Paciente
</a>
<a href="/clients" class="nav-item active">
<span class="icon">🖥️</span> Dispositivos
</a>
<a href="/?action=settings" class="nav-item">
<span class="icon">⚙️</span> Configurações
</a>
<a href="/?action=plugins" class="nav-item">
<span class="icon">🔌</span> Plugins / Wasabi
</a>
<a href="/?action=sync" class="nav-item">
<span class="icon">🔄</span> Sincronização
</a>
<a href="/reset" class="nav-item text-danger">
<span class="icon">⚠️</span> Zerar Sistema
</a>
<a href="/admin-clinics" class="nav-item">
<span class="icon">🏢</span> Gerenciar Clínicas
</a>
<a href="#" class="nav-item" onclick="logout(); return false;">
<span class="icon">🚪</span> Sair
</a>
</nav>
<div class="sidebar-footer">
<div class="sidebar-brand">
<span>🦷 DentalSys</span>
<span class="version-badge">v2.0.3</span>
</div>
</div>
</aside>
<main class="main-content">
<div class="clients-container">
<div class="clients-header">
<div>
<h1>🖥️ Clientes Conectados</h1>
<p style="color: var(--text-secondary); margin-top: 5px; font-size: 1rem; font-weight: 500;">Monitoramento em tempo real dos clientes Socket.IO</p>
</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 class="stats" id="stats">
<!-- Stats serão carregados aqui -->
</div>
<div class="clients-list-card">
<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() {
try {
const response = await fetch('/api/socket/status');
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.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...';
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'
}
});
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>