refactor(cafes,cups): remove notes field from Cafe and Cup entities

- Add migrations 0014 and 0015 to drop notes columns
- Remove notes from domain structs, repositories, views, CLI, routes
- Remove notes textarea/columns from web templates
- Update bootstrap script, tests, backup module, and README
This commit is contained in:
Jon Seager 2026-02-03 21:09:34 +00:00
parent 23817ff95a
commit ae3c1090de
No known key found for this signature in database
11 changed files with 29 additions and 80 deletions

View file

@ -124,7 +124,7 @@ brewlog bag <cmd> Manage bags of coffee
brewlog gear <cmd> Manage brewing gear (grinders, brewers, filter papers)
brewlog brew <cmd> Manage brews (add, list, get, delete — no update)
brewlog cafe <cmd> Manage cafes
brewlog cup <cmd> Manage cups (tasting notes with ratings)
brewlog cup <cmd> Manage cups (cafe visits with ratings)
brewlog token <cmd> Manage API tokens (create, list, revoke)
brewlog backup Export all data to JSON on stdout
brewlog restore --file F Restore data from a JSON backup into an empty database

View file

@ -0,0 +1 @@
ALTER TABLE cups DROP COLUMN notes;

View file

@ -515,129 +515,128 @@ fi
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Red Brick Espresso") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')" \
--notes "Rich and balanced; the house espresso blend is a crowd-pleaser." \
--rating 4
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Kamwangi") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')" \
--notes "Juicy Kenyan pour-over, bright blackcurrant notes." \
--rating 5
# Prufrock Coffee (London) - 3 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Kochere") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
--notes "Delicate floral Ethiopian; exceptional barista technique." \
--rating 5
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Laja") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
--notes "Interesting Mexican natural, lots of chocolate." \
--rating 4
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Ben Saïd Natural") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
--notes "Guest roast from Tim Wendelboe; bergamot hit was incredible." \
--rating 5
# Hola Coffee (Madrid) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Cerro Azul") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')" \
--notes "Clean Colombian washed, plum and grapefruit forward." \
--rating 4
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Guji Highland") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')" \
--notes "Peachy natural from April Coffee, worked well as espresso." \
--rating 3
# Toma Café (Madrid) - 1 cup
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Daterra Sweet Collection") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Toma Café") | .id')" \
--notes "Smooth Brazilian, hazelnut and chocolate. Nice afternoon flat white."
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Toma Café") | .id')"
# The Barn (Berlin) - 3 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Gatomboya") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
--notes "Their own roast; classic Kenyan brightness with tomato acidity." \
--rating 4
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Los Pirineos") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
--notes "Honey-processed El Salvador, maple sweetness." \
--rating 4
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Halo Beriti") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
--notes "La Cabra guest roast, jasmine and lemon." \
--rating 5
# Companion Coffee (Berlin) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="El Paraiso 92 Anaerobic") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')" \
--notes "Wild anaerobic from Dak; passionfruit explosion." \
--rating 5
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Oreti SL28") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')" \
--notes "Crisp Kenyan SL28, grapefruit and plum." \
--rating 4
# Man Versus Machine (Munich) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Finca Tamana Washed") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')" \
--notes "Clean Colombian, red apple and caramel. Beautiful V60." \
--rating 4
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Esmeralda Geisha") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')" \
--notes "Incredible Geisha from Friedhats; jasmine and papaya. Worth every penny." \
--rating 5
# Vits der Kaffee (Munich) - 1 cup
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="San Fermin") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Vits der Kaffee") | .id')" \
--notes "Origin Coffee guest roast; fruity Colombian, blood orange finish." \
--rating 3
# Full Court Press (Bristol) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Suke Quto") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')" \
--notes "Natural Ethiopian from Stow Coffee; blackberry and jasmine." \
--rating 5
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Death to Decaf") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')" \
--notes "Surprisingly good decaf from Dark Arts. Cocoa and cherry." \
--rating 3
# Small Street Espresso (Bristol) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Simbi") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')" \
--notes "Rwandan washed from Bows; clean black tea and orange." \
--rating 4
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Chumeca") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')" \
--notes "Costa Rican honey from Bows; sweet mandarin and honeycomb." \
--rating 4
# ============================================================================

View file

