ai cooking
0
fork

Configure Feed

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

alright let google track us when we put an id in (#278)

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
80b19d08 9f74ef12

+150 -72
+1 -1
AGENTS.md
··· 42 42 - Keep commits scoped and reviewable; avoid mixing refactors with feature changes unless necessary. 43 43 44 44 ## Security & Configuration Notes 45 - - Required env vars: `KROGER_CLIENT_ID`, `KROGER_CLIENT_SECRET`, `AI_API_KEY`; optional `CLARITY_PROJECT_ID`, `HISTORY_PATH`. Azure logging uses `AZURE_STORAGE_ACCOUNT_NAME` and `AZURE_STORAGE_PRIMARY_ACCOUNT_KEY`. 45 + - Required env vars: `KROGER_CLIENT_ID`, `KROGER_CLIENT_SECRET`, `AI_API_KEY`; optional `CLARITY_PROJECT_ID`, `GOOGLE_TAG_ID`, `HISTORY_PATH`. Azure logging uses `AZURE_STORAGE_ACCOUNT_NAME` and `AZURE_STORAGE_PRIMARY_ACCOUNT_KEY`. 46 46 - Never commit secrets or generated recipe outputs. If testing against real APIs, use minimal scopes and rotate keys promptly. 47 47 - Any handler that lets you see data from multiple users should go behind the /admin mux to secure it.
+1
README.md
··· 14 14 - `AI_API_KEY` - OpenAI or Anthropic API key (required) 15 15 ### Optional 16 16 - `CLARITY_PROJECT_ID` - Microsoft Clarity project ID for web analytics (optional) 17 + - `GOOGLE_TAG_ID` - Google Ads/gtag ID for web analytics (optional) 17 18 - `SENDGRID_API_KEY` - To allow sending weekly recipe lists via email 18 19 19 20 if you're
+8 -4
cmd/careme/web.go
··· 81 81 mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) { 82 82 ctx := r.Context() 83 83 data := struct { 84 - ClarityScript template.HTML 85 - Style seasons.Style 84 + ClarityScript template.HTML 85 + GoogleTagScript template.HTML 86 + Style seasons.Style 86 87 }{ 87 - ClarityScript: templates.ClarityScript(), 88 - Style: seasons.GetCurrentStyle(), 88 + ClarityScript: templates.ClarityScript(), 89 + GoogleTagScript: templates.GoogleTagScript(), 90 + Style: seasons.GetCurrentStyle(), 89 91 } 90 92 if err := templates.About.Execute(w, data); err != nil { 91 93 slog.ErrorContext(ctx, "about template execute error", "error", err) ··· 120 122 } 121 123 data := struct { 122 124 ClarityScript template.HTML 125 + GoogleTagScript template.HTML 123 126 User *utypes.User 124 127 FavoriteStoreName string 125 128 Style seasons.Style 126 129 ServerSignedIn bool 127 130 }{ 128 131 ClarityScript: templates.ClarityScript(), 132 + GoogleTagScript: templates.GoogleTagScript(), 129 133 User: currentUser, 130 134 FavoriteStoreName: favoriteStoreName, 131 135 Style: seasons.GetCurrentStyle(),
+2 -3
deploy/deploy.yaml
··· 32 32 env: 33 33 - name: CLARITY_PROJECT_ID 34 34 value: "td2gxd3sq9" 35 + - name: GOOGLE_TAG_ID 36 + value: "AW-17902827663" 35 37 - name: ADMIN_EMAILS 36 38 value: "paul.miller@gmail.com" 37 39 volumeMounts: ··· 93 95 envFrom: 94 96 - secretRef: 95 97 name: careme-secrets3 96 - env: 97 - - name: CLARITY_PROJECT_ID 98 - value: "td2gxd3sq9" 99 98 resources: 100 99 requests: 101 100 cpu: 50m
+14 -12
internal/locations/locations.go
··· 318 318 } 319 319 320 320 data := struct { 321 - Locations []Location 322 - Zip string 323 - FavoriteStore string 324 - ClarityScript template.HTML 325 - Style seasons.Style 326 - ServerSignedIn bool 321 + Locations []Location 322 + Zip string 323 + FavoriteStore string 324 + ClarityScript template.HTML 325 + GoogleTagScript template.HTML 326 + Style seasons.Style 327 + ServerSignedIn bool 327 328 }{ 328 - Locations: locs, 329 - Zip: zip, 330 - FavoriteStore: favoriteStore, 331 - ClarityScript: templates.ClarityScript(), 332 - Style: seasons.GetCurrentStyle(), 333 - ServerSignedIn: serverSignedIn, 329 + Locations: locs, 330 + Zip: zip, 331 + FavoriteStore: favoriteStore, 332 + ClarityScript: templates.ClarityScript(), 333 + GoogleTagScript: templates.GoogleTagScript(), 334 + Style: seasons.GetCurrentStyle(), 335 + ServerSignedIn: serverSignedIn, 334 336 } 335 337 return templates.Location.Execute(w, data) 336 338 }
+12 -10
internal/locations/mock.go
··· 46 46 func (m mock) Register(mux *http.ServeMux, _ auth.AuthClient) { 47 47 mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { 48 48 data := struct { 49 - Locations []Location 50 - Zip string 51 - FavoriteStore string 52 - ClarityScript template.HTML 53 - Style seasons.Style 49 + Locations []Location 50 + Zip string 51 + FavoriteStore string 52 + ClarityScript template.HTML 53 + GoogleTagScript template.HTML 54 + Style seasons.Style 54 55 }{ 55 - Locations: lo.Values(fakes), 56 - Zip: r.URL.Query().Get("zip"), 57 - FavoriteStore: "", 58 - ClarityScript: templates.ClarityScript(), 59 - Style: seasons.GetCurrentStyle(), 56 + Locations: lo.Values(fakes), 57 + Zip: r.URL.Query().Get("zip"), 58 + FavoriteStore: "", 59 + ClarityScript: templates.ClarityScript(), 60 + GoogleTagScript: templates.GoogleTagScript(), 61 + Style: seasons.GetCurrentStyle(), 60 62 } 61 63 if err := templates.Location.Execute(w, data); err != nil { 62 64 http.Error(w, "template error", http.StatusInternalServerError)
+46 -42
internal/recipes/html.go
··· 16 16 func FormatShoppingListHTML(p *generatorParams, l ai.ShoppingList, signedIn bool, writer http.ResponseWriter) { 17 17 // TODO just put params into shopping list and pass that up? 18 18 data := struct { 19 - Location locations.Location 20 - Date string 21 - ClarityScript template.HTML 22 - Instructions string 23 - Hash string 24 - Recipes []ai.Recipe 25 - ShoppingList []ai.Ingredient 26 - ConversationID string 27 - Style seasons.Style 28 - ServerSignedIn bool 19 + Location locations.Location 20 + Date string 21 + ClarityScript template.HTML 22 + GoogleTagScript template.HTML 23 + Instructions string 24 + Hash string 25 + Recipes []ai.Recipe 26 + ShoppingList []ai.Ingredient 27 + ConversationID string 28 + Style seasons.Style 29 + ServerSignedIn bool 29 30 }{ 30 - Location: *p.Location, 31 - Date: p.Date.Format("2006-01-02"), 32 - ClarityScript: templates.ClarityScript(), 33 - Instructions: p.Instructions, 34 - Hash: p.Hash(), 35 - Recipes: l.Recipes, 36 - ShoppingList: shoppingListForDisplay(l.Recipes), 37 - ConversationID: l.ConversationID, 38 - Style: seasons.GetCurrentStyle(), 39 - ServerSignedIn: signedIn, 31 + Location: *p.Location, 32 + Date: p.Date.Format("2006-01-02"), 33 + ClarityScript: templates.ClarityScript(), 34 + GoogleTagScript: templates.GoogleTagScript(), 35 + Instructions: p.Instructions, 36 + Hash: p.Hash(), 37 + Recipes: l.Recipes, 38 + ShoppingList: shoppingListForDisplay(l.Recipes), 39 + ConversationID: l.ConversationID, 40 + Style: seasons.GetCurrentStyle(), 41 + ServerSignedIn: signedIn, 40 42 } 41 43 42 44 if err := templates.ShoppingList.Execute(writer, data); err != nil { ··· 50 52 return j.CreatedAt.Compare(i.CreatedAt) 51 53 }) 52 54 data := struct { 53 - Location locations.Location 54 - Date string 55 - ClarityScript template.HTML 56 - Recipe ai.Recipe 57 - OriginHash string 58 - ConversationID string 59 - Thread []RecipeThreadEntry 60 - Feedback RecipeFeedback 61 - RecipeHash string 62 - Style seasons.Style 63 - ServerSignedIn bool 55 + Location locations.Location 56 + Date string 57 + ClarityScript template.HTML 58 + GoogleTagScript template.HTML 59 + Recipe ai.Recipe 60 + OriginHash string 61 + ConversationID string 62 + Thread []RecipeThreadEntry 63 + Feedback RecipeFeedback 64 + RecipeHash string 65 + Style seasons.Style 66 + ServerSignedIn bool 64 67 }{ 65 - Location: *p.Location, 66 - Date: p.Date.Format("2006-01-02"), 67 - ClarityScript: templates.ClarityScript(), 68 - Recipe: recipe, 69 - OriginHash: recipe.OriginHash, 70 - ConversationID: p.ConversationID, 71 - Thread: thread, 72 - Feedback: feedback, 73 - RecipeHash: recipe.ComputeHash(), 74 - Style: seasons.GetCurrentStyle(), 75 - ServerSignedIn: signedIn, 68 + Location: *p.Location, 69 + Date: p.Date.Format("2006-01-02"), 70 + ClarityScript: templates.ClarityScript(), 71 + GoogleTagScript: templates.GoogleTagScript(), 72 + Recipe: recipe, 73 + OriginHash: recipe.OriginHash, 74 + ConversationID: p.ConversationID, 75 + Thread: thread, 76 + Feedback: feedback, 77 + RecipeHash: recipe.ComputeHash(), 78 + Style: seasons.GetCurrentStyle(), 79 + ServerSignedIn: signedIn, 76 80 } 77 81 78 82 if err := templates.Recipe.Execute(writer, data); err != nil {
+35
internal/recipes/html_test.go
··· 104 104 } 105 105 } 106 106 107 + func TestFormatShoppingListHTML_IncludesGoogleTagScript(t *testing.T) { 108 + loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"} 109 + p := DefaultParams(&loc, time.Now()) 110 + 111 + prev := templates.GoogleTagID 112 + t.Cleanup(func() { 113 + templates.GoogleTagID = prev 114 + }) 115 + templates.GoogleTagID = "AW-1234567890" 116 + w := httptest.NewRecorder() 117 + FormatShoppingListHTML(p, list, true, w) 118 + if !bytes.Contains(w.Body.Bytes(), []byte("www.googletagmanager.com/gtag/js?id=AW-1234567890")) { 119 + t.Error("HTML should contain Google tag script URL") 120 + } 121 + 122 + if !bytes.Contains(w.Body.Bytes(), []byte("gtag('config', 'AW-1234567890');")) { 123 + t.Error("HTML should contain Google tag ID") 124 + } 125 + } 126 + 127 + func TestFormatShoppingListHTML_NoGoogleTagWhenEmpty(t *testing.T) { 128 + loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"} 129 + p := DefaultParams(&loc, time.Now()) 130 + prev := templates.GoogleTagID 131 + t.Cleanup(func() { 132 + templates.GoogleTagID = prev 133 + }) 134 + templates.GoogleTagID = "" 135 + w := httptest.NewRecorder() 136 + FormatShoppingListHTML(p, list, true, w) 137 + if bytes.Contains(w.Body.Bytes(), []byte("googletagmanager.com")) { 138 + t.Error("HTML should not contain Google tag script when tag ID is empty") 139 + } 140 + } 141 + 107 142 func TestFormatShoppingListHTML_HomePageLink(t *testing.T) { 108 143 loc := locations.Location{ID: "L1", Name: "Store", Address: "1 Main St"} 109 144 p := DefaultParams(&loc, time.Now())
+2
internal/recipes/server.go
··· 502 502 ctx := r.Context() 503 503 spinnerData := struct { 504 504 ClarityScript template.HTML 505 + GoogleTagScript template.HTML 505 506 Style seasons.Style 506 507 RefreshInterval string // seconds 507 508 }{ 508 509 ClarityScript: templates.ClarityScript(), 510 + GoogleTagScript: templates.GoogleTagScript(), 509 511 Style: seasons.GetCurrentStyle(), 510 512 RefreshInterval: "10", // seconds 511 513 }
+1
internal/templates/about.html
··· 11 11 <script src="https://cdnjs.cloudflare.com/ajax/libs/fotorama/4.6.4/fotorama.min.js" defer></script> 12 12 13 13 {{.ClarityScript}} 14 + {{.GoogleTagScript}} 14 15 </head> 15 16 <body class="min-h-screen bg-gradient-to-b from-brand-50 to-white antialiased"> 16 17 <main class="px-4 py-10">
+1
internal/templates/home.html
··· 8 8 {{template "tailwind_head" .Style}} 9 9 10 10 {{.ClarityScript}} 11 + {{.GoogleTagScript}} 11 12 </head> 12 13 <body class="relative min-h-screen overflow-x-hidden bg-gradient-to-b from-brand-50/80 via-white to-brand-50/80 text-ink-700 antialiased"> 13 14 <div class="pointer-events-none absolute inset-0" aria-hidden="true">
+1
internal/templates/locations.html
··· 8 8 {{template "tailwind_head" .Style}} 9 9 10 10 {{.ClarityScript}} 11 + {{.GoogleTagScript}} 11 12 </head> 12 13 <body class="min-h-screen bg-gradient-to-b from-brand-50 to-white antialiased"> 13 14 <main class="px-4 py-10">
+1
internal/templates/recipe.html
··· 23 23 {{template "tailwind_head" .Style}} 24 24 25 25 {{.ClarityScript}} 26 + {{.GoogleTagScript}} 26 27 </head> 27 28 <body class="min-h-screen bg-gradient-to-b from-brand-50 to-white antialiased"> 28 29 <main class="px-4 py-10">
+1
internal/templates/shoppinglist.html
··· 23 23 {{template "tailwind_head" .Style}} 24 24 25 25 {{.ClarityScript}} 26 + {{.GoogleTagScript}} 26 27 </head> 27 28 <body class="min-h-screen bg-gradient-to-b from-brand-50 to-white text-ink-700 antialiased"> 28 29 <main class="px-4 py-10">
+1
internal/templates/spinner.html
··· 11 11 {{template "tailwind_head" .Style}} 12 12 13 13 {{.ClarityScript}} 14 + {{.GoogleTagScript}} 14 15 </head> 15 16 <body class="min-h-screen overflow-hidden bg-gradient-to-b from-brand-50 to-white text-ink-700 antialiased"> 16 17 <main class="grid min-h-screen place-items-center px-4" role="status" aria-live="polite">
+20
internal/templates/templates.go
··· 42 42 43 43 //todo pull from config. 44 44 Clarityproject = os.Getenv("CLARITY_PROJECT_ID") 45 + GoogleTagID = os.Getenv("GOOGLE_TAG_ID") 45 46 return nil 46 47 } 47 48 ··· 54 55 } 55 56 56 57 var Clarityproject string 58 + var GoogleTagID string 57 59 58 60 // ClarityScript generates the Microsoft Clarity tracking script HTML 59 61 func ClarityScript() template.HTML { ··· 71 73 72 74 return template.HTML(script) 73 75 } 76 + 77 + // GoogleTagScript generates the Google tag snippet HTML. 78 + func GoogleTagScript() template.HTML { 79 + if GoogleTagID == "" { 80 + return "" 81 + } 82 + 83 + script := `<script async src="https://www.googletagmanager.com/gtag/js?id=` + GoogleTagID + `"></script> 84 + <script> 85 + window.dataLayer = window.dataLayer || []; 86 + function gtag(){dataLayer.push(arguments);} 87 + gtag('js', new Date()); 88 + 89 + gtag('config', '` + GoogleTagID + `'); 90 + </script>` 91 + 92 + return template.HTML(script) 93 + }
+1
internal/templates/user.html
··· 8 8 {{template "tailwind_head" .Style}} 9 9 10 10 {{.ClarityScript}} 11 + {{.GoogleTagScript}} 11 12 </head> 12 13 <body class="min-h-screen bg-gradient-to-b from-brand-50 to-white antialiased"> 13 14 <main class="px-4 py-10">
+2
internal/users/server.go
··· 170 170 } 171 171 data := struct { 172 172 ClarityScript template.HTML 173 + GoogleTagScript template.HTML 173 174 User *utypes.User 174 175 Success bool 175 176 FavoriteStoreName string ··· 178 179 ServerSignedIn bool 179 180 }{ 180 181 ClarityScript: templates.ClarityScript(), 182 + GoogleTagScript: templates.GoogleTagScript(), 181 183 User: userForTemplate, 182 184 Success: success, 183 185 FavoriteStoreName: favoriteStoreName,