added list (home) and details pages

This commit is contained in:
2026-03-26 22:12:54 +01:00
parent 26b12e9dca
commit 453f48804f
10 changed files with 599 additions and 2059 deletions

View File

@@ -1,55 +1,151 @@
use std::sync::Arc;
use axum::{
extract::State,
extract::{Path, State},
response::{Html, IntoResponse},
};
use serde::Serialize;
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};
use tera::Context;
use crate::{AppState, TEMPLATES};
/// Handler for the Home Page
pub async fn home_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
#[derive(Debug, Serialize, Deserialize)]
pub struct Report {
pub id: u64,
pub mac_address: String,
pub timestamp: String,
pub slimes: HashMap<String, Vec<String>>,
pub benchmark: Benchmark,
pub client_version: String,
pub signature: String,
}
#[derive(serde::Serialize)]
pub struct ReportRow {
pub id: u64,
pub score: u64,
pub cores: u32,
pub ram: String,
pub os: String,
pub hostname: String,
pub time_ago: String,
pub signature: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Benchmark {
pub logical_cores: u32,
pub multi_thread: ScoreDetail,
pub single_thread: ScoreDetail,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScoreDetail {
pub score: u64,
}
fn format_time_ago(timestamp: &str) -> String {
let Ok(ts) = DateTime::parse_from_rfc3339(timestamp) else {
return "unknown".into();
};
let now = Utc::now();
let diff = now.signed_duration_since(ts.with_timezone(&Utc));
if diff.num_days() > 0 {
format!("{}d ago", diff.num_days())
} else if diff.num_hours() > 0 {
format!("{}h ago", diff.num_hours())
} else if diff.num_minutes() > 0 {
format!("{}min ago", diff.num_minutes())
} else {
"just now".into()
}
}
fn parse_total_ram(ram_str: &str) -> String {
let total_part = ram_str.split('/').last().unwrap_or("").trim();
let digits: String = total_part.chars().filter(|c| c.is_ascii_digit()).collect();
if let Ok(mb) = digits.parse::<f32>() {
format!("{}GB", (mb / 1024.0).round())
} else {
"Unknown".into()
}
}
pub async fn home_handler(
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (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::<Vec<Report>>().await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("JSON Error: {}", e),
)
})?;
let reports: Vec<ReportRow> = raw_reports
.into_iter()
.map(|s| ReportRow {
id: s.id,
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()),
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()),
time_ago: format_time_ago(&s.timestamp),
signature: s.signature,
})
.collect();
let mut context = Context::new();
context.insert("reports", &reports);
context.insert("title", &state.app_name);
context.insert("server_time", &chrono::Local::now().to_rfc2822());
match TEMPLATES.render("index.html", &context) {
Ok(html) => Html(html).into_response(),
Err(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Ok(html) => Ok(Html(html)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
}
}
/// Handler for the Showcase Page
pub async fn showcase_handler() -> impl IntoResponse {
#[derive(Serialize)]
struct ShowcaseItem {
name: String,
description: String,
}
pub async fn report_details_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<u64>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let url = format!("https://alatreon.org/slimes/{}", id);
let report = reqwest::get(url)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.json::<Report>()
.await
.map_err(|e| (StatusCode::NOT_FOUND, format!("Report not found: {}", e)))?;
let mut context = Context::new();
context.insert("title", "Showcase");
context.insert("report", &report);
context.insert("title", &format!("Report #{} | {}", id, state.app_name));
context.insert("time_ago", &format_time_ago(&report.timestamp));
let items = vec![
ShowcaseItem {
name: "Project Alpha".into(),
description: "A cool Rust project".into(),
},
ShowcaseItem {
name: "Project Beta".into(),
description: "An Axum powered API".into(),
},
ShowcaseItem {
name: "Project Gamma".into(),
description: "Tera template engine".into(),
},
];
context.insert("items", &items);
match TEMPLATES.render("showcase.html", &context) {
Ok(html) => Html(html).into_response(),
Err(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
match TEMPLATES.render("details.html", &context) {
Ok(html) => Ok(Html(html)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
}
}