Commit Graph

9 Commits

Author SHA1 Message Date
Arnaud 1dd8abfd52 fix: standardize locked-vs-hidden treatment across all 9 per-tier gated features (v0.79.0)
Rule, applied consistently everywhere via a new isFeatureAvailableAnyTier() helper: if a feature is enabled on at least one tier, it stays visible for locked-out viewers with a small "Pro" badge and opens an upgrade prompt on click; if a feature is disabled on every tier, it hides entirely, since there's no upgrade path to point at.

Covers: recipe variations, meal/drink pairings, nutrition estimation, Markdown export (5 call sites), weekly nutrition, import from URL, import from photo, and the Instacart grocery-delivery menu item. Previously inconsistent — some hid outright, one showed a lock icon overlapping its own icon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:33:04 +02:00
Arnaud 55c6fc5ab7 feat: 6 new per-tier feature flags (off by default) + flag-icon locale switcher + hide BYOK when disabled (v0.74.0)
Extends the existing feature-flags system (previously 3 keys, all
default-enabled) with 6 more: recipe_import_url, recipe_import_photo,
nutrition_estimation, markdown_export, weekly_nutrition,
grocery_delivery. Each FEATURE_DEFINITIONS entry now carries its own
defaultEnabled -- the new 6 default to false, the original 3 stay
true -- so no migration/seed was needed for the "off by default"
requirement, just a per-key fallback instead of a blanket true.

Gated server-side (requireFeatureEnabledResponse, a new shared
helper avoiding six copies of the same try/catch) on: import-url,
import-photo, nutrition POST estimate, bulk markdown export, weekly
meal-plan nutrition GET, Instacart export. Gated client-side by
hiding the trigger entirely (not just disabling) on every page that
renders one: recipe detail (meal/drink pairing buttons, nutrition
panel's estimate button, markdown export), recipes list (import-URL
button, including the OS Share Target auto-import path), new-recipe
page (photo import), meal-plan page (markdown export, weekly
nutrition bar), shopping-list/collection/pantry pages (markdown
export), shopping-list page (Instacart button, now gated by both the
existing env-var check AND the tier flag).

Also: BYOK section on Settings -> AI now hidden entirely for
non-BYOK users (previously showed a locked-and-teased notice, same
inconsistency the Model Prefs fix closed yesterday). Language
switcher shows a flag icon (FlagGB/FlagFR, moved from
components/marketing to components/shared so both the logged-in
settings switcher and the logged-out marketing one can use it)
instead of plain "English"/"Français" text-only options.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 11:16:41 +02:00
Arnaud 002f14ced0 feat: batch-cook shopping list (already worked) + leftover expiry reminders
Shopping list add already worked generically for batch-cook recipes —
no code needed there.

New: mark a specific batch-cook dish as "cooked today", track its
fridge expiry (cookingHistory.batchDishId), surface a "Leftovers
expiring soon" widget on the pantry page, and send a daily push+email
reminder via a new /api/internal/cron/leftover-reminders endpoint
(mirrors the weekly-digest cron pattern; doesn't use the social
notifications table, which requires a non-null actor and isn't built
for self-reminders).

Also fixes, from user-reported bugs:
- Recipe cards showed no batch-cook badge/dish-count/prep-time in some
  views — added dishCount + prepMins/cookMins (now generated by the AI
  and persisted) to the card component and /recipes query.
- Batch-cook descriptions occasionally contained raw markdown
  (**bold**) — added explicit "plain prose only" prompt instructions
  and a stripMarkdown() defensive fallback at render time.
- Truncated/cut-off descriptions — the generateObject call had no
  maxOutputTokens set, so long structured responses could get cut off
  mid-field; now capped explicitly at 8000.
- Generate dialogs (batch-cook + the main AI dialog) could show
  buttons unreachable once the progress bar appeared mid-generation —
  restructured so the action row is pinned outside the scrollable
  content area, not affected by content height changes.
- /api/internal/* routes were being redirected to /login by middleware
  before their own CRON_SECRET check ever ran (pre-existing bug,
  affected the weekly-digest cron too) — added to PUBLIC_PATHS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 10:03:52 +02:00
Arnaud b0849c3989 feat: cooking history/gallery, unit conversion, nutrition diary, pantry scan, digest cron, nutrition-targeted meal plans
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>
2026-07-10 08:06:28 +02:00
Arnaud 362f65656b fix: audit fixes — tier-quota bypass, webhook SSRF, auth hardening, pagination, a11y
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>
2026-07-09 21:50:35 +02:00
Arnaud cd444d4d23 feat: surface pantry-expiry recipe suggestions
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>
2026-07-09 15:26:44 +02:00
Arnaud 212d6f7335 fix: browser tab title should always just say "Epicure"
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>
2026-07-09 14:58:03 +02:00
Arnaud 1677e40668 feat: copy/export as Markdown wherever print exists
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>
2026-07-08 22:15:48 +02:00
Arnaud 3636ab27ae feat(meal-plan): weekly planner, pantry, shopping lists, nutrition tracking
AI-generated weekly meal plans with pantry-awareness. Manual entry per slot.
Pantry inventory management. Auto-generated shopping lists from meal plan.
Weekly nutrition bar chart vs daily goals. Nutrition goals settings.
2026-07-01 08:10:39 +02:00