Compare commits
11 Commits
fd9344b52b
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
ffb728df64
|
|||
|
d6f069d4a5
|
|||
|
79103c6712
|
|||
|
8a0401989c
|
|||
|
5c11d5d091
|
|||
|
48c0c85962
|
|||
|
590d58c152
|
|||
|
e2649165a7
|
|||
|
7698911078
|
|||
|
b4e1774993
|
|||
|
8cf21681fa
|
2079
Cargo.lock
generated
2079
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,3 +4,5 @@ members = [
|
|||||||
"cli",
|
"cli",
|
||||||
"lib",
|
"lib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
resolver = "2"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
##################
|
##################
|
||||||
### BASE STAGE ###
|
### BASE STAGE ###
|
||||||
##################
|
##################
|
||||||
FROM rust:1.53 as base
|
FROM rust:1.56 as base
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN cargo install strip_cargo_version
|
RUN cargo install strip_cargo_version
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "stl_cli"
|
name = "stl_cli"
|
||||||
version = "2.6.6"
|
version = "2.6.8"
|
||||||
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "stl"
|
name = "stl"
|
||||||
@ -14,7 +14,7 @@ thiserror = "1.0.24"
|
|||||||
notify = "4.0.16"
|
notify = "4.0.16"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
uuid = { version = "1.8.0", features = ["serde", "v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
structopt = "0.3.21"
|
structopt = "0.3.21"
|
||||||
syscalls = { version = "0.3", default-features = false }
|
syscalls = { version = "0.3", default-features = false }
|
||||||
|
|||||||
@ -18,29 +18,31 @@ use tokio::task::{spawn, JoinHandle};
|
|||||||
pub fn run(opt: DaemonOpt) -> Result<(), Error> {
|
pub fn run(opt: DaemonOpt) -> Result<(), Error> {
|
||||||
let opt: &'static DaemonOpt = Box::leak(Box::new(opt));
|
let opt: &'static DaemonOpt = Box::leak(Box::new(opt));
|
||||||
let cookie: &'static str =
|
let cookie: &'static str =
|
||||||
Box::leak(std::fs::read_to_string(&opt.cookie_file)?.into_boxed_str());
|
Box::leak(std::fs::read_to_string(&opt.cookie_file)?.into_boxed_str()).trim();
|
||||||
|
|
||||||
let rt = Runtime::new()?;
|
let rt = Runtime::new()?;
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
info!("creating output file");
|
info!("creating output file");
|
||||||
fs::write(&opt.file, b"").await?;
|
fs::write(&opt.file, b"").await?;
|
||||||
|
|
||||||
|
// setup INotify watcher
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
let mut watcher = INotifyWatcher::new_raw(tx).unwrap();
|
let mut watcher = INotifyWatcher::new_raw(tx).unwrap();
|
||||||
watcher
|
watcher
|
||||||
.watch(&opt.file, RecursiveMode::NonRecursive)
|
.watch(&opt.file, RecursiveMode::NonRecursive)
|
||||||
.expect("failed to watch file");
|
.expect("failed to watch file");
|
||||||
|
let mut inotify = proxy_channel(rx);
|
||||||
|
|
||||||
|
// perform first category update
|
||||||
soft_fail(update_category_list(opt, cookie).await);
|
soft_fail(update_category_list(opt, cookie).await);
|
||||||
let mut last_update = Instant::now();
|
let mut last_update = Instant::now();
|
||||||
|
|
||||||
let mut wfe: JoinHandle<_> = spawn(wait_for_event(opt, cookie));
|
// start WFE long-poll
|
||||||
|
let mut wfe_handle: Option<JoinHandle<_>> = Some(spawn(wait_for_event(opt, cookie)));
|
||||||
let mut rx = proxy_channel(rx);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let event = match rx.recv().await {
|
// wait for next inotify event
|
||||||
|
let event = match inotify.recv().await {
|
||||||
Some(event) => event,
|
Some(event) => event,
|
||||||
None => return Err(Error::ChannelClosed),
|
None => return Err(Error::ChannelClosed),
|
||||||
};
|
};
|
||||||
@ -48,33 +50,42 @@ pub fn run(opt: DaemonOpt) -> Result<(), Error> {
|
|||||||
debug!("event op {:?}", event.op);
|
debug!("event op {:?}", event.op);
|
||||||
|
|
||||||
match event.op {
|
match event.op {
|
||||||
Ok(Op::CHMOD) => { /* YEESSS */ }
|
Ok(Op::CHMOD) => { /* we only care about this event */ }
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("CHMOD event detected");
|
debug!("CHMOD event detected");
|
||||||
|
|
||||||
let update_categories;
|
// poll wfe and decide whether we want to restart it, and whether we want to update the
|
||||||
let restart_wfe;
|
// categories
|
||||||
|
let restart_wfe: bool;
|
||||||
|
let update_categories: Option<bool>;
|
||||||
|
if let Some(mut wfe) = wfe_handle.take() {
|
||||||
|
match poll!(&mut wfe) {
|
||||||
|
Poll::Ready(Ok(Ok(_wfe))) => {
|
||||||
|
restart_wfe = true;
|
||||||
|
update_categories = Some(true);
|
||||||
|
}
|
||||||
|
Poll::Ready(Ok(Err(e))) => {
|
||||||
|
error!("error waiting for event (falling back to timing): {}", e);
|
||||||
|
restart_wfe = false;
|
||||||
|
update_categories = None;
|
||||||
|
}
|
||||||
|
Poll::Ready(Err(_)) => {
|
||||||
|
restart_wfe = false;
|
||||||
|
update_categories = None;
|
||||||
|
}
|
||||||
|
Poll::Pending => {
|
||||||
|
restart_wfe = false;
|
||||||
|
update_categories = Some(false);
|
||||||
|
|
||||||
match poll!(&mut wfe) {
|
// wfe task is not complete, we will continue waiting for it next loop
|
||||||
Poll::Ready(Ok(Ok(_wfe))) => {
|
wfe_handle = Some(wfe)
|
||||||
restart_wfe = true;
|
}
|
||||||
update_categories = Some(true);
|
|
||||||
}
|
|
||||||
Poll::Ready(Ok(Err(e))) => {
|
|
||||||
error!("error waiting for event (falling back to timing): {}", e);
|
|
||||||
restart_wfe = false;
|
|
||||||
update_categories = None;
|
|
||||||
}
|
|
||||||
Poll::Ready(Err(_)) => {
|
|
||||||
restart_wfe = false;
|
|
||||||
update_categories = None;
|
|
||||||
}
|
|
||||||
Poll::Pending => {
|
|
||||||
restart_wfe = false;
|
|
||||||
update_categories = Some(false);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
restart_wfe = false;
|
||||||
|
update_categories = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = last_update.elapsed();
|
let elapsed = last_update.elapsed();
|
||||||
@ -94,13 +105,13 @@ pub fn run(opt: DaemonOpt) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if update_categories || restart_wfe {
|
if update_categories || restart_wfe {
|
||||||
wfe = spawn(wait_for_event(opt, cookie));
|
wfe_handle = Some(spawn(wait_for_event(opt, cookie)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
struct WaitForEvent {
|
struct WaitForEvent {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
timeout: bool,
|
timeout: bool,
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "stl_lib"
|
name = "stl_lib"
|
||||||
version = "2.6.6"
|
version = "2.6.8"
|
||||||
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
uuid = { version = "1.8.0", features = ["serde", "v4"] }
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ pub(self) use uuid::Uuid;
|
|||||||
pub mod global {}
|
pub mod global {}
|
||||||
|
|
||||||
pub mod trees {
|
pub mod trees {
|
||||||
|
// ------- time tracking --------
|
||||||
pub mod category {
|
pub mod category {
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
|
|
||||||
@ -55,9 +56,43 @@ pub mod trees {
|
|||||||
/// The time when this session was ended
|
/// The time when this session was ended
|
||||||
pub ended: DateTime<Local>,
|
pub ended: DateTime<Local>,
|
||||||
|
|
||||||
// FIXME: this field is currently not used
|
|
||||||
/// Whether the item has been "deleted", e.g. it shoudn't be shown in the view
|
/// Whether the item has been "deleted", e.g. it shoudn't be shown in the view
|
||||||
|
// FIXME: this field is currently not used
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// -------------------------------------
|
||||||
|
|
||||||
|
// ------- daily tasks tracking --------
|
||||||
|
pub mod daily {
|
||||||
|
use super::super::*;
|
||||||
|
|
||||||
|
pub const NAME: &str = "DAILIES";
|
||||||
|
|
||||||
|
pub type K = Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct V {
|
||||||
|
/// The name of the task.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
// Task should be done every <unit_count> <unit>s, e.g. every 3 days or every 1 week.
|
||||||
|
pub unit_count: u32,
|
||||||
|
pub unit: TimeUnit,
|
||||||
|
|
||||||
|
/// Whether the item has been "deleted", e.g. it shoudn't be shown in the view
|
||||||
|
// FIXME: this field is currently not used
|
||||||
|
#[serde(default)]
|
||||||
|
pub deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum TimeUnit {
|
||||||
|
Day,
|
||||||
|
Week,
|
||||||
|
Month,
|
||||||
|
Year,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -------------------------------------
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "stl"
|
name = "stl"
|
||||||
description = "studielogg aka scuffed toggl"
|
description = "studielogg aka scuffed toggl"
|
||||||
version = "2.6.6"
|
version = "2.6.8"
|
||||||
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dotenv = "0.13.0"
|
dotenv = "0.13.0"
|
||||||
@ -15,27 +15,23 @@ futures = "0.3"
|
|||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
sled = "0.34"
|
sled = "0.34"
|
||||||
semver = "0.11"
|
semver = "0.11"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
uuid = { version = "1.8.0", features = ["serde", "v4"] }
|
||||||
duplicate = "0.2"
|
duplicate = "0.2"
|
||||||
bincode = "1"
|
bincode = "1"
|
||||||
handlebars = "3"
|
handlebars = "4.1.0"
|
||||||
itertools = "0.10.0"
|
itertools = "0.10.0"
|
||||||
|
|
||||||
[dependencies.stl_lib]
|
[dependencies.stl_lib]
|
||||||
path = "../lib"
|
path = "../lib"
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1"
|
version = "1.37.0"
|
||||||
features = ["sync", "time"]
|
features = ["sync", "time"]
|
||||||
|
|
||||||
[dependencies.rocket]
|
[dependencies.rocket]
|
||||||
#version = "0.4"
|
version = "0.5.0"
|
||||||
git = "https://github.com/SergioBenitez/Rocket"
|
features = ["secrets", "json", "uuid"]
|
||||||
branch = "master"
|
|
||||||
features = ["secrets"]
|
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_dyn_templates]
|
||||||
#version = "0.4"
|
version = "0.1.0"
|
||||||
git = "https://github.com/SergioBenitez/Rocket"
|
features = ["handlebars"]
|
||||||
branch = "master"
|
|
||||||
features = ["handlebars_templates", "uuid"]
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ DB_PATH=./database
|
|||||||
#ROCKET_PORT=8000
|
#ROCKET_PORT=8000
|
||||||
#ROCKET_WORKERS=[number of cpus * 2]
|
#ROCKET_WORKERS=[number of cpus * 2]
|
||||||
#ROCKET_LOG="normal"
|
#ROCKET_LOG="normal"
|
||||||
#ROCKET_SECRET_KEY=[randomly generated at launch]
|
#ROCKET_SECRET_KEY=[random string, 44 or 88 for base64, 64 for hex]
|
||||||
#ROCKET_LIMITS="{ forms = 32768 }"
|
#ROCKET_LIMITS="{ forms = 32768 }"
|
||||||
ROCKET_TEMPLATE_DIR="templates"
|
ROCKET_TEMPLATE_DIR="templates"
|
||||||
## =============================== ##
|
## =============================== ##
|
||||||
|
|||||||
@ -5,10 +5,10 @@ use rocket::{
|
|||||||
http::{Cookie, CookieJar, Status},
|
http::{Cookie, CookieJar, Status},
|
||||||
post,
|
post,
|
||||||
request::{FromRequest, Outcome, Request},
|
request::{FromRequest, Outcome, Request},
|
||||||
response::Redirect,
|
response::{content::RawHtml, Redirect},
|
||||||
uri, State,
|
uri, State,
|
||||||
};
|
};
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
|
|
||||||
pub struct MasterPassword(String);
|
pub struct MasterPassword(String);
|
||||||
impl From<String> for MasterPassword {
|
impl From<String> for MasterPassword {
|
||||||
@ -17,6 +17,8 @@ impl From<String> for MasterPassword {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AUTH_QUERY_KEY: &str = "pw";
|
||||||
|
|
||||||
const AUTH_COOKIE_KEY: &str = "authorized";
|
const AUTH_COOKIE_KEY: &str = "authorized";
|
||||||
const AUTH_COOKIE_VAL: &str = "true";
|
const AUTH_COOKIE_VAL: &str = "true";
|
||||||
|
|
||||||
@ -31,18 +33,46 @@ impl<'a> FromRequest<'a> for Authorized {
|
|||||||
type Error = Unauthorized;
|
type Error = Unauthorized;
|
||||||
|
|
||||||
async fn from_request(request: &'a Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(request: &'a Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
// Check if user has been authorized by cookie
|
||||||
let cookies = request.cookies();
|
let cookies = request.cookies();
|
||||||
|
|
||||||
match cookies.get_private(AUTH_COOKIE_KEY) {
|
match cookies.get_private(AUTH_COOKIE_KEY) {
|
||||||
Some(cookie) if cookie.value() == AUTH_COOKIE_VAL => Outcome::Success(Authorized),
|
Some(cookie) if cookie.value() == AUTH_COOKIE_VAL => {
|
||||||
_ => Outcome::Failure((Status::Unauthorized, Unauthorized)),
|
return Outcome::Success(Authorized)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request
|
||||||
|
.guard::<&State<MasterPassword>>()
|
||||||
|
.await
|
||||||
|
.map_error(|_| (Status::Unauthorized, Unauthorized))
|
||||||
|
.and_then(|master_pass| {
|
||||||
|
// Check if query string contains password
|
||||||
|
request
|
||||||
|
.uri()
|
||||||
|
.query()
|
||||||
|
.iter()
|
||||||
|
.inspect(|q| eprintln!("1 {q:?}"))
|
||||||
|
.flat_map(|q| q.split('&'))
|
||||||
|
.flat_map(|q| q.percent_decode())
|
||||||
|
.inspect(|q| eprintln!("2 {q:?}"))
|
||||||
|
.flat_map(|kv| {
|
||||||
|
kv.split_once('=')
|
||||||
|
.map(|(k, v)| (k.to_owned(), v.to_owned()))
|
||||||
|
})
|
||||||
|
.inspect(|q| eprintln!("3 {q:?}"))
|
||||||
|
.filter(|(k, _)| k == AUTH_QUERY_KEY)
|
||||||
|
.filter(|(_, v)| v == &master_pass.0)
|
||||||
|
.map(|_| Outcome::Success(Authorized))
|
||||||
|
.next()
|
||||||
|
.unwrap_or(Outcome::Error((Status::Unauthorized, Unauthorized)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[catch(401)]
|
#[catch(401)]
|
||||||
pub fn login_page(_req: &Request) -> Template {
|
pub fn login_page(_req: &Request) -> RawHtml<Template> {
|
||||||
Template::render("login", &())
|
RawHtml(Template::render("login", &()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
@ -54,7 +84,7 @@ pub struct Login {
|
|||||||
pub fn login(
|
pub fn login(
|
||||||
cookies: &CookieJar,
|
cookies: &CookieJar,
|
||||||
login: Form<Login>,
|
login: Form<Login>,
|
||||||
master_pass: State<MasterPassword>,
|
master_pass: &State<MasterPassword>,
|
||||||
) -> Redirect {
|
) -> Redirect {
|
||||||
if login.password == master_pass.0 {
|
if login.password == master_pass.0 {
|
||||||
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, AUTH_COOKIE_VAL));
|
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, AUTH_COOKIE_VAL));
|
||||||
|
|||||||
@ -28,8 +28,8 @@ pub fn migrate(db: &mut Db) -> Result<(), MigrationError> {
|
|||||||
// Convert to new value
|
// Convert to new value
|
||||||
let v2_value = v2::trees::session::V {
|
let v2_value = v2::trees::session::V {
|
||||||
category,
|
category,
|
||||||
started: DateTime::<Utc>::from_utc(started, Utc).into(),
|
started: DateTime::<Utc>::from_naive_utc_and_offset(started, Utc).into(),
|
||||||
ended: DateTime::<Utc>::from_utc(ended, Utc).into(),
|
ended: DateTime::<Utc>::from_naive_utc_and_offset(ended, Utc).into(),
|
||||||
deleted: false,
|
deleted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,7 +63,8 @@ pub fn migrate(db: &mut Db) -> Result<(), MigrationError> {
|
|||||||
name,
|
name,
|
||||||
description: None,
|
description: None,
|
||||||
color,
|
color,
|
||||||
started: started.map(|ndt| DateTime::<Utc>::from_utc(ndt, Utc).into()),
|
started: started
|
||||||
|
.map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc).into()),
|
||||||
parent: None,
|
parent: None,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -57,4 +57,24 @@ pub mod trees {
|
|||||||
.collect::<Result<Result<_, _>, _>>()??)
|
.collect::<Result<Result<_, _>, _>>()??)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod daily {
|
||||||
|
use super::super::*;
|
||||||
|
pub use stl_lib::v2::trees::daily::*;
|
||||||
|
|
||||||
|
pub fn get_all(tree: &sled::Tree) -> Result<HashMap<K, V>, StatusJson> {
|
||||||
|
Ok(tree
|
||||||
|
.iter()
|
||||||
|
.map(|result| {
|
||||||
|
result
|
||||||
|
.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
|
||||||
|
})
|
||||||
|
.collect::<Result<Result<_, _>, _>>()??)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put(tree: &sled::Tree, key: &K, val: &V) -> Result<(), StatusJson> {
|
||||||
|
tree.insert(serialize(key)?, serialize(val)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use handlebars::handlebars_helper;
|
use handlebars::handlebars_helper;
|
||||||
use rocket_contrib::templates::Engines;
|
use rocket_dyn_templates::Engines;
|
||||||
|
|
||||||
pub fn register_helpers(engines: &mut Engines) {
|
pub fn register_helpers(engines: &mut Engines) {
|
||||||
handlebars_helper!(pretty_datetime: |dt: str| {
|
handlebars_helper!(pretty_datetime: |dt: str| {
|
||||||
|
|||||||
@ -14,8 +14,8 @@ use crate::util::EventNotifier;
|
|||||||
use bincode::{deserialize, serialize};
|
use bincode::{deserialize, serialize};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use rocket_contrib::serve::StaticFiles;
|
use rocket::fs::FileServer;
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use std::{env, io};
|
use std::{env, io};
|
||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
@ -33,7 +33,9 @@ async fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
let birth_date: BirthDate = env::var("BIRTH_DATE")
|
let birth_date: BirthDate = env::var("BIRTH_DATE")
|
||||||
.map(|s| BirthDate(s.parse().expect("failed to parse BIRTH_DATE")))
|
.map(|s| BirthDate(s.parse().expect("failed to parse BIRTH_DATE")))
|
||||||
.unwrap_or_else(|_| BirthDate(NaiveDate::from_ymd(2000, 1, 1)));
|
.unwrap_or_else(|_| {
|
||||||
|
BirthDate(NaiveDate::from_ymd_opt(2000, 1, 1).expect("Date is correct"))
|
||||||
|
});
|
||||||
|
|
||||||
let mut sled = sled::open(db_path)?;
|
let mut sled = sled::open(db_path)?;
|
||||||
match sled.insert(
|
match sled.insert(
|
||||||
@ -47,7 +49,7 @@ async fn main() -> io::Result<()> {
|
|||||||
SCHEMA_VERSION, prev_schema_version
|
SCHEMA_VERSION, prev_schema_version
|
||||||
);
|
);
|
||||||
|
|
||||||
migrate(&mut sled, prev_schema_version, SCHEMA_VERSION).expect("Migration failed")
|
migrate(&mut sled, prev_schema_version, SCHEMA_VERSION).expect("migration failed")
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
println!("Schema version: {}, previously: None", SCHEMA_VERSION);
|
println!("Schema version: {}, previously: None", SCHEMA_VERSION);
|
||||||
@ -64,7 +66,7 @@ async fn main() -> io::Result<()> {
|
|||||||
.manage(master_pass)
|
.manage(master_pass)
|
||||||
.manage(birth_date)
|
.manage(birth_date)
|
||||||
.manage(EventNotifier::new())
|
.manage(EventNotifier::new())
|
||||||
.mount("/static", StaticFiles::from("static"))
|
.mount("/static", FileServer::from("static"))
|
||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
rocket::routes![
|
rocket::routes![
|
||||||
@ -76,6 +78,8 @@ async fn main() -> io::Result<()> {
|
|||||||
routes::pages::bump_session,
|
routes::pages::bump_session,
|
||||||
routes::pages::stats::single_stats,
|
routes::pages::stats::single_stats,
|
||||||
routes::pages::stats::all_stats,
|
routes::pages::stats::all_stats,
|
||||||
|
routes::pages::dailies::dailies,
|
||||||
|
routes::pages::dailies::new_daily,
|
||||||
routes::pages::weeks::weeks,
|
routes::pages::weeks::weeks,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -89,10 +93,10 @@ async fn main() -> io::Result<()> {
|
|||||||
routes::api::category::unarchive,
|
routes::api::category::unarchive,
|
||||||
routes::api::category::remove_parent,
|
routes::api::category::remove_parent,
|
||||||
routes::api::category::set_parent,
|
routes::api::category::set_parent,
|
||||||
routes::api::start_session,
|
routes::api::session::start_session,
|
||||||
routes::api::end_session,
|
routes::api::session::end_session,
|
||||||
routes::api::edit_session,
|
routes::api::session::edit_session,
|
||||||
routes::api::delete_session,
|
routes::api::session::delete_session,
|
||||||
routes::api::wait_for_event,
|
routes::api::wait_for_event,
|
||||||
auth::login,
|
auth::login,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,180 +1,17 @@
|
|||||||
pub mod category;
|
pub mod category;
|
||||||
|
pub mod session;
|
||||||
|
|
||||||
use crate::auth::Authorized;
|
use crate::auth::Authorized;
|
||||||
use crate::routes::pages;
|
|
||||||
use crate::status_json::StatusJson;
|
|
||||||
use crate::util::EventNotifier;
|
use crate::util::EventNotifier;
|
||||||
use bincode::{deserialize, serialize};
|
use rocket::serde::json::Json;
|
||||||
use chrono::{Duration, Local, NaiveDateTime, TimeZone};
|
use rocket::{get, State};
|
||||||
use rocket::form::{Form, FromForm};
|
|
||||||
use rocket::http::Status;
|
|
||||||
use rocket::response::Redirect;
|
|
||||||
use rocket::{get, post, uri, State};
|
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use rocket_contrib::uuid::Uuid;
|
|
||||||
use sled::Transactional;
|
|
||||||
use stl_lib::wfe::WaitForEvent;
|
use stl_lib::wfe::WaitForEvent;
|
||||||
|
|
||||||
#[post("/category/<category_uuid>/start_session")]
|
|
||||||
pub fn start_session(
|
|
||||||
_auth: Authorized,
|
|
||||||
category_uuid: Uuid,
|
|
||||||
event_notifier: State<'_, EventNotifier>,
|
|
||||||
db: State<'_, sled::Db>,
|
|
||||||
) -> Result<StatusJson, StatusJson> {
|
|
||||||
toggle_category_session(category_uuid, true, event_notifier, db)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/category/<category_uuid>/end_session")]
|
|
||||||
pub fn end_session(
|
|
||||||
_auth: Authorized,
|
|
||||||
category_uuid: Uuid,
|
|
||||||
event_notifier: State<'_, EventNotifier>,
|
|
||||||
db: State<'_, sled::Db>,
|
|
||||||
) -> Result<StatusJson, StatusJson> {
|
|
||||||
toggle_category_session(category_uuid, false, event_notifier, db)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_category_session(
|
|
||||||
category_uuid: Uuid,
|
|
||||||
set_active: bool,
|
|
||||||
event_notifier: State<'_, EventNotifier>,
|
|
||||||
db: State<'_, sled::Db>,
|
|
||||||
) -> Result<StatusJson, StatusJson> {
|
|
||||||
use crate::database::latest::trees::{category, session};
|
|
||||||
|
|
||||||
let category_uuid_s = sled::IVec::from(serialize(&category_uuid.into_inner())?);
|
|
||||||
|
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
|
||||||
let sessions_tree = db.open_tree(session::NAME)?;
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
(&categories_tree, &sessions_tree).transaction(|(tx_categories, tx_sessions)| {
|
|
||||||
match tx_categories.get(&category_uuid_s)? {
|
|
||||||
None => return Ok(Err(Status::NotFound)),
|
|
||||||
Some(data) => {
|
|
||||||
let mut category: category::V = deserialize(&data).unwrap();
|
|
||||||
let now = Local::now();
|
|
||||||
|
|
||||||
match (set_active, category.started.take()) {
|
|
||||||
(false, Some(started)) => {
|
|
||||||
// only save sessions longer than 5 minutes
|
|
||||||
let duration = now - started;
|
|
||||||
if duration > Duration::minutes(5) {
|
|
||||||
let session_uuid = serialize(&uuid::Uuid::new_v4()).unwrap();
|
|
||||||
let session = session::V {
|
|
||||||
category: category_uuid.into_inner(),
|
|
||||||
started,
|
|
||||||
ended: now,
|
|
||||||
deleted: category.deleted,
|
|
||||||
};
|
|
||||||
tx_sessions.insert(session_uuid, serialize(&session).unwrap())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(true, None) => {
|
|
||||||
category.started = Some(now);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Category is already in the correct state
|
|
||||||
return Ok(Ok(Status::Ok.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?;
|
|
||||||
event_notifier.notify_event();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Ok(Status::Ok.into()))
|
|
||||||
})??,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, FromForm)]
|
|
||||||
pub struct EditSession {
|
|
||||||
category: Uuid,
|
|
||||||
started: String,
|
|
||||||
ended: String,
|
|
||||||
deleted: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/session/<session_uuid>/edit", data = "<session>")]
|
|
||||||
pub fn edit_session(
|
|
||||||
_auth: Authorized,
|
|
||||||
session_uuid: Uuid,
|
|
||||||
session: Form<EditSession>,
|
|
||||||
db: State<'_, sled::Db>,
|
|
||||||
) -> Result<Redirect, StatusJson> {
|
|
||||||
use crate::database::latest::trees::session;
|
|
||||||
|
|
||||||
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
|
|
||||||
|
|
||||||
let session = session::V {
|
|
||||||
category: session.category.into_inner(),
|
|
||||||
started: Local
|
|
||||||
.from_local_datetime(&NaiveDateTime::parse_from_str(
|
|
||||||
&session.started,
|
|
||||||
"%Y-%m-%d %H:%M",
|
|
||||||
)?)
|
|
||||||
.unwrap(),
|
|
||||||
ended: Local
|
|
||||||
.from_local_datetime(&NaiveDateTime::parse_from_str(
|
|
||||||
&session.ended,
|
|
||||||
"%Y-%m-%d %H:%M",
|
|
||||||
)?)
|
|
||||||
.unwrap(),
|
|
||||||
deleted: session.deleted,
|
|
||||||
};
|
|
||||||
|
|
||||||
if session.started >= session.ended {
|
|
||||||
return Err(StatusJson::new(
|
|
||||||
Status::BadRequest,
|
|
||||||
"started must be earlier than ended",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
db.open_tree(session::NAME)?
|
|
||||||
.insert(session_uuid_s, serialize(&session)?)?;
|
|
||||||
|
|
||||||
// FIXME: Uuid does not implement FromUriParam for some reason... File an issue?
|
|
||||||
//Ok(Redirect::to(uri!(pages::session_edit: session_uuid)))
|
|
||||||
Ok(Redirect::to(format!("/session/{}/edit", session_uuid)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/session/<session_uuid>/delete")]
|
|
||||||
pub fn delete_session(
|
|
||||||
_auth: Authorized,
|
|
||||||
session_uuid: Uuid,
|
|
||||||
db: State<'_, sled::Db>,
|
|
||||||
) -> Result<Redirect, StatusJson> {
|
|
||||||
use crate::database::latest::trees::session;
|
|
||||||
|
|
||||||
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
|
|
||||||
|
|
||||||
let sessions_tree = db.open_tree(session::NAME)?;
|
|
||||||
|
|
||||||
match sessions_tree.remove(session_uuid_s)? {
|
|
||||||
Some(_) => Ok(Redirect::to(uri!(pages::history))),
|
|
||||||
None => Err(Status::NotFound.into()),
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: mark as deleted instead of removing
|
|
||||||
// Ok(sessions_tree.transaction(|tx_sessions| {
|
|
||||||
// match tx_sessions.get(&session_uuid_s)? {
|
|
||||||
// None => return Ok(Err(Status::NotFound)),
|
|
||||||
// Some(data) => {
|
|
||||||
// let mut session: session::V = deserialize(&data).unwrap();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Ok(Ok(Redirect::to(uri!(pages::history))))
|
|
||||||
// })??)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/wait_for_event?<timeout>")]
|
#[get("/wait_for_event?<timeout>")]
|
||||||
pub async fn wait_for_event(
|
pub async fn wait_for_event(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
timeout: Option<u64>,
|
timeout: Option<u64>,
|
||||||
event_notifier: State<'_, EventNotifier>,
|
event_notifier: &State<EventNotifier>,
|
||||||
) -> Json<WaitForEvent> {
|
) -> Json<WaitForEvent> {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
|
|||||||
@ -3,16 +3,16 @@ use crate::database::latest::trees::category;
|
|||||||
use crate::status_json::StatusJson;
|
use crate::status_json::StatusJson;
|
||||||
use rocket::form::{Form, FromForm};
|
use rocket::form::{Form, FromForm};
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::serde::uuid::Uuid;
|
||||||
use rocket::{delete, get, post, State};
|
use rocket::{delete, get, post, State};
|
||||||
use rocket_contrib::json::Json;
|
|
||||||
use rocket_contrib::uuid::Uuid;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[get("/category?<include_archived>")]
|
#[get("/category?<include_archived>")]
|
||||||
pub fn get_all(
|
pub fn get_all(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
include_archived: Option<bool>,
|
include_archived: Option<bool>,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<Json<HashMap<category::K, category::V>>, StatusJson> {
|
) -> Result<Json<HashMap<category::K, category::V>>, StatusJson> {
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
let mut categories = category::get_all(&categories_tree)?;
|
let mut categories = category::get_all(&categories_tree)?;
|
||||||
@ -26,10 +26,10 @@ pub fn get_all(
|
|||||||
pub fn get(
|
pub fn get(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<Json<category::V>, StatusJson> {
|
) -> Result<Json<category::V>, StatusJson> {
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
Ok(Json(category::get(&categories_tree, &id.into_inner())?))
|
Ok(Json(category::get(&categories_tree, &id)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
@ -43,7 +43,7 @@ pub struct NewCategory {
|
|||||||
pub fn create(
|
pub fn create(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
category: Form<NewCategory>,
|
category: Form<NewCategory>,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<StatusJson, StatusJson> {
|
) -> Result<StatusJson, StatusJson> {
|
||||||
let category = category.into_inner();
|
let category = category.into_inner();
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ pub fn create(
|
|||||||
pub fn remove_parent(
|
pub fn remove_parent(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<StatusJson, StatusJson> {
|
) -> Result<StatusJson, StatusJson> {
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
|
|
||||||
@ -87,21 +87,19 @@ pub fn set_parent(
|
|||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
parent_id: Json<Uuid>,
|
parent_id: Json<Uuid>,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<StatusJson, StatusJson> {
|
) -> Result<StatusJson, StatusJson> {
|
||||||
let parent_id = *parent_id.into_inner();
|
|
||||||
|
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
|
|
||||||
// check for parent cycles
|
// check for parent cycles
|
||||||
let mut next_parent_id = parent_id;
|
let mut next_parent_id = *parent_id;
|
||||||
loop {
|
loop {
|
||||||
if next_parent_id == *id {
|
if next_parent_id == id {
|
||||||
return Err(StatusJson::new(Status::BadRequest, "Parent cycle detected"));
|
return Err(StatusJson::new(Status::BadRequest, "Parent cycle detected"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// this also makes sure that parent exists
|
// this also makes sure that parent exists
|
||||||
let parent = category::get(&categories_tree, &&next_parent_id)?;
|
let parent = category::get(&categories_tree, &next_parent_id)?;
|
||||||
|
|
||||||
match parent.parent {
|
match parent.parent {
|
||||||
Some(grandparent_id) => next_parent_id = grandparent_id,
|
Some(grandparent_id) => next_parent_id = grandparent_id,
|
||||||
@ -112,7 +110,7 @@ pub fn set_parent(
|
|||||||
let _parent = category::get(&categories_tree, &parent_id)?;
|
let _parent = category::get(&categories_tree, &parent_id)?;
|
||||||
|
|
||||||
let category = category::V {
|
let category = category::V {
|
||||||
parent: Some(parent_id),
|
parent: Some(*parent_id),
|
||||||
..category::get(&categories_tree, &id)?
|
..category::get(&categories_tree, &id)?
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -125,7 +123,7 @@ pub fn set_parent(
|
|||||||
pub fn archive(
|
pub fn archive(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<StatusJson, StatusJson> {
|
) -> Result<StatusJson, StatusJson> {
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
|
|
||||||
@ -143,7 +141,7 @@ pub fn archive(
|
|||||||
pub fn unarchive(
|
pub fn unarchive(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<StatusJson, StatusJson> {
|
) -> Result<StatusJson, StatusJson> {
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
|
|
||||||
|
|||||||
167
server/src/routes/api/session.rs
Normal file
167
server/src/routes/api/session.rs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
use crate::auth::Authorized;
|
||||||
|
use crate::routes::pages;
|
||||||
|
use crate::status_json::StatusJson;
|
||||||
|
use crate::util::EventNotifier;
|
||||||
|
use bincode::{deserialize, serialize};
|
||||||
|
use chrono::{Duration, Local, NaiveDateTime, TimeZone};
|
||||||
|
use rocket::form::{Form, FromForm};
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::response::Redirect;
|
||||||
|
use rocket::serde::uuid::Uuid;
|
||||||
|
use rocket::{post, uri, State};
|
||||||
|
use sled::Transactional;
|
||||||
|
|
||||||
|
#[post("/category/<category_uuid>/start_session")]
|
||||||
|
pub fn start_session(
|
||||||
|
_auth: Authorized,
|
||||||
|
category_uuid: Uuid,
|
||||||
|
event_notifier: &State<EventNotifier>,
|
||||||
|
db: &State<sled::Db>,
|
||||||
|
) -> Result<StatusJson, StatusJson> {
|
||||||
|
toggle_category_session(category_uuid, true, event_notifier, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/category/<category_uuid>/end_session")]
|
||||||
|
pub fn end_session(
|
||||||
|
_auth: Authorized,
|
||||||
|
category_uuid: Uuid,
|
||||||
|
event_notifier: &State<EventNotifier>,
|
||||||
|
db: &State<sled::Db>,
|
||||||
|
) -> Result<StatusJson, StatusJson> {
|
||||||
|
toggle_category_session(category_uuid, false, event_notifier, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_category_session(
|
||||||
|
category_uuid: Uuid,
|
||||||
|
set_active: bool,
|
||||||
|
event_notifier: &State<EventNotifier>,
|
||||||
|
db: &State<sled::Db>,
|
||||||
|
) -> Result<StatusJson, StatusJson> {
|
||||||
|
use crate::database::latest::trees::{category, session};
|
||||||
|
|
||||||
|
let category_uuid_s = sled::IVec::from(serialize(&category_uuid)?);
|
||||||
|
|
||||||
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
|
let sessions_tree = db.open_tree(session::NAME)?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
(&categories_tree, &sessions_tree).transaction(|(tx_categories, tx_sessions)| {
|
||||||
|
match tx_categories.get(&category_uuid_s)? {
|
||||||
|
None => return Ok(Err(Status::NotFound)),
|
||||||
|
Some(data) => {
|
||||||
|
let mut category: category::V = deserialize(&data).unwrap();
|
||||||
|
let now = Local::now();
|
||||||
|
|
||||||
|
match (set_active, category.started.take()) {
|
||||||
|
(false, Some(started)) => {
|
||||||
|
// only save sessions longer than 5 minutes
|
||||||
|
let duration = now - started;
|
||||||
|
if duration > Duration::minutes(5) {
|
||||||
|
let session_uuid = serialize(&uuid::Uuid::new_v4()).unwrap();
|
||||||
|
let session = session::V {
|
||||||
|
category: category_uuid,
|
||||||
|
started,
|
||||||
|
ended: now,
|
||||||
|
deleted: category.deleted,
|
||||||
|
};
|
||||||
|
tx_sessions.insert(session_uuid, serialize(&session).unwrap())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(true, None) => {
|
||||||
|
category.started = Some(now);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Category is already in the correct state
|
||||||
|
return Ok(Ok(Status::Ok.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?;
|
||||||
|
event_notifier.notify_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Ok(Status::Ok.into()))
|
||||||
|
})??,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct EditSession {
|
||||||
|
category: Uuid,
|
||||||
|
started: String,
|
||||||
|
ended: String,
|
||||||
|
deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/session/<session_uuid>/edit", data = "<session>")]
|
||||||
|
pub fn edit_session(
|
||||||
|
_auth: Authorized,
|
||||||
|
session_uuid: Uuid,
|
||||||
|
session: Form<EditSession>,
|
||||||
|
db: &State<sled::Db>,
|
||||||
|
) -> Result<Redirect, StatusJson> {
|
||||||
|
use crate::database::latest::trees::session;
|
||||||
|
|
||||||
|
let session_uuid_s = sled::IVec::from(serialize(&session_uuid)?);
|
||||||
|
|
||||||
|
let session = session::V {
|
||||||
|
category: session.category,
|
||||||
|
started: Local
|
||||||
|
.from_local_datetime(&NaiveDateTime::parse_from_str(
|
||||||
|
&session.started,
|
||||||
|
"%Y-%m-%d %H:%M",
|
||||||
|
)?)
|
||||||
|
.unwrap(),
|
||||||
|
ended: Local
|
||||||
|
.from_local_datetime(&NaiveDateTime::parse_from_str(
|
||||||
|
&session.ended,
|
||||||
|
"%Y-%m-%d %H:%M",
|
||||||
|
)?)
|
||||||
|
.unwrap(),
|
||||||
|
deleted: session.deleted,
|
||||||
|
};
|
||||||
|
|
||||||
|
if session.started >= session.ended {
|
||||||
|
return Err(StatusJson::new(
|
||||||
|
Status::BadRequest,
|
||||||
|
"started must be earlier than ended",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
db.open_tree(session::NAME)?
|
||||||
|
.insert(session_uuid_s, serialize(&session)?)?;
|
||||||
|
|
||||||
|
// FIXME: Uuid does not implement FromUriParam for some reason... File an issue?
|
||||||
|
//Ok(Redirect::to(uri!(pages::session_edit: session_uuid)))
|
||||||
|
Ok(Redirect::to(format!("/session/{}/edit", session_uuid)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/session/<session_uuid>/delete")]
|
||||||
|
pub fn delete_session(
|
||||||
|
_auth: Authorized,
|
||||||
|
session_uuid: Uuid,
|
||||||
|
db: &State<sled::Db>,
|
||||||
|
) -> Result<Redirect, StatusJson> {
|
||||||
|
use crate::database::latest::trees::session;
|
||||||
|
|
||||||
|
let session_uuid_s = sled::IVec::from(serialize(&session_uuid)?);
|
||||||
|
|
||||||
|
let sessions_tree = db.open_tree(session::NAME)?;
|
||||||
|
|
||||||
|
match sessions_tree.remove(session_uuid_s)? {
|
||||||
|
Some(_) => Ok(Redirect::to(uri!(pages::history))),
|
||||||
|
None => Err(Status::NotFound.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: mark as deleted instead of removing
|
||||||
|
// Ok(sessions_tree.transaction(|tx_sessions| {
|
||||||
|
// match tx_sessions.get(&session_uuid_s)? {
|
||||||
|
// None => return Ok(Err(Status::NotFound)),
|
||||||
|
// Some(data) => {
|
||||||
|
// let mut session: session::V = deserialize(&data).unwrap();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Ok(Ok(Redirect::to(uri!(pages::history))))
|
||||||
|
// })??)
|
||||||
|
}
|
||||||
@ -1,22 +1,25 @@
|
|||||||
|
pub mod dailies;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod weeks;
|
pub mod weeks;
|
||||||
|
|
||||||
use crate::auth::Authorized;
|
use crate::auth::Authorized;
|
||||||
use crate::database::latest::trees::{category, session};
|
use crate::database::latest::trees::{category, session};
|
||||||
|
use crate::routes::api;
|
||||||
use crate::status_json::StatusJson;
|
use crate::status_json::StatusJson;
|
||||||
use crate::util::EventNotifier;
|
use crate::util::EventNotifier;
|
||||||
use bincode::{deserialize, serialize};
|
use bincode::{deserialize, serialize};
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
|
use rocket::response::content::RawHtml;
|
||||||
use rocket::response::Redirect;
|
use rocket::response::Redirect;
|
||||||
|
use rocket::serde::uuid::Uuid;
|
||||||
use rocket::{get, post, uri, State};
|
use rocket::{get, post, uri, State};
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use rocket_contrib::uuid::Uuid;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub fn index(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
pub fn index(_auth: Authorized, db: &State<sled::Db>) -> Result<RawHtml<Template>, StatusJson> {
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
struct Node {
|
struct Node {
|
||||||
category: category::V,
|
category: category::V,
|
||||||
@ -89,17 +92,17 @@ pub fn index(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, Sta
|
|||||||
categories: top_level_nodes,
|
categories: top_level_nodes,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Template::render("index", &context))
|
Ok(RawHtml(Template::render("index", &context)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/category/<category_uuid>/start_session")]
|
#[post("/category/<category_uuid>/start_session")]
|
||||||
pub fn start_session(
|
pub fn start_session(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
category_uuid: Uuid,
|
category_uuid: Uuid,
|
||||||
event_notifier: State<'_, EventNotifier>,
|
event_notifier: &State<EventNotifier>,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<Redirect, StatusJson> {
|
) -> Result<Redirect, StatusJson> {
|
||||||
super::api::toggle_category_session(category_uuid, true, event_notifier, db)?;
|
api::session::toggle_category_session(category_uuid, true, event_notifier, db)?;
|
||||||
Ok(Redirect::to(uri!(index)))
|
Ok(Redirect::to(uri!(index)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,10 +110,10 @@ pub fn start_session(
|
|||||||
pub fn end_session(
|
pub fn end_session(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
category_uuid: Uuid,
|
category_uuid: Uuid,
|
||||||
event_notifier: State<'_, EventNotifier>,
|
event_notifier: &State<EventNotifier>,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<Redirect, StatusJson> {
|
) -> Result<Redirect, StatusJson> {
|
||||||
super::api::toggle_category_session(category_uuid, false, event_notifier, db)?;
|
api::session::toggle_category_session(category_uuid, false, event_notifier, db)?;
|
||||||
Ok(Redirect::to(uri!(index)))
|
Ok(Redirect::to(uri!(index)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,12 +122,12 @@ pub fn bump_session(
|
|||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
category_uuid: Uuid,
|
category_uuid: Uuid,
|
||||||
minutes: i64,
|
minutes: i64,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<Redirect, StatusJson> {
|
) -> Result<Redirect, StatusJson> {
|
||||||
use crate::database::latest::trees::category;
|
use crate::database::latest::trees::category;
|
||||||
|
|
||||||
let duration = chrono::Duration::minutes(minutes);
|
let duration = chrono::Duration::minutes(minutes);
|
||||||
let category_uuid_s = sled::IVec::from(serialize(&category_uuid.into_inner())?);
|
let category_uuid_s = sled::IVec::from(serialize(&category_uuid)?);
|
||||||
|
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
|
|
||||||
@ -162,15 +165,15 @@ pub fn bump_session(
|
|||||||
pub fn session_edit(
|
pub fn session_edit(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
session_uuid: Uuid,
|
session_uuid: Uuid,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<Template, StatusJson> {
|
) -> Result<RawHtml<Template>, StatusJson> {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct SessionPageContext {
|
struct SessionPageContext {
|
||||||
session: session::V,
|
session: session::V,
|
||||||
session_id: uuid::Uuid,
|
session_id: uuid::Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_uuid_s = sled::IVec::from(serialize(&session_uuid.into_inner())?);
|
let session_uuid_s = sled::IVec::from(serialize(&session_uuid)?);
|
||||||
|
|
||||||
let sessions_tree = db.open_tree(session::NAME)?;
|
let sessions_tree = db.open_tree(session::NAME)?;
|
||||||
match sessions_tree.get(session_uuid_s)? {
|
match sessions_tree.get(session_uuid_s)? {
|
||||||
@ -178,16 +181,16 @@ pub fn session_edit(
|
|||||||
Some(data) => {
|
Some(data) => {
|
||||||
let context = SessionPageContext {
|
let context = SessionPageContext {
|
||||||
session: deserialize(&data).unwrap(),
|
session: deserialize(&data).unwrap(),
|
||||||
session_id: session_uuid.into_inner(),
|
session_id: session_uuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Template::render("edit_session", &context))
|
Ok(RawHtml(Template::render("edit_session", &context)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/history")]
|
#[get("/history")]
|
||||||
pub fn history(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
pub fn history(_auth: Authorized, db: &State<sled::Db>) -> Result<RawHtml<Template>, StatusJson> {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct HistoryEntryContext {
|
struct HistoryEntryContext {
|
||||||
category: category::V,
|
category: category::V,
|
||||||
@ -234,5 +237,5 @@ pub fn history(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, S
|
|||||||
context.entries.sort_by_key(|entry| entry.session.started);
|
context.entries.sort_by_key(|entry| entry.session.started);
|
||||||
context.entries.reverse();
|
context.entries.reverse();
|
||||||
|
|
||||||
Ok(Template::render("history", &context))
|
Ok(RawHtml(Template::render("history", &context)))
|
||||||
}
|
}
|
||||||
|
|||||||
58
server/src/routes/pages/dailies.rs
Normal file
58
server/src/routes/pages/dailies.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use rocket::{
|
||||||
|
form::Form,
|
||||||
|
get, post,
|
||||||
|
response::{content::RawHtml, Redirect},
|
||||||
|
uri, FromForm, State,
|
||||||
|
};
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::Authorized,
|
||||||
|
database::latest::trees::daily::{self, V as Daily},
|
||||||
|
status_json::StatusJson,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
pub struct NewDaily {
|
||||||
|
name: String,
|
||||||
|
unit: String,
|
||||||
|
unit_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/dailies/new", data = "<daily>")]
|
||||||
|
pub fn new_daily(
|
||||||
|
_auth: Authorized,
|
||||||
|
db: &State<sled::Db>,
|
||||||
|
daily: Form<NewDaily>,
|
||||||
|
) -> Result<Redirect, StatusJson> {
|
||||||
|
let daily = daily.into_inner();
|
||||||
|
let daily = Daily {
|
||||||
|
name: daily.name,
|
||||||
|
unit: serde_json::from_str(&format!("\"{}\"", daily.unit)).unwrap(), // TODO
|
||||||
|
unit_count: daily.unit_count,
|
||||||
|
deleted: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let dailies_tree = db.open_tree(daily::NAME)?;
|
||||||
|
daily::put(&dailies_tree, &Uuid::new_v4(), &daily)?;
|
||||||
|
|
||||||
|
Ok(Redirect::to(uri!(dailies)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/dailies")]
|
||||||
|
pub fn dailies(_auth: Authorized, db: &State<sled::Db>) -> Result<RawHtml<Template>, StatusJson> {
|
||||||
|
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||||
|
struct TemplateContext {
|
||||||
|
dailies: Vec<Daily>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let dailies_tree = db.open_tree(daily::NAME)?;
|
||||||
|
let mut ctx = TemplateContext::default();
|
||||||
|
|
||||||
|
ctx.dailies = daily::get_all(&dailies_tree)?.into_values().collect();
|
||||||
|
ctx.dailies.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
Ok(RawHtml(Template::render("dailies", &ctx)))
|
||||||
|
}
|
||||||
@ -2,12 +2,10 @@ use crate::auth::Authorized;
|
|||||||
use crate::database::latest::trees::{category, session};
|
use crate::database::latest::trees::{category, session};
|
||||||
use crate::status_json::StatusJson;
|
use crate::status_json::StatusJson;
|
||||||
use crate::util::OrdL;
|
use crate::util::OrdL;
|
||||||
use chrono::{Date, DateTime, Datelike, Duration, Local, Timelike};
|
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, Timelike};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rocket::http::Status;
|
use rocket::{get, http::Status, response::content::RawHtml, serde::uuid::Uuid, State};
|
||||||
use rocket::{get, State};
|
use rocket_dyn_templates::Template;
|
||||||
use rocket_contrib::templates::Template;
|
|
||||||
use rocket_contrib::uuid::Uuid;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
@ -142,8 +140,8 @@ fn category_stats_ctx(
|
|||||||
pub fn single_stats(
|
pub fn single_stats(
|
||||||
_auth: Authorized,
|
_auth: Authorized,
|
||||||
category_id: Uuid,
|
category_id: Uuid,
|
||||||
db: State<'_, sled::Db>,
|
db: &State<sled::Db>,
|
||||||
) -> Result<Template, StatusJson> {
|
) -> Result<RawHtml<Template>, StatusJson> {
|
||||||
let categories_tree = db.open_tree(category::NAME)?;
|
let categories_tree = db.open_tree(category::NAME)?;
|
||||||
let sessions_tree = db.open_tree(session::NAME)?;
|
let sessions_tree = db.open_tree(session::NAME)?;
|
||||||
|
|
||||||
@ -154,12 +152,12 @@ pub fn single_stats(
|
|||||||
let sessions: HashMap<session::K, session::V> = session::get_all(&sessions_tree)?;
|
let sessions: HashMap<session::K, session::V> = session::get_all(&sessions_tree)?;
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let ctx = category_stats_ctx(now, *category_id, category, &sessions, &child_map);
|
let ctx = category_stats_ctx(now, category_id, category, &sessions, &child_map);
|
||||||
Ok(Template::render("stats_single", dbg!(&ctx)))
|
Ok(RawHtml(Template::render("stats_single", dbg!(&ctx))))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/stats")]
|
#[get("/stats")]
|
||||||
pub fn all_stats(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
|
pub fn all_stats(_auth: Authorized, db: &State<sled::Db>) -> Result<RawHtml<Template>, StatusJson> {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct StatsContext {
|
struct StatsContext {
|
||||||
categories_stats: Vec<CategoryStatsCtx>,
|
categories_stats: Vec<CategoryStatsCtx>,
|
||||||
@ -187,15 +185,11 @@ pub fn all_stats(_auth: Authorized, db: State<'_, sled::Db>) -> Result<Template,
|
|||||||
|
|
||||||
let context = StatsContext { categories_stats };
|
let context = StatsContext { categories_stats };
|
||||||
|
|
||||||
Ok(Template::render("stats_all", &context))
|
Ok(RawHtml(Template::render("stats_all", &context)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the duration of `day` that is covered by the span `start..end`
|
/// Compute the subset of `day` that is covered by the span `start..end`
|
||||||
fn span_duration_of_day(
|
fn span_duration_of_day(start: DateTime<Local>, end: DateTime<Local>, day: NaiveDate) -> Duration {
|
||||||
start: DateTime<Local>,
|
|
||||||
end: DateTime<Local>,
|
|
||||||
day: Date<Local>,
|
|
||||||
) -> Duration {
|
|
||||||
if end < start {
|
if end < start {
|
||||||
panic!("start must come before end");
|
panic!("start must come before end");
|
||||||
}
|
}
|
||||||
@ -203,26 +197,29 @@ fn span_duration_of_day(
|
|||||||
// if the span is 0
|
// if the span is 0
|
||||||
// or if the day is not in the span
|
// or if the day is not in the span
|
||||||
// the duration is zero
|
// the duration is zero
|
||||||
if end == start || start.date() > day || end.date() < day {
|
if end == start || start.date_naive() > day || end.date_naive() < day {
|
||||||
return Duration::zero();
|
return Duration::zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
if start.date() < day {
|
// TODO: deal with unwrap
|
||||||
if end.date() > day {
|
let day_start = day.and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Local).unwrap();
|
||||||
|
|
||||||
|
if start.date_naive() < day {
|
||||||
|
if end.date_naive() > day {
|
||||||
Duration::days(1)
|
Duration::days(1)
|
||||||
} else {
|
} else {
|
||||||
debug_assert_eq!(end.date(), day);
|
debug_assert_eq!(end.date_naive(), day);
|
||||||
|
|
||||||
end - day.and_hms(0, 0, 0)
|
end - day_start
|
||||||
}
|
}
|
||||||
} else if end.date() > day {
|
} else if end.date_naive() > day {
|
||||||
debug_assert_eq!(start.date(), day);
|
debug_assert_eq!(start.date_naive(), day);
|
||||||
|
|
||||||
day.and_hms(0, 0, 0) + Duration::days(1) - start
|
day_start + Duration::days(1) - start
|
||||||
} else {
|
} else {
|
||||||
debug_assert!(start < end);
|
debug_assert!(start < end);
|
||||||
debug_assert_eq!(start.date(), day);
|
debug_assert_eq!(start.date_naive(), day);
|
||||||
debug_assert_eq!(end.date(), day);
|
debug_assert_eq!(end.date_naive(), day);
|
||||||
|
|
||||||
end - start
|
end - start
|
||||||
}
|
}
|
||||||
@ -309,14 +306,14 @@ where
|
|||||||
{
|
{
|
||||||
const NUM_WEEKS: usize = 12;
|
const NUM_WEEKS: usize = 12;
|
||||||
|
|
||||||
let today = Local::today();
|
let today = Local::now().date_naive();
|
||||||
let last_day = today
|
let last_day = today
|
||||||
// take at least NUM_WEEKS * 7 days
|
// take at least NUM_WEEKS * 7 days
|
||||||
- Duration::weeks(NUM_WEEKS as i64)
|
- Duration::weeks(NUM_WEEKS as i64)
|
||||||
// round up to nearest monday
|
// round up to nearest monday
|
||||||
- Duration::days(today.weekday().num_days_from_monday() as i64);
|
- Duration::days(today.weekday().num_days_from_monday() as i64);
|
||||||
|
|
||||||
let mut days: BTreeMap<Date<Local>, Duration> = Default::default();
|
let mut days: BTreeMap<NaiveDate, Duration> = Default::default();
|
||||||
|
|
||||||
// calculate the time spent logging this category for every day of the last NUM_WEEKS
|
// calculate the time spent logging this category for every day of the last NUM_WEEKS
|
||||||
for session in sessions {
|
for session in sessions {
|
||||||
@ -352,8 +349,8 @@ where
|
|||||||
|
|
||||||
let month = day.month();
|
let month = day.month();
|
||||||
|
|
||||||
let month_border = |other_day: Date<_>| other_day.month() != month;
|
let month_border = |other_day: NaiveDate| other_day.month() != month;
|
||||||
let month_or_week_border = |other_day: Date<_>| {
|
let month_or_week_border = |other_day: NaiveDate| {
|
||||||
other_day.iso_week() != week || month_border(other_day)
|
other_day.iso_week() != week || month_border(other_day)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@ use crate::auth::Authorized;
|
|||||||
use crate::status_json::StatusJson;
|
use crate::status_json::StatusJson;
|
||||||
use chrono::{Duration, Local, NaiveDate};
|
use chrono::{Duration, Local, NaiveDate};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rocket::{get, State};
|
use rocket::{get, response::content::RawHtml, State};
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::iter::{once, repeat};
|
use std::iter::{once, repeat};
|
||||||
@ -11,7 +11,10 @@ use std::iter::{once, repeat};
|
|||||||
pub struct BirthDate(pub NaiveDate);
|
pub struct BirthDate(pub NaiveDate);
|
||||||
|
|
||||||
#[get("/weeks")]
|
#[get("/weeks")]
|
||||||
pub fn weeks(_auth: Authorized, birth_date: State<BirthDate>) -> Result<Template, StatusJson> {
|
pub fn weeks(
|
||||||
|
_auth: Authorized,
|
||||||
|
birth_date: &State<BirthDate>,
|
||||||
|
) -> Result<RawHtml<Template>, StatusJson> {
|
||||||
type Color<'a> = Cow<'a, str>;
|
type Color<'a> = Cow<'a, str>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@ -54,10 +57,10 @@ pub fn weeks(_auth: Authorized, birth_date: State<BirthDate>) -> Result<Template
|
|||||||
periods: Vec<PeriodCtx<'a>>,
|
periods: Vec<PeriodCtx<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Local::now().date();
|
let now = Local::now().date_naive();
|
||||||
let birth_date = birth_date.0;
|
let birth_date = birth_date.0;
|
||||||
|
|
||||||
let lived: Duration = now.naive_local() - birth_date;
|
let lived: Duration = now - birth_date;
|
||||||
let one_year = Duration::days(365);
|
let one_year = Duration::days(365);
|
||||||
|
|
||||||
let life_expectancy = (one_year * 81).num_weeks();
|
let life_expectancy = (one_year * 81).num_weeks();
|
||||||
@ -105,5 +108,5 @@ pub fn weeks(_auth: Authorized, birth_date: State<BirthDate>) -> Result<Template
|
|||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Template::render("weeks", &context))
|
Ok(RawHtml(Template::render("weeks", &context)))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,8 @@ use duplicate::duplicate;
|
|||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::response::{Responder, Response};
|
use rocket::response::{Responder, Response};
|
||||||
|
use rocket::serde::json::{json, Json};
|
||||||
use rocket::Request;
|
use rocket::Request;
|
||||||
use rocket_contrib::json;
|
|
||||||
use rocket_contrib::json::Json; // macro
|
|
||||||
|
|
||||||
/// An error message which can be serialized as JSON.
|
/// An error message which can be serialized as JSON.
|
||||||
///
|
///
|
||||||
@ -81,7 +80,7 @@ impl From<T> for StatusJson {
|
|||||||
impl From<Status> for StatusJson {
|
impl From<Status> for StatusJson {
|
||||||
fn from(status: Status) -> StatusJson {
|
fn from(status: Status) -> StatusJson {
|
||||||
StatusJson {
|
StatusJson {
|
||||||
description: status.reason.to_string(),
|
description: status.reason().map(|r| r.to_string()).unwrap_or_default(),
|
||||||
status,
|
status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -233,6 +233,88 @@ ul.striped_list > li:nth-child(odd) ul li:nth-child(odd) { background-color:#30
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dailies_list {
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dailies_entry {
|
||||||
|
color: wheat;
|
||||||
|
font-size: 1.5em;
|
||||||
|
background: #3a3743;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
border: .2rem solid #45374f;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: background-color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dailies_entry:hover {
|
||||||
|
background-color: #4c4858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dailies_entry:active {
|
||||||
|
background-color: #312e38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dailies_entry > span {
|
||||||
|
flex-grow: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
.dailies_checkbox input[type=checkbox] {
|
||||||
|
visibility:hidden;
|
||||||
|
display:none
|
||||||
|
}
|
||||||
|
.dailies_checkbox *,
|
||||||
|
.dailies_checkbox :after,
|
||||||
|
.dailies_checkbox :before {
|
||||||
|
box-sizing:border-box
|
||||||
|
}
|
||||||
|
.dailies_checkbox .container {
|
||||||
|
cursor:pointer;
|
||||||
|
user-select:none;
|
||||||
|
font-size:25px;
|
||||||
|
display:block;
|
||||||
|
position:relative
|
||||||
|
}
|
||||||
|
.dailies_checkbox .checkmark {
|
||||||
|
--spread:10px;
|
||||||
|
background:#000;
|
||||||
|
border-radius:50px;
|
||||||
|
width:1.3em;
|
||||||
|
height:1.3em;
|
||||||
|
transition:all .7s;
|
||||||
|
position:relative;
|
||||||
|
top:0;
|
||||||
|
left:0
|
||||||
|
}
|
||||||
|
.dailies_checkbox .container input:checked~.checkmark {
|
||||||
|
box-shadow:-5px -5px var(--spread)0px #5b51d8,0 -5px var(--spread)0px #833ab4,5px -5px var(--spread)0px #e1306c,5px 0 var(--spread)0px #fd1d1d,5px 5px var(--spread)0px #f77737,0 5px var(--spread)0px #fcaf45,-5px 5px var(--spread)0px #ffdc80;
|
||||||
|
background:#000
|
||||||
|
}
|
||||||
|
.dailies_checkbox .checkmark:after {
|
||||||
|
content:"";
|
||||||
|
display:none;
|
||||||
|
position:absolute
|
||||||
|
}
|
||||||
|
.dailies_checkbox .container input:checked~.checkmark:after {
|
||||||
|
display:block
|
||||||
|
}
|
||||||
|
.dailies_checkbox .container .checkmark:after {
|
||||||
|
border:.15em solid wheat;
|
||||||
|
border-width:0 .15em .15em 0;
|
||||||
|
width:.25em;
|
||||||
|
height:.5em;
|
||||||
|
top:.34em;
|
||||||
|
left:.5em;
|
||||||
|
transform:rotate(45deg)
|
||||||
|
}
|
||||||
|
|
||||||
.life_calendar {
|
.life_calendar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
14
server/templates/dailies.hbs
Normal file
14
server/templates/dailies.hbs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body>
|
||||||
|
{{> header}}
|
||||||
|
<h1>Dailies</h2>
|
||||||
|
|
||||||
|
<div class="category_list">
|
||||||
|
{{#each categories}}
|
||||||
|
{{>category_entry}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
server/templates/dailies.html.hbs
Normal file
37
server/templates/dailies.html.hbs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{> head}}
|
||||||
|
<body>
|
||||||
|
{{> header}}
|
||||||
|
|
||||||
|
<div class="striped_list dailies_list">
|
||||||
|
<h1>Dailies</h2>
|
||||||
|
{{#each dailies}}
|
||||||
|
<button class="dailies_entry">
|
||||||
|
<div class="dailies_checkbox"><label class="container"><input type="checkbox"><div class="checkmark"></div></label></div>
|
||||||
|
<span>
|
||||||
|
{{this.name}}
|
||||||
|
<span>1 gång per</span>
|
||||||
|
{{this.unit_count}}
|
||||||
|
{{this.unit}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<form action="/dailies/new" method="post">
|
||||||
|
<span>Namn</span>
|
||||||
|
<input type="text" name="name"></input>
|
||||||
|
<span>1 gång per</span>
|
||||||
|
<input type="number" name="unit_count"></input>
|
||||||
|
<select name="unit">
|
||||||
|
<option value="Day">Dag</option>
|
||||||
|
<option value="Week">Vecka</option>
|
||||||
|
<option value="Month">Månad</option>
|
||||||
|
<option value="Year">År</option>
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<button type="submit">spara</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user