feat(dashboard): add real system metrics endpoint and fix UI dark-theme issues
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

- 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:
2026-03-02 10:52:15 -05:00
parent 8613f30c7b
commit d386820d16
6 changed files with 315 additions and 135 deletions

View File

@@ -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');

View File

@@ -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>