Add stats page
This commit is contained in:
@ -50,6 +50,7 @@ fn main() -> io::Result<()> {
|
||||
routes::pages::index,
|
||||
routes::pages::history,
|
||||
routes::pages::session_edit,
|
||||
routes::pages::stats,
|
||||
routes::api::edit_session,
|
||||
routes::api::create_category,
|
||||
routes::api::start_session,
|
||||
|
||||
@ -2,12 +2,13 @@ use crate::database::latest::trees::{categories, sessions};
|
||||
use crate::status_json::StatusJson;
|
||||
use bincode::deserialize;
|
||||
use bincode::serialize;
|
||||
use chrono::{DateTime, Local, Timelike};
|
||||
use rocket::http::Status;
|
||||
use rocket::{get, State};
|
||||
use rocket_contrib::templates::Template;
|
||||
use rocket_contrib::uuid::Uuid;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::time::Duration;
|
||||
|
||||
#[get("/")]
|
||||
@ -105,3 +106,121 @@ pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
|
||||
Ok(Template::render("history", &context))
|
||||
}
|
||||
|
||||
#[get("/stats")]
|
||||
pub fn stats(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StatsContext {
|
||||
categories_stats: Vec<CategoryStatsContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct CategoryStatsContext {
|
||||
category_id: categories::K,
|
||||
category: categories::V,
|
||||
|
||||
last_session_start: Option<DateTime<Local>>,
|
||||
secs_last_session: u64,
|
||||
secs_last_week: u64,
|
||||
secs_last_month: u64,
|
||||
|
||||
bars: Vec<(u32, u32, u32)>,
|
||||
}
|
||||
|
||||
let now = Local::now();
|
||||
|
||||
let categories_tree = db.open_tree(categories::NAME)?;
|
||||
let sessions_tree = db.open_tree(sessions::NAME)?;
|
||||
|
||||
let categories: HashMap<categories::K, categories::V> = categories_tree
|
||||
.iter()
|
||||
.map(|result| {
|
||||
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
|
||||
})
|
||||
.collect::<Result<Result<_, _>, _>>()??;
|
||||
|
||||
let sessions: HashMap<sessions::K, sessions::V> = sessions_tree
|
||||
.iter()
|
||||
.map(|result| {
|
||||
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
|
||||
})
|
||||
.collect::<Result<Result<_, _>, _>>()??;
|
||||
|
||||
let mut categories_stats: Vec<_> = categories
|
||||
.into_iter()
|
||||
.map(|(category_id, category)| {
|
||||
fn sum_sessions<'a>(iter: impl IntoIterator<Item = &'a sessions::V>) -> u64 {
|
||||
iter.into_iter()
|
||||
.map(|session| session.ended - session.started)
|
||||
.map(|duration| duration.num_seconds() as u64)
|
||||
.sum()
|
||||
}
|
||||
|
||||
let my_sessions = sessions
|
||||
.values()
|
||||
.filter(|session| session.category == category_id);
|
||||
|
||||
let last_session = my_sessions.clone().max_by_key(|session| &session.started);
|
||||
|
||||
let secs_last_session = sum_sessions(last_session);
|
||||
|
||||
let secs_last_week = sum_sessions(
|
||||
my_sessions
|
||||
.clone()
|
||||
.filter(|session| (now - session.started) <= chrono::Duration::days(7)),
|
||||
);
|
||||
|
||||
let secs_last_month = sum_sessions(
|
||||
my_sessions
|
||||
.clone()
|
||||
.filter(|session| (now - session.started) <= chrono::Duration::days(30)),
|
||||
);
|
||||
|
||||
let mut stats_per_hour = BTreeMap::new();
|
||||
for session in my_sessions {
|
||||
let step_size = chrono::Duration::minutes(60);
|
||||
let started_hour = session
|
||||
.started
|
||||
.with_minute(0)
|
||||
.unwrap()
|
||||
.with_second(0)
|
||||
.unwrap()
|
||||
.with_nanosecond(0)
|
||||
.unwrap();
|
||||
|
||||
let mut time = started_hour;
|
||||
while time < session.ended {
|
||||
*stats_per_hour.entry(time.hour()).or_default() += 1;
|
||||
time = time.checked_add_signed(step_size).unwrap();
|
||||
}
|
||||
}
|
||||
let max_weight = *stats_per_hour.values().max().unwrap_or(&0);
|
||||
for weight in stats_per_hour.values_mut() {
|
||||
*weight = *weight * 100 / max_weight;
|
||||
}
|
||||
|
||||
CategoryStatsContext {
|
||||
category_id,
|
||||
category,
|
||||
|
||||
last_session_start: last_session.map(|session| session.started),
|
||||
secs_last_session,
|
||||
secs_last_week,
|
||||
secs_last_month,
|
||||
|
||||
bars: (0..24)
|
||||
.map(|hour| {
|
||||
let percentage = *stats_per_hour.entry(hour).or_default();
|
||||
(hour + 1, percentage, 100 - percentage)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
categories_stats.sort_by(|a, b| a.category.name.cmp(&b.category.name));
|
||||
|
||||
let context = StatsContext { categories_stats };
|
||||
|
||||
Ok(Template::render("stats", &context))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user