···7575 t.Error("HTML should contain Regenerate button")
7676 }
77777878+ // Check that "Finalize" button exists
7979+ if !strings.Contains(html, `Finalize`) {
8080+ t.Error("HTML should contain Finalize button")
8181+ }
8282+8383+ // Check for finalize submit button (not a POST form anymore)
8484+ if !strings.Contains(html, `name="finalize"`) {
8585+ t.Error("HTML should have finalize submit button")
8686+ }
8787+ if !strings.Contains(html, `value="true"`) {
8888+ t.Error("HTML should have finalize value set to true")
8989+ }
9090+7891 // Check that recipes are present with their titles
7992 if !strings.Contains(html, "Recipe One") {
8093 t.Error("HTML should contain Recipe One title")
-1
internal/recipes/generator.go
···101101 instructions += " Enjoyed and saved :"
102102 }*/
103103 for _, saved := range p.Saved {
104104- saved.Saved = true
105104 // This ended up giving me a "Preference update + replacements requested" recipe
106105 // instructions += saved.Title + "; " //is this enough or do we keep the exact one?
107106 shoppingList.Recipes = append(shoppingList.Recipes, saved)
+2
internal/recipes/params.go
···127127 savedHashes := lo.FilterMap(r.URL.Query()["saved"], clean)
128128 dismissedHashes := lo.FilterMap(r.URL.Query()["dismissed"], clean)
129129 // Load saved recipes from cache by their hashes
130130+ // TODO is it overkill to pull full recip in param instead of just hash?
130131 for _, hash := range savedHashes {
131132 recipe, err := s.SingleFromCache(ctx, hash)
132133 if err != nil {
133134 slog.ErrorContext(ctx, "failed to load saved recipe by hash", "hash", hash, "error", err)
134135 continue
135136 }
137137+ recipe.Saved = true
136138 slog.InfoContext(ctx, "adding saved recipe to params", "title", recipe.Title, "hash", hash)
137139 p.Saved = append(p.Saved, *recipe)
138140 }
+53-32
internal/recipes/server.go
···113113 }
114114115115 if hashParam := r.URL.Query().Get("h"); hashParam != "" {
116116+ //TODO check if generating and spin.
116117 slist, err := s.FromCache(ctx, hashParam)
117118 if err != nil {
118119 slog.ErrorContext(ctx, "failed to load recipe list for hash", "hash", hashParam, "error", err)
···127128 Name: "Unknown Location",
128129 }, time.Now())
129130 }
131131+ if r.URL.Query().Get("mail") == "true" {
132132+ FormatMail(p, *slist, w)
133133+ return
134134+ }
130135 FormatChatHTML(p, *slist, w)
131131- // backfill
132132- go func() {
133133- cutoff := lo.Must(time.Parse(time.DateOnly, "2025-12-22"))
134134- if p.Date.After(cutoff) {
135135- return
136136- }
137137- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
138138- defer cancel()
139139- // nothing we can do on failure anyways. Aleaady logged
140140- _ = s.SaveRecipes(ctx, slist.Recipes, p.Hash())
141141- }()
142136 return
143137 }
144138···155149 return
156150 }
157151158158- for _, last := range currentUser.LastRecipes {
159159- if last.CreatedAt.Before(time.Now().AddDate(0, 0, -14)) {
160160- break
152152+ // Handle finalize - save recipes to user profile and display filtered list
153153+ if r.URL.Query().Get("finalize") == "true" {
154154+ // Check if user is authenticated
155155+ if currentUser.ID == "" {
156156+ http.Error(w, "must be logged in to finalize recipes", http.StatusUnauthorized)
157157+ return
158158+ }
159159+160160+ // If no recipes are saved, just return to home
161161+ if len(p.Saved) == 0 {
162162+ http.Error(w, "no recipes selected to save", http.StatusBadRequest)
163163+ return
164164+ }
165165+166166+ // Save recipes to user profile
167167+ if err := s.saveRecipesToUserProfile(ctx, currentUser.ID, p.Saved); err != nil {
168168+ slog.ErrorContext(ctx, "failed to save recipes to user profile", "user_id", currentUser.ID, "error", err)
169169+ http.Error(w, "failed to save recipes", http.StatusInternalServerError)
170170+ return
171171+ }
172172+ slog.InfoContext(ctx, "finalized recipes", "user_id", currentUser.ID, "count", len(p.Saved))
173173+174174+ // Display the saved recipes
175175+ shoppingList := &ai.ShoppingList{
176176+ Recipes: p.Saved,
177177+ ConversationID: p.ConversationID,
178178+ }
179179+180180+ // should finlize go into params to get a different hash that previous one with unsaved?
181181+ // or should we shove a guid or iteration in params along with conversation id. Response id?
182182+ if err := s.SaveShoppingList(ctx, shoppingList, p); err != nil {
183183+ slog.ErrorContext(ctx, "save error", "error", err)
161184 }
162162- p.LastRecipes = append(p.LastRecipes, last.Title)
185185+ http.Redirect(w, r, "/recipes?h="+p.Hash(), http.StatusSeeOther)
186186+ return
163187 }
164188165189 hash := p.Hash()
166166- if list, err := s.FromCache(ctx, hash); err == nil {
190190+ if _, err := s.FromCache(ctx, hash); err == nil {
167191 // TODO check not found error explicitly
168168- if r.URL.Query().Get("mail") == "true" {
169169- FormatMail(p, *list, w)
170170- return
192192+ http.Redirect(w, r, "/recipes?h="+p.Hash(), http.StatusSeeOther)
193193+ return
194194+ }
195195+196196+ for _, last := range currentUser.LastRecipes {
197197+ if last.CreatedAt.Before(time.Now().AddDate(0, 0, -14)) {
198198+ break
171199 }
172172- FormatChatHTML(p, *list, w)
173173- // backfill
174174- go func() {
175175- cutoff := lo.Must(time.Parse(time.DateOnly, "2025-12-22"))
176176- if p.Date.After(cutoff) {
177177- return
178178- }
179179- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
180180- defer cancel()
181181- // nothing we can do on failure anyways. Aleaady logged
182182- _ = s.SaveRecipes(ctx, list.Recipes, p.Hash())
183183- }()
184184- return
200200+ p.LastRecipes = append(p.LastRecipes, last.Title)
185201 }
186202187203 s.wg.Add(1)
···213229 }
214230 }()
215231 // TODO should we just redirect to cache page here?
232232+ // need to save params first and do spin in hash loop above.
233233+ s.Spin(w, r)
234234+}
235235+func (s *server) Spin(w http.ResponseWriter, r *http.Request) {
216236 w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
237237+ ctx := r.Context()
217238 spinnerData := struct {
218239 ClarityScript template.HTML
219240 Style seasons.Style