Add system utilization metrics to aster web UI
This commit is contained in:
Generated
+1
@@ -166,6 +166,7 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
||||
@@ -20,6 +20,7 @@ image = "0.25.6"
|
||||
mime_guess = "2.0.5"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.142"
|
||||
sysinfo = "0.37.0"
|
||||
tokio = { version = "1.47.1", features = ["fs", "macros", "net", "rt-multi-thread"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||
|
||||
@@ -22,6 +22,7 @@ use clap::Parser;
|
||||
use image::{DynamicImage, ImageFormat, RgbImage, imageops::FilterType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use sysinfo::{Components, Disks, System};
|
||||
use tokio::fs;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
@@ -84,6 +85,7 @@ struct StateResponse {
|
||||
active_images: Vec<String>,
|
||||
images: Vec<ImageView>,
|
||||
display: DisplayStatus,
|
||||
system: SystemView,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -114,6 +116,28 @@ struct DisplayStatus {
|
||||
updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct SystemView {
|
||||
cpu_usage_percent: String,
|
||||
load_avg_one: String,
|
||||
load_avg_five: String,
|
||||
load_avg_fifteen: String,
|
||||
mem_usage_percent: String,
|
||||
mem_used: String,
|
||||
mem_total: String,
|
||||
swap_usage_percent: String,
|
||||
swap_used: String,
|
||||
swap_total: String,
|
||||
disk_usage_percent: String,
|
||||
disk_used: String,
|
||||
disk_total: String,
|
||||
cpu_count: usize,
|
||||
process_count: usize,
|
||||
uptime: String,
|
||||
temperature_cpu: Option<String>,
|
||||
temperature_gpu: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActivateRequest {
|
||||
images: Vec<String>,
|
||||
@@ -475,9 +499,106 @@ async fn build_state_response(state: &AppState) -> Result<StateResponse> {
|
||||
active_images: current_active_images(&monitor),
|
||||
images: list_images(state).await?,
|
||||
display: read_display_status(&state.display_status),
|
||||
system: collect_system_view(),
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_system_view() -> SystemView {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
let load_avg = System::load_average();
|
||||
let total_memory = sys.total_memory();
|
||||
let used_memory = sys.used_memory();
|
||||
let total_swap = sys.total_swap();
|
||||
let used_swap = sys.used_swap();
|
||||
|
||||
let mut disks = Disks::new();
|
||||
disks.refresh(false);
|
||||
let disk_total: u64 = disks.iter().map(|disk| disk.total_space()).sum();
|
||||
let disk_used: u64 = disks
|
||||
.iter()
|
||||
.map(|disk| disk.total_space().saturating_sub(disk.available_space()))
|
||||
.sum();
|
||||
|
||||
let mut components = Components::new();
|
||||
components.refresh(false);
|
||||
|
||||
SystemView {
|
||||
cpu_usage_percent: format!("{:.1}", sys.global_cpu_usage()),
|
||||
load_avg_one: format!("{:.2}", load_avg.one),
|
||||
load_avg_five: format!("{:.2}", load_avg.five),
|
||||
load_avg_fifteen: format!("{:.2}", load_avg.fifteen),
|
||||
mem_usage_percent: format!("{:.1}", percentage(used_memory, total_memory)),
|
||||
mem_used: format_bytes(used_memory),
|
||||
mem_total: format_bytes(total_memory),
|
||||
swap_usage_percent: format!("{:.1}", percentage(used_swap, total_swap)),
|
||||
swap_used: format_bytes(used_swap),
|
||||
swap_total: format_bytes(total_swap),
|
||||
disk_usage_percent: format!("{:.1}", percentage(disk_used, disk_total)),
|
||||
disk_used: format_bytes(disk_used),
|
||||
disk_total: format_bytes(disk_total),
|
||||
cpu_count: sys.cpus().len(),
|
||||
process_count: sys.processes().len(),
|
||||
uptime: format_uptime(System::uptime()),
|
||||
temperature_cpu: component_temperature(&components, "Tctl"),
|
||||
temperature_gpu: component_temperature(&components, "amdgpu"),
|
||||
}
|
||||
}
|
||||
|
||||
fn component_temperature(components: &Components, needle: &str) -> Option<String> {
|
||||
components
|
||||
.iter()
|
||||
.find(|component| component.label().contains(needle))
|
||||
.and_then(|component| component.temperature())
|
||||
.map(|value| format!("{value:.1} °C"))
|
||||
}
|
||||
|
||||
fn percentage(used: u64, total: u64) -> f64 {
|
||||
if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
used as f64 * 100.0 / total as f64
|
||||
}
|
||||
}
|
||||
|
||||
fn format_uptime(seconds: u64) -> String {
|
||||
let days = seconds / 86_400;
|
||||
let hours = (seconds % 86_400) / 3_600;
|
||||
let minutes = (seconds % 3_600) / 60;
|
||||
|
||||
if days == 0 {
|
||||
format!("{hours:02}:{minutes:02}")
|
||||
} else if days == 1 {
|
||||
format!("1 day, {hours:02}:{minutes:02}")
|
||||
} else {
|
||||
format!("{days} days, {hours:02}:{minutes:02}")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_bytes(bytes: u64) -> String {
|
||||
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
const THRESHOLD: f64 = 1024.0;
|
||||
|
||||
if bytes == 0 {
|
||||
return "0 B".to_string();
|
||||
}
|
||||
|
||||
let mut size = bytes as f64;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= THRESHOLD && unit_index < UNITS.len() - 1 {
|
||||
size /= THRESHOLD;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
if unit_index > 0 {
|
||||
format!("{size:.2} {}", UNITS[unit_index])
|
||||
} else {
|
||||
format!("{} {}", size, UNITS[unit_index])
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
|
||||
let mut out = Vec::new();
|
||||
let mut dir = fs::read_dir(&state.image_dir).await?;
|
||||
@@ -889,6 +1010,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
.hero h1 { margin: 0 0 8px; font-size: clamp(2rem, 3vw, 3.4rem); line-height: 1; }
|
||||
.hero p, .muted { color: var(--muted); }
|
||||
.status strong { display: block; margin-top: 8px; font-size: 1.3rem; }
|
||||
.status small { display: block; margin-top: 6px; color: var(--muted); }
|
||||
.controls { display: grid; grid-template-columns: 180px 1fr auto; gap: 12px; align-items: end; margin-top: 16px; }
|
||||
label { display: grid; gap: 8px; color: var(--muted); font-size: .9rem; }
|
||||
input, button {
|
||||
@@ -904,6 +1026,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
.ghost { background: rgba(9,14,20,.7); color: var(--text); }
|
||||
.danger { background: rgba(255,127,115,.14); color: #ffc7c1; }
|
||||
.content { grid-template-columns: 1.45fr 1fr; }
|
||||
.metrics-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; margin-bottom: 18px; }
|
||||
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
||||
.tile {
|
||||
border: 1px solid var(--line);
|
||||
@@ -929,7 +1052,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
background: rgba(10,14,20,.94); display: none;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.hero, .content, .controls, .status-grid { grid-template-columns: 1fr; }
|
||||
.hero, .content, .controls, .status-grid, .metrics-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -965,6 +1088,14 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
<div class="card status"><span class="muted">Config Path</span><strong id="configPathValue" style="font-size: 1rem;">-</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="metrics-grid">
|
||||
<div class="card status"><span class="muted">CPU</span><strong id="cpuUsageValue">-</strong><small id="cpuMetaValue">-</small></div>
|
||||
<div class="card status"><span class="muted">Memory</span><strong id="memUsageValue">-</strong><small id="memMetaValue">-</small></div>
|
||||
<div class="card status"><span class="muted">Storage</span><strong id="diskUsageValue">-</strong><small id="diskMetaValue">-</small></div>
|
||||
<div class="card status"><span class="muted">Swap</span><strong id="swapUsageValue">-</strong><small id="swapMetaValue">-</small></div>
|
||||
<div class="card status"><span class="muted">Load Avg</span><strong id="loadValue">-</strong><small id="uptimeValue">-</small></div>
|
||||
<div class="card status"><span class="muted">Temperatures</span><strong id="tempCpuValue">-</strong><small id="tempGpuValue">-</small></div>
|
||||
</section>
|
||||
<section class="content">
|
||||
<div class="card">
|
||||
<h2>Available Images</h2>
|
||||
@@ -984,6 +1115,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
activeImages: [],
|
||||
setup: { switch_time: "10", custom_panel: false },
|
||||
display: {},
|
||||
system: {},
|
||||
};
|
||||
const el = {
|
||||
gallery: document.getElementById("gallery"),
|
||||
@@ -996,6 +1128,18 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
displayTargetValue: document.getElementById("displayTargetValue"),
|
||||
displayErrorValue: document.getElementById("displayErrorValue"),
|
||||
configPathValue: document.getElementById("configPathValue"),
|
||||
cpuUsageValue: document.getElementById("cpuUsageValue"),
|
||||
cpuMetaValue: document.getElementById("cpuMetaValue"),
|
||||
memUsageValue: document.getElementById("memUsageValue"),
|
||||
memMetaValue: document.getElementById("memMetaValue"),
|
||||
diskUsageValue: document.getElementById("diskUsageValue"),
|
||||
diskMetaValue: document.getElementById("diskMetaValue"),
|
||||
swapUsageValue: document.getElementById("swapUsageValue"),
|
||||
swapMetaValue: document.getElementById("swapMetaValue"),
|
||||
loadValue: document.getElementById("loadValue"),
|
||||
uptimeValue: document.getElementById("uptimeValue"),
|
||||
tempCpuValue: document.getElementById("tempCpuValue"),
|
||||
tempGpuValue: document.getElementById("tempGpuValue"),
|
||||
upload: document.getElementById("upload"),
|
||||
apply: document.getElementById("apply"),
|
||||
disable: document.getElementById("disable"),
|
||||
@@ -1048,6 +1192,7 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
state.activeImages = data.active_images;
|
||||
state.setup = data.setup;
|
||||
state.display = data.display || {};
|
||||
state.system = data.system || {};
|
||||
el.switchTime.value = data.setup.switch_time || "10";
|
||||
el.customPanelValue.textContent = data.setup.custom_panel ? "Active" : "Disabled";
|
||||
el.imageCountValue.textContent = String(data.active_images.length);
|
||||
@@ -1058,6 +1203,18 @@ const INDEX_HTML: &str = r##"<!DOCTYPE html>
|
||||
el.displayTargetValue.textContent = state.display.device || "-";
|
||||
el.displayErrorValue.textContent = state.display.last_error || "";
|
||||
el.configPathValue.textContent = data.monitor_path;
|
||||
el.cpuUsageValue.textContent = `${state.system.cpu_usage_percent || "-"} %`;
|
||||
el.cpuMetaValue.textContent = `Load ${state.system.load_avg_one || "-"} / ${state.system.load_avg_five || "-"} / ${state.system.load_avg_fifteen || "-"}`;
|
||||
el.memUsageValue.textContent = `${state.system.mem_usage_percent || "-"} %`;
|
||||
el.memMetaValue.textContent = `${state.system.mem_used || "-"} / ${state.system.mem_total || "-"}`;
|
||||
el.diskUsageValue.textContent = `${state.system.disk_usage_percent || "-"} %`;
|
||||
el.diskMetaValue.textContent = `${state.system.disk_used || "-"} / ${state.system.disk_total || "-"}`;
|
||||
el.swapUsageValue.textContent = `${state.system.swap_usage_percent || "-"} %`;
|
||||
el.swapMetaValue.textContent = `${state.system.swap_used || "-"} / ${state.system.swap_total || "-"}`;
|
||||
el.loadValue.textContent = `${state.system.cpu_count || "-"} CPU / ${state.system.process_count || "-"} proc`;
|
||||
el.uptimeValue.textContent = `Uptime ${state.system.uptime || "-"}`;
|
||||
el.tempCpuValue.textContent = `CPU ${state.system.temperature_cpu || "-"}`;
|
||||
el.tempGpuValue.textContent = `GPU ${state.system.temperature_gpu || "-"}`;
|
||||
render();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user