commit 244eb6e099746db6ea486d7f3f19e201f5296252 Author: Jakob Dalsgaard Date: Mon May 12 10:58:23 2025 +0200 First commit diff --git a/db/createdb.sql b/db/createdb.sql new file mode 100644 index 0000000..5229ad8 --- /dev/null +++ b/db/createdb.sql @@ -0,0 +1,31 @@ +create table exercise ( + id smallserial primary key, + name varchar not null +); +create table shorthand ( + exercise smallint not null, + name varchar unique not null, + constraint fk_exercise foreign key (exercise) references exercise(id) +); +create table account ( + id serial primary key, + name varchar not null, + login varchar not null, + birthdate date not null +); +create table training ( + id serial primary key, + account integer not null, + time timestamptz not null default timezone('utc', now()), + exercise smallint not null, + runs smallint not null, + reps smallint not null, + kilos numeric(4,1) not null, + constraint fk_exercise foreign key (exercise) references exercise(id), + constraint fk_account foreign key (account) references account(id) +); +create view dailylift as select + date_trunc('day', time) as time, account, exercise, sum(runs * reps * kilos) as lift + from training group by 1, 2, 3; + + diff --git a/db/drop.sql b/db/drop.sql new file mode 100644 index 0000000..c262d8f --- /dev/null +++ b/db/drop.sql @@ -0,0 +1,5 @@ +drop view dailylift; +drop table training; +drop table shorthand; +drop table exercise; +drop table account; diff --git a/db/load.sql b/db/load.sql new file mode 100644 index 0000000..20db432 --- /dev/null +++ b/db/load.sql @@ -0,0 +1,13 @@ +insert into exercise (name) values ('Bench Press'); +insert into shorthand values(currval('exercise_id_seq'), 'bp'); +insert into exercise (name) values ('Abdominals'); +insert into shorthand values(currval('exercise_id_seq'), 'abs'); +insert into exercise (name) values ('Squat'); +insert into shorthand values(currval('exercise_id_seq'), 'squat'); +insert into exercise (name) values ('Biceps'); +insert into shorthand values(currval('exercise_id_seq'), 'biceps'); +insert into exercise (name) values ('Triceps'); +insert into shorthand values(currval('exercise_id_seq'), 'triceps'); +insert into account (name, login, birthdate) values ('Jakob Dalsgaard', 'jakob', '1975-03-08'); + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0b8b0c5 --- /dev/null +++ b/readme.md @@ -0,0 +1,63 @@ +Training Database +================= + +Toolset for registrering weight training with a focus on ease of registrering from +a command line. + +To install, start by creating the database: + +``` +create database training; +create user 'jakob'; +``` + +Edit `pg_hba.conf` to have 'jakob' access the training database directly: + +``` +# TYPE DATABASE USER ADDRESS METHOD +local training jakob peer +``` + +As postgres user, do a config reload: + +``` +select pg_reload_conf(); +``` + +Then install rust toolchain by rustup.rs -- make and install the binary (I am assuming you have +a `bin` directly in your homedir, that is in your path): + +``` +cd train-cli +cargo build --release +cp target/release/train-cli $HOME/bin/train +``` + +Right now the executable is hardcoded with database name `training` and the use +of unix domain socket in `/var/run/postgresql`. + +Now training can be invoked with: + +``` +train squat 3 10 60 +``` + +Which would register squauts, 3 runs of 10 reps of 60kg -- at local time and date. Optionally a +time, date time or rfc3339 timestamp can be specified: + +``` +train squat 3 10 60 "12:05:00" +train squat 3 10 60 "2025-12-24 18:00:00" +train squat 3 10 60 "2025-12-24T18:00:00+0200" +``` + +The two former will source missing date and timezone information from the user session, i.e. +type `date` on your commandline to see what you have. + +When travelling you might opt for specifying a specific location on the command line, like: + +``` +TZ=Australia/Sydney train squat 3 10 60 +``` + +Data in the database can be visualized with, for example, Grafana, more info to follow. diff --git a/train-cli/Cargo.toml b/train-cli/Cargo.toml new file mode 100644 index 0000000..981842f --- /dev/null +++ b/train-cli/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "train-cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +postgres = { version = "0.19.10", features = ["with-chrono-0_4"] } +sqlx = { version = "0.8.5", features = ["rust_decimal"] } +clap = { version = "4.5.37", features = ["derive"] } +whoami = "1.6.0" +rust_decimal = { version = "1.37.1", features = ["db-postgres"] } +chrono = "0.4.41" + + diff --git a/train-cli/src/main.rs b/train-cli/src/main.rs new file mode 100644 index 0000000..932887f --- /dev/null +++ b/train-cli/src/main.rs @@ -0,0 +1,82 @@ +use clap::Parser; +use postgres::{Client, NoTls}; +use whoami; +use rust_decimal::Decimal; +use rust_decimal::prelude::FromPrimitive; +use chrono::{DateTime, Local, FixedOffset, TimeZone, NaiveTime, LocalResult, NaiveDateTime}; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct TrainingArgs { + // name of exercise + exercise: String, + + // number of runs + runs: i16, + + // number of reps per run + reps: i16, + + // kilos + kilos: f32, + + // time + time: Option, +} + +fn main() { + let args = TrainingArgs::parse(); + insert_training(args); +} + +fn parse_time (time: String) -> Option> { + // check if time is an rfc3339 formatted timestamp, i.e. "2025-05-20T14:30:10+0200" (can also + // have milliseconds + let datetimetz = DateTime::parse_from_rfc3339(&time); + if let Ok(dt) = datetimetz { + return Some(dt); + } + // check if time is a simple "date time" without timezone, then apply user session + // time zone + let naive_datetime = NaiveDateTime::parse_from_str(&time, "%Y-%m-%d %H:%M:%S"); + if let Ok(ndt) = naive_datetime { + let dt: DateTime = Local.from_local_datetime(&ndt).unwrap(); + return Some(dt.into()); + } + // check if time is merely a simple hour:minute:second -- then apply + // current date from user session + let naive_time = NaiveTime::parse_from_str(&time, "%H:%M:%S"); + if let Ok(nt) = naive_time { + return match Local::now().with_time(nt) { + LocalResult::Single(dt) => Some(dt.into()), + LocalResult::Ambiguous(earliest, _latest) => Some(earliest.into()), + LocalResult::None => None + } + } + return None; +} + + +fn insert_training (args: TrainingArgs) { + let mut client = Client::connect("dbname=training host=/var/run/postgresql", NoTls).unwrap(); + let dec_kilos = Decimal::from_f32(args.kilos).unwrap(); + let dt = match args.time { + Some(time) => parse_time(time), + None => Some(Local::now().into()), + }; + + match dt { + None => println!("Invalid time/date specified"), + Some(time) => { + let res = client.execute("insert into training values + (default, (select id from account where login=$1), $2, + (select e.id from exercise e inner join shorthand s on s.exercise=e.id where s.name=$3), $4, $5, $6)", + &[&whoami::username(), &time, &args.exercise, &args.runs, &args.reps, &dec_kilos]); + match res { + Ok(_inserted) => println!("Training inserted"), + Err(e) => println!("Training not inserted, since: {}", e) + } + } + } + +}