feat(brews): add brew time field
- Add brew_time INTEGER column to brews table (nullable, stored as seconds) - Display as M:SS format with stepper control (+/-5s) in add form - Replace ratio with brew time in brew list Recipe column - Show brew time in brew cards and timeline events - Add --brew-time CLI flag and update bootstrap script (1:45-2:45 range)
This commit is contained in:
parent
ed0cb2fd39
commit
902d52b171
17 changed files with 115 additions and 13 deletions
|
|
@ -11,6 +11,7 @@ CREATE TABLE brews (
|
|||
water_volume INTEGER NOT NULL,
|
||||
water_temp REAL NOT NULL,
|
||||
quick_notes TEXT,
|
||||
brew_time INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -855,6 +855,7 @@ fi
|
|||
--water-volume 250 \
|
||||
--water-temp 92.0 \
|
||||
--quick-notes good \
|
||||
--brew-time 135 \
|
||||
--created-at "2025-11-18T08:30:00Z"
|
||||
|
||||
# AeroPress brew - Finca Tamana with J-Max (ratio 1:15) — late Nov
|
||||
|
|
@ -866,6 +867,7 @@ fi
|
|||
--brewer-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Original") | .id')" \
|
||||
--water-volume 255 \
|
||||
--water-temp 88.0 \
|
||||
--brew-time 105 \
|
||||
--created-at "2025-11-25T09:00:00Z"
|
||||
|
||||
# Double V60 brew - La Linda with Comandante (ratio 1:16.7) — mid Dec
|
||||
|
|
@ -878,6 +880,7 @@ fi
|
|||
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="FAST Specialty 02") | .id')" \
|
||||
--water-volume 500 \
|
||||
--water-temp 91.0 \
|
||||
--brew-time 165 \
|
||||
--created-at "2025-12-15T10:00:00Z"
|
||||
|
||||
# Stagg XF brew - Guji Highland with J-Max (ratio 1:16.7) — late Dec
|
||||
|
|
@ -891,6 +894,7 @@ fi
|
|||
--water-volume 300 \
|
||||
--water-temp 93.0 \
|
||||
--quick-notes too-fast,under-extracted \
|
||||
--brew-time 110 \
|
||||
--created-at "2025-12-22T08:00:00Z"
|
||||
|
||||
# Light V60 brew - Red Brick Espresso with Comandante (ratio 1:16.7) — early Jan
|
||||
|
|
@ -903,6 +907,7 @@ fi
|
|||
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="V60 Tabbed 02") | .id')" \
|
||||
--water-volume 200 \
|
||||
--water-temp 94.0 \
|
||||
--brew-time 150 \
|
||||
--created-at "2026-01-02T09:30:00Z"
|
||||
|
||||
# AeroPress inverted - El Paraiso Anaerobic with J-Max (ratio 1:15) — mid Jan
|
||||
|
|
@ -915,6 +920,7 @@ fi
|
|||
--water-volume 225 \
|
||||
--water-temp 85.0 \
|
||||
--quick-notes too-hot,over-extracted \
|
||||
--brew-time 120 \
|
||||
--created-at "2026-01-12T08:15:00Z"
|
||||
|
||||
# V60 brew - Suke Quto with J-Max (ratio 1:16) — late Jan
|
||||
|
|
@ -928,6 +934,7 @@ fi
|
|||
--water-volume 320 \
|
||||
--water-temp 92.0 \
|
||||
--quick-notes good \
|
||||
--brew-time 140 \
|
||||
--created-at "2026-01-20T09:00:00Z"
|
||||
|
||||
# Stagg XF brew - Simbi with Comandante (ratio 1:16) — late Jan
|
||||
|
|
@ -940,6 +947,7 @@ fi
|
|||
--filter-paper-id "$(./target/debug/brewlog gear list | jq -r '.[] | select(.model=="Stagg XF Filters") | .id')" \
|
||||
--water-volume 256 \
|
||||
--water-temp 90.0 \
|
||||
--brew-time 130 \
|
||||
--created-at "2026-01-28T08:45:00Z"
|
||||
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -190,6 +190,8 @@ pub(crate) struct NewBrewSubmission {
|
|||
#[serde(default, deserialize_with = "deserialize_quick_notes")]
|
||||
quick_notes: Vec<QuickNote>,
|
||||
#[serde(default)]
|
||||
brew_time: Option<i32>,
|
||||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
|
|
@ -209,6 +211,11 @@ impl NewBrewSubmission {
|
|||
"water temperature must be between 0 and 100",
|
||||
));
|
||||
}
|
||||
if let Some(bt) = self.brew_time
|
||||
&& bt <= 0
|
||||
{
|
||||
return Err(AppError::validation("brew time must be positive"));
|
||||
}
|
||||
|
||||
Ok(NewBrew {
|
||||
bag_id: self.bag_id,
|
||||
|
|
@ -220,6 +227,7 @@ impl NewBrewSubmission {
|
|||
water_volume: self.water_volume,
|
||||
water_temp: self.water_temp,
|
||||
quick_notes: self.quick_notes,
|
||||
brew_time: self.brew_time,
|
||||
created_at: self.created_at,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,10 +78,16 @@ pub struct Brew {
|
|||
pub water_volume: i32,
|
||||
pub water_temp: f64,
|
||||
pub quick_notes: Vec<QuickNote>,
|
||||
pub brew_time: Option<i32>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Format seconds as "M:SS" (e.g., 150 -> "2:30").
|
||||
pub fn format_brew_time(seconds: i32) -> String {
|
||||
format!("{}:{:02}", seconds / 60, seconds % 60)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrewWithDetails {
|
||||
#[serde(flatten)]
|
||||
|
|
@ -124,7 +130,10 @@ impl BrewWithDetails {
|
|||
},
|
||||
TimelineEventDetail {
|
||||
label: "Grinder".to_string(),
|
||||
value: format!("{} \u{00B7} {:.1}", self.grinder_name, self.brew.grind_setting),
|
||||
value: format!(
|
||||
"{} \u{00B7} {:.1}",
|
||||
self.grinder_name, self.brew.grind_setting
|
||||
),
|
||||
},
|
||||
TimelineEventDetail {
|
||||
label: "Brewer".to_string(),
|
||||
|
|
@ -132,6 +141,17 @@ impl BrewWithDetails {
|
|||
},
|
||||
];
|
||||
|
||||
if let Some(bt) = self.brew.brew_time {
|
||||
// Insert after Water (index 2) so it appears right beneath it
|
||||
details.insert(
|
||||
3,
|
||||
TimelineEventDetail {
|
||||
label: "Brew Time".to_string(),
|
||||
value: format_brew_time(bt),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref fp_name) = self.filter_paper_name {
|
||||
details.push(TimelineEventDetail {
|
||||
label: "Filter".to_string(),
|
||||
|
|
@ -171,6 +191,7 @@ impl BrewWithDetails {
|
|||
grind_setting: self.brew.grind_setting,
|
||||
water_volume: self.brew.water_volume,
|
||||
water_temp: self.brew.water_temp,
|
||||
brew_time: self.brew.brew_time,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -188,6 +209,8 @@ pub struct NewBrew {
|
|||
pub water_temp: f64,
|
||||
pub quick_notes: Vec<QuickNote>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub brew_time: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub struct TimelineBrewData {
|
|||
pub grind_setting: f64,
|
||||
pub water_volume: i32,
|
||||
pub water_temp: f64,
|
||||
pub brew_time: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ impl BackupService {
|
|||
|
||||
async fn export_brews(&self) -> anyhow::Result<Vec<Brew>> {
|
||||
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, quick_notes, 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, brew_time, created_at, updated_at FROM brews ORDER BY id",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
|
|
@ -410,7 +410,7 @@ impl BackupService {
|
|||
};
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO brews (id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO brews (id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, brew_time, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(i64::from(brew.id))
|
||||
.bind(i64::from(brew.bag_id))
|
||||
|
|
@ -422,6 +422,7 @@ impl BackupService {
|
|||
.bind(brew.water_volume)
|
||||
.bind(brew.water_temp)
|
||||
.bind(quick_notes_json.as_deref())
|
||||
.bind(brew.brew_time)
|
||||
.bind(brew.created_at)
|
||||
.bind(brew.updated_at)
|
||||
.execute(&mut **tx)
|
||||
|
|
@ -650,6 +651,7 @@ struct BrewRecord {
|
|||
water_volume: i32,
|
||||
water_temp: f64,
|
||||
quick_notes: Option<String>,
|
||||
brew_time: Option<i32>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -675,6 +677,7 @@ impl BrewRecord {
|
|||
water_volume: self.water_volume,
|
||||
water_temp: self.water_temp,
|
||||
quick_notes,
|
||||
brew_time: self.brew_time,
|
||||
created_at: self.created_at,
|
||||
updated_at: self.updated_at,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ impl<'a> BrewsClient<'a> {
|
|||
water_volume: i32,
|
||||
water_temp: f64,
|
||||
quick_notes: Vec<QuickNote>,
|
||||
brew_time: Option<i32>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
) -> Result<BrewWithDetails> {
|
||||
let url = self.inner.endpoint("api/v1/brews")?;
|
||||
|
|
@ -46,6 +47,9 @@ impl<'a> BrewsClient<'a> {
|
|||
let labels: Vec<&str> = quick_notes.iter().map(|n| n.label()).collect();
|
||||
payload["quick_notes"] = serde_json::json!(labels);
|
||||
}
|
||||
if let Some(bt) = brew_time {
|
||||
payload["brew_time"] = serde_json::json!(bt);
|
||||
}
|
||||
if let Some(ts) = created_at {
|
||||
payload["created_at"] = serde_json::json!(ts);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +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.quick_notes, br.brew_time,
|
||||
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,
|
||||
|
|
@ -90,6 +90,7 @@ impl SqlBrewRepository {
|
|||
water_volume: record.water_volume,
|
||||
water_temp: record.water_temp,
|
||||
quick_notes: Self::decode_quick_notes(record.quick_notes),
|
||||
brew_time: record.brew_time,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
|
|
@ -108,6 +109,7 @@ impl SqlBrewRepository {
|
|||
water_volume: record.water_volume,
|
||||
water_temp: record.water_temp,
|
||||
quick_notes: Self::decode_quick_notes(record.quick_notes),
|
||||
brew_time: record.brew_time,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
},
|
||||
|
|
@ -165,9 +167,9 @@ impl BrewRepository for SqlBrewRepository {
|
|||
// Insert the brew
|
||||
let created_at = brew.created_at.unwrap_or_else(Utc::now);
|
||||
let insert_query = r"
|
||||
INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at)
|
||||
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
|
||||
INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, brew_time, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, brew_time, created_at, updated_at
|
||||
";
|
||||
|
||||
let record = query_as::<_, BrewRecord>(insert_query)
|
||||
|
|
@ -183,6 +185,7 @@ impl BrewRepository for SqlBrewRepository {
|
|||
.bind(brew.water_volume)
|
||||
.bind(brew.water_temp)
|
||||
.bind(Self::encode_quick_notes(&brew.quick_notes))
|
||||
.bind(brew.brew_time)
|
||||
.bind(created_at)
|
||||
.bind(created_at)
|
||||
.fetch_one(&mut *tx)
|
||||
|
|
@ -198,7 +201,7 @@ impl BrewRepository for SqlBrewRepository {
|
|||
|
||||
async fn get(&self, id: BrewId) -> Result<Brew, RepositoryError> {
|
||||
let query = r"
|
||||
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
|
||||
SELECT id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, brew_time, created_at, updated_at
|
||||
FROM brews
|
||||
WHERE id = ?
|
||||
";
|
||||
|
|
@ -297,6 +300,7 @@ struct BrewRecord {
|
|||
water_volume: i32,
|
||||
water_temp: f64,
|
||||
quick_notes: Option<String>,
|
||||
brew_time: Option<i32>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -313,6 +317,7 @@ struct BrewWithDetailsRecord {
|
|||
water_volume: i32,
|
||||
water_temp: f64,
|
||||
quick_notes: Option<String>,
|
||||
brew_time: Option<i32>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
roast_name: String,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ pub struct AddBrewCommand {
|
|||
#[arg(long, value_delimiter = ',')]
|
||||
pub quick_notes: Vec<String>,
|
||||
|
||||
/// Brew time in seconds (e.g., 150 for 2:30)
|
||||
#[arg(long)]
|
||||
pub brew_time: Option<i32>,
|
||||
|
||||
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||
#[arg(long)]
|
||||
pub created_at: Option<String>,
|
||||
|
|
@ -95,6 +99,7 @@ pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result
|
|||
command.water_volume,
|
||||
command.water_temp,
|
||||
quick_notes,
|
||||
command.brew_time,
|
||||
created_at,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::domain::brews::{Brew, BrewWithDetails, QuickNote};
|
||||
use crate::domain::brews::{Brew, BrewWithDetails, QuickNote, format_brew_time};
|
||||
|
||||
use super::relative_date;
|
||||
|
||||
|
|
@ -43,6 +43,7 @@ pub struct BrewView {
|
|||
pub water_volume: String,
|
||||
pub water_temp: String,
|
||||
pub ratio: String,
|
||||
pub brew_time: Option<String>,
|
||||
pub quick_notes: Vec<QuickNoteView>,
|
||||
pub quick_notes_label: String,
|
||||
pub quick_notes_raw: String,
|
||||
|
|
@ -54,6 +55,7 @@ pub struct BrewView {
|
|||
pub grind_setting_raw: f64,
|
||||
pub water_volume_raw: i32,
|
||||
pub water_temp_raw: f64,
|
||||
pub brew_time_raw: Option<i32>,
|
||||
}
|
||||
|
||||
impl BrewView {
|
||||
|
|
@ -110,6 +112,7 @@ impl BrewView {
|
|||
water_volume: format!("{}ml", brew.brew.water_volume),
|
||||
water_temp: format!("{:.1}\u{00B0}C", brew.brew.water_temp),
|
||||
ratio,
|
||||
brew_time: brew.brew.brew_time.map(format_brew_time),
|
||||
quick_notes,
|
||||
quick_notes_label,
|
||||
quick_notes_raw,
|
||||
|
|
@ -120,6 +123,7 @@ impl BrewView {
|
|||
grind_setting_raw: brew.brew.grind_setting,
|
||||
water_volume_raw: brew.brew.water_volume,
|
||||
water_temp_raw: brew.brew.water_temp,
|
||||
brew_time_raw: brew.brew.brew_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,6 +137,7 @@ pub struct BrewDefaultsView {
|
|||
pub grind_setting: f64,
|
||||
pub water_volume: i32,
|
||||
pub water_temp: f64,
|
||||
pub brew_time: Option<i32>,
|
||||
}
|
||||
|
||||
impl Default for BrewDefaultsView {
|
||||
|
|
@ -147,6 +152,7 @@ impl Default for BrewDefaultsView {
|
|||
grind_setting: 6.0,
|
||||
water_volume: 250,
|
||||
water_temp: 91.0,
|
||||
brew_time: Some(120),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -166,6 +172,7 @@ impl From<Brew> for BrewDefaultsView {
|
|||
grind_setting: brew.grind_setting,
|
||||
water_volume: brew.water_volume,
|
||||
water_temp: brew.water_temp,
|
||||
brew_time: brew.brew_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ pub struct TimelineBrewDataView {
|
|||
pub grind_setting: f64,
|
||||
pub water_volume: i32,
|
||||
pub water_temp: f64,
|
||||
pub brew_time: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -131,6 +132,7 @@ impl TimelineEventView {
|
|||
grind_setting: bd.grind_setting,
|
||||
water_volume: bd.water_volume,
|
||||
water_temp: bd.water_temp,
|
||||
brew_time: bd.brew_time,
|
||||
});
|
||||
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
data-on:submit="$_extracting = true; $_extractError = ''; @post('/api/v1/extract-bag-scan', {contentType: 'form'})"
|
||||
data-on:datastar-fetch="if (!$_extracting) return; if (evt.detail.type === 'finished') { $_extracting = false; $_scanExtracted = true; document.getElementById('scan-extract-form').reset() } else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Extraction failed. Please try again.' }"
|
||||
>
|
||||
{{ scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe the coffee bag…", "Waiting for response…") }}
|
||||
{{ scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError", "Describe a coffee bag…", "Waiting for response…") }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
@ -68,6 +68,7 @@
|
|||
data-signals:_brew-grind="{{ defaults.grind_setting }}"
|
||||
data-signals:_brew-volume="{{ defaults.water_volume }}"
|
||||
data-signals:_brew-weight="{{ defaults.coffee_weight }}"
|
||||
data-signals:_brew-time="{{ defaults.brew_time.unwrap_or(120) }}"
|
||||
data-signals:_cafe-id="''"
|
||||
data-signals:_cafe-name="''"
|
||||
data-signals:_cafe-city="''"
|
||||
|
|
@ -349,6 +350,20 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Brew Time -->
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Brew Time</h4>
|
||||
<div class="flex flex-col gap-1 text-sm" style="max-width: 16rem">
|
||||
<span class="text-text">Time (M:SS)</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="btn-adjust" data-on:click="$_brewTime = Math.max(5, $_brewTime - 5)">-</button>
|
||||
<input type="hidden" name="brew_time" data-attr:value="$_brewTime" />
|
||||
<input type="text" readonly class="input-field flex-1 text-center"
|
||||
data-attr:value="Math.floor($_brewTime / 60) + ':' + String($_brewTime % 60).padStart(2, '0')" />
|
||||
<button type="button" class="btn-adjust" data-on:click="$_brewTime = $_brewTime + 5">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Quick Notes -->
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-text-muted uppercase tracking-wide mb-3">Quick Notes</h4>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
||||
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
||||
<input type="hidden" name="quick_notes" value="{{ brew.quick_notes_raw }}" />
|
||||
{% if let Some(bt) = brew.brew_time_raw %}
|
||||
<input type="hidden" name="brew_time" value="{{ bt }}" />
|
||||
{% endif %}
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
|
|
@ -32,7 +35,7 @@
|
|||
<p class="mt-0.5 text-sm text-text-muted truncate">{{ brew.roaster_name }}</p>
|
||||
</div>
|
||||
<div class="mt-2 flex-1 space-y-1 text-text-secondary" style="font-size: 0.8125rem">
|
||||
<p>{{ brew.coffee_weight }} · {{ brew.water_volume }}</p>
|
||||
<p>{{ brew.coffee_weight }} · {{ brew.water_volume }}{% if let Some(bt) = brew.brew_time %} · {{ bt }}{% endif %}</p>
|
||||
<p>{{ brew.grinder_model }} · {{ brew.grind_setting }}</p>
|
||||
<p>{{ brew.brewer_name }}</p>
|
||||
{% if let Some(fp) = brew.filter_paper_name %}<p>{{ fp }}</p>{% endif %}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
<input type="hidden" name="water_volume" value="{{ brew.water_volume_raw }}" />
|
||||
<input type="hidden" name="water_temp" value="{{ brew.water_temp_raw }}" />
|
||||
<input type="hidden" name="quick_notes" value="{{ brew.quick_notes_raw }}" />
|
||||
{% if let Some(bt) = brew.brew_time_raw %}
|
||||
<input type="hidden" name="brew_time" value="{{ bt }}" />
|
||||
{% endif %}
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">
|
||||
{{ icons::plus_circle("h-4 w-4") }} Brew Again
|
||||
|
|
@ -83,9 +86,11 @@
|
|||
<td data-label="Grinder" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.grinder_name }}</td>
|
||||
<td data-label="Recipe" class="px-4 py-3 whitespace-nowrap">
|
||||
<div>{{ brew.coffee_weight }} · {{ brew.water_volume }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{{ brew.ratio }} · {{ brew.water_temp }}</div>
|
||||
<div class="hidden md:block text-xs text-text-muted">{% if let Some(bt) = brew.brew_time %}{{ bt }} · {% endif %}{{ brew.water_temp }}</div>
|
||||
</td>
|
||||
<td data-label="Ratio" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.ratio }}</td>
|
||||
{% if let Some(bt) = brew.brew_time %}
|
||||
<td data-label="Brew Time" class="px-4 py-3 whitespace-nowrap md:hidden">{{ bt }}</td>
|
||||
{% endif %}
|
||||
<td data-label="Temp" class="px-4 py-3 whitespace-nowrap md:hidden">{{ brew.water_temp }}</td>
|
||||
<td data-label="Brewer" class="px-4 py-3 whitespace-nowrap">
|
||||
<div>{{ brew.brewer_name }}</div>
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ async fn populate_test_data(db: &TestDb) -> (Roaster, Roast, Bag, Gear, Gear, Ge
|
|||
water_volume: 250,
|
||||
water_temp: 93.5,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ async fn creating_a_brew_returns_201_for_valid_data() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ async fn creating_a_brew_with_filter_paper_returns_201() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -114,6 +116,7 @@ async fn creating_a_brew_deducts_from_bag_remaining() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -158,6 +161,7 @@ async fn creating_a_brew_fails_if_insufficient_coffee_in_bag() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -226,6 +230,7 @@ async fn listing_brews_returns_200_and_enriched_data() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -281,6 +286,7 @@ async fn listing_brews_without_filter_paper_returns_none() {
|
|||
water_volume: 255,
|
||||
water_temp: 88.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -331,6 +337,7 @@ async fn listing_brews_with_bag_filter_returns_filtered_results() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -353,6 +360,7 @@ async fn listing_brews_with_bag_filter_returns_filtered_results() {
|
|||
water_volume: 255,
|
||||
water_temp: 88.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -400,6 +408,7 @@ async fn getting_a_brew_returns_200_for_valid_id() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
@ -451,6 +460,7 @@ async fn deleting_a_brew_returns_204() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -634,6 +634,7 @@ async fn creating_a_brew_surfaces_on_the_timeline() {
|
|||
water_volume: 250,
|
||||
water_temp: 92.0,
|
||||
quick_notes: Vec::new(),
|
||||
brew_time: None,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue