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:
@@ -92,6 +92,7 @@ pub fn router(state: AppState) -> Router {
|
|||||||
)
|
)
|
||||||
.route("/api/providers/{name}/test", post(providers::handle_test_provider))
|
.route("/api/providers/{name}/test", post(providers::handle_test_provider))
|
||||||
.route("/api/system/health", get(system::handle_system_health))
|
.route("/api/system/health", get(system::handle_system_health))
|
||||||
|
.route("/api/system/metrics", get(system::handle_system_metrics))
|
||||||
.route("/api/system/logs", get(system::handle_system_logs))
|
.route("/api/system/logs", get(system::handle_system_logs))
|
||||||
.route("/api/system/backup", post(system::handle_system_backup))
|
.route("/api/system/backup", post(system::handle_system_backup))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ use tracing::warn;
|
|||||||
|
|
||||||
use super::{ApiResponse, DashboardState};
|
use super::{ApiResponse, DashboardState};
|
||||||
|
|
||||||
|
/// Read a value from /proc files, returning None on any failure.
|
||||||
|
fn read_proc_file(path: &str) -> Option<String> {
|
||||||
|
std::fs::read_to_string(path).ok()
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_system_health(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
pub(super) async fn handle_system_health(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
let mut components = HashMap::new();
|
let mut components = HashMap::new();
|
||||||
components.insert("api_server".to_string(), "online".to_string());
|
components.insert("api_server".to_string(), "online".to_string());
|
||||||
@@ -37,8 +42,7 @@ pub(super) async fn handle_system_health(State(state): State<DashboardState>) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read real memory usage from /proc/self/status
|
// Read real memory usage from /proc/self/status
|
||||||
let memory_mb = std::fs::read_to_string("/proc/self/status")
|
let memory_mb = read_proc_file("/proc/self/status")
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.lines().find(|l| l.starts_with("VmRSS:")).map(|l| l.to_string()))
|
.and_then(|s| s.lines().find(|l| l.starts_with("VmRSS:")).map(|l| l.to_string()))
|
||||||
.and_then(|l| l.split_whitespace().nth(1).and_then(|v| v.parse::<f64>().ok()))
|
.and_then(|l| l.split_whitespace().nth(1).and_then(|v| v.parse::<f64>().ok()))
|
||||||
.map(|kb| kb / 1024.0)
|
.map(|kb| kb / 1024.0)
|
||||||
@@ -60,6 +64,162 @@ pub(super) async fn handle_system_health(State(state): State<DashboardState>) ->
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Real system metrics from /proc (Linux only).
|
||||||
|
pub(super) async fn handle_system_metrics(
|
||||||
|
State(state): State<DashboardState>,
|
||||||
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
// --- CPU usage (aggregate across all cores) ---
|
||||||
|
// /proc/stat first line: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||||
|
let cpu_percent = read_proc_file("/proc/stat")
|
||||||
|
.and_then(|s| {
|
||||||
|
let line = s.lines().find(|l| l.starts_with("cpu "))?.to_string();
|
||||||
|
let fields: Vec<u64> = line
|
||||||
|
.split_whitespace()
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|v| v.parse().ok())
|
||||||
|
.collect();
|
||||||
|
if fields.len() >= 4 {
|
||||||
|
let idle = fields[3];
|
||||||
|
let total: u64 = fields.iter().sum();
|
||||||
|
if total > 0 {
|
||||||
|
Some(((total - idle) as f64 / total as f64 * 100.0 * 10.0).round() / 10.0)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
// --- Memory (system-wide from /proc/meminfo) ---
|
||||||
|
let meminfo = read_proc_file("/proc/meminfo").unwrap_or_default();
|
||||||
|
let parse_meminfo = |key: &str| -> u64 {
|
||||||
|
meminfo
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.starts_with(key))
|
||||||
|
.and_then(|l| l.split_whitespace().nth(1))
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.unwrap_or(0)
|
||||||
|
};
|
||||||
|
let mem_total_kb = parse_meminfo("MemTotal:");
|
||||||
|
let mem_available_kb = parse_meminfo("MemAvailable:");
|
||||||
|
let mem_used_kb = mem_total_kb.saturating_sub(mem_available_kb);
|
||||||
|
let mem_total_mb = mem_total_kb as f64 / 1024.0;
|
||||||
|
let mem_used_mb = mem_used_kb as f64 / 1024.0;
|
||||||
|
let mem_percent = if mem_total_kb > 0 {
|
||||||
|
(mem_used_kb as f64 / mem_total_kb as f64 * 100.0 * 10.0).round() / 10.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Process-specific memory (VmRSS) ---
|
||||||
|
let process_rss_mb = read_proc_file("/proc/self/status")
|
||||||
|
.and_then(|s| s.lines().find(|l| l.starts_with("VmRSS:")).map(|l| l.to_string()))
|
||||||
|
.and_then(|l| l.split_whitespace().nth(1).and_then(|v| v.parse::<f64>().ok()))
|
||||||
|
.map(|kb| (kb / 1024.0 * 10.0).round() / 10.0)
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
// --- Disk usage of the data directory ---
|
||||||
|
let (disk_total_gb, disk_used_gb, disk_percent) = {
|
||||||
|
// statvfs via libc would be ideal; use df as a simple fallback
|
||||||
|
std::process::Command::new("df")
|
||||||
|
.args(["-BM", "--output=size,used,pcent", "."])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| {
|
||||||
|
let out = String::from_utf8_lossy(&o.stdout);
|
||||||
|
let line = out.lines().nth(1)?.to_string();
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
let total = parts[0].trim_end_matches('M').parse::<f64>().unwrap_or(0.0) / 1024.0;
|
||||||
|
let used = parts[1].trim_end_matches('M').parse::<f64>().unwrap_or(0.0) / 1024.0;
|
||||||
|
let pct = parts[2].trim_end_matches('%').parse::<f64>().unwrap_or(0.0);
|
||||||
|
Some(((total * 10.0).round() / 10.0, (used * 10.0).round() / 10.0, pct))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or((0.0, 0.0, 0.0))
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Uptime ---
|
||||||
|
let uptime_seconds = read_proc_file("/proc/uptime")
|
||||||
|
.and_then(|s| s.split_whitespace().next().and_then(|v| v.parse::<f64>().ok()))
|
||||||
|
.unwrap_or(0.0) as u64;
|
||||||
|
|
||||||
|
// --- Load average ---
|
||||||
|
let (load_1, load_5, load_15) = read_proc_file("/proc/loadavg")
|
||||||
|
.and_then(|s| {
|
||||||
|
let parts: Vec<&str> = s.split_whitespace().collect();
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
Some((
|
||||||
|
parts[0].parse::<f64>().unwrap_or(0.0),
|
||||||
|
parts[1].parse::<f64>().unwrap_or(0.0),
|
||||||
|
parts[2].parse::<f64>().unwrap_or(0.0),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or((0.0, 0.0, 0.0));
|
||||||
|
|
||||||
|
// --- Network (from /proc/net/dev, aggregate non-lo interfaces) ---
|
||||||
|
let (net_rx_bytes, net_tx_bytes) = read_proc_file("/proc/net/dev")
|
||||||
|
.map(|s| {
|
||||||
|
s.lines()
|
||||||
|
.skip(2) // skip header lines
|
||||||
|
.filter(|l| !l.trim().starts_with("lo:"))
|
||||||
|
.fold((0u64, 0u64), |(rx, tx), line| {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() >= 10 {
|
||||||
|
let r = parts[1].parse::<u64>().unwrap_or(0);
|
||||||
|
let t = parts[9].parse::<u64>().unwrap_or(0);
|
||||||
|
(rx + r, tx + t)
|
||||||
|
} else {
|
||||||
|
(rx, tx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
|
||||||
|
// --- Database pool ---
|
||||||
|
let db_pool_size = state.app_state.db_pool.size();
|
||||||
|
let db_pool_idle = state.app_state.db_pool.num_idle();
|
||||||
|
|
||||||
|
// --- Active WebSocket listeners ---
|
||||||
|
let ws_listeners = state.app_state.dashboard_tx.receiver_count();
|
||||||
|
|
||||||
|
Json(ApiResponse::success(serde_json::json!({
|
||||||
|
"cpu": {
|
||||||
|
"usage_percent": cpu_percent,
|
||||||
|
"load_average": [load_1, load_5, load_15],
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"total_mb": (mem_total_mb * 10.0).round() / 10.0,
|
||||||
|
"used_mb": (mem_used_mb * 10.0).round() / 10.0,
|
||||||
|
"usage_percent": mem_percent,
|
||||||
|
"process_rss_mb": process_rss_mb,
|
||||||
|
},
|
||||||
|
"disk": {
|
||||||
|
"total_gb": disk_total_gb,
|
||||||
|
"used_gb": disk_used_gb,
|
||||||
|
"usage_percent": disk_percent,
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"rx_bytes": net_rx_bytes,
|
||||||
|
"tx_bytes": net_tx_bytes,
|
||||||
|
},
|
||||||
|
"uptime_seconds": uptime_seconds,
|
||||||
|
"connections": {
|
||||||
|
"db_active": db_pool_size - db_pool_idle as u32,
|
||||||
|
"db_idle": db_pool_idle,
|
||||||
|
"websocket_listeners": ws_listeners,
|
||||||
|
},
|
||||||
|
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_system_logs(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
pub(super) async fn handle_system_logs(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
|
|
||||||
|
|||||||
@@ -1120,3 +1120,41 @@ body {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--bg4);
|
background: var(--bg4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Monitoring Layout */
|
||||||
|
.monitoring-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-stream {
|
||||||
|
height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-charts {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings: Token Items */
|
||||||
|
.token-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg0);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--bg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings: Warning Card */
|
||||||
|
.warning-card {
|
||||||
|
border: 1px dashed var(--warning);
|
||||||
|
background: rgba(215, 153, 33, 0.08);
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ class Dashboard {
|
|||||||
'providers': 'Providers',
|
'providers': 'Providers',
|
||||||
'monitoring': 'Monitoring',
|
'monitoring': 'Monitoring',
|
||||||
'settings': 'Settings',
|
'settings': 'Settings',
|
||||||
'logs': 'Logs'
|
'logs': 'Logs',
|
||||||
|
'models': 'Models'
|
||||||
};
|
};
|
||||||
if (titleElement) titleElement.textContent = titles[page] || 'Dashboard';
|
if (titleElement) titleElement.textContent = titles[page] || 'Dashboard';
|
||||||
|
|
||||||
@@ -193,8 +194,10 @@ class Dashboard {
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-title">System Health</h3>
|
<div class="card-header">
|
||||||
<div id="system-health" style="margin-top: 1rem;"></div>
|
<h3 class="card-title">System Health</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="system-health"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -400,10 +403,10 @@ class Dashboard {
|
|||||||
<i class="fas fa-pause"></i> Pause Stream
|
<i class="fas fa-pause"></i> Pause Stream
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-2" style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
|
<div class="monitoring-layout">
|
||||||
<div>
|
<div>
|
||||||
<h4>Incoming Requests</h4>
|
<h4>Incoming Requests</h4>
|
||||||
<div id="request-stream" class="monitoring-stream" style="height: 400px; overflow-y: auto; background: #f8fafc; border-radius: 8px; margin-top: 1rem; padding: 1rem;"></div>
|
<div id="request-stream" class="monitoring-stream"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4>System Performance</h4>
|
<h4>System Performance</h4>
|
||||||
@@ -411,7 +414,7 @@ class Dashboard {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-3" style="margin-top: 1.5rem;">
|
<div class="grid-3 monitoring-charts">
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<h3 class="card-title">Latency (ms)</h3>
|
<h3 class="card-title">Latency (ms)</h3>
|
||||||
<canvas id="response-time-chart" height="200"></canvas>
|
<canvas id="response-time-chart" height="200"></canvas>
|
||||||
|
|||||||
@@ -27,31 +27,40 @@ class MonitoringPage {
|
|||||||
const container = document.getElementById('system-metrics');
|
const container = document.getElementById('system-metrics');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const metrics = [
|
try {
|
||||||
{ label: 'CPU Usage', value: '24%', trend: 'down', color: 'success' },
|
const data = await window.api.get('/system/metrics');
|
||||||
{ label: 'Memory Usage', value: '1.8 GB', trend: 'stable', color: 'warning' },
|
const metrics = [
|
||||||
{ label: 'Disk I/O', value: '45 MB/s', trend: 'up', color: 'primary' },
|
{ label: 'CPU Usage', value: `${data.cpu.usage_percent}%`, trend: data.cpu.usage_percent > 80 ? 'up' : data.cpu.usage_percent < 30 ? 'down' : 'stable' },
|
||||||
{ label: 'Network', value: '125 KB/s', trend: 'up', color: 'info' },
|
{ 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: 'Active Connections', value: '42', trend: 'stable', color: 'success' },
|
{ 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: 'Queue Length', value: '3', trend: 'down', color: 'success' }
|
{ 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 => `
|
container.innerHTML = metrics.map(metric => `
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<div class="metric-label">${metric.label}</div>
|
<div class="metric-label">${metric.label}</div>
|
||||||
<div class="metric-value">${metric.value}</div>
|
<div class="metric-value">${metric.value}</div>
|
||||||
<div class="metric-trend ${metric.trend}">
|
<div class="metric-trend ${metric.trend}">
|
||||||
<i class="fas fa-arrow-${metric.trend === 'up' ? 'up' : metric.trend === 'down' ? 'down' : 'minus'}"></i>
|
<i class="fas fa-arrow-${metric.trend === 'up' ? 'up' : metric.trend === 'down' ? 'down' : 'minus'}"></i>
|
||||||
|
</div>
|
||||||
</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
|
// Add CSS for metrics
|
||||||
this.addMetricStyles();
|
this.addMetricStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
addMetricStyles() {
|
addMetricStyles() {
|
||||||
|
// Avoid injecting duplicate styles
|
||||||
|
if (document.getElementById('monitoring-metric-styles')) return;
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
|
style.id = 'monitoring-metric-styles';
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.metric-item {
|
.metric-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -100,15 +109,6 @@ class MonitoringPage {
|
|||||||
color: var(--text-secondary);
|
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 {
|
.stream-entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -148,67 +148,19 @@ class MonitoringPage {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-top: 0.125rem;
|
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);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadCharts() {
|
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.loadResponseTimeChart();
|
||||||
await this.loadErrorRateChart();
|
await this.loadErrorRateChart();
|
||||||
await this.loadRateLimitChart();
|
await this.loadRateLimitChart();
|
||||||
@@ -216,14 +168,21 @@ class MonitoringPage {
|
|||||||
|
|
||||||
async loadResponseTimeChart() {
|
async loadResponseTimeChart() {
|
||||||
try {
|
try {
|
||||||
// Generate demo data for response time
|
// Bucket recent logs by minute for latency chart
|
||||||
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 total = b.items.reduce((sum, r) => sum + (r.duration || 0), 0);
|
||||||
|
return Math.round(total / b.items.length);
|
||||||
|
});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: labels,
|
labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Response Time (ms)',
|
label: 'Avg Response Time (ms)',
|
||||||
data: labels.map(() => Math.floor(Math.random() * 200) + 300),
|
data: values,
|
||||||
color: '#3b82f6',
|
color: '#83a598',
|
||||||
fill: true
|
fill: true
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -231,14 +190,11 @@ class MonitoringPage {
|
|||||||
window.chartManager.createLineChart('response-time-chart', data, {
|
window.chartManager.createLineChart('response-time-chart', data, {
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
title: {
|
title: { display: true, text: 'Milliseconds' },
|
||||||
display: true,
|
beginAtZero: true
|
||||||
text: 'Milliseconds'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading response time chart:', error);
|
console.error('Error loading response time chart:', error);
|
||||||
}
|
}
|
||||||
@@ -246,13 +202,20 @@ class MonitoringPage {
|
|||||||
|
|
||||||
async loadErrorRateChart() {
|
async loadErrorRateChart() {
|
||||||
try {
|
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 = {
|
const data = {
|
||||||
labels: labels,
|
labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Error Rate (%)',
|
label: 'Error Rate (%)',
|
||||||
data: labels.map(() => Math.random() * 5),
|
data: values,
|
||||||
color: '#ef4444',
|
color: '#fb4934',
|
||||||
fill: true
|
fill: true
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -260,19 +223,14 @@ class MonitoringPage {
|
|||||||
window.chartManager.createLineChart('error-rate-chart', data, {
|
window.chartManager.createLineChart('error-rate-chart', data, {
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
title: {
|
title: { display: true, text: 'Percentage' },
|
||||||
display: true,
|
beginAtZero: true,
|
||||||
text: 'Percentage'
|
|
||||||
},
|
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function(value) {
|
callback: function(value) { return value + '%'; }
|
||||||
return value + '%';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading error rate chart:', error);
|
console.error('Error loading error rate chart:', error);
|
||||||
}
|
}
|
||||||
@@ -280,37 +238,57 @@ class MonitoringPage {
|
|||||||
|
|
||||||
async loadRateLimitChart() {
|
async loadRateLimitChart() {
|
||||||
try {
|
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 = {
|
const data = {
|
||||||
labels: labels,
|
labels: labels.length > 0 ? labels : ['No data'],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Rate Limit Usage',
|
label: 'Requests by Client',
|
||||||
data: [65, 45, 78, 34, 60],
|
data: values.length > 0 ? values : [0],
|
||||||
color: '#10b981'
|
color: '#8ec07c'
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
window.chartManager.createBarChart('rate-limit-chart', data, {
|
window.chartManager.createBarChart('rate-limit-chart', data, {
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
title: {
|
title: { display: true, text: 'Request Count' },
|
||||||
display: true,
|
beginAtZero: true
|
||||||
text: 'Percentage'
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
callback: function(value) {
|
|
||||||
return value + '%';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading rate limit chart:', 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() {
|
setupEventListeners() {
|
||||||
// Pause/resume monitoring button
|
// Pause/resume monitoring button
|
||||||
const pauseBtn = document.getElementById('pause-monitoring');
|
const pauseBtn = document.getElementById('pause-monitoring');
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class SettingsPage {
|
|||||||
<label>Authentication Tokens</label>
|
<label>Authentication Tokens</label>
|
||||||
<div class="tokens-list" style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem;">
|
<div class="tokens-list" style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem;">
|
||||||
${this.settings.server.auth_tokens.map(token => `
|
${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>
|
<code style="flex: 1;">${token}</code>
|
||||||
<button class="btn-action" title="Copy" onclick="navigator.clipboard.writeText('${token}')">
|
<button class="btn-action" title="Copy" onclick="navigator.clipboard.writeText('${token}')">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
@@ -105,7 +105,7 @@ class SettingsPage {
|
|||||||
</div>
|
</div>
|
||||||
</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;">
|
<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>
|
<i class="fas fa-exclamation-triangle" style="font-size: 1.5rem; color: var(--warning);"></i>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user