Six M-sized items from HANDOFF.md's new-features backlog:
- Profile tabs: cooking-history stats (total cooked, last-cooked, streak)
and a "cooked it" photo gallery, both owner-only
- Display-time unit conversion (metric<->imperial) for recipe ingredients,
respecting each user's unitPref; original value always shown alongside
the conversion
- Nutrition daily diary: per-day macro totals computed from cooking history
x recipe nutritionData, compared against user goals
- Pantry scan: real barcode lookup (zxing + Open Food Facts, no API key)
with an AI-vision fallback for unbarcoded items, always confirm-before-
insert, both paths tier/rate-limited like other AI features
- Weekly digest email: new followers/comments/ratings + trending recipes,
sent via a new `cron` Docker stage (alpine+crond+curl) and `digest-cron`
compose service hitting a bearer-token-protected internal route
- Meal-plan generation can now target a user's nutrition goals as a
prompt-level nudge (recipes are AI-invented, not DB-sourced, so this
can't be a hard macro constraint)
Caught a real deploy-breaking issue while adding the cron stage: appending
it after `runner` silently changed the Dockerfile's default build target,
and `web`'s compose config didn't pin one — fixed by pinning `target:
runner` explicitly. Verified with typecheck, lint, and three separate
`docker build --target` runs (runner/cron/migrator) plus `docker compose
config` validation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>