ai cooking
0
fork

Configure Feed

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

Kubelint (#375)

* get task and github workflows on same page

* maybe taskfile is better

* more taskfoolery

authored by

Paul Miller and committed by
GitHub
e81da5ca 5434d142

+855 -529
+65 -4
.github/workflows/go.yml
··· 1 1 # .github/workflows/ghcr-publish-pr-gate.yml 2 2 name: Build and Publish 3 3 4 + env: 5 + GOLANGCI_LINT_CACHE: /tmp/golangci-lint 6 + 4 7 on: 5 8 push: 6 9 branches: [master, main] ··· 30 33 - name: Run Go tests 31 34 run: go test ./... 32 35 36 + vet: 37 + name: vet 38 + runs-on: ubuntu-latest 39 + permissions: 40 + contents: read 41 + 42 + steps: 43 + - uses: actions/checkout@v4 44 + 45 + - uses: actions/setup-go@v5 46 + with: 47 + go-version-file: go.mod 48 + cache: true 49 + 50 + - name: Run go vet 51 + run: go vet ./... 52 + 53 + format: 54 + name: format 55 + runs-on: ubuntu-latest 56 + permissions: 57 + contents: read 58 + 59 + steps: 60 + - uses: actions/checkout@v4 61 + 62 + - uses: actions/setup-go@v5 63 + with: 64 + go-version-file: go.mod 65 + cache: true 66 + 67 + - name: Install gofumpt 68 + run: bash tools/install.sh gofumpt 69 + 70 + - name: Check Go formatting 71 + run: test -z "$(./bin/gofumpt -l .)" 72 + 33 73 lint: 34 74 name: lint 35 75 runs-on: ubuntu-latest ··· 44 84 go-version-file: go.mod 45 85 cache: true 46 86 47 - - uses: golangci/golangci-lint-action@v9 87 + - name: Install golangci-lint 88 + run: bash tools/install.sh golangci-lint 89 + 90 + - name: Run golangci-lint 91 + run: ./bin/golangci-lint run ./... --timeout=5m 92 + 93 + k8s-lint: 94 + name: k8s-lint 95 + runs-on: ubuntu-latest 96 + permissions: 97 + contents: read 98 + 99 + steps: 100 + - uses: actions/checkout@v4 101 + 102 + - uses: actions/setup-go@v5 48 103 with: 49 - version: v2.11.3 50 - args: --timeout=5m 104 + go-version-file: go.mod 105 + cache: true 106 + 107 + - name: Install kubeconform 108 + run: bash tools/install.sh kubeconform 109 + 110 + - name: Validate Kubernetes manifests 111 + run: ./bin/kubeconform -strict -summary deploy/*.yaml 51 112 52 113 publish: 53 114 name: publish (${{ matrix.name }}) 54 - needs: [verify, lint] 115 + needs: [verify, vet, format, lint, k8s-lint] 55 116 runs-on: ubuntu-latest 56 117 permissions: 57 118 contents: read
+1
.gitignore
··· 37 37 # Cache directory and compiled binary 38 38 cache/ 39 39 careme 40 + bin/
+138 -6
Taskfile.yml
··· 4 4 env: 5 5 GOCACHE: '{{default "/tmp/go-build" .GOCACHE}}' 6 6 GOMODCACHE: '{{default "/tmp/go-modcache" .GOMODCACHE}}' 7 + GOLANGCI_LINT_CACHE: '{{default "/tmp/golangci-lint" .GOLANGCI_LINT_CACHE}}' 7 8 8 9 tasks: 9 10 default: ··· 12 13 - task --list 13 14 silent: true 14 15 16 + ci: 17 + desc: Run the local equivalent of CI checks and image builds 18 + deps: 19 + - verify 20 + - vet 21 + - format-check 22 + - lint 23 + - k8s-lint 24 + - tailwind-check 25 + - docker-build-all 26 + 15 27 verify: 16 - desc: Run the standard local verification suite 28 + desc: Run the broader local verification suite 17 29 deps: 18 30 - fmt 19 31 - vet 20 - - lint 21 32 - test 33 + - lint 34 + - k8s-lint 35 + - tailwind-check 22 36 23 37 fmt: 24 - desc: Format Go code 38 + desc: Format Go code with gofumpt 39 + deps: 40 + - gofumpt 25 41 cmds: 26 - - go fmt ./... 42 + - ./bin/gofumpt -w . 27 43 28 44 vet: 29 45 desc: Run go vet ··· 32 48 33 49 lint: 34 50 desc: Run golangci-lint 51 + deps: 52 + - golangci-lint 35 53 cmds: 36 - - golangci-lint run ./... 54 + - ./bin/golangci-lint run ./... --timeout=5m 55 + 56 + fmt-check: 57 + desc: Check Go formatting with gofumpt 58 + deps: 59 + - gofumpt 60 + cmds: 61 + - | 62 + files="$(./bin/gofumpt -l .)" 63 + test -z "$files" || { 64 + printf '%s\n' "$files" 65 + exit 1 66 + } 67 + 68 + k8s-lint: 69 + desc: Validate Kubernetes manifests with kubeconform 70 + deps: 71 + - kubeconform 72 + cmds: 73 + - ./bin/kubeconform -strict -summary deploy/*.yaml 37 74 38 75 test: 39 76 desc: Run tests with mocks enabled by default ··· 47 84 cmds: 48 85 - bash tailwind/generate.sh 49 86 87 + tailwind-check: 88 + desc: Regenerate Tailwind output and fail on uncommitted changes 89 + cmds: 90 + - bash tailwind/generate.sh 91 + - git diff --exit-code 92 + 93 + docker-build: 94 + desc: Build one Docker image locally without pushing 95 + vars: 96 + CMD_PATH: '{{default "./cmd/careme" .CMD_PATH}}' 97 + IMAGE: '{{default "careme-local" .IMAGE}}' 98 + cmds: 99 + - docker build --build-arg CMD_PATH={{.CMD_PATH}} -t {{.IMAGE}} . 100 + 101 + docker-build-all: 102 + desc: Build the same image matrix as CI locally without pushing 103 + internal: true 104 + deps: 105 + - docker-build-careme 106 + - docker-build-wholefoods 107 + - docker-build-albertsons 108 + - docker-build-publix 109 + - docker-build-aldi 110 + 111 + docker-build-careme: 112 + desc: Build the careme image locally 113 + internal: true 114 + cmds: 115 + - task: docker-build 116 + vars: 117 + CMD_PATH: ./cmd/careme 118 + IMAGE: careme-local 119 + 120 + docker-build-wholefoods: 121 + desc: Build the wholefoods image locally 122 + internal: true 123 + cmds: 124 + - task: docker-build 125 + vars: 126 + CMD_PATH: ./cmd/wholefoods 127 + IMAGE: careme-wholefoods-local 128 + 129 + docker-build-albertsons: 130 + desc: Build the albertsons image locally 131 + internal: true 132 + cmds: 133 + - task: docker-build 134 + vars: 135 + CMD_PATH: ./cmd/albertsons 136 + IMAGE: careme-albertsons-local 137 + 138 + docker-build-publix: 139 + desc: Build the publix image locally 140 + internal: true 141 + cmds: 142 + - task: docker-build 143 + vars: 144 + CMD_PATH: ./cmd/publix 145 + IMAGE: careme-publix-local 146 + 147 + docker-build-aldi: 148 + desc: Build the aldi image locally 149 + internal: true 150 + cmds: 151 + - task: docker-build 152 + vars: 153 + CMD_PATH: ./cmd/aldi 154 + IMAGE: careme-aldi-local 155 + 50 156 serve: 51 157 desc: Run the local web server 52 158 vars: ··· 55 161 - go run ./cmd/careme -serve -addr {{.ADDR}} 56 162 57 163 zipcode: 58 - desc: Look up Kroger locations for a ZIP code 164 + desc: Look up locations for a ZIP code 59 165 vars: 60 166 ZIP: '{{default "98101" .ZIP}}' 61 167 cmds: ··· 75 181 NAMESPACE: '{{default "careme" .NAMESPACE}}' 76 182 cmds: 77 183 - bash deploy/deploy.sh {{.REF}} {{.NAMESPACE}} 184 + 185 + tools: 186 + desc: Install pinned local CI tools into ./bin 187 + cmds: 188 + - bash tools/install.sh 189 + 190 + gofumpt: 191 + internal: true 192 + status: 193 + - test -x ./bin/gofumpt 194 + cmds: 195 + - bash tools/install.sh gofumpt 196 + 197 + golangci-lint: 198 + internal: true 199 + status: 200 + - test -x ./bin/golangci-lint 201 + cmds: 202 + - bash tools/install.sh golangci-lint 203 + 204 + kubeconform: 205 + internal: true 206 + status: 207 + - test -x ./bin/kubeconform 208 + cmds: 209 + - bash tools/install.sh kubeconform
+6 -5
cmd/albertsons/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/albertsons" 5 - "careme/internal/cache" 6 - "careme/internal/logsetup" 7 4 "context" 8 5 "errors" 9 6 "flag" ··· 13 10 "net/http" 14 11 "strings" 15 12 "time" 13 + 14 + "careme/internal/albertsons" 15 + "careme/internal/cache" 16 + "careme/internal/logsetup" 16 17 ) 17 18 18 19 func main() { ··· 84 85 for _, page := range pages { 85 86 locationID := strings.TrimSpace(urlMap[page.URL]) 86 87 if locationID != "" { 87 - //exists, err := cacheStore.Exists(ctx, albertsons.StoreCachePrefix+locationID) 88 - //if err == nil && exists { 88 + // exists, err := cacheStore.Exists(ctx, albertsons.StoreCachePrefix+locationID) 89 + // if err == nil && exists { 89 90 continue 90 91 // } 91 92 }
+3 -2
cmd/albertsons/main_test.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/albertsons" 5 - "careme/internal/cache" 6 4 "context" 7 5 "fmt" 8 6 "io" ··· 11 9 "sync/atomic" 12 10 "testing" 13 11 "time" 12 + 13 + "careme/internal/albertsons" 14 + "careme/internal/cache" 14 15 ) 15 16 16 17 func TestSelectedChainsDefaultsToAll(t *testing.T) {
+4 -3
cmd/aldi/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/aldi" 5 - "careme/internal/cache" 6 - "careme/internal/logsetup" 7 4 "context" 8 5 "flag" 9 6 "fmt" ··· 11 8 "log/slog" 12 9 "net/http" 13 10 "time" 11 + 12 + "careme/internal/aldi" 13 + "careme/internal/cache" 14 + "careme/internal/logsetup" 14 15 ) 15 16 16 17 type summaryClient interface {
+3 -2
cmd/aldi/main_test.go
··· 1 1 package main 2 2 3 3 import ( 4 + "context" 5 + "testing" 6 + 4 7 "careme/internal/aldi" 5 8 "careme/internal/cache" 6 - "context" 7 - "testing" 8 9 ) 9 10 10 11 func TestSyncLocationsCachesSummaries(t *testing.T) {
+8 -7
cmd/careme/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/config" 5 - "careme/internal/logsetup" 6 - "careme/internal/mail" 7 - "careme/internal/static" 8 - "careme/internal/templates" 9 4 "context" 10 5 _ "embed" 11 6 "flag" 12 7 "log" 13 8 "log/slog" 14 9 "os" 10 + 11 + "careme/internal/config" 12 + "careme/internal/logsetup" 13 + "careme/internal/mail" 14 + "careme/internal/static" 15 + "careme/internal/templates" 15 16 ) 16 17 17 18 func main() { 18 19 var serve, mailer bool 19 20 var addr string 20 21 21 - //left for back compat does noting 22 + // left for back compat does noting 22 23 flag.BoolVar(&serve, "serve", false, "dead we always serve") 23 24 flag.BoolVar(&mailer, "mail", false, "Run one-shot mail sender and exit") 24 25 flag.StringVar(&addr, "addr", ":8080", "Address to bind in server mode") 25 26 flag.Parse() 26 27 27 - if err := os.MkdirAll("recipes", 0755); err != nil { 28 + if err := os.MkdirAll("recipes", 0o755); err != nil { 28 29 log.Fatalf("failed to create recipes directory: %v", err) 29 30 } 30 31
+4 -3
cmd/careme/middleware.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/logsetup" 5 4 "errors" 6 5 "log/slog" 7 6 "net/http" ··· 11 10 "strconv" 12 11 "strings" 13 12 "time" 13 + 14 + "careme/internal/logsetup" 14 15 15 16 "github.com/clerk/clerk-sdk-go/v2" 16 17 azureappinsights "github.com/microsoft/ApplicationInsights-Go/appinsights" ··· 32 33 33 34 func (l *logger) ServeHTTP(w http.ResponseWriter, r *http.Request) { 34 35 start := time.Now() 35 - //should we use auth client? 36 + // should we use auth client? 36 37 user := "" 37 38 if claims, ok := clerk.SessionClaimsFromContext(r.Context()); ok { 38 39 user = claims.Subject ··· 150 151 } 151 152 152 153 func (r *recoverer) ServeHTTP(w http.ResponseWriter, req *http.Request) { 153 - //app insights could also track this https://github.com/microsoft/ApplicationInsights-Go?tab=readme-ov-file#exceptions 154 + // app insights could also track this https://github.com/microsoft/ApplicationInsights-Go?tab=readme-ov-file#exceptions 154 155 defer func() { 155 156 if err := recover(); err != nil { 156 157 slog.ErrorContext(req.Context(), "panic recovered", "error", err, "stack", debug.Stack())
+1 -1
cmd/careme/ready.go
··· 20 20 return err 21 21 } 22 22 } 23 - //not thread safe? only ever set to true 23 + // not thread safe? only ever set to true 24 24 r.done = true 25 25 return nil 26 26 }
+13 -13
cmd/careme/web.go
··· 1 1 package main 2 2 3 3 import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "html/template" 8 + "log/slog" 9 + "net/http" 10 + "os" 11 + "os/signal" 12 + "syscall" 13 + "time" 14 + 4 15 "careme/internal/actowiz" 5 16 "careme/internal/admin" 6 17 "careme/internal/auth" ··· 15 26 "careme/internal/templates" 16 27 "careme/internal/users" 17 28 utypes "careme/internal/users/types" 18 - "context" 19 - "errors" 20 - "fmt" 21 - "html/template" 22 - "log/slog" 23 - "net/http" 24 - "os" 25 - "os/signal" 26 - "syscall" 27 - "time" 28 29 ) 29 30 30 31 func runServer(cfg *config.Config, addr string) error { ··· 102 103 http.Error(w, "unable to load account", http.StatusInternalServerError) 103 104 return 104 105 } 105 - //no user is fine we'll just pass nil currentUser to template 106 + // no user is fine we'll just pass nil currentUser to template 106 107 // just have two different templates? 107 - 108 108 } 109 109 110 110 var favoriteStoreName string ··· 112 112 loc, locErr := locationStorage.GetLocationByID(ctx, currentUser.FavoriteStore) 113 113 if locErr != nil { 114 114 slog.ErrorContext(ctx, "failed to get location name for favorite store", "location_id", currentUser.FavoriteStore, "error", locErr) 115 - //mutation intentionally not saved bac. 115 + // mutation intentionally not saved bac. 116 116 currentUser.FavoriteStore = "" 117 117 } else { 118 118 favoriteStoreName = loc.Name
+3 -4
cmd/careme/web_e2e_test.go
··· 27 27 defer srv.Close() 28 28 29 29 client := newTestClient(t) 30 - resp := mustGet(t, client, srv.URL+"/ready") //our readiness probe works even with mocks? 30 + resp := mustGet(t, client, srv.URL+"/ready") // our readiness probe works even with mocks? 31 31 if resp.StatusCode != http.StatusOK { 32 32 t.Fatalf("expected /ready to return 200 OK, got %d", resp.StatusCode) 33 33 } ··· 118 118 t.Fatalf("expected recipe page to persist stars value, got body: %s", recipeBody) 119 119 } 120 120 121 - //TODO step 6 make sure recipes are saved to user page? 122 - 121 + // TODO step 6 make sure recipes are saved to user page? 123 122 } 124 123 125 124 func TestZipFromCoordinatesRedirect(t *testing.T) { ··· 154 153 t.Helper() 155 154 156 155 cfg := &config.Config{Mocks: config.MockConfig{Enable: true}} 157 - err := templates.Init(cfg, "dummyhash") //initialize templates so they don't hit the file system during tests 156 + err := templates.Init(cfg, "dummyhash") // initialize templates so they don't hit the file system during tests 158 157 if err != nil { 159 158 t.Fatalf("failed to create templates %v", err) 160 159 }
+4 -3
cmd/heb/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/heb" 6 - "careme/internal/sitemapfetch" 7 4 "context" 8 5 "errors" 9 6 "flag" ··· 12 9 "log/slog" 13 10 "net/http" 14 11 "time" 12 + 13 + "careme/internal/cache" 14 + "careme/internal/heb" 15 + "careme/internal/sitemapfetch" 15 16 ) 16 17 17 18 func main() {
+3 -2
cmd/heb/main_test.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/heb" 6 4 "context" 7 5 "fmt" 8 6 "io" ··· 11 9 "sync/atomic" 12 10 "testing" 13 11 "time" 12 + 13 + "careme/internal/cache" 14 + "careme/internal/heb" 14 15 ) 15 16 16 17 func TestSyncFromSitemapSkipsKnownURLs(t *testing.T) {
+4 -3
cmd/ingredients/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/config" 5 - "careme/internal/kroger" 6 - "careme/internal/recipes" 7 4 "context" 8 5 "flag" 9 6 "fmt" 10 7 "log" 8 + 9 + "careme/internal/config" 10 + "careme/internal/kroger" 11 + "careme/internal/recipes" 11 12 ) 12 13 13 14 func main() {
+7 -9
cmd/producecheck/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/config" 5 - "careme/internal/kroger" 6 - "careme/internal/recipes" 7 4 "context" 8 5 "flag" 9 6 "fmt" ··· 11 8 "slices" 12 9 "strings" 13 10 "unicode" 11 + 12 + "careme/internal/config" 13 + "careme/internal/kroger" 14 + "careme/internal/recipes" 14 15 15 16 "github.com/samber/lo" 16 17 "golang.org/x/text/unicode/norm" ··· 24 25 var locationID string 25 26 var produceCSV string 26 27 27 - //local to bellevue Fred Meyer 70100023, factoria 70500822 28 + // local to bellevue Fred Meyer 70100023, factoria 70500822 28 29 flag.StringVar(&locationID, "location", "70500874", "Kroger location ID to validate") 29 30 flag.StringVar(&locationID, "l", "70500874", "Kroger location ID to validate (short)") 30 31 flag.StringVar(&produceCSV, "produce", strings.Join(all, ","), "Comma-separated produce list to check") ··· 90 91 } 91 92 92 93 func checkProduceAvailability(ctx context.Context, client staplesProvider, locationID string, produce []string) ([]string, int, error) { 93 - 94 - //todo check total number of queries. 94 + // todo check total number of queries. 95 95 96 96 ingredients, err := client.FetchStaples(ctx, locationID) 97 97 if err != nil { ··· 112 112 113 113 // TODO have staples return subset of ingredient that can say the search term it go a match on 114 114 // then we can give info on whats coming from what queries. Could use categories for wholefoods. 115 - //annotateUniqueOnlyMatches(stats) 115 + // annotateUniqueOnlyMatches(stats) 116 116 printProduceFilterSummary(stats, len(produce)) 117 117 118 118 return evaluateProduceAvailability(produce, ingredients), len(ingredients), nil ··· 190 190 }*/ 191 191 192 192 func printProduceFilterSummary(stat produceFilterStats, totalProduceTerms int) { 193 - 194 193 fmt.Printf("- %s -> %d ingredients, %d/%d produce terms, %d matches, %d unique-only products\n", 195 194 stat.FilterTerm, 196 195 stat.IngredientMatches, ··· 318 317 } 319 318 320 319 func normalizeToken(s string) string { 321 - 322 320 switch { 323 321 case strings.HasSuffix(s, "ies") && len(s) > 3: 324 322 s = s[:len(s)-3] + "y"
+2 -1
cmd/producecheck/main_test.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/kroger" 5 4 "reflect" 6 5 "testing" 6 + 7 + "careme/internal/kroger" 7 8 ) 8 9 9 10 func TestParseProduceList(t *testing.T) {
+4 -3
cmd/publix/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/logsetup" 6 - "careme/internal/publix" 7 4 "context" 8 5 "errors" 9 6 "flag" ··· 13 10 "net/http" 14 11 "strconv" 15 12 "time" 13 + 14 + "careme/internal/cache" 15 + "careme/internal/logsetup" 16 + "careme/internal/publix" 16 17 ) 17 18 18 19 type syncConfig struct {
+3 -4
cmd/publix/main_test.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/publix" 6 4 "context" 7 5 "net/http" 8 6 "net/http/httptest" 9 7 "testing" 8 + 9 + "careme/internal/cache" 10 + "careme/internal/publix" 10 11 ) 11 12 12 13 func TestSyncStoresCachesSuccessesAndMisses(t *testing.T) { ··· 55 56 if _, ok := missingIDs["1084"]; !ok { 56 57 t.Fatalf("expected missing store id 1084 to be cached") 57 58 } 58 - 59 59 } 60 60 61 61 func TestSyncStoresSkipsKnownMissingAndCachedSuccesses(t *testing.T) { ··· 132 132 if stats.Synced != 1 { 133 133 t.Fatalf("unexpected stats: %+v", stats) 134 134 } 135 - 136 135 } 137 136 138 137 const sampleStoreHTML = `<!doctype html>
+3 -2
cmd/purgeshoppinglist/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/recipes" 6 4 "context" 7 5 "flag" 8 6 "fmt" ··· 12 10 "path/filepath" 13 11 "slices" 14 12 "strings" 13 + 14 + "careme/internal/cache" 15 + "careme/internal/recipes" 15 16 ) 16 17 17 18 type purgeCache interface {
+2 -1
cmd/purgeshoppinglist/main_test.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/cache" 6 5 "context" 7 6 "errors" 8 7 "testing" 8 + 9 + "careme/internal/cache" 9 10 ) 10 11 11 12 func TestPurgeInvalidShoppingListsApply(t *testing.T) {
+3 -2
cmd/walmartstores/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/config" 5 - "careme/internal/walmart" 6 4 "context" 7 5 "flag" 8 6 "fmt" 9 7 "log/slog" 10 8 "os" 11 9 "time" 10 + 11 + "careme/internal/config" 12 + "careme/internal/walmart" 12 13 ) 13 14 14 15 const defaultConsumerID = "52dae855-d02f-488b-b179-1df6700d7dcf"
+5 -4
cmd/wholefoods/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/logsetup" 6 - "careme/internal/wholefoods" 7 4 "context" 8 5 "errors" 9 6 "flag" ··· 12 9 "log/slog" 13 10 "net/http" 14 11 "time" 12 + 13 + "careme/internal/cache" 14 + "careme/internal/logsetup" 15 + "careme/internal/wholefoods" 15 16 ) 16 17 17 18 func main() { ··· 102 103 refs = append(refs, wholefoods.StoreReference{ID: storeID, URL: url}) 103 104 } 104 105 105 - //TOD remove stores from url map not in itemap? 106 + // TOD remove stores from url map not in itemap? 106 107 107 108 if updated { 108 109 if err := wholefoods.SaveStoreURLMap(ctx, cacheStore, refs); err != nil {
+3 -2
cmd/wholefoods/main_test.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/wholefoods" 6 4 "context" 7 5 "fmt" 8 6 "net/http" 9 7 "net/http/httptest" 10 8 "sync/atomic" 11 9 "testing" 10 + 11 + "careme/internal/cache" 12 + "careme/internal/wholefoods" 12 13 ) 13 14 14 15 func TestResolveStoreReferencesFillsMissingCachedSitemapEntries(t *testing.T) {
+4 -4
cmd/zipstorecount/main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/config" 6 - "careme/internal/locations" 7 4 "context" 8 5 "encoding/csv" 9 6 "errors" ··· 18 15 "sync" 19 16 "text/tabwriter" 20 17 "time" 18 + 19 + "careme/internal/cache" 20 + "careme/internal/config" 21 + "careme/internal/locations" 21 22 ) 22 23 23 24 type zipStoreCount struct { ··· 84 85 resultsChan <- zipQueryResult{ 85 86 counts: countStoresByChain(mzc, stores), 86 87 } 87 - 88 88 }(code) 89 89 } 90 90 wg.Wait()
+2 -1
cmd/zipstorecount/main_test.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/locations" 6 5 "reflect" 7 6 "sort" 8 7 "strings" 9 8 "testing" 9 + 10 + "careme/internal/locations" 10 11 ) 11 12 12 13 func TestExtractZipCodes_UsesSecondColumn(t *testing.T) {
+43
deploy/cronjob-careme-mail.yaml
··· 1 + apiVersion: batch/v1 2 + kind: CronJob 3 + metadata: 4 + name: careme-mail 5 + labels: 6 + app: careme 7 + spec: 8 + schedule: "0 * * * *" 9 + concurrencyPolicy: Forbid 10 + successfulJobsHistoryLimit: 2 11 + failedJobsHistoryLimit: 3 12 + jobTemplate: 13 + spec: 14 + backoffLimit: 1 15 + template: 16 + metadata: 17 + labels: 18 + app: careme 19 + job: careme-mail 20 + spec: 21 + restartPolicy: Never 22 + securityContext: 23 + runAsNonRoot: true 24 + runAsUser: 65532 25 + runAsGroup: 65532 26 + containers: 27 + - name: careme-mail 28 + image: ghcr.io/paulgmiller/careme:${IMAGE_TAG} 29 + imagePullPolicy: IfNotPresent 30 + args: ["-mail"] 31 + envFrom: 32 + - secretRef: 33 + name: careme-secrets3 34 + env: 35 + - name: APPLICATIONINSIGHTS_CONNECTION_STRING 36 + value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 37 + resources: 38 + requests: 39 + cpu: 50m 40 + memory: 64Mi 41 + limits: 42 + cpu: 500m 43 + memory: 256Mi
+42
deploy/cronjob-wholefoods-scrape.yaml
··· 1 + apiVersion: batch/v1 2 + kind: CronJob 3 + metadata: 4 + name: wholefoods-scrape 5 + labels: 6 + app: careme 7 + spec: 8 + schedule: "0 6 * * 0" 9 + concurrencyPolicy: Forbid 10 + successfulJobsHistoryLimit: 2 11 + failedJobsHistoryLimit: 3 12 + jobTemplate: 13 + spec: 14 + backoffLimit: 1 15 + template: 16 + metadata: 17 + labels: 18 + app: careme 19 + job: wholefoods-scrape 20 + spec: 21 + restartPolicy: Never 22 + securityContext: 23 + runAsNonRoot: true 24 + runAsUser: 65532 25 + runAsGroup: 65532 26 + containers: 27 + - name: wholefoods 28 + image: ghcr.io/paulgmiller/careme-wholefoods:${IMAGE_TAG} 29 + imagePullPolicy: IfNotPresent 30 + envFrom: 31 + - secretRef: 32 + name: careme-secrets3 33 + env: 34 + - name: APPLICATIONINSIGHTS_CONNECTION_STRING 35 + value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 36 + resources: 37 + requests: 38 + cpu: 50m 39 + memory: 64Mi 40 + limits: 41 + cpu: 500m 42 + memory: 256Mi
+15 -11
deploy/deploy.sh
··· 2 2 set -euo pipefail 3 3 4 4 ref="${1:-origin/master}" 5 - deploy_file="deploy/deploy.yaml" 5 + deploy_dir="deploy" 6 + manifest_files=( 7 + "${deploy_dir}/deploy.yaml" 8 + "${deploy_dir}/cronjob-careme-mail.yaml" 9 + "${deploy_dir}/cronjob-wholefoods-scrape.yaml" 10 + ) 6 11 namespace="${2:-careme}" 7 12 short_len=7 8 13 ··· 25 30 26 31 export IMAGE_TAG="${commit_hash:0:${short_len}}" 27 32 28 - if [[ ! -f "${deploy_file}" ]]; then 29 - echo "error: deploy file not found: ${deploy_file}" >&2 30 - exit 1 31 - fi 32 - 33 - if [[ "$(<"${deploy_file}")" != *'${IMAGE_TAG}'* ]]; then 34 - echo "error: ${deploy_file} does not contain \${IMAGE_TAG}" >&2 35 - exit 1 36 - fi 33 + for manifest_file in "${manifest_files[@]}"; do 34 + if [[ ! -f "${manifest_file}" ]]; then 35 + echo "error: deploy file not found: ${manifest_file}" >&2 36 + exit 1 37 + fi 38 + done 37 39 38 40 echo "Deploying image: ${IMAGE_TAG}" 39 - envsubst '${IMAGE_TAG}' <"${deploy_file}" | kubectl apply -f - -n "${namespace}" 41 + for manifest_file in "${manifest_files[@]}"; do 42 + envsubst '${IMAGE_TAG}' <"${manifest_file}" | kubectl apply -f - -n "${namespace}" 43 + done 40 44 41 45 echo "Waiting for rollout of deployment/careme" 42 46 kubectl rollout status deployment/careme -n "${namespace}" -w
-87
deploy/deploy.yaml
··· 68 68 - name: recipes 69 69 emptyDir: {} 70 70 --- 71 - apiVersion: batch/v1 72 - kind: CronJob 73 - metadata: 74 - name: careme-mail 75 - labels: 76 - app: careme 77 - spec: 78 - schedule: "0 * * * *" 79 - concurrencyPolicy: Forbid 80 - successfulJobsHistoryLimit: 2 81 - failedJobsHistoryLimit: 3 82 - jobTemplate: 83 - spec: 84 - backoffLimit: 1 85 - template: 86 - metadata: 87 - labels: 88 - app: careme 89 - job: careme-mail 90 - spec: 91 - restartPolicy: Never 92 - securityContext: 93 - runAsNonRoot: true 94 - runAsUser: 65532 95 - runAsGroup: 65532 96 - containers: 97 - - name: careme-mail 98 - image: ghcr.io/paulgmiller/careme:${IMAGE_TAG} 99 - imagePullPolicy: IfNotPresent 100 - args: ["-mail"] 101 - envFrom: 102 - - secretRef: 103 - name: careme-secrets3 104 - env: 105 - - name: APPLICATIONINSIGHTS_CONNECTION_STRING 106 - value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 107 - resources: 108 - requests: 109 - cpu: 50m 110 - memory: 64Mi 111 - limits: 112 - cpu: 500m 113 - memory: 256Mi 114 - --- 115 - apiVersion: batch/v1 116 - kind: CronJob 117 - metadata: 118 - name: wholefoods-scrape 119 - labels: 120 - app: careme 121 - spec: 122 - schedule: "0 6 * * 0" 123 - concurrencyPolicy: Forbid 124 - successfulJobsHistoryLimit: 2 125 - failedJobsHistoryLimit: 3 126 - jobTemplate: 127 - spec: 128 - backoffLimit: 1 129 - template: 130 - metadata: 131 - labels: 132 - app: careme 133 - job: wholefoods-scrape 134 - spec: 135 - restartPolicy: Never 136 - securityContext: 137 - runAsNonRoot: true 138 - runAsUser: 65532 139 - runAsGroup: 65532 140 - containers: 141 - - name: wholefoods 142 - image: ghcr.io/paulgmiller/careme-wholefoods:${IMAGE_TAG} 143 - imagePullPolicy: IfNotPresent 144 - envFrom: 145 - - secretRef: 146 - name: careme-secrets3 147 - env: 148 - - name: APPLICATIONINSIGHTS_CONNECTION_STRING 149 - value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 150 - resources: 151 - requests: 152 - cpu: 50m 153 - memory: 64Mi 154 - limits: 155 - cpu: 500m 156 - memory: 256Mi 157 - --- 158 71 apiVersion: v1 159 72 kind: Service 160 73 metadata:
+15 -15
internal/actowiz/server.go
··· 8 8 const scrapeIntervalDays = 7 9 9 10 10 var defaultStoreIDs = []string{ 11 - "safeway_490", //1645 140th ave 11 + "safeway_490", // 1645 140th ave 12 12 "safeway_1600", // 300 bellevue way 13 - "safeway_496", //15000 ne 24th st 13 + "safeway_496", // 15000 ne 24th st 14 14 "safeway_1444", // se 38th 15 - "safeway_1472", //35 e college way mt vernon 16 - "haggen_3450", //2601 e division st mt vernon 17 - "safeway_423", //35th ave ne, u district 18 - "safeway_1550", //7300 Roosevelt way ne, u district 15 + "safeway_1472", // 35 e college way mt vernon 16 + "haggen_3450", // 2601 e division st mt vernon 17 + "safeway_423", // 35th ave ne, u district 18 + "safeway_1550", // 7300 Roosevelt way ne, u district 19 19 "starmarket_366", // 177 Beacon St, jamaica plains 20 20 "starmarket_2576", // 33 kilmarnock st, jamaica plains 21 21 "starmarket_2573", // 130 granite st, boston 22 - "albertsons_453", //462 ne sunset blvd, renton 22 + "albertsons_453", // 462 ne sunset blvd, renton 23 23 "safeway_3319", // 4300 ne 4th st 24 - "safeway_336", //2725 ne sunset bl" 25 - "safeway_1502", //1701 sant rita rd, pleasanton 26 - "safeway_1880", //8858 waltham woods rd baltimore. 27 - "jewelosco_3170", //S pulaski rd, chicago 28 - "albertsons_4260", //2164 e buckingham dr dallas 24 + "safeway_336", // 2725 ne sunset bl" 25 + "safeway_1502", // 1701 sant rita rd, pleasanton 26 + "safeway_1880", // 8858 waltham woods rd baltimore. 27 + "jewelosco_3170", // S pulaski rd, chicago 28 + "albertsons_4260", // 2164 e buckingham dr dallas 29 29 "safeway_2917", // 1605 bridge st denver 30 30 "albertsons_4131", // 3910 Crenshaw blvd, los angeles 31 31 "vons_2261", // 31 w 3rd los angeles ··· 35 35 "acmemarkets_2680", // 100 Grove lane deleware 36 36 "acmemarkets_806", // 100 suburban dr 37 37 "acmemarkets_882", // 907 paoli pike west chester 38 - "safeway_2799", //28455 n vistancia blvd phoenix 38 + "safeway_2799", // 28455 n vistancia blvd phoenix 39 39 "safeway_72", // 910 w happy val rid phoenix 40 40 "safeway_1231", // 12032 sunnyside rd portland 41 - "safeway_382", //3527 se 1222nd ave portland 41 + "safeway_382", // 3527 se 1222nd ave portland 42 42 "albertsons_1641", // 7070 arcibald ave san bernardino 43 43 "safeway_1215", // 660 bailey rd san fran 44 - "safeway_1294", //210 washington ave s seattle 44 + "safeway_1294", // 210 washington ave s seattle 45 45 "safeway_1668", // 5510 norbeck rd DC 46 46 "safeway_2781", // 11201 georgia ave DC 47 47 }
-1
internal/actowiz/server_test.go
··· 34 34 if len(got.StoreIDs) < 30 { 35 35 t.Fatalf("store id count = %d, want 30", len(got.StoreIDs)) 36 36 } 37 - 38 37 }
+4 -4
internal/actowiz/staples.go
··· 20 20 21 21 var ( 22 22 embeddedSafewayProducts = mustLoadSafewayProducts() 23 - defaultStaplesSignature = "everything" //no filtering yet" 23 + defaultStaplesSignature = "everything" // no filtering yet" 24 24 ) 25 25 26 26 type identityProvider struct{} ··· 48 48 } 49 49 50 50 func all() []kroger.Ingredient { 51 - //do this once instead of every time? 51 + // do this once instead of every time? 52 52 ingredients := make([]kroger.Ingredient, 0, len(embeddedSafewayProducts)) 53 53 for _, product := range embeddedSafewayProducts { 54 54 if !product.Availability { ··· 107 107 return true 108 108 } 109 109 } 110 - //categories might help for wine? 110 + // categories might help for wine? 111 111 return false 112 112 } 113 113 114 114 func productToIngredient(product SafewayProduct) kroger.Ingredient { 115 - description, size := splitProductName(product.ProductName) //dubious size is really always 115 + description, size := splitProductName(product.ProductName) // dubious size is really always 116 116 regularPrice := float32Ptr(product.MRP) 117 117 salePrice := float32Ptr(product.DiscountedPrice) 118 118 if salePrice == nil {
+1 -1
internal/actowiz/types.go
··· 13 13 ProductName string `json:"Product Name"` 14 14 ID int64 `json:"ID"` 15 15 URL string `json:"URL"` 16 - ProductDescription string `json:"Product Description"` //this is really long 16 + ProductDescription string `json:"Product Description"` // this is really long 17 17 MRP *float64 `json:"MRP"` 18 18 DiscountedPrice *float64 `json:"Discounted Price"` 19 19 Category string `json:"Category"`
+3 -2
internal/admin/middleware.go
··· 1 1 package admin 2 2 3 3 import ( 4 - "careme/internal/auth" 5 - "careme/internal/config" 6 4 "errors" 7 5 "log/slog" 8 6 "net/http" 9 7 "strings" 8 + 9 + "careme/internal/auth" 10 + "careme/internal/config" 10 11 ) 11 12 12 13 type middleware struct {
+3 -2
internal/admin/middleware_test.go
··· 1 1 package admin 2 2 3 3 import ( 4 - "careme/internal/auth" 5 - "careme/internal/config" 6 4 "context" 7 5 "errors" 8 6 "net/http" 9 7 "net/http/httptest" 10 8 "testing" 9 + 10 + "careme/internal/auth" 11 + "careme/internal/config" 11 12 ) 12 13 13 14 type stubAuthClient struct {
+13 -13
internal/ai/client.go
··· 1 1 package ai 2 2 3 3 import ( 4 - "careme/internal/kroger" 5 - "careme/internal/locations" 6 4 "context" 7 5 "encoding/base64" 8 6 "encoding/json" ··· 12 10 "log/slog" 13 11 "strings" 14 12 "time" 13 + 14 + "careme/internal/kroger" 15 + "careme/internal/locations" 15 16 16 17 openai "github.com/openai/openai-go/v3" 17 18 "github.com/openai/openai-go/v3/conversations" ··· 32 33 // todo collapse closer to 33 34 type Ingredient struct { 34 35 Name string `json:"name"` 35 - Quantity string `json:"quantity"` //should this and price be numbers? need units then 36 - Price string `json:"price"` //TODO exclude empty 36 + Quantity string `json:"quantity"` // should this and price be numbers? need units then 37 + Price string `json:"price"` // TODO exclude empty 37 38 } 38 39 39 40 type Recipe struct { ··· 46 47 Health string `json:"health"` 47 48 DrinkPairing string `json:"drink_pairing"` 48 49 WineStyles []string `json:"wine_styles"` 49 - OriginHash string `json:"origin_hash,omitempty" jsonschema:"-"` //not in schema 50 - Saved bool `json:"previously_saved,omitempty" jsonschema:"-"` //not in schema 50 + OriginHash string `json:"origin_hash,omitempty" jsonschema:"-"` // not in schema 51 + Saved bool `json:"previously_saved,omitempty" jsonschema:"-"` // not in schema 51 52 } 52 53 53 54 // ComputeHash calculates the fnv128 hash of the recipe content ··· 74 75 return base64.URLEncoding.EncodeToString(fnv.Sum(nil)) 75 76 } 76 77 77 - //intionally not including ConversationID to preserve old hashes 78 - 78 + // intionally not including ConversationID to preserve old hashes 79 79 type ShoppingList struct { 80 80 ConversationID string `json:"conversation_id,omitempty" jsonschema:"-"` 81 81 Recipes []Recipe `json:"recipes" jsonschema:"required"` ··· 88 88 89 89 // ignoring model for now. 90 90 func NewClient(apiKey, _ string) *Client { 91 - //ignor model for now. 91 + // ignor model for now. 92 92 r := jsonschema.Reflector{ 93 93 DoNotReference: true, // no $defs and no $ref 94 94 ExpandedStruct: true, // put the root type inline (not a $ref) ··· 161 161 Format: responses.ResponseFormatTextConfigUnionParam{ 162 162 OfJSONSchema: &responses.ResponseFormatTextJSONSchemaConfigParam{ 163 163 Name: "recipes", 164 - Schema: schema, //https://platform.openai.com/docs/guides/structured-outputs?example=structured-data 164 + Schema: schema, // https://platform.openai.com/docs/guides/structured-outputs?example=structured-data 165 165 }, 166 166 }, 167 167 } ··· 176 176 177 177 params := responses.ResponseNewParams{ 178 178 Model: c.model, 179 - //only new input 179 + // only new input 180 180 Input: responses.ResponseNewParamsInputUnion{ 181 181 OfInputItemList: messages, 182 182 }, ··· 303 303 }, 304 304 Text: scheme(c.schema), 305 305 } 306 - //should we stream. Can we pass past generation. 306 + // should we stream. Can we pass past generation. 307 307 308 308 resp, err := client.Responses.New(ctx, params) 309 309 if err != nil { ··· 319 319 // buildRecipeMessages creates separate messages for the LLM to process more efficiently 320 320 func (c *Client) buildRecipeMessages(location *locations.Location, saleIngredients []kroger.Ingredient, instructions []string, date time.Time, lastRecipes []string) (responses.ResponseInputParam, error) { 321 321 var messages []responses.ResponseInputItemUnionParam 322 - //constants we might make variable later 322 + // constants we might make variable later 323 323 messages = append(messages, user("Prioritize ingredients that are in season for the current date and user's state location "+date.Format("January 2nd")+" in "+location.State+".")) 324 324 messages = append(messages, user("Default: each recipe should serve 2 people.")) 325 325 messages = append(messages, user("Default: generate 3 recipes"))
+1 -1
internal/ai/recipe_test.go
··· 52 52 } 53 53 54 54 hash := recipe.ComputeHash() 55 - //fnv 128 url encoded is 24 55 + // fnv 128 url encoded is 24 56 56 if len(hash) != 24 { 57 57 t.Fatalf("expected hash length of 24, got %d", len(hash)) 58 58 }
+5 -4
internal/albertsons/cache.go
··· 1 1 package albertsons 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 - "careme/internal/sitemapfetch" 7 4 "context" 8 5 "encoding/json" 9 6 "errors" 10 7 "fmt" 11 8 "log/slog" 9 + 10 + "careme/internal/cache" 11 + locationtypes "careme/internal/locations/types" 12 + "careme/internal/sitemapfetch" 12 13 13 14 "github.com/samber/lo" 14 15 lop "github.com/samber/lo/parallel" ··· 50 51 return nil, fmt.Errorf("list cached store summaries: %w", err) 51 52 } 52 53 53 - //expensive. Just save a smaller map of centroids 54 + // expensive. Just save a smaller map of centroids 54 55 summaries := lop.Map(keys, func(key string, _ int) *StoreSummary { 55 56 reader, err := c.Get(ctx, StoreCachePrefix+key) 56 57 if err != nil {
+2 -1
internal/albertsons/cache_test.go
··· 1 1 package albertsons 2 2 3 3 import ( 4 - "careme/internal/cache" 5 4 "context" 6 5 "testing" 6 + 7 + "careme/internal/cache" 7 8 ) 8 9 9 10 func TestSaveStoreURLMapRoundTrip(t *testing.T) {
+3 -2
internal/albertsons/discovery.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/sitemapfetch" 6 5 "context" 7 6 "encoding/json" 8 7 "fmt" ··· 13 12 "slices" 14 13 "strconv" 15 14 "strings" 15 + 16 + "careme/internal/sitemapfetch" 16 17 ) 17 18 18 19 type Chain struct { ··· 348 349 } 349 350 } 350 351 351 - //seems fragile? 352 + // seems fragile? 352 353 lat, lon := extractGeoPosition(body) 353 354 354 355 return &StoreSummary{
+4 -3
internal/albertsons/locations.go
··· 1 1 package albertsons 2 2 3 3 import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + 4 8 "careme/internal/cache" 5 9 "careme/internal/config" 6 10 "careme/internal/locations/nearby" 7 11 locationtypes "careme/internal/locations/types" 8 - "context" 9 - "fmt" 10 - "strings" 11 12 ) 12 13 13 14 type centroidByZip interface {
+3 -2
internal/albertsons/locations_test.go
··· 1 1 package albertsons 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "strings" 8 6 "testing" 7 + 8 + "careme/internal/cache" 9 + locationtypes "careme/internal/locations/types" 9 10 ) 10 11 11 12 func TestNewLocationBackendBuildsIndexAndLookup(t *testing.T) {
+3 -2
internal/aldi/cache.go
··· 1 1 package aldi 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "encoding/json" 8 6 "errors" 9 7 "fmt" 10 8 "log/slog" 9 + 10 + "careme/internal/cache" 11 + locationtypes "careme/internal/locations/types" 11 12 12 13 "github.com/samber/lo" 13 14 lop "github.com/samber/lo/parallel"
+1 -1
internal/aldi/client.go
··· 18 18 StoreCachePrefix = "aldi/stores/" 19 19 LocationIDPrefix = "aldi_" 20 20 DefaultBaseURL = "https://locator.uberall.com" 21 - DefaultWidgetKey = "LETA2YVm6txbe0b9lS297XdxDX4qVQ" //what is this? 21 + DefaultWidgetKey = "LETA2YVm6txbe0b9lS297XdxDX4qVQ" // what is this? 22 22 DefaultLanguage = "en_US" 23 23 DefaultCountry = "US" 24 24 )
+4 -5
internal/aldi/locations.go
··· 1 1 package aldi 2 2 3 3 import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + 4 8 "careme/internal/cache" 5 9 "careme/internal/config" 6 10 "careme/internal/locations/nearby" 7 11 locationtypes "careme/internal/locations/types" 8 - "context" 9 - "fmt" 10 - "strings" 11 12 ) 12 13 13 14 type centroidByZip interface { ··· 32 33 return nil, fmt.Errorf("create ALDI list cache: %w", err) 33 34 } 34 35 return newLocationBackend(ctx, listCache, zipLookup) 35 - 36 36 } 37 37 38 38 func newLocationBackend(ctx context.Context, c cache.ListCache, zipLookup centroidByZip) (*LocationBackend, error) { 39 - 40 39 summaries, err := loadCachedStoreSummaries(ctx, c) 41 40 if err != nil { 42 41 return nil, err
+3 -2
internal/aldi/locations_test.go
··· 1 1 package aldi 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "strings" 8 6 "testing" 7 + 8 + "careme/internal/cache" 9 + locationtypes "careme/internal/locations/types" 9 10 ) 10 11 11 12 func TestNewLocationBackendBuildsIndexAndLookup(t *testing.T) {
+5 -9
internal/auth/clerk.go
··· 1 1 package auth 2 2 3 3 import ( 4 - "careme/internal/config" 5 - "careme/internal/templates" 6 4 "context" 7 5 "errors" 8 6 "fmt" ··· 12 10 "strings" 13 11 "time" 14 12 13 + "careme/internal/config" 14 + "careme/internal/templates" 15 + 15 16 "github.com/clerk/clerk-sdk-go/v2" 16 17 clerkhttp "github.com/clerk/clerk-sdk-go/v2/http" 17 18 "github.com/clerk/clerk-sdk-go/v2/jwks" ··· 19 20 "github.com/clerk/clerk-sdk-go/v2/user" 20 21 ) 21 22 22 - var ( 23 - ErrNoSession = errors.New("no valid session found") 24 - ) 23 + var ErrNoSession = errors.New("no valid session found") 25 24 26 25 type AuthClient interface { 27 26 GetUserEmail(ctx context.Context, clerkUserID string) (string, error) ··· 62 61 } 63 62 64 63 func (c *clerkClient) GetUserEmail(ctx context.Context, clerkUserID string) (string, error) { 65 - 66 - //todo can we pull this right off of claims? not woth bothering? 64 + // todo can we pull this right off of claims? not woth bothering? 67 65 clerkUser, err := c.userClient.Get(ctx, clerkUserID) 68 66 if err != nil { 69 67 return "", fmt.Errorf("failed to fetch clerk user: %w", err) ··· 106 104 107 105 // WithClerkHTTP wraps the http.Handler with Clerk's authentication middleware 108 106 func (c *clerkClient) WithAuthHTTP(handler http.Handler) http.Handler { 109 - 110 107 purgeAndRedirect := clerkhttp.AuthorizationFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 111 108 slog.Info("authorization failure, purging cookies and redirecting") 112 109 // Clear any existing Clerk cookies by setting them to expired ··· 117 114 })) 118 115 119 116 useSessionCookie := clerkhttp.AuthorizationJWTExtractor(func(r *http.Request) string { 120 - 121 117 if c, err := r.Cookie("__session"); err == nil { 122 118 return c.Value 123 119 }
+2 -1
internal/auth/mock.go
··· 1 1 package auth 2 2 3 3 import ( 4 - "careme/internal/config" 5 4 "context" 6 5 "net/http" 6 + 7 + "careme/internal/config" 7 8 ) 8 9 9 10 // Client wraps Clerk SDK functionality
+5 -3
internal/cache/file.go
··· 10 10 "strings" 11 11 ) 12 12 13 - var ErrNotFound = errors.New("cache entry not found") 14 - var ErrAlreadyExists = errors.New("cache entry already exists") 13 + var ( 14 + ErrNotFound = errors.New("cache entry not found") 15 + ErrAlreadyExists = errors.New("cache entry already exists") 16 + ) 15 17 16 18 type PutCondition uint8 17 19 ··· 113 115 func (fc *FileCache) Put(_ context.Context, key, value string, opts PutOptions) error { 114 116 fullPath := filepath.Join(fc.Dir, key) 115 117 dir := filepath.Dir(fullPath) 116 - if err := os.MkdirAll(dir, 0755); err != nil { 118 + if err := os.MkdirAll(dir, 0o755); err != nil { 117 119 return err 118 120 } 119 121
+4 -2
internal/cache/memory.go
··· 14 14 data map[string]string 15 15 } 16 16 17 - var _ Cache = (*InMemoryCache)(nil) 18 - var _ ListCache = (*InMemoryCache)(nil) 17 + var ( 18 + _ Cache = (*InMemoryCache)(nil) 19 + _ ListCache = (*InMemoryCache)(nil) 20 + ) 19 21 20 22 func NewInMemoryCache() *InMemoryCache { 21 23 return &InMemoryCache{
+5 -3
internal/config/config.go
··· 96 96 type WalmartConfig struct { 97 97 ConsumerID string 98 98 KeyVersion string 99 - PrivateKey string //base 64 the ssh key you give to Walmart (eg bas64 -w0 keys/walmart_prod) 99 + PrivateKey string // base 64 the ssh key you give to Walmart (eg bas64 -w0 keys/walmart_prod) 100 100 BaseURL string 101 101 HTTPClient *http.Client 102 102 } ··· 105 105 return c.ConsumerID != "" && c.PrivateKey != "" 106 106 } 107 107 108 - var localhostSigninRedirect = "?redirect_url=http://localhost:8080/auth/establish" 109 - var localhostSignupRedirect = "?redirect_url=http://localhost:8080/auth/establish?signup=true" 108 + var ( 109 + localhostSigninRedirect = "?redirect_url=http://localhost:8080/auth/establish" 110 + localhostSignupRedirect = "?redirect_url=http://localhost:8080/auth/establish?signup=true" 111 + ) 110 112 111 113 // move to auth pacakage? 112 114 func (c *ClerkConfig) Signin() string {
+4 -3
internal/heb/cache.go
··· 1 1 package heb 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 - "careme/internal/sitemapfetch" 7 4 "context" 8 5 "encoding/json" 9 6 "errors" 10 7 "fmt" 11 8 "log/slog" 9 + 10 + "careme/internal/cache" 11 + locationtypes "careme/internal/locations/types" 12 + "careme/internal/sitemapfetch" 12 13 13 14 "github.com/samber/lo" 14 15 lop "github.com/samber/lo/parallel"
+2 -1
internal/heb/cache_test.go
··· 1 1 package heb 2 2 3 3 import ( 4 - "careme/internal/cache" 5 4 "context" 6 5 "testing" 6 + 7 + "careme/internal/cache" 7 8 ) 8 9 9 10 func TestSaveStoreURLMapRoundTrip(t *testing.T) {
+4 -3
internal/heb/locations.go
··· 1 1 package heb 2 2 3 3 import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + 4 8 "careme/internal/cache" 5 9 "careme/internal/config" 6 10 "careme/internal/locations/nearby" 7 11 locationtypes "careme/internal/locations/types" 8 - "context" 9 - "fmt" 10 - "strings" 11 12 ) 12 13 13 14 type centroidByZip interface {
+3 -2
internal/heb/locations_test.go
··· 1 1 package heb 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "strings" 8 6 "testing" 7 + 8 + "careme/internal/cache" 9 + locationtypes "careme/internal/locations/types" 9 10 ) 10 11 11 12 func TestNewLocationBackendBuildsIndexAndLookup(t *testing.T) {
+4 -3
internal/ingredients/server.go
··· 1 1 package ingredients 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/kroger" 6 - "careme/internal/recipes" 7 4 "encoding/json" 8 5 "errors" 9 6 "log/slog" 10 7 "net/http" 8 + 9 + "careme/internal/cache" 10 + "careme/internal/kroger" 11 + "careme/internal/recipes" 11 12 ) 12 13 13 14 type server struct {
+5 -4
internal/ingredients/server_test.go
··· 1 1 package ingredients 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/kroger" 6 - "careme/internal/locations" 7 - "careme/internal/recipes" 8 4 "net/http" 9 5 "net/http/httptest" 10 6 "strings" 11 7 "testing" 12 8 "time" 9 + 10 + "careme/internal/cache" 11 + "careme/internal/kroger" 12 + "careme/internal/locations" 13 + "careme/internal/recipes" 13 14 ) 14 15 15 16 func TestServerReturnsIngredientsJSON(t *testing.T) {
+3 -3
internal/kroger/client.go
··· 1 1 package kroger 2 2 3 3 import ( 4 - "careme/internal/config" 5 - 6 4 "context" 7 5 "encoding/json" 8 6 "fmt" ··· 12 10 "strings" 13 11 "sync" 14 12 "time" 13 + 14 + "careme/internal/config" 15 15 ) 16 16 17 17 //go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml swagger.yaml ··· 67 67 endpoint := "https://api.kroger.com/v1/connect/oauth2/token" 68 68 data := url.Values{} 69 69 data.Set("grant_type", "client_credentials") 70 - data.Set("scope", "product.compact") //wierd for location? 70 + data.Set("scope", "product.compact") // wierd for location? 71 71 72 72 req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(data.Encode())) 73 73 if err != nil {
+2 -1
internal/kroger/locations.go
··· 1 1 package kroger 2 2 3 3 import ( 4 - locationtypes "careme/internal/locations/types" 5 4 "context" 6 5 "fmt" 6 + 7 + locationtypes "careme/internal/locations/types" 7 8 ) 8 9 9 10 const chainName = "kroger"
+2 -1
internal/kroger/locations_test.go
··· 1 1 package kroger 2 2 3 3 import ( 4 + "testing" 5 + 4 6 locationtypes "careme/internal/locations/types" 5 - "testing" 6 7 ) 7 8 8 9 func TestClientWithResponsesIsID(t *testing.T) {
+2 -3
internal/kroger/staples.go
··· 1 1 package kroger 2 2 3 3 import ( 4 - "careme/internal/parallelism" 5 4 "context" 6 5 "encoding/json" 7 6 "fmt" ··· 9 8 "net/http" 10 9 "slices" 11 10 "strconv" 11 + 12 + "careme/internal/parallelism" 12 13 13 14 "github.com/samber/lo" 14 15 ) ··· 55 56 56 57 func (p StaplesProvider) FetchStaples(ctx context.Context, locationID string) ([]Ingredient, error) { 57 58 return parallelism.Flatten(defaultStaples(), func(category staplesFilter) ([]Ingredient, error) { 58 - 59 59 ingredients, err := searchIngredients(ctx, p.client, locationID, category.Term, category.Brands, category.Frozen, 0) 60 60 slog.InfoContext(ctx, "Found ingredients for category", "count", len(ingredients), "category", category.Term, "location", locationID) 61 61 return ingredients, err 62 62 }) 63 - 64 63 } 65 64 66 65 func (p StaplesProvider) GetIngredients(ctx context.Context, locationID string, searchTerm string, skip int) ([]Ingredient, error) {
+6 -6
internal/kroger/type.go
··· 12 12 ProductId *string `json:"id,omitempty"` 13 13 AisleNumber *string `json:"number,omitempty"` 14 14 Brand *string `json:"brand,omitempty"` 15 - //CountryOrigin *string `json:"countryOrigin,omitempty"` 15 + // CountryOrigin *string `json:"countryOrigin,omitempty"` 16 16 Description *string `json:"description,omitempty"` 17 - //Favorite *bool `json:"favorite,omitempty"` //what does this mean? 18 - //InventoryStockLevel *string `json:"stockLevel,omitempty"` 17 + // Favorite *bool `json:"favorite,omitempty"` //what does this mean? 18 + // InventoryStockLevel *string `json:"stockLevel,omitempty"` 19 19 PriceSale *float32 `json:"salePrice,omitempty"` 20 20 PriceRegular *float32 `json:"regularPrice,omitempty"` 21 21 Size *string `json:"size,omitempty"` 22 - //not used by llm. 22 + // not used by llm. 23 23 Categories *[]string `json:"categories,omitempty"` 24 - //Figure out what is in taxonomies 24 + // Figure out what is in taxonomies 25 25 } 26 26 27 27 // this is what we'll actually pass to the llm ··· 44 44 toStr(i.Size), 45 45 floatToStr(i.PriceRegular), 46 46 floatToStr(i.PriceSale), 47 - //todo add a dicount? 47 + // todo add a dicount? 48 48 } 49 49 if len(header) != len(row) { 50 50 return fmt.Errorf("header and row length mismatch: %d vs %d", len(header), len(row))
+4 -3
internal/locations/albertsons_test.go
··· 1 1 package locations 2 2 3 3 import ( 4 + "context" 5 + "os" 6 + "testing" 7 + 4 8 "careme/internal/albertsons" 5 9 "careme/internal/cache" 6 10 "careme/internal/config" 7 - "context" 8 - "os" 9 - "testing" 10 11 ) 11 12 12 13 func TestNewAddsAlbertsonsBackendWhenEnabled(t *testing.T) {
+4 -3
internal/locations/aldi_test.go
··· 1 1 package locations 2 2 3 3 import ( 4 + "context" 5 + "os" 6 + "testing" 7 + 4 8 "careme/internal/aldi" 5 9 "careme/internal/cache" 6 10 "careme/internal/config" 7 - "context" 8 - "os" 9 - "testing" 10 11 ) 11 12 12 13 func TestNewAddsALDIBackendWhenEnabled(t *testing.T) {
+4 -3
internal/locations/heb_test.go
··· 1 1 package locations 2 2 3 3 import ( 4 + "context" 5 + "os" 6 + "testing" 7 + 4 8 "careme/internal/cache" 5 9 "careme/internal/config" 6 10 "careme/internal/heb" 7 - "context" 8 - "os" 9 - "testing" 10 11 ) 11 12 12 13 func TestNewAddsHEBBackendWhenEnabled(t *testing.T) {
+24 -23
internal/locations/locations.go
··· 1 1 package locations 2 2 3 3 import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "html/template" 9 + "io" 10 + "log/slog" 11 + "net/http" 12 + "net/url" 13 + "sort" 14 + "strconv" 15 + "sync" 16 + "time" 17 + 4 18 "careme/internal/albertsons" 5 19 "careme/internal/aldi" 6 20 "careme/internal/auth" ··· 16 30 utypes "careme/internal/users/types" 17 31 "careme/internal/walmart" 18 32 "careme/internal/wholefoods" 19 - "context" 20 - "encoding/json" 21 - "errors" 22 - "fmt" 23 - "html/template" 24 - "io" 25 - "log/slog" 26 - "net/http" 27 - "net/url" 28 - "sort" 29 - "strconv" 30 - "sync" 31 - "time" 32 33 ) 33 34 34 35 type userLookup interface { ··· 42 43 } 43 44 44 45 // bad for rural areas if zip code is huge? 45 - const maxLocationDistanceMiles = 20.0 46 - const locationCachePrefix = "location/" 46 + const ( 47 + maxLocationDistanceMiles = 20.0 48 + locationCachePrefix = "location/" 49 + ) 47 50 48 51 type locationServer struct { 49 52 storage locationGetter ··· 77 80 return nil, fmt.Errorf("cache is required") 78 81 } 79 82 if cfg.Mocks.Enable { 80 - //should probably have something else return th mock so we can just return concerete type here. 83 + // should probably have something else return th mock so we can just return concerete type here. 81 84 return mock{}, nil 82 85 } 83 86 ··· 110 113 zipCentroids: centroids, 111 114 cache: c, 112 115 }, nil 113 - 114 116 } 115 117 116 118 func NewServer(storage locationGetter, zipFetcher zipFetcher, userStorage userLookup) *locationServer { ··· 147 149 } 148 150 149 151 func (l *locationStorage) GetLocationsByZip(ctx context.Context, zipcode string) ([]Location, error) { 150 - 151 152 results := make(chan []Location, len(l.client)) 152 153 errors := make(chan error, len(l.client)) 153 154 var wg sync.WaitGroup ··· 187 188 188 189 requestedCentroid, hasRequestedCentroid := l.zipCentroids.ZipCentroidByZIP(zipcode) 189 190 if !hasRequestedCentroid { 190 - //were missign zip codes. Make this an error later? 191 + // were missign zip codes. Make this an error later? 191 192 slog.WarnContext(ctx, "requested zip has no centroid; returning unsorted locations without distance filter", "zip", zipcode, "count", len(allLocations)) 192 193 return allLocations, nil 193 194 } ··· 235 236 } 236 237 237 238 func (l *locationStorage) storeLocationIfMissing(loc Location) error { 238 - //itentionally giving its own context so its not canceled 239 + // itentionally giving its own context so its not canceled 239 240 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 240 241 defer cancel() 241 242 loc.CachedAt = time.Now().UTC() ··· 252 253 if err != nil { 253 254 return fmt.Errorf("failed to marshal location for cache: %w", err) 254 255 } 255 - //TODO clean out old ones? 256 + // TODO clean out old ones? 256 257 if err := l.cache.Put(ctx, id, string(locationJSON), cache.IfNoneMatch()); err != nil && !errors.Is(err, cache.ErrAlreadyExists) { 257 258 return err 258 259 } ··· 277 278 return *loc.Lat, *loc.Lon 278 279 } 279 280 280 - //do we actualyl want to fall back? 281 + // do we actualyl want to fall back? 281 282 centroid, _ := zipCentroids.ZipCentroidByZIP(loc.ZipCode) 282 283 return centroid.Lat, centroid.Lon 283 284 } 284 285 285 286 func (l *locationServer) Ready(ctx context.Context) error { 286 - _, err := l.storage.GetLocationsByZip(ctx, "98005") //magic number is my zip code :) 287 + _, err := l.storage.GetLocationsByZip(ctx, "98005") // magic number is my zip code :) 287 288 return err 288 289 } 289 290
+2 -1
internal/locations/locations_test.go
··· 1 1 package locations 2 2 3 3 import ( 4 - cachepkg "careme/internal/cache" 5 4 "context" 6 5 "encoding/json" 7 6 "fmt" 8 7 "io" 9 8 "testing" 10 9 "time" 10 + 11 + cachepkg "careme/internal/cache" 11 12 ) 12 13 13 14 func TestGetLocationByIDUsesCache(t *testing.T) {
+4 -3
internal/locations/mock.go
··· 1 1 package locations 2 2 3 3 import ( 4 - "careme/internal/auth" 5 - "careme/internal/seasons" 6 - "careme/internal/templates" 7 4 "context" 8 5 "fmt" 9 6 "html/template" 10 7 "net/http" 8 + 9 + "careme/internal/auth" 10 + "careme/internal/seasons" 11 + "careme/internal/templates" 11 12 12 13 "github.com/samber/lo" 13 14 )
+3 -2
internal/locations/nearby/nearby.go
··· 1 1 package nearby 2 2 3 3 import ( 4 - "careme/internal/locations/geo" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "log/slog" 8 6 "sort" 9 7 "strings" 8 + 9 + "careme/internal/locations/geo" 10 + locationtypes "careme/internal/locations/types" 10 11 ) 11 12 12 13 type CentroidLookup interface {
+1
internal/locations/types/errors.go
··· 10 10 func DisabledBackendError(backend string) error { 11 11 return fmt.Errorf("%w: %s", errDisabledBackend, backend) 12 12 } 13 + 13 14 func IsDisabledBackendError(err error) bool { 14 15 return errors.Is(err, errDisabledBackend) 15 16 }
+4 -3
internal/locations/wholefoods_test.go
··· 1 1 package locations 2 2 3 3 import ( 4 + "context" 5 + "os" 6 + "testing" 7 + 4 8 "careme/internal/cache" 5 9 "careme/internal/config" 6 10 "careme/internal/wholefoods" 7 - "context" 8 - "os" 9 - "testing" 10 11 ) 11 12 12 13 func TestNewAddsWholeFoodsBackendWhenEnabled(t *testing.T) {
+3 -2
internal/locations/zip_centroids.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/locations/geo" 6 - locationtypes "careme/internal/locations/types" 7 5 _ "embed" 8 6 "encoding/csv" 9 7 "errors" 10 8 "strconv" 11 9 "strings" 10 + 11 + "careme/internal/locations/geo" 12 + locationtypes "careme/internal/locations/types" 12 13 ) 13 14 14 15 type zipCentroidIndex struct {
+2 -1
internal/locations/zip_centroids_test.go
··· 1 1 package locations 2 2 3 3 import ( 4 + "testing" 5 + 4 6 locationtypes "careme/internal/locations/types" 5 - "testing" 6 7 ) 7 8 8 9 func TestZipCentroidByZIP_KnownZip(t *testing.T) {
+1 -2
internal/logsetup/logger.go
··· 13 13 const AppInsightsConnectionStringEnv = "APPLICATIONINSIGHTS_CONNECTION_STRING" 14 14 15 15 func Configure(ctx context.Context) (func(), error) { 16 - 17 16 handlers := []slog.Handler{slog.NewTextHandler(os.Stdout, nil)} 18 17 19 - closeFn := func() {} //can be a list if we have multiple 18 + closeFn := func() {} // can be a list if we have multiple 20 19 21 20 if connectionString := os.Getenv(AppInsightsConnectionStringEnv); connectionString != "" { 22 21 handler, err := appinsights.NewHandler(connectionString, nil)
+8 -7
internal/mail/mail.go
··· 5 5 6 6 import ( 7 7 "bytes" 8 - "careme/internal/cache" 9 - "careme/internal/config" 10 - "careme/internal/locations" 11 - "careme/internal/recipes" 12 - "careme/internal/users" 13 - utypes "careme/internal/users/types" 14 8 "context" 15 9 "encoding/json" 16 10 "errors" ··· 19 13 "net/http" 20 14 "os" 21 15 "time" 16 + 17 + "careme/internal/cache" 18 + "careme/internal/config" 19 + "careme/internal/locations" 20 + "careme/internal/recipes" 21 + "careme/internal/users" 22 + utypes "careme/internal/users/types" 22 23 23 24 "github.com/sendgrid/rest" 24 25 "github.com/sendgrid/sendgrid-go" ··· 168 169 } 169 170 } 170 171 171 - //can orphan recipes here with crash or shutdown. Params should have a start time 172 + // can orphan recipes here with crash or shutdown. Params should have a start time 172 173 173 174 shoppingList, err = m.generator.GenerateRecipes(ctx, p) 174 175 if err != nil {
-2
internal/mail/mail_test.go
··· 102 102 } 103 103 104 104 func TestSendEmail_DoesNotRecordSentClaimOnNonSuccessSendGridStatus(t *testing.T) { 105 - 106 105 fc := newFakeMailCache(t) 107 106 location := &locations.Location{ID: "123", Name: "Test Store", Address: "123 Test St", ZipCode: "98005"} 108 107 m := &mailer{ ··· 131 130 } 132 131 133 132 func TestSendEmail_RecordsSentClaimOnSuccessSendGridStatus(t *testing.T) { 134 - 135 133 fc := newFakeMailCache(t) 136 134 location := &locations.Location{ID: "123", Name: "Test Store", Address: "123 Test St", ZipCode: "98005"} 137 135 m := &mailer{
+3 -2
internal/publix/cache.go
··· 1 1 package publix 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "encoding/json" 8 6 "errors" 9 7 "fmt" 10 8 "log/slog" 11 9 "slices" 10 + 11 + "careme/internal/cache" 12 + locationtypes "careme/internal/locations/types" 12 13 13 14 "github.com/samber/lo" 14 15 lop "github.com/samber/lo/parallel"
+1 -1
internal/publix/client.go
··· 102 102 return nil, fmt.Errorf("request %q: %w", endpoint, err) 103 103 } 104 104 defer func() { 105 - //why do we need to copy to discard here? 105 + // why do we need to copy to discard here? 106 106 // Because some servers (including Cloudflare) will not close the connection 107 107 // if the response body is not fully read, which can lead to resource leaks and 108 108 // exhaustion of available connections in the HTTP client's connection pool.
+4 -4
internal/publix/locations.go
··· 1 1 package publix 2 2 3 3 import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + 4 8 "careme/internal/cache" 5 9 "careme/internal/config" 6 10 "careme/internal/locations/nearby" 7 11 locationtypes "careme/internal/locations/types" 8 - "context" 9 - "fmt" 10 - "strings" 11 12 ) 12 13 13 14 type centroidByZip interface { ··· 37 38 } 38 39 39 40 func newLocationBackend(ctx context.Context, c cache.ListCache, zipLookup centroidByZip) (*LocationBackend, error) { 40 - 41 41 summaries, err := loadCachedStoreSummaries(ctx, c) 42 42 if err != nil { 43 43 return nil, err
+3 -2
internal/publix/locations_test.go
··· 1 1 package publix 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "strings" 8 6 "testing" 7 + 8 + "careme/internal/cache" 9 + locationtypes "careme/internal/locations/types" 9 10 ) 10 11 11 12 func TestNewLocationBackendBuildsIndexAndLookup(t *testing.T) {
+3 -2
internal/recipes/buttons_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/locations" 6 4 "net/http/httptest" 7 5 "strings" 8 6 "testing" 9 7 "time" 8 + 9 + "careme/internal/ai" 10 + "careme/internal/locations" 10 11 ) 11 12 12 13 // Test that the HTML contains Save and Dismiss buttons for recipes.
+2 -1
internal/recipes/feedback.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/cache" 5 4 "context" 6 5 "encoding/json" 7 6 "log/slog" 8 7 "time" 8 + 9 + "careme/internal/cache" 9 10 10 11 "github.com/samber/lo" 11 12 )
+13 -12
internal/recipes/generator.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 - "careme/internal/config" 7 - "careme/internal/kroger" 8 - "careme/internal/locations" 9 - "careme/internal/parallelism" 10 - "careme/internal/wholefoods" 11 4 "context" 12 5 "encoding/base64" 13 6 "errors" ··· 18 11 "strings" 19 12 "time" 20 13 14 + "careme/internal/ai" 15 + "careme/internal/cache" 16 + "careme/internal/config" 17 + "careme/internal/kroger" 18 + "careme/internal/locations" 19 + "careme/internal/parallelism" 20 + "careme/internal/wholefoods" 21 + 21 22 "github.com/samber/lo" 22 23 "github.com/samber/lo/mutable" 23 24 ) ··· 64 65 var styles []string 65 66 for _, style := range recipe.WineStyles { 66 67 style = strings.TrimSpace(style) 67 - if style != "" { //would this ever happen? 68 + if style != "" { // would this ever happen? 68 69 styles = append(styles, style) 69 70 } 70 71 } 71 72 72 - //whole foods search not actually implmented hard code categories 73 + // whole foods search not actually implmented hard code categories 73 74 if wholefoods.NewIdentityProvider().IsID(location) { 74 - styles = []string{"red-wine", "white-wine", "sparkling"} //rose 75 + styles = []string{"red-wine", "white-wine", "sparkling"} // rose 75 76 } 76 77 77 78 if len(styles) == 0 { ··· 158 159 // should never happen? How do you get save on first generte? 159 160 // shoppingList.Recipes = append(shoppingList.Recipes, p.Saved...) 160 161 161 - //TODO this does not get saved in params and thus must be loaded from html 162 + // TODO this does not get saved in params and thus must be loaded from html 162 163 // could update params after first generation or pregenerate before we save params. 163 164 p.ConversationID = shoppingList.ConversationID 164 165 slog.InfoContext(ctx, "generated chat", "location", p.String(), "duration", time.Since(start), "hash", hash) ··· 185 186 if err != nil { 186 187 return nil, fmt.Errorf("failed to get ingredients for staples: %w", err) 187 188 } 188 - //should this be pushed down into staple proivder? go off product id? 189 + // should this be pushed down into staple proivder? go off product id? 189 190 ingredients = uniqueByDescription(ingredients) 190 191 191 192 mutable.Shuffle(ingredients)
+5 -4
internal/recipes/generator_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 - "careme/internal/kroger" 7 - "careme/internal/locations" 8 4 "context" 9 5 "slices" 10 6 "sync" 11 7 "testing" 12 8 "time" 9 + 10 + "careme/internal/ai" 11 + "careme/internal/cache" 12 + "careme/internal/kroger" 13 + "careme/internal/locations" 13 14 ) 14 15 15 16 type captureWineQuestionAIClient struct {
+8 -7
internal/recipes/html.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/locations" 6 - "careme/internal/seasons" 7 - "careme/internal/templates" 8 4 "html/template" 9 5 "io" 10 6 "net/http" 11 7 "slices" 12 8 "strings" 9 + 10 + "careme/internal/ai" 11 + "careme/internal/locations" 12 + "careme/internal/seasons" 13 + "careme/internal/templates" 13 14 ) 14 15 15 16 // shoppingRecipeView is a thin wrapper around ai.Recipe for the shopping list page. ··· 21 22 type shoppingRecipeView struct { 22 23 ai.Recipe 23 24 Hash string 24 - DisplayIngredients []ai.Ingredient //merged food and wine 25 - Dismissed bool //saved already in recipe 25 + DisplayIngredients []ai.Ingredient // merged food and wine 26 + Dismissed bool // saved already in recipe 26 27 Wine shoppingRecipeWineView 27 28 } 28 29 ··· 153 154 154 155 // FormatRecipeThreadHTML renders the question thread fragment for HTMX swaps. 155 156 func FormatRecipeThreadHTML(thread []RecipeThreadEntry, signedIn bool, conversationID string, writer http.ResponseWriter) { 156 - //memory waste because we alwways resort? 157 + // memory waste because we alwways resort? 157 158 slices.SortFunc(thread, func(i, j RecipeThreadEntry) int { 158 159 return j.CreatedAt.Compare(i.CreatedAt) 159 160 })
+5 -4
internal/recipes/html_test.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/ai" 6 - "careme/internal/config" 7 - "careme/internal/locations" 8 - "careme/internal/templates" 9 5 "net/http" 10 6 "net/http/httptest" 11 7 "os" 12 8 "strings" 13 9 "testing" 14 10 "time" 11 + 12 + "careme/internal/ai" 13 + "careme/internal/config" 14 + "careme/internal/locations" 15 + "careme/internal/templates" 15 16 16 17 "golang.org/x/net/html" 17 18 )
+12 -10
internal/recipes/io.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 - "careme/internal/kroger" 7 4 "context" 8 5 "encoding/json" 9 6 "errors" 10 7 "fmt" 11 8 "log/slog" 12 9 10 + "careme/internal/ai" 11 + "careme/internal/cache" 12 + "careme/internal/kroger" 13 + 13 14 "github.com/samber/lo" 14 15 ) 15 16 16 - const recipeCachePrefix = "recipe/" 17 - const ShoppingListCachePrefix = "shoppinglist/" 18 - const ingredientsCachePrefix = "ingredients/" 19 - const paramsCachePrefix = "params/" 17 + const ( 18 + recipeCachePrefix = "recipe/" 19 + ShoppingListCachePrefix = "shoppinglist/" 20 + ingredientsCachePrefix = "ingredients/" 21 + paramsCachePrefix = "params/" 22 + ) 20 23 21 24 type recipeio struct { 22 25 Cache cache.Cache ··· 50 53 shoppinglist, err := rio.Cache.Get(ctx, primaryKey) 51 54 if err != nil { 52 55 return nil, fmt.Errorf("error getting shopping list for hash %s: %w", hash, err) 53 - 54 56 } 55 57 defer func() { 56 58 if err := shoppinglist.Close(); err != nil { ··· 71 73 72 74 func (rio recipeio) ParamsFromCache(ctx context.Context, hash string) (*generatorParams, error) { 73 75 primaryKey := paramsCachePrefix + hash 74 - //have to convert legacy hashes because each recipe stored an origin hash and we didn't rewrite them 76 + // have to convert legacy hashes because each recipe stored an origin hash and we didn't rewrite them 75 77 paramsReader, err := rio.Cache.Get(ctx, primaryKey) 76 78 if err != nil { 77 79 return nil, fmt.Errorf("error getting params for hash %s: %w", hash, err) ··· 90 92 } 91 93 92 94 func (rio recipeio) IngredientsFromCache(ctx context.Context, hash string) ([]kroger.Ingredient, error) { 93 - //honor legacy hashes? I don't think so gets converted in server 95 + // honor legacy hashes? I don't think so gets converted in server 94 96 primaryKey := ingredientsCachePrefix + hash 95 97 ingredientBlob, err := rio.Cache.Get(ctx, primaryKey) 96 98 if err != nil {
+5 -4
internal/recipes/io_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 - "careme/internal/kroger" 7 - "careme/internal/locations" 8 4 "errors" 9 5 "os" 10 6 "path/filepath" 11 7 "sync" 12 8 "testing" 13 9 "time" 10 + 11 + "careme/internal/ai" 12 + "careme/internal/cache" 13 + "careme/internal/kroger" 14 + "careme/internal/locations" 14 15 ) 15 16 16 17 func TestSaveParams_IsAtomic(t *testing.T) {
+2 -1
internal/recipes/mock.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 4 "context" 6 5 "fmt" 7 6 "log/slog" 8 7 "math/rand" 9 8 "time" 9 + 10 + "careme/internal/ai" 10 11 11 12 "github.com/google/uuid" 12 13 )
+2 -1
internal/recipes/mock_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/locations" 5 4 "context" 6 5 "testing" 7 6 "time" 7 + 8 + "careme/internal/locations" 8 9 ) 9 10 10 11 func TestMockGenerateRecipes_Returns3Recipes(t *testing.T) {
+5 -4
internal/recipes/params.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/ai" 6 - "careme/internal/locations" 7 5 "context" 8 6 "encoding/base64" 9 7 "errors" ··· 14 12 "net/http" 15 13 "strings" 16 14 "time" 15 + 16 + "careme/internal/ai" 17 + "careme/internal/locations" 17 18 18 19 "github.com/samber/lo" 19 20 ) ··· 30 31 Location *locations.Location `json:"location,omitempty"` 31 32 Date time.Time `json:"date,omitempty"` 32 33 // People int 33 - //per round instuctions 34 + // per round instuctions 34 35 Instructions string `json:"instructions,omitempty"` 35 36 Directive string `json:"directive,omitempty"` // this is the new one that will be used. Can remove GenerationPrompt after a while. 36 37 LastRecipes []string `json:"last_recipes,omitempty"` 37 38 // UserID string `json:"user_id,omitempty"` 38 39 ConversationID string `json:"conversation_id,omitempty"` // Can remove if we pass it in separately to generate recipes? 39 - //TODO Both should just be title and hash insread of full ai.Recipe 40 + // TODO Both should just be title and hash insread of full ai.Recipe 40 41 Saved []ai.Recipe `json:"saved_recipes,omitempty"` 41 42 Dismissed []ai.Recipe `json:"dismissed_recipes,omitempty"` 42 43 }
+2 -1
internal/recipes/params_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/locations" 5 4 "context" 6 5 "net/http/httptest" 7 6 "testing" 8 7 "time" 8 + 9 + "careme/internal/locations" 9 10 ) 10 11 11 12 type staticLocationLookup struct {
+4 -3
internal/recipes/selection.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 4 "context" 7 5 "encoding/json" 8 6 "errors" 9 7 "fmt" 10 8 "strings" 11 9 "time" 10 + 11 + "careme/internal/ai" 12 + "careme/internal/cache" 12 13 13 14 "github.com/samber/lo" 14 15 ) ··· 92 93 if err != nil { 93 94 return fmt.Errorf("failed to marshal recipe selection: %w", err) 94 95 } 95 - //good place for etags :) 96 + // good place for etags :) 96 97 if err := s.Cache.Put(ctx, recipeSelectionKey(userID, originHash), string(body), cache.Unconditional()); err != nil { 97 98 return fmt.Errorf("failed to save recipe selection: %w", err) 98 99 }
+23 -23
internal/recipes/server.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/ai" 6 - "careme/internal/auth" 7 - "careme/internal/cache" 8 - "careme/internal/config" 9 - "careme/internal/locations" 10 - "careme/internal/seasons" 11 - "careme/internal/templates" 12 - "careme/internal/users" 13 - utypes "careme/internal/users/types" 14 5 "context" 15 6 "errors" 16 7 "fmt" ··· 22 13 "strings" 23 14 "sync" 24 15 "time" 16 + 17 + "careme/internal/ai" 18 + "careme/internal/auth" 19 + "careme/internal/cache" 20 + "careme/internal/config" 21 + "careme/internal/locations" 22 + "careme/internal/seasons" 23 + "careme/internal/templates" 24 + "careme/internal/users" 25 + utypes "careme/internal/users/types" 25 26 26 27 "github.com/samber/lo" 27 28 ) ··· 146 147 FormatRecipeHTML(p, *recipe, signedIn, thread, feedback, wineRecommendation, w) 147 148 return 148 149 } 149 - //we didn't go back and update old recipes's with new hash so have to handle that here. Could still backfill 150 + // we didn't go back and update old recipes's with new hash so have to handle that here. Could still backfill 150 151 if normalizedHash, ok := legacyHashToCurrent(recipe.OriginHash, legacyRecipeHashSeed); ok { 151 152 slog.InfoContext(ctx, "normalized legacy origin hash to current hash", "origin_hash", recipe.OriginHash, "hash", normalizedHash) 152 153 recipe.OriginHash = normalizedHash 153 - //could resave to backfill but don't think we'll ever get them all without looping 154 + // could resave to backfill but don't think we'll ever get them all without looping 154 155 } 155 156 p, err := s.ParamsFromCache(ctx, recipe.OriginHash) 156 157 if err != nil { 157 158 slog.ErrorContext(ctx, "failed to load params for hash", "hash", recipe.OriginHash, "error", err) 158 - //http.Error(w, "recipe not found or expired", http.StatusNotFound) 159 - //return 159 + // http.Error(w, "recipe not found or expired", http.StatusNotFound) 160 + // return 160 161 p = DefaultParams(&locations.Location{ 161 162 ID: "", 162 163 Name: "Unknown Location", ··· 217 218 return 218 219 } 219 220 220 - //this is going to take a while. Start a go routine? and spin? 221 + // this is going to take a while. Start a go routine? and spin? 221 222 // can't use request context because it will be canceled when request finishes but we want to finish processing question and save it to cache. 222 223 ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 45*time.Second) 223 224 defer cancel() ··· 443 444 return 444 445 } 445 446 446 - //could pass this in with htmx instead of loading title 447 + // could pass this in with htmx instead of loading title 447 448 recipe, err := s.SingleFromCache(ctx, recipeHash) 448 449 if err != nil { 449 450 if errors.Is(err, cache.ErrNotFound) { ··· 604 605 http.Error(w, "failed to prepare regeneration", http.StatusInternalServerError) 605 606 return 606 607 } 607 - //so we have a choice we could save slection here matching params 608 + // so we have a choice we could save slection here matching params 608 609 // or backfill it on first load after regeneration Backfilling is a little more resilient 609 - //selection := recipeSelectionFromParams(p) 610 - //if err := s.saveRecipeSelection(ctx, currentUser.ID, newHash, selection); 610 + // selection := recipeSelectionFromParams(p) 611 + // if err := s.saveRecipeSelection(ctx, currentUser.ID, newHash, selection); 611 612 s.kickgeneration(ctx, p, currentUser) 612 613 613 614 redirectToHash(w, r, newHash, true /*useStart*/) ··· 640 641 return 641 642 } 642 643 if len(p.Saved) == 0 { 643 - //ui does not allow this 644 + // ui does not allow this 644 645 slog.ErrorContext(ctx, "Got zero saved recipes finalize", "hash", hash) 645 646 http.Error(w, "no recipes selected to save", http.StatusBadRequest) 646 647 return ··· 679 680 680 681 selection, err := s.loadRecipeSelection(ctx, userID, hash) 681 682 if err != nil { 682 - //should we just fall back to params? selection saving 683 + // should we just fall back to params? selection saving 683 684 return nil, fmt.Errorf("failed to load recipe selection") 684 685 } 685 686 ··· 700 701 func (s *server) notFound(ctx context.Context, w http.ResponseWriter, r *http.Request) { 701 702 startArg := r.URL.Query().Get(queryArgStart) 702 703 hashParam := r.URL.Query().Get(queryArgHash) 703 - //okay give them a new start time. 704 + // okay give them a new start time. 704 705 if startArg == "" { 705 706 redirectToHash(w, r, hashParam, true /*useStart*/) 706 707 return ··· 834 835 } 835 836 836 837 p.Directive = currentUser.Directive 837 - //if params are already saved redirect and assume someone kicks off genration 838 + // if params are already saved redirect and assume someone kicks off genration 838 839 839 840 if err := s.SaveParams(ctx, p); err != nil { 840 841 if errors.Is(err, ErrAlreadyExists) { ··· 997 998 } 998 999 999 1000 func (s *server) removeRecipeFromUserProfile(ctx context.Context, currentUser utypes.User, recipeHash string) error { 1000 - 1001 1001 recipeHash = strings.TrimSpace(recipeHash) 1002 1002 if recipeHash == "" { 1003 1003 return fmt.Errorf("invalid recipe hash")
+8 -6
internal/recipes/server_test.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 - "careme/internal/ai" 6 - "careme/internal/auth" 7 - "careme/internal/cache" 8 - "careme/internal/locations" 9 - "careme/internal/users" 10 - utypes "careme/internal/users/types" 11 5 "context" 12 6 "encoding/base64" 13 7 "fmt" ··· 18 12 "strings" 19 13 "testing" 20 14 "time" 15 + 16 + "careme/internal/ai" 17 + "careme/internal/auth" 18 + "careme/internal/cache" 19 + "careme/internal/locations" 20 + "careme/internal/users" 21 + utypes "careme/internal/users/types" 21 22 ) 22 23 23 24 func TestRedirectToHash(t *testing.T) { ··· 41 42 t.Errorf("handler returned wrong location: got %v want prefix %v", location, expectedLocation) 42 43 } 43 44 } 45 + 44 46 func legacyRecipeHash(hash string) (string, bool) { 45 47 return currentHashToLegacy(hash, legacyRecipeHashSeed) 46 48 }
+2 -1
internal/recipes/shopping_list_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 4 "reflect" 6 5 "testing" 6 + 7 + "careme/internal/ai" 7 8 ) 8 9 9 10 func TestShoppingListForDisplay(t *testing.T) {
+4 -3
internal/recipes/staples.go
··· 1 1 package recipes 2 2 3 3 import ( 4 + "context" 5 + "fmt" 6 + "testing" 7 + 4 8 "careme/internal/actowiz" 5 9 "careme/internal/config" 6 10 "careme/internal/kroger" 7 11 "careme/internal/walmart" 8 12 "careme/internal/wholefoods" 9 - "context" 10 - "fmt" 11 - "testing" 12 13 ) 13 14 14 15 // todo make this a indepenedent ingredient object not kroger.
+5 -4
internal/recipes/staples_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 + "context" 5 + "slices" 6 + "testing" 7 + "time" 8 + 4 9 "careme/internal/actowiz" 5 10 "careme/internal/cache" 6 11 "careme/internal/kroger" 7 12 "careme/internal/locations" 8 - "context" 9 - "slices" 10 - "testing" 11 - "time" 12 13 ) 13 14 14 15 type stubStaplesProvider struct {
+3 -2
internal/recipes/storage_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 4 "encoding/json" 7 5 "os" 8 6 "path/filepath" 9 7 "testing" 8 + 9 + "careme/internal/ai" 10 + "careme/internal/cache" 10 11 ) 11 12 12 13 func TestRecipeFileNaming(t *testing.T) {
+2 -1
internal/recipes/thread.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/cache" 5 4 "context" 6 5 "encoding/json" 7 6 "log/slog" 8 7 "time" 8 + 9 + "careme/internal/cache" 9 10 10 11 "github.com/samber/lo" 11 12 )
+5 -5
internal/recipes/user_profile_test.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 - "careme/internal/users" 7 - utypes "careme/internal/users/types" 8 4 "context" 9 5 "os" 10 6 "strings" 11 7 "testing" 12 8 "time" 9 + 10 + "careme/internal/ai" 11 + "careme/internal/cache" 12 + "careme/internal/users" 13 + utypes "careme/internal/users/types" 13 14 ) 14 15 15 16 func TestSaveRecipesToUserProfile(t *testing.T) { ··· 72 73 if updatedUser.LastRecipes[0].Hash != savedRecipe.ComputeHash() { 73 74 t.Errorf("recipe hash mismatch: expected %q, got %q", savedRecipe.ComputeHash(), updatedUser.LastRecipes[0].Hash) 74 75 } 75 - 76 76 } 77 77 78 78 func TestSaveRecipesToUserProfile_NoDuplicates(t *testing.T) {
+3 -2
internal/recipes/wine.go
··· 1 1 package recipes 2 2 3 3 import ( 4 - "careme/internal/ai" 5 - "careme/internal/cache" 6 4 "context" 7 5 "encoding/json" 8 6 "fmt" 9 7 "io" 10 8 "log/slog" 9 + 10 + "careme/internal/ai" 11 + "careme/internal/cache" 11 12 ) 12 13 13 14 const wineRecommendationsCachePrefix = "wine_recommendations/"
+5 -6
internal/sitemap/sitemap.go
··· 1 1 package sitemap 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/recipes" 6 4 "encoding/xml" 7 5 "fmt" 8 6 "log/slog" 9 7 "net/http" 10 8 "strings" 9 + 10 + "careme/internal/cache" 11 + "careme/internal/recipes" 11 12 ) 12 13 13 14 type Server struct { ··· 26 27 ) 27 28 28 29 func New(c cache.ListCache) *Server { 29 - 30 30 return &Server{cache: c} 31 31 } 32 32 ··· 47 47 } 48 48 49 49 func (s *Server) handleSitemap(w http.ResponseWriter, r *http.Request) { 50 - 51 50 hashes, err := s.cache.List(r.Context(), recipes.ShoppingListCachePrefix, "") 52 51 if err != nil { 53 52 http.Error(w, "failed to load sitemap", http.StatusInternalServerError) ··· 57 56 entries := make([]urlEntry, 0, len(hashes)+1) 58 57 entries = append(entries, urlEntry{Loc: domain + "/about"}) 59 58 60 - //this is going to get too big. at some point we need a real db to find latest 61 - //or we track new entries and expire a lsit. 59 + // this is going to get too big. at some point we need a real db to find latest 60 + // or we track new entries and expire a lsit. 62 61 for _, key := range hashes { 63 62 hash := strings.TrimPrefix(key, recipes.ShoppingListCachePrefix) 64 63 entries = append(entries, urlEntry{Loc: domain + "/recipes?h=" + hash})
+4 -3
internal/sitemap/sitemap_test.go
··· 1 1 package sitemap 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/locations" 6 - "careme/internal/recipes" 7 4 "context" 8 5 "encoding/xml" 9 6 "fmt" ··· 12 9 "strings" 13 10 "testing" 14 11 "time" 12 + 13 + "careme/internal/cache" 14 + "careme/internal/locations" 15 + "careme/internal/recipes" 15 16 ) 16 17 17 18 func TestHandleSitemapReturnsXMLWithCachedRecipeHashes(t *testing.T) {
+2 -1
internal/sitemapfetch/urlmap.go
··· 1 1 package sitemapfetch 2 2 3 3 import ( 4 - "careme/internal/cache" 5 4 "context" 6 5 "encoding/json" 7 6 "fmt" 7 + 8 + "careme/internal/cache" 8 9 ) 9 10 10 11 func SaveURLMap(ctx context.Context, c cache.Cache, cacheKey string, urlMap map[string]string) error {
+2 -1
internal/sitemapfetch/urlmap_test.go
··· 1 1 package sitemapfetch 2 2 3 3 import ( 4 - "careme/internal/cache" 5 4 "context" 6 5 "testing" 6 + 7 + "careme/internal/cache" 7 8 ) 8 9 9 10 func TestURLMapRoundTrip(t *testing.T) {
+2 -2
internal/static/static.go
··· 1 1 package static 2 2 3 3 import ( 4 - "careme/internal/seasons" 5 4 "crypto/sha256" 6 5 _ "embed" 7 6 "fmt" 8 7 "log/slog" 9 8 "net/http" 9 + 10 + "careme/internal/seasons" 10 11 ) 11 12 12 13 //go:embed tailwind.css ··· 36 37 37 38 // Register serves static assets and wires template asset paths. 38 39 func Register(mux *http.ServeMux) { 39 - 40 40 mux.HandleFunc(TailwindAssetPath, func(w http.ResponseWriter, r *http.Request) { 41 41 w.Header().Set("Content-Type", "text/css; charset=utf-8") 42 42 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
+2 -1
internal/static/static_test.go
··· 1 1 package static 2 2 3 3 import ( 4 + "testing" 5 + 4 6 "careme/internal/seasons" 5 - "testing" 6 7 ) 7 8 8 9 func TestFaviconBySeason(t *testing.T) {
+8 -5
internal/templates/templates.go
··· 1 1 package templates 2 2 3 3 import ( 4 - "careme/internal/config" 5 4 "embed" 6 5 "html/template" 7 6 "os" 7 + 8 + "careme/internal/config" 8 9 ) 9 10 10 11 //go:embed *.html ··· 40 41 Location = ensure(tmpls, "locations.html") 41 42 Mail = ensure(tmpls, "mail.html") 42 43 43 - //todo pull from config. 44 + // todo pull from config. 44 45 Clarityproject = os.Getenv("CLARITY_PROJECT_ID") 45 46 GoogleTagID = os.Getenv("GOOGLE_TAG_ID") 46 47 GoogleConversionLabel = os.Getenv("GOOGLE_CONVERSION_LABEL") ··· 55 56 return tmpl 56 57 } 57 58 58 - var Clarityproject string 59 - var GoogleTagID string 60 - var GoogleConversionLabel string 59 + var ( 60 + Clarityproject string 61 + GoogleTagID string 62 + GoogleConversionLabel string 63 + ) 61 64 62 65 // ClarityScript generates the Microsoft Clarity tracking script HTML 63 66 func ClarityScript() template.HTML {
+2 -1
internal/users/admin_page.go
··· 1 1 package users 2 2 3 3 import ( 4 - utypes "careme/internal/users/types" 5 4 "html/template" 6 5 "log/slog" 7 6 "net/http" 8 7 "sort" 9 8 "strings" 9 + 10 + utypes "careme/internal/users/types" 10 11 ) 11 12 12 13 type adminUserView struct {
+3 -2
internal/users/admin_page_test.go
··· 1 1 package users 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - utypes "careme/internal/users/types" 6 4 "net/http" 7 5 "net/http/httptest" 8 6 "regexp" 9 7 "strings" 10 8 "testing" 11 9 "time" 10 + 11 + "careme/internal/cache" 12 + utypes "careme/internal/users/types" 12 13 ) 13 14 14 15 func TestAdminUsersPageRendersEmailsAndRecipes(t *testing.T) {
+6 -5
internal/users/server.go
··· 1 1 package users 2 2 3 3 import ( 4 - "careme/internal/auth" 5 - "careme/internal/locations" 6 - "careme/internal/seasons" 7 - "careme/internal/templates" 8 - utypes "careme/internal/users/types" 9 4 "context" 10 5 "errors" 11 6 "html/template" ··· 13 8 "net/http" 14 9 "strings" 15 10 "time" 11 + 12 + "careme/internal/auth" 13 + "careme/internal/locations" 14 + "careme/internal/seasons" 15 + "careme/internal/templates" 16 + utypes "careme/internal/users/types" 16 17 ) 17 18 18 19 type locationGetter interface {
+5 -5
internal/users/server_e2e_test.go
··· 1 1 package users 2 2 3 3 import ( 4 - "careme/internal/auth" 5 - "careme/internal/cache" 6 - "careme/internal/config" 7 - "careme/internal/templates" 8 4 "net/http" 9 5 "net/http/httptest" 10 6 "net/url" ··· 12 8 "path/filepath" 13 9 "strings" 14 10 "testing" 11 + 12 + "careme/internal/auth" 13 + "careme/internal/cache" 14 + "careme/internal/config" 15 + "careme/internal/templates" 15 16 ) 16 17 17 18 func TestMain(m *testing.M) { ··· 22 23 } 23 24 24 25 func TestUserPageUpdate_E2E(t *testing.T) { 25 - 26 26 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 27 27 storage := NewStorage(cacheStore) 28 28
+3 -2
internal/users/server_favorite_test.go
··· 1 1 package users 2 2 3 3 import ( 4 - "careme/internal/auth" 5 - "careme/internal/cache" 6 4 "context" 7 5 "net/http" 8 6 "net/http/httptest" ··· 10 8 "path/filepath" 11 9 "strings" 12 10 "testing" 11 + 12 + "careme/internal/auth" 13 + "careme/internal/cache" 13 14 ) 14 15 15 16 type noSessionAuth struct{}
+4 -3
internal/users/server_test.go
··· 1 1 package users 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - "careme/internal/locations" 6 - utypes "careme/internal/users/types" 7 4 "context" 8 5 "errors" 9 6 "html/template" ··· 14 11 "strings" 15 12 "testing" 16 13 "time" 14 + 15 + "careme/internal/cache" 16 + "careme/internal/locations" 17 + utypes "careme/internal/users/types" 17 18 ) 18 19 19 20 type testAuthClient struct{}
+5 -4
internal/users/storage.go
··· 1 1 package users 2 2 3 3 import ( 4 - "careme/internal/auth" 5 - "careme/internal/cache" 6 - utypes "careme/internal/users/types" 7 4 "context" 8 5 "encoding/json" 9 6 "errors" ··· 13 10 "net/http" 14 11 "strings" 15 12 "time" 13 + 14 + "careme/internal/auth" 15 + "careme/internal/cache" 16 + utypes "careme/internal/users/types" 16 17 ) 17 18 18 19 type Storage struct { ··· 120 121 } 121 122 122 123 newUser := utypes.User{ 123 - ID: clerkUserID, //do we need this o be independent for housholds? 124 + ID: clerkUserID, // do we need this o be independent for housholds? 124 125 Email: []string{normalizeEmail(primaryEmail)}, 125 126 CreatedAt: time.Now(), 126 127 ShoppingDay: time.Saturday.String(),
+1 -2
internal/walmart/client.go
··· 51 51 52 52 // NewClient creates a Walmart affiliates client. 53 53 func NewClient(cfg config.WalmartConfig) (*Client, error) { 54 - 55 54 if !cfg.IsEnabled() { 56 55 return nil, locationtypes.DisabledBackendError("Walmart") 57 56 } ··· 158 157 } 159 158 storesURL.RawQuery = params.Encode() 160 159 161 - //slog.InfoContext(ctx, "searching Walmart stores", "url", storesURL.String()) 160 + // slog.InfoContext(ctx, "searching Walmart stores", "url", storesURL.String()) 162 161 163 162 req, err := http.NewRequestWithContext(ctx, http.MethodGet, storesURL.String(), nil) 164 163 if err != nil {
+2 -1
internal/walmart/client_test.go
··· 1 1 package walmart 2 2 3 3 import ( 4 - "careme/internal/config" 5 4 "context" 6 5 "crypto" 7 6 "crypto/rand" ··· 15 14 "net/http/httptest" 16 15 "strings" 17 16 "testing" 17 + 18 + "careme/internal/config" 18 19 ) 19 20 20 21 func TestCanonicalize_SortsAndFormatsLikeJavaExample(t *testing.T) {
+3 -2
internal/walmart/locations.go
··· 1 1 package walmart 2 2 3 3 import ( 4 - locationtypes "careme/internal/locations/types" 5 4 "context" 6 5 "fmt" 7 6 "strconv" 7 + 8 + locationtypes "careme/internal/locations/types" 8 9 ) 9 10 10 11 func (c *Client) GetLocationByID(_ context.Context, locationID string) (*locationtypes.Location, error) { 11 - //depending on cache to protect us. 12 + // depending on cache to protect us. 12 13 return nil, fmt.Errorf("walmart GetLocationByID not supported yet for ID %s", locationID) 13 14 } 14 15
+5 -4
internal/wholefoods/cache.go
··· 1 1 package wholefoods 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 - "careme/internal/sitemapfetch" 7 4 "context" 8 5 "encoding/json" 9 6 "errors" ··· 11 8 "log/slog" 12 9 "strconv" 13 10 11 + "careme/internal/cache" 12 + locationtypes "careme/internal/locations/types" 13 + "careme/internal/sitemapfetch" 14 + 14 15 "github.com/samber/lo" 15 16 lop "github.com/samber/lo/parallel" 16 17 ) 17 18 18 19 const ( 19 20 Container = "wholefoods" 20 - //prefixes are a little redundant since we already have a container. Could simpify with reimport. 21 + // prefixes are a little redundant since we already have a container. Could simpify with reimport. 21 22 StoreCachePrefix = "wholefoods/stores/" 22 23 StoreURLMapCacheKey = "wholefoods/store_url_map.json" 23 24 LocationIDPrefix = "wholefoods_"
+2 -1
internal/wholefoods/cache_test.go
··· 1 1 package wholefoods 2 2 3 3 import ( 4 - "careme/internal/cache" 5 4 "context" 6 5 "testing" 6 + 7 + "careme/internal/cache" 7 8 ) 8 9 9 10 func TestStoreURLMapRoundTrip(t *testing.T) {
+1 -1
internal/wholefoods/client.go
··· 65 65 ImageThumbnail string `json:"imageThumbnail"` 66 66 Store int `json:"store"` 67 67 IsLocal bool `json:"isLocal"` 68 - //unit of measure. 68 + // unit of measure. 69 69 UOM string `json:"uom,omitempty"` 70 70 } 71 71
+2 -1
internal/wholefoods/discovery.go
··· 1 1 package wholefoods 2 2 3 3 import ( 4 - "careme/internal/sitemapfetch" 5 4 "context" 6 5 "fmt" 7 6 "io" 8 7 "net/http" 9 8 "regexp" 9 + 10 + "careme/internal/sitemapfetch" 10 11 ) 11 12 12 13 var storeIDRe = regexp.MustCompile(`store-id="(\d+)"`)
+5 -5
internal/wholefoods/locations.go
··· 1 1 package wholefoods 2 2 3 3 import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + 4 8 "careme/internal/cache" 5 9 "careme/internal/config" 6 10 "careme/internal/locations/nearby" 7 11 locationtypes "careme/internal/locations/types" 8 - "context" 9 - "fmt" 10 - "strings" 11 12 ) 12 13 13 14 type centroidByZip interface { ··· 37 38 } 38 39 39 40 func newLocationBackend(ctx context.Context, c cache.ListCache, zipLookup centroidByZip) (*LocationBackend, error) { 40 - 41 - //Is this too much? should we just fetch a single blob that is all coordinates -> store ids and lazily fetch stores? 41 + // Is this too much? should we just fetch a single blob that is all coordinates -> store ids and lazily fetch stores? 42 42 summaries, err := loadCachedStoreSummaries(ctx, c) 43 43 if err != nil { 44 44 return nil, err
+3 -2
internal/wholefoods/locations_test.go
··· 1 1 package wholefoods 2 2 3 3 import ( 4 - "careme/internal/cache" 5 - locationtypes "careme/internal/locations/types" 6 4 "context" 7 5 "strings" 8 6 "testing" 7 + 8 + "careme/internal/cache" 9 + locationtypes "careme/internal/locations/types" 9 10 ) 10 11 11 12 func TestNewLocationBackendBuildsIndexAndLookup(t *testing.T) {
+6 -6
internal/wholefoods/staples.go
··· 50 50 return nil, fmt.Errorf("whole foods client is required") 51 51 } 52 52 53 - //should identity provider do this? 53 + // should identity provider do this? 54 54 storeID := strings.TrimPrefix(locationID, LocationIDPrefix) 55 55 if storeID == locationID || storeID == "" { 56 56 return nil, fmt.Errorf("invalid whole foods location id %q", locationID) ··· 108 108 "goat-lamb-veal", 109 109 "game-meats", 110 110 } 111 - //rice-grains? 112 - //pasta-noodles 113 - //red-wine, white-wine, sparkling 111 + // rice-grains? 112 + // pasta-noodles 113 + // red-wine, white-wine, sparkling 114 114 } 115 115 116 116 func productToIngredient(product Product) kroger.Ingredient { ··· 134 134 size = &sizeText 135 135 }*/ 136 136 137 - //categories := compactStrings(localCategory(product)) 137 + // categories := compactStrings(localCategory(product)) 138 138 139 139 hasher := fnv.New32a() 140 140 _ = lo.Must(hasher.Write([]byte(product.Slug))) ··· 143 143 ProductId: stringPtr(productId), 144 144 Brand: stringPtr(strings.TrimSpace(product.Brand)), 145 145 Description: stringPtr(strings.TrimSpace(product.Name)), 146 - //Size: size, 146 + // Size: size, 147 147 PriceRegular: regularPrice, 148 148 PriceSale: salePrice, 149 149 // / Categories: slicePtr(categories),
+45
tools/install.sh
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 5 + tool_dir="${repo_root}/bin" 6 + 7 + # shellcheck source=/dev/null 8 + source "${repo_root}/tools/versions.sh" 9 + 10 + mkdir -p "${tool_dir}" 11 + export GOBIN="${tool_dir}" 12 + 13 + install_golangci_lint() { 14 + go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}" 15 + } 16 + 17 + install_gofumpt() { 18 + go install "mvdan.cc/gofumpt@${GOFUMPT_VERSION}" 19 + } 20 + 21 + install_kubeconform() { 22 + go install "github.com/yannh/kubeconform/cmd/kubeconform@${KUBECONFORM_VERSION}" 23 + } 24 + 25 + if [[ $# -eq 0 ]]; then 26 + set -- golangci-lint gofumpt kubeconform 27 + fi 28 + 29 + for tool_name in "$@"; do 30 + case "${tool_name}" in 31 + golangci-lint) 32 + install_golangci_lint 33 + ;; 34 + gofumpt) 35 + install_gofumpt 36 + ;; 37 + kubeconform) 38 + install_kubeconform 39 + ;; 40 + *) 41 + echo "unknown tool: ${tool_name}" >&2 42 + exit 1 43 + ;; 44 + esac 45 + done
+5
tools/versions.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + GOLANGCI_LINT_VERSION=v2.11.3 4 + GOFUMPT_VERSION=v0.9.2 5 + KUBECONFORM_VERSION=v0.7.0