Fixes the Feature 13 review findings on the Discover deck (Maestro task 4fc551f0-3c11-4c15-b638-f2c08a28eb29): - F13-01: a batch failure (start, poll or job) is shown with its Retry whatever the deck has, so a replenish that fails while cards remain is no longer silent until the deck runs out. - F13-02: GET /api/v1/suggestions/deck reports the batch pending or running (active_batch, read in the deck's snapshot), and a decision that calls for a replenish returns the job of the batch it started or joined. The page follows a newly reported active batch, so a reopened page or a second session shows skeleton cards while the batch runs instead of "All caught up". - F13-03: ← / → leave the deck alone while it is inert behind a modal (the Quest log sheet or its confirmation). - F13-04: a one- or two-card deck keeps all three depths: skeleton cards stand in for the missing ones while a batch finds more Side quests, blank stack-edge cards while none is coming. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.8 KiB
Go
79 lines
2.8 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/suggest"
|
|
)
|
|
|
|
// postSuggestionBatch starts a manual suggestion batch and answers 202 with
|
|
// its batch_id and the job_id that GET /jobs/{id} reports on, without
|
|
// waiting for the job. When a batch is already pending or running it
|
|
// answers 202 with that batch instead of starting another.
|
|
func postSuggestionBatch(logger *slog.Logger, svc *suggest.Service) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
b, err := svc.StartBatch(r.Context(), suggest.TriggerManual)
|
|
if err != nil {
|
|
writeSuggestionError(w, r, logger, "start suggestion batch", err)
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusAccepted, b)
|
|
}
|
|
}
|
|
|
|
// getSuggestionDeck answers GET /suggestions/deck with the pending
|
|
// suggestions, oldest first, and the batch pending or running, or null.
|
|
func getSuggestionDeck(logger *slog.Logger, svc *suggest.Service) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
d, err := svc.Deck(r.Context())
|
|
if err != nil {
|
|
writeSuggestionError(w, r, logger, "list suggestion deck", err)
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, d)
|
|
}
|
|
}
|
|
|
|
// postSuggestionDecision records Try it (accept) or Pass (decline) on a
|
|
// suggestion, recording the session's actor as its dinner's updated_by.
|
|
func postSuggestionDecision(logger *slog.Logger, svc *suggest.Service, accept bool) http.HandlerFunc {
|
|
decide, action := svc.Decline, "decline suggestion"
|
|
if accept {
|
|
decide, action = svc.Accept, "accept suggestion"
|
|
}
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := actorOrFail(w, r, logger)
|
|
if !ok {
|
|
return
|
|
}
|
|
d, err := decide(r.Context(), chi.URLParam(r, "id"), actor.Subject)
|
|
if err != nil {
|
|
writeSuggestionError(w, r, logger, action, err)
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, d)
|
|
}
|
|
}
|
|
|
|
// writeSuggestionError maps a suggest.Service error to its API error: an
|
|
// unknown suggestion is 404, and a decision on one already decided the
|
|
// other way, or whose dinner left the deck, is 409.
|
|
func writeSuggestionError(w http.ResponseWriter, r *http.Request, logger *slog.Logger, action string, err error) {
|
|
var decided *suggest.DecidedError
|
|
switch {
|
|
case errors.As(err, &decided):
|
|
writeError(w, r, logger, http.StatusConflict, "suggestion_decided", decided.Error())
|
|
case errors.Is(err, suggest.ErrDinnerNotSuggested):
|
|
writeError(w, r, logger, http.StatusConflict, "dinner_not_suggested", err.Error())
|
|
case errors.Is(err, suggest.ErrNotFound):
|
|
writeError(w, r, logger, http.StatusNotFound, "not_found", "suggestion not found")
|
|
default:
|
|
logger.ErrorContext(r.Context(), action, "error", err)
|
|
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
|
}
|
|
}
|