feat: push+email notifications, recipe notes, fork/clone, pantry-aware lists, GDPR export

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>
This commit is contained in:
Arnaud
2026-07-10 07:50:58 +02:00
parent d035378520
commit 45b886e398
23 changed files with 9813 additions and 23 deletions
+31 -1
View File
@@ -29,7 +29,19 @@
"reply": "{name} replied to your comment",
"reaction": "{name} reacted to your comment",
"rating": "{name} rated your recipe",
"mention": "{name} mentioned you in a comment"
"mention": "{name} mentioned you in a comment",
"detail": {
"comment": "{name} commented on your recipe \"{title}\"",
"rating": "{name} rated your recipe \"{title}\" {stars} stars"
},
"pushTitle": {
"follow": "New follower",
"comment": "New comment",
"reply": "New reply",
"reaction": "New reaction",
"rating": "New rating",
"mention": "You were mentioned"
}
},
"recipe": {
"byAuthor": "by {name}",
@@ -81,6 +93,11 @@
"pairingBulkSuccess": "{count} recipes generated — find them in your library",
"pairingDialogTitle": "Complete the meal",
"pairingDialogDescription": "AI-suggested dishes that pair well with this recipe. Generate any of them as a new recipe.",
"forkTooltip": "Fork this recipe",
"forked": "Recipe forked to your library",
"forkFailed": "Failed to fork recipe",
"forkLimitReached": "Recipe limit reached for your tier",
"forkedFrom": "Forked from {title}",
"pairingFindingLabel": "Finding perfect pairings…",
"pairingSuggestButton": "Suggest pairings",
"pairingRegenerate": "Regenerate",
@@ -508,6 +525,15 @@
"tagLabel": "Tag",
"clearFilters": "Clear filters"
},
"recipeNotes": {
"title": "Your private notes",
"visibilityHint": "Only visible to you — never shared with the recipe's author or anyone else.",
"placeholder": "e.g. I used less salt than the recipe calls for, great with rice instead of pasta…",
"save": "Save",
"saving": "Saving…",
"saved": "Note saved",
"saveFailed": "Failed to save note"
},
"recipeForm": {
"titleLabel": "Title *",
"titleRequired": "Title is required",
@@ -690,6 +716,7 @@
"copiedToClipboard": "List copied to clipboard",
"exportBuildFailed": "Could not build export",
"instacartNotConfigured": "Instacart isn't configured yet",
"alreadyInPantry": "Already in pantry",
"items": "{checked}/{total} items",
"itemsChecked": "{checked}/{total} items checked",
"fromMealPlan": " · From meal plan",
@@ -872,6 +899,9 @@
"changePasswordButton": "Change password",
"passwordChanged": "Password changed successfully.",
"passwordChangeFailed": "Failed to change password.",
"downloadData": "Download my data",
"downloadDataDescription": "Get a JSON export of all the personal data Epicure holds about you — your profile, recipes, activity, and settings.",
"downloadDataButton": "Download my data",
"webhookCreateFailed": "Failed to create webhook",
"webhookDeleteFailed": "Failed to delete webhook",
"webhookUpdateFailed": "Failed to update webhook",