Open the Quest log sheet from the Quest log pill
CI / ci (push) Successful in 2m57s
CI / ci (pull_request) Successful in 2m39s

Tapping "Quest log · N" opens a bottom sheet listing the Quest log, each
dinner with its 48 px washed photo, name, difficulty and minutes. A row
opens /dinner/:id; its remove button asks to confirm, then archives the
dinner (DELETE /api/v1/dinners/{id}) and takes it off the count. An empty
Quest log says "Your Quest log is empty — swipe right on a side quest to
add one." The sheet replaces the pill's interim name list.

The Quest log is the dinners accepted and not yet scheduled, so GET
/api/v1/dinners?status=quest_log now leaves out a dinner on the plan for
the household's current week (by its timezone) or a later week, read
from week_plans and plan_days. The pill's count and the sheet both come
from that one read.

Task: 5ad0b188-176e-4107-91dd-5d667ca6a4d2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 04:12:05 -04:00
co-authored by Claude Opus 5.5
parent a866dfad06
commit f4075d15b8
14 changed files with 524 additions and 92 deletions
+35 -2
View File
@@ -16,10 +16,12 @@ import (
"slices"
"strings"
"sync"
"time"
"unicode/utf8"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/harness"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/jobs"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
@@ -273,12 +275,20 @@ func (s *Service) createRegular(ctx context.Context, r NewRegular, updatedBy str
}
// List returns the dinners with status, which must be regular or
// quest_log, ordered by normalized name.
// quest_log, ordered by normalized name. The Quest log is the dinners
// accepted and not yet scheduled, so a quest_log dinner on the plan for the
// household's current week or a later one is left out of it.
func (s *Service) List(ctx context.Context, status string) ([]Dinner, error) {
if !slices.Contains(listable, status) {
return nil, invalid("status", "must be one of "+strings.Join(listable, ", "))
}
rows, err := db.New(s.st.Reader).ListDinnersByStatus(ctx, status)
var rows []db.Dinner
var err error
if status == StatusQuestLog {
rows, err = s.listQuestLog(ctx)
} else {
rows, err = db.New(s.st.Reader).ListDinnersByStatus(ctx, status)
}
if err != nil {
return nil, fmt.Errorf("dinners: list %s: %w", status, err)
}
@@ -289,6 +299,29 @@ func (s *Service) List(ctx context.Context, status string) ([]Dinner, error) {
return out, nil
}
// listQuestLog reads the Quest log in one read transaction, so the
// household's current week and the plans come from the same snapshot.
func (s *Service) listQuestLog(ctx context.Context) (rows []db.Dinner, err error) {
tx, err := s.st.Reader.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("begin read: %w", err)
}
defer func() {
err = errors.Join(err, tx.Rollback())
}()
q := db.New(tx)
settings, err := q.GetHouseholdSettings(ctx)
if err != nil {
return nil, fmt.Errorf("read household settings: %w", err)
}
loc, err := household.LoadZone(settings.Timezone)
if err != nil {
return nil, fmt.Errorf("household settings: %w", err)
}
thisWeek := household.WeekStart(household.Today(time.Now(), loc))
return q.ListUnplannedQuestLogDinners(ctx, thisWeek.String())
}
// Update applies p to dinner id, recording updatedBy. An invalid patch is a
// *ValidationError and a new name repeating another unarchived dinner a
// *DuplicateError; either changes nothing. Renaming a dinner still pending