Add system utilization metrics to aster web UI
Rust / Clippy, Rustfmt, Tests (push) Waiting to run
Rust / Linux-x64 build (push) Blocked by required conditions
Rust / GitHub release (push) Blocked by required conditions

This commit is contained in:
2026-06-09 20:55:14 +02:00
parent be41c92e7b
commit 0304f1428f
3 changed files with 160 additions and 1 deletions
Generated
+1
View File
@@ -166,6 +166,7 @@ dependencies = [
"mime_guess",
"serde",
"serde_json",
"sysinfo",
"tokio",
"tracing",
"tracing-subscriber",
+1
View File
@@ -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"] }
+158 -1
View File
@@ -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();
}