Compare commits

..

No commits in common. "713dff67cef5d35eab8c947745269fb8a7103612" and "a182e4e0cb4e8e488b0904769e1626bd3b94e5a5" have entirely different histories.

23 changed files with 202 additions and 2410 deletions

1
.gitignore vendored
View file

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

117
Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 4
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -20,12 +20,6 @@ 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"
@ -143,9 +137,9 @@ dependencies = [
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
@ -161,9 +155,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.44" version = "1.2.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@ -224,23 +218,6 @@ 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"
@ -428,9 +405,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.82" version = "0.3.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@ -594,9 +571,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -816,9 +793,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.108" version = "2.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -831,6 +808,22 @@ 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"
@ -998,9 +991,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]] [[package]]
name = "valuable" name = "valuable"
@ -1031,9 +1024,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.105" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -1043,22 +1036,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-backend"
version = "0.2.105" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -1066,10 +1050,33 @@ dependencies = [
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-macro"
version = "0.2.105" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]

1876
Cargo.nix

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
[package] [package]
name = "food-tracker" name = "task_counter"
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,80 +1,6 @@
{ {
"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=",
@ -90,49 +16,12 @@
"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": {
"cargo2nix": "cargo2nix", "nixpkgs": "nixpkgs",
"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,
@ -148,24 +37,9 @@
"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_2" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1731533236,

View file

@ -2,35 +2,16 @@
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 = import nixpkgs { pkgs = nixpkgs.legacyPackages.${system};
inherit system; in {
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,17 +1,14 @@
INSERT OR REPLACE INTO UPDATE
day_serving (day, food_id, servings_eaten) food
VALUES ( SET
CURRENT_DATE, actual_servings = MAX(
?1, (SELECT
MAX(coalesce(( actual_servings
SELECT FROM
servings_eaten food
FROM WHERE id = ?1) - 1,
day_serving 0)
WHERE WHERE
food_id = ?1 id = ?1
AND day = CURRENT_DATE RETURNING
) - 1, actual_servings
0
), 0)
) RETURNING servings_eaten;

View file

@ -4,14 +4,9 @@ SELECT
name, name,
kc_per_serving, kc_per_serving,
target_servings, target_servings,
coalesce(day_serving.servings_eaten, 0), actual_servings,
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
food.id = ?1; id = ?1

View file

@ -1,18 +1,10 @@
SELECT SELECT
food.id, id,
food.portion, portion,
food.name, name,
food.kc_per_serving, kc_per_serving,
food.target_servings, target_servings,
coalesce(day_serving.servings_eaten, 0) as servings_eaten, actual_servings,
food.color 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,12 +1,4 @@
SELECT SELECT
SUM(kc_per_serving * coalesce(servings_eaten, 0)) AS kc, SUM(kc_per_serving * actual_servings) 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;

View file

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

View file

@ -1,17 +1,9 @@
INSERT OR REPLACE INTO UPDATE
day_serving (day, food_id, servings_eaten) food
VALUES ( SET actual_servings = (
CURRENT_DATE, SELECT actual_servings
?1, FROM food
coalesce(( WHERE id = ?1
SELECT ) + 1
servings_eaten WHERE id = ?1
FROM RETURNING actual_servings
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, IntoResponse, Response}, response::Html,
routing::{get, post}, routing::{get, post},
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use rusqlite::{CachedStatement, Connection, Row, Transaction}; use rusqlite::{CachedStatement, Connection, Row};
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: f32, bs: i32,
protein: f32, protein: i32,
} }
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,
servings_eaten: i32, actual_servings: 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)?,
servings_eaten: row.get(5)?, actual_servings: row.get(5)?,
color: row.get(6)?, color: row.get(6)?,
}) })
} }
@ -80,7 +80,8 @@ impl Food {
struct PreparedStatements {} struct PreparedStatements {}
impl<'conn> PreparedStatements { impl<'conn> PreparedStatements {
fn check(conn: &Connection) -> anyhow::Result<Self> { fn check(conn: &Connection) -> rusqlite::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"))?;
@ -90,6 +91,11 @@ 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")
@ -121,82 +127,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 {
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>>;
fn get_version(tx: &Transaction) -> anyhow::Result<usize> { #[tokio::main]
Ok(tx.query_one(include_str!("get_version.sql"), (), |row| row.get(0))?) async fn main() -> Result<(), std::io::Error> {
} 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();
fn get_migrations() -> [&'static str; 5] { let db_connecion_str = "./foods.db".to_string();
[ debug!(db_connecion_str, "opening database");
include_str!("migrations/1.sql"), let conn = Connection::open(db_connecion_str).expect("failed to open database");
include_str!("migrations/2.sql"), PreparedStatements::check(&conn).expect("failed to prepare sql statements");
include_str!("migrations/3.sql"), if let Err(e) = PreparedStatements::create_tables(&conn).execute(()) {
include_str!("migrations/4.sql"), error!(?e, "failed to create tables");
include_str!("migrations/5.sql"), panic!("failed to create tables: {:#?}", e);
]
}
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(())
}
fn app(conn: Connection) -> axum::Router { let app = Router::new()
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))
@ -222,40 +181,7 @@ fn app(conn: Connection) -> axum::Router {
}), }),
) )
.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)
@ -267,11 +193,10 @@ async fn main() -> anyhow::Result<()> {
.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) -> anyhow::Result<Vec<Food>> { fn get_foods(conn: &ConnState) -> rusqlite::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
@ -281,16 +206,15 @@ fn get_foods(conn: &ConnState) -> anyhow::Result<Vec<Food>> {
Ok(foods) Ok(foods)
} }
fn get_sum(conn: &Arc<Mutex<Connection>>) -> anyhow::Result<Sum> { fn get_sum(conn: &Arc<Mutex<Connection>>) -> rusqlite::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 sum = stmt.query_one((), |row| { let kc = stmt.query_one((), |row| row.get(0))?;
Ok(Sum { let sum = Sum {
kc: row.get(0)?, kc,
bs: row.get(1)?, bs: 99,
protein: row.get(2)?, protein: 99,
}) };
})?;
debug!(?sum); debug!(?sum);
Ok(sum) Ok(sum)
} }
@ -301,15 +225,19 @@ fn get_date() -> String {
date date
} }
async fn root(State(conn): State<ConnState>) -> Result<Html<String>, AppError> { async fn root(State(conn): State<ConnState>) -> Result<Html<String>, StatusCode> {
let foods = get_foods(&conn)?; let foods = get_foods(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let sum = get_sum(&conn)?; let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let date = get_date(); let date = get_date();
let index = IndexTemplate { foods, sum, date }; let index = IndexTemplate { foods, sum, date };
Ok(Html(index.render()?)) Ok(Html(
index
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }
fn do_increase(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::Result<()> { fn do_increase(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::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))?;
@ -317,7 +245,7 @@ fn do_increase(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
fn get_food(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::Result<Food> { fn get_food(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::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)?;
@ -328,16 +256,20 @@ fn get_food(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::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>, AppError> { ) -> Result<Html<String>, StatusCode> {
do_increase(&conn, id)?; do_increase(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let food = get_food(&conn, id)?; let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let sum = get_sum(&conn)?; let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let date = get_date(); let date = get_date();
let update = FoodUpdateTemplate { food, sum, date }; let update = FoodUpdateTemplate { food, sum, date };
Ok(Html(update.render()?)) Ok(Html(
update
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }
fn do_decrease(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::Result<()> { fn do_decrease(conn: &Arc<Mutex<Connection>>, id: i32) -> rusqlite::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))?;
@ -348,16 +280,20 @@ fn do_decrease(conn: &Arc<Mutex<Connection>>, id: i32) -> anyhow::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>, AppError> { ) -> Result<Html<String>, StatusCode> {
do_decrease(&conn, id)?; do_decrease(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let food = get_food(&conn, id)?; let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let sum = get_sum(&conn)?; let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let date = get_date(); let date = get_date();
let update = FoodUpdateTemplate { food, sum, date }; let update = FoodUpdateTemplate { food, sum, date };
Ok(Html(update.render()?)) Ok(Html(
update
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }
fn do_set(conn: &Arc<Mutex<Connection>>, id: i32, amount: i32) -> anyhow::Result<()> { fn do_set(conn: &Arc<Mutex<Connection>>, id: i32, amount: i32) -> rusqlite::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))?;
@ -368,11 +304,15 @@ fn do_set(conn: &Arc<Mutex<Connection>>, id: i32, amount: i32) -> anyhow::Result
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>, AppError> { ) -> Result<Html<String>, StatusCode> {
do_set(&conn, id, amount)?; do_set(&conn, id, amount).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let food = get_food(&conn, id)?; let food = get_food(&conn, id).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let sum = get_sum(&conn)?; let sum = get_sum(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let date = get_date(); let date = get_date();
let update = FoodUpdateTemplate { food, sum, date }; let update = FoodUpdateTemplate { food, sum, date };
Ok(Html(update.render()?)) Ok(Html(
update
.render()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
))
} }

View file

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

View file

@ -1,23 +0,0 @@
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;

View file

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

View file

@ -1,23 +0,0 @@
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;

View file

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

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.servings_eaten %} {% if loop.index as i32 <= food.actual_servings %}
<input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}" value="●" class="checked"> <input type="checkbox" checked hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}">
{% else %} {% else %}
<input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}" value="●" class="unchecked"> <input type="checkbox" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}">
{% endif %} {% endif %}
</label> </label>
{% else %} {% else %}
<label class="bad"> <label class="bad">
{% if loop.index as i32 <= food.servings_eaten %} {% if loop.index as i32 <= food.actual_servings %}
<input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}" value="●" class="checked"> <input type="checkbox" checked hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 - 1}}" hx-target="#card-{{ food.id }}">
{% else %} {% else %}
<input type="button" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}" value="●" class="unchecked"> <input type="checkbox" hx-post="/set/{{ food.id }}/to/{{ loop.index as i32 }}" hx-target="#card-{{ food.id }}">
{% endif %} {% endif %}
</label> </label>
{% endif %} {% endif %}

View file

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