feat(dashboard): add real system metrics endpoint and fix UI dark-theme issues
- Add /api/system/metrics endpoint reading real data from /proc (CPU, memory, disk, network, load avg, uptime, connections) - Replace hardcoded fake monitoring metrics with live API data - Replace random chart data with real latency/error-rate/client-request charts from DB logs - Fix light-mode colors leaking into dark theme (monitoring stream bg, settings tokens, warning card) - Add 'models' to page title map, fix System Health card structure - Move inline styles to CSS classes (monitoring-layout, monitoring-stream, token-item, warning-card) - Prevent duplicate style injection in monitoring page
This commit is contained in:
@@ -26,32 +26,41 @@ class MonitoringPage {
|
||||
async loadSystemMetrics() {
|
||||
const container = document.getElementById('system-metrics');
|
||||
if (!container) return;
|
||||
|
||||
const metrics = [
|
||||
{ label: 'CPU Usage', value: '24%', trend: 'down', color: 'success' },
|
||||
{ label: 'Memory Usage', value: '1.8 GB', trend: 'stable', color: 'warning' },
|
||||
{ label: 'Disk I/O', value: '45 MB/s', trend: 'up', color: 'primary' },
|
||||
{ label: 'Network', value: '125 KB/s', trend: 'up', color: 'info' },
|
||||
{ label: 'Active Connections', value: '42', trend: 'stable', color: 'success' },
|
||||
{ label: 'Queue Length', value: '3', trend: 'down', color: 'success' }
|
||||
];
|
||||
|
||||
container.innerHTML = metrics.map(metric => `
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">${metric.label}</div>
|
||||
<div class="metric-value">${metric.value}</div>
|
||||
<div class="metric-trend ${metric.trend}">
|
||||
<i class="fas fa-arrow-${metric.trend === 'up' ? 'up' : metric.trend === 'down' ? 'down' : 'minus'}"></i>
|
||||
|
||||
try {
|
||||
const data = await window.api.get('/system/metrics');
|
||||
const metrics = [
|
||||
{ label: 'CPU Usage', value: `${data.cpu.usage_percent}%`, trend: data.cpu.usage_percent > 80 ? 'up' : data.cpu.usage_percent < 30 ? 'down' : 'stable' },
|
||||
{ label: 'Memory', value: `${(data.memory.used_mb / 1024).toFixed(1)} / ${(data.memory.total_mb / 1024).toFixed(1)} GB`, trend: data.memory.usage_percent > 80 ? 'up' : 'stable' },
|
||||
{ label: 'Disk', value: `${data.disk.used_gb.toFixed(1)} / ${data.disk.total_gb.toFixed(1)} GB`, trend: data.disk.usage_percent > 80 ? 'up' : 'stable' },
|
||||
{ label: 'Process RSS', value: `${data.memory.process_rss_mb} MB`, trend: 'stable' },
|
||||
{ label: 'Load Average', value: data.cpu.load_average.map(v => v.toFixed(2)).join(' / '), trend: data.cpu.load_average[0] > 2 ? 'up' : 'down' },
|
||||
{ label: 'Connections', value: `${data.connections.db_active} DB, ${data.connections.websocket_listeners} WS`, trend: 'stable' },
|
||||
];
|
||||
|
||||
container.innerHTML = metrics.map(metric => `
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">${metric.label}</div>
|
||||
<div class="metric-value">${metric.value}</div>
|
||||
<div class="metric-trend ${metric.trend}">
|
||||
<i class="fas fa-arrow-${metric.trend === 'up' ? 'up' : metric.trend === 'down' ? 'down' : 'minus'}"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Error loading system metrics:', error);
|
||||
container.innerHTML = '<div class="metric-item"><div class="metric-label" style="color: var(--danger);">Failed to load metrics</div></div>';
|
||||
}
|
||||
|
||||
// Add CSS for metrics
|
||||
this.addMetricStyles();
|
||||
}
|
||||
|
||||
addMetricStyles() {
|
||||
// Avoid injecting duplicate styles
|
||||
if (document.getElementById('monitoring-metric-styles')) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'monitoring-metric-styles';
|
||||
style.textContent = `
|
||||
.metric-item {
|
||||
display: flex;
|
||||
@@ -100,15 +109,6 @@ class MonitoringPage {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.monitoring-stream {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.5rem;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.stream-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -148,67 +148,19 @@ class MonitoringPage {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.log-stream {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0.5rem;
|
||||
background-color: var(--bg-secondary);
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--text-light);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log-info .log-level {
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.log-warn .log-level {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.log-error .log-level {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.log-debug .log-level {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadCharts() {
|
||||
// Fetch recent logs for chart data
|
||||
try {
|
||||
const logs = await window.api.get('/system/logs');
|
||||
this.recentLogs = Array.isArray(logs) ? logs : [];
|
||||
} catch (error) {
|
||||
console.error('Error loading logs for charts:', error);
|
||||
this.recentLogs = [];
|
||||
}
|
||||
await this.loadResponseTimeChart();
|
||||
await this.loadErrorRateChart();
|
||||
await this.loadRateLimitChart();
|
||||
@@ -216,29 +168,33 @@ class MonitoringPage {
|
||||
|
||||
async loadResponseTimeChart() {
|
||||
try {
|
||||
// Generate demo data for response time
|
||||
const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`);
|
||||
// Bucket recent logs by minute for latency chart
|
||||
const buckets = this.bucketByMinute(this.recentLogs, 20);
|
||||
const labels = buckets.map(b => b.label);
|
||||
const values = buckets.map(b => {
|
||||
if (b.items.length === 0) return 0;
|
||||
const total = b.items.reduce((sum, r) => sum + (r.duration || 0), 0);
|
||||
return Math.round(total / b.items.length);
|
||||
});
|
||||
|
||||
const data = {
|
||||
labels: labels,
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Response Time (ms)',
|
||||
data: labels.map(() => Math.floor(Math.random() * 200) + 300),
|
||||
color: '#3b82f6',
|
||||
label: 'Avg Response Time (ms)',
|
||||
data: values,
|
||||
color: '#83a598',
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
window.chartManager.createLineChart('response-time-chart', data, {
|
||||
scales: {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Milliseconds'
|
||||
}
|
||||
title: { display: true, text: 'Milliseconds' },
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading response time chart:', error);
|
||||
}
|
||||
@@ -246,33 +202,35 @@ class MonitoringPage {
|
||||
|
||||
async loadErrorRateChart() {
|
||||
try {
|
||||
const labels = Array.from({ length: 20 }, (_, i) => `${i + 1}m`);
|
||||
const buckets = this.bucketByMinute(this.recentLogs, 20);
|
||||
const labels = buckets.map(b => b.label);
|
||||
const values = buckets.map(b => {
|
||||
if (b.items.length === 0) return 0;
|
||||
const errors = b.items.filter(r => r.status === 'error').length;
|
||||
return parseFloat((errors / b.items.length * 100).toFixed(1));
|
||||
});
|
||||
|
||||
const data = {
|
||||
labels: labels,
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Error Rate (%)',
|
||||
data: labels.map(() => Math.random() * 5),
|
||||
color: '#ef4444',
|
||||
data: values,
|
||||
color: '#fb4934',
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
window.chartManager.createLineChart('error-rate-chart', data, {
|
||||
scales: {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Percentage'
|
||||
},
|
||||
title: { display: true, text: 'Percentage' },
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
callback: function(value) { return value + '%'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading error rate chart:', error);
|
||||
}
|
||||
@@ -280,37 +238,57 @@ class MonitoringPage {
|
||||
|
||||
async loadRateLimitChart() {
|
||||
try {
|
||||
const labels = ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'];
|
||||
// Show requests-per-client from recent logs
|
||||
const clientCounts = {};
|
||||
for (const log of this.recentLogs) {
|
||||
const client = log.client_id || 'unknown';
|
||||
clientCounts[client] = (clientCounts[client] || 0) + 1;
|
||||
}
|
||||
const sorted = Object.entries(clientCounts).sort((a, b) => b[1] - a[1]).slice(0, 8);
|
||||
const labels = sorted.map(([c]) => c.length > 16 ? c.substring(0, 14) + '...' : c);
|
||||
const values = sorted.map(([, v]) => v);
|
||||
|
||||
const data = {
|
||||
labels: labels,
|
||||
labels: labels.length > 0 ? labels : ['No data'],
|
||||
datasets: [{
|
||||
label: 'Rate Limit Usage',
|
||||
data: [65, 45, 78, 34, 60],
|
||||
color: '#10b981'
|
||||
label: 'Requests by Client',
|
||||
data: values.length > 0 ? values : [0],
|
||||
color: '#8ec07c'
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
window.chartManager.createBarChart('rate-limit-chart', data, {
|
||||
scales: {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Percentage'
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
title: { display: true, text: 'Request Count' },
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading rate limit chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Bucket log entries into N-minute-wide bins ending at now. */
|
||||
bucketByMinute(logs, count) {
|
||||
const now = Date.now();
|
||||
const buckets = Array.from({ length: count }, (_, i) => {
|
||||
const minutesAgo = count - 1 - i;
|
||||
return { label: minutesAgo === 0 ? 'now' : `${minutesAgo}m`, items: [], start: now - (minutesAgo + 1) * 60000, end: now - minutesAgo * 60000 };
|
||||
});
|
||||
for (const log of logs) {
|
||||
const ts = new Date(log.timestamp).getTime();
|
||||
for (const bucket of buckets) {
|
||||
if (ts >= bucket.start && ts < bucket.end) {
|
||||
bucket.items.push(log);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Pause/resume monitoring button
|
||||
const pauseBtn = document.getElementById('pause-monitoring');
|
||||
|
||||
@@ -42,7 +42,7 @@ class SettingsPage {
|
||||
<label>Authentication Tokens</label>
|
||||
<div class="tokens-list" style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem;">
|
||||
${this.settings.server.auth_tokens.map(token => `
|
||||
<div class="token-item" style="display: flex; gap: 0.5rem; align-items: center; background: #f8fafc; padding: 0.5rem; border-radius: 6px; border: 1px solid #e2e8f0;">
|
||||
<div class="token-item">
|
||||
<code style="flex: 1;">${token}</code>
|
||||
<button class="btn-action" title="Copy" onclick="navigator.clipboard.writeText('${token}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
@@ -105,7 +105,7 @@ class SettingsPage {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="border: 1px dashed var(--warning); background: #fffbeb;">
|
||||
<div class="card warning-card">
|
||||
<div class="card-body" style="display: flex; align-items: center; gap: 1rem;">
|
||||
<i class="fas fa-exclamation-triangle" style="font-size: 1.5rem; color: var(--warning);"></i>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user