Full audit (bugs/UI-UX/backend/feature-gap) turned up a money-leak AI quota
bypass, webhook SSRF, and a long tail of missing pagination/auth/a11y work.
Fixes land together since HANDOFF.md tracked them as one backlog.
- AI routes charge tier quota before generating; nutrition POST is author-only
- Webhook dispatch re-validates URL per delivery (SSRF/DNS-rebinding), treats
redirects as failures; recipe.published now actually dispatches
- New indexes/unique constraints on recipes, meal-planning, comments FK cascade
- Recipe PUT/restore snapshot only inside the transaction, after validation
- Recipe DELETE cleans up S3 objects (recipe + review photos)
- Optimistic UI (favorite/star/follow/shopping-list) rolls back on failure
- Upload presign enforces file size cap + per-tier storage quota
- Route-level loading/error/not-found states across (app), admin, and root
- middleware.ts guards (app)/admin; requireAdmin checks DB role, not cached
session; rate limiting applied to both session and API-key branches,
bucketed per key; Stripe webhook dedupes by event id
- Pagination added to recipes, feed, profile, comments, pantry, admin tables
- Nav shows real avatar + profile link + dark-mode toggle; destructive actions
standardized on AlertDialog
- Unsaved-changes guard + real ingredient/step validation on recipe form;
canonical /recipes/[id] used in-app; next/image migration; aria-labels and
alt text across icon buttons, avatars, recipe photos
- packages/api-types removed (zero callers, too drifted to safely rewire);
openapi.ts and ai-keys error shape drift fixed; BYOK decrypt failures now
surface instead of silently falling back to the platform key
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pantry page now shows a "Use it up soon" widget with recipes that use
soon-to-expire pantry items, across the user's own recipes plus public/
unlisted ones (the existing /recipes/can-cook page only looked at the
user's own). Extracted the matching/scoring logic shared by both into
lib/pantry-match.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root layout used title.template ("%s | Epicure") and 38 pages set
their own title, producing "Recipes | Epicure", "Messages — Epicure",
etc. Drop the template, set a flat "Epicure" default, and clear every
page's title override so they all inherit it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added a shared ExportMarkdownButton (copy to clipboard / download .md)
next to every existing print button: recipe, shopping list,
collection, meal plan, pantry. Each surface gets a small serializer in
lib/markdown/ built from data already in scope at that page — no new
queries except pantry, where items now thread through as a prop to
PantryPageHeader instead of being fetched only for PantryManager.
Also fixes an unrelated bug hit while verifying the collection export:
RecipeCard called the client-only useTranslations() hook without
"use client", so it rendered fine everywhere it happened to run inside
an already-client tree but 500'd — "Couldn't find next-intl config
file" — when Next tried to run it as a Server Component, which only
happens on the collection detail page (its only caller). Collections
with recipes in them were completely broken.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>