Correction de comportement sur la gestion des recettes de sources
externes — l'implémentation précédente avait dérivé d'une lecture
erronée du besoin :
- Plus aucun bouton d'import nulle part. Parcourir une source
(RecipesPage, hors planning) ne fait plus jamais que prévisualiser
— RecipeDetailPanel n'affiche plus de lien "Importer cette
recette", seulement un bouton icône discret vers la page d'origine
quand la recette en a une (nouveau .recipe-detail-panel__source-link,
même emplacement que l'étoile favori).
- Une recette externe n'est importée dans la base qu'au moment où
quelqu'un l'ajoute effectivement à son planning — jamais avant.
RecipePickerDialog.handleSelectDraftItem est désormais le seul
endroit de toute l'appli qui importe quoi que ce soit : cliquer sur
un item pas encore importé y déclenche une tentative d'import
transparente (POST /sources/.../import puis POST /planning/items),
sans écran intermédiaire, dès que rien ne manque
(tryBuildCompleteImport, nouveau apps/web/src/features/recipes/
recipe-import-draft.ts). Seul un ingrédient non résolu (ou une
erreur réseau) fait encore basculer vers l'écran de revue existant
(ImportRecipePage), pré-rempli, pour compléter ce qui manque.
- RecipeSourcesPanel gagne onSelectDraftItem (remplace planningSlot,
qui n'a plus de raison d'être puisqu'il n'y a plus de lien d'import
à qui le transmettre) : quand ce callback est fourni
(RecipePickerDialog uniquement), un item pas encore importé n'est
plus prévisualisé sur place, il est remonté tel quel à l'appelant.
Tests :
- planning.feature : le scénario existant retire l'étape "je clique
le lien Importer cette recette" (redirection désormais automatique
puisque le draft de test a un ingrédient non résolu) ; nouveau
scénario pour le chemin transparent (draft entièrement résolu,
aucun écran de revue).
- recipe-sources.feature : le scénario qui important depuis /recettes
(hors planning) est supprimé — cette capacité n'existe plus hors
planning. Le scénario de deep-link vérifie maintenant l'absence du
bouton d'import et la présence du lien discret.
- pnpm exec tsc -b --force (web) — propre.
- pnpm exec biome check — propre.
- pnpm --filter web build — propre.
- Cypress non exécutable localement sur cette machine (crash GPU
Electron connu) — scénarios vérifiés par relecture attentive
contre le markup/les clés i18n réels ; CI (GitHub Actions) fera
foi à l'exécution.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suite au retour utilisateur sur le plan « onglet Sources » livré
précédemment (#44-#48) : navigation transparente, page recette pour un
item externe, comportement d'ajout au planning déjà importé.
L'onglet « Sources » (RecipesPage) reste un onglet à part (décision
explicite : pas de fusion des listes perso/foyer/publique/externe) —
mais son affichage se comporte désormais « comme si c'était importé » :
- `SourceItemPreviewPanel` est supprimé, fusionné dans
`RecipeDetailPanel` lui-même (nouvel état `"loaded-draft"`) : un item
pas encore importé se voit exactement comme une vraie recette — même
en-tête, même mise en page description/étapes — la seule différence
étant les actions proposées (« Importer cette recette » là où une
vraie recette montre Modifier/Supprimer). La liste brute des
ingrédients et l'indice « non résolu » disparaissent de cette vue :
cette complexité reste réservée à l'écran de revue d'import
(ImportRecipePage), pas à un simple aperçu.
- Un item pas encore importé gagne une vraie URL adressable —
`/recettes/sources/:sourceKey/:externalId` (nouvelle route,
RecipesPage) — au même titre qu'une vraie recette a `/recettes/:id`.
Avant, le sélectionner ne changeait que de l'état React local dans
`RecipeSourcesPanel`, sans URL propre : ni lien direct, ni retour
arrière/rafraîchissement possibles. `RecipeSourcesPanel` gagne
`initialSelection`/`onItemSelected` pour rester piloté par cette URL
sans avoir à connaître le routage lui-même — `RecipePickerDialog`
(qui prévisualise dans une modale sans URL propre) laisse les deux
non renseignés et garde son comportement inchangé.
- `onSelectImportedRecipe` (déjà présent) continue de traiter un item
déjà importé exactement comme une vraie recette — c'est justement ce
qui rend la navigation transparente pour ce cas.
Le troisième point du retour (vérifier si la recette est déjà en base
avant de l'ajouter au planning, ne rien faire si oui, l'importer sinon)
était déjà le comportement de #48 — inchangé ici, aucune régression:
`RecipePickerDialog` résout un item déjà importé vers sa vraie recette
sans ré-import, et n'importe que les items qui ne le sont pas encore.
Aucun changement backend.
Tests :
- Cypress : nouvelle assertion d'URL dans le scénario « Previews a
not-yet-imported item » de recipe-sources.feature, et nouveau
scénario « Deep-links straight to a not-yet-imported item's own
page » — la CI confirmera.
- `pnpm --filter api test` — 282 tests toujours au vert (aucun
changement backend).
- `pnpm exec tsc -b --force` (web) — propre.
- `pnpm exec biome check` — propre.
- `pnpm -r build` — propre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expose côté UI ce que tech-step-matcher.ts détecte déjà à la sauvegarde
(Step.techSteps) mais qui restait backend-only : dans le panneau détail
d'une recette, les mots exacts ayant déclenché une technique sont
surlignés, avec un tooltip (survol/focus clavier) donnant son nom.
- tech-step-matcher.ts : matchTechStepSpans(description, mappings) expose
désormais {techStepId, start, end} en plus de la simple séquence d'ids
(déjà calculé en interne, jusqu'ici jeté). matchTechSteps devient un
wrapper fin dessus — aucun changement à ses ~12 tests existants ni à
recipe-translation.ts.
- StepTechStep gagne start/end (nullable, pas de backfill — même leçon que
l'incident de migration ingredient_unit_catalog : NOT NULL sans défaut
sur une table déjà peuplée casse le déploiement). Une ligne pré-existante
sans span est simplement omise de la réponse API plutôt que de fuiter un
null, jusqu'à ce que la recette soit resauvegardée.
- recipe.service.ts : createRecipe/updateRecipe persistent start/end ;
StepView expose techSteps: { techStep: {id,key}, start, end }[]. Le
recalcul complet à chaque édition (ajout/modif/suppression d'étape) était
déjà garanti par le delete-then-recreate existant d'updateRecipe — testé
explicitement (nouveau test "recomputes techniques from scratch...").
- Frontend : StepDescription.tsx (découpe le texte via
highlight-tech-steps.ts, pur et testé) remplace le <p> brut dans
RecipeDetailPanel. Nouveau Tooltip.tsx (composants/ui, CSS pur, aucune
lib externe — même esprit que Dialog.tsx) : un <button> (focusable
nativement, pas de tabIndex sur un <mark> non interactif) affiche le nom
de la technique (catalog.techSteps.<key>) au survol/focus.
Tests : matchTechStepSpans (spans corrects, chevauchement résolu),
recipe.test.ts (forme API + recalcul complet sur modif/ajout/suppression
d'étape, avec vérification que les anciennes lignes StepTechStep sont bien
supprimées), splitDescriptionByTechSteps (tri, bornes invalides ignorées,
chevauchement résiduel ignoré), scénario Cypress recipes.feature
(surlignage + tooltip au focus).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute Recipe.portions (combien de portions la recette produit telle
qu'écrite) — formulaire de création/édition, fiche détail, migration
Prisma (backfill à 4, même pattern que planning_item.portions).
Le sélecteur de recette du planning pré-remplit désormais son propre
champ "portions" depuis cette valeur au lieu de toujours démarrer à 1
(RecipeSummaryView.portions), tout en gardant PlanningItem.portions
indépendant (une recette peut être mise à l'échelle pour un créneau).
Couverture : tests API (création/édition/validation), scénarios
cypress (formulaire + préchargement en édition).
* chore(web): session de polish global — version, checkbox, danger zone, icônes
- Affiche le numéro de version (package.json, injecté via Vite) en bas de
la sidebar, masqué en mode collapse et en mobile.
- Factorise les checkbox/radio dupliqués (AllergySelect, DietTagSelect,
IngredientPicker, UserPreferencesPage) en composants partagés
CheckboxOption/RadioOption (components/ui/), et inverse le layout pour
que la case soit à gauche du label.
- Teinte la "zone de danger" de suppression de compte en rouge (fond +
bordure), pas seulement le bouton.
- Migre les icônes de navigation générale vers lucide-react (nav-icons.tsx
devient un fichier de ré-export) ; les pictogrammes d'ingrédients métier
restent en SVG custom (pas d'équivalents fins côté lucide).
Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle manuelle (sidebar desktop/collapsed/mobile, light/dark).
* feat(web): icônes d'ingrédients depuis foodiconpack.com + page de crédits
- Remplace 19 des 22 pictogrammes génériques d'ingrédients par des icônes
curées du pack gratuit "Common ingredient icons"/"Common Utensils" de
foodiconpack.com (CC BY 4.0) : carotte, pomme, basilic, bœuf, poulet,
saumon, crevette, riz, pois chiches, amandes, lait, cheddar, œufs,
cannelle, miel, huile d'olive, bière, marmite, sucre.
- BREAD/DOUGH/SPROUT restent en SVG custom : pas d'équivalent net dans le
pack (packs "ingrédients"/"ustensiles"/"plats"/"boissons" vérifiés).
Architecture inchangée : `icon` reste un enum de 22 valeurs partagées en
base (pas de migration, pas de mapping par ingrédient — cf. le
commentaire du fichier sur l'historique emoji→enum générique).
- Nouveau wrapper FilledIcon (fill="currentColor", viewBox 2048) à côté du
wrapper Icon existant (stroke) — les deux stylent au même endroit via
CSS, donc le mélange des 19+3 icônes reste visuellement homogène.
- Ajoute /parametres/credits (CreditsPage) créditant foodiconpack.com et
liant la licence CC BY 4.0, requis par la licence des icônes utilisées ;
nouvelle entrée de nav "Crédits" (icône lucide Info).
Vérifié : pnpm build, pnpm lint, pnpm --filter web e2e (43/43), et
vérification visuelle (grille des 22 icônes dans le picker, page crédits).
* feat(web,api): zone dangereuse rouge, préférences élargies, onglet favoris par défaut, e2e recettes, catalogue en uid+i18n
- Zone dangereuse (compte) : le bouton "Supprimer mon compte" est rouge.
- Pages préférences/paramétrage : contenu centré et élargi (32rem -> 56rem)
au lieu de coller à gauche sur un écran large.
- Page recettes : l'onglet "Favoris" est sélectionné par défaut.
- Ajout de apps/web/cypress/e2e/recipes.cy.ts (onglets, recherche, sélection
master-detail, favori, suppression, lien nouvelle recette).
- Catalogue de référence (ingrédients/régimes/allergènes) : la colonne
`name` (le libellé français, utilisé comme clé unique) devient `key`, un
slug stable et opaque au sens produit (ex. "vegetarien", "boeuf_hache").
Le libellé lui-même déménage entièrement côté client, dans
apps/web/src/locales/fr/translation.json sous le namespace `catalog.*`,
résolu via `t(\`catalog.ingredients.${key}\`)` etc. — même schéma que
IngredientCategory/IngredientSubcategory. Migration Prisma
(rename + backfill des ~456 lignes déjà seedées), seed/service/tests API
et composants web mis à jour en conséquence.
- apps/api/src/utils/slugify.ts + scripts/generate-catalog-i18n.ts
(regénère le fichier de traduction depuis reference-seed-data.ts).
- 102 tests Mocha + 32 scénarios Cucumber passent contre la base migrée.
Note : cypress run plante dans cet environnement (le processus GPU
Chromium/Electron crash même headless, indépendamment des flags) — les
recipes.cy.ts n'ont pas pu être exécutés ici ; vérifiés par lecture du code
source des composants visés et par un passage manuel dans le navigateur de
prévisualisation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(api): les uids du catalogue sont en anglais, pas des slugs français
reference-seed-data.ts reste rédigé en français (c'est juste le libellé
d'autoring, jamais stocké/exposé), mais la clé stable (`Diet.key`/
`Category.key`/`Ingredient.key`) qu'on en dérive doit elle-même être un
identifiant anglais, indépendant de la langue d'autoring — pas juste le
même texte français passé à slugify().
- apps/api/src/db/catalog-en-keys.ts : dictionnaire écrit à la main
(label français -> clé anglaise) pour les 5 régimes, 14 allergènes et
437 ingrédients ; getEnglishKey() lève une erreur explicite si un
nouvel élément n'a pas encore d'entrée plutôt que de retomber sur un
slug français silencieux.
- scripts/validate-catalog-en-keys.ts : vérifie que chaque diet/allergène/
ingrédient de reference-seed-data.ts a une entrée, et que les clés
anglaises résultantes sont uniques (437/437, 14/14, 5/5 — zéro manquant,
zéro collision).
- reference-seed-data.ts et scripts/generate-catalog-i18n.ts utilisent
désormais getEnglishKey() au lieu de slugify(nom français).
- Nouvelle migration (20260818193000_catalog_keys_to_english) qui
remappe les lignes déjà seedées avec un slug français (par la migration
précédente) vers leur clé anglaise définitive.
- apps/web/src/locales/fr/translation.json régénéré : catalog.* est
maintenant indexé par clé anglaise ("vegetarian", "eggs",
"ground_beef"...), toujours avec le libellé français en valeur.
- Tests/step-definitions mis à jour (getEnglishKey() au lieu de
slugify()) ; 102 tests Mocha + 32 scénarios Cucumber passent contre la
base migrée. Vérifié aussi en direct via GET /reference/diets et
/reference/allergies.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(web): crypto.randomUUID plante hors contexte sécurisé, empêchant d'associer un ingrédient
Écran noir + "TypeError: crypto.randomUUID is not a function" au clic sur
une carte d'ingrédient dans le formulaire de recette. crypto.randomUUID()
n'est défini que dans un "contexte sécurisé" (https, ou littéralement le
host "localhost") — il est absent sur une IP locale (test sur un vrai
appareil), dans une WebView Capacitor (l'enrobage mobile prévu pour cette
app), ou en http sur un vrai domaine. RecipeFormPage/StepListEditor s'en
servaient pour générer l'identité React (`key`) de chaque ligne
d'ingrédient/étape en brouillon.
- apps/web/src/lib/client-key.ts : remplace par un générateur qui ne
touche jamais `crypto` — un compteur + Math.random suffit, cette valeur
n'a besoin d'être unique que le temps de la session de rendu, jamais
envoyée au serveur.
- apps/web/cypress/e2e/recipe-form.cy.ts : couvre l'association d'un
ingrédient (recherche, sélection, exclusion du picker une fois
sélectionné, retrait), la création et l'édition d'une recette, et un
test de non-régression dédié qui supprime crypto.randomUUID avant le
chargement de la page (comme le ferait un vrai contexte non sécurisé)
pour vérifier que l'ajout de plusieurs ingrédients/étapes ne plante
plus.
Vérifié en direct dans le navigateur de prévisualisation en supprimant
crypto.randomUUID à la main (reproduit le crash), puis en confirmant que
l'ajout d'ingrédient fonctionne à nouveau après le correctif. cypress run
ne peut toujours pas s'exécuter dans cet environnement (voir le commit
précédent) — non exécutés avec Cypress lui-même, mais vérifiés par
lecture des sélecteurs réels et rejoués à la main dans le navigateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome
- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
key manquant, ça affichait littéralement "catalog.diets.undefined" au
lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
{key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
au lieu du message "Cette recette n'existe pas." — bug dans mon propre
test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
avaient quelques soucis de formatage/tri d'imports (des sed multi-
fichiers, pas d'édition via l'outil habituel) — corrigés par
`biome check --write`.
Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>