feat: Implement server and client metrics collection and monitoring
- Added a new PHP script for collecting server metrics every 30 seconds. - Created a ServerMonitoring class to handle metrics collection for CPU, RAM, Disk, and Network. - Introduced database tables for storing server and client metrics. - Updated server view template to display real-time metrics using Chart.js. - Added translations for monitoring UI elements. - Created a new monitoring template for detailed server metrics visualization. - Implemented client speed tracking and display in the monitoring UI.
This commit is contained in:
@@ -1,5 +1,34 @@
|
||||
{% extends "layout.twig" %}
|
||||
{% block title %}{{ t('servers.title') }}{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.server-metrics {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.metric-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.metric-badge i {
|
||||
font-size: 10px;
|
||||
}
|
||||
.metric-sparkline {
|
||||
height: 20px;
|
||||
width: 40px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
@@ -27,12 +56,13 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.name') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.host') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.status') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('common.metrics') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('servers.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for server in servers %}
|
||||
<tr class="border-t">
|
||||
<tr class="border-t" data-server-id="{{ server.id }}">
|
||||
<td class="px-6 py-4 font-medium">{{ server.name }}</td>
|
||||
<td class="px-6 py-4">{{ server.host }}</td>
|
||||
<td class="px-6 py-4">
|
||||
@@ -40,6 +70,30 @@
|
||||
{{ server.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if server.status == 'active' %}
|
||||
<div class="server-metrics" id="metrics-{{ server.id }}">
|
||||
<div class="metric-badge">
|
||||
<i class="fas fa-microchip" style="color:#dc3545"></i>
|
||||
<span class="metric-val" data-metric="cpu">--</span>
|
||||
</div>
|
||||
<div class="metric-badge">
|
||||
<i class="fas fa-memory" style="color:#ffc107"></i>
|
||||
<span class="metric-val" data-metric="ram">--</span>
|
||||
</div>
|
||||
<div class="metric-badge">
|
||||
<i class="fas fa-hdd" style="color:#17a2b8"></i>
|
||||
<span class="metric-val" data-metric="disk">--</span>
|
||||
</div>
|
||||
<div class="metric-badge">
|
||||
<i class="fas fa-network-wired" style="color:#28a745"></i>
|
||||
<span class="metric-val" data-metric="network">--</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-gray-400 text-xs">Not deployed</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 space-x-3">
|
||||
<a href="/servers/{{ server.id }}" class="text-purple-600 hover:text-purple-900">
|
||||
<i class="fas fa-eye mr-1"></i>{{ t('servers.view') }}
|
||||
@@ -64,3 +118,65 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Get all active server IDs
|
||||
const activeServers = [
|
||||
{% for server in servers %}
|
||||
{% if server.status == 'active' %}
|
||||
{{ server.id }},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
async function updateServerMetrics(serverId) {
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${serverId}/metrics?hours=1`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log(`Server ${serverId} metrics:`, data);
|
||||
|
||||
if (data.success && data.metrics.length > 0) {
|
||||
const latest = data.metrics[data.metrics.length - 1];
|
||||
const metricsDiv = document.getElementById(`metrics-${serverId}`);
|
||||
|
||||
console.log(`Server ${serverId} latest:`, latest);
|
||||
|
||||
if (metricsDiv) {
|
||||
const cpuVal = parseFloat(latest.cpu_percent).toFixed(1);
|
||||
metricsDiv.querySelector('[data-metric="cpu"]').textContent = `${cpuVal}%`;
|
||||
|
||||
const ramPercent = (latest.ram_used_mb / latest.ram_total_mb * 100).toFixed(0);
|
||||
metricsDiv.querySelector('[data-metric="ram"]').textContent = `${ramPercent}%`;
|
||||
|
||||
const diskPercent = (latest.disk_used_gb / latest.disk_total_gb * 100).toFixed(0);
|
||||
metricsDiv.querySelector('[data-metric="disk"]').textContent = `${diskPercent}%`;
|
||||
|
||||
const netRx = parseFloat(latest.network_rx_mbps).toFixed(1);
|
||||
const netTx = parseFloat(latest.network_tx_mbps).toFixed(1);
|
||||
metricsDiv.querySelector('[data-metric="network"]').textContent = `↓${netRx}↑${netTx}`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch metrics for server ${serverId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllMetrics() {
|
||||
activeServers.forEach(serverId => {
|
||||
updateServerMetrics(serverId);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load
|
||||
if (activeServers.length > 0) {
|
||||
updateAllMetrics();
|
||||
// Update every 30 seconds
|
||||
setInterval(updateAllMetrics, 30000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}{{ server.name }} - Monitoring{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.metrics-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.metric-card h3 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clients-metrics {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.clients-metrics h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.client-speed-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.client-speeds {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.speed-up {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.speed-down {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.refresh-info {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #e7f3ff;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="max-width: 1400px; margin: 0 auto;">
|
||||
<a href="/servers/{{ server.id }}" class="back-link">← Back to Server</a>
|
||||
|
||||
<h1>{{ server.name }} - Monitoring</h1>
|
||||
|
||||
<div class="refresh-info">
|
||||
Auto-refresh every 30 seconds
|
||||
</div>
|
||||
|
||||
<div class="metrics-container">
|
||||
<div class="metric-card">
|
||||
<h3>
|
||||
CPU Usage
|
||||
<span class="metric-value" id="cpu-current">--</span>
|
||||
</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="cpu-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<h3>
|
||||
RAM Usage
|
||||
<span class="metric-value" id="ram-current">--</span>
|
||||
</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="ram-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<h3>
|
||||
Disk Usage
|
||||
<span class="metric-value" id="disk-current">--</span>
|
||||
</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="disk-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<h3>
|
||||
Network Speed
|
||||
<span class="metric-value" id="network-current">--</span>
|
||||
</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="network-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clients-metrics">
|
||||
<h3>Client Speeds</h3>
|
||||
<div id="clients-list">
|
||||
<div class="no-data">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
const serverId = {{ server.id }};
|
||||
let charts = {};
|
||||
|
||||
// Chart configurations
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'minute',
|
||||
displayFormats: {
|
||||
minute: 'HH:mm'
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize charts
|
||||
function initCharts() {
|
||||
// CPU Chart
|
||||
charts.cpu = new Chart(document.getElementById('cpu-chart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'CPU %',
|
||||
data: [],
|
||||
borderColor: '#dc3545',
|
||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
scales: {
|
||||
...chartOptions.scales,
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// RAM Chart
|
||||
charts.ram = new Chart(document.getElementById('ram-chart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'RAM MB',
|
||||
data: [],
|
||||
borderColor: '#ffc107',
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
|
||||
// Disk Chart
|
||||
charts.disk = new Chart(document.getElementById('disk-chart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Disk GB',
|
||||
data: [],
|
||||
borderColor: '#17a2b8',
|
||||
backgroundColor: 'rgba(23, 162, 184, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
|
||||
// Network Chart
|
||||
charts.network = new Chart(document.getElementById('network-chart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'RX Mbps',
|
||||
data: [],
|
||||
borderColor: '#007bff',
|
||||
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'TX Mbps',
|
||||
data: [],
|
||||
borderColor: '#28a745',
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update server metrics
|
||||
async function updateServerMetrics() {
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${serverId}/metrics?hours=1`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.metrics.length > 0) {
|
||||
const metrics = data.metrics;
|
||||
const latest = metrics[metrics.length - 1];
|
||||
|
||||
// Update current values
|
||||
document.getElementById('cpu-current').textContent = `${latest.cpu_percent}%`;
|
||||
document.getElementById('ram-current').textContent =
|
||||
`${latest.ram_used_mb} / ${latest.ram_total_mb} MB (${Math.round(latest.ram_used_mb / latest.ram_total_mb * 100)}%)`;
|
||||
document.getElementById('disk-current').textContent =
|
||||
`${latest.disk_used_gb} / ${latest.disk_total_gb} GB (${Math.round(latest.disk_used_gb / latest.disk_total_gb * 100)}%)`;
|
||||
document.getElementById('network-current').textContent =
|
||||
`↓ ${latest.network_rx_mbps} Mbps ↑ ${latest.network_tx_mbps} Mbps`;
|
||||
|
||||
// Update charts
|
||||
const timestamps = metrics.map(m => new Date(m.collected_at));
|
||||
|
||||
charts.cpu.data.labels = timestamps;
|
||||
charts.cpu.data.datasets[0].data = metrics.map(m => parseFloat(m.cpu_percent));
|
||||
charts.cpu.update('none');
|
||||
|
||||
charts.ram.data.labels = timestamps;
|
||||
charts.ram.data.datasets[0].data = metrics.map(m => parseInt(m.ram_used_mb));
|
||||
charts.ram.update('none');
|
||||
|
||||
charts.disk.data.labels = timestamps;
|
||||
charts.disk.data.datasets[0].data = metrics.map(m => parseFloat(m.disk_used_gb));
|
||||
charts.disk.update('none');
|
||||
|
||||
charts.network.data.labels = timestamps;
|
||||
charts.network.data.datasets[0].data = metrics.map(m => parseFloat(m.network_rx_mbps));
|
||||
charts.network.data.datasets[1].data = metrics.map(m => parseFloat(m.network_tx_mbps));
|
||||
charts.network.update('none');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch server metrics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update client speeds
|
||||
async function updateClientSpeeds() {
|
||||
try {
|
||||
const clientIds = {{ clients|map(c => c.id)|json_encode|raw }};
|
||||
|
||||
if (clientIds.length === 0) {
|
||||
document.getElementById('clients-list').innerHTML =
|
||||
'<div class="no-data">No active clients</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const clientsData = [];
|
||||
|
||||
for (const clientId of clientIds) {
|
||||
try {
|
||||
const response = await fetch(`/api/clients/${clientId}/metrics?hours=1`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('auth_token')
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.metrics.length > 0) {
|
||||
const latest = data.metrics[data.metrics.length - 1];
|
||||
const client = {{ clients|json_encode|raw }}.find(c => c.id === clientId);
|
||||
|
||||
clientsData.push({
|
||||
name: client.name,
|
||||
speedUp: parseFloat(latest.speed_up_kbps),
|
||||
speedDown: parseFloat(latest.speed_down_kbps)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch client ${clientId} metrics:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by total speed
|
||||
clientsData.sort((a, b) => (b.speedUp + b.speedDown) - (a.speedUp + a.speedDown));
|
||||
|
||||
// Render clients list
|
||||
const html = clientsData.map(c => `
|
||||
<div class="client-speed-item">
|
||||
<span class="client-name">${c.name}</span>
|
||||
<div class="client-speeds">
|
||||
<span class="speed-up">↑ ${c.speedUp.toFixed(2)} Kbps</span>
|
||||
<span class="speed-down">↓ ${c.speedDown.toFixed(2)} Kbps</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('clients-list').innerHTML = html ||
|
||||
'<div class="no-data">No active traffic</div>';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update client speeds:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
initCharts();
|
||||
updateServerMetrics();
|
||||
updateClientSpeeds();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(() => {
|
||||
updateServerMetrics();
|
||||
updateClientSpeeds();
|
||||
}, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
+277
-3
@@ -1,8 +1,50 @@
|
||||
{% extends "layout.twig" %}
|
||||
{% block title %}{{ server.name }}{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.metric-mini {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
.metric-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.metric-icon {
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
min-width: 60px;
|
||||
}
|
||||
.metric-chart {
|
||||
height: 30px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
min-width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
<div class="mb-6"><h1 class="text-3xl font-bold">{{ server.name }}</h1><p class="text-gray-600">{{ server.host }}</p></div>
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold">{{ server.name }}</h1>
|
||||
<p class="text-gray-600">{{ server.host }}</p>
|
||||
</div>
|
||||
|
||||
{% if import_message %}
|
||||
<div class="mb-6 {% if import_message.type == 'success' %}bg-green-50 border-green-400 text-green-700{% else %}bg-red-50 border-red-400 text-red-700{% endif %} border px-4 py-3 rounded">
|
||||
@@ -13,12 +55,41 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-white rounded shadow p-6">
|
||||
<h3 class="font-bold mb-4">Server Info</h3>
|
||||
<h3 class="font-bold mb-4">{{ t('servers.server_info') }}</h3>
|
||||
<dl class="space-y-2">
|
||||
<div><dt class="text-sm text-gray-600">Status</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div>
|
||||
<div><dt class="text-sm text-gray-600">{{ t('common.status') }}</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div>
|
||||
<div><dt class="text-sm text-gray-600">VPN Port</dt><dd>{{ server.vpn_port }}</dd></div>
|
||||
<div><dt class="text-sm text-gray-600">Subnet</dt><dd>{{ server.vpn_subnet }}</dd></div>
|
||||
</dl>
|
||||
|
||||
{% if server.status == 'active' %}
|
||||
<div class="metric-mini" id="serverMetrics">
|
||||
<div class="metric-row">
|
||||
<i class="fas fa-microchip metric-icon" style="color:#dc3545"></i>
|
||||
<span class="metric-label">CPU</span>
|
||||
<canvas id="cpuSparkline" class="metric-chart"></canvas>
|
||||
<div class="metric-value" id="cpuValue">--</div>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<i class="fas fa-memory metric-icon" style="color:#ffc107"></i>
|
||||
<span class="metric-label">RAM</span>
|
||||
<canvas id="ramSparkline" class="metric-chart"></canvas>
|
||||
<div class="metric-value" id="ramValue">--</div>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<i class="fas fa-hdd metric-icon" style="color:#17a2b8"></i>
|
||||
<span class="metric-label">Disk</span>
|
||||
<canvas id="diskSparkline" class="metric-chart"></canvas>
|
||||
<div class="metric-value" id="diskValue">--</div>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<i class="fas fa-network-wired metric-icon" style="color:#28a745"></i>
|
||||
<span class="metric-label">Network</span>
|
||||
<canvas id="networkSparkline" class="metric-chart"></canvas>
|
||||
<div class="metric-value" id="networkValue">--</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="bg-white rounded shadow p-6">
|
||||
<h3 class="font-bold mb-4">{{ t('clients.create') }}</h3>
|
||||
@@ -97,6 +168,7 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.expiration') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.traffic') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.traffic_limit') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('common.speed') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.last_handshake') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.actions') }}</th>
|
||||
</tr>
|
||||
@@ -164,6 +236,9 @@
|
||||
<span class="text-gray-400">{{ t('clients.unlimited') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<div id="client-speed-{{ client.id }}" class="text-gray-400">-</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
{% if client.last_handshake %}
|
||||
<span class="text-gray-600">{{ client.last_handshake }}</span>
|
||||
@@ -431,5 +506,204 @@ async function deleteBackup(backupId) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Server metrics sparklines
|
||||
{% if server.status == 'active' %}
|
||||
const serverId = {{ server.id }};
|
||||
let sparklineCharts = {};
|
||||
|
||||
function initSparklines() {
|
||||
const chartConfig = {
|
||||
type: 'line',
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false }
|
||||
},
|
||||
scales: {
|
||||
x: { display: false },
|
||||
y: { display: false, beginAtZero: true }
|
||||
},
|
||||
elements: {
|
||||
line: { borderWidth: 2, tension: 0.4 },
|
||||
point: { radius: 0 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sparklineCharts.cpu = new Chart(document.getElementById('cpuSparkline'), {
|
||||
...chartConfig,
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
data: [],
|
||||
borderColor: '#dc3545',
|
||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartConfig.options,
|
||||
scales: {
|
||||
...chartConfig.options.scales,
|
||||
y: { display: false, beginAtZero: true, max: 100 }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sparklineCharts.ram = new Chart(document.getElementById('ramSparkline'), {
|
||||
...chartConfig,
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
data: [],
|
||||
borderColor: '#ffc107',
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.1)',
|
||||
fill: true
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
sparklineCharts.disk = new Chart(document.getElementById('diskSparkline'), {
|
||||
...chartConfig,
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
data: [],
|
||||
borderColor: '#17a2b8',
|
||||
backgroundColor: 'rgba(23, 162, 184, 0.1)',
|
||||
fill: true
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
sparklineCharts.network = new Chart(document.getElementById('networkSparkline'), {
|
||||
...chartConfig,
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
data: [],
|
||||
borderColor: '#28a745',
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
||||
fill: true
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updateServerMetrics() {
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${serverId}/metrics?hours=1`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('Metrics response:', data);
|
||||
|
||||
if (data.success && data.metrics.length > 0) {
|
||||
const metrics = data.metrics.slice(-20); // Last 20 points
|
||||
const latest = metrics[metrics.length - 1];
|
||||
|
||||
// Update values with more details
|
||||
const cpuVal = parseFloat(latest.cpu_percent).toFixed(1);
|
||||
document.getElementById('cpuValue').textContent = `${cpuVal}%`;
|
||||
|
||||
const ramUsed = Math.round(latest.ram_used_mb / 1024 * 10) / 10;
|
||||
const ramTotal = Math.round(latest.ram_total_mb / 1024 * 10) / 10;
|
||||
const ramPercent = (latest.ram_used_mb / latest.ram_total_mb * 100).toFixed(0);
|
||||
document.getElementById('ramValue').textContent = `${ramPercent}% (${ramUsed}G)`;
|
||||
|
||||
const diskUsed = parseFloat(latest.disk_used_gb).toFixed(0);
|
||||
const diskTotal = parseFloat(latest.disk_total_gb).toFixed(0);
|
||||
const diskPercent = (latest.disk_used_gb / latest.disk_total_gb * 100).toFixed(0);
|
||||
document.getElementById('diskValue').textContent = `${diskPercent}% (${diskUsed}G)`;
|
||||
|
||||
const netRx = parseFloat(latest.network_rx_mbps).toFixed(1);
|
||||
const netTx = parseFloat(latest.network_tx_mbps).toFixed(1);
|
||||
document.getElementById('networkValue').textContent = `↓${netRx} ↑${netTx}`;
|
||||
|
||||
// Update sparklines
|
||||
const labels = metrics.map((_, i) => i);
|
||||
|
||||
sparklineCharts.cpu.data.labels = labels;
|
||||
sparklineCharts.cpu.data.datasets[0].data = metrics.map(m => parseFloat(m.cpu_percent));
|
||||
sparklineCharts.cpu.update('none');
|
||||
|
||||
sparklineCharts.ram.data.labels = labels;
|
||||
sparklineCharts.ram.data.datasets[0].data = metrics.map(m => (m.ram_used_mb / m.ram_total_mb * 100));
|
||||
sparklineCharts.ram.update('none');
|
||||
|
||||
sparklineCharts.disk.data.labels = labels;
|
||||
sparklineCharts.disk.data.datasets[0].data = metrics.map(m => (m.disk_used_gb / m.disk_total_gb * 100));
|
||||
sparklineCharts.disk.update('none');
|
||||
|
||||
sparklineCharts.network.data.labels = labels;
|
||||
sparklineCharts.network.data.datasets[0].data = metrics.map(m => parseFloat(m.network_rx_mbps) + parseFloat(m.network_tx_mbps));
|
||||
sparklineCharts.network.update('none');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch metrics:', error);
|
||||
// Show error in UI
|
||||
document.getElementById('cpuValue').textContent = 'Error';
|
||||
document.getElementById('ramValue').textContent = 'Error';
|
||||
document.getElementById('diskValue').textContent = 'Error';
|
||||
document.getElementById('networkValue').textContent = 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize sparklines if active
|
||||
if (document.getElementById('cpuSparkline')) {
|
||||
initSparklines();
|
||||
updateServerMetrics();
|
||||
setInterval(updateServerMetrics, 30000); // Update every 30 seconds
|
||||
}
|
||||
|
||||
// Update client speeds
|
||||
async function updateClientSpeeds() {
|
||||
const clientRows = document.querySelectorAll('[id^="client-speed-"]');
|
||||
|
||||
console.log('Found client speed rows:', clientRows.length);
|
||||
|
||||
for (const row of clientRows) {
|
||||
const clientId = row.id.replace('client-speed-', '');
|
||||
|
||||
console.log(`Fetching metrics for client ${clientId}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/clients/${clientId}/metrics?hours=1`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log(`Client ${clientId} metrics:`, data);
|
||||
|
||||
if (data.success && data.metrics && data.metrics.length > 0) {
|
||||
const latest = data.metrics[data.metrics.length - 1];
|
||||
const speedUp = parseFloat(latest.speed_up_kbps).toFixed(1);
|
||||
const speedDown = parseFloat(latest.speed_down_kbps).toFixed(1);
|
||||
|
||||
// Format as compact badge
|
||||
row.innerHTML = `<span class="text-xs text-gray-700">↑${speedUp} ↓${speedDown} KB/s</span>`;
|
||||
} else {
|
||||
console.log(`No metrics for client ${clientId}`);
|
||||
row.innerHTML = '<span class="text-xs text-gray-400">-</span>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch metrics for client ${clientId}:`, error);
|
||||
row.innerHTML = '<span class="text-xs text-gray-400">-</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update client speeds on load and every 30 seconds
|
||||
if (document.querySelector('[id^="client-speed-"]')) {
|
||||
updateClientSpeeds();
|
||||
setInterval(updateClientSpeeds, 30000);
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user