The existing recipe chat (RecipeChatPanel) is tightly scoped to one
recipe — recipeId is required and its system prompt embeds that recipe's
full ingredient/step context. Added a sibling, not a modification: a new
floating assistant on the recipes list page for questions that aren't
about any specific recipe (technique, substitutions, timing, food safety).
New /api/v1/ai/cooking-chat route (same quota/rate-limit/model-resolution
pattern as recipe-chat, minus the recipe lookup) and CookingAssistantPanel
component (same floating-button + Sheet UI). Positioned at bottom-24
rather than bottom-6 so it doesn't sit in the same band as the recipe
list's floating bulk-action bar during select mode.
Verified locally: asked a real cooking question through the actual UI,
got a correctly-formatted markdown answer with no recipe context leaking
in (confirmed the request payload only carries the question, no recipeId).
sourceUrl was already persisted and shown on the recipe detail page, but
never surfaced on cards in the grid/list/compact views or the simpler
RecipeCard (collections) — added a small external-link icon with a
tooltip naming the source hostname, sized identically to the existing
batch-cook badge so it doesn't reintroduce the compact-view alignment
bug fixed earlier this session.
The dish-count/servings and time spans had no fixed width (unlike the
difficulty badge slot, which reserves w-16 even when empty) — a batch-cook
recipe's longer "N dishes"/"Nm total" text, or a missing time value,
changed that column's width row-to-row, shifting the date and icon
cluster after it. Gave both spans fixed widths so every compact row has
identical column layout regardless of content.
- AI recipe generation always used the app's own locale rather than a
separate language picker — removed the picker, generation and the
saved recipe's language now both follow useLocale() directly.
- Bulk-select action bar had an ungated max-w-md that capped it at 448px
even past the sm: breakpoint where sm:w-auto was supposed to let it
grow to content — added sm:max-w-none so desktop never scrolls.
- Batch-cooking generation had no free-text "describe" field like the
standard recipe generator does — added one (BatchCookFields, both the
standalone dialog and the AI dialog's batch tab), threaded through the
API route and generateBatchCook's prompt as additional guidance.
- Recipes imported from a URL never actually got their sourceUrl
persisted — CreateRecipeSchema didn't declare the field, so Zod
silently stripped it before it ever reached the insert. The detail
page's "Source: <hostname>" link already existed but was dead code
for every real import until now.
Verified all 4 live: generate dialog has no language selector, batch
tab has a working describe textarea, bulk-select bar renders at full
content width with zero horizontal overflow on desktop, and a recipe
created with sourceUrl now round-trips and renders its source link.
recipe-form.tsx previously had no batch-cook awareness at all — editing an
AI-generated batch-cook recipe and saving silently corrupted it (steps lost
their per-dish `appliesTo` tags, recipeBatchDishes rows went stale/orphaned,
since the create/update API schemas and edit-page query never touched them).
Adds a "batch-cook recipe" toggle to the form, an editable dish list (name,
description, fridge days, freezer-friendly/note, day-of instructions), and
per-step dish tagging (click a dish chip to mark which dish(es) that step
advances; empty = shared prep). Renaming a dish propagates to any step
still tagged with the old name instead of orphaning it. Wired through
Create/Update API schemas and the edit page's query/payload.
Verified locally: created a 2-dish batch recipe from scratch through the
real form, confirmed it renders identically to an AI-generated one (grouped
steps, dishes & storage cards), edited it, renamed a dish, reloaded, and
confirmed the step tag followed the rename rather than orphaning.
recipe-form/API routes were never sending/persisting photos on create+update.
PhotoUploader accumulated uploads via a stale closure over the photos prop, so
only the last file of a multi-select survived. Once both were fixed, thumbnails
still 400'd because Next's image optimizer blocks upstream hosts resolving to
private/loopback IPs (localhost:9000 MinIO) — skip optimization for
storage-hosted photos since they're already web-sized user uploads.
- Presigned upload URLs were signed against STORAGE_ENDPOINT (the
internal docker hostname, e.g. http://minio:9000) instead of a
browser-reachable URL — uploads/edits with photos were broken in
prod. Now signed with a separate client bound to STORAGE_PUBLIC_URL,
which also needed threading through as a Docker build arg (CSP
headers are computed at build time) and into compose.prod.yml/
.env.production (gitignored, not committed).
- recipe-form.tsx used the wrong i18n namespace for the visibility
label (t("recipeForm") instead of t_recipe("recipe")), causing
MISSING_MESSAGE in French.
- Compact recipe-list view: batch-cook rows showed no dish count, and
the date/difficulty columns shifted per-row depending on whether a
difficulty badge was present — reserved a fixed-width slot for it.
- All three card icon badges (batch-cook, favorite, visibility) now
share the same muted color instead of the batch badge standing out.
- Recipes filter dropdown: clicking a filter option closed the whole
menu, and the tag text input couldn't be typed into (the menu's
roving-focus/type-ahead handling was intercepting keystrokes) — menu
items now stay open on click, and the tag input stops event
propagation so it behaves like a normal text field.
- Recipe form: added a floating save/cancel bar so long recipes don't
require scrolling back down to save.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Batch-cook indicator moved from next to the title into the icon row
alongside favorite/visibility (all three now get a tooltip, matching
the existing favorite-button pattern).
- Favorite button's tooltip text was hardcoded English ("Save"/
"Saved") instead of using the translation keys already used for its
aria-label.
- Shortened the batch-cook generate dialog's "Generating…" label
(fr.json edited by user) — the long text was the real cause of the
modal overlap, not the dialog structure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Batch-cook recipes no longer live in a separate /batch-cooking section:
- Removed the dedicated page/route and its nav button.
- /recipes shows both kinds together, with a chef-hat badge marking
batch sessions, and a filter to isolate/hide them.
- "Batch cooking" is now a third tab in the existing "Generate recipe
with AI" dialog (alongside Describe/Photo) instead of a separate
button — one AI entry point instead of three.
- Extracted the counters/difficulty/dietary form into BatchCookFields,
shared between that dialog and the meal-planner's batch-cooking
dialog.
- Also fixed a dialog resize issue (progress bar mounting mid-generate
grew the dialog's height, which the positioning library didn't
always handle) by reserving space up front and capping dialog height
with a scroll fallback.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Recipes page action buttons reordered/restyled to push AI generation
and batch cooking ahead of manual recipe creation.
- Recipe detail page hides meal/drink pairing (doesn't make sense for
a multi-dish batch session).
- Print pages force light mode regardless of app theme (dark bg +
hardcoded dark text was unreadable) and render sectioned steps +
dishes/storage for batch-cook recipes.
- Markdown export mirrors the same sectioned structure.
- Cooking mode tags each step with which dish(es) it belongs to.
- Meal planner: mealPlanEntries.batchDishId lets a slot point at one
specific dish within a batch session; picking a batch-cook recipe
now prompts for which dish, and the grid shows the dish name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New AI-generated batch-cooking flow: pick N dinners/lunches + servings,
get one unified recipe covering a full prep session — merged shopping
list, steps tagged per-dish (a step can advance several dishes at once,
e.g. a shared oven bake), and per-dish storage (fridge/freezer) + exact
day-of reheat/finishing instructions.
Schema: recipes.isBatchCook, recipeSteps.appliesTo (dish names), new
recipeBatchDishes table. Batch recipes are excluded from the normal
/recipes list and live under their own /batch-cooking section.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every "nothing here" page had its own copy-pasted dashed-border box —
icon + one muted line, inconsistent (some had a CTA, some didn't, no
description text anywhere). Replaced with one shared EmptyState
component: icon in a soft tinted circle, a real heading plus optional
description, and primary/secondary actions (either a Link or an
arbitrary action slot for things like "New Collection" that open a
dialog rather than navigate).
Applied to: recipes (no recipes / no search match), favorites, feed
(no one followed / no new posts / trending / for-you), collections
(index + detail), shopping lists, pantry, notifications, can-cook.
Left the small inline "no trending"/"no recent" lines inside Explore's
already-labeled sections and the notification-bell dropdown alone —
different context, a full empty-state box would be heavier than the
space warrants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
stopPropagation() alone doesn't cancel a Link's native anchor navigation —
that's the browser's default action for the click reaching the <a>, not
a bubbling JS handler propagation can block. Needed preventDefault() too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was floating over the cover photo with a circular dark backdrop — too
heavy. Moved into the bottom meta row next to the visibility lock icon,
plain ghost icon button matching the recipe detail page's style exactly
(no background, same size/variant). List and compact rows were already
placed there with no backdrop, only the grid card needed this.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Added a heart toggle to all three recipe-grid views (grid card overlay,
list row, compact row), hidden while in multi-select mode. Detail page
already had it; this was the other place it made sense.
- recipes/page.tsx now fetches which of the current page's recipes are
already favorited and passes it through.
- FavoriteButton gained an optional iconClassName so the unfavorited heart
reads on a photo overlay (was invisible in light mode against the dark
backdrop — filled/favorited state was already fine since it's opaque red).
- Fixed the floating bulk-select action bar overflowing both edges of the
viewport on mobile (screenshotted: centered with no max-width, wider
than the screen, clipping "Supprimer" on one side and the selected-count
label on the other). Now caps to viewport width, scrolls horizontally
as a fallback, and drops button text labels below sm (icon + aria-label
+ tooltip only), matching the icon-row convention used elsewhere.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Favoriting already worked (heart toggle on the recipe detail page, DB
table, API route) but there was nowhere to see what you'd favorited —
the actual missing piece behind "add a favorites feature".
- New /recipes/favorites page: paginated grid of favorited recipes (cover
photo, difficulty, time, author), sorted by most-recently favorited.
A previously-favorited recipe stays visible even if the author later
goes private, matching the existing private-accounts scope decision
(existing access isn't revoked, only new discovery is affected).
- Unfavoriting from that page removes the card immediately — added an
optional onToggle callback to FavoriteButton for this.
- Added a lightweight "My Recipes / Favorites" tab pair at the top of
/recipes linking between the two, rather than a new top-level nav item
(nav is already at 8 entries).
Deliberately out of scope: favorite toggles on the main recipe grid, feed
cards, or search/explore results — favoriting your own recipes is odd
UX, and wiring session-aware favorited state into the public
(unauthenticated) search route is a separate, bigger change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Five S-sized items from HANDOFF.md's new-features backlog, all wiring up
previously-orphaned infra:
- createNotification now sends web push + email for every notification type
(follow/comment/reply/reaction/rating/mention), not just comments
- Personal recipe notes: private per-user notes on any viewable recipe
(recipeNotes table had zero API/UI before this)
- Recipe fork/clone: deep-copies a viewable recipe into your own library as
a private draft, linked via recipeVariations, respects tier quota
- Pantry-aware shopping lists: meal-plan-generated lists now subtract
on-hand pantry quantities (ingredientId match, falling back to normalized
name match) and flag partial/ambiguous matches instead of guessing
- GDPR data export: downloadable JSON of a user's own content and activity
across every relevant table, secrets/internal tables excluded
New migrations 0025 (unique index for recipe-notes upsert) and 0026
(shopping_list_items.in_pantry) generated, left unapplied like 0023/0024.
Verified with typecheck, lint, and a full local `docker build`.
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>
Select recipes → add to an existing collection or create a new one inline. Fixes duplicate "visibility" i18n key bug found during verification.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Grid was the only layout. Add a view-mode toggle (grid/list/compact)
persisted to localStorage:
- List: thumbnail + description + inline metadata, one per row
- Compact: dense single-line rows (thumb, title, servings/time/
difficulty/date/visibility) for scanning large libraries
Selection mode and bulk actions work identically across all three.
Also fixes a real bug hit while wiring this up: "recipe.visibility"
was defined twice in the messages files — once as a flat string
("Visibility", used as the bulk-actions dropdown label) and once as
the {private,unlisted,public} label object. The object silently won
in JSON, so t("visibility") resolved to an object and next-intl threw
INSUFFICIENT_PATH the moment you opened that dropdown. Renamed the
flat one to visibilityMenuLabel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Version compare used positional (index-by-index) list alignment —
inserting one ingredient in the middle shifted every subsequent line
out of alignment, making the whole rest of the list look changed.
Switch to diffArrays (LCS-based) so insertions/deletions are
detected properly; adjacent removed+added chunks still get paired
for word-level diffing so a single edited item doesn't render as a
full delete+insert.
- Translated "Title"/"Description"/"Ingredients"/"Steps" diff section
headers, "Version History" sheet title, compare/restore buttons,
loading/empty states, and the version-detail ingredient/step count
summary (with proper plural forms).
- formatDate() in version history was hardcoded to "en-US" regardless
of the app's locale — now uses the session locale.
- ExportMarkdownButton had no hover tooltip (bare title attribute) —
wrap it in the same Tooltip pattern every other icon button in the
toolbar uses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full sweep of hardcoded English strings across:
- Meal pairing and drink pairing dialogs (titles, descriptions, role/
type labels, regenerate/generate buttons, progress labels) — also
fixed drink type labels that had French text hardcoded regardless
of locale ("Sans alcool"/"Chaud").
- Nutrition panel — had no useTranslations at all.
- Comments: header, empty/loading state, post/reply/cancel/delete
buttons, relative timestamps (just now/Xm ago/Xh ago/Xd ago), and
comment-reactions' toasts + aria-labels.
- Rating stars toasts.
- Direct messages: thread, conversation list, message button, both
/messages pages.
- People search page and component.
- URL import dialog (title, description, buttons, toasts).
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>
- sourceUrl was already saved on URL import but never displayed —
render it as a link (hostname only) under the description.
- Extract cook-mode's wake lock into a reusable useWakeLock hook, add
visibility-change re-acquisition (a wake lock releases when the tab
goes background and won't come back on its own), and use it on the
regular recipe view too, not just cook mode.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ai-generate-dialog: useLocale() returns {locale,setLocale} object, not
string — was stringifying whole object as default language value
- auth/server: add trustedOrigins so session cookie survives reverse-proxy
deployments where BETTER_AUTH_URL differs from container's own view
- admin: add tier limit editor (/admin/tiers) and per-user usage reset
button, both audit-logged
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mobile:
- Recipes search bar full-width on mobile instead of capped narrow
- Cook mode ingredients panel stacks above the step instead of
squeezing it into a narrow column
- Version history Compare/Restore buttons wrap onto their own row
- Recipe edit ingredient fields wrap instead of forcing horizontal
scroll on narrow viewports
i18n: translates remaining hardcoded strings across recipes
filter/sort, adapt-recipe and AI variations dialogs, the full
settings section (sidebar + 6 sub-pages + BYOK/model-prefs/
API-keys/webhooks managers), explore tab, collections (new/fork/
share dialogs), meal planning (planner, AI generation phases, new
shopping list, shared-plan view), photo import, recipe bulk-select
toolbar, and recipe action-button tooltips. Also fixes the recipes
page subtitle, which wasn't just unworded but missing its {count}
interpolation entirely — it always rendered as the bare word
"results" regardless of how many recipes existed.
Feature: adds a ShareRecipeButton that copies the public /r/{id}
link to the clipboard, with a notice when the recipe isn't Public
yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the six previously-unscoped feature ideas plus a mobile
layout fix reported via screenshot:
- Mobile: Recipes/Collections/Pantry/Meal Plan/Shopping Lists headers
now stack and wrap instead of clipping buttons on narrow viewports.
- Recipe diff/compare view: word/list diff against any past version,
next to Restore in version history.
- Shared meal plans & shopping lists: new shoppingListMembers/
mealPlanMembers tables (viewer/editor roles, mirrors
collectionMembers), share dialogs, membership-checked routes.
- PDF cookbook export: /print/collection/[id] renders a whole
collection with page breaks, using the existing print-CSS pattern
instead of adding a PDF rendering dependency.
- Grocery delivery handoff: shopping lists can copy-as-text (works
today) or send to Instacart once INSTACART_API_KEY is configured
(stub adapter — real API needs a partner agreement).
- Personalized "For You" feed tab: ranks public recipes by tag/
dietary overlap with the user's favorited/highly-rated history.
- PWA: added manifest.json + icons on top of the existing service
worker so the app is installable; cook-mode pages were already
cached for offline use.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recipe cards, the recipe detail page, and meal planner still rendered raw
"{n} servings"/"{n}m cook"/"{n} srv" text and untranslated difficulty labels
outside the earlier i18n pass. Also wires the user's locale into AI features
that previously always answered in English regardless of app language:
recipe chat, ingredient substitution, recipe ideas, and generate-from-idea.
The recipe-language picker in the AI generate dialog now defaults to the
user's locale instead of always defaulting to English.
Wires dozens of components and server pages to next-intl instead of hardcoded
English text, and adds a lightweight getMessages/formatMessage helper for
server components (print pages, public recipe page, cook metadata) since
next-intl/server isn't wired into this app's routing. Backfills missing
en/fr message keys that existing code already referenced but fr.json lacked.
- Substitute finder never applied the user's BYOK/admin AI key (only fell
back to raw process.env), so it silently failed whenever the key was
stored via settings rather than a literal env var. Now resolves via
getDefaultProviderWithKey like every other AI route. Popover also
surfaces real error messages instead of swallowing failures.
- Ingredients with quantity 0 (salt, pepper, "to taste") rendered the
literal digit "0" in cooking mode, print view, public recipe page,
shopping lists, and serving scaler — several sites relied on generic
truthiness/filter(Boolean), which doesn't catch a stored "0" string.
Added a shared hasQuantity() helper and applied it everywhere quantity
is rendered, plus in the AI recipe-chat context sent to the model.
- Recipe chat panel rendered a duplicate close button on top of shadcn
Sheet's own built-in close X, producing a garbled overlapping glyph.
Removed the duplicate.
- Recipe chat assistant replies are markdown from the model but were
rendered as raw text; added react-markdown so formatting actually
renders.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>