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

@@ -35,8 +35,8 @@ func (handler *Handler) List(responseWriter http.ResponseWriter, request *http.R
userID := middleware.UserIDFromCtx(request.Context())
userProducts, listError := handler.repo.List(request.Context(), userID)
if listError != nil {
slog.Error("list user products", "user_id", userID, "err", listError)
writeErrorJSON(responseWriter, http.StatusInternalServerError, "failed to list user products")
slog.ErrorContext(request.Context(), "list user products", "user_id", userID, "err", listError)
writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to list user products")
return
}
if userProducts == nil {
@@ -50,18 +50,18 @@ func (handler *Handler) Create(responseWriter http.ResponseWriter, request *http
userID := middleware.UserIDFromCtx(request.Context())
var req CreateRequest
if decodeError := json.NewDecoder(request.Body).Decode(&req); decodeError != nil {
writeErrorJSON(responseWriter, http.StatusBadRequest, "invalid request body")
writeErrorJSON(responseWriter, request, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" {
writeErrorJSON(responseWriter, http.StatusBadRequest, "name is required")
writeErrorJSON(responseWriter, request, http.StatusBadRequest, "name is required")
return
}
userProduct, createError := handler.repo.Create(request.Context(), userID, req)
if createError != nil {
slog.Error("create user product", "user_id", userID, "err", createError)
writeErrorJSON(responseWriter, http.StatusInternalServerError, "failed to create user product")
slog.ErrorContext(request.Context(), "create user product", "user_id", userID, "err", createError)
writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to create user product")
return
}
writeJSON(responseWriter, http.StatusCreated, userProduct)
@@ -72,7 +72,7 @@ func (handler *Handler) BatchCreate(responseWriter http.ResponseWriter, request
userID := middleware.UserIDFromCtx(request.Context())
var items []CreateRequest
if decodeError := json.NewDecoder(request.Body).Decode(&items); decodeError != nil {
writeErrorJSON(responseWriter, http.StatusBadRequest, "invalid request body")
writeErrorJSON(responseWriter, request, http.StatusBadRequest, "invalid request body")
return
}
if len(items) == 0 {
@@ -82,8 +82,8 @@ func (handler *Handler) BatchCreate(responseWriter http.ResponseWriter, request
userProducts, batchError := handler.repo.BatchCreate(request.Context(), userID, items)
if batchError != nil {
slog.Error("batch create user products", "user_id", userID, "err", batchError)
writeErrorJSON(responseWriter, http.StatusInternalServerError, "failed to create user products")
slog.ErrorContext(request.Context(), "batch create user products", "user_id", userID, "err", batchError)
writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to create user products")
return
}
writeJSON(responseWriter, http.StatusCreated, userProducts)
@@ -96,18 +96,18 @@ func (handler *Handler) Update(responseWriter http.ResponseWriter, request *http
var req UpdateRequest
if decodeError := json.NewDecoder(request.Body).Decode(&req); decodeError != nil {
writeErrorJSON(responseWriter, http.StatusBadRequest, "invalid request body")
writeErrorJSON(responseWriter, request, http.StatusBadRequest, "invalid request body")
return
}
userProduct, updateError := handler.repo.Update(request.Context(), id, userID, req)
if errors.Is(updateError, ErrNotFound) {
writeErrorJSON(responseWriter, http.StatusNotFound, "user product not found")
writeErrorJSON(responseWriter, request, http.StatusNotFound, "user product not found")
return
}
if updateError != nil {
slog.Error("update user product", "id", id, "err", updateError)
writeErrorJSON(responseWriter, http.StatusInternalServerError, "failed to update user product")
slog.ErrorContext(request.Context(), "update user product", "id", id, "err", updateError)
writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to update user product")
return
}
writeJSON(responseWriter, http.StatusOK, userProduct)
@@ -120,24 +120,28 @@ func (handler *Handler) Delete(responseWriter http.ResponseWriter, request *http
if deleteError := handler.repo.Delete(request.Context(), id, userID); deleteError != nil {
if errors.Is(deleteError, ErrNotFound) {
writeErrorJSON(responseWriter, http.StatusNotFound, "user product not found")
writeErrorJSON(responseWriter, request, http.StatusNotFound, "user product not found")
return
}
slog.Error("delete user product", "id", id, "err", deleteError)
writeErrorJSON(responseWriter, http.StatusInternalServerError, "failed to delete user product")
slog.ErrorContext(request.Context(), "delete user product", "id", id, "err", deleteError)
writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to delete user product")
return
}
responseWriter.WriteHeader(http.StatusNoContent)
}
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) {