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} a répondu à votre commentaire",
"reaction": "{name} a réagi à votre commentaire",
"rating": "{name} a noté votre recette",
"mention": "{name} vous a mentionné dans un commentaire"
"mention": "{name} vous a mentionné dans un commentaire",
"detail": {
"comment": "{name} a commenté votre recette « {title} »",
"rating": "{name} a noté votre recette « {title} » {stars} étoiles"
},
"pushTitle": {
"follow": "Nouvel abonné",
"comment": "Nouveau commentaire",
"reply": "Nouvelle réponse",
"reaction": "Nouvelle réaction",
"rating": "Nouvelle note",
"mention": "Vous avez été mentionné"
}
},
"recipe": {
"byAuthor": "par {name}",
@@ -82,6 +94,11 @@
"pairingBulkSuccess": "{count} recettes générées — retrouvez-les dans votre bibliothèque",
"pairingDialogTitle": "Compléter le repas",
"pairingDialogDescription": "Plats suggérés par l'IA qui accompagnent bien cette recette. Générez-en n'importe lequel comme nouvelle recette.",
"forkTooltip": "Copier cette recette",
"forked": "Recette copiée dans votre bibliothèque",
"forkFailed": "Échec de la copie de la recette",
"forkLimitReached": "Limite de recettes atteinte pour votre offre",
"forkedFrom": "Copiée depuis {title}",
"pairingFindingLabel": "Recherche des meilleurs accords…",
"pairingSuggestButton": "Suggérer des accompagnements",
"pairingRegenerate": "Régénérer",
@@ -499,6 +516,15 @@
"tagLabel": "Tag",
"clearFilters": "Effacer les filtres"
},
"recipeNotes": {
"title": "Vos notes personnelles",
"visibilityHint": "Visible uniquement par vous — jamais partagé avec l'auteur de la recette ou quiconque d'autre.",
"placeholder": "ex. : j'ai mis moins de sel que ce que la recette indique, très bon avec du riz à la place des pâtes…",
"save": "Enregistrer",
"saving": "Enregistrement…",
"saved": "Note enregistrée",
"saveFailed": "Échec de l'enregistrement de la note"
},
"recipeForm": {
"titleLabel": "Titre *",
"titleRequired": "Le titre est requis",
@@ -678,6 +704,7 @@
"copiedToClipboard": "Liste copiée dans le presse-papiers",
"exportBuildFailed": "Impossible de générer l'export",
"instacartNotConfigured": "Instacart n'est pas encore configuré",
"alreadyInPantry": "Déjà au garde-manger",
"items": "{checked}/{total} articles",
"itemsChecked": "{checked}/{total} articles cochés",
"fromMealPlan": " · Depuis le planning repas",
@@ -860,6 +887,9 @@
"changePasswordButton": "Changer le mot de passe",
"passwordChanged": "Mot de passe modifié avec succès.",
"passwordChangeFailed": "Échec de la modification du mot de passe.",
"downloadData": "Télécharger mes données",
"downloadDataDescription": "Obtenez un export JSON de toutes les données personnelles qu'Epicure détient sur vous — votre profil, vos recettes, votre activité et vos paramètres.",
"downloadDataButton": "Télécharger mes données",
"webhookCreateFailed": "Échec de la création du webhook",
"webhookDeleteFailed": "Échec de la suppression du webhook",
"webhookUpdateFailed": "Échec de la mise à jour du webhook",