First commit

This commit is contained in:
2025-05-12 10:58:23 +02:00
commit 244eb6e099
6 changed files with 210 additions and 0 deletions

31
db/createdb.sql Normal file
View File

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

5
db/drop.sql Normal file
View File

@@ -0,0 +1,5 @@
drop view dailylift;
drop table training;
drop table shorthand;
drop table exercise;
drop table account;

13
db/load.sql Normal file
View File

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

63
readme.md Normal file
View File

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

16
train-cli/Cargo.toml Normal file
View File

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

82
train-cli/src/main.rs Normal file
View File

@@ -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<String>,
}
fn main() {
let args = TrainingArgs::parse();
insert_training(args);
}
fn parse_time (time: String) -> Option<DateTime<FixedOffset>> {
// 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> = 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)
}
}
}
}