ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #150 from paulgmiller/pmiller/regenerate

Regenerate after 10 minute timeout.

authored by

Paul Miller and committed by
GitHub
eee2f2c2 9823f58c

+113 -34
+1
cmd/careme/middleware.go
··· 17 17 if r.URL.Path == "/ready" { 18 18 return 19 19 } 20 + // TODO log status code. 20 21 slog.Info("request", "method", r.Method, "url", r.URL.Path, "query", r.URL.Query(), "form", r.Form, "duration", time.Since(start)) 21 22 } 22 23
+79 -32
internal/recipes/server.go
··· 16 16 "html/template" 17 17 "log/slog" 18 18 "net/http" 19 + "net/url" 19 20 "sync" 20 21 "time" 21 22 ··· 98 99 FormatChatHTML(p, list, w) 99 100 } 100 101 102 + const ( 103 + queryArgHash = "h" 104 + queryArgStart = "start" 105 + ) 106 + 107 + func (s *server) notFound(ctx context.Context, w http.ResponseWriter, r *http.Request) { 108 + startArg := r.URL.Query().Get(queryArgStart) 109 + hashParam := r.URL.Query().Get(queryArgHash) 110 + if startTime, err := time.Parse(time.RFC3339Nano, startArg); err == nil { 111 + if time.Since(startTime) > time.Minute*10 { 112 + p, err := loadParamsFromHash(ctx, hashParam, s.cache) 113 + if err != nil { 114 + slog.ErrorContext(ctx, "failed to load params for hash", "hash", hashParam, "error", err) 115 + http.Error(w, "recipe not found or expired", http.StatusNotFound) 116 + return 117 + } 118 + currentUser, err := users.FromRequest(r, s.storage) 119 + if err != nil { 120 + if errors.Is(err, users.ErrNotFound) { 121 + users.ClearCookie(w) 122 + http.Redirect(w, r, "/", http.StatusSeeOther) 123 + return 124 + } 125 + slog.ErrorContext(ctx, "failed to load user for recipes", "error", err) 126 + http.Error(w, "unable to load account", http.StatusInternalServerError) 127 + return 128 + } 129 + s.kickgeneration(ctx, p, currentUser) 130 + redirectToHash(w, r, p.Hash(), true /*useStart*/) 131 + return 132 + } 133 + } 134 + s.Spin(w, r) 135 + return 136 + } 137 + 101 138 func (s *server) handleRecipes(w http.ResponseWriter, r *http.Request) { 102 139 ctx := r.Context() 103 - 104 - if hashParam := r.URL.Query().Get("h"); hashParam != "" { 105 - 140 + if hashParam := r.URL.Query().Get(queryArgHash); hashParam != "" { 106 141 slist, err := s.FromCache(ctx, hashParam) // ideally should memory cache this so lots of reloads don't constantly go out to azure 107 142 if err != nil { 108 143 if errors.Is(err, cache.ErrNotFound) { 109 - //how do we time this out and go try and regenerate 110 - //should we put start time in params or a seperate blob 111 - s.Spin(w, r) 144 + s.notFound(ctx, w, r) 112 145 return 113 146 } 114 147 slog.ErrorContext(ctx, "failed to load recipe list for hash", "hash", hashParam, "error", err) 115 148 http.Error(w, "recipe not found or expired", http.StatusNotFound) 116 149 return 117 150 } 151 + if r.URL.Query().Has(queryArgStart) { 152 + redirectToHash(w, r, hashParam, false /*useStart*/) 153 + return 154 + } 155 + 156 + 118 157 p, err := loadParamsFromHash(ctx, hashParam, s.cache) 119 158 if err != nil { 120 159 slog.ErrorContext(ctx, "failed to load params for hash", "hash", hashParam, "error", err) 121 - p = DefaultParams(&locations.Location{ 122 - ID: "", 123 - Name: "Unknown Location", 124 - }, time.Now()) 160 + http.Error(w, "failed to load recipe parameters", http.StatusInternalServerError) 161 + return 125 162 } 126 163 if r.URL.Query().Get("mail") == "true" { 127 164 FormatMail(p, *slist, w) ··· 140 177 // p.UserID = currentUser.ID 141 178 142 179 //if params are already saved redirect and assume someone kicks off genration 180 + 181 + if err := s.SaveParams(ctx, p); err != nil { 182 + if errors.Is(err, AlreadyExists) { 183 + slog.InfoContext(ctx, "params already existed redirecting", "hash", p.Hash()) 184 + redirectToHash(w, r, p.Hash(), false /*useStart*/) 185 + return 186 + } 187 + slog.ErrorContext(ctx, "failed to save params", "error", err) 188 + http.Error(w, "internal server error", http.StatusInternalServerError) 189 + return 190 + } 191 + 192 + hash := p.Hash() 143 193 144 194 currentUser, err := users.FromRequest(r, s.storage) 145 195 if err != nil { ··· 150 200 } 151 201 slog.ErrorContext(ctx, "failed to load user for recipes", "error", err) 152 202 http.Error(w, "unable to load account", http.StatusInternalServerError) 153 - return 154 - } 155 - if currentUser == nil { 156 - currentUser = &users.User{LastRecipes: []users.Recipe{}} 157 - } 158 - 159 - if err := s.SaveParams(ctx, p); err != nil { 160 - if errors.Is(err, AlreadyExists) { 161 - slog.InfoContext(ctx, "params already existed redirecting", "hash", p.Hash()) 162 - redirectToHash(w, r, p.Hash()) 163 - return 164 - } 165 - slog.ErrorContext(ctx, "failed to save params", "error", err) 166 - http.Error(w, "internal server error", http.StatusInternalServerError) 167 203 return 168 204 } 169 205 170 - //After this failures lead to recipe orphaning. 171 - 172 - hash := p.Hash() 173 - 174 206 // Handle finalize - save recipes to user profile and display filtered list 175 207 if r.URL.Query().Get("finalize") == "true" { 176 208 // Check if user is authenticated ··· 206 238 http.Error(w, "failed to save finalized recipes", http.StatusInternalServerError) 207 239 return 208 240 } 209 - redirectToHash(w, r, hash) 241 + redirectToHash(w, r, hash, false /*useStart*/) 210 242 return 211 243 } 212 244 245 + s.kickgeneration(ctx, p, currentUser) 246 + 247 + redirectToHash(w, r, hash, true /*useStart*/) 248 + } 249 + 250 + func (s *server) kickgeneration(ctx context.Context, p *generatorParams, currentUser *users.User) { 213 251 for _, last := range currentUser.LastRecipes { 214 252 if last.CreatedAt.Before(time.Now().AddDate(0, 0, -14)) { 215 253 break 216 254 } 217 255 p.LastRecipes = append(p.LastRecipes, last.Title) 218 256 } 257 + 258 + hash := p.Hash() 219 259 220 260 s.wg.Add(1) 221 261 go func() { ··· 244 284 } 245 285 } 246 286 }() 247 - redirectToHash(w, r, hash) 248 287 } 288 + 249 289 func (s *server) Spin(w http.ResponseWriter, r *http.Request) { 250 290 w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate") 251 291 ctx := r.Context() ··· 265 305 } 266 306 } 267 307 268 - func redirectToHash(w http.ResponseWriter, r *http.Request, hash string) { 269 - http.Redirect(w, r, "/recipes?h="+hash, http.StatusSeeOther) 308 + func redirectToHash(w http.ResponseWriter, r *http.Request, hash string, useStart bool) { 309 + u := url.URL{Path: "/recipes"} 310 + args := url.Values{} 311 + args.Set(queryArgHash, hash) 312 + if useStart { 313 + args.Set(queryArgStart, time.Now().Format(time.RFC3339Nano)) 314 + } 315 + u.RawQuery = args.Encode() 316 + http.Redirect(w, r, u.String(), http.StatusSeeOther) 270 317 } 271 318 272 319 func (s *server) Wait() {
+31
internal/recipes/server_test.go
··· 1 + package recipes 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "net/http/httptest" 7 + "strings" 8 + "testing" 9 + ) 10 + 11 + func TestRedirectToHash(t *testing.T) { 12 + // Create a ResponseRecorder to record the response 13 + rr := httptest.NewRecorder() 14 + // Create a dummy request 15 + req := httptest.NewRequest("GET", "/dummy", nil) 16 + 17 + hash := "testhash" 18 + redirectToHash(rr, req, hash, true) 19 + 20 + // Check the status code 21 + if status := rr.Code; status != http.StatusSeeOther { 22 + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusSeeOther) 23 + } 24 + 25 + // Check the Location header 26 + expectedLocation := fmt.Sprintf("/recipes?h=%s&start=", hash) 27 + location := rr.Header().Get("Location") 28 + if !strings.HasPrefix(location, expectedLocation) { 29 + t.Errorf("handler returned wrong location: got %v want prefix %v", location, expectedLocation) 30 + } 31 + }
+2 -2
internal/users/http.go
··· 37 37 cookie, err := r.Cookie(CookieName) 38 38 if err != nil { 39 39 if errors.Is(err, http.ErrNoCookie) { 40 - return nil, nil 40 + return nil, ErrNotFound 41 41 } 42 42 return nil, err 43 43 } 44 44 if cookie.Value == "" { 45 - return nil, nil 45 + return nil, ErrNotFound 46 46 } 47 47 user, err := store.GetByID(cookie.Value) 48 48 if err != nil {