Compare commits

..

11 Commits

Author SHA1 Message Date
ffb728df64 wip 2 2024-04-16 23:32:55 +02:00
d6f069d4a5 update deps 2024-04-16 23:32:55 +02:00
79103c6712 wip 2024-04-16 23:32:53 +02:00
8a0401989c wip 2024-04-16 21:46:44 +02:00
5c11d5d091 Specify workspace resolver 2024-04-11 18:53:13 +02:00
48c0c85962 server: Add optional password query parameter 2022-05-01 02:04:37 +02:00
590d58c152 daemon: Trim cookie string
If the cookie file had a newline in it, the program would not work
2022-04-28 15:20:02 +02:00
e2649165a7 Update dependencies 2021-10-22 13:00:29 +02:00
7698911078 Update to rust 2021 2021-10-22 12:55:12 +02:00
b4e1774993 daemon: Fix poll after finished 2021-10-02 17:44:08 +02:00
8cf21681fa Test edit (bump version to 2.6.7) 2021-07-08 01:55:41 +02:00
25 changed files with 1811 additions and 1192 deletions

2079
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,3 +4,5 @@ members = [
"cli", "cli",
"lib", "lib",
] ]
resolver = "2"

View File

@ -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

View File

@ -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 }

View File

@ -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,15 +50,17 @@ 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) { match poll!(&mut wfe) {
Poll::Ready(Ok(Ok(_wfe))) => { Poll::Ready(Ok(Ok(_wfe))) => {
restart_wfe = true; restart_wfe = true;
@ -74,8 +78,15 @@ pub fn run(opt: DaemonOpt) -> Result<(), Error> {
Poll::Pending => { Poll::Pending => {
restart_wfe = false; restart_wfe = false;
update_categories = Some(false); update_categories = Some(false);
// wfe task is not complete, we will continue waiting for it next loop
wfe_handle = Some(wfe)
} }
} }
} else {
restart_wfe = false;
update_categories = None;
}
let elapsed = last_update.elapsed(); let elapsed = last_update.elapsed();
debug!( debug!(
@ -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,

View File

@ -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"] }

View File

@ -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,
}
}
// -------------------------------------
} }

View File

@ -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"]

View File

@ -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"
## =============================== ## ## =============================== ##

View File

@ -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));

View File

@ -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,
}; };

View File

@ -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(())
}
}
} }

View File

@ -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| {

View File

@ -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,
], ],

View File

@ -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(

View File

@ -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)?;

View 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))))
// })??)
}

View File

@ -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)))
} }

View 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)))
}

View File

@ -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)
}; };

View File

@ -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)))
} }

View File

@ -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,
} }
} }

View File

@ -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;

View 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>

View 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>