let metricsChart = null; function initChart(data) { const container = document.getElementById('metricsChart').parentNode; const oldCanvas = document.getElementById('metricsChart'); const newCanvas = document.createElement('canvas'); newCanvas.id = 'metricsChart'; newCanvas.height = 300; container.replaceChild(newCanvas, oldCanvas); const ctx = newCanvas.getContext('2d'); const labels = data.map(m => { const d = new Date(m.created_at); return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0'); }); metricsChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: 'CPU', data: data.map(m => m.cpu_usage), borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, tension: 0.3, pointRadius: 1, }, { label: 'RAM', data: data.map(m => m.ram_usage), borderColor: '#22c55e', backgroundColor: 'rgba(34, 197, 94, 0.1)', fill: true, tension: 0.3, pointRadius: 1, }, { label: 'Disk', data: data.map(m => m.disk_usage), borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.1)', fill: true, tension: 0.3, pointRadius: 1, }, ], }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index', }, plugins: { legend: { labels: { color: '#9ca3af', font: { family: "'Inter', sans-serif" }, boxWidth: 12, padding: 16, }, }, tooltip: { backgroundColor: '#1a1d2b', borderColor: '#2a2d3d', borderWidth: 1, titleColor: '#e1e4ed', bodyColor: '#9ca3af', padding: 10, cornerRadius: 8, callbacks: { label: function(ctx) { return ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(1) + '%'; }, }, }, }, scales: { x: { ticks: { color: '#6b7280', maxTicksLimit: 12, font: { size: 11 }, }, grid: { color: 'rgba(255,255,255,0.04)', }, }, y: { min: 0, max: 100, ticks: { color: '#6b7280', font: { size: 11 }, callback: function(v) { return v + '%'; }, }, grid: { color: 'rgba(255,255,255,0.04)', }, }, }, }, }); } if (historicalData.length > 0) { initChart(historicalData); } let pendingAgentAction = null; function showAgentModal(action) { pendingAgentAction = action; const modal = document.getElementById('agentConfirmModal'); const title = document.getElementById('agentModalTitle'); const msg = document.getElementById('agentModalMessage'); const btn = document.getElementById('agentModalConfirmBtn'); if (action === 'install') { title.textContent = 'Install Monitoring Agent'; msg.innerHTML = 'This will install the monitoring agent on serverName via SSH.'; btn.textContent = 'Install'; btn.className = 'btn btn-primary'; } else { title.textContent = 'Uninstall Monitoring Agent'; msg.innerHTML = 'This will uninstall the monitoring agent from serverName via SSH. The service will be stopped and all agent files removed.'; btn.textContent = 'Uninstall'; btn.className = 'btn btn-danger'; } modal.style.display = 'flex'; modal.onclick = function(e) { if (e.target === modal) closeAgentModal(); }; } function closeAgentModal() { document.getElementById('agentConfirmModal').style.display = 'none'; pendingAgentAction = null; } function confirmAgentAction() { if (!pendingAgentAction) return; const action = pendingAgentAction; const btn = document.getElementById('agentModalConfirmBtn'); btn.disabled = true; btn.textContent = action === 'install' ? 'Installing...' : 'Uninstalling...'; closeAgentModal(); fetch('/servers/' + serverId + '/agent/' + action, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCsrfToken(), }, body: '_csrf_token=' + encodeURIComponent(getCsrfToken()), }) .then(r => r.json()) .then(data => { if (data.success) { showToast('success', action === 'install' ? 'Agent installed successfully.' : 'Agent uninstalled successfully.'); setTimeout(() => location.reload(), 1500); } else { let msg = data.error || (action === 'install' ? 'Installation failed.' : 'Uninstallation failed.'); if (data.output) msg += ' Output: ' + data.output.replace(/\n/g, ' | '); showToast('error', msg); } }) .catch(err => { showToast('error', 'Request failed: ' + err.message); }) .finally(() => { btn.disabled = false; btn.textContent = action === 'install' ? 'Install' : 'Uninstall'; }); } function copyAgentKey() { const key = document.getElementById('agentKeyFull').value; navigator.clipboard.writeText(key).then(() => { showToast('success', 'Agent key copied to clipboard.'); }).catch(() => { showToast('error', 'Failed to copy agent key.'); }); } function regenerateAgentKey() { if (!confirm('Regenerate agent key? The current key will stop working immediately.')) return; fetch('/servers/' + serverId + '/agent/regenerate-key', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCsrfToken(), }, body: '_csrf_token=' + encodeURIComponent(getCsrfToken()), }) .then(r => r.json()) .then(data => { if (data.success) { showToast('success', 'Agent key regenerated.'); setTimeout(() => location.reload(), 1500); } else { showToast('error', data.error || 'Failed to regenerate key.'); } }) .catch(err => showToast('error', 'Request failed: ' + err.message)); } function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content || ''; } function serverAction(action) { if (action === 'shutdown' || action === 'reboot') { if (!confirm('Are you sure you want to ' + action + ' this server?')) return; } fetch('/servers/' + serverId + '/' + action, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCsrfToken(), }, body: '_csrf_token=' + encodeURIComponent(getCsrfToken()), }) .then(r => r.json()) .then(data => { showToast(data.success ? 'success' : 'error', data.message); }) .catch(err => showToast('error', 'Request failed: ' + err.message)); } function restartService() { const service = prompt('Enter service name to restart (e.g., nginx, mysql, apache2):'); if (!service) return; fetch('/servers/' + serverId + '/restart-service', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCsrfToken(), }, body: 'service=' + encodeURIComponent(service) + '&_csrf_token=' + encodeURIComponent(getCsrfToken()), }) .then(r => r.json()) .then(data => { showToast(data.success ? 'success' : 'error', data.message); }) .catch(err => showToast('error', 'Request failed: ' + err.message)); } function refreshMetrics() { fetch('/servers/' + serverId + '/metrics', { headers: { 'Accept': 'application/json' }, }) .then(r => r.json()) .then(data => { if (data.success && data.data) { const m = data.data; document.getElementById('cpuMetric').textContent = (m.cpu_usage || '-') + '%'; document.getElementById('ramMetric').textContent = (m.ram_usage || '-') + '%'; document.getElementById('diskMetric').textContent = (m.disk_usage || '-') + '%'; document.getElementById('loadMetric').textContent = m.load_average || '-'; document.getElementById('uptimeMetric').textContent = m.uptime || '-'; if (metricsChart && metricsChart.data.labels.length > 0) { const now = new Date(); const label = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0'); metricsChart.data.labels.push(label); metricsChart.data.datasets[0].data.push(m.cpu_usage); metricsChart.data.datasets[1].data.push(m.ram_usage); metricsChart.data.datasets[2].data.push(m.disk_usage); if (metricsChart.data.labels.length > 48) { metricsChart.data.labels.shift(); metricsChart.data.datasets.forEach(ds => ds.data.shift()); } metricsChart.update('none'); } } }); } setInterval(refreshMetrics, 30000); function showThresholdModal() { document.getElementById('thresholdModal').style.display = 'flex'; document.getElementById('thresholdModal').onclick = function(e) { if (e.target === this) closeThresholdModal(); }; } function closeThresholdModal() { document.getElementById('thresholdModal').style.display = 'none'; } function saveThresholds() { const form = document.getElementById('thresholdForm'); const data = new URLSearchParams(new FormData(form)); data.append('_csrf_token', getCsrfToken()); document.querySelector('#thresholdModal .btn-primary').disabled = true; fetch('/servers/' + serverId + '/thresholds', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCsrfToken(), }, body: data.toString(), }) .then(r => r.json()) .then(d => { showToast(d.success ? 'success' : 'error', d.message ?? d.error); if (d.success) closeThresholdModal(); }) .catch(e => showToast('error', 'Request failed: ' + e.message)) .finally(() => { document.querySelector('#thresholdModal .btn-primary').disabled = false; }); }