diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5c27c8..1e19098 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
+## 0.62.0 — 2026-07-20 23:45
+
+### Added
+- Settings → Features: new Chatbots toggle — hides both the recipe cooking-chat panel and the recipe-list cooking assistant when off.
+
+### Fixed
+- Feature toggles (Settings → Features) were only respected by the mobile hamburger menu — the desktop horizontal nav still showed every item regardless of your saved preferences. Both now read the same visibility list.
+
## 0.61.0 — 2026-07-20 23:15
### Added
diff --git a/apps/web/app/(app)/recipes/[id]/page.tsx b/apps/web/app/(app)/recipes/[id]/page.tsx
index a8d59e5..3c1b875 100644
--- a/apps/web/app/(app)/recipes/[id]/page.tsx
+++ b/apps/web/app/(app)/recipes/[id]/page.tsx
@@ -35,6 +35,7 @@ import { CommentsSection } from "@/components/social/comments-section";
import { getPublicUrl } from "@/lib/storage";
import { cn, stripMarkdown } from "@/lib/utils";
import { RecipeChatPanel } from "@/components/recipe/recipe-chat-panel";
+import { getFeaturePrefs } from "@/lib/feature-prefs";
import { BatchCookSteps } from "@/components/recipe/batch-cook-steps";
import { BatchCookDishes } from "@/components/recipe/batch-cook-dishes";
import { KeepScreenAwake } from "@/components/recipe/keep-screen-awake";
@@ -71,7 +72,7 @@ export default async function RecipePage({ params }: Params) {
const DIETARY_LABELS = m.recipe.dietary;
const unitPref = (session.user as { unitPref?: string }).unitPref === "imperial" ? "imperial" : "metric";
- const [recipe, ratingData, favoriteData, myRating, forkedFrom, myNote, dishCookLog, featureFlags] = await Promise.all([
+ const [recipe, ratingData, favoriteData, myRating, forkedFrom, myNote, dishCookLog, featureFlags, featurePrefs] = await Promise.all([
db.query.recipes.findFirst({
where: and(
eq(recipes.id, id),
@@ -102,6 +103,7 @@ export default async function RecipePage({ params }: Params) {
columns: { batchDishId: true, cookedAt: true },
}),
getFeatureFlagMatrix(),
+ getFeaturePrefs(session.user.id),
]);
if (!recipe) notFound();
@@ -507,7 +509,7 @@ export default async function RecipePage({ params }: Params) {
-
+ {featurePrefs.chatbots && }
);
}
diff --git a/apps/web/app/(app)/recipes/page.tsx b/apps/web/app/(app)/recipes/page.tsx
index 186aa2b..ad8c549 100644
--- a/apps/web/app/(app)/recipes/page.tsx
+++ b/apps/web/app/(app)/recipes/page.tsx
@@ -9,6 +9,7 @@ import { RecipesEmptyState } from "@/components/recipe/recipes-empty-state";
import { RecipesGrid } from "@/components/recipe/recipes-grid";
import { CookingAssistantPanel } from "@/components/recipe/cooking-assistant-panel";
import { getMessages } from "@/lib/i18n/server";
+import { getFeaturePrefs } from "@/lib/feature-prefs";
export const metadata: Metadata = {};
@@ -40,6 +41,7 @@ export default async function RecipesPage({ searchParams }: { searchParams: Sear
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return null;
const m = getMessages((session.user as { locale?: string }).locale);
+ const featurePrefs = await getFeaturePrefs(session.user.id);
const { q, sort, visibility, difficulty, tag, page: pageParam, batchCook, recipeType } = await searchParams;
const query = (q ?? "").trim().slice(0, 200);
@@ -160,7 +162,7 @@ export default async function RecipesPage({ searchParams }: { searchParams: Sear
)}
-
+ {featurePrefs.chatbots && }
);
}
diff --git a/apps/web/app/api/v1/users/me/feature-prefs/route.ts b/apps/web/app/api/v1/users/me/feature-prefs/route.ts
index 2e0e123..1df3ab0 100644
--- a/apps/web/app/api/v1/users/me/feature-prefs/route.ts
+++ b/apps/web/app/api/v1/users/me/feature-prefs/route.ts
@@ -11,6 +11,7 @@ const PutSchema = z.object({
shoppingLists: z.boolean().optional(),
collections: z.boolean().optional(),
messages: z.boolean().optional(),
+ chatbots: z.boolean().optional(),
});
export async function GET() {
diff --git a/apps/web/components/layout/nav.tsx b/apps/web/components/layout/nav.tsx
index d14b2ea..75ba73d 100644
--- a/apps/web/components/layout/nav.tsx
+++ b/apps/web/components/layout/nav.tsx
@@ -52,7 +52,7 @@ export function Nav() {
// backend default — avoids a flash of items disappearing on load for the
// (much more common) case where a user hasn't hidden anything.
const [featurePrefs, setFeaturePrefs] = useState({
- nutrition: true, pantry: true, mealPlan: true, shoppingLists: true, collections: true, messages: true,
+ nutrition: true, pantry: true, mealPlan: true, shoppingLists: true, collections: true, messages: true, chatbots: true,
});
useEffect(() => {
@@ -121,7 +121,7 @@ export function Nav() {
Epicure