recipes.Grounder ties search, fetch/extract, Rank, the photo cache and
normalize_recipe together. Ground(ctx, dishName, query) reuses a stored
source_url among the top candidates without fetching, otherwise tries the
ranked recipes best first and stores the photo, recipe, ingredients and
steps in one transaction, returning Grounded{recipe_id, photo_id, method,
minutes_total, difficulty, source_url, site_name} or a typed
*ErrUngroundable listing every reason. GroundURL grounds a pasted URL the
same way; GroundMany grounds items 3 at a time with per-item results.
PhotoCache is split into DownloadPhoto (fetch, decode, size check, no
writes) and CachePhoto(ctx, tx, photo), which records the row in the
caller's transaction; RemovePhotoFiles cleans up after a rollback.
Migration 0007 adds photo_id, method, difficulty and minutes_total to
recipes, and api_key_last4 to recipe_search_settings. SearXNG is now
called at recipe_search_settings.base_url.
GET/PATCH /api/v1/settings/recipe-search (Brave key sealed with
secretbox, only api_key_set and api_key_last4 returned) and POST
/api/v1/settings/recipe-search/test (searches "chicken tikka masala",
returns the count, 409 not_configured or 502 search_failed).
Maestro task 53afd59c-6c35-4af7-904c-6c167d1ad69d.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
71 lines
2.6 KiB
Go
71 lines
2.6 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes"
|
|
)
|
|
|
|
// searchTestResponse is the POST /settings/recipe-search/test body.
|
|
type searchTestResponse struct {
|
|
Query string `json:"query"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
func getRecipeSearchSettings(logger *slog.Logger, svc *recipes.SearchSettingsService) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
settings, err := svc.Get(r.Context())
|
|
if err != nil {
|
|
logger.ErrorContext(r.Context(), "get recipe search settings", "error", err)
|
|
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, settings)
|
|
}
|
|
}
|
|
|
|
// patchRecipeSearchSettings takes the Brave API key in api_key; the
|
|
// response, like GET's, never holds it.
|
|
func patchRecipeSearchSettings(logger *slog.Logger, svc *recipes.SearchSettingsService) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
patch, err := decodePatch[recipes.SearchSettingsPatch](http.MaxBytesReader(w, r.Body, maxSettingsBody))
|
|
if err != nil {
|
|
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", err.Error())
|
|
return
|
|
}
|
|
settings, err := svc.Update(r.Context(), patch)
|
|
var invalid *recipes.SearchSettingsError
|
|
if errors.As(err, &invalid) {
|
|
writeError(w, r, logger, http.StatusBadRequest, "invalid_settings", invalid.Error())
|
|
return
|
|
}
|
|
if err != nil {
|
|
logger.ErrorContext(r.Context(), "update recipe search settings", "error", err)
|
|
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, settings)
|
|
}
|
|
}
|
|
|
|
// postRecipeSearchTest searches for recipes.SearchTestQuery with the saved
|
|
// settings. Recipe search not configured is 409; a search that fails is
|
|
// 502 with the provider's error, which never holds the API key.
|
|
func postRecipeSearchTest(logger *slog.Logger, svc *recipes.SearchSettingsService) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
count, err := svc.TestSearch(r.Context())
|
|
if errors.Is(err, recipes.ErrSearchNotConfigured) {
|
|
writeError(w, r, logger, http.StatusConflict, "not_configured", "recipe search is not configured")
|
|
return
|
|
}
|
|
if err != nil {
|
|
logger.WarnContext(r.Context(), "recipe search test failed", "error", err)
|
|
writeError(w, r, logger, http.StatusBadGateway, "search_failed", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, searchTestResponse{Query: recipes.SearchTestQuery, Count: count})
|
|
}
|
|
}
|