diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-08-24 06:20:52 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-08-24 06:20:52 +0000 |
| commit | 5c659d9046add8c77bd7699961a910505dcb4bc2 (patch) | |
| tree | 8bde9a62d98d2a21f030d4c6cbd3ceecb9a7adf8 /internal | |
| parent | c46c135f52090a082660cb06131282796f5cd592 (diff) | |
From the sitemap audit: /tabs/meals (same dead-tab pattern as the
removed /tabs/conditions, only reachable via ?tab=meals with no nav
button), /partials/lists (superseded by inline .Lists rendering in
trello-board.html), /shopping/toggle and /shopping/mode/{store}/toggle
(superseded by the one-way complete/filter model), plus the orphaned
trello-boards.html and error-banner.html templates that nothing
rendered or included. Rewrote the meals grouping test to exercise
groupMeals() directly since that logic is still live via the timeline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/handlers/handlers.go | 31 | ||||
| -rw-r--r-- | internal/handlers/handlers_test.go | 271 | ||||
| -rw-r--r-- | internal/handlers/shopping.go | 83 | ||||
| -rw-r--r-- | internal/handlers/tab_state_test.go | 8 |
4 files changed, 12 insertions, 381 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index fc75b61..69e1c65 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -787,23 +787,6 @@ func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// HandleGetListsOptions returns HTML options for lists in a given board -func (h *Handler) HandleGetListsOptions(w http.ResponseWriter, r *http.Request) { - boardID := r.URL.Query().Get("board_id") - if boardID == "" { - JSONError(w, http.StatusBadRequest, "board_id is required", nil) - return - } - - lists, err := h.trelloClient.GetLists(r.Context(), boardID) - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to fetch lists", err) - return - } - - HTMLResponse(w, h.renderer, "lists-options", lists) -} - // taskDetailData is what the "task-detail" template renders (the modal // opened from any tab's task card). Recurrence fields are doot-native only. type taskDetailData struct { @@ -1234,20 +1217,6 @@ type CombinedMeal struct { Meals []models.Meal // original meal records } -// HandleTabMeals renders the Meals tab (PlanToEat) -func (h *Handler) HandleTabMeals(w http.ResponseWriter, r *http.Request) { - startDate := config.Today() - endDate := startDate.AddDate(0, 0, 7) - - meals, err := h.store.GetMeals(startDate, endDate) - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to fetch meals", err) - return - } - - HTMLResponse(w, h.renderer, "meals-tab", struct{ Meals []CombinedMeal }{groupMeals(meals)}) -} - // mealTypeOrder returns sort order for meal types func mealTypeOrder(mealType string) int { switch mealType { diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index ca24778..61fb75d 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -819,169 +819,6 @@ func TestShoppingComplete_ExternalItemMarkedChecked(t *testing.T) { } } -// TestShoppingToggle_UpdatesItemState verifies toggle correctly updates state -func TestShoppingToggle_UpdatesItemState(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - h := &Handler{ - store: db, - renderer: newTestRenderer(), - config: &config.Config{}, - } - - // Add user item - _ = db.SaveUserShoppingItem("Toggle Item", "TestStore") - items, _ := db.GetUserShoppingItems() - itemID := items[0].ID - - // Toggle to checked - req := httptest.NewRequest("POST", "/shopping/toggle", nil) - req.Form = map[string][]string{ - "id": {fmt.Sprintf("user-%d", itemID)}, - "source": {"user"}, - "checked": {"true"}, - } - w := httptest.NewRecorder() - h.HandleShoppingToggle(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Verify item is checked - items, _ = db.GetUserShoppingItems() - if !items[0].Checked { - t.Error("Expected item to be checked after toggle") - } - - // Toggle back to unchecked - req = httptest.NewRequest("POST", "/shopping/toggle", nil) - req.Form = map[string][]string{ - "id": {fmt.Sprintf("user-%d", itemID)}, - "source": {"user"}, - "checked": {"false"}, - } - w = httptest.NewRecorder() - h.HandleShoppingToggle(w, req) - - items, _ = db.GetUserShoppingItems() - if items[0].Checked { - t.Error("Expected item to be unchecked after second toggle") - } -} - -// TestShoppingToggle_ExternalSources verifies toggle works for external sources -func TestShoppingToggle_ExternalSources(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - h := &Handler{ - store: db, - renderer: newTestRenderer(), - config: &config.Config{}, - } - - tests := []struct { - source string - itemID string - }{ - {"trello", "trello-toggle-123"}, - {"plantoeat", "pte-toggle-456"}, - } - - for _, tc := range tests { - t.Run(tc.source, func(t *testing.T) { - // Toggle to checked - req := httptest.NewRequest("POST", "/shopping/toggle", nil) - req.Form = map[string][]string{ - "id": {tc.itemID}, - "source": {tc.source}, - "checked": {"true"}, - } - w := httptest.NewRecorder() - h.HandleShoppingToggle(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - checks, _ := db.GetShoppingItemChecks(tc.source) - if !checks[tc.itemID] { - t.Errorf("Expected %s item to be checked", tc.source) - } - }) - } -} - -// TestShoppingToggle_UnknownSource verifies error for unknown source -func TestShoppingToggle_UnknownSource(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - h := &Handler{ - store: db, - config: &config.Config{}, - } - - req := httptest.NewRequest("POST", "/shopping/toggle", nil) - req.Form = map[string][]string{ - "id": {"unknown-123"}, - "source": {"unknown"}, - "checked": {"true"}, - } - w := httptest.NewRecorder() - h.HandleShoppingToggle(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("Expected status 400 for unknown source, got %d", w.Code) - } -} - -// TestShoppingModeToggle_ReturnsUpdatedList verifies shopping mode toggle returns items -func TestShoppingModeToggle_ReturnsUpdatedList(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - h := &Handler{ - store: db, - renderer: newTestRenderer(), - config: &config.Config{}, - } - - // Add items - _ = db.SaveUserShoppingItem("Item 1", "TestStore") - _ = db.SaveUserShoppingItem("Item 2", "TestStore") - - items, _ := db.GetUserShoppingItems() - itemID := items[0].ID - - // Toggle in shopping mode - req := httptest.NewRequest("POST", "/shopping/mode/TestStore/toggle", nil) - req.Form = map[string][]string{ - "id": {fmt.Sprintf("user-%d", itemID)}, - "source": {"user"}, - "checked": {"true"}, - } - - rctx := chi.NewRouteContext() - rctx.URLParams.Add("store", "TestStore") - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - - w := httptest.NewRecorder() - h.HandleShoppingModeToggle(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Response should be HTML (items list) - contentType := w.Header().Get("Content-Type") - if !strings.Contains(contentType, "text/html") { - t.Errorf("Expected HTML response, got %s", contentType) - } -} - // TestShoppingTabFiltersCheckedItems verifies checked items excluded from tab func TestShoppingTabFiltersCheckedItems(t *testing.T) { db, cleanup := setupTestDB(t) @@ -1057,26 +894,6 @@ func TestHandleTabTasks(t *testing.T) { } } -func TestHandleTabMeals(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - h := &Handler{ - store: db, - renderer: newTestRenderer(), - config: &config.Config{CacheTTLMinutes: 5}, - } - - req := httptest.NewRequest("GET", "/tabs/meals", nil) - w := httptest.NewRecorder() - - h.HandleTabMeals(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } -} - func TestHandleTabPlanning(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() @@ -2120,35 +1937,6 @@ func TestHandleSetTaskRecurrence_InvalidFreq_Returns400(t *testing.T) { } } -// ============================================================================= -// HandleGetListsOptions template tests -// ============================================================================= - -// TestHandleGetListsOptions_RendersTemplate verifies that HandleGetListsOptions uses -// the renderer with the "lists-options" template. -func TestHandleGetListsOptions_RendersTemplate(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - h.trelloClient = &mockTrelloClient{boards: []models.Board{}} - - req := httptest.NewRequest("GET", "/trello/lists?board_id=board1", nil) - w := httptest.NewRecorder() - h.HandleGetListsOptions(w, req) - - mr := h.renderer.(*MockRenderer) - var found bool - for _, call := range mr.Calls { - if call.Name == "lists-options" { - found = true - break - } - } - if !found { - t.Error("Expected renderer to be called with 'lists-options' template") - } -} - // TestHandleSyncSources_AddsLogEntry verifies that syncing sources creates a sync log entry. func TestHandleSyncSources_AddsLogEntry(t *testing.T) { h, cleanup := setupTestHandler(t) @@ -2339,67 +2127,32 @@ func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { } // ============================================================================= -// HandleTabMeals grouping test +// groupMeals grouping test // ============================================================================= -// TestHandleTabMeals_GroupingMergesRecipes verifies that multiple meals sharing -// the same date+mealType are combined into a single CombinedMeal entry whose -// RecipeNames contains all merged recipe names. -func TestHandleTabMeals_GroupingMergesRecipes(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - +// TestGroupMeals_MergesRecipes verifies that multiple meals sharing the same +// date+mealType are combined into a single CombinedMeal entry whose +// RecipeNames contains all merged recipe names. groupMeals is still live +// logic used by the timeline (see timeline_logic.go). +func TestGroupMeals_MergesRecipes(t *testing.T) { today := config.Today() meals := []models.Meal{ {ID: "m1", RecipeName: "Pasta", Date: today, MealType: "dinner", RecipeURL: "http://example.com/pasta"}, {ID: "m2", RecipeName: "Salad", Date: today, MealType: "dinner", RecipeURL: "http://example.com/salad"}, {ID: "m3", RecipeName: "Oatmeal", Date: today, MealType: "breakfast", RecipeURL: "http://example.com/oatmeal"}, } - if err := db.SaveMeals(meals); err != nil { - t.Fatalf("Failed to save meals: %v", err) - } - renderer := NewMockRenderer() - h := &Handler{ - store: db, - renderer: renderer, - config: &config.Config{CacheTTLMinutes: 5}, - } - - req := httptest.NewRequest("GET", "/tabs/meals", nil) - w := httptest.NewRecorder() - h.HandleTabMeals(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected 200, got %d", w.Code) - } - - var mealsCall *RenderCall - for i, call := range renderer.Calls { - if call.Name == "meals-tab" { - c := renderer.Calls[i] - mealsCall = &c - break - } - } - if mealsCall == nil { - t.Fatal("Expected meals-tab to be rendered") - } - - data, ok := mealsCall.Data.(struct{ Meals []CombinedMeal }) - if !ok { - t.Fatalf("Expected meals data struct, got %T", mealsCall.Data) - } + combined := groupMeals(meals) // m1 + m2 share date+dinner → 1 CombinedMeal; m3 is breakfast → 1 CombinedMeal - if len(data.Meals) != 2 { - t.Errorf("Expected 2 combined meals, got %d", len(data.Meals)) + if len(combined) != 2 { + t.Errorf("Expected 2 combined meals, got %d", len(combined)) } var dinner *CombinedMeal - for i := range data.Meals { - if data.Meals[i].MealType == "dinner" { - dinner = &data.Meals[i] + for i := range combined { + if combined[i].MealType == "dinner" { + dinner = &combined[i] break } } diff --git a/internal/handlers/shopping.go b/internal/handlers/shopping.go index e8e80da..1c58126 100644 --- a/internal/handlers/shopping.go +++ b/internal/handlers/shopping.go @@ -73,47 +73,6 @@ func (h *Handler) HandleShoppingQuickAdd(w http.ResponseWriter, r *http.Request) }{allStores, grouped}) } -// HandleShoppingToggle toggles a shopping item's checked state -func (h *Handler) HandleShoppingToggle(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Failed to parse form", err) - return - } - - id := r.FormValue("id") - source := r.FormValue("source") - checked := r.FormValue("checked") == "true" - - switch source { - case "user": - var userID int64 - if _, err := fmt.Sscanf(id, "user-%d", &userID); err != nil { - JSONError(w, http.StatusBadRequest, "Invalid user item ID", err) - return - } - if err := h.store.ToggleUserShoppingItem(userID, checked); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err) - return - } - case "trello", "plantoeat": - // Store checked state locally for external sources - if err := h.store.SetShoppingItemChecked(source, id, checked); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err) - return - } - default: - JSONError(w, http.StatusBadRequest, "Unknown source", nil) - return - } - - // Return refreshed shopping tab - stores := h.aggregateShoppingLists(r.Context()) - HTMLResponse(w, h.renderer, "shopping-tab", struct { - Stores []models.ShoppingStore - Grouped bool - }{stores, true}) -} - // HandleShoppingMode renders the focused shopping mode for a single store func (h *Handler) HandleShoppingMode(w http.ResponseWriter, r *http.Request) { storeName := chi.URLParam(r, "store") @@ -143,48 +102,6 @@ func (h *Handler) HandleShoppingMode(w http.ResponseWriter, r *http.Request) { HTMLResponse(w, h.renderer, "shopping-mode.html", data) } -// HandleShoppingModeToggle toggles an item in shopping mode and returns updated list -func (h *Handler) HandleShoppingModeToggle(w http.ResponseWriter, r *http.Request) { - storeName := chi.URLParam(r, "store") - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Failed to parse form", err) - return - } - - id := r.FormValue("id") - source := r.FormValue("source") - checked := r.FormValue("checked") == "true" - - // Toggle the item - switch source { - case "user": - var userID int64 - if _, err := fmt.Sscanf(id, "user-%d", &userID); err != nil { - JSONError(w, http.StatusBadRequest, "Invalid user item ID", err) - return - } - if err := h.store.ToggleUserShoppingItem(userID, checked); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err) - return - } - case "trello", "plantoeat": - if err := h.store.SetShoppingItemChecked(source, id, checked); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err) - return - } - } - - // URL decode the store name - storeName, _ = url.QueryUnescape(storeName) - - // Return updated item list partial - allStores := h.aggregateShoppingLists(r.Context()) - HTMLResponse(w, h.renderer, "shopping-mode-items", struct { - StoreName string - Items []models.UnifiedShoppingItem - }{storeName, models.FlattenItemsForStore(allStores, storeName)}) -} - // HandleShoppingModeComplete removes an item from the shopping list func (h *Handler) HandleShoppingModeComplete(w http.ResponseWriter, r *http.Request) { storeName := chi.URLParam(r, "store") diff --git a/internal/handlers/tab_state_test.go b/internal/handlers/tab_state_test.go index 9b34033..bf833af 100644 --- a/internal/handlers/tab_state_test.go +++ b/internal/handlers/tab_state_test.go @@ -24,7 +24,6 @@ func TestHandleDashboard_TabState(t *testing.T) { <body> <button class="tab-button tab-button-active" hx-get="/tabs/tasks">Tasks</button> <button class="tab-button tab-button-active" hx-get="/tabs/planning">Planning</button> -<button class="tab-button tab-button-active" hx-get="/tabs/meals">Meals</button> </body> </html>` _, err := w.Write([]byte(html)) @@ -63,13 +62,6 @@ func TestHandleDashboard_TabState(t *testing.T) { expectedActive: `class="tab-button tab-button-active"`, expectedHxGet: `hx-get="/tabs/planning"`, }, - { - name: "meals tab from query param", - url: "/?tab=meals", - expectedTab: "meals", - expectedActive: `class="tab-button tab-button-active"`, - expectedHxGet: `hx-get="/tabs/meals"`, - }, } for _, tt := range tests { |
