Add stats_single page & header with links

This commit is contained in:
2021-02-04 19:53:28 +01:00
parent 597101ed47
commit 5fa1819bea
15 changed files with 285 additions and 232 deletions

2
Cargo.lock generated
View File

@ -1879,7 +1879,7 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "stl"
version = "2.2.0"
version = "2.3.0"
dependencies = [
"bincode",
"chrono",

View File

@ -1,7 +1,7 @@
[package]
name = "stl"
description = "studielogg aka scuffed toggl"
version = "2.2.0"
version = "2.3.0"
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
license = "MPL-2.0"
edition = "2018"

View File

@ -59,7 +59,8 @@ async fn main() -> io::Result<()> {
routes::pages::index,
routes::pages::history,
routes::pages::session_edit,
routes::pages::stats::view,
routes::pages::stats::single_stats,
routes::pages::stats::all_stats,
routes::api::edit_session,
routes::api::create_category,
routes::api::start_session,

View File

@ -3,8 +3,7 @@ pub mod stats;
use crate::auth::Authorized;
use crate::database::latest::trees::{categories, sessions};
use crate::status_json::StatusJson;
use bincode::deserialize;
use bincode::serialize;
use bincode::{deserialize, serialize};
use rocket::http::Status;
use rocket::{get, State};
use rocket_contrib::templates::Template;

View File

@ -1,35 +1,120 @@
use crate::auth::Authorized;
use crate::database::latest::trees::{categories, sessions};
use crate::status_json::StatusJson;
use bincode::deserialize;
use bincode::{deserialize, 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::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
#[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_max: 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 view(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
pub fn all_stats(_auth: Authorized, 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_max: f64,
bars: Vec<(u32, f64, f64)>,
}
let now = Local::now();
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
.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);
@ -113,7 +191,7 @@ pub fn view(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, Stat
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>
@ -163,8 +241,7 @@ where
}
}
dbg!(&stats_per_hour);
let sum_weight: f64 = dbg!(stats_per_hour.values().sum());
let sum_weight: f64 = stats_per_hour.values().sum();
for weight in stats_per_hour.values_mut() {
*weight = *weight * 100.0 / sum_weight;
*weight = (*weight * 10.0).trunc() / 10.0;

View File

@ -1,39 +1,28 @@
<!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="">
{{> head}}
<body>
<h1 class="title">stl</h1>
<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>
<h1 class="title">stl</h1>
<div>
<a href="/history">tillbaka</a>
<br>
<br>
<form action="/session/{{session_id}}/edit" method="post">
<input type="hidden" id="category" name="category" value="{{session.category}}">
<input type="hidden" id="deleted" name="deleted" value="{{session.deleted}}">
<span>Started:</span>
<input type="text" id="started" name="started" value="{{pretty_datetime session.started}}"></input>
<br>
<span>Ended:</span>
<input type="text" id="ended" name="ended" value="{{pretty_datetime session.ended}}"></input>
<br>
<button type="submit">spara</button>
</form>
<br>
<form action="/session/{{this.session_id}}/delete" method="post">
<button type="submit">ta bort</button>
</form>
</div>
</body>
<div>
<a href="/history">tillbaka</a>
<br>
<br>
<form action="/session/{{session_id}}/edit" method="post">
<input type="hidden" id="category" name="category" value="{{session.category}}">
<input type="hidden" id="deleted" name="deleted" value="{{session.deleted}}">
<span>Started:</span>
<input type="text" id="started" name="started" value="{{pretty_datetime session.started}}"></input>
<br>
<span>Ended:</span>
<input type="text" id="ended" name="ended" value="{{pretty_datetime session.ended}}"></input>
<br>
<button type="submit">spara</button>
</form>
<br>
<form action="/session/{{this.session_id}}/delete" method="post">
<button type="submit">ta bort</button>
</form>
</div>
</body>
</html>

12
templates/head.hbs Normal file
View 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
View File

@ -0,0 +1,7 @@
<h1 class="title">
<a href="/stats">🗠</a>
-
<a href="/">stl</a>
-
<a href="/history">🕮</a>
</h1>

View File

@ -1,33 +1,23 @@
<!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="">
{{> head}}
<body>
{{> header}}
<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>
<h1 class="title">stl</h1>
<ul class="striped_list">
{{#each entries}}
<li class="history_entry">
<span class="history_entry_category">{{this.category.name}}</span>
<span>under</span>
<span class="history_entry_duration">{{pretty_seconds this.duration.secs}}</span>
<span>från</span>
<span class="history_entry_started">{{pretty_datetime this.session.started}}</span>
<span>tills</span>
<span class="history_entry_ended">{{pretty_datetime this.session.ended}}</span>
<span>---</span>
<a href="/session/{{this.session_id}}/edit" class="history_entry_edit_button">ändra</a>
</li>
{{/each}}
</ul>
</body>
<ul class="striped_list">
{{#each entries}}
<li class="history_entry">
<span class="history_entry_category">{{this.category.name}}</span>
<span>under</span>
<span class="history_entry_duration">{{pretty_seconds this.duration.secs}}</span>
<span>från</span>
<span class="history_entry_started">{{pretty_datetime this.session.started}}</span>
<span>tills</span>
<span class="history_entry_ended">{{pretty_datetime this.session.ended}}</span>
<span>---</span>
<a href="/session/{{this.session_id}}/edit" class="history_entry_edit_button">ändra</a>
</li>
{{/each}}
</ul>
</body>
</html>

View File

@ -1,72 +1,62 @@
<!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="">
{{> head}}
<body>
<script>
function toggle_category(id) {
// Find out whether the button is in active (play) or inactive (paused) state
let toggled_class = "category_button_toggled";
let cl = document.getElementById("toggle-button-" + id).classList;
let active = cl.contains(toggled_class);
<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">
// Get the corresponding route to activate/inactivate the category
let url;
if(active) {
url = "/category/" + id + "/end_session";
cl.remove(toggled_class);
} else {
url = "/category/" + id + "/start_session";
cl.add(toggled_class);
}
<title>stl</title>
<script>
function toggle_category(id) {
// Find out whether the button is in active (play) or inactive (paused) state
let toggled_class = "category_button_toggled";
let cl = document.getElementById("toggle-button-" + id).classList;
let active = cl.contains(toggled_class);
//var params = "lorem=ipsum&name=alpha";
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
// Get the corresponding route to activate/inactivate the category
let url;
if(active) {
url = "/category/" + id + "/end_session";
cl.remove(toggled_class);
} else {
url = "/category/" + id + "/start_session";
cl.add(toggled_class);
}
//Send the proper header information along with the request
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
//var params = "lorem=ipsum&name=alpha";
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.send();
}
</script>
//Send the proper header information along with the request
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
{{> header}}
xhr.send();
}
</script>
</head>
<body>
<h1 class="title">stl</h1>
<ul class="striped_list">
{{#each categories}}
<li class="category_entry">
<div class="category_icon"
style="background-color: {{this.1.color}}"
></div>
<span class="category_name">{{this.1.name}}</span>
{{#if this.1.started}}
<form action="/category/{{this.0}}/bump_session/minutes/5", method="post">
<button style="height: 100%; color: green;" type="submit">+5</button>
</form>
{{/if}}
<div class="category_button_container">
<button
id="toggle-button-{{this.0}}"
onClick="toggle_category('{{this.0}}')"
{{#if this.1.started}}
class="category_button category_button_toggled"
{{else}}
class="category_button"
{{/if}}>
</button>
</div>
</li>
{{/each}}
</ul>
</body>
<ul class="striped_list">
{{#each categories}}
<li class="category_entry">
<div class="category_icon"
style="background-color: {{this.1.color}}"
></div>
<span class="category_name">{{this.1.name}}</span>
{{#if this.1.started}}
<form action="/category/{{this.0}}/bump_session/minutes/5", method="post">
<button style="height: 100%; color: green;" type="submit">+5</button>
</form>
{{/if}}
<div class="category_button_container">
<button
id="toggle-button-{{this.0}}"
onClick="toggle_category('{{this.0}}')"
{{#if this.1.started}}
class="category_button category_button_toggled"
{{else}}
class="category_button"
{{/if}}>
</button>
</div>
</li>
{{/each}}
</ul>
</body>
</html>

View File

@ -1,22 +1,11 @@
<!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="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
<title>stl</title>
</head>
<body>
<h1 class="title">stl</h1>
<h2>Logga in</h2>
<form action="/login" method="post">
<input type="password" id="password" name="password"></input>
</form>
</body>
{{>head}}
<body>
<h1 class="title">stl</h1>
<h2>Logga in</h2>
<form action="/login" method="post">
<input type="password" id="password" name="password"></input>
</form>
</body>
</html>

View File

@ -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}}&percnt;</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}}&percnt;</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
View 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
View 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}}&percnt;</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}}&percnt;</span>
</div>
<div class="chart_histogram_col_label">{{this.0}}</div>
</div>
{{/each}}
</div>
</div>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en">
{{> head}}
<body>
{{> header}}
{{> stats_chart this}}
</body>
</html>