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:
@@ -45,20 +45,20 @@ func NewHandler(repo DiaryRepository, dishRepo DishRepository, recipeRepo Recipe
|
||||
func (h *Handler) GetByDate(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
|
||||
}
|
||||
|
||||
date := r.URL.Query().Get("date")
|
||||
if date == "" {
|
||||
writeError(w, http.StatusBadRequest, "date query parameter required (YYYY-MM-DD)")
|
||||
writeError(w, r, http.StatusBadRequest, "date query parameter required (YYYY-MM-DD)")
|
||||
return
|
||||
}
|
||||
|
||||
entries, listError := h.repo.ListByDate(r.Context(), userID, date)
|
||||
if listError != nil {
|
||||
slog.Error("list diary by date", "err", listError)
|
||||
writeError(w, http.StatusInternalServerError, "failed to load diary")
|
||||
slog.ErrorContext(r.Context(), "list diary by date", "err", listError)
|
||||
writeError(w, r, http.StatusInternalServerError, "failed to load diary")
|
||||
return
|
||||
}
|
||||
if entries == nil {
|
||||
@@ -71,21 +71,21 @@ func (h *Handler) GetByDate(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) Create(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
|
||||
}
|
||||
|
||||
var req CreateRequest
|
||||
if decodeError := json.NewDecoder(r.Body).Decode(&req); decodeError != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
writeError(w, r, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if req.Date == "" || req.MealType == "" {
|
||||
writeError(w, http.StatusBadRequest, "date and meal_type are required")
|
||||
writeError(w, r, http.StatusBadRequest, "date and meal_type are required")
|
||||
return
|
||||
}
|
||||
if req.DishID == nil && req.ProductID == nil && req.Name == "" {
|
||||
writeError(w, http.StatusBadRequest, "dish_id, product_id, or name is required")
|
||||
writeError(w, r, http.StatusBadRequest, "dish_id, product_id, or name is required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,8 +94,8 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
if req.DishID == nil {
|
||||
dishID, _, resolveError := h.dishRepo.FindOrCreate(r.Context(), req.Name)
|
||||
if resolveError != nil {
|
||||
slog.Error("resolve dish for diary entry", "name", req.Name, "err", resolveError)
|
||||
writeError(w, http.StatusInternalServerError, "failed to resolve dish")
|
||||
slog.ErrorContext(r.Context(), "resolve dish for diary entry", "name", req.Name, "err", resolveError)
|
||||
writeError(w, r, http.StatusInternalServerError, "failed to resolve dish")
|
||||
return
|
||||
}
|
||||
req.DishID = &dishID
|
||||
@@ -104,8 +104,8 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
if req.RecipeID == nil {
|
||||
recipeID, _, recipeError := h.recipeRepo.FindOrCreateRecipe(r.Context(), *req.DishID, 0, 0, 0, 0)
|
||||
if recipeError != nil {
|
||||
slog.Error("find or create recipe for diary entry", "dish_id", *req.DishID, "err", recipeError)
|
||||
writeError(w, http.StatusInternalServerError, "failed to resolve recipe")
|
||||
slog.ErrorContext(r.Context(), "find or create recipe for diary entry", "dish_id", *req.DishID, "err", recipeError)
|
||||
writeError(w, r, http.StatusInternalServerError, "failed to resolve recipe")
|
||||
return
|
||||
}
|
||||
req.RecipeID = &recipeID
|
||||
@@ -114,8 +114,8 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
entry, createError := h.repo.Create(r.Context(), userID, req)
|
||||
if createError != nil {
|
||||
slog.Error("create diary entry", "err", createError)
|
||||
writeError(w, http.StatusInternalServerError, "failed to create diary entry")
|
||||
slog.ErrorContext(r.Context(), "create diary entry", "err", createError)
|
||||
writeError(w, r, http.StatusInternalServerError, "failed to create diary entry")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, entry)
|
||||
@@ -125,7 +125,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) GetRecent(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
|
||||
}
|
||||
limit := 10
|
||||
@@ -136,8 +136,8 @@ func (h *Handler) GetRecent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
items, queryError := h.repo.GetRecent(r.Context(), userID, limit)
|
||||
if queryError != nil {
|
||||
slog.Error("get recent diary items", "err", queryError)
|
||||
writeError(w, http.StatusInternalServerError, "failed to get recent items")
|
||||
slog.ErrorContext(r.Context(), "get recent diary items", "err", queryError)
|
||||
writeError(w, r, http.StatusInternalServerError, "failed to get recent items")
|
||||
return
|
||||
}
|
||||
if items == nil {
|
||||
@@ -150,31 +150,35 @@ func (h *Handler) GetRecent(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) Delete(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
|
||||
}
|
||||
|
||||
id := chi.URLParam(r, "id")
|
||||
if deleteError := h.repo.Delete(r.Context(), id, userID); deleteError != nil {
|
||||
if deleteError == ErrNotFound {
|
||||
writeError(w, http.StatusNotFound, "diary entry not found")
|
||||
writeError(w, r, http.StatusNotFound, "diary entry not found")
|
||||
return
|
||||
}
|
||||
slog.Error("delete diary entry", "err", deleteError)
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete diary entry")
|
||||
slog.ErrorContext(r.Context(), "delete diary entry", "err", deleteError)
|
||||
writeError(w, r, http.StatusInternalServerError, "failed to delete diary entry")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type errorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Error string `json:"error"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
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(errorResponse{Error: msg})
|
||||
_ = json.NewEncoder(w).Encode(errorResponse{
|
||||
Error: msg,
|
||||
RequestID: middleware.RequestIDFromCtx(r.Context()),
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
|
||||
Reference in New Issue
Block a user