Parse @username in comment content, notify mentioned users (dedup
against recipe-author/parent-author who are already notified via
comment/reply), and render @username as a link to their profile.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- user_blocks table (composite PK), Block/Unblock button on profiles.
Blocking severs any existing follow relationship both ways and
prevents the blocked party from following, commenting on the
blocker's recipes, or the blocker from seeing their comments.
- reports table (recipe/comment/user targets, pending/reviewed/
dismissed status). ReportButton on comments, admin review queue at
/admin/reports with dismiss/mark-reviewed actions, audit-logged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Part of the social-feature backlog (follow, comments, reactions, ratings,
feed, threading) audited earlier — see conversation.
- notifications table: follow/comment/reply/reaction/rating events,
replaces the fully-dead feed_items table (feed_item_type enum existed
but had zero references anywhere in the codebase).
- Bell UI in the nav with unread badge, mark-all-read, 30s poll.
- Rate limiting on comment posting (20/min), follow/unfollow (30/min),
and comment reactions (60/min) — previously unthrottled.
- /recipes/[id] queried by (id, authorId=session.user) only, so any
recipe not owned by the viewer 404'd regardless of visibility.
Widen the query to include public/unlisted recipes and gate the
owner-only actions (edit, delete, version history, translate, AI
content generation) behind an isOwner check.
- user_follows had no primary key/unique constraint, so the follow
route's onConflictDoNothing() was a silent no-op — concurrent follow
clicks could insert duplicate rows and inflate follower counts. Add
a composite primary key on (follower_id, following_id).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- New invites table: token-gated signup, optional email lock,
role/tier override, single-use, expiry.
- SIGNUPS_DISABLED site setting toggle at /admin/settings.
- databaseHooks.user.create gate in auth/server.ts blocks new account
creation (email + Google OAuth) when disabled unless a valid invite
cookie is present; applies invite role/tier and marks it consumed.
- /admin/invites: create/list/revoke shareable invite links.
- /admin/users: "Create user" dialog — admin sets email/role/tier,
account is pre-verified, user gets a set-password email (admin
never sees a password).
- Signup page reads ?invite=, validates via public
/api/v1/invites/[token], locks the form when signups are closed
and no valid invite is present.
- proxy.ts: allowlist /api/v1/invites/ for anonymous invite checks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- signOut() cleared the cookie but never navigated, leaving the user
stuck on the current page — redirect to /login on success.
- better-auth's core "image" field had no matching Drizzle column
(schema uses avatarUrl), so every OAuth signup threw
BetterAuthError: unable_to_create_user, silently failing before a
session was ever created. Remap via user.fields.image.
Use -1 as a sentinel meaning "no cap" in tier_definitions numeric
columns. checkAndIncrementTierLimit skips the usage-cap WHERE clause
when the limit is -1. Admin tier UI gets an Unlimited switch per
field that disables the input and sends -1.
- 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>
AI routes had no consistent error handling — a provider failure (e.g.
OpenRouter returning a degraded-model error) crashed the route as an
uncaught 500 and, worse, still charged the user's monthly aiCall
quota for a request that never succeeded.
Adds withAiQuota()/aiErrorResponse() (lib/ai/ai-error.ts) and applies
them across all 12 AI generation routes: charges the quota, runs the
AI call, refunds the credit and returns a clean user-facing message
if it throws. Frontend needs no changes — existing dialogs already
toast whatever `error` string comes back.
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>
Fixes the 13-item codebase health scan backlog: wraps meal-plan
generation in a transaction, adds missing userId/GIN indexes, fixes
an IPv6-parsing gap in the webhook SSRF guard (and an identical
duplicated bug in the AI URL-import path, now consolidated onto one
implementation), paginates the collections list, dedupes the AI
recipe Zod schemas, wires up Stripe tier sync, rate-limits AI key
rotation, gets `pnpm typecheck` actually working, and adds test
coverage for the previously-untested admin/webhooks routes.
Two flagged issues (collection removeRecipeId IDOR, tier-limit race)
turned out to already be fixed/non-issues on inspection — noted in
TODO.md rather than silently dropped.
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>
Add root Dockerfile (standalone Next output, multi-stage pnpm build), drop
in-stack caddy in favor of publishing web's port for an external traefik
LXC (file-provider dynamic config included), and document the portainer
deploy flow.
Also fixes issues that blocked any production build: a bad auth-client
type cast, the ai SDK's mimeType->mediaType rename, an implicit-any
callback param, and push.ts eagerly calling webpush.setVapidDetails at
module import time (which crashed page-data collection whenever VAPID
env vars weren't present at build) — now lazily configured on first send.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>