From 010b7005954be6ce4f33dddd9ca5dca1c0cfe95e Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Thu, 5 Feb 2026 20:42:03 +0000 Subject: [PATCH] feat(brews): add quick notes Predefined toggleable tags for brews: Good, Too Fast, Too Slow, Too Hot, Under Extracted, Over Extracted. - Add quick_notes TEXT column storing JSON array - Add QuickNote enum with label/form-value/positive helpers - Update repository to encode/decode JSON, backup to export notes - Add toggle pill UI on brew form with Datastar signals - Show pills on home page cards (muted for Good, amber for others) - Show comma-joined text in data table Notes column - Add --quick-notes flag to CLI, update HTTP client - Update bootstrap script with sample quick notes --- build.rs | 4 ++ migrations/0021_add_brew_notes.sql | 1 + scripts/bootstrap-db.sh | 12 +++-- src/application/routes/api/brews.rs | 47 +++++++++++++++++- src/application/routes/app/add.rs | 1 + src/domain/brews.rs | 63 ++++++++++++++++++++++++ src/infrastructure/backup.rs | 14 +++++- src/infrastructure/client/brews.rs | 7 ++- src/infrastructure/repositories/brews.rs | 36 ++++++++++++-- src/presentation/cli/brews.rs | 12 +++++ src/presentation/web/templates.rs | 6 ++- src/presentation/web/views/brews.rs | 40 ++++++++++++++- src/presentation/web/views/mod.rs | 2 +- templates/pages/add.html | 32 ++++++++++++ templates/pages/home.html | 7 +++ templates/partials/lists/brew_list.html | 8 +++ tests/server/backup.rs | 1 + tests/server/brews_api.rs | 10 ++++ 18 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 migrations/0021_add_brew_notes.sql diff --git a/build.rs b/build.rs index aa55b0b..1300a96 100644 --- a/build.rs +++ b/build.rs @@ -25,6 +25,10 @@ fn tailwind() { println!("cargo:rerun-if-changed={entry}"); } for entry in walkdir("static") { + // Skip the generated output file to avoid circular rebuilds + if entry == "static/css/styles.css" { + continue; + } println!("cargo:rerun-if-changed={entry}"); } diff --git a/migrations/0021_add_brew_notes.sql b/migrations/0021_add_brew_notes.sql new file mode 100644 index 0000000..5784907 --- /dev/null +++ b/migrations/0021_add_brew_notes.sql @@ -0,0 +1 @@ +ALTER TABLE brews ADD COLUMN quick_notes TEXT; diff --git a/scripts/bootstrap-db.sh b/scripts/bootstrap-db.sh index 0da21f7..131550b 100755 --- a/scripts/bootstrap-db.sh +++ b/scripts/bootstrap-db.sh @@ -754,7 +754,8 @@ fi --brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 02") | .id')" \ --filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 Tabbed 02") | .id')" \ --water-volume 250 \ - --water-temp 92.0 + --water-temp 92.0 \ + --quick-notes good # AeroPress brew - Finca Tamana with J-Max (ratio 1:15) ./target/debug/brewlog brew add \ @@ -786,7 +787,8 @@ fi --brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Stagg XF") | .id')" \ --filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Stagg XF Filters") | .id')" \ --water-volume 300 \ - --water-temp 93.0 + --water-temp 93.0 \ + --quick-notes too-fast,under-extracted # Light V60 brew - Red Brick Espresso with Comandante (ratio 1:16.7) ./target/debug/brewlog brew add \ @@ -807,7 +809,8 @@ fi --grind-setting 18.0 \ --brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Original") | .id')" \ --water-volume 225 \ - --water-temp 85.0 + --water-temp 85.0 \ + --quick-notes too-hot,over-extracted # V60 brew - Suke Quto with J-Max (ratio 1:16) ./target/debug/brewlog brew add \ @@ -818,7 +821,8 @@ fi --brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 02") | .id')" \ --filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="FAST Specialty 02") | .id')" \ --water-volume 320 \ - --water-temp 92.0 + --water-temp 92.0 \ + --quick-notes good # Stagg XF brew - Simbi with Comandante (ratio 1:16) ./target/debug/brewlog brew add \ diff --git a/src/application/routes/api/brews.rs b/src/application/routes/api/brews.rs index 0ac8b3e..2eb6de3 100644 --- a/src/application/routes/api/brews.rs +++ b/src/application/routes/api/brews.rs @@ -13,7 +13,7 @@ use crate::application::routes::support::{ }; use crate::application::server::AppState; use crate::domain::bags::BagFilter; -use crate::domain::brews::{BrewFilter, BrewSortKey, BrewWithDetails, NewBrew}; +use crate::domain::brews::{BrewFilter, BrewSortKey, BrewWithDetails, NewBrew, QuickNote}; use crate::domain::gear::{GearCategory, GearFilter, GearSortKey}; use crate::domain::ids::{BagId, BrewId, GearId}; use crate::domain::listing::{ListRequest, PageSize, SortDirection}; @@ -21,6 +21,7 @@ use crate::domain::timeline::{NewTimelineEvent, TimelineBrewData, TimelineEventD use crate::presentation::web::templates::BrewListTemplate; use crate::presentation::web::views::{ BagOptionView, BrewDefaultsView, BrewView, GearOptionView, ListNavigator, Paginated, + QuickNoteView, }; const BREW_PAGE_PATH: &str = "/data?type=brews"; @@ -37,6 +38,7 @@ pub(crate) struct BrewFormData { pub(crate) brewer_options: Vec, pub(crate) filter_paper_options: Vec, pub(crate) defaults: BrewDefaultsView, + pub(crate) quick_note_options: Vec, } pub(crate) async fn load_brew_form_data(state: &AppState) -> Result { @@ -80,12 +82,19 @@ pub(crate) async fn load_brew_form_data(state: &AppState) -> Result(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value: Option = Option::deserialize(deserializer)?; + match value { + None | Some(serde_json::Value::Null) => Ok(Vec::new()), + Some(serde_json::Value::String(s)) if s.is_empty() => Ok(Vec::new()), + Some(serde_json::Value::String(s)) => Ok(s + .split(',') + .filter_map(|v| QuickNote::from_str_value(v.trim())) + .collect()), + Some(serde_json::Value::Array(arr)) => Ok(arr + .iter() + .filter_map(|v| v.as_str().and_then(QuickNote::from_str_value)) + .collect()), + Some(_) => Err(serde::de::Error::custom("invalid quick_notes")), + } +} + #[derive(Debug, Deserialize)] pub(crate) struct NewBrewSubmission { bag_id: BagId, @@ -158,6 +187,8 @@ pub(crate) struct NewBrewSubmission { filter_paper_id: Option, water_volume: i32, water_temp: f64, + #[serde(default, deserialize_with = "deserialize_quick_notes")] + quick_notes: Vec, } impl NewBrewSubmission { @@ -186,6 +217,7 @@ impl NewBrewSubmission { filter_paper_id: self.filter_paper_id, water_volume: self.water_volume, water_temp: self.water_temp, + quick_notes: self.quick_notes, }) } } @@ -309,6 +341,19 @@ fn brew_timeline_event(enriched: &BrewWithDetails) -> NewTimelineEvent { value: ratio, }); + if !enriched.brew.quick_notes.is_empty() { + let labels: Vec<&str> = enriched + .brew + .quick_notes + .iter() + .map(|n| n.label()) + .collect(); + details.push(TimelineEventDetail { + label: "Notes".to_string(), + value: labels.join(", "), + }); + } + NewTimelineEvent { entity_type: "brew".to_string(), entity_id: enriched.brew.id.into_inner(), diff --git a/src/application/routes/app/add.rs b/src/application/routes/app/add.rs index 69f51ea..32eb658 100644 --- a/src/application/routes/app/add.rs +++ b/src/application/routes/app/add.rs @@ -99,6 +99,7 @@ pub(crate) async fn add_page( filter_paper_options: brew_form.filter_paper_options, cafe_options, defaults: brew_form.defaults, + quick_note_options: brew_form.quick_note_options, }; render_html(template).map(IntoResponse::into_response) diff --git a/src/domain/brews.rs b/src/domain/brews.rs index 233d20a..1d2995d 100644 --- a/src/domain/brews.rs +++ b/src/domain/brews.rs @@ -4,6 +4,67 @@ use serde::{Deserialize, Serialize}; use super::ids::{BagId, BrewId, GearId}; use super::listing::{SortDirection, SortKey}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum QuickNote { + Good, + TooFast, + TooSlow, + TooHot, + UnderExtracted, + OverExtracted, +} + +impl QuickNote { + pub fn label(self) -> &'static str { + match self { + Self::Good => "Good", + Self::TooFast => "Too Fast", + Self::TooSlow => "Too Slow", + Self::TooHot => "Too Hot", + Self::UnderExtracted => "Under Extracted", + Self::OverExtracted => "Over Extracted", + } + } + + pub fn form_value(self) -> &'static str { + match self { + Self::Good => "good", + Self::TooFast => "too-fast", + Self::TooSlow => "too-slow", + Self::TooHot => "too-hot", + Self::UnderExtracted => "under-extracted", + Self::OverExtracted => "over-extracted", + } + } + + pub fn from_str_value(s: &str) -> Option { + match s { + "good" | "Good" => Some(Self::Good), + "too-fast" | "Too Fast" => Some(Self::TooFast), + "too-slow" | "Too Slow" => Some(Self::TooSlow), + "too-hot" | "Too Hot" => Some(Self::TooHot), + "under-extracted" | "Under Extracted" => Some(Self::UnderExtracted), + "over-extracted" | "Over Extracted" => Some(Self::OverExtracted), + _ => None, + } + } + + pub fn is_positive(self) -> bool { + matches!(self, Self::Good) + } + + pub fn all() -> &'static [Self] { + &[ + Self::Good, + Self::TooFast, + Self::TooSlow, + Self::TooHot, + Self::UnderExtracted, + Self::OverExtracted, + ] + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Brew { pub id: BrewId, @@ -15,6 +76,7 @@ pub struct Brew { pub filter_paper_id: Option, pub water_volume: i32, pub water_temp: f64, + pub quick_notes: Vec, pub created_at: DateTime, pub updated_at: DateTime, } @@ -42,6 +104,7 @@ pub struct NewBrew { pub filter_paper_id: Option, pub water_volume: i32, pub water_temp: f64, + pub quick_notes: Vec, } /// Filter criteria for brew queries. diff --git a/src/infrastructure/backup.rs b/src/infrastructure/backup.rs index 3c302f3..e690048 100644 --- a/src/infrastructure/backup.rs +++ b/src/infrastructure/backup.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string}; use crate::domain::bags::Bag; -use crate::domain::brews::Brew; +use crate::domain::brews::{Brew, QuickNote}; use crate::domain::cafes::Cafe; use crate::domain::gear::{Gear, GearCategory}; use crate::domain::ids::{BagId, BrewId, CafeId, GearId, RoastId, RoasterId, TimelineEventId}; @@ -164,7 +164,7 @@ impl BackupService { async fn export_brews(&self) -> anyhow::Result> { let records = sqlx::query_as::<_, BrewRecord>( - "SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, created_at, updated_at FROM brews ORDER BY id", + "SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at FROM brews ORDER BY id", ) .fetch_all(&self.pool) .await @@ -561,12 +561,21 @@ struct BrewRecord { filter_paper_id: Option, water_volume: i32, water_temp: f64, + quick_notes: Option, created_at: DateTime, updated_at: DateTime, } impl BrewRecord { fn into_domain(self) -> Brew { + let quick_notes = match self.quick_notes { + Some(s) if !s.is_empty() => serde_json::from_str::>(&s) + .unwrap_or_default() + .iter() + .filter_map(|v| QuickNote::from_str_value(v)) + .collect(), + _ => Vec::new(), + }; Brew { id: BrewId::new(self.id), bag_id: BagId::new(self.bag_id), @@ -577,6 +586,7 @@ impl BrewRecord { filter_paper_id: self.filter_paper_id.map(GearId::new), water_volume: self.water_volume, water_temp: self.water_temp, + quick_notes, created_at: self.created_at, updated_at: self.updated_at, } diff --git a/src/infrastructure/client/brews.rs b/src/infrastructure/client/brews.rs index 877d6fd..d0bb267 100644 --- a/src/infrastructure/client/brews.rs +++ b/src/infrastructure/client/brews.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; -use crate::domain::brews::BrewWithDetails; +use crate::domain::brews::{BrewWithDetails, QuickNote}; use crate::domain::ids::{BagId, BrewId, GearId}; use super::BrewlogClient; @@ -25,6 +25,7 @@ impl<'a> BrewsClient<'a> { filter_paper_id: Option, water_volume: i32, water_temp: f64, + quick_notes: Vec, ) -> Result { let url = self.inner.endpoint("api/v1/brews")?; let mut payload = serde_json::json!({ @@ -39,6 +40,10 @@ impl<'a> BrewsClient<'a> { if let Some(fp_id) = filter_paper_id { payload["filter_paper_id"] = serde_json::json!(fp_id); } + if !quick_notes.is_empty() { + let labels: Vec<&str> = quick_notes.iter().map(|n| n.label()).collect(); + payload["quick_notes"] = serde_json::json!(labels); + } let response = self .inner diff --git a/src/infrastructure/repositories/brews.rs b/src/infrastructure/repositories/brews.rs index 46cd615..488fb43 100644 --- a/src/infrastructure/repositories/brews.rs +++ b/src/infrastructure/repositories/brews.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use sqlx::query_as; use crate::domain::RepositoryError; -use crate::domain::brews::{Brew, BrewFilter, BrewSortKey, BrewWithDetails, NewBrew}; +use crate::domain::brews::{Brew, BrewFilter, BrewSortKey, BrewWithDetails, NewBrew, QuickNote}; use crate::domain::ids::{BagId, BrewId, GearId}; use crate::domain::listing::{ListRequest, Page, SortDirection}; use crate::domain::repositories::BrewRepository; @@ -13,6 +13,7 @@ const BASE_SELECT: &str = r" SELECT br.id, br.bag_id, br.coffee_weight, br.grinder_id, br.grind_setting, br.brewer_id, br.filter_paper_id, br.water_volume, br.water_temp, + br.quick_notes, br.created_at, br.updated_at, r.name as roast_name, r.slug as roast_slug, rr.name as roaster_name, rr.slug as roaster_slug, @@ -51,6 +52,26 @@ impl SqlBrewRepository { } } + fn decode_quick_notes(raw: Option) -> Vec { + match raw { + Some(s) if !s.is_empty() => serde_json::from_str::>(&s) + .unwrap_or_default() + .iter() + .filter_map(|v| QuickNote::from_str_value(v)) + .collect(), + _ => Vec::new(), + } + } + + fn encode_quick_notes(notes: &[QuickNote]) -> Option { + if notes.is_empty() { + None + } else { + let labels: Vec<&str> = notes.iter().map(|n| n.label()).collect(); + serde_json::to_string(&labels).ok() + } + } + fn to_domain(record: BrewRecord) -> Brew { Brew { id: BrewId::new(record.id), @@ -62,6 +83,7 @@ impl SqlBrewRepository { filter_paper_id: record.filter_paper_id.map(GearId::new), water_volume: record.water_volume, water_temp: record.water_temp, + quick_notes: Self::decode_quick_notes(record.quick_notes), created_at: record.created_at, updated_at: record.updated_at, } @@ -79,6 +101,7 @@ impl SqlBrewRepository { filter_paper_id: record.filter_paper_id.map(GearId::new), water_volume: record.water_volume, water_temp: record.water_temp, + quick_notes: Self::decode_quick_notes(record.quick_notes), created_at: record.created_at, updated_at: record.updated_at, }, @@ -135,9 +158,9 @@ impl BrewRepository for SqlBrewRepository { // Insert the brew let insert_query = r" - INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - RETURNING id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, created_at, updated_at + INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + RETURNING id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at "; let record = query_as::<_, BrewRecord>(insert_query) @@ -152,6 +175,7 @@ impl BrewRepository for SqlBrewRepository { ) .bind(brew.water_volume) .bind(brew.water_temp) + .bind(Self::encode_quick_notes(&brew.quick_notes)) .fetch_one(&mut *tx) .await .map_err(|err| RepositoryError::unexpected(err.to_string()))?; @@ -165,7 +189,7 @@ impl BrewRepository for SqlBrewRepository { async fn get(&self, id: BrewId) -> Result { let query = r" - SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, created_at, updated_at + SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at FROM brews WHERE id = ? "; @@ -263,6 +287,7 @@ struct BrewRecord { filter_paper_id: Option, water_volume: i32, water_temp: f64, + quick_notes: Option, created_at: DateTime, updated_at: DateTime, } @@ -278,6 +303,7 @@ struct BrewWithDetailsRecord { filter_paper_id: Option, water_volume: i32, water_temp: f64, + quick_notes: Option, created_at: DateTime, updated_at: DateTime, roast_name: String, diff --git a/src/presentation/cli/brews.rs b/src/presentation/cli/brews.rs index 85ff48d..2d88827 100644 --- a/src/presentation/cli/brews.rs +++ b/src/presentation/cli/brews.rs @@ -3,6 +3,7 @@ use clap::{Args, Subcommand}; use super::macros::{define_delete_command, define_get_command}; use super::print_json; +use crate::domain::brews::QuickNote; use crate::domain::ids::{BagId, BrewId, GearId}; use crate::infrastructure::client::BrewlogClient; @@ -60,9 +61,19 @@ pub struct AddBrewCommand { /// Water temperature in Celsius #[arg(long, default_value = "91.0")] pub water_temp: f64, + + /// Quick notes (comma-separated: good,too-fast,too-slow,too-hot,under-extracted,over-extracted) + #[arg(long, value_delimiter = ',')] + pub quick_notes: Vec, } pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result<()> { + let quick_notes: Vec = command + .quick_notes + .iter() + .filter_map(|s| QuickNote::from_str_value(s)) + .collect(); + let brew = client .brews() .create( @@ -74,6 +85,7 @@ pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result command.filter_paper_id.map(GearId::new), command.water_volume, command.water_temp, + quick_notes, ) .await?; print_json(&brew) diff --git a/src/presentation/web/templates.rs b/src/presentation/web/templates.rs index 4204c46..6549ba1 100644 --- a/src/presentation/web/templates.rs +++ b/src/presentation/web/templates.rs @@ -2,8 +2,9 @@ use askama::Template; use super::views::{ BagOptionView, BagView, BrewDefaultsView, BrewView, CafeOptionView, CafeView, CupView, - GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, RoastOptionView, RoastView, - RoasterOptionView, RoasterView, StatsView, TimelineEventView, TimelineMonthView, + GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, QuickNoteView, + RoastOptionView, RoastView, RoasterOptionView, RoasterView, StatsView, TimelineEventView, + TimelineMonthView, }; use crate::domain::bags::BagSortKey; use crate::domain::brews::BrewSortKey; @@ -170,6 +171,7 @@ pub struct AddTemplate { pub filter_paper_options: Vec, pub cafe_options: Vec, pub defaults: BrewDefaultsView, + pub quick_note_options: Vec, } pub fn render_template(template: T) -> Result { diff --git a/src/presentation/web/views/brews.rs b/src/presentation/web/views/brews.rs index 783a7b7..620ae71 100644 --- a/src/presentation/web/views/brews.rs +++ b/src/presentation/web/views/brews.rs @@ -1,7 +1,28 @@ -use crate::domain::brews::{Brew, BrewWithDetails}; +use crate::domain::brews::{Brew, BrewWithDetails, QuickNote}; use super::relative_date; +#[derive(Clone)] +pub struct QuickNoteView { + pub label: String, + pub pill_class: &'static str, + pub form_value: String, +} + +impl From for QuickNoteView { + fn from(note: QuickNote) -> Self { + Self { + label: note.label().to_string(), + pill_class: if note.is_positive() { + "pill pill-success" + } else { + "pill pill-warning" + }, + form_value: note.form_value().to_string(), + } + } +} + #[derive(Clone)] pub struct BrewView { pub id: String, @@ -21,6 +42,8 @@ pub struct BrewView { pub water_volume: String, pub water_temp: String, pub ratio: String, + pub quick_notes: Vec, + pub quick_notes_label: String, pub created_date: String, pub created_time: String, pub relative_date_label: String, @@ -42,6 +65,19 @@ impl BrewView { "\u{2014}".to_string() }; + let quick_notes: Vec = brew + .brew + .quick_notes + .iter() + .copied() + .map(QuickNoteView::from) + .collect(); + let quick_notes_label = quick_notes + .iter() + .map(|n| n.label.as_str()) + .collect::>() + .join(", "); + Self { id: brew.brew.id.to_string(), bag_id: brew.brew.bag_id.into_inner(), @@ -63,6 +99,8 @@ impl BrewView { water_volume: format!("{}ml", brew.brew.water_volume), water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp), ratio, + quick_notes, + quick_notes_label, created_date: brew.brew.created_at.format("%Y-%m-%d").to_string(), created_time: brew.brew.created_at.format("%H:%M").to_string(), relative_date_label: relative_date(brew.brew.created_at), diff --git a/src/presentation/web/views/mod.rs b/src/presentation/web/views/mod.rs index da3880a..0b90b4e 100644 --- a/src/presentation/web/views/mod.rs +++ b/src/presentation/web/views/mod.rs @@ -8,7 +8,7 @@ mod roasts; mod timeline; pub use bags::{BagOptionView, BagView}; -pub use brews::{BrewDefaultsView, BrewView}; +pub use brews::{BrewDefaultsView, BrewView, QuickNoteView}; pub use cafes::{CafeOptionView, CafeView, NearbyCafeView}; pub use cups::CupView; pub use gear::{GearOptionView, GearView}; diff --git a/templates/pages/add.html b/templates/pages/add.html index 733633b..d4a8cd8 100644 --- a/templates/pages/add.html +++ b/templates/pages/add.html @@ -81,6 +81,12 @@ data-signals:_user-lat="0" data-signals:_user-lng="0" data-signals:_reviewing-cafe="false" + data-signals:_qn-good="false" + data-signals:_qn-too-fast="false" + data-signals:_qn-too-slow="false" + data-signals:_qn-too-hot="false" + data-signals:_qn-under-extracted="false" + data-signals:_qn-over-extracted="false" >

Manual Add

@@ -341,6 +347,32 @@ + +
+

Quick Notes

+
+ + + + + + +
+ +
+ {% if !brew.quick_notes.is_empty() %} +
+ {% for qn in brew.quick_notes %} + {{ qn.label }} + {% endfor %} +
+ {% endif %} {% if is_authenticated %}
Grind Recipe Brewer + Notes {% if is_authenticated %} Actions {% endif %} @@ -66,6 +67,13 @@ {% if let Some(fp_name) = brew.filter_paper_name %} {{ fp_name }} {% endif %} + + {% if !brew.quick_notes_label.is_empty() %} + {{ brew.quick_notes_label }} + {% else %} + + {% endif %} + {% if is_authenticated %}
diff --git a/tests/server/backup.rs b/tests/server/backup.rs index ef7e575..91b9d77 100644 --- a/tests/server/backup.rs +++ b/tests/server/backup.rs @@ -201,6 +201,7 @@ async fn populate_test_data(db: &TestDb) -> (Roaster, Roast, Bag, Gear, Gear, Ge filter_paper_id: Some(filter_paper.id), water_volume: 250, water_temp: 93.5, + quick_notes: Vec::new(), }) .await .expect("failed to create brew"); diff --git a/tests/server/brews_api.rs b/tests/server/brews_api.rs index dc90e39..0bed962 100644 --- a/tests/server/brews_api.rs +++ b/tests/server/brews_api.rs @@ -25,6 +25,7 @@ async fn creating_a_brew_returns_201_for_valid_data() { filter_paper_id: None, water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; // Act @@ -71,6 +72,7 @@ async fn creating_a_brew_with_filter_paper_returns_201() { filter_paper_id: Some(filter_paper.id), water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; // Act @@ -109,6 +111,7 @@ async fn creating_a_brew_deducts_from_bag_remaining() { filter_paper_id: None, water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; // Act @@ -151,6 +154,7 @@ async fn creating_a_brew_fails_if_insufficient_coffee_in_bag() { filter_paper_id: None, water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; // Act @@ -217,6 +221,7 @@ async fn listing_brews_returns_200_and_enriched_data() { filter_paper_id: Some(filter_paper.id), water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; client @@ -270,6 +275,7 @@ async fn listing_brews_without_filter_paper_returns_none() { filter_paper_id: None, water_volume: 255, water_temp: 88.0, + quick_notes: Vec::new(), }; client @@ -318,6 +324,7 @@ async fn listing_brews_with_bag_filter_returns_filtered_results() { filter_paper_id: None, water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; client @@ -338,6 +345,7 @@ async fn listing_brews_with_bag_filter_returns_filtered_results() { filter_paper_id: None, water_volume: 255, water_temp: 88.0, + quick_notes: Vec::new(), }; client @@ -383,6 +391,7 @@ async fn getting_a_brew_returns_200_for_valid_id() { filter_paper_id: None, water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; let create_response = client @@ -432,6 +441,7 @@ async fn deleting_a_brew_returns_204() { filter_paper_id: None, water_volume: 250, water_temp: 92.0, + quick_notes: Vec::new(), }; let create_response = client