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:
dbastrikin
2026-03-22 00:35:11 +02:00
parent 9306d59d36
commit 5096df2102
49 changed files with 824 additions and 299 deletions

View File

@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/go-chi/chi/v5"
"github.com/food-ai/backend/internal/infra/middleware"
)
// ProductSearcher is the data layer interface used by Handler for search.
@@ -51,7 +52,7 @@ func (handler *Handler) Search(responseWriter http.ResponseWriter, request *http
products, searchError := handler.repo.Search(request.Context(), query, limit)
if searchError != nil {
slog.Error("search catalog products", "q", query, "err", searchError)
slog.ErrorContext(request.Context(), "search catalog products", "q", query, "err", searchError)
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.WriteHeader(http.StatusInternalServerError)
_, _ = responseWriter.Write([]byte(`{"error":"search failed"}`))
@@ -71,15 +72,15 @@ func (handler *Handler) Search(responseWriter http.ResponseWriter, request *http
func (handler *Handler) GetByBarcode(responseWriter http.ResponseWriter, request *http.Request) {
barcode := chi.URLParam(request, "barcode")
if barcode == "" {
writeErrorJSON(responseWriter, http.StatusBadRequest, "barcode is required")
writeErrorJSON(responseWriter, request, http.StatusBadRequest, "barcode is required")
return
}
// Check the local catalog first.
catalogProduct, lookupError := handler.repo.GetByBarcode(request.Context(), barcode)
if lookupError != nil {
slog.Error("lookup product by barcode", "barcode", barcode, "err", lookupError)
writeErrorJSON(responseWriter, http.StatusInternalServerError, "lookup failed")
slog.ErrorContext(request.Context(), "lookup product by barcode", "barcode", barcode, "err", lookupError)
writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "lookup failed")
return
}
if catalogProduct != nil {
@@ -90,15 +91,15 @@ func (handler *Handler) GetByBarcode(responseWriter http.ResponseWriter, request
// Not in catalog — fetch from Open Food Facts.
fetchedProduct, fetchError := handler.openFoodFacts.Fetch(request.Context(), barcode)
if fetchError != nil {
slog.Warn("open food facts fetch failed", "barcode", barcode, "err", fetchError)
writeErrorJSON(responseWriter, http.StatusNotFound, "product not found")
slog.WarnContext(request.Context(), "open food facts fetch failed", "barcode", barcode, "err", fetchError)
writeErrorJSON(responseWriter, request, http.StatusNotFound, "product not found")
return
}
// Persist the fetched product so subsequent lookups are served from the DB.
savedProduct, upsertError := handler.repo.UpsertByBarcode(request.Context(), fetchedProduct)
if upsertError != nil {
slog.Warn("upsert product from open food facts", "barcode", barcode, "err", upsertError)
slog.WarnContext(request.Context(), "upsert product from open food facts", "barcode", barcode, "err", upsertError)
// Return the fetched data even if we could not cache it.
writeJSON(responseWriter, http.StatusOK, fetchedProduct)
return
@@ -107,13 +108,17 @@ func (handler *Handler) GetByBarcode(responseWriter http.ResponseWriter, request
}
type errorResponse struct {
Error string `json:"error"`
Error string `json:"error"`
RequestID string `json:"request_id,omitempty"`
}
func writeErrorJSON(responseWriter http.ResponseWriter, status int, msg string) {
func writeErrorJSON(responseWriter http.ResponseWriter, request *http.Request, status int, msg string) {
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.WriteHeader(status)
_ = json.NewEncoder(responseWriter).Encode(errorResponse{Error: msg})
_ = json.NewEncoder(responseWriter).Encode(errorResponse{
Error: msg,
RequestID: middleware.RequestIDFromCtx(request.Context()),
})
}
func writeJSON(responseWriter http.ResponseWriter, status int, value any) {