From a82b6da9ca0d40da9103dd6198693312a590e499 Mon Sep 17 00:00:00 2001 From: eiiko6 Date: Sat, 4 Apr 2026 13:04:26 +0200 Subject: [PATCH] added analytics page --- Cargo.lock | 1 + Cargo.toml | 1 + src/handlers.rs | 189 +++++++++++++++++++++++++++++++++++- src/main.rs | 7 +- templates/_layout.html | 59 ++++++++++++ templates/analytics.html | 200 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 templates/analytics.html diff --git a/Cargo.lock b/Cargo.lock index c138e5d..a2115df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1882,6 +1882,7 @@ dependencies = [ "lazy_static", "reqwest", "serde", + "serde_json", "tera", "tokio", "tower-http", diff --git a/Cargo.toml b/Cargo.toml index 7563d67..4f852fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ tower_governor = "0.8.0" serde = { version = "1.0.228", features = ["derive"] } chrono = { version = "0.4.44", features = ["serde"] } reqwest = { version = "0.13.2", features = ["json"] } +serde_json = "1.0.149" diff --git a/src/handlers.rs b/src/handlers.rs index 60bc322..1ad3981 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -156,7 +156,6 @@ pub async fn home_handler( let sort_field = params.sort.unwrap_or_else(|| "score".to_string()); let order = params.order.unwrap_or_else(|| "desc".to_string()); - reports.sort_by(|a, b| { let cmp = match sort_field.as_str() { "single" => a.single_score.cmp(&b.single_score), @@ -169,7 +168,6 @@ pub async fn home_handler( "time" => a.timestamp.cmp(&b.timestamp), _ => a.score.cmp(&b.score), }; - if order == "asc" { cmp } else { cmp.reverse() } }); @@ -178,6 +176,7 @@ pub async fn home_handler( context.insert("title", &state.app_name); context.insert("current_sort", &sort_field); context.insert("current_order", &order); + context.insert("current_page", "leaderboard"); // NEW match TEMPLATES.render("index.html", &context) { Ok(html) => Ok(Html(html)), @@ -212,3 +211,189 @@ pub async fn report_details_handler( Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } + +async fn get_processed_reports() -> Result, (StatusCode, String)> { + let response = reqwest::get("https://alatreon.org/slimes?limit=1000") + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let raw_reports = response.json::>().await.map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("JSON Error: {}", e), + ) + })?; + + Ok(raw_reports + .into_iter() + .map(|s| { + let ram_raw = s + .slimes + .get("RAM") + .and_then(|v| v.first()) + .map(|r| parse_total_ram_raw(r)) + .unwrap_or(0.0); + ReportRow { + id: s.id, + score: (s.benchmark.multi_thread.score + s.benchmark.single_thread.score) / 2, + single_score: s.benchmark.single_thread.score, + multi_score: s.benchmark.multi_thread.score, + cores: s.benchmark.logical_cores, + ram: s + .slimes + .get("RAM") + .and_then(|v| v.first()) + .map(|r| parse_total_ram(r)) + .unwrap_or_else(|| "-".into()), + ram_raw, + os: s + .slimes + .get("OS") + .and_then(|v| v.first()) + .cloned() + .unwrap_or_else(|| "Unknown".into()), + hostname: s + .slimes + .get("Hostname") + .and_then(|v| v.first()) + .cloned() + .unwrap_or_else(|| "-".into()), + signature: s.signature, + timestamp: s.timestamp.clone(), + time_ago: format_time_ago(&s.timestamp), + is_new: is_recent(&s.timestamp), + } + }) + .collect()) +} + +#[derive(Serialize)] +pub struct AnalyticsData { + pub os_labels: Vec, + pub os_scores: Vec, + pub os_max_labels: Vec, + pub os_max_scores: Vec, + pub os_min_labels: Vec, + pub os_min_scores: Vec, + pub dist_labels: Vec, + pub dist_counts: Vec, + pub avg_score_per_gb: f32, + pub avg_score_per_thread: f32, + pub total_reports: usize, +} + +pub async fn analytics_handler( + State(state): State>, +) -> Result { + let reports = get_processed_reports().await?; + if reports.is_empty() { + return Err((StatusCode::NOT_FOUND, "No data available".into())); + } + + // Average score per OS + let mut os_map: HashMap = HashMap::new(); + for r in &reports { + let entry = os_map.entry(r.os.clone()).or_insert((0, 0)); + entry.0 += r.score; + entry.1 += 1; + } + let mut os_stats: Vec<(String, u64)> = os_map + .into_iter() + .map(|(os, (sum, count))| (os, sum / count as u64)) + .collect(); + os_stats.sort_by(|a, b| b.1.cmp(&a.1)); + + // Highest score per OS + let mut os_max_map: HashMap = HashMap::new(); + for r in &reports { + let entry = os_max_map.entry(r.os.clone()).or_insert(0); + if r.score > *entry { + *entry = r.score; + } + } + let mut os_max_stats: Vec<(String, u64)> = os_max_map.into_iter().collect(); + os_max_stats.sort_by(|a, b| b.1.cmp(&a.1)); + + // Lowest score per OS + let mut os_min_map: HashMap = HashMap::new(); + for r in &reports { + os_min_map + .entry(r.os.clone()) + .and_modify(|e| { + if r.score < *e { + *e = r.score; + } + }) + .or_insert(r.score); + } + + let mut os_min_stats: Vec<(String, u64)> = os_min_map.into_iter().collect(); + os_min_stats.sort_by(|a, b| b.1.cmp(&a.1)); + + // Score distribution + let min_score = reports.iter().map(|r| r.score).min().unwrap_or(0); + let max_score = reports.iter().map(|r| r.score).max().unwrap_or(0); + + let range = (max_score - min_score).max(1); + let bin_width = range as f32 / 10.0; + + let mut dist_counts = vec![0u32; 10]; + let mut dist_labels = vec![String::new(); 10]; + + for i in 0..10 { + let start = min_score + (i as f32 * bin_width) as u64; + let end = min_score + ((i + 1) as f32 * bin_width) as u64; + dist_labels[i] = format!("{}-{}", start, end); + } + + for r in &reports { + let mut bin_idx = ((r.score - min_score) as f32 / bin_width).floor() as usize; + if bin_idx >= 10 { + bin_idx = 9; + } + dist_counts[bin_idx] += 1; + } + + // Ratios + let total_reports = reports.len(); + let avg_score_per_gb = reports + .iter() + .filter(|r| r.ram_raw > 0.0) + .map(|r| r.score as f32 / r.ram_raw) + .sum::() + / total_reports as f32; + let avg_score_per_thread = reports + .iter() + .filter(|r| r.cores > 0) + .map(|r| r.score as f32 / r.cores as f32) + .sum::() + / total_reports as f32; + + let data = AnalyticsData { + os_labels: os_stats.iter().map(|x| x.0.clone()).collect(), + os_scores: os_stats.iter().map(|x| x.1).collect(), + os_max_labels: os_max_stats.iter().map(|x| x.0.clone()).collect(), + os_max_scores: os_max_stats.iter().map(|x| x.1).collect(), + os_min_labels: os_min_stats.iter().map(|x| x.0.clone()).collect(), + os_min_scores: os_min_stats.iter().map(|x| x.1).collect(), + dist_labels, + dist_counts, + avg_score_per_gb, + avg_score_per_thread, + total_reports, + }; + + let json_data = serde_json::to_string(&data) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let mut context = Context::new(); + context.insert("title", &format!("Analytics | {}", state.app_name)); + context.insert("json_data", &json_data); + context.insert("data", &data); + context.insert("current_page", "analytics"); + + match TEMPLATES.render("analytics.html", &context) { + Ok(html) => Ok(Html(html)), + Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), + } +} diff --git a/src/main.rs b/src/main.rs index 6db6c9a..53a0637 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use tera::Tera; mod handlers; use handlers::home_handler; -use crate::handlers::report_details_handler; +use crate::handlers::{analytics_handler, report_details_handler}; lazy_static! { pub static ref TEMPLATES: Tera = { @@ -17,6 +17,10 @@ lazy_static! { ("_base.css", include_str!("../templates/_base.css")), ("index.html", include_str!("../templates/homepage.html")), ("details.html", include_str!("../templates/details.html")), + ( + "analytics.html", + include_str!("../templates/analytics.html"), + ), ]) .unwrap(); tera @@ -58,6 +62,7 @@ async fn main() -> anyhow::Result<()> { let app = Router::new() .route("/", get(home_handler)) .route("/report/{id}", get(report_details_handler)) + .route("/analytics", get(analytics_handler)) .with_state(shared_state); let addr = if cli.host { diff --git a/templates/_layout.html b/templates/_layout.html index 46668d4..19124e6 100644 --- a/templates/_layout.html +++ b/templates/_layout.html @@ -9,9 +9,68 @@ + +
{% block content %}{% endblock %}
{{ now() | date(format="%Y") }} - Slimes
+ + diff --git a/templates/analytics.html b/templates/analytics.html new file mode 100644 index 0000000..ea1a2d0 --- /dev/null +++ b/templates/analytics.html @@ -0,0 +1,200 @@ +{% extends "_layout.html" %} + +{% block content %} +
+

Benchmark Analytics

+

Insights from {{ data.total_reports }} benchmarks

+
+ +
+
+ RAM Efficiency + {{ data.avg_score_per_gb | round(precision=1) }} + Avg Score per GB RAM +
+
+ CPU Thread Efficiency + {{ data.avg_score_per_thread | round(precision=1) }} + Avg Score per Thread +
+ +
+

Score Distribution

+ +
+ +
+

Avg Score by Operating System

+ +
+ +
+

Highest Score by Operating System

+ +
+ +
+

Lowest Score by Operating System

+ +
+
+ + + + + +{% endblock %}