From 5c659d9046add8c77bd7699961a910505dcb4bc2 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 24 Aug 2026 06:20:52 +0000 Subject: Remove confirmed-orphaned routes, handlers, and templates 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 Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK --- .agent/design.md | 11 -- cmd/dashboard/main.go | 4 - internal/handlers/handlers.go | 31 ---- internal/handlers/handlers_test.go | 271 ++-------------------------- internal/handlers/shopping.go | 83 --------- internal/handlers/tab_state_test.go | 8 - web/static/js/app.js | 2 +- web/templates/partials/error-banner.html | 12 -- web/templates/partials/lists-options.html | 4 - web/templates/partials/meals-tab.html | 10 - web/templates/partials/plantoeat-meals.html | 35 ---- web/templates/partials/trello-boards.html | 45 ----- 12 files changed, 13 insertions(+), 503 deletions(-) delete mode 100644 web/templates/partials/error-banner.html delete mode 100644 web/templates/partials/lists-options.html delete mode 100644 web/templates/partials/meals-tab.html delete mode 100644 web/templates/partials/plantoeat-meals.html delete mode 100644 web/templates/partials/trello-boards.html diff --git a/.agent/design.md b/.agent/design.md index 47ff403..829f86f 100644 --- a/.agent/design.md +++ b/.agent/design.md @@ -351,15 +351,6 @@ Aggregated shopping lists from Trello + PlanToEat + user items. - Large touch targets - Fixed bottom quick-add -### Meals View (`/tabs/meals`) - -PlanToEat meal schedule for the next 7 days. - -**Features:** -- Meals grouped by date + meal type (breakfast/lunch/dinner) -- Multiple recipes for same slot combined with " + " separator -- Sorted by date, then meal type order - ### Conditions Page (`/conditions`) **Public route** - no authentication required. @@ -828,7 +819,6 @@ npx tailwindcss -i web/static/css/input.css -o web/static/css/output.css --watch | GET | `/tabs/timeline` | Timeline partial | | GET | `/tabs/tasks` | Tasks partial | | GET | `/tabs/planning` | Planning partial | -| GET | `/tabs/meals` | Meals partial | | GET | `/tabs/shopping` | Shopping partial | | GET | `/conditions` | Live feeds page (public) | | POST | `/complete-atom` | Complete task/card/bug | @@ -838,7 +828,6 @@ npx tailwindcss -i web/static/css/input.css -o web/static/css/output.css --watch | POST | `/bugs` | Submit bug report | | POST | `/shopping/add` | Add shopping item | | GET | `/shopping/mode/{store}` | Shopping mode view | -| POST | `/shopping/mode/{store}/toggle` | Toggle shopping item | | POST | `/shopping/mode/{store}/complete` | Complete shopping item | | GET | `/settings` | Settings page | | POST | `/settings/features` | Create feature toggle | diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index c88dc45..c9e1078 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -334,7 +334,6 @@ func main() { // Tab routes for HTMX r.Get("/tabs/tasks", h.HandleTabTasks) r.Get("/tabs/planning", h.HandleTabPlanning) - r.Get("/tabs/meals", h.HandleTabMeals) r.Get("/tabs/timeline", h.HandleTimeline) r.Get("/tabs/shopping", h.HandleTabShopping) @@ -355,7 +354,6 @@ func main() { // Unified Quick Add (for Tasks tab) r.Post("/unified-add", h.HandleUnifiedAdd) - r.Get("/partials/lists", h.HandleGetListsOptions) r.Get("/partials/shopping-lists", h.HandleGetShoppingLists) // Task detail/edit @@ -367,11 +365,9 @@ func main() { // Shopping quick-add r.Post("/shopping/add", h.HandleShoppingQuickAdd) - r.Post("/shopping/toggle", h.HandleShoppingToggle) // Shopping mode (focused single-store view) r.Get("/shopping/mode/{store}", h.HandleShoppingMode) - r.Post("/shopping/mode/{store}/toggle", h.HandleShoppingModeToggle) r.Post("/shopping/mode/{store}/complete", h.HandleShoppingModeComplete) // Passkey management (WebAuthn) 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) { - ` _, 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 { diff --git a/web/static/js/app.js b/web/static/js/app.js index 37d2a30..8058dc2 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -320,7 +320,7 @@ function toggleTask(taskId) { const SWIPE_THRESHOLD = 50; // Minimum px for a swipe // Get ordered list of tab names (main tabs only for swipe) - const TAB_ORDER = ['timeline', 'shopping', 'conditions', 'tasks', 'planning', 'meals']; + const TAB_ORDER = ['timeline', 'shopping', 'tasks', 'planning']; function handleSwipe() { const swipeDistance = touchEndX - touchStartX; diff --git a/web/templates/partials/error-banner.html b/web/templates/partials/error-banner.html deleted file mode 100644 index 628424f..0000000 --- a/web/templates/partials/error-banner.html +++ /dev/null @@ -1,12 +0,0 @@ -{{define "error-banner"}} -{{if .Errors}} -
-

Errors:

-
    - {{range .Errors}} -
  • {{.}}
  • - {{end}} -
-
-{{end}} -{{end}} diff --git a/web/templates/partials/lists-options.html b/web/templates/partials/lists-options.html deleted file mode 100644 index f08fdcf..0000000 --- a/web/templates/partials/lists-options.html +++ /dev/null @@ -1,4 +0,0 @@ -{{define "lists-options"}} -{{range .}} -{{end}} -{{end}} diff --git a/web/templates/partials/meals-tab.html b/web/templates/partials/meals-tab.html deleted file mode 100644 index 97c3a4e..0000000 --- a/web/templates/partials/meals-tab.html +++ /dev/null @@ -1,10 +0,0 @@ -{{define "meals-tab"}} -
- - {{template "plantoeat-meals" .}} -
-{{end}} diff --git a/web/templates/partials/plantoeat-meals.html b/web/templates/partials/plantoeat-meals.html deleted file mode 100644 index f05c98c..0000000 --- a/web/templates/partials/plantoeat-meals.html +++ /dev/null @@ -1,35 +0,0 @@ -{{define "plantoeat-meals"}} -
- -
-
-

Upcoming Meals

-
- - {{if .Meals}} -
- {{range .Meals}} -
-

{{range $i, $name := .RecipeNames}}{{if $i}} + {{end}}{{$name}}{{end}}

-
- {{.Date.Format "Mon, Jan 2"}} - - {{.MealType}} - -
-
- {{end}} -
- {{else}} -
- - - -

No meals planned

-

- Schedule your meals to see them here. -

-
- {{end}} -
-{{end}} diff --git a/web/templates/partials/trello-boards.html b/web/templates/partials/trello-boards.html deleted file mode 100644 index d51446d..0000000 --- a/web/templates/partials/trello-boards.html +++ /dev/null @@ -1,45 +0,0 @@ -{{define "trello-boards"}} -{{if .Boards}} -
- -
-
-

Trello Boards

-
- - -
- {{range .Boards}} - {{if .Cards}} - {{template "trello-board" .}} - {{end}} - {{end}} -
- - -
- - - Empty Boards - - - - - -
-
- {{range .Boards}} - {{if not .Cards}} -
-

{{.Name}}

-

No cards

-
- {{end}} - {{end}} -
-
-
-
-{{end}} -{{end}} -- cgit v1.2.3