Add stats_single page & header with links
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1879,7 +1879,7 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stl"
|
name = "stl"
|
||||||
version = "2.2.0"
|
version = "2.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "stl"
|
name = "stl"
|
||||||
description = "studielogg aka scuffed toggl"
|
description = "studielogg aka scuffed toggl"
|
||||||
version = "2.2.0"
|
version = "2.3.0"
|
||||||
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|||||||
@ -59,7 +59,8 @@ async fn main() -> io::Result<()> {
|
|||||||
routes::pages::index,
|
routes::pages::index,
|
||||||
routes::pages::history,
|
routes::pages::history,
|
||||||
routes::pages::session_edit,
|
routes::pages::session_edit,
|
||||||
routes::pages::stats::view,
|
routes::pages::stats::single_stats,
|
||||||
|
routes::pages::stats::all_stats,
|
||||||
routes::api::edit_session,
|
routes::api::edit_session,
|
||||||
routes::api::create_category,
|
routes::api::create_category,
|
||||||
routes::api::start_session,
|
routes::api::start_session,
|
||||||
|
|||||||
@ -3,8 +3,7 @@ pub mod stats;
|
|||||||
use crate::auth::Authorized;
|
use crate::auth::Authorized;
|
||||||
use crate::database::latest::trees::{categories, sessions};
|
use crate::database::latest::trees::{categories, sessions};
|
||||||
use crate::status_json::StatusJson;
|
use crate::status_json::StatusJson;
|
||||||
use bincode::deserialize;
|
use bincode::{deserialize, serialize};
|
||||||
use bincode::serialize;
|
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::{get, State};
|
use rocket::{get, State};
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
|
|||||||
@ -1,21 +1,16 @@
|
|||||||
use crate::auth::Authorized;
|
use crate::auth::Authorized;
|
||||||
use crate::database::latest::trees::{categories, sessions};
|
use crate::database::latest::trees::{categories, sessions};
|
||||||
use crate::status_json::StatusJson;
|
use crate::status_json::StatusJson;
|
||||||
use bincode::deserialize;
|
use bincode::{deserialize, serialize};
|
||||||
use chrono::{DateTime, Local, Timelike};
|
use chrono::{DateTime, Local, Timelike};
|
||||||
|
use rocket::http::Status;
|
||||||
use rocket::{get, State};
|
use rocket::{get, State};
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
|
use rocket_contrib::uuid::Uuid;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
#[get("/stats")]
|
|
||||||
pub fn view(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct StatsContext {
|
|
||||||
categories_stats: Vec<CategoryStatsContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct CategoryStatsContext {
|
struct CategoryStatsContext {
|
||||||
category_id: categories::K,
|
category_id: categories::K,
|
||||||
@ -30,6 +25,96 @@ pub fn view(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, Stat
|
|||||||
bars: Vec<(u32, f64, f64)>,
|
bars: Vec<(u32, f64, f64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/stats/<category_uuid>")]
|
||||||
|
pub fn single_stats(
|
||||||
|
_auth: Authorized,
|
||||||
|
category_uuid: Uuid,
|
||||||
|
db: State<'_, sled::Db>,
|
||||||
|
) -> Result<Template, StatusJson> {
|
||||||
|
let category_uuid_s = sled::IVec::from(serialize(&category_uuid.into_inner())?);
|
||||||
|
|
||||||
|
let categories_tree = db.open_tree(categories::NAME)?;
|
||||||
|
let sessions_tree = db.open_tree(sessions::NAME)?;
|
||||||
|
|
||||||
|
let category: categories::V = match categories_tree.get(category_uuid_s)? {
|
||||||
|
None => Err(Status::NotFound)?,
|
||||||
|
Some(data) => deserialize(&data).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 my_sessions = sessions
|
||||||
|
.values()
|
||||||
|
.filter(|session| session.category == *category_uuid);
|
||||||
|
|
||||||
|
let last_session = my_sessions.clone().max_by_key(|session| &session.started);
|
||||||
|
|
||||||
|
let secs_last_session = sum_sessions(last_session);
|
||||||
|
|
||||||
|
let now = Local::now();
|
||||||
|
|
||||||
|
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 = compute_percentage_per_hour(my_sessions);
|
||||||
|
let biggest_hour = *stats_per_hour
|
||||||
|
.values()
|
||||||
|
.max_by(|f1, f2| match () {
|
||||||
|
_ if f1 == f2 => Ordering::Equal,
|
||||||
|
_ if f1 > f2 => Ordering::Greater,
|
||||||
|
_ => Ordering::Less,
|
||||||
|
})
|
||||||
|
.unwrap_or(&1.0);
|
||||||
|
|
||||||
|
let context = CategoryStatsContext {
|
||||||
|
category_id: *category_uuid,
|
||||||
|
category,
|
||||||
|
|
||||||
|
last_session_start: last_session.map(|session| session.started),
|
||||||
|
secs_last_session,
|
||||||
|
secs_last_week,
|
||||||
|
secs_last_month,
|
||||||
|
|
||||||
|
bars_max: biggest_hour,
|
||||||
|
bars: (0..24)
|
||||||
|
.map(|hour| {
|
||||||
|
let percentage = *stats_per_hour.entry(hour).or_default();
|
||||||
|
(hour, percentage, biggest_hour - percentage)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Template::render("stats_single", &context))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/stats")]
|
||||||
|
pub fn all_stats(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct StatsContext {
|
||||||
|
categories_stats: Vec<CategoryStatsContext>,
|
||||||
|
}
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
|
|
||||||
let categories_tree = db.open_tree(categories::NAME)?;
|
let categories_tree = db.open_tree(categories::NAME)?;
|
||||||
@ -52,13 +137,6 @@ pub fn view(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, Stat
|
|||||||
let mut categories_stats: Vec<_> = categories
|
let mut categories_stats: Vec<_> = categories
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(category_id, category)| {
|
.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
|
let my_sessions = sessions
|
||||||
.values()
|
.values()
|
||||||
.filter(|session| session.category == category_id);
|
.filter(|session| session.category == category_id);
|
||||||
@ -113,7 +191,7 @@ pub fn view(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, Stat
|
|||||||
|
|
||||||
let context = StatsContext { categories_stats };
|
let context = StatsContext { categories_stats };
|
||||||
|
|
||||||
Ok(Template::render("stats", &context))
|
Ok(Template::render("stats_all", &context))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_percentage_per_hour<'a, I>(sessions: I) -> BTreeMap<u32, f64>
|
fn compute_percentage_per_hour<'a, I>(sessions: I) -> BTreeMap<u32, f64>
|
||||||
@ -163,8 +241,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbg!(&stats_per_hour);
|
let sum_weight: f64 = stats_per_hour.values().sum();
|
||||||
let sum_weight: f64 = dbg!(stats_per_hour.values().sum());
|
|
||||||
for weight in stats_per_hour.values_mut() {
|
for weight in stats_per_hour.values_mut() {
|
||||||
*weight = *weight * 100.0 / sum_weight;
|
*weight = *weight * 100.0 / sum_weight;
|
||||||
*weight = (*weight * 10.0).trunc() / 10.0;
|
*weight = (*weight * 10.0).trunc() / 10.0;
|
||||||
|
|||||||
@ -1,17 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
{{> head}}
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="">
|
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
|
|
||||||
<link rel="stylesheet" href="/static/styles/common.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
|
|
||||||
|
|
||||||
<title>stl</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1 class="title">stl</h1>
|
<h1 class="title">stl</h1>
|
||||||
|
|
||||||
|
|||||||
12
templates/head.hbs
Normal file
12
templates/head.hbs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
|
||||||
|
<link rel="stylesheet" href="/static/styles/common.css">
|
||||||
|
<link rel="stylesheet" href="/static/styles/charts.css">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
|
||||||
|
|
||||||
|
<title>stl</title>
|
||||||
|
</head>
|
||||||
7
templates/header.hbs
Normal file
7
templates/header.hbs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<h1 class="title">
|
||||||
|
<a href="/stats">🗠</a>
|
||||||
|
-
|
||||||
|
<a href="/">stl</a>
|
||||||
|
-
|
||||||
|
<a href="/history">🕮</a>
|
||||||
|
</h1>
|
||||||
@ -1,19 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
{{> head}}
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="">
|
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
|
|
||||||
<link rel="stylesheet" href="/static/styles/common.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
|
|
||||||
|
|
||||||
<title>stl</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1 class="title">stl</h1>
|
{{> header}}
|
||||||
|
|
||||||
<ul class="striped_list">
|
<ul class="striped_list">
|
||||||
{{#each entries}}
|
{{#each entries}}
|
||||||
<li class="history_entry">
|
<li class="history_entry">
|
||||||
|
|||||||
@ -1,15 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
{{> head}}
|
||||||
<meta charset="utf-8">
|
<body>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="">
|
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
|
|
||||||
<link rel="stylesheet" href="/static/styles/common.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
|
|
||||||
|
|
||||||
<title>stl</title>
|
|
||||||
<script>
|
<script>
|
||||||
function toggle_category(id) {
|
function toggle_category(id) {
|
||||||
// Find out whether the button is in active (play) or inactive (paused) state
|
// Find out whether the button is in active (play) or inactive (paused) state
|
||||||
@ -37,10 +29,8 @@
|
|||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
{{> header}}
|
||||||
<h1 class="title">stl</h1>
|
|
||||||
|
|
||||||
<ul class="striped_list">
|
<ul class="striped_list">
|
||||||
{{#each categories}}
|
{{#each categories}}
|
||||||
|
|||||||
@ -1,17 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
{{>head}}
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="">
|
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
|
|
||||||
<link rel="stylesheet" href="/static/styles/common.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
|
|
||||||
|
|
||||||
<title>stl</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1 class="title">stl</h1>
|
<h1 class="title">stl</h1>
|
||||||
<h2>Logga in</h2>
|
<h2>Logga in</h2>
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="">
|
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
|
|
||||||
<link rel="stylesheet" href="/static/styles/common.css">
|
|
||||||
<link rel="stylesheet" href="/static/styles/charts.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
|
|
||||||
|
|
||||||
<title>stl</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1 class="title">stl</h1>
|
|
||||||
|
|
||||||
{{#each categories_stats}}
|
|
||||||
{{#if this.last_session_start}}
|
|
||||||
<div class="hline"></div>
|
|
||||||
<h2>
|
|
||||||
<span>Kategori:</span>
|
|
||||||
<span>{{this.category.name}}</span>
|
|
||||||
<span style="color: {{this.category.color}};">●</span>
|
|
||||||
</h2>
|
|
||||||
<h2>
|
|
||||||
<span>Senaste session:</span>
|
|
||||||
<span class="history_entry_started">{{pretty_datetime this.last_session_start}}</span>
|
|
||||||
<span>i</span>
|
|
||||||
<span class="history_entry_duration">{{pretty_seconds this.secs_last_session}}</span>
|
|
||||||
</h2>
|
|
||||||
<h2>
|
|
||||||
<span>Senaste veckan:</span>
|
|
||||||
<span class="history_entry_duration">{{pretty_seconds this.secs_last_week}}</span>
|
|
||||||
<span>Senaste månaden:</span>
|
|
||||||
<span class="history_entry_duration">{{pretty_seconds this.secs_last_month}}</span>
|
|
||||||
</h2>
|
|
||||||
<h2>Andel per timme:</h2>
|
|
||||||
<div class="chart_histogram">
|
|
||||||
<div class="chart_histogram_legend">{{this.bars_max}}%</div>
|
|
||||||
{{#each this.bars}}
|
|
||||||
<div class="chart_histogram_col">
|
|
||||||
<div style="flex-grow: {{this.2}};"></div>
|
|
||||||
<div class="chart_histogram_col_line chart_col_tooltip" style="flex-grow: {{this.1}};">
|
|
||||||
<span class="chart_col_tooltiptext">{{this.1}}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="chart_histogram_col_label">{{this.0}}</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/each}}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
13
templates/stats_all.hbs
Normal file
13
templates/stats_all.hbs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body>
|
||||||
|
{{> header}}
|
||||||
|
|
||||||
|
{{#each categories_stats}}
|
||||||
|
{{#if this.last_session_start}}
|
||||||
|
{{> stats_chart this}}
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
templates/stats_chart.hbs
Normal file
33
templates/stats_chart.hbs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<div>
|
||||||
|
<div class="hline"></div>
|
||||||
|
<h2>
|
||||||
|
<span>Kategori:</span>
|
||||||
|
<a href="/stats/{{category_id}}">{{category.name}}</a>
|
||||||
|
<span style="color: {{category.color}};">●</span>
|
||||||
|
</h2>
|
||||||
|
<h2>
|
||||||
|
<span>Senaste session:</span>
|
||||||
|
<span class="history_entry_started">{{pretty_datetime last_session_start}}</span>
|
||||||
|
<span>i</span>
|
||||||
|
<span class="history_entry_duration">{{pretty_seconds secs_last_session}}</span>
|
||||||
|
</h2>
|
||||||
|
<h2>
|
||||||
|
<span>Senaste veckan:</span>
|
||||||
|
<span class="history_entry_duration">{{pretty_seconds secs_last_week}}</span>
|
||||||
|
<span>Senaste månaden:</span>
|
||||||
|
<span class="history_entry_duration">{{pretty_seconds secs_last_month}}</span>
|
||||||
|
</h2>
|
||||||
|
<h2>Andel per timme:</h2>
|
||||||
|
<div class="chart_histogram">
|
||||||
|
<div class="chart_histogram_legend">{{bars_max}}%</div>
|
||||||
|
{{#each bars}}
|
||||||
|
<div class="chart_histogram_col">
|
||||||
|
<div style="flex-grow: {{this.2}};"></div>
|
||||||
|
<div class="chart_histogram_col_line chart_col_tooltip" style="flex-grow: {{this.1}};">
|
||||||
|
<span class="chart_col_tooltiptext">{{this.1}}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart_histogram_col_label">{{this.0}}</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
8
templates/stats_single.hbs
Normal file
8
templates/stats_single.hbs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body>
|
||||||
|
{{> header}}
|
||||||
|
{{> stats_chart this}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user