From 18de2714d05c766f1298dfdf988f014d298ccb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Fri, 24 Oct 2025 23:08:23 +0200 Subject: [PATCH 01/10] make table strict, fix bug where app crashes on empty db --- foods.db | Bin 12288 -> 8192 bytes src/create_tables.sql | 2 +- src/main.rs | 12 ++++-------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/foods.db b/foods.db index a769939169bc94dd8d7dd8b4ed5755d245622c3f..c8e49e5ecb512dec93feab9e8697a967629394df 100644 GIT binary patch delta 360 zcmZojXmFSyCB(qQzyQV!P{24*$4G#Ii9xTbke7jhfsyYR1K+WYg%N!9P3Da3;-aFA zErBJ8Nja%$`S~dz%;X&8>KNjx5aQ_MQPAL0P*70FOi}Rk3vqRK4N?dQ^7M5K zid67+jno0l733F{Waj57gt$h8DERq@DENi?_&}uc5_40riSuVC#}}j)#TTa*m1X9o zD@^v{mk|NkQ<7Meo>~GEFIJdb36)4pE-6jSL6Y#|mjT14svx2aa9O$bn2Pr>733F{Waj57afY}?geds=hbZ`k`uKok@)C1Xu?S};#}}j)#TTa*m1X9o zE7ijcgX>jradmSH^$Afh0GU#fSd^Yx0#jZLw;NNrAxL>*a!F}o4l&BXrYGmK=HwSC zaZbL)FDs_Q#TgLf>FXF2so?DzsR8n-CYL5CoHjPBW>n$jwPfJq`NzU%%zu>Eo4=1g zgkO;FI^Qh5G@gHath_IIck?dXSa_3%(Q@*4S!*HQYz84|TSi7sX+wP@1?SQN_sM1Q z3Y>g83}(`%)I2$5vy{ox PreparedStatements { fn check(conn: &Connection) -> rusqlite::Result { - conn.prepare_cached(include_str!("create_tables.sql"))?; conn.prepare_cached(include_str!("increase.sql"))?; conn.prepare_cached(include_str!("decrease.sql"))?; conn.prepare_cached(include_str!("get_food.sql"))?; @@ -91,11 +90,6 @@ impl<'conn> PreparedStatements { Ok(PreparedStatements {}) } - fn create_tables(conn: &'conn Connection) -> CachedStatement<'conn> { - conn.prepare_cached(include_str!("create_tables.sql")) - .expect("cached statement is invalid") - } - fn increase(conn: &'conn Connection) -> CachedStatement<'conn> { conn.prepare_cached(include_str!("increase.sql")) .expect("cached statement is invalid") @@ -149,12 +143,14 @@ async fn main() -> Result<(), std::io::Error> { let db_connecion_str = "./foods.db".to_string(); debug!(db_connecion_str, "opening database"); let conn = Connection::open(db_connecion_str).expect("failed to open database"); - PreparedStatements::check(&conn).expect("failed to prepare sql statements"); - if let Err(e) = PreparedStatements::create_tables(&conn).execute(()) { + + if let Err(e) = conn.execute(include_str!("create_tables.sql"), ()) { error!(?e, "failed to create tables"); panic!("failed to create tables: {:#?}", e); } + PreparedStatements::check(&conn).expect("failed to prepare sql statements"); + let app = Router::new() .route("/", get(root)) .route("/increase/{id}", post(increase)) From 07bfc65b6754d39e117f50e54b1e00ef24192d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Fri, 24 Oct 2025 23:26:26 +0200 Subject: [PATCH 02/10] new data format with more information --- foods.db | Bin 8192 -> 12288 bytes src/get_foods.sql | 2 ++ src/get_version.sql | 1 + src/main.rs | 50 +++++++++++++++++++++++++++++++++++++++++-- src/migrations/1.sql | 6 ++++++ src/migrations/2.sql | 23 ++++++++++++++++++++ src/migrations/3.sql | 6 ++++++ src/migrations/4.sql | 23 ++++++++++++++++++++ 8 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/get_version.sql create mode 100644 src/migrations/1.sql create mode 100644 src/migrations/2.sql create mode 100644 src/migrations/3.sql create mode 100644 src/migrations/4.sql diff --git a/foods.db b/foods.db index c8e49e5ecb512dec93feab9e8697a967629394df..81ff588f777d56554ae5df3a96484e5c93a53a99 100644 GIT binary patch literal 12288 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCV31&7U|?ooU|?ckU|?imU=UznU|?ZD z0FW3mA0dNaNigVD74m`%;N@oE<>vf|kQ!x;hQMeDjE2By2#kinXb6mkz-S22B?Q_e zS=hz(^%=VpOA?cEQi}_7lHyBJa|`0Z+;~F_0cPhQSH}=ng%C$4A6Eq>40$C54K7Zl z%oHU~PrndXch?{tE>5L_{GyV~{5&Pj5Z8zh1wa1~1;0=qACOF5Vs0uH;q2u2g4Ck; z;?$zD%)E5CS#YfiF0O8lp*|rB1|UO95{uGPOJJ&t@hCS0DNjr;DNW2FMmgB@!A`+I6Kqydeo1O( zo)Tw}tD_I5uTAw3CO}mx*x5q$re!9j77?Knq7I?20Adn2;Gup&H^tr6&o#&~#MMQ? z(I?z7GFZVeSV2QW393^`L0`epz(7+$O99c%xXIk&c2# zeqLs}j)HGuQEqBZVoItAOgBt3jAj)v;Nf8q7eyH2Tv`yEm{t;iO9#saQ*IsxX>}$} zr~~zl6r4*7+;UPgi<2|--9RM63FS1i)Rg3u6cLz3%q;<2JPe}2jEs!Zj8NCQrIwT^ zXgHOYlz`mqS6W=01Z5wdmZOkblB}nx!OR3R3T6_FW`5zx$-^M+$jHb6F;U+HB zi$6OLgLoh#Bk^Vyc?ufgrA1Y# zIeDNokXa1&zGeW-WCzBXATY~;6-3RP#i--Y3UMklBthPH%1JCKNr(7WK_fUZC$R)% zSYCR5P71_4E)1vI|A6FQrU+dY9tK%aNYpSg87esIgOag-QgKNDLf2)8$C#M*=rQv! zh{`i^g3<;^xvP&)YGz(>Vs2`l9J8`nibaY=VhSv_SXnE4n0OeZU75h@*_jL>F&~g$ zkepwXr=w7mpP`^(YnWVGpx~ZbQk0pJs>yJI4Q7mk&>0Xo>mUVjEW-(RMji%fTWHE; zgqjA@nVOlBs_$2t0uD%Mgr_hw!A+8cgr9>Tgihh*$!Fl>{>{L@oj;5B0l5Dk$Nig+ zlee7LjOQKC8J_7pzEDd?snHM^4S~@R7!85Z5Eu=C(GVC7fzc2c4S}H>0s+v@9;gQc z>We#<77*w_f;xLlOt3x~W^YdcX&it9%Qygw53ekP1~DOKj2I{20GUL@I037O0%BYM ztO?ZFOhg(NVA-IHG!DSQWQczpfVqWt7lR~L*V;n{1~?d{8NoJ%_z*KN03R1%e&L8T z4ghipp>Y5<^l<=Y1%68gHLQ+95mbkCQ$a%*92nhHct4euwIUcYoIpa?AKJS#Vu2+H z%<+UsNcSJv&|p%!|M0Xn`1=103=D=SAx(he;|(1EfDTv?GXMbZwNu#tXM#tx1A6}- E0CH62hX4Qo delta 593 zcmZojXmFSy&C0~Uz`!)oK$?+pVxclW6N6q=Auj_110&xt2EJpP4FyDaHa560)i;?l zvWts~GPVSkBqrsgrse0SfH0GDkgH>et3rsQlaH$cNJK${OF=f-~E%1g{m#U{?5og80~S`=TL zT2z*qm##3`o=;NT0HUWPu_!&Y1SVAsl9LjHNF^qhlqTk&NJ;WSwI%206eJ^IqWH!rR810}T@sMimJ@ z4hBgpE=w*B2GL+fMn>t$+OmdRMw}cB(vFOb9MY3(Wwp4h zIXD=^BN;h4r6Vs2`l s9J8`nibaY=qA3#xgS0CXC!_S_3VqedbL2U=3>i5Xq;0{DJu0sW0H30Ui~s-t diff --git a/src/get_foods.sql b/src/get_foods.sql index dbd3e94..69523a1 100644 --- a/src/get_foods.sql +++ b/src/get_foods.sql @@ -8,3 +8,5 @@ SELECT color FROM food +ORDER BY + sort_order, name; diff --git a/src/get_version.sql b/src/get_version.sql new file mode 100644 index 0000000..4edeca1 --- /dev/null +++ b/src/get_version.sql @@ -0,0 +1 @@ +PRAGMA user_version; diff --git a/src/main.rs b/src/main.rs index d1d1eb7..3ab24fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use axum::{ routing::{get, post}, }; use parking_lot::Mutex; -use rusqlite::{CachedStatement, Connection, Row}; +use rusqlite::{CachedStatement, Connection, Row, Transaction}; use tower_http::trace::TraceLayer; use tower_request_id::{RequestId, RequestIdLayer}; use tracing::{debug, error, info, info_span}; @@ -123,6 +123,50 @@ impl<'conn> PreparedStatements { type ConnState = Arc>; +fn get_version(tx: &Transaction) -> rusqlite::Result { + tx.query_one(include_str!("get_version.sql"), (), |row| row.get(0)) +} + +fn get_migrations() -> [&'static str; 4] { + [ + include_str!("migrations/1.sql"), + include_str!("migrations/2.sql"), + include_str!("migrations/3.sql"), + include_str!("migrations/4.sql"), + ] +} + +fn do_migrations(conn: &mut Connection) -> rusqlite::Result<()> { + let migrations = get_migrations(); + let num_migrations = migrations.len(); + let tx = conn.transaction()?; + let version = get_version(&tx)?; + if version < migrations.len() { + info!( + migrations_to_apply = num_migrations - version, + "need to apply some migrations" + ); + let mut mig_number = version; + for migration in migrations.iter().skip(version) { + mig_number += 1; + info!(mig_number, "applying migration"); + debug!(migration = migration); + tx.execute_batch(migration)?; + if get_version(&tx)? != mig_number { + panic!( + "expected user_version to eq {} after applying migration {}. maybe a missing 'PRAGMA user_version =' ?", + mig_number, mig_number + ); + } + } + tx.commit()?; + info!("applied all migrations"); + } else { + info!("no migrations to apply"); + } + Ok(()) +} + #[tokio::main] async fn main() -> Result<(), std::io::Error> { tracing_subscriber::registry() @@ -142,13 +186,15 @@ async fn main() -> Result<(), std::io::Error> { let db_connecion_str = "./foods.db".to_string(); debug!(db_connecion_str, "opening database"); - let conn = Connection::open(db_connecion_str).expect("failed to open database"); + let mut conn = Connection::open(db_connecion_str).expect("failed to open database"); if let Err(e) = conn.execute(include_str!("create_tables.sql"), ()) { error!(?e, "failed to create tables"); panic!("failed to create tables: {:#?}", e); } + do_migrations(&mut conn).expect("failed to do database migrations"); + PreparedStatements::check(&conn).expect("failed to prepare sql statements"); let app = Router::new() diff --git a/src/migrations/1.sql b/src/migrations/1.sql new file mode 100644 index 0000000..b5d5cff --- /dev/null +++ b/src/migrations/1.sql @@ -0,0 +1,6 @@ +ALTER TABLE + food +ADD COLUMN + portion_weight INTEGER NOT NULL DEFAULT 100 CHECK (portion_weight > 0); + +PRAGMA user_version = 1; diff --git a/src/migrations/2.sql b/src/migrations/2.sql new file mode 100644 index 0000000..755c45c --- /dev/null +++ b/src/migrations/2.sql @@ -0,0 +1,23 @@ +ALTER TABLE + food +ADD COLUMN + -- per 100g + protein REAL NOT NULL DEFAULT 5.0 CHECK (protein > 0); + +ALTER TABLE + food +ADD COLUMN + -- per 100g + fiber REAL NOT NULL DEFAULT 5.0 CHECK (fiber > 0); + +ALTER TABLE + food +ADD COLUMN + protein_per_portion REAL NOT NULL GENERATED ALWAYS AS ((protein / 100) * portion_weight) VIRTUAL; + +ALTER TABLE + food +ADD COLUMN + fiber_per_portion REAL NOT NULL GENERATED ALWAYS AS ((fiber / 100) * portion_weight) VIRTUAL; + +PRAGMA user_version = 2; diff --git a/src/migrations/3.sql b/src/migrations/3.sql new file mode 100644 index 0000000..46c6d51 --- /dev/null +++ b/src/migrations/3.sql @@ -0,0 +1,6 @@ +ALTER TABLE + food +ADD COLUMN + sort_order INTEGER NOT NULL DEFAULT 0; + +PRAGMA user_version = 3; diff --git a/src/migrations/4.sql b/src/migrations/4.sql new file mode 100644 index 0000000..c6d9caa --- /dev/null +++ b/src/migrations/4.sql @@ -0,0 +1,23 @@ +CREATE TABLE "sqlb_temp_table_1" ( + "id" INTEGER, + "portion" TEXT NOT NULL, + "name" TEXT NOT NULL, + "kc_per_serving" INTEGER NOT NULL DEFAULT 0, + "target_servings" INTEGER NOT NULL DEFAULT 1, + "actual_servings" INTEGER NOT NULL DEFAULT 0, + "color" TEXT NOT NULL DEFAULT 'white', + "portion_weight" INTEGER NOT NULL DEFAULT 100 CHECK("portion_weight" > 0), + "protein" REAL NOT NULL DEFAULT 5.0 CHECK("protein" >= 0), + "fiber" REAL NOT NULL DEFAULT 5.0 CHECK("fiber" >= 0), + "protein_per_portion" REAL NOT NULL GENERATED ALWAYS AS (("protein" / 100) * "portion_weight") VIRTUAL, + "fiber_per_portion" REAL NOT NULL GENERATED ALWAYS AS (("fiber" / 100) * "portion_weight") VIRTUAL, + "sort_order" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY("id") +) STRICT; +INSERT INTO "main"."sqlb_temp_table_1" ("actual_servings","color","fiber","id","kc_per_serving","name","portion","portion_weight","protein","sort_order","target_servings") SELECT "actual_servings","color","fiber","id","kc_per_serving","name","portion","portion_weight","protein","sort_order","target_servings" FROM "main"."food"; +PRAGMA defer_foreign_keys = '1'; +DROP TABLE "main"."food"; +ALTER TABLE "main"."sqlb_temp_table_1" RENAME TO "food"; +PRAGMA defer_foreign_keys = '0'; + +PRAGMA user_version = 4; From fa0d8930b8c175c4e4ae555e8b1f8cd7a7362095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Sat, 25 Oct 2025 13:45:02 +0200 Subject: [PATCH 03/10] show protein and fiber sums --- foods.db | Bin 12288 -> 12288 bytes src/get_sum.sql | 4 +++- src/main.rs | 22 +++++++++++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/foods.db b/foods.db index 81ff588f777d56554ae5df3a96484e5c93a53a99..51e282b0a438edcb266f26615fade99281c582e4 100644 GIT binary patch delta 306 zcmZojXh@hK&FD5!##zvfL9a`Qmw|zSk(Zl+mz(qBWZt1u~>rC6j`B&Ik>&IEy34uT+R=B$(e ze|8=Q@jymKj>+P3x|5A`Y&F!Fc^E|H85udG895l43>92`d{Q&>iW75F^W>Oe2C=eM zOrEaO$tX2hQCE!9j*EvuG?d_Z4K5~op3oJ z=g;DOz|Y0ElP`|@HyO!~+=_87GU&=`u1+Hqx+W9tP21Mh?cw4!R&kxw-+2Y7+~U^wpVp7)0e685yM+IT)D? z6#Su27$co@VZCyU7GOnxKFG1*y9nUQmHhMf82 ed3y604JQ`rF#1g1r0+X rusqlite::Result> { fn get_sum(conn: &Arc>) -> rusqlite::Result { let conn = conn.lock(); let mut stmt = PreparedStatements::get_sum(&conn); - let kc = stmt.query_one((), |row| row.get(0))?; - let sum = Sum { - kc, - bs: 99, - protein: 99, - }; + let sum = stmt.query_one((), |row| { + Ok(Sum { + kc: row.get(0)?, + bs: row.get(1)?, + protein: row.get(2)?, + }) + })?; debug!(?sum); Ok(sum) } @@ -269,7 +270,10 @@ fn get_date() -> String { async fn root(State(conn): State) -> Result, StatusCode> { let foods = get_foods(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let sum = get_sum(&conn).map_err(|e| { + error!(?e); + StatusCode::INTERNAL_SERVER_ERROR + })?; let date = get_date(); let index = IndexTemplate { foods, sum, date }; Ok(Html( From 897309a3b99f978a688b50c0979f3f81203ba0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Sat, 25 Oct 2025 13:56:39 +0200 Subject: [PATCH 04/10] pleasing button layout, indicating that it is checked --- foods.db | Bin 12288 -> 12288 bytes templates/food.html | 8 ++++---- templates/index.html | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/foods.db b/foods.db index 51e282b0a438edcb266f26615fade99281c582e4..9f2a85377b5944fb2e6566f7ce295e8f226fc6cb 100644 GIT binary patch delta 147 zcmZojXh@hK%~&x}#+k8VW5QB-K^|X5-UIv_dGh(W_;&J#@}+MUQ&_^w!WYNDHhF`# z_he4JP$7Ox22Sx*Mn=v6b{+=tKt@K!$>MUlGEA(>X$Hw@7O5%rzm37bfgMa4GftSi sQ?Hp(W}>46XA~O`14uLDW#Z(Ryh~qe@?3c-#uNba0v~b! diff --git a/templates/food.html b/templates/food.html index c280ac3..0b2e1cf 100644 --- a/templates/food.html +++ b/templates/food.html @@ -21,17 +21,17 @@ {% if loop.index as i32 <= food.target_servings %} {% else %} {% endif %} diff --git a/templates/index.html b/templates/index.html index 5893591..90dcc9d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -132,5 +132,22 @@ accent-color: hsl(from #cb8175 h 90% l); background-color: hsl(from #cb8175 h 60% l); } + + input[type="button"] { + display: inline-block; + border: none; + width: 100%; + height: 100%; + background-color: inherit; + cursor: pointer; + } + + .unchecked { + color: transparent; + } + + input[type="button"]:hover { + background-color: beige; + } \ No newline at end of file From 425ec50a5ffe445564034982d24ffb3be82b168b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Sun, 26 Oct 2025 13:41:42 +0100 Subject: [PATCH 05/10] put router definition into own function --- src/main.rs | 58 ++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5cb85fe..20d450e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,6 +167,36 @@ fn do_migrations(conn: &mut Connection) -> rusqlite::Result<()> { Ok(()) } +fn app(conn: Connection) -> axum::Router { + Router::new() + .route("/", get(root)) + .route("/increase/{id}", post(increase)) + .route("/decrease/{id}", post(decrease)) + .route("/set/{id}/to/{amount}", post(set)) + .layer( + TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { + let matched_path = request + .extensions() + .get::() + .map(MatchedPath::as_str); + let request_id = request + .extensions() + .get::() + .map(ToString::to_string) + .unwrap_or_else(|| "unknown".into()); + info_span!( + "request", + method = ?request.method(), + matched_path, + uri = ?request.uri(), + id = %request_id, + ) + }), + ) + .layer(RequestIdLayer) + .with_state(Arc::new(Mutex::new(conn))) +} + #[tokio::main] async fn main() -> Result<(), std::io::Error> { tracing_subscriber::registry() @@ -197,33 +227,7 @@ async fn main() -> Result<(), std::io::Error> { PreparedStatements::check(&conn).expect("failed to prepare sql statements"); - let app = Router::new() - .route("/", get(root)) - .route("/increase/{id}", post(increase)) - .route("/decrease/{id}", post(decrease)) - .route("/set/{id}/to/{amount}", post(set)) - .layer( - TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { - let matched_path = request - .extensions() - .get::() - .map(MatchedPath::as_str); - let request_id = request - .extensions() - .get::() - .map(ToString::to_string) - .unwrap_or_else(|| "unknown".into()); - info_span!( - "request", - method = ?request.method(), - matched_path, - uri = ?request.uri(), - id = %request_id, - ) - }), - ) - .layer(RequestIdLayer) - .with_state(Arc::new(Mutex::new(conn))); + let app = app(conn); let address = "0.0.0.0:3001"; let listener = tokio::net::TcpListener::bind(address) From ad3a2359825855955c1ec06e8659a8ba62b0f34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Mon, 27 Oct 2025 10:46:22 +0100 Subject: [PATCH 06/10] better error handling using anyhow --- Cargo.lock | 7 ++++ Cargo.toml | 1 + foods.db | Bin 12288 -> 12288 bytes src/main.rs | 109 ++++++++++++++++++++++++++++------------------------ 4 files changed, 66 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed74a88..98d877b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "askama" version = "0.14.0" @@ -812,6 +818,7 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" name = "task_counter" version = "0.1.0" dependencies = [ + "anyhow", "askama", "axum", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 9cbee44..63257d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0.100" askama = "0.14.0" axum = "0.8.6" chrono = "0.4.42" diff --git a/foods.db b/foods.db index 9f2a85377b5944fb2e6566f7ce295e8f226fc6cb..10e67b16840b68bae1ebcc777463ba475786a5ca 100644 GIT binary patch delta 51 zcmZojXh@hK%~&~6#+k8lW5P;##)F%=6gKnnvNJHayD{=GNZT?pa!x)VuR8gmw$x@$ Hz3+?wks=Pu delta 35 rcmZojXh@hK%~&x}#+k8VW5P;##sizV6gKlRvQ6Hg?Y)^(?>i#^-scP_ diff --git a/src/main.rs b/src/main.rs index 20d450e..f5000fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use axum::{ Router, extract::{MatchedPath, Path, State}, http::{Request, StatusCode}, - response::Html, + response::{Html, IntoResponse, Response}, routing::{get, post}, }; use parking_lot::Mutex; @@ -80,7 +80,7 @@ impl Food { struct PreparedStatements {} impl<'conn> PreparedStatements { - fn check(conn: &Connection) -> rusqlite::Result { + fn check(conn: &Connection) -> anyhow::Result { conn.prepare_cached(include_str!("increase.sql"))?; conn.prepare_cached(include_str!("decrease.sql"))?; conn.prepare_cached(include_str!("get_food.sql"))?; @@ -121,10 +121,35 @@ impl<'conn> PreparedStatements { } } +// Make our own error that wraps `anyhow::Error`. +struct AppError(anyhow::Error); + +// Tell axum how to convert `AppError` into a response. +impl IntoResponse for AppError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into +// `Result<_, AppError>`. That way you don't need to do that manually. +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} + type ConnState = Arc>; -fn get_version(tx: &Transaction) -> rusqlite::Result { - tx.query_one(include_str!("get_version.sql"), (), |row| row.get(0)) +fn get_version(tx: &Transaction) -> anyhow::Result { + Ok(tx.query_one(include_str!("get_version.sql"), (), |row| row.get(0))?) } fn get_migrations() -> [&'static str; 4] { @@ -136,7 +161,7 @@ fn get_migrations() -> [&'static str; 4] { ] } -fn do_migrations(conn: &mut Connection) -> rusqlite::Result<()> { +fn do_migrations(conn: &mut Connection) -> anyhow::Result<()> { let migrations = get_migrations(); let num_migrations = migrations.len(); let tx = conn.transaction()?; @@ -198,7 +223,7 @@ fn app(conn: Connection) -> axum::Router { } #[tokio::main] -async fn main() -> Result<(), std::io::Error> { +async fn main() -> anyhow::Result<()> { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { @@ -239,10 +264,11 @@ async fn main() -> Result<(), std::io::Error> { .local_addr() .expect("failed to get local listening address") ); - axum::serve(listener, app).await + axum::serve(listener, app).await?; + Ok(()) } -fn get_foods(conn: &ConnState) -> rusqlite::Result> { +fn get_foods(conn: &ConnState) -> anyhow::Result> { let conn = conn.lock(); let mut stmt = PreparedStatements::get_foods(&conn); let foods: Vec<_> = stmt @@ -252,7 +278,7 @@ fn get_foods(conn: &ConnState) -> rusqlite::Result> { Ok(foods) } -fn get_sum(conn: &Arc>) -> rusqlite::Result { +fn get_sum(conn: &Arc>) -> anyhow::Result { let conn = conn.lock(); let mut stmt = PreparedStatements::get_sum(&conn); let sum = stmt.query_one((), |row| { @@ -272,22 +298,15 @@ fn get_date() -> String { date } -async fn root(State(conn): State) -> Result, StatusCode> { - let foods = get_foods(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let sum = get_sum(&conn).map_err(|e| { - error!(?e); - StatusCode::INTERNAL_SERVER_ERROR - })?; +async fn root(State(conn): State) -> Result, AppError> { + let foods = get_foods(&conn)?; + let sum = get_sum(&conn)?; let date = get_date(); let index = IndexTemplate { foods, sum, date }; - Ok(Html( - index - .render() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, - )) + Ok(Html(index.render()?)) } -fn do_increase(conn: &Arc>, id: i32) -> rusqlite::Result<()> { +fn do_increase(conn: &Arc>, id: i32) -> anyhow::Result<()> { let conn = conn.lock(); let mut stmt = PreparedStatements::increase(&conn); let new: i32 = stmt.query_one((id,), |row| row.get(0))?; @@ -295,7 +314,7 @@ fn do_increase(conn: &Arc>, id: i32) -> rusqlite::Result<()> { Ok(()) } -fn get_food(conn: &Arc>, id: i32) -> rusqlite::Result { +fn get_food(conn: &Arc>, id: i32) -> anyhow::Result { let conn = conn.lock(); let mut stmt = PreparedStatements::get_food(&conn); let food = stmt.query_one((id,), Food::from_row)?; @@ -306,20 +325,16 @@ fn get_food(conn: &Arc>, id: i32) -> rusqlite::Result { async fn increase( State(conn): State>>, Path(id): Path, -) -> Result, StatusCode> { - do_increase(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; +) -> Result, AppError> { + do_increase(&conn, id)?; + let food = get_food(&conn, id)?; + let sum = get_sum(&conn)?; let date = get_date(); let update = FoodUpdateTemplate { food, sum, date }; - Ok(Html( - update - .render() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, - )) + Ok(Html(update.render()?)) } -fn do_decrease(conn: &Arc>, id: i32) -> rusqlite::Result<()> { +fn do_decrease(conn: &Arc>, id: i32) -> anyhow::Result<()> { let conn = conn.lock(); let mut stmt = PreparedStatements::decrease(&conn); let new: i32 = stmt.query_one((id,), |row| row.get(0))?; @@ -330,20 +345,16 @@ fn do_decrease(conn: &Arc>, id: i32) -> rusqlite::Result<()> { async fn decrease( State(conn): State>>, Path(id): Path, -) -> Result, StatusCode> { - do_decrease(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; +) -> Result, AppError> { + do_decrease(&conn, id)?; + let food = get_food(&conn, id)?; + let sum = get_sum(&conn)?; let date = get_date(); let update = FoodUpdateTemplate { food, sum, date }; - Ok(Html( - update - .render() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, - )) + Ok(Html(update.render()?)) } -fn do_set(conn: &Arc>, id: i32, amount: i32) -> rusqlite::Result<()> { +fn do_set(conn: &Arc>, id: i32, amount: i32) -> anyhow::Result<()> { let conn = conn.lock(); let mut stmt = PreparedStatements::set(&conn); let new: i32 = stmt.query_one((id, amount), |row| row.get(0))?; @@ -354,15 +365,11 @@ fn do_set(conn: &Arc>, id: i32, amount: i32) -> rusqlite::Resu async fn set( State(conn): State>>, Path((id, amount)): Path<(i32, i32)>, -) -> Result, StatusCode> { - do_set(&conn, id, amount).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; +) -> Result, AppError> { + do_set(&conn, id, amount)?; + let food = get_food(&conn, id)?; + let sum = get_sum(&conn)?; let date = get_date(); let update = FoodUpdateTemplate { food, sum, date }; - Ok(Html( - update - .render() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, - )) + Ok(Html(update.render()?)) } From 4a3ffb5ac69e0a6520036d66e78dc3c33184d141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Mon, 27 Oct 2025 11:05:33 +0100 Subject: [PATCH 07/10] fixed background color --- foods.db | Bin 12288 -> 12288 bytes templates/index.html | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/foods.db b/foods.db index 10e67b16840b68bae1ebcc777463ba475786a5ca..0e4ba02ee96532f764c64771505e71fd1cf1ff2e 100644 GIT binary patch delta 253 zcmZojXh@hK%{XbIj5Fh;jR{NT#kv15@*d>h$dk`!%>9Qyly4_rGH)HP?PeK;huj>j zJPhK2jEtO<56O8<_SLz-!pF&=Fj-MohSQ#lhe0%$k%MuvgRZU!n{t|2YD#iS3iAs` zP96qnN06QdU8~6lbv;=4xfrx2^XjRY>9O!I$ci#DGDm z!yq0wnO{$3a<3l8WCuBx$?x@~C;RGLVBy`xAT?Q0SB9ODgF~8;gJZImtfsJXnptW} za!LyG3r9{K25Cn|Mvlo+`Wlnh>k6^+#WAomFt|@XsO!NJ%)!GjnO9G3a<87? Date: Tue, 28 Oct 2025 20:26:30 +0100 Subject: [PATCH 08/10] implement basic multi-day support --- foods.db | Bin 12288 -> 16384 bytes src/decrease.sql | 31 +++++++++++++++++-------------- src/get_food.sql | 9 +++++++-- src/get_foods.sql | 20 +++++++++++++------- src/get_sum.sql | 11 ++++++++--- src/increase.sql | 26 +++++++++++++++++--------- src/main.rs | 9 ++++++--- src/migrations/5.sql | 23 +++++++++++++++++++++++ src/set.sql | 9 +++++---- templates/food.html | 4 ++-- 10 files changed, 98 insertions(+), 44 deletions(-) create mode 100644 src/migrations/5.sql diff --git a/foods.db b/foods.db index 0e4ba02ee96532f764c64771505e71fd1cf1ff2e..322ae5cb8ebe8df2e4ccd74a68360a25493c7785 100644 GIT binary patch delta 1499 zcmZojXlP)ZAg#KJfq{XA0Re>2_>2q;46G9i<%L%<=yeJ4@-j0pbFX9Iwc=jKb7o^< zJ?G|+oO>A~o1|IU#g&yAn`=uFlX6m15-a12Q;W(n^U{%+OwK{Bjv=lJA&yQyt_nyJ z3L0Dr3JMCkx(c4b{<;=s28LjMN@AtLWIjGId*{%gAXmSTco)YIR~@iaT7G^?d}fM* zr(cMxyK9hwpMQvgU#O1{L>6jwaeQiGNotleu~IxmulB*u_Of8M`tki}D`c zEW&-4aq<&>85Lgs84P@UuNnE@@!#R!&A*E8HGeyQDSrxo5Wge80lz#yAK%~2h5{ly z69afRyUI*qsu$p8QDkH^GB7gLH8jvQvfyPFU_%vQW98>(QDo#msA1ygLlxlT2bsc& zP{GU3gQ|jupBq(xTYw8>O+7*n7aKn(iy|W@LWEO*1EdBaz`@4Pj;e&6|15(bvK}^0 zepXZ^tO6{krm!$EFfj0g!jgf(7fcB-qpD!$5MX3c;Ec&0Nb zO%Bvi;N)fym1ks>W@MaPts|+$z{*;|dyqj&)RvKhLz}%$eT+rbBhBPH-l&}+{_Rkx73mn z1r4Xtl9JRSoyj(`;*tz7^$ZNmFC00!8KfOS>e(45%jv63Uau=)!oaM+m(0K+ZOh20 z$j8kfDF~8hWMnc_2rkLibItUIJCU7%#mAMAn?c%k@;-U>$?tWgICNOJ8DvEn87B+N ztFbZdVPIegn4GTX!xqHB%^)5*IZMxE@_aohHa~Vy;7-oc)1K@g$1?f7o-|to8#jY^ z>f{)GtI6}_q}aWgxEZ8fnI;#?Ycn!Vo+~eb^Zfhx*YeNd z@8+-K&*YEbcjq_bSLGK0=Z%dGcbMu0P*VgSADRF!FPZ=k51IfsH<|z!7n%SkCz=2U n2TULrnjP8MVIoigHa0W?R#r3t78W!CW@a=2aQ@{)WHfUCzTPcQ delta 771 zcmZo@U~EX3Agwxyfq{XUfq{XEfq{XMfq_AQfq{XA0Rcc_EE5~$1t&4+bqVo;RPk~% z@N#p0+-xY&$hr9==U&Fm3;CQGrP_E{*v0kr8N1y}5|eULiwkp-;!9F<3*t*AbMxv? z&g7jp`6<`Q%?tVB8C78B^Ka+Rrmg`v4=3af|$4-bR5C`|64jIb%o23>9*25EIBP7Y~CMh+%JeIo_u(gL@f)Xd`K z41G5c$#6nB%`7z~IVDAexrL9DL1Ch!sEEBC7Y~DIusxV$fpd}s&iVuE$ zMRPr9?7-r~SwFZWThBkKxFmp?Njc3RIn5$9#s0D}7%(yI3E|*j5RYV>%&V_G`Hd{c z Response { + let error = &self.0; + error!(?error, "error returned to client"); ( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", self.0), @@ -152,12 +154,13 @@ fn get_version(tx: &Transaction) -> anyhow::Result { Ok(tx.query_one(include_str!("get_version.sql"), (), |row| row.get(0))?) } -fn get_migrations() -> [&'static str; 4] { +fn get_migrations() -> [&'static str; 5] { [ include_str!("migrations/1.sql"), include_str!("migrations/2.sql"), include_str!("migrations/3.sql"), include_str!("migrations/4.sql"), + include_str!("migrations/5.sql"), ] } diff --git a/src/migrations/5.sql b/src/migrations/5.sql new file mode 100644 index 0000000..8125898 --- /dev/null +++ b/src/migrations/5.sql @@ -0,0 +1,23 @@ +CREATE TABLE day_serving ( + -- ISO-8601 + day TEXT NOT NULL DEFAULT CURRENT_DATE, + food_id INTEGER NOT NULL, + servings_eaten INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(day, food_id), + FOREIGN KEY(food_id) REFERENCES food(id) +) STRICT; + +INSERT INTO + day_serving (food_id, servings_eaten) +SELECT + id, + actual_servings +FROM + food; + +ALTER TABLE + food +DROP COLUMN + actual_servings; + +PRAGMA user_version = 5; diff --git a/src/set.sql b/src/set.sql index 6830998..75a98d2 100644 --- a/src/set.sql +++ b/src/set.sql @@ -1,8 +1,9 @@ UPDATE - food + day_serving SET - actual_servings = MAX(?2, 0) + servings_eaten = MAX(?2, 0) WHERE - id = ?1 + food_id = ?1 + AND day = CURRENT_DATE RETURNING - actual_servings + servings_eaten diff --git a/templates/food.html b/templates/food.html index 0b2e1cf..f3656fb 100644 --- a/templates/food.html +++ b/templates/food.html @@ -20,7 +20,7 @@ {% for counter in self::range(10) %} {% if loop.index as i32 <= food.target_servings %} {% else %}