@ -142,7 +142,7 @@ pub(crate) async fn update_cup(
Path(id): Path<CupId>,
Json(payload): Json<UpdateCup>,
) -> Result<Json<Cup>, ApiError> {
let has_changes = payload.notes.is_some() || payload.rating.is_some();
let has_changes = payload.rating.is_some();
if !has_changes {
return Err(AppError::validation("no changes provided").into());

View file

@ -9,7 +9,6 @@ pub struct Cup {
pub id: CupId,
pub roast_id: RoastId,
pub cafe_id: CafeId,
pub notes: Option<String>,
pub rating: Option<i32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@ -31,13 +30,11 @@ pub struct CupWithDetails {
pub struct NewCup {
pub roast_id: RoastId,
pub cafe_id: CafeId,
pub notes: Option<String>,
pub rating: Option<i32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateCup {
pub notes: Option<String>,
pub rating: Option<i32>,
}

View file

@ -13,7 +13,7 @@ use crate::infrastructure::database::DatabasePool;
const BASE_SELECT: &str = r"
SELECT
c.id, c.roast_id, c.cafe_id, c.notes, c.rating,
c.id, c.roast_id, c.cafe_id, c.rating,
c.created_at, c.updated_at,
r.name as roast_name, r.slug as roast_slug,
rr.name as roaster_name, rr.slug as roaster_slug,
@ -57,7 +57,6 @@ impl SqlCupRepository {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
notes: record.notes,
rating: record.rating,
created_at: record.created_at,
updated_at: record.updated_at,
@ -70,7 +69,6 @@ impl SqlCupRepository {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
notes: record.notes,
rating: record.rating,
created_at: record.created_at,
updated_at: record.updated_at,
@ -141,12 +139,11 @@ impl CupRepository for SqlCupRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
let record = query_as::<_, CupRecord>(
"INSERT INTO cups (roast_id, cafe_id, notes, rating) VALUES (?, ?, ?, ?) \
RETURNING id, roast_id, cafe_id, notes, rating, created_at, updated_at",
"INSERT INTO cups (roast_id, cafe_id, rating) VALUES (?, ?, ?) \
RETURNING id, roast_id, cafe_id, rating, created_at, updated_at",
)
.bind(new_cup.roast_id.into_inner())
.bind(new_cup.cafe_id.into_inner())
.bind(new_cup.notes.as_deref())
.bind(new_cup.rating)
.fetch_one(&mut *tx)
.await
@ -196,7 +193,7 @@ impl CupRepository for SqlCupRepository {
async fn get(&self, id: CupId) -> Result<Cup, RepositoryError> {
let record = query_as::<_, CupRecord>(
"SELECT id, roast_id, cafe_id, notes, rating, created_at, updated_at FROM cups WHERE id = ?",
"SELECT id, roast_id, cafe_id, rating, created_at, updated_at FROM cups WHERE id = ?",
)
.bind(i64::from(id))
.fetch_optional(&self.pool)
@ -268,7 +265,6 @@ impl CupRepository for SqlCupRepository {
let mut builder = QueryBuilder::new("UPDATE cups SET updated_at = CURRENT_TIMESTAMP");
let mut sep = true;
push_update_field!(builder, sep, "notes", changes.notes);
push_update_field!(builder, sep, "rating", changes.rating);
let _ = sep;
@ -308,7 +304,6 @@ struct CupRecord {
id: i64,
roast_id: i64,
cafe_id: i64,
notes: Option<String>,
rating: Option<i32>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
@ -319,7 +314,6 @@ struct CupWithDetailsRecord {
id: i64,
roast_id: i64,
cafe_id: i64,
notes: Option<String>,
rating: Option<i32>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,

View file

@ -38,8 +38,6 @@ pub struct AddCupCommand {
#[arg(long)]
pub cafe_id: i64,
#[arg(long)]
pub notes: Option<String>,
#[arg(long)]
pub rating: Option<i32>,
}
@ -47,7 +45,6 @@ pub async fn add_cup(client: &BrewlogClient, command: AddCupCommand) -> Result<(
let payload = NewCup {
roast_id: RoastId::new(command.roast_id),
cafe_id: CafeId::new(command.cafe_id),
notes: command.notes,
rating: command.rating,
};
@ -67,14 +64,11 @@ pub struct UpdateCupCommand {
#[arg(long)]
pub id: i64,
#[arg(long)]
pub notes: Option<String>,
#[arg(long)]
pub rating: Option<i32>,
}
pub async fn update_cup(client: &BrewlogClient, command: UpdateCupCommand) -> Result<()> {
let payload = UpdateCup {
notes: command.notes,
rating: command.rating,
};

View file

@ -9,8 +9,6 @@ pub struct CupView {
pub roaster_slug: String,
pub cafe_name: String,
pub cafe_slug: String,
pub notes: String,
pub has_notes: bool,
pub rating: String,
pub has_rating: bool,
pub created_at: String,
@ -18,8 +16,6 @@ pub struct CupView {
impl CupView {
pub fn from_domain(cup: CupWithDetails) -> Self {
let notes = cup.cup.notes.clone().unwrap_or_default();
let has_notes = !notes.is_empty();
let rating = cup
.cup
.rating
@ -34,8 +30,6 @@ impl CupView {
roaster_slug: cup.roaster_slug,
cafe_name: cup.cafe_name,
cafe_slug: cup.cafe_slug,
notes,
has_notes,
rating,
has_rating,
created_at: cup.cup.created_at.format("%Y-%m-%d %H:%M").to_string(),

View file

@ -70,15 +70,6 @@
<option value="1">1 - Poor</option>
</select>
</label>
<label class="sm:col-span-2 flex flex-col gap-1 text-sm">
<span class="text-stone-700">Notes</span>
<textarea
name="notes"
rows="2"
class="input-field"
placeholder="Tasting impressions, preparation style..."
></textarea>
</label>
</div>
<div class="flex items-center justify-end gap-2">
<button

View file

@ -22,7 +22,6 @@
{% call table::sortable_header("Coffee", "roast", navigator, "#cup-list") %}
{% call table::sortable_header("Cafe", "cafe", navigator, "#cup-list") %}
{% call table::sortable_header("Rating", "rating", navigator, "#cup-list") %}
<th scope="col" class="mobile-hidden px-4 py-3">Notes</th>
<th scope="col" class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
@ -52,9 +51,6 @@
>
</td>
<td data-label="Rating" class="px-4 py-3 whitespace-nowrap">{{ cup.rating }}</td>
<td data-label="Notes" class="mobile-hidden px-4 py-3 text-sm text-stone-600">
{% if cup.has_notes %}{{ cup.notes }}{% else %}&mdash;{% endif %}
</td>
<td data-label="" class="px-4 py-3 text-right">
{% if is_authenticated %}
<div class="inline-flex items-center gap-1">

View file

@ -16,7 +16,6 @@ async fn creating_a_cup_returns_a_201_for_valid_data() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
notes: Some("Excellent pour-over".to_string()),
rating: Some(5),
};
@ -33,7 +32,6 @@ async fn creating_a_cup_returns_a_201_for_valid_data() {
let cup: Cup = response.json().await.expect("Failed to parse response");
assert_eq!(cup.roast_id, roast.id);
assert_eq!(cup.cafe_id, cafe.id);
assert_eq!(cup.notes, Some("Excellent pour-over".to_string()));
assert_eq!(cup.rating, Some(5));
}
@ -49,7 +47,6 @@ async fn creating_a_cup_without_optional_fields_returns_a_201() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
notes: None,
rating: None,
};
@ -64,7 +61,6 @@ async fn creating_a_cup_without_optional_fields_returns_a_201() {
assert_eq!(response.status(), 201);
let cup: Cup = response.json().await.expect("Failed to parse response");
assert_eq!(cup.notes, None);
assert_eq!(cup.rating, None);
}
@ -76,7 +72,6 @@ async fn creating_a_cup_requires_authentication() {
let new_cup = NewCup {
roast_id: RoastId::new(1),
cafe_id: CafeId::new(1),
notes: None,
rating: None,
};
@ -119,7 +114,6 @@ async fn listing_cups_returns_a_200_with_enriched_data() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
notes: None,
rating: Some(4),
};
@ -158,7 +152,6 @@ async fn getting_a_cup_returns_a_200_with_details() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
notes: Some("Great".to_string()),
rating: Some(3),
};
@ -215,7 +208,6 @@ async fn updating_a_cup_returns_a_200_for_valid_data() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
notes: None,
rating: None,
};
@ -232,10 +224,7 @@ async fn updating_a_cup_returns_a_200_for_valid_data() {
.await
.expect("Failed to parse response");
let update = UpdateCup {
notes: Some("Updated notes".to_string()),
rating: Some(4),
};
let update = UpdateCup { rating: Some(4) };
let response = client
.put(app.api_url(&format!("/cups/{}", cup.id)))
@ -248,7 +237,6 @@ async fn updating_a_cup_returns_a_200_for_valid_data() {
assert_eq!(response.status(), 200);
let updated: Cup = response.json().await.expect("Failed to parse response");
assert_eq!(updated.notes, Some("Updated notes".to_string()));
assert_eq!(updated.rating, Some(4));
}
@ -264,7 +252,6 @@ async fn updating_a_cup_with_no_changes_returns_a_400() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
notes: None,
rating: None,
};
@ -281,10 +268,7 @@ async fn updating_a_cup_with_no_changes_returns_a_400() {
.await
.expect("Failed to parse response");
let update = UpdateCup {
notes: None,
rating: None,
};
let update = UpdateCup { rating: None };
let response = client
.put(app.api_url(&format!("/cups/{}", cup.id)))
@ -309,7 +293,6 @@ async fn deleting_a_cup_returns_a_204_for_valid_id() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
notes: None,
rating: None,
};