diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b6e5ab..f560b3d 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.85.0 — 2026-07-24 21:00
+
+### Added
+- Admin → Ingredients: manage canonical ingredients and their name variants (e.g. "sel"/"sel fin"/"table salt") without a code change — previously only editable by hand in the seed file.
+
+### Fixed
+- Deleting a shopping list, recipe, or collection from its own page could leave you one back-button press away from a "Page not found" screen — the deleted URL stayed in browser history. Now replaces the history entry instead of pushing a new one.
+
## 0.84.0 — 2026-07-24 20:00
### Added
diff --git a/FEATURE_AUDIT.md b/FEATURE_AUDIT.md
index e1bb53b..49b375f 100644
--- a/FEATURE_AUDIT.md
+++ b/FEATURE_AUDIT.md
@@ -61,6 +61,7 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real
| Weekly nutrition rollup | Exists | | `apps/web/app/api/v1/meal-plans/[weekStart]/nutrition` |
| Meal plan print + Markdown export | Exists | Undocumented until this pass despite section 1 mentioning print/markdown generally | `/print/meal-plan`, `apps/web/lib/markdown/meal-plan.ts` |
| Multiple shopping lists | Exists | | `apps/web/app/api/v1/shopping-lists/**` |
+| "Page not found" after deleting (closed 2026-07-24) | Fixed | Deleting a shopping list, recipe, or collection from its own detail page navigated away with `router.push`, leaving the just-deleted URL in browser history — hitting the back button landed straight on a real 404 (`(app)/not-found.tsx`, literally "Page not found"). Switched all three to `router.replace` so the dead URL is never in history to go back to. | `apps/web/components/shopping-lists/shopping-list-actions-menu.tsx`, `apps/web/components/recipe/delete-recipe-button.tsx`, `apps/web/components/collections/delete-collection-dialog.tsx` |
| Generate list from meal-plan week | Exists | Merges duplicate ingredients, flags items already covered by pantry (never silently drops) | `apps/web/lib/pantry-shopping-match.ts` |
| Add to list from a single recipe | Exists | Separate path from meal-plan-based generation; also reachable via the cooking-chat AI tool ("add to shopping list") | `apps/web/components/recipe/add-to-shopping-list-button.tsx`, `apps/web/lib/ai/tools/add-to-shopping-list-tool.ts` |
| Shared/collaborative lists | Exists | Member roles **and** an anonymous public link that can grant full anonymous edit rights (Google-Docs-style), not just read — `isPublic` + `publicEditable`; disabling `isPublic` force-revokes `publicEditable`. Also has its own print view with a QR code, and a public page distinct from the meal-plan one. | `apps/web/lib/shopping-list-access.ts`, `apps/web/app/s/[id]/page.tsx`, `/print/shopping-list/[id]` |
@@ -70,7 +71,8 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real
| Grocery delivery integration | **Partial / stub** | Generic export payload works (integrator-shaped — no visible in-app UI consumer besides the Instacart adapter); Instacart adapter is an explicit stub (requires a partnership agreement Epicure doesn't have — returns 501 if unconfigured, throws if "configured") | `apps/web/lib/grocery-providers/instacart.ts` |
| Other delivery/price integrations (DoorDash, Kroger, Walmart, live pricing) | **Missing** | Confirmed absent by repo-wide search | — |
| Pantry manual CRUD | Exists (extended 2026-07-24, twice) | Full edit dialog added (previously add+delete only, despite the API already supporting `PUT`) — name/quantity/unit/expiry plus **notes** (free text) and **category** (same `GROCERY_CATEGORIES` slugs shopping lists use; edit dialog's category dropdown initially displayed the raw slug/`__other__` instead of the translated label — fixed by passing a value→label render function to `SelectValue`, same pattern already used in `shopping-list-view.tsx`'s sort dropdown). List always renders all 9 category sections (8 `GROCERY_CATEGORIES` + Other) as collapsible groups, even empty ones — not just categories that currently have items — and items can be dragged between sections (dnd-kit `useDraggable`/`useDroppable`, cross-category drop only; no within-category manual reorder since pantry items have no `sortOrder` column to persist one). An "Auto-categorize" action (same `guessAisle` heuristic as the shopping list) fills in categories for uncategorized items in one click. Includes a bulk case-insensitive name+unit merge endpoint (used by the scan-confirm flow). | `apps/web/app/api/v1/pantry/**`, `apps/web/components/meal-plan/pantry-manager.tsx`, `apps/web/components/pantry/pantry-item-dialog.tsx` |
-| Ingredient alias matching (new 2026-07-24) | Exists | The `ingredients` table (canonical name + `aliases[]`) existed but was never populated or queried anywhere. Seeded with ~10 bilingual EN/FR staples (salt, sugar, pepper, flour, butter, milk, egg, onion, garlic, olive oil — `packages/db/src/seed.ts`) and wired into every place that compares ingredient names by raw text: pantry add/edit (sets `ingredientId` when a name/alias matches), the can-cook / "use it up soon" scorer, auto-deduct-on-cook, and shopping-list pantry-awareness on generation. Resolution happens at compare-time from free text (`resolveIngredientKey`), not from a stored FK on both sides — recipe ingredients still don't carry `ingredientId`. **No UI to add/edit aliases** — the seed list in `packages/db/src/seed.ts` is the only place today; growing it means editing that file and re-running `pnpm db:seed` (idempotent, `onConflictDoNothing` on name). | `apps/web/lib/ingredient-match.ts`, `packages/db/src/seed.ts` |
+| Ingredient alias matching (new 2026-07-24) | Exists | The `ingredients` table (canonical name + `aliases[]`) existed but was never populated or queried anywhere. Seeded with ~10 bilingual EN/FR staples (salt, sugar, pepper, flour, butter, milk, egg, onion, garlic, olive oil — `packages/db/src/seed.ts`) and wired into every place that compares ingredient names by raw text: pantry add/edit (sets `ingredientId` when a name/alias matches), the can-cook / "use it up soon" scorer, auto-deduct-on-cook, and shopping-list pantry-awareness on generation. Resolution happens at compare-time from free text (`resolveIngredientKey`), not from a stored FK on both sides — recipe ingredients still don't carry `ingredientId`. | `apps/web/lib/ingredient-match.ts`, `packages/db/src/seed.ts` |
+| Admin: Ingredients CRUD (new 2026-07-24) | Exists | Admin-only page to manage canonical ingredients + aliases without a code change/redeploy — previously the seed file was the only place to add them. Create/edit/delete; name is unique (409 on collision); deleting an ingredient just unlinks any pantry item/recipe ingredient that pointed at it (`ingredientId` is `onDelete: "set null"` on both), nothing else is deleted. | `apps/web/app/admin/ingredients/page.tsx`, `apps/web/app/api/v1/admin/ingredients/**`, `apps/web/components/admin/ingredients-manager.tsx` |
| Merge duplicate pantry items (new 2026-07-24, relaxed same day) | Exists | One-shot cleanup for pre-existing pantry rows that are the same ingredient under different names (added before alias matching existed) — groups by resolved ingredient key **alone** (unit is deliberately not part of the grouping key, so two rows merge even with mismatched, missing, or differently-unit'd quantities); sums quantities only when every row in a group has a parseable quantity **and** the same unit, otherwise keeps the first known (quantity, unit) pair rather than guessing. Concatenates notes, keeps the soonest expiry. Manual trigger (a button in the pantry toolbar), not automatic — manual single-item add still always inserts a new row rather than silently merging, since two batches of the same ingredient can have different expiry dates worth tracking separately. | `apps/web/app/api/v1/pantry/merge-duplicates/route.ts` |
| Auto-deduct pantry on cook | Exists (closed 2026-07-24) | Both UI callers that previously hardcoded `deductFromPantry: false` (`batch-cook-dishes.tsx`, `meal-planner.tsx`) now pass `true`. The new general "Mark cooked" feature (see below) additionally exposes it as a per-cook checkbox, default checked, rather than a silent always-on. Matching now goes through the ingredient-alias resolver, not a raw name string compare. | `apps/web/app/api/v1/recipes/[id]/cooked/route.ts`, `apps/web/components/recipe/batch-cook-dishes.tsx`, `apps/web/components/meal-plan/meal-planner.tsx` |
| Cook log edit/delete (new 2026-07-24) | Exists | Plain (non-batch) cook-log entries can now be listed, edited (date/servings/notes), and removed — previously log-once, no way to fix a mistake or remove a duplicate entry. The recipe page's "Cooked N times" text is a hover tooltip (up to 8 dates, "+N more" beyond) that also opens a full manage sheet on click. Editing/deleting never touches pantry quantities — a deduction from when the entry was created isn't reversed or reapplied. | `apps/web/app/api/v1/recipes/[id]/cooked/[logId]/route.ts`, `apps/web/components/recipe/{mark-cooked-section,edit-cook-log-dialog}.tsx` |
diff --git a/apps/web/app/admin/ingredients/page.tsx b/apps/web/app/admin/ingredients/page.tsx
new file mode 100644
index 0000000..6ee5a99
--- /dev/null
+++ b/apps/web/app/admin/ingredients/page.tsx
@@ -0,0 +1,31 @@
+import type { Metadata } from "next";
+import { db, ingredients, asc } from "@epicure/db";
+import { requireFullAdminPage } from "@/lib/require-admin-page";
+import { IngredientsManager } from "@/components/admin/ingredients-manager";
+
+export const metadata: Metadata = {};
+
+export default async function AdminIngredientsPage() {
+ await requireFullAdminPage();
+
+ const rows = await db.select().from(ingredients).orderBy(asc(ingredients.name));
+
+ return (
+
+
+
Ingredients
+
+ Canonical ingredients and their name variants (e.g. "sel", "sel fin", "table salt") — lets pantry items and recipe ingredients written differently still be recognized as the same thing. Used by pantry add/edit, can-cook scoring, auto-deduct on cook, and shopping-list pantry-awareness.
+