Compare commits

...

10 commits

23 changed files with 2401 additions and 193 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
result result
result-bin
.direnv .direnv

99
Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 4
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -20,6 +20,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "askama" name = "askama"
version = "0.14.0" version = "0.14.0"
@ -137,9 +143,9 @@ dependencies = [
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.4" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
@ -155,9 +161,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.41" version = "1.2.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@ -218,6 +224,23 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "food-tracker"
version = "0.1.0"
dependencies = [
"anyhow",
"askama",
"axum",
"chrono",
"parking_lot",
"rusqlite",
"tokio",
"tower-http",
"tower-request-id",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@ -405,9 +428,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.81" version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@ -571,9 +594,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.101" version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -793,9 +816,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.107" version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -808,22 +831,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "task_counter"
version = "0.1.0"
dependencies = [
"askama",
"axum",
"chrono",
"parking_lot",
"rusqlite",
"tokio",
"tower-http",
"tower-request-id",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.9" version = "1.1.9"
@ -991,9 +998,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.19" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]] [[package]]
name = "valuable" name = "valuable"
@ -1024,9 +1031,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.104" version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -1035,25 +1042,11 @@ dependencies = [
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.104" version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -1061,22 +1054,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.104" version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [ dependencies = [
"bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.104" version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]

1876
Cargo.nix Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,10 @@
[package] [package]
name = "task_counter" name = "food-tracker"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.100"
askama = "0.14.0" askama = "0.14.0"
axum = "0.8.6" axum = "0.8.6"
chrono = "0.4.42" chrono = "0.4.42"

130
flake.lock generated
View file

@ -1,6 +1,80 @@
{ {
"nodes": { "nodes": {
"cargo2nix": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1750364353,
"narHash": "sha256-l06DIwnB4JHwP1isUUXk85F+AHQkUSUyAWnAmRxXICg=",
"owner": "cargo2nix",
"repo": "cargo2nix",
"rev": "a709c74619e1a2b68ed12bb398e12fbe29d69657",
"type": "github"
},
"original": {
"owner": "cargo2nix",
"ref": "release-0.12",
"repo": "cargo2nix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": {
"lastModified": 1705099185,
"narHash": "sha256-SxJenKtvcrKJd0TyJQMO3p6VA7PEp+vmMnmlKFzWMNs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2bce5ccff0ad7abda23e8bb56434b6877a446694",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1760580664, "lastModified": 1760580664,
"narHash": "sha256-/YdfibIrnqXAL8p5kqCU345mzpHoOtuVIkMiI2pF4Dc=", "narHash": "sha256-/YdfibIrnqXAL8p5kqCU345mzpHoOtuVIkMiI2pF4Dc=",
@ -16,12 +90,49 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_3": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs", "cargo2nix": "cargo2nix",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay",
"utils": "utils" "utils": "utils"
} }
}, },
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1761851069,
"narHash": "sha256-VHqBFyQdXE10lvBaNCaJSD5xw1WH6Thqq92OIB6MqZo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "0881bcdf6c34cd3ba558b19d7a74d8ffc9e1fff0",
"type": "github"
},
"original": {
"owner": "oxalica",
"ref": "stable",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": { "systems": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
@ -37,9 +148,24 @@
"type": "github" "type": "github"
} }
}, },
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": { "utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems_2"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1731533236,

View file

@ -2,16 +2,35 @@
inputs = { inputs = {
utils.url = "github:numtide/flake-utils"; utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
cargo2nix = {
url = "github:cargo2nix/cargo2nix/release-0.12";
inputs.rust-overlay.follows = "rust-overlay";
};
rust-overlay.url = "github:oxalica/rust-overlay/stable";
}; };
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,
utils, utils,
cargo2nix,
rust-overlay,
}: }:
utils.lib.eachDefaultSystem ( utils.lib.eachDefaultSystem (
system: let system: let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = import nixpkgs {
in { inherit system;
overlays = [cargo2nix.overlays.default];
};
rustPkgs = pkgs.rustBuilder.makePackageSet {
rustVersion = "1.86.0";
packageFun = import ./Cargo.nix;
};
in rec {
packages = {
food-tracker = rustPkgs.workspace.food-tracker {};
default = packages.food-tracker;
};
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = [ buildInputs = [
pkgs.cargo pkgs.cargo

BIN
foods.db

Binary file not shown.

View file

@ -6,4 +6,4 @@ CREATE TABLE IF NOT EXISTS food (
target_servings INTEGER NOT NULL DEFAULT 1, target_servings INTEGER NOT NULL DEFAULT 1,
actual_servings INTEGER NOT NULL DEFAULT 0, actual_servings INTEGER NOT NULL DEFAULT 0,
color TEXT NOT NULL DEFAULT 'white' color TEXT NOT NULL DEFAULT 'white'
); ) STRICT;

View file

@ -1,14 +1,17 @@
UPDATE INSERT OR REPLACE INTO
food day_serving (day, food_id, servings_eaten)
SET VALUES (
actual_servings = MAX( CURRENT_DATE,
(SELECT ?1,
actual_servings MAX(coalesce((
SELECT
servings_eaten
FROM FROM
food day_serving
WHERE id = ?1) - 1, WHERE
0) food_id = ?1
WHERE AND day = CURRENT_DATE
id = ?1 ) - 1,
RETURNING 0
actual_servings ), 0)
) RETURNING servings_eaten;

View file

@ -4,9 +4,14 @@ SELECT
name, name,
kc_per_serving, kc_per_serving,
target_servings, target_servings,
actual_servings, coalesce(day_serving.servings_eaten, 0),
color color
FROM FROM
food food
LEFT JOIN
day_serving
ON
day_serving.food_id = food.id
AND day_serving.day = CURRENT_DATE
WHERE WHERE
id = ?1 food.id = ?1;

View file

@ -1,10 +1,18 @@
SELECT SELECT
id, food.id,
portion, food.portion,
name, food.name,
kc_per_serving, food.kc_per_serving,
target_servings, food.target_servings,
actual_servings, coalesce(day_serving.servings_eaten, 0) as servings_eaten,
color food.color
FROM FROM
food food
LEFT JOIN
day_serving
ON
day_serving.food_id = food.id
AND coalesce(day_serving.day, CURRENT_DATE) = CURRENT_DATE
ORDER BY
sort_order, name;

View file

@ -1,4 +1,12 @@
SELECT SELECT
SUM(kc_per_serving * actual_servings) AS kc SUM(kc_per_serving * coalesce(servings_eaten, 0)) AS kc,
SUM(protein_per_portion * coalesce(servings_eaten, 0)) AS protein,
SUM(fiber_per_portion * coalesce(servings_eaten, 0)) AS bs
FROM FROM
food food
LEFT JOIN
day_serving
ON
day_serving.food_id = food.id
AND coalesce(day_serving.day, CURRENT_DATE) = CURRENT_DATE;

1
src/get_version.sql Normal file
View file

@ -0,0 +1 @@
PRAGMA user_version;

View file

@ -1,9 +1,17 @@
UPDATE INSERT OR REPLACE INTO
food day_serving (day, food_id, servings_eaten)
SET actual_servings = ( VALUES (
SELECT actual_servings CURRENT_DATE,
FROM food ?1,
WHERE id = ?1 coalesce((
) + 1 SELECT
WHERE id = ?1 servings_eaten
RETURNING actual_servings FROM
day_serving
WHERE
food_id = ?1
AND day = CURRENT_DATE
) + 1,
1
)
) RETURNING servings_eaten;

View file

@ -5,11 +5,11 @@ use axum::{
Router, Router,
extract::{MatchedPath, Path, State}, extract::{MatchedPath, Path, State},
http::{Request, StatusCode}, http::{Request, StatusCode},
response::Html, response::{Html, IntoResponse, Response},
routing::{get, post}, routing::{get, post},
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use rusqlite::{CachedStatement, Connection, Row}; use rusqlite::{CachedStatement, Connection, Row, Transaction};
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tower_request_id::{RequestId, RequestIdLayer}; use tower_request_id::{RequestId, RequestIdLayer};
use tracing::{debug, error, info, info_span}; use tracing::{debug, error, info, info_span};
@ -18,8 +18,8 @@ use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _
#[derive(Debug)] #[derive(Debug)]
struct Sum { struct Sum {
kc: i32, kc: i32,
bs: i32, bs: f32,
protein: i32, protein: f32,
} }
impl std::fmt::Display for Sum { impl std::fmt::Display for Sum {
@ -56,7 +56,7 @@ struct Food {
name: String, name: String,
kc_per_serving: i32, kc_per_serving: i32,
target_servings: i32, target_servings: i32,
actual_servings: i32, servings_eaten: i32,
color: String, color: String,
} }
@ -69,7 +69,7 @@ impl Food {
name: row.get(2)?, name: row.get(2)?,
kc_per_serving: row.get(3)?, kc_per_serving: row.get(3)?,
target_servings: row.get(4)?, target_servings: row.get(4)?,
actual_servings: row.get(5)?, servings_eaten: row.get(5)?,
color: row.get(6)?, color: row.get(6)?,
}) })
} }
@ -80,8 +80,7 @@ impl Food {
struct PreparedStatements {} struct PreparedStatements {}
impl<'conn> PreparedStatements { impl<'conn> PreparedStatements {
fn check(conn: &Connection) -> rusqlite::Result<Self> { fn check(conn: &Connection) -> anyhow::Result<Self> {
conn.prepare_cached(include_str!("create_tables.sql"))?;
conn.prepare_cached(include_str!("increase.sql"))?; conn.prepare_cached(include_str!("increase.sql"))?;
conn.prepare_cached(include_str!("decrease.sql"))?; conn.prepare_cached(include_str!("decrease.sql"))?;
conn.prepare_cached(include_str!("get_food.sql"))?; conn.prepare_cached(include_str!("get_food.sql"))?;
@ -91,11 +90,6 @@ impl<'conn> PreparedStatements {
Ok(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> { fn increase(conn: &'conn Connection) -> CachedStatement<'conn> {
conn.prepare_cached(include_str!("increase.sql")) conn.prepare_cached(include_str!("increase.sql"))
.expect("cached statement is invalid") .expect("cached statement is invalid")
@ -127,35 +121,82 @@ 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 {
let error = &self.0;
error!(?error, "error returned to client");
(
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<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
type ConnState = Arc<Mutex<Connection>>; type ConnState = Arc<Mutex<Connection>>;
#[tokio::main] fn get_version(tx: &Transaction) -> anyhow::Result<usize> {
async fn main() -> Result<(), std::io::Error> { Ok(tx.query_one(include_str!("get_version.sql"), (), |row| row.get(0))?)
tracing_subscriber::registry() }
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
// axum logs rejections from built-in extractors with the `axum::rejection`
// target, at `TRACE` level. `axum::rejection=trace` enables showing those events
format!(
"{}=debug,tower_http=debug,axum::rejection=trace",
env!("CARGO_CRATE_NAME")
)
.into()
}),
)
.with(tracing_subscriber::fmt::layer())
.init();
let db_connecion_str = "./foods.db".to_string(); fn get_migrations() -> [&'static str; 5] {
debug!(db_connecion_str, "opening database"); [
let conn = Connection::open(db_connecion_str).expect("failed to open database"); include_str!("migrations/1.sql"),
PreparedStatements::check(&conn).expect("failed to prepare sql statements"); include_str!("migrations/2.sql"),
if let Err(e) = PreparedStatements::create_tables(&conn).execute(()) { include_str!("migrations/3.sql"),
error!(?e, "failed to create tables"); include_str!("migrations/4.sql"),
panic!("failed to create tables: {:#?}", e); include_str!("migrations/5.sql"),
]
}
fn do_migrations(conn: &mut Connection) -> anyhow::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(())
}
let app = Router::new() fn app(conn: Connection) -> axum::Router {
Router::new()
.route("/", get(root)) .route("/", get(root))
.route("/increase/{id}", post(increase)) .route("/increase/{id}", post(increase))
.route("/decrease/{id}", post(decrease)) .route("/decrease/{id}", post(decrease))
@ -181,7 +222,40 @@ async fn main() -> Result<(), std::io::Error> {
}), }),
) )
.layer(RequestIdLayer) .layer(RequestIdLayer)
.with_state(Arc::new(Mutex::new(conn))); .with_state(Arc::new(Mutex::new(conn)))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
// axum logs rejections from built-in extractors with the `axum::rejection`
// target, at `TRACE` level. `axum::rejection=trace` enables showing those events
format!(
"{}=debug,tower_http=debug,axum::rejection=trace",
env!("CARGO_CRATE_NAME")
)
.into()
}),
)
.with(tracing_subscriber::fmt::layer())
.init();
let db_connecion_str = "./foods.db".to_string();
debug!(db_connecion_str, "opening 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 = app(conn);
let address = "0.0.0.0:3001"; let address = "0.0.0.0:3001";
let listener = tokio::net::TcpListener::bind(address) let listener = tokio::net::TcpListener::bind(address)
@ -193,10 +267,11 @@ async fn main() -> Result<(), std::io::Error> {
.local_addr() .local_addr()
.expect("failed to get local listening address") .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<Vec<Food>> { fn get_foods(conn: &ConnState) -> anyhow::Result<Vec<Food>> {
let conn = conn.lock(); let conn = conn.lock();
let mut stmt = PreparedStatements::get_foods(&conn); let mut stmt = PreparedStatements::get_foods(&conn);
let foods: Vec<_> = stmt let foods: Vec<_> = stmt
@ -206,15 +281,16 @@ fn get_foods(conn: &ConnState) -> rusqlite::Result<Vec<Food>> {
Ok(foods) Ok(foods)
} }
fn get_sum(conn: &Arc<Mutex<Connection>>) -> rusqlite::Result<Sum> { fn get_sum(conn: &Arc<Mutex<Connection>>) -> anyhow::Result<Sum> {
let conn = conn.lock(); let conn = conn.lock();
let mut stmt = PreparedStatements::get_sum(&conn); let mut stmt = PreparedStatements::get_sum(&conn);
let kc = stmt.query_one((), |row| row.get(0))?; let sum = stmt.query_one((), |row| {
let sum = Sum { Ok(Sum {
kc, kc: row.get(0)?,
bs: 99, bs: row.get(1)?,
protein: 99, protein: row.get(2)?,
}; })
})?;
debug!(?sum); debug!(?sum);
Ok(sum) Ok(sum)
} }
@ -225,19 +301,15 @@ fn get_date() -> String {
date date
} }
async fn root(State(conn): State<ConnState>) -> Result<Html<String>, StatusCode> { async fn root(State(conn): State<ConnState>) -> Result<Html<String>, AppError> {
let foods = get_foods(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let foods = get_foods(&conn)?;
let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let sum = get_sum(&conn)?;
let date = get_date(); let date = get_date();
let index = IndexTemplate { foods, sum, date }; let index = IndexTemplate { foods, sum, date };
Ok(Html( Ok(Html(index.render()?))
index
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }
fn do_increase(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::Result<()> { fn do_increase(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::Result<()> {
let conn = conn.lock(); let conn = conn.lock();
let mut stmt = PreparedStatements::increase(&conn); let mut stmt = PreparedStatements::increase(&conn);
let new: i32 = stmt.query_one((id,), |row| row.get(0))?; let new: i32 = stmt.query_one((id,), |row| row.get(0))?;
@ -245,7 +317,7 @@ fn do_increase(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::Result<()> {
Ok(()) Ok(())
} }
fn get_food(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::Result<Food> { fn get_food(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::Result<Food> {
let conn = conn.lock(); let conn = conn.lock();
let mut stmt = PreparedStatements::get_food(&conn); let mut stmt = PreparedStatements::get_food(&conn);
let food = stmt.query_one((id,), Food::from_row)?; let food = stmt.query_one((id,), Food::from_row)?;
@ -256,20 +328,16 @@ fn get_food(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::Result<Food> {
async fn increase( async fn increase(
State(conn): State<Arc<Mutex<Connection>>>, State(conn): State<Arc<Mutex<Connection>>>,
Path(id): Path<i32>, Path(id): Path<i32>,
) -> Result<Html<String>, StatusCode> { ) -> Result<Html<String>, AppError> {
do_increase(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; do_increase(&conn, id)?;
let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let food = get_food(&conn, id)?;
let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let sum = get_sum(&conn)?;
let date = get_date(); let date = get_date();
let update = FoodUpdateTemplate { food, sum, date }; let update = FoodUpdateTemplate { food, sum, date };
Ok(Html( Ok(Html(update.render()?))
update
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }
fn do_decrease(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::Result<()> { fn do_decrease(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::Result<()> {
let conn = conn.lock(); let conn = conn.lock();
let mut stmt = PreparedStatements::decrease(&conn); let mut stmt = PreparedStatements::decrease(&conn);
let new: i32 = stmt.query_one((id,), |row| row.get(0))?; let new: i32 = stmt.query_one((id,), |row| row.get(0))?;
@ -280,20 +348,16 @@ fn do_decrease(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::Result<()> {
async fn decrease( async fn decrease(
State(conn): State<Arc<Mutex<Connection>>>, State(conn): State<Arc<Mutex<Connection>>>,
Path(id): Path<i32>, Path(id): Path<i32>,
) -> Result<Html<String>, StatusCode> { ) -> Result<Html<String>, AppError> {
do_decrease(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; do_decrease(&conn, id)?;
let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let food = get_food(&conn, id)?;
let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let sum = get_sum(&conn)?;
let date = get_date(); let date = get_date();
let update = FoodUpdateTemplate { food, sum, date }; let update = FoodUpdateTemplate { food, sum, date };
Ok(Html( Ok(Html(update.render()?))
update
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }
fn do_set(conn: &Arc<Mutex<Connection>>, id: i32, amount: i32) -> rusqlite::Result<()> { fn do_set(conn: &Arc<Mutex<Connection>>, id: i32, amount: i32) -> anyhow::Result<()> {
let conn = conn.lock(); let conn = conn.lock();
let mut stmt = PreparedStatements::set(&conn); let mut stmt = PreparedStatements::set(&conn);
let new: i32 = stmt.query_one((id, amount), |row| row.get(0))?; let new: i32 = stmt.query_one((id, amount), |row| row.get(0))?;
@ -304,15 +368,11 @@ fn do_set(conn: &Arc<Mutex<Connection>>, id: i32, amount: i32) -> rusqlite::Resu
async fn set( async fn set(
State(conn): State<Arc<Mutex<Connection>>>, State(conn): State<Arc<Mutex<Connection>>>,
Path((id, amount)): Path<(i32, i32)>, Path((id, amount)): Path<(i32, i32)>,
) -> Result<Html<String>, StatusCode> { ) -> Result<Html<String>, AppError> {
do_set(&conn, id, amount).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; do_set(&conn, id, amount)?;
let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let food = get_food(&conn, id)?;
let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let sum = get_sum(&conn)?;
let date = get_date(); let date = get_date();
let update = FoodUpdateTemplate { food, sum, date }; let update = FoodUpdateTemplate { food, sum, date };
Ok(Html( Ok(Html(update.render()?))
update
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }

6
src/migrations/1.sql Normal file
View file

@ -0,0 +1,6 @@
ALTER TABLE
food
ADD COLUMN
portion_weight INTEGER NOT NULL DEFAULT 100 CHECK (portion_weight > 0);
PRAGMA user_version = 1;

23
src/migrations/2.sql Normal file
View file

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

6
src/migrations/3.sql Normal file
View file

@ -0,0 +1,6 @@
ALTER TABLE
food
ADD COLUMN
sort_order INTEGER NOT NULL DEFAULT 0;
PRAGMA user_version = 3;

23
src/migrations/4.sql Normal file
View file

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

23
src/migrations/5.sql Normal file
View file

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

View file

@ -1,8 +1,9 @@
UPDATE INSERT OR REPLACE INTO
food day_serving (day, food_id, servings_eaten)
SET VALUES (
actual_servings = MAX(?2, 0) CURRENT_DATE,
WHERE ?1,
id = ?1 MAX(?2, 0)
RETURNING )
actual_servings RETURNING servings_eaten;

View file

@ -20,18 +20,18 @@
{% for counter in self::range(10) %} {% for counter in self::range(10) %}
{% if loop.index as i32 <= food.target_servings %} {% if loop.index as i32 <= food.target_servings %}
<label class="ok"> <label class="ok">
{% if loop.index as i32 <= food.actual_servings %} {% if loop.index as i32 <= food.servings_eaten %}
<input type="checkbox" checked hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}"> <input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}" value="●" class="checked">
{% else %} {% else %}
<input type="checkbox" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}"> <input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}" value="●" class="unchecked">
{% endif %} {% endif %}
</label> </label>
{% else %} {% else %}
<label class="bad"> <label class="bad">
{% if loop.index as i32 <= food.actual_servings %} {% if loop.index as i32 <= food.servings_eaten %}
<input type="checkbox" checked hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}"> <input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}" value="●" class="checked">
{% else %} {% else %}
<input type="checkbox" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}"> <input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}" value="●" class="unchecked">
{% endif %} {% endif %}
</label> </label>
{% endif %} {% endif %}

View file

@ -48,7 +48,7 @@
} }
.card { .card {
background-color: hsl(from #a8c8a6 h 30% l); background-color: #f6edcd;
} }
hr { hr {
@ -132,5 +132,22 @@
accent-color: hsl(from #cb8175 h 90% l); accent-color: hsl(from #cb8175 h 90% l);
background-color: hsl(from #cb8175 h 60% 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;
}
</style> </style>
</html> </html>