···202202}
203203204204// Interpolate fills in computed/derived fields from existing data.
205205+// - BrewerType from BrewerObj if not set
205206// - WaterAmount from sum of pours if not set
206207// - Ratio from water/coffee amounts
207208func (r *Recipe) Interpolate() {
209209+ // Derive brewer type from joined brewer object if missing
210210+ if r.BrewerType == "" && r.BrewerObj != nil && r.BrewerObj.BrewerType != "" {
211211+ r.BrewerType = r.BrewerObj.BrewerType
212212+ }
208213 // Derive water amount from pours if missing
209214 if r.WaterAmount == 0 && len(r.Pours) > 0 {
210215 var total int
+28
internal/models/recipe_filter_test.go
···190190 assert.Equal(t, 0.0, recipe.Ratio) // can't compute ratio without coffee
191191}
192192193193+func TestRecipeInterpolate_BrewerTypeFromObj(t *testing.T) {
194194+ // When BrewerType is empty but BrewerObj has a type, Interpolate should populate it
195195+ recipe := &Recipe{
196196+ Name: "V60 Recipe",
197197+ BrewerObj: &Brewer{Name: "V60", BrewerType: BrewerTypePourover},
198198+ }
199199+ recipe.Interpolate()
200200+ assert.Equal(t, BrewerTypePourover, recipe.BrewerType)
201201+}
202202+203203+func TestRecipeInterpolate_BrewerTypeAlreadySet(t *testing.T) {
204204+ // When BrewerType is already set, Interpolate should not overwrite it
205205+ recipe := &Recipe{
206206+ Name: "Espresso Recipe",
207207+ BrewerType: BrewerTypeEspresso,
208208+ BrewerObj: &Brewer{Name: "Gaggia", BrewerType: BrewerTypeOther},
209209+ }
210210+ recipe.Interpolate()
211211+ assert.Equal(t, BrewerTypeEspresso, recipe.BrewerType)
212212+}
213213+214214+func TestRecipeInterpolate_NoBrewerObj(t *testing.T) {
215215+ // When no BrewerObj, BrewerType should remain empty
216216+ recipe := &Recipe{Name: "Basic Recipe"}
217217+ recipe.Interpolate()
218218+ assert.Equal(t, "", recipe.BrewerType)
219219+}
220220+193221func TestMatchesFilter_InterpolatesWaterFromPours(t *testing.T) {
194222 // Recipe with no water_amount but pours that sum to 250g
195223 recipe := &Recipe{
+11
static/js/brew-form.js
···322322 if (recipe.brewer_rkey) {
323323 this.onBrewerChange(recipe.brewer_rkey);
324324 }
325325+ // Fallback: use recipe's brewer_type directly if category wasn't
326326+ // resolved from the dropdown cache (e.g. cross-user recipe with no
327327+ // local brewer match, or stale cache).
328328+ if (!this.brewerCategory) {
329329+ const recipeBrewerType =
330330+ recipe.brewer_type || recipe.brewer_obj?.brewer_type || "";
331331+ if (recipeBrewerType) {
332332+ this.brewerCategory =
333333+ this.normalizeBrewerCategory(recipeBrewerType);
334334+ }
335335+ }
325336326337 // Always reset pours, then apply recipe pours if present
327338 this.pours =