fix: fix menu generation errors and show planned meals on home screen
Backend fixes: - migration 003: add 'menu' value to recipe_source enum (was causing SQLSTATE 22P02) - migration 004: rename recipe_products→recipe_ingredients, product_id→ingredient_id (was causing SQLSTATE 42P01) - dish/repository.go: fix INSERT INTO tags using $1/$1 for two columns → $1/$2 (was causing SQLSTATE 42P08) - home/handler.go: replace non-existent saved_recipes table with correct joins (recipes→dishes→dish_translations, user_saved_recipes) so today's plan and recommendations load correctly - reqlog: new slog.Handler wrapper that adds request_id and stack trace to ERROR-level logs - all handlers: slog.Error→slog.ErrorContext so error logs include request context; writeError includes request_id in response body Client: - home_screen.dart: extend home screen to future dates, show planned meals as ghost entries - l10n: add new localisation keys for home screen date navigation and planned meal UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/food-ai/backend/internal/infra/middleware"
|
||||
"github.com/food-ai/backend/internal/infra/locale"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,7 @@ func NewHandler(pool *pgxpool.Pool) *Handler {
|
||||
func (h *Handler) GetSummary(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.UserIDFromCtx(r.Context())
|
||||
if userID == "" {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
writeError(w, r, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ func (h *Handler) getDailyGoal(ctx context.Context, userID string) int {
|
||||
userID,
|
||||
).Scan(&goal)
|
||||
if err != nil {
|
||||
slog.Warn("home: get daily goal", "user_id", userID, "err", err)
|
||||
slog.WarnContext(ctx, "home: get daily goal", "user_id", userID, "err", err)
|
||||
return 2000
|
||||
}
|
||||
return goal
|
||||
@@ -99,21 +100,24 @@ func (h *Handler) getLoggedCalories(ctx context.Context, userID, date string) fl
|
||||
// getTodayPlan returns the three meal slots planned for today.
|
||||
// If no menu exists, all three slots are returned with nil recipe fields.
|
||||
func (h *Handler) getTodayPlan(ctx context.Context, userID, weekStart string, dow int) []MealPlan {
|
||||
lang := locale.FromContext(ctx)
|
||||
const q = `
|
||||
SELECT mi.meal_type,
|
||||
sr.title,
|
||||
sr.image_url,
|
||||
(sr.nutrition->>'calories')::float
|
||||
COALESCE(dt.name, d.name) AS title,
|
||||
d.image_url,
|
||||
rec.calories_per_serving
|
||||
FROM menu_plans mp
|
||||
JOIN menu_items mi ON mi.menu_plan_id = mp.id
|
||||
LEFT JOIN saved_recipes sr ON sr.id = mi.recipe_id
|
||||
LEFT JOIN recipes rec ON rec.id = mi.recipe_id
|
||||
LEFT JOIN dishes d ON d.id = rec.dish_id
|
||||
LEFT JOIN dish_translations dt ON dt.dish_id = d.id AND dt.lang = $4
|
||||
WHERE mp.user_id = $1
|
||||
AND mp.week_start::text = $2
|
||||
AND mi.day_of_week = $3`
|
||||
|
||||
rows, err := h.pool.Query(ctx, q, userID, weekStart, dow)
|
||||
rows, err := h.pool.Query(ctx, q, userID, weekStart, dow, lang)
|
||||
if err != nil {
|
||||
slog.Warn("home: get today plan", "err", err)
|
||||
slog.WarnContext(ctx, "home: get today plan", "err", err)
|
||||
return defaultPlan()
|
||||
}
|
||||
defer rows.Close()
|
||||
@@ -175,7 +179,7 @@ func (h *Handler) getExpiringSoon(ctx context.Context, userID string) []Expiring
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
slog.Warn("home: get expiring soon", "err", err)
|
||||
slog.WarnContext(ctx, "home: get expiring soon", "err", err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
@@ -197,19 +201,25 @@ func (h *Handler) getExpiringSoon(ctx context.Context, userID string) []Expiring
|
||||
return result
|
||||
}
|
||||
|
||||
// getRecommendations returns the 3 most recently generated recipe recommendations.
|
||||
// getRecommendations returns the 3 most recently saved recommendation recipes.
|
||||
func (h *Handler) getRecommendations(ctx context.Context, userID string) []Recommendation {
|
||||
lang := locale.FromContext(ctx)
|
||||
rows, err := h.pool.Query(ctx, `
|
||||
SELECT id, title, COALESCE(image_url, ''),
|
||||
(nutrition->>'calories')::float
|
||||
FROM saved_recipes
|
||||
WHERE user_id = $1 AND source = 'recommendation'
|
||||
ORDER BY saved_at DESC
|
||||
SELECT rec.id,
|
||||
COALESCE(dt.name, d.name),
|
||||
COALESCE(d.image_url, ''),
|
||||
rec.calories_per_serving
|
||||
FROM user_saved_recipes usr
|
||||
JOIN recipes rec ON rec.id = usr.recipe_id
|
||||
JOIN dishes d ON d.id = rec.dish_id
|
||||
LEFT JOIN dish_translations dt ON dt.dish_id = d.id AND dt.lang = $2
|
||||
WHERE usr.user_id = $1 AND rec.source = 'recommendation'
|
||||
ORDER BY usr.saved_at DESC
|
||||
LIMIT 3`,
|
||||
userID,
|
||||
userID, lang,
|
||||
)
|
||||
if err != nil {
|
||||
slog.Warn("home: get recommendations", "err", err)
|
||||
slog.WarnContext(ctx, "home: get recommendations", "err", err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
@@ -238,10 +248,10 @@ func mondayOfISOWeek(year, week int) time.Time {
|
||||
return monday1.AddDate(0, 0, (week-1)*7)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
func writeError(w http.ResponseWriter, r *http.Request, status int, msg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"error": msg, "request_id": middleware.RequestIDFromCtx(r.Context())})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
|
||||
Reference in New Issue
Block a user