Réutilise l'API Releases de Forgejo (publique, sans authentification) comme
source de vérité pour "quelle est la dernière version" — zéro nouvel
endpoint backend : useAppUpdateCheck compare App.getInfo().version
(@capacitor/app, nouvelle dépendance) au tag_name de
GET .../releases/latest, et pointe "Mettre à jour" vers l'URL stable de
téléchargement (issue #65).
Comparaison de version en logique pure (version-compare.ts), séparée du
hook effectful — testée isolément via un test de composant Cypress
(version-compare.cy.tsx, même patron que highlight-tech-steps.cy.tsx :
aucun montage, juste describe/it/expect sur des fonctions pures).
Natif uniquement (Capacitor.isNativePlatform()) : @capacitor/app et
@capacitor/preferences restent en import dynamique dans le hook, jamais
dans le bundle web (même patron que capacitor-token-store.ts) — vérifié
dans le build (deux petits chunks séparés, pas dans le bundle principal).
Vérification au démarrage à froid + à la reprise depuis l'arrière-plan
(appStateChange), avec un throttle de 6h et une mémorisation de la version
déjà rejetée dans @capacitor/preferences pour ne jamais re-harceler sur la
même version. Bannière discrète (pas une Dialog modale), bouton "Mettre à
jour" ouvrant le lien de téléchargement dans le navigateur système plutôt
qu'un flux PackageInstaller in-app.
Vérifié : pnpm lint, pnpm --filter web build, le test de composant (9/9),
et layout/smoke/planning-page e2e (18/18, la bannière étant un no-op sur
le web ne devait rien casser — confirmé).
Closes#66
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
StepDescription faisait `useState(techSteps)` puis `liveTechSteps.flatMap(...)` :
quand la prop `techSteps` arrivait `undefined`, `.flatMap` levait
`TypeError: Cannot read properties of undefined (reading 'flatMap')`, remontée
par Cypress comme une erreur applicative non capturée. Observé de façon
intermittente sur le parcours « modifier une recette » : après enregistrement,
`RecipeFormPage` navigue vers `/recettes/:id`, dont le détail se rendait parfois
avant que `techSteps` ne soit peuplé.
- `StepDescription` : `techSteps` devient optionnel et `liveTechSteps` retombe
sur `[]` (dans le `useState` et le `useEffect` de resync), pour que tous les
consommateurs (`.flatMap`, `splitDescriptionSegments`,
`findTechStepForTemperature`, lookup `activeStepTechStep`) aient toujours un
vrai tableau — même posture « tolérer la donnée manquante, ne pas planter »
que le reste du composant.
- `recipe-form.ts` : le mock de la recette 7 envoyait une étape sans `techSteps`,
contrairement au vrai `StepView` et à tous les autres mocks de la suite —
corrigé en `techSteps: []`.
- `recipes.cy.ts` : nouveau test de non-régression déterministe — un payload
d'étape sans `techSteps` doit rendre la description en texte brut. Vérifié
qu'il échoue bien (`flatMap` undefined) sans le correctif du composant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'UI par-dessus l'API `/admin/api/tours*`. Nouvelle entrée
`/admin/didacticiels` dans `AdminLayout` (i18n `admin.nav.tours`), route
sous `RequireAdmin`+`AdminLayout` dans `App.tsx`.
`ToursPage.tsx` : liste des scénarios à gauche (nom, état activé/désactivé,
nombre d'étapes) ; à droite l'éditeur du scénario sélectionné —
- renommer, activer/désactiver ;
- éditer la liste d'étapes (route, cible via `<select>` alimenté par
`GET /tours/targets`, titre, contenu), réordonner (↑/↓), ajouter,
retirer ;
- « Enregistrer » → un seul `PATCH /:id` qui remplace tout (name/enabled +
étapes rénumérotées `order` = index), erreurs API affichées telles quelles ;
- « Supprimer le scénario » (confirmation `window.confirm`) ;
- « Nouveau scénario » → formulaire clé + nom → `POST /`.
`admin-client.ts` gagne `getTourScenarios` / `getTourTargets` /
`createTourScenario` / `updateTourScenario` / `deleteTourScenario`.
Tests Cypress (`admin-tours.cy.ts`, 4) : liste (activés + désactivés,
nombre d'étapes) ; édition nom + réordonnancement d'étapes → un seul PATCH
avec la liste complète réordonnée ; création → POST `{key,name}` ;
suppression après confirmation → DELETE. 4/4 en local ; `pnpm build` +
`pnpm lint` verts.
Closes#44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consomme le socle backend de #42/#44 (`GET /reference/tours`,
`GET/POST /profile/tours`). Deux tours : orientation générale (auto sur
`/`) et correction des métadonnées de recette (auto à l'ouverture du
premier popover de correction).
- Dépendance `driver.js` (~5 ko, MIT, zéro dépendance).
- `features/tour/tour-engine.ts` : wrapper vanilla autour de `driver.js` —
gère ce que la lib ne fait pas seule : la navigation React Router entre
étapes situées sur des routes différentes, et un `onFinish` unique
(`"completed"` seulement si on atteint la fin, `"skipped"` pour la croix
/ Échap / clic sur l'overlay).
- `features/tour/TourContext.tsx` : `TourProvider` (charge scénarios +
progression, keyed sur `user?.id` comme `ThemeContext`) et `useTour()`
(`startTour(key)` / `isTourCompleted(key)` / `tours` / `isReady`).
`isReady` est **calculé au render** contre l'id utilisateur chargé — pas
stocké — pour qu'un changement de `user` le repasse à `false`
immédiatement, avant qu'un effet enfant (l'auto-déclenchement) ne tourne
sur des données de complétion périmées.
- `main.tsx` : `<TourProvider>` sous `ThemeProvider`.
- Attributs `data-tour-*` : liens de nav (`AppLayout`), grille / bouton `+`
du 1er créneau / bouton Cuisiner (`PlanningPage`), page liste de courses
(`ShoppingListPage`), description d'étape + section technique du popover
(`StepDescription` / `TechStepCorrectionPopover`).
- Déclenchement auto : tour `general` sur `/` (`PlanningPage`, ref-gardé),
tour `techStepMetadata` à `activeCorrection` non-null (`StepDescription`).
- Section « Revoir un tutoriel » dans `/parametres/preferences-utilisateur`
— un `map` sur `tours`, un bouton « Revoir » par scénario.
- `tour.scss` re-skinne le popover `driver.js` sur les tokens du thème.
- i18n `tour.*` (chrome uniquement : boutons du popover, section « Revoir » ;
le contenu des étapes vient de la base).
Tests Cypress (`tour.feature`, 5 scénarios) : auto-démarrage + élément
surligné pour un nouveau venu ; « Terminé » → complétion enregistrée
`skipped:false` ; croix → `skipped:true` ; un tour déjà vu ne se relance
pas ; « Revoir » le relance depuis les paramètres. Vérifié 5/5 en local.
`pnpm build` + `pnpm lint` verts.
Refs #42
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un visiteur déconnecté sur `/` voit désormais une vraie page de
présentation avant qu'on lui demande de se connecter, au lieu d'être
renvoyé directement vers `/login`.
- `RequireAuth.tsx` devient sensible au chemin : dans sa branche `!user`,
`pathname === "/"` rend `<LandingPage />` ; toutes les autres routes
protégées redirigent vers `/login` exactement comme avant. Aucun
changement de la table de routes — `/` reste `<PlanningPage>` niché sous
`RequireAuth`+`AppLayout`, et `AppLayout` (donc la sidebar) ne monte
jamais pour la vitrine.
- `pages/landing/LandingPage.tsx` + `landing-page.scss` : hero (accroche +
illustration SVG dessinée à la main dans le langage de `lucide-react`,
zéro dépendance ni asset externe) + deux CTA, grille de 4
fonctionnalités (icônes lucide), section « aperçu », CTA final. Copie
entièrement sous le namespace i18n `landing.*`.
- `index.html` : favicon SVG (monogramme « bC ») + `<meta description>` —
`/` est maintenant une page publique partageable.
- Les 3 captures d'écran réelles de la section « aperçu » sont servies
statiquement depuis `public/landing/` (un fichier manquant retombe sur
son `alt`, ne casse pas le build) ; elles restent à produire par un
humain avec l'app lancée sur des données réelles — cf.
`public/landing/README.md`.
- Tests Cypress `landing.feature` : vitrine visible déconnecté sur `/`,
CTA vers `/login` et `/signup`, autre route protégée toujours redirigée,
visiteur connecté voit bien le planning. `smoke.cy.ts` mis à jour pour
tester la redirection depuis `/recettes` (puisque `/` ne redirige plus).
Closes#41
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Réutilise la philosophie déjà en place pour la sidebar (défilement
horizontal plutôt que compression illisible) au lieu d'un nouveau patron
par page. Tout est strictement additif sous 640px — le rendu desktop est
inchangé.
- `AppLayout.scss` : `.app-content` passe de `--space-xl` (32px) à
`--space-md` (16px) sous 640px — 64px de largeur utile récupérés sur un
écran de ~375px.
- `planning-page.scss` : la grille 7 jours défile déjà horizontalement
(`overflow: auto` + `min-width: 62rem`) ; ce qui casse au tactile, c'est
le `✕` de suppression d'une recette (`opacity: 0` hors survol) — épinglé
visible + cible tactile agrandie sous 640px, idem le bouton « + ».
- `dialog.scss` : `.dialog-panel` garde une gouttière latérale et gagne en
hauteur utile sous 640px (le `--space-2xl` vertical est trop cher sur un
écran court).
- `recipe-picker-dialog.scss` : le split 2 colonnes forcé du catalogue
(plus spécifique que le `@media` de recipes.scss) repasse à une colonne
sous 640px.
Tests : `layout.cy.ts` gagne un bloc « page content under 640px » —
padding de `.app-content`, grille sur son propre scroll sans déborder la
page, `✕` visible au tactile. 10/10 en local.
Refs #39
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dernière PR (4/4) du chantier « gestion des restes » (issue #34).
`@dnd-kit/core` (+ `@dnd-kit/utilities`) — première et seule utilisation
de DnD de l'app. `PlanningPage` monte un `<DndContext>` (Mouse + Touch +
Keyboard sensors, distance d'activation 5px pour ne pas voler le clic
« Jeter ») autour de la grille et du panier.
- `LeftoversBasket` : chaque ligne a une poignée `⠿` `useDraggable` — le
nœud draggable est la poignée elle-même (petite), pas toute la `<li>`,
pour qu'une détection de collision au pointeur (`pointerWithin`, repli
`closestCenter` pour le clavier) résolve proprement vers la seule case
sous le curseur. `data` porte `sourceItemId`/`recipeName`/`available`.
- `MealCell` : `useDroppable` (`data: { date, weekDay, meal }`),
surbrillance `--drop-over` au survol. Un item « restes »
(`sourceItem !== null`) porte un badge ♻️ sur sa pastille.
- Au drop, `onDragEnd` ouvre `LeftoverAssignDialog` (quantité, défaut =
disponible, plafonné) → `POST /planning/items/leftover`
(`apiClient.assignLeftover`) → nouvel item patché dans `planning.items`.
Tests : `planning-leftovers.feature` (Gherkin) — parcours complet drag →
confirmation quantité → pastille ♻️ visible → compteur du panier à jour ;
+ jet d'un reste depuis le panier. Le drag est piloté par événements
souris bruts (mousedown poignée → mousemove ×2 → mouseup cible).
Vérifié localement : 2/2, et aucune régression sur
`leftovers-basket.cy.ts` / `planning-page.cy.ts` / `planning.feature`.
i18n : `planning.leftovers.*` (badge, poignée, dialogue). Docs :
`frontend-architecture.md`.
Closes#34
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR 3/4 du chantier « gestion des restes » (issue #34).
`LeftoversBasket` (`features/planning/`) : une icône 🧺 avec badge de
compte dans l'en-tête du planning, à côté de `WeekNavigator`. Ouvre un
popover listant les restes encore disponibles de la semaine affichée —
même patron montage conditionnel + fermeture au `mousedown` extérieur que
`CalendarPopover`.
Les lignes sont dérivées à chaque rendu de `planning.items` par
`collectLeftoverLines` (`leftovers-basket.ts`, pur) : chaque item « frais »
(`sourceItem === null`) dont `computeAvailableLeftover` est `> 0`, le
`leftoverUses` étant reconstitué depuis le tableau plat (les items
« restes » qui pointent vers lui) — aucun aller-retour API en plus.
Chaque ligne : recette, jour/repas d'origine, portions restantes, bouton
« Jeter » → `PATCH /planning/items/:id/discard-leftover` (`apiClient.
discardLeftover`), la réponse est patchée dans `planning.items` par id
(`handleLeftoverDiscarded`). État vide quand rien ne reste.
Le glisser-déposer d'un reste sur un autre créneau arrive en PR 4.
Tests : `leftovers-basket.cy.ts` (mode layout) — badge de compte, ouverture
/ fermeture au clic extérieur, contenu d'une ligne, état vide, « Jeter »
qui appelle le PATCH et retire la ligne. Vérifié localement, 3/3.
i18n : `planning.leftovers.*`. Docs : `frontend-architecture.md`.
Réf #34
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Première PR du chantier « gestion des restes » (issue #34) — les
fondations, sans encore d'UI de panier.
À l'ajout d'une recette au planning on ne saisit plus qu'un seul nombre,
`portionsRequested` (« combien ce créneau doit nourrir »). Le serveur en
dérive `portions` (ce qui est réellement préparé) via
`computeBatchedPortions` : arrondi au multiple supérieur du rendement de
la recette (on cuisine des lots entiers, jamais une fraction de plat).
L'écart est le futur pool de restes de l'item.
Modèle (`PlanningItem`, purement additif) :
- `portionsRequested` NOT NULL — migration nullable → backfill (`= portions`
pour les lignes existantes, sans restes à l'époque) → verrou, même
discipline que `ingredient_unit_catalog`.
- `sourcePlanningItemId` (auto-FK nullable, `onDelete: Cascade`) et
`discardedPortions` (défaut 0) — posés maintenant, utilisés plus tard.
Partagé (`@batch-cooking/shared/logic/planning-portions.ts`) :
- `computeBatchedPortions(recipePortions, requested)` → `{ portions, leftover }`
- `computeAvailableLeftover(item)` → pool encore disponible (posé pour les
PRs suivantes : validation d'écriture côté API, affichage du panier
côté web — une seule formule, deux appelants)
API : `addPlanningItemSchema` prend `portionsRequested` ; `addPlanningItem`
calcule et stocke les deux valeurs + l'évènement analytics ; `PlanningItemView`
et `getPlanningForDate` exposent `portionsRequested`/`discardedPortions`/
`sourceItem`.
Front : `RecipePickerDialog` — champ « portions demandées » + indice en
direct « → N préparées, M en restes » quand l'arrondi crée un reste ;
envoie `portionsRequested`. `RecipeImportForm` — envoie `portionsRequested`
(un import atterrit à son propre rendement, donc reste nul).
Tests : `planning-portions.test.ts` (les deux exemples de #34 + bornes) ;
nouveau bloc `POST /planning/items` dans `planning.test.ts` (arrondi au
lot, plusieurs lots, borne exacte, 400 sur non-positif) ; fixtures
`planningItem.create` des suites existantes mises à jour ; scénario
Cypress planning ajusté (`portionsRequested` dans le corps).
Docs : `batch-cooking-modele.md`, `backend-architecture.md`,
`frontend-architecture.md`.
Réf #34
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Depuis la refonte #29-#32, attacher un ingrédient/ustensile/température à
une correction de technique demande de surligner le passage concerné dans
la description. Mais le `<p>` de la description est un frère du popover
dans le DOM, et le listener « clic extérieur » du popover (`mousedown`
hors de son propre DOM → `onClose`) fermait le popover dès le `mousedown`
qui démarre ce glisser-sélectionner — avant même le `mouseup`. Le
`mouseup` ouvrait alors un nouveau popover vierge pour le texte
sélectionné, traité comme une toute nouvelle proposition de technique (la
technique choisie et les métadonnées déjà ajoutées étaient perdues).
`TechStepCorrectionPopover` accepte désormais un prop optionnel
`descriptionContainerRef` (le `containerRef` que `StepDescription` a
déjà) : un `mousedown` à l'intérieur n'est plus un clic extérieur.
`StepDescription` pilote déjà explicitement chaque interaction dans la
description (`setActiveCorrection`/`setResolvedMetadataSpan`), donc retirer
cette fermeture concurrente ne perd rien ; un clic vraiment ailleurs ferme
toujours le popover.
Comble aussi le trou de couverture que `TechStepCorrectionPopover.cy.tsx`
prétendait déjà délégué à un scénario e2e inexistant : nouveau test dans
`recipes.cy.ts` avec une vraie sélection de texte (Range/Selection sur les
nœuds `data-offset`, séquence mousedown→mouseup réelle) — vérifié qu'il
échoue sans le correctif.
Closes#36
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le scénario échouait en CI sur deux points de fragilité, pas sur le fond :
1. `IntersectionObserver` ne se redéclenche qu'à une *transition*
d'intersection — avec des pages courtes la sentinelle restait visible en
continu et seule la 2e page se chargeait. Les pages font maintenant 15
éléments (la liste déborde son conteneur) et chaque page suivante est
tirée par un `scrollTo("bottom")` explicite, répété 5 fois.
2. Une fois la liste débordante, `should("be.visible")` sur un élément
sorti du conteneur `overflow:auto` échoue (même classe de bug que #1) —
l'assertion "3e page chargée" fait maintenant `.scrollIntoView()` avant.
L'assertion anti-boucle porte sur le journal des curseurs demandés
(tolérance 2× pour le double-effet React StrictMode du serveur e2e), pas
sur un `Set.size` strict.
Vérifié localement : 7/7 avec le correctif, "Page 3 Item 1" jamais chargée
sans lui.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le scénario ajouté supposait que l'IntersectionObserver se redéclenchait
tout seul avec une liste courte — faux : il ne se déclenche qu'à une
*transition* d'intersection. Les pages font maintenant 15 éléments (la
liste déborde son conteneur, la sentinelle démarre sous la ligne de
flottaison) et chaque page suivante est demandée via un
`scrollTo("bottom")` explicite, qui produit une vraie transition.
Avec l'ancien code bugué, la sentinelle stale rappelle « cursor-2 » au 2e
scroll : « Page 3 Item 1 » n'apparaît jamais et un curseur est redemandé
— les deux assertions échouent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux volets de l'issue #38.
**Bug de bouclage** — l'`IntersectionObserver` de `SourceItemTable` n'a
que `[hasMore]` comme dépendance et ne se réabonne donc jamais pendant un
scroll normal (`hasMore` reste `true`). Il figeait la toute première
closure `onLoadMore`, laquelle rappelait indéfiniment le même curseur
périmé — la liste s'allongeait en boucle avec les mêmes éléments et
`nextCursor` n'atteignait jamais son vrai `null`.
- `SourceItemTable` lit maintenant `onLoadMore` via une ref réassignée à
chaque rendu : l'observer appelle toujours la closure fraîche, sans
réabonnement inutile (intention d'origine du `biome-ignore` conservée,
le commentaire n'étant plus nécessaire).
- `RecipeSourcesPanel` dédoublonne le merge par `externalId` et traite
« cette page n'a rien apporté de nouveau » comme une fin de liste même
si la source renvoie encore un `nextCursor` — garde-fou contre une
éventuelle heuristique de fin imparfaite côté adaptateur.
**Couverture recherche vide** — `theMealDbAdapter.list()` tapait
`/search.php?s=` même sans requête, ce que TheMealDB borne à ~25 recettes
(`nextCursor` toujours `null`). Une requête vide déroule désormais la
liste des catégories (`/list.php?c=list` puis `/filter.php?c=…`), une
catégorie par pas de curseur, pour un vrai « tout parcourir ».
Tests : nouveau scénario Cypress à 3 pages avec curseurs distincts
réellement vérifiés (aucun curseur redemandé) ; nouveaux cas Mocha pour
la marche par catégories (première page + curseur, suivi du curseur,
arrêt sur la dernière catégorie, page vide au-delà, requête blanche =
requête vide).
Closes#38
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le tableau de bord /admin/monitoring fonctionnait en polling côté client
(15 s) déclenchant une sonde active côté serveur à chaque appel — deux
onglets admin ouverts = deux séries de sondes redondantes.
Inverse le mécanisme : le serveur sonde une fois par minute et pousse le
résultat aux clients connectés via WebSocket (socket.io, room
"monitoring"). Décisions actées avec l'utilisateur : l'endpoint REST
disparaît entièrement (plus de repli, 100% WebSocket) ; la sonde tourne
en continu dès le démarrage, sans lien avec la présence d'un admin
connecté (le plus simple des deux, coût des sondes négligeable).
Backend :
- ExpressServer (packages/express-tools) expose désormais son
http.Server sous-jacent (créé explicitement, plus dans .listen()) —
socket.io s'attache au serveur HTTP lui-même, pas à Express/un port.
- MonitoringBroadcastService (nouveau) : sonde immédiate au démarrage
puis toutes les 60 s (réutilise getMonitoring() tel quel, aucune
duplication), diffuse à la room "monitoring", rejoue le dernier
résultat en cache à tout nouvel admin qui rejoint (pas d'attente
jusqu'à une minute pour un onglet fraîchement ouvert).
- requireAdminSocket (nouveau) : middleware de handshake socket.io,
équivalent WebSocket de requireAdmin — même vérification (cookie +
JWT + tokenVersion), factorisée dans resolveAdminFromToken
(require-admin.ts) puisque les deux ne diffèrent que par où ils lisent
le cookie brut (un handshake socket.io ne passe pas par
cookie-parser).
- GET /admin/api/monitoring supprimée (route + service de test associés).
- server.ts (pas app.ts) instancie socket.io — les tests supertest n'ont
pas besoin d'un serveur WebSocket.
Frontend :
- MonitoringSocketClient (nouveau, api/monitoring-socket.ts) : wrapper
socket.io-client, interface MonitoringSocketClientLike volontairement
petite et injectable dans MonitoringPage (prop optionnelle) pour rester
testable sans serveur réel.
- MonitoringPage : data et connectionState séparés — une coupure de
connexion après un premier chargement affiche un badge "reconnexion en
cours" sans jamais effacer le dernier board connu (repose sur la
reconnexion automatique de socket.io-client). Seul le tout premier
chargement affiche l'état plein-écran chargement/erreur.
- adminApiClient.getMonitoring() supprimée (plus utilisée).
Tests :
- Nouveau apps/api/test/admin-monitoring-socket.test.ts : vraie connexion
socket.io-client contre un serveur réel démarré dans le test (port 0),
authentification avec un admin seedé + cookie signé, vérifie la
réception de monitoring:update et le rejet sans cookie admin valide.
Les tests heartbeat de l'ancien admin-monitoring.test.ts (route
/internal/tech-steps/heartbeat, sans rapport avec ce changement) sont
déplacés vers tech-step-worker.routes.test.ts, leur router d'origine.
- Nouveau cypress/component/MonitoringPage.cy.tsx : monte la page avec un
client factice, vérifie rendu des cartes, badge de reconnexion sans
perte du board, états chargement/erreur — pas de test e2e Gherkin
contre un vrai WebSocket (aucun mécanisme pour ça dans ce repo,
fragile ; le test Mocha ci-dessus est la preuve bout-en-bout serveur).
- apps/web/cypress/e2e/admin-monitoring.cy.ts (REST) supprimé, remplacé
par le test composant ci-dessus.
Vérification : pnpm lint clean, build complet du monorepo clean, suite
Cypress e2e complète 27/27 fichiers (114/114 tests), suite composant
complète 5/5 fichiers (54/54 tests, dont les 4 nouveaux). Suite Mocha
apps/api non exécutable cette session (Postgres/Docker injoignable,
comme pour #33) — le nouveau test socket.io n'a donc pas pu être
run-vérifié, seulement relu attentivement et vérifié par le lint.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un rafraîchissement (F5) sur /admin/monitoring affichait le JSON brut de
l'API au lieu de la page — même chose à craindre pour tout futur /admin/*
GET sans segment supplémentaire.
Cause : `server.mountRouter("/admin", adminRouter)` (app.ts) montait
l'API admin exactement sous /admin, la même racine que les pages
/admin/* d'apps/web, servies depuis la même origine/le même process en
prod (image Docker unique). Express matche par chemin, sans égard au
Accept — `GET /admin/monitoring` (route API) et la page du même nom
étaient en conflit direct ; l'API gagnait systématiquement (montée
avant serveStaticFrontend), donc chaque F5 sur cette page recevait le
JSON de l'API plutôt que index.html.
Seule /admin/monitoring collidait réellement aujourd'hui (seul
sous-routeur admin à répondre GET sur sa racine bare) mais le
mécanisme touche toute future route admin nommée comme sa page — d'où
un correctif structurel plutôt qu'un renommage ponctuel.
Correctif : l'API admin est montée sous /admin/api (app.ts), jamais
bare /admin. Toute la surface admin bouge en conséquence
(auth/metrics/monitoring/tech-steps/catalog) ; les pages front
(/admin/monitoring, /admin/catalogue, /admin/corrections...) sont
inchangées, seul admin-client.ts change de base URL. Mis à jour en
cohérence : commentaires JSDoc citant les routes (apps/api + apps/web +
packages/shared), specs/backend-architecture.md,
specs/batch-cooking-modele.md, les intercepts Cypress (globs + un
matcher regex qui avait échappé au premier passage) et les chemins
supertest du suite Mocha admin-*.test.ts.
Simplifie au passage admin-monitoring.cy.ts : le contournement
resourceType ajouté en #32 pour cette même collision n'a plus lieu
d'être une fois les chemins distincts (retour à un intercept déclaratif
simple) — le bug Cypress/Chrome qu'il avait mis au jour reste noté en
commentaire pour référence future.
Vérifié : pnpm lint clean ; build api+web clean ; suite e2e complète
28/28 fichiers, 116/116 tests verts (dont les 16 tests admin-*).
Suite Mocha apps/api non exécutable cette session (Postgres/Docker
injoignable) — chemins mis à jour mécaniquement en cohérence avec le
nouveau mount point, à faire tourner en CI/à la prochaine session avec
accès DB.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`admin-monitoring.cy.ts` échouait de façon reproductible ("renders one
card per service": cy.wait timeout, "No request ever occurred").
Diagnostiqué par isolation : le matcher déclaratif `resourceType: "fetch"`
de Cypress 13.17.0 ne capte plus du tout la requête réelle de
AdminApiClient.getMonitoring() sur le Chrome 152 de cette machine — un
intercept `{resourceType: "fetch"}` sans filtre d'URL ne voit passer que
/admin/auth/me, jamais /admin/monitoring, alors que le `req.resourceType`
de cette même requête, lu depuis un handler matché par url/method seuls,
vaut bien "fetch" de façon fiable et reproductible. Le filtrage déclaratif
est cassé, pas la requête elle-même.
Remplace le matcher par un handler de fonction qui laisse passer la
requête de document (`cy.visit()` lui-même, sur la même URL que l'API en
same-origin/CI) via `req.continue()` et ne stub que le reste — vérifié
stable sur 3 runs consécutifs.
Corrige au passage le test "shows an error state" qui passait pour la
mauvaise raison : sans serveur API réel, une requête non interceptée
échoue aussi au niveau réseau, ce que MonitoringPage.catch() ne
distingue pas d'un vrai 500 — désormais aliasé et attendu comme le
premier test, pour prouver qu'il dépend bien du mock.
Suite e2e complète: 28/28 fichiers, 116/116 tests verts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le step Gherkin partagé « the recipe should include the ingredient »
utilisait un `.should("be.visible")` nu sur `.ingredient-row__name`.
Sur le formulaire d'édition de recette, la liste d'ingrédients peut se
trouver sous le pli du scroll propre à `.app-content` (Nom/Description/
Photo/Portions/Visibilité/Régimes viennent avant) — un `.should`
seul ne scrolle pas automatiquement, contrairement aux commandes
d'interaction (`.click()`/`.check()`).
Repéré comme bug pré-existant et hors périmètre lors de la vérification
de #29/#30 (voir la note dans tech-step-metadata-editor-redesign.md).
Même correctif que « I should see the highlighted technique »
(common.steps.ts) et « I should see the highlighted temperature »
(recipes.ts) : `.scrollIntoView()` avant l'assertion.
Reproduit avant correctif (recipe-form.feature, scénario « Preloads an
existing recipe's ingredients when editing »: 4/5 puis 5/5 après le
correctif). Suite e2e complète: 27/28 fichiers verts (la seule
exception, admin-monitoring.cy.ts, est un flake distinct et
sans rapport, signalé séparément).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.recipe-detail-panel__steps utilisait un compteur CSS (counter-reset/
counter-increment: step) sur un sélecteur `li` — un descendant, pas
seulement les enfants directs. Depuis la refonte du popover de correction
(tech-step-metadata-editor-redesign), celui-ci imbrique ses propres <li>
plusieurs niveaux plus bas (une par ingrédient/ustensile/température
attaché, plus la liste du picker de technique — jusqu'à ~74 entrées) :
chacun incrémentait ce même compteur "step", faussant le numéro affiché de
toutes les étapes suivantes dès qu'une correction avec métadonnées
attachées (ou son picker de technique) était ouverte. C'est le bug
rapporté : "les étapes affichées sont numérotées n'importe comment".
Correctif : `li`/`img`/`p` → `> li`/`> img`/`> p` (enfants directs
uniquement) dans cette règle. Même principe pour `img`/`p` : leurs styles
(image bloc, paragraphe inline sans marge) ne doivent s'appliquer qu'au
propre <p>/<img> de l'étape, pas à ceux du popover imbriqué plus loin.
Test de non-régression ajouté (recipes.cy.ts) : ouvre une correction avec
ingrédient + ustensile attachés puis le picker de technique, vérifie via
getComputedStyle que ces <li> imbriqués n'ont pas rejoint la chaîne du
compteur (`counter-increment: none`) alors que les <li> des étapes
elles-mêmes l'ont toujours (`counter-increment: step 1`) — confirmé que ce
test échoue bien sans le correctif (`step 1` au lieu de `none`) avant de le
restaurer.
Vérifié : pnpm lint + build clean. recipes.cy.ts (9/9, dont le nouveau
test), recipes.feature (5/5), recipe-sources.feature (6/6).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suite à la maquette validée : redessine TechStepCorrectionPopover et ajoute
l'édition de la température, jusqu'ici la seule des 4 métadonnées sans
aucune interface (ni dans l'UI, ni côté API — voir la PR back-end empilée
sous celle-ci).
- StepDescription.tsx : le surlignage de température devient cliquable
(un <button>, plus un <span> mort) — rouvre le popover de correction de
la technique à laquelle il est rattaché (findTechStepForTemperature,
résolu par span puisque la température n'a pas d'existence propre, elle
est une métadonnée de clause comme les ingrédients/ustensiles).
- TechStepCorrectionPopover.tsx : réécrit. Les 4 sections (Technique,
Ingrédients, Ustensiles, Température) restent toutes visibles en même
temps — plus de bascule de tout le popover vers un sous-écran pendant
l'ajout d'un ingrédient/ustensile/température, seule la section
concernée affiche son propre mini-formulaire, les autres (dont le
picker de technique) restent inchangées à côté. Le picker de technique
se cache derrière "Changer" une fois une technique choisie (rien à
décider une fois confirmée correcte). Chaque ingrédient/ustensile/
température attaché s'affiche en ligne avec sa citation exacte du texte
source (nouveau : la traçabilité vers le texte existait déjà en donnée,
mais n'était affichée nulle part). Nouveau sélecteur à 3 modes
(°C / Thermostat / Qualitatif) pour la température, cohérent avec le
modèle de données (un seul des trois à la fois). Icônes SVG par
catégorie (tag/feuille/poêle/thermomètre), même langage stroke-based
que layouts/nav-icons.tsx.
- recipes.scss : nouveau langage visuel du popover (sections avec icône,
lignes de métadonnées façon carte plutôt que puces, bouton "+ Ajouter"
en pointillés plutôt qu'un lien texte, sélecteurs segmentés). Les deux
surlignages (.step-tech-step, .step-temperature) passent de
`cursor: help` à `cursor: pointer` — les deux sont maintenant de vrais
déclencheurs cliquables.
- i18n : nouvelles clés (changeTechnique, temperatureSection,
addTemperature, removeTemperature, temperatureMode.*) ; clé
currentTechnique supprimée (le pictogramme remplace la phrase "Technique
retenue : X") ; préfixe "+ " retiré de addIngredient/addUtensil (l'icône
+ le porte déjà visuellement).
- Tests : TechStepCorrectionPopover.cy.tsx largement réécrit (10 tests,
dont 2 nouveaux sur la température) ; recipes.feature/recipes.ts : nouveau
scénario Gherkin "cliquer un surlignage de température rouvre le popover
de sa technique" (fixture Gratin dédiée), step "Changer" ajouté au
scénario de relabel existant.
Vérifié : pnpm lint + build (shared/api/web) clean. Suites Cypress
exécutées localement (Chrome headless) : TechStepCorrectionPopover.cy.tsx
10/10, suite component complète 50/50, recipes.feature 5/5, recipes.cy.ts
8/8, recipe-sources.feature 6/6, planning-page.cy.ts 7/7. Un échec
pré-existant et sans rapport (recipe-form.feature, "Preloads an existing
recipe's ingredients when editing") reproduit identiquement avec ce
travail entièrement stashé — confirmé non lié.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cliquer sur une recette de l'onglet Recettes de /cuisiner naviguait vers
/recettes/:id, quittant la session de cuisine en cours. La ligne devient un
bouton qui déplie désormais le détail complet de la recette directement
sous elle (RecipeDetailPanel, showActions=false — même traitement "aperçu"
que RecipePickerDialog), chargé à la demande via GET /recipes/:id, un seul
dépliant ouvert à la fois (même logique que le sous-menu Paramètres de la
sidebar). RecipesTab est désormais keyed par semaine comme PlanTab, pour
que changer de semaine referme un dépliant devenu obsolète.
Clé i18n recipesTab.viewRecipe supprimée (plus d'appelant).
Tests : cooking-session-page.cy.ts (déplier/replier, un seul ouvert à la
fois) + cooking-session.feature (scénario dédié). pnpm lint + pnpm --filter
web build clean ; 16/16 tests passants (Chrome headless).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dans l'onglet Plan de /cuisiner, la liste de tâches d'une phase était un
flux plat — la recette concernée n'apparaissait qu'en petite ligne sous
chaque carte. Regroupe désormais les tâches par combinaison exacte de
recettes (groupTasksByRecipes, cooking-session.ts) : chaque groupe devient
une mini-section titrée par le(s) nom(s) de recette(s), un pool
merged-prep/merged-setup formant sa propre section partagée plutôt que
d'être rattaché à une seule recette. La ligne "pour X" par carte devient
redondante avec le titre de section et est retirée (clé i18n forRecipes
supprimée).
Couverture ajoutée : cooking-session-page.cy.ts (mode layout, 3 sections
distinctes sur la fixture à préchauffages multiples) + cooking-session.feature
(parcours Gherkin, section partagée + sections solo).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'optimiseur ne mutualisait que le pré-découpage. Il regroupe maintenant
aussi les actions SETUP (préchauffage, ébullition) entre recettes, mais
seulement quand elles sont compatibles : deux préchauffages ne fusionnent
que s'ils visent la même température détectée. Deux préchauffages à des
températures différentes restent séparés, ordonnés du plus froid au plus
chaud, et la mise en place porte une note « Four requis à N températures —
enchaîner … ». Défaut optimiste : deux préchauffages sans température
détectée fusionnent quand même (supposés compatibles).
- cooking-optimizer.ts : temperatureKey (comparaison exacte, aucune
conversion inter-représentations), isPureSetupStep / setupSignature /
extractMergedSetup en miroir du chemin merged-prep ; comparableTemp pour
l'ordre seul ; detectApplianceTemperatureConflicts → CookingPhaseView.notes
- cooking-session.service.ts : `temperatures` dans l'include + mapping
- packages/shared : CookingTemperatureView, kind « merged-setup »,
CookingTaskView.temperatures, CookingPhaseNoteView, CookingPhaseView.notes
- web : temperature-label.ts extrait de StepDescription et réutilisé ;
taskHeadline gère merged-setup ; phaseNoteText ; bandeau .cooking-phase__note
- i18n : cookingSession.task.mergedSetupLabel, cookingSession.note.*
- tests : 7 cas Mocha « setup pooling » + test DB cooking-session,
cas Cypress mode layout + scénario Gherkin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Remanie /cuisiner : au lieu d'empiler toutes les phases, deux onglets
(Recettes, Plan) et un stepper qui parcourt les phases une à une. Chaque
tâche est cochable ; la progression est mémorisée par semaine dans
localStorage — aucun état serveur, même parti pris que le reste de la page.
- CookingSessionPage : barre d'onglets + PlanTab (stepper, nav
précédent/suivant, clic direct sur un nœud) + RecipesTab (liste des
recettes de la semaine, lien vers la fiche)
- cooking-progress.ts : CookingProgressStore (load/save localStorage,
clé batchcooking:cookingProgress:<semaine>)
- cooking-session.ts : reconcileChecked / phaseProgress (helpers purs)
- réutilise CheckboxOption pour les cartes de tâche cochables
- i18n : cookingSession.tabs / stepper / progress / recipesTab
- tests Cypress (mode layout + Gherkin) mis à jour et étendus
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3e type de metadonnee de clause, a cote des ingredients et des ustensiles :
temperature en °C (« 180°C », « 200 degres », °F converti), numero de
thermostat (« th. 6 ») et intensite qualitative (« feu doux/moyen/vif » ->
low/medium/high).
services/tech-step-intent-service :
- `temperature_extraction.py` : `extract_temperatures(text, locale)` pur,
a base de regex (independant du modele spaCy et de l'entrainement).
- `ProcessResult` / `ProcessResponse` gagnent `temperatures` (ajout additif
au contrat ; cle `gas_mark` en snake_case sur le fil). `process()` les
extrait meme pour une locale pas encore entrainee.
- Tests `test_temperature_extraction.py` (10) ; `test_routes_process.py` :
2 assertions d'egalite stricte gagnent `"temperatures": []`, +1 cas.
apps/api :
- `IntentServiceTemperature` (mappe `gas_mark` -> `gasMark` a la frontiere).
- `TechStepMatch.temperatures` : filtrees par appartenance de span a la
clause, meme regle que les ustensiles.
- `model StepTechStepTemperature` (aucune FK — la valeur structuree EST la
donnee) + migration manuelle `20260829120000_step_tech_step_temperature`
(cascade via le TRUNCATE de `step_tech_step`, rien a ajouter a
reset-db.ts). Persistance + lecture dans `recipe.service.ts`
(`recipeInclude`, `toStepTechStepViews`) et l'include de sequence fraiche
du service de correction ; `sources.service.ts` (apercu d'import) les
fait transiter.
packages/shared : `StepTechStepTemperatureView` + `temperatures` sur
`StepTechStepView`.
apps/web :
- `splitDescriptionSegments` enrobe `splitDescriptionByTechSteps` et
redecoupe les segments non-keyword autour des spans de temperature (la
logique technique intriquee reste intacte). `?? []` tolere une payload
d'avant `temperatures`.
- `StepDescription` : surlignage `.step-temperature` + Tooltip via
`temperatureLabel` ; i18n `recipes.temperature.*`.
- Tests composants +4 (22 verts) ; pas d'UI de correction (v1, comme les
ustensiles).
Verifie : biome + tsc + `pnpm -r build` ; web 102/103 e2e (l'echec est le
flake pre-existant recipe-form.feature, sans rapport) + 49/49 composants ;
pytest temperature + routes 15/15. `pnpm --filter api test` (Postgres +
intent-service requis) non lance ici.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La route page `/admin/monitoring` et le chemin API `GET /admin/monitoring`
sont identiques : un glob `**/admin/monitoring` capturait aussi la requete
document du `cy.visit()`. Le hardcode `http://localhost:3000/...` marchait
en local (VITE_API_URL dans apps/web/.env) mais pas en CI (pas de .env ->
API en meme origine sur :5173, donc collision totale) : `cy.wait
(@getMonitoring)` timeout.
`resourceType: "fetch"` epingle l'intercept sur le seul XHR d'AdminApiClient.
Verifie en simulant la CI (sans apps/web/.env) : 16/16 specs admin verts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'admin etait une 2e app front Vite independante (apps/admin-web, port 5174,
Dockerfile nginx, service compose dedie, job CI propre) non demandee. Toute
l'UI passe dans apps/web sous le prefixe /admin ; seul le frontend est
fusionne, l'authentification admin reste entierement separee.
Front (apps/web/src) :
- pages -> pages/admin/{login,dashboard,monitoring,corrections,catalog}/,
layout -> layouts/AdminLayout.tsx, contexte + garde -> features/admin/.
- client API -> api/admin-client.ts : classe AdminApiError (evite la
collision avec ApiError), lit VITE_API_URL (plus de VITE_ADMIN_API_URL).
- routes /admin/* dans App.tsx, enveloppees d'AdminAuthProvider +
RequireAdmin -> le probe GET /admin/auth/me ne tourne que sous /admin.
- reutilise l'i18n, lib/zod-errors, services/error-message.service et le
theme SCSS de apps/web ; bloc i18n admin.* fusionne dans la locale fr
(les cles errors etaient deja toutes presentes).
- corrige une race dans CatalogPage (reponse d'un onglet precedent qui
ecrasait l'onglet courant, exposee par le double-mount StrictMode) via
un ref requestSeq.
Auth admin inchangee : table AdminUser, cookie admin_session,
ADMIN_JWT_SECRET, script create-admin.ts.
Infra :
- docker-compose : service admin-web + ADMIN_WEB_PORT supprimes (l'app
`app` sert deja le front construit).
- ADMIN_CORS_ORIGIN retire (meme origine) : env.ts, app.ts, .env.example.
- job CI "Run admin-web E2E tests" supprime ; les specs admin-* tournent
dans le job web (apps/web/cypress/e2e/admin-*.{cy.ts,feature}).
- apps/api/.env.example : ajout ADMIN_JWT_SECRET / ADMIN_INITIAL_*.
- recharts ajoute a apps/web ; pnpm-lock regenere.
- specs/backend-architecture.md : section admin mise a jour.
Verifie : biome + tsc -b (web/api) + pnpm -r build verts ; Cypress web
102/103 (l'unique echec est le flake pre-existant recipe-form.feature
"Preloads ..." de clipping headless, sans rapport) ; 16/16 specs admin ;
45/45 composants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Quand le catalogue seede ne couvre pas un ingredient, l'utilisateur pouvait
etre bloque (creation manuelle) ou perdre silencieusement la ligne (import).
Une ligne de recette accepte desormais `placeholderName` (texte libre) au
lieu de `ingredientId` : l'API cree une ligne `Ingredient` `isPlaceholder`
(cle `placeholder:<uuid>`, `displayName`, `createdById`) dans la transaction
de la recette, et emet `ingredient.placeholder_created`. Ces lignes sont
exclues de `GET /reference/ingredients` et de `ingredient-matcher`.
Front : bouton "Ajouter << ... >>" dans l'etat vide de `IngredientPicker`
(formulaire + import), badge "a completer" sur la ligne, helper
`ingredientLabel` applique partout ou un libelle d'ingredient est rendu.
Admin : `/admin/catalog/*` (+ page `apps/admin-web`) liste les placeholders
regroupes par nom normalise, "marquer traite" (`reviewedAt`) et purge des
orphelins. La promotion en vraie entree catalogue reste manuelle.
Migration `ingredient_placeholder` ecrite a la main (Postgres indisponible).
Suites Mocha DB-backed ecrites, non executees en session ; test pur
`normalizePlaceholderName` + Cypress admin-web/web verts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Bouton "Commencer a cuisiner" dans l'en-tete du planning, actif seulement
quand la semaine affichee contient >= 1 recette ; navigue vers
/cuisiner?date=<semaine>.
- Page CookingSessionPage (/cuisiner) : consomme GET /cooking-session, rend
les phases (mise en place / cuisson / dressage), les taches mutualisees
avec badge "Mutualise" + ingredients/ustensiles resolus, et la bande
"Pendant ce temps" pour les cuissons de fond. Semaine lue depuis ?date=,
WeekNavigator en fallback ; recalcul a chaque visite (comme la liste de
courses).
- Logique pure extraite dans cooking-session.ts (composition des libelles,
formatage des quantites).
- i18n : bloc cookingSession.* + planning.startCooking.
- apiClient.getCookingPlanForWeek.
- Tests Cypress : cooking-session-page.cy.ts (layout, 3 cas), cooking-session
.feature (parcours planning -> /cuisiner), assertions bouton dans
planning-page.cy.ts ; step generique "button should be disabled" mutualise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux retours utilisateur distincts, meme cause : le popover de correction
presentait la selection de technique et l'edition des ingredients/
ustensiles comme deux etapes successives (liste plate -> "Valider" pour
la selection, puis un ecran separe pour les metadonnees), et cette liste
de techniques elle-meme n'etait qu'un flot de ~74 boutons sans recherche
ni tri, illisible en pratique.
TechStepCorrectionPopover.tsx : les deux fonctions fusionnent en un seul
ecran — la technique se choisit desormais via le meme CatalogSearchPicker
(recherche + liste filtrée) deja utilise pour les ingredients/ustensiles,
avec le choix courant marque visuellement (nouveau prop `selectedId`), et
les sections Ingredients/Ustensiles restent affichees en permanence a cote
plutot que masquees tant qu'aucune technique n'est choisie. "Valider" reste
desactive tant qu'aucune technique n'est selectionnee.
CatalogSearchPicker.tsx : nouveau prop optionnel `selectedId` pour marquer
visuellement l'item courant dans la liste (utilise par le picker de
technique, pas par les sous-flux ingredient/ustensile qui n'ont pas de
notion de "choix courant").
Tests Cypress (component + e2e) et traductions mis a jour pour ce nouvel
ecran unique.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ouvrir le popover de correction depuis un highlight existant tombait sur
la liste de choix (avec l'option "Aucune technique ici"), pas sur la vue
metadonnees — pour voir/editer les ingredients/ustensiles deja rattaches
a une technique correctement detectee, il fallait recliquer cette meme
technique dans la liste, sans aucun indice que c'est ce qu'il fallait
faire (rien ne la distingue des autres dans cette liste). Resultat cote
utilisateur : la fonctionnalite de correction/edition de metadonnees
etait techniquement presente mais invisible en pratique.
TechStepCorrectionPopover demarre desormais selectionne sur
`previousTechStepId` quand il est defini (un clic sur un highlight
existant) — droit dans la vue Ingredients/Ustensiles, deja pre-remplie.
"Changer" reste disponible pour rejoindre la liste complete (relabelliser
ou supprimer la correspondance).
Tests Cypress (component + e2e) mis a jour pour ce nouveau point d'entree
par defaut.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Etend le flux de correction existant (TechStepCorrectionPopover) pour que
l'utilisateur associe lui-meme des ingredients (avec quantite/unite) et
des ustensiles a la technique qu'il corrige, avec le meme marquage
source: "manual" que la technique elle-meme.
Backend :
- submitTechStepCorrectionSchema (packages/shared) accepte des tableaux
ingredients/utensils optionnels, chacun avec son propre span [start,end)
selectionne par l'utilisateur. Omis = ne touche pas aux metadonnees
existantes ; tableau (meme vide) = remplace tout ce qui existait sur
cette occurrence (auto ET manuel precedent - decision validee avec
l'utilisateur).
- applyManualCorrection (recipe-tech-step-correction.service.ts) ecrit
les nouvelles lignes StepTechStepIngredient/StepTechStepUtensil apres
avoir vide celles de l'occurrence via deleteMany - meme chemin de code
que ce soit une creation ou une mise a jour de la technique.
- Nouveaux asserts d'existence (ingredient/unite/ustensile) + validation
de span, nouveau code d'erreur UTENSIL_NOT_FOUND.
- source ajoute a StepTechStepIngredientView/StepTechStepUtensilView
(le calque manquait ce que la colonne DB portait deja).
Frontend :
- TechStepCorrectionPopover passe d'un clic = soumission immediate a un
flux selection-puis-confirmation, avec deux nouvelles sections
Ingredients/Ustensiles pre-remplies avec l'existant.
- Ajouter un ingredient/ustensile demande une selection de texte dediee
dans la description encore visible (StepDescription geree via un
nouvel etat pendingSpanRequest/resolvedMetadataSpan) - pas de raccourci
sur le span de la correction elle-meme.
- Nouveau CatalogSearchPicker.tsx, plus leger que IngredientPicker pour
ce contexte de popover, reutilise pour les deux catalogues.
- getUtensils() ajoute a apiClient.
Tests : nouveaux cas Mocha (attache/remplace/omission/validations) dans
recipe-tech-step-correction.test.ts, TechStepCorrectionPopover.cy.tsx
etendu avec le nouveau flux, recipes.ts (e2e) ajuste au clic Valider
supplementaire.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GET /shopping-list?date= (shopping-list.service.ts/.routes.ts) somme les
ingrédients de chaque recette planifiée sur la semaine, mis à l'échelle par
les portions de chaque créneau (PlanningItem.portions / Recipe.portions),
regroupés par paire (ingredientId, unitId) — jamais null contrairement à
GET /planning, une semaine vide redescend en items: [].
Côté web, ShoppingListPage rend cette liste groupée par rayon (même
IngredientCategory que IngredientPicker), triée alphabétiquement en
français à l'intérieur d'un rayon (shopping-list.ts, logique pure extraite
du composant). WeekNavigator (flèches + calendrier) est extrait de
PlanningPage vers features/planning/ pour être partagé entre les deux
pages ; ses libellés migrent de planning.* vers common.weekNav.*/
common.calendar.*/common.days.*, plus génériques pour une page qui n'est
plus seulement le planning.
ComingSoonPage retiré (plus aucun appelant, Liste de courses avait le
dernier stub restant).
Tests : Mocha (agrégation, mise à l'échelle par portions, unités non
fusionnées) + Cucumber (shopping-list.feature : liste vide, groupement/tri,
navigation de semaine) + mise à jour de layout.cy.ts/planning-page.cy.ts
pour le nouveau rendu.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): corrige la liste vide de sevenFiftyGAdapter quand le filtre est vide
L'endpoint IA que list() utilisait pour toute recherche (SEARCH_URL,
/genius/query/) répond avec un corps de réponse vide dès que query est
vide — vérifié en direct. Résultat : parcourir la source 750g sans filtre
ne remontait jamais aucune recette.
Corrigé en lisant un endpoint différent quand query est vide/absent :
dernieres-recettes.htm, le vrai catalogue paginé "dernières recettes" de
750g.com (pagination réelle via &page=N, contrairement à l'endpoint de
recherche). nextCursor suit désormais cette même distinction : toujours
null pour une recherche par texte (l'endpoint ne pagine pas), calculé
normalement pour le parcours sans filtre (une page sans aucune carte en
est le signal de fin, cet endpoint ne renvoyant ni 404 ni redirection une
fois la dernière page dépassée).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(recipes): scroll infini + placeholders sur le parcours des sources externes
Remplace le bouton "Voir plus" de RecipeSourcesPanel par un scroll
infini : une ligne sentinelle en fin de liste (SourceItemTable), observée
via IntersectionObserver scopé au conteneur scrollable de la table,
déclenche le chargement de la page suivante quand elle approche du bas.
Le panel précharge en plus la page suivante dès que la page courante
s'affiche (pas seulement au moment où la sentinelle devient visible), pour
qu'un défilement rapide tombe le plus souvent sur une réponse déjà
arrivée plutôt que de déclencher un aller-retour réseau à ce moment précis.
Pendant un chargement (préchargé ou non), SourceItemTable ajoute des
lignes squelettes qui pulsent en bas de la liste au lieu de laisser un
vide. Un échec de chargement n'efface plus la liste déjà chargée comme
avant (bug corrigé au passage) — un message avec un lien "Réessayer"
s'affiche à la place ; ce correctif inclut aussi le nettoyage d'un
préchargement en échec qui, sinon, aurait fait rejouer indéfiniment la
même promesse déjà rejetée à chaque tentative de réessai.
Deux nouveaux scénarios Cypress (recipe-sources.feature) : chargement
automatique de pages supplémentaires sans bouton, et réessai après un
échec du chargement suivant. Suite e2e complète relancée (78/79, le seul
échec restant est un test préexistant sans rapport, recipe-form.feature,
signalé séparément).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(tech-steps): fiabilise la detection des tech steps (corpus + LLM + corrections utilisateur)
Une seule feature livree en une seule PR, en 5 phases :
- Phase 1 : enrichit le corpus NLP (tech-step-training-data.ts) et ajoute
un harness d'evaluation (precision/rappel/F1) avec un jeu de test etiquete
- la premiere metrique objective de qualite pour ce classifieur.
- Phase 2 : schema Prisma (StepTechStepCorrection, TechStepTrainingSuggestion)
+ endpoints utilisateur (POST/GET corrections, ouverts a tout viewer, pas
seulement l'auteur) + endpoints internes /internal/tech-steps/* proteges
par secret partage (requireInternalWorker).
- Phase 3 : UI de highlight/correction cote web (selection de texte ->
association a une technique, ou clic sur un highlight existant pour le
corriger/supprimer) - verifiee via Cypress (component + e2e, en Chrome
reel).
- Phase 4 : worker LLM autonome (services/tech-step-llm-worker, hors du
monorepo pnpm comme experiments/llm-tech-step-poc) qui audite les clauses
a faible confiance et transforme les corrections utilisateur en
suggestions d'entrainement, sans jamais toucher le chemin interactif.
- Phase 5 : script retrain-tech-steps.ts (gate de regression F1 + backfill)
et list-pending-training-suggestions.ts pour la revue humaine avant
application au corpus.
Verification effectuee cette session : tsc/biome sur l'ensemble du repo,
build complet (pnpm build), suite Cypress complete (component 39/39, e2e
75/76 - le seul echec est preexistant et sans rapport, cote
recipe-form.feature/ingredient-picker), tests unitaires du worker (6/6) et
son install/typecheck reels contre node-llama-cpp. Les tests Mocha
d'apps/api (Phases 1 et 2) n'ont pas pu etre executes dans cette session
(pas de Postgres local disponible) - a lancer avant merge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(tech-steps): calibre le seuil F1 sur une vraie execution et corrige un bug de comptage
Docker etant redevenu disponible dans cette session, j'ai pu lancer pour de
vrai la suite Mocha d'apps/api (334/334, y compris les tests Phase 1/2
qui n'avaient pu etre executes precedemment) ainsi que les scripts de la
Phase 5 contre une vraie base de test.
- tech-step-eval-dataset.ts : corrige un vrai bug d'auteur - "Take the
plates..." collisionnait avec le synonyme anglais enregistre "plates"
(technique plate), invalidant ce cas negatif. Remplace par "dishes".
- tech-step-eval-runner.ts : F1 reel mesure = 0.815 (33 TP / 9 FP / 6 FN).
Documente ce chiffre et les vraies erreurs de classification decouvertes
(ex: "Blanchissez les haricots verts..." classifie a tort comme "peel")
- des faiblesses reelles du classifieur que ce harness est cense
detecter, pas a masquer en ajustant le jeu de test.
- retrain-tech-steps.ts : le script loggait `appliedIds.length`/
`rejectedIds.length` (ce qui a ete demande) au lieu du `count` reel
retourne par `updateMany` (ce qui a vraiment ete modifie) - un id
inexistant faisait afficher un faux succes. Decouvert en executant le
script pour de vrai avec des ids partiellement invalides.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(tech-steps): corrige un span de correction incorrect sur un highlight existant
Bug reel trouve en lancant l'application pour de vrai et en cliquant sur
un highlight existant : la correction soumise couvrait presque toute la
description au lieu du seul mot-cle cliqué (ex: [6, 56) au lieu de [6, 13)
pour "mijoter").
Cause : StepDescription.tsx capturait `start` dans un `const` par
iteration de `.map()` (correct), mais utilisait `offset` directement (la
variable mutable partagee, pas une valeur capturee) pour `end` dans le
gestionnaire onClick - une fermeture classique sur variable de boucle
encore mutee. Par le temps ou l'utilisateur clique reellement (bien apres
la fin du rendu), `offset` contient sa valeur finale (fin de la
description entiere), pas celle du segment concerne.
Corrige en capturant `end` dans un `const` au meme endroit que `start`.
Renforce aussi l'assertion e2e correspondante (recipes.ts) qui ne
verifiait auparavant que la requete avait ete faite, jamais son contenu -
elle serait passee malgre ce bug.
Verifie en conditions reelles : recette creee via l'UI, correction
soumise, span persiste verifie directement en base (start=6, end=13,
previous=simmer, corrected=grill).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: ignore les telechargements Cypress (artefact de run local)
* feat(tech-steps): distingue les corrections manuelles des détections auto
Les corrections utilisateur (via TechStepCorrectionPopover) sont
désormais écrites directement dans StepTechStep, avec une colonne
`source` ("auto" | "manual") qui les distingue des matches du
classifieur NLP :
- Migration `step_tech_step_source` ajoutant `source` (défaut "auto")
- `applyManualCorrection`/`renumberStepTechSteps` dans
recipe-tech-step-correction.service.ts : une correction met à jour
ou crée l'entrée StepTechStep concernée (source "manual"), la
réponse de l'endpoint inclut désormais le techSteps à jour du step
(SubmitTechStepCorrectionResult), pas seulement l'audit de
correction
- backfill-tech-steps.ts préserve les entrées "manual" existantes :
seules les entrées "auto" sont recalculées, et un nouveau match
auto chevauchant une correction manuelle est ignoré plutôt
qu'inséré en doublon — vérifié en base réelle (une correction
manuelle survit intacte à un backfill complet)
- Le front distingue visuellement les deux (StepDescription.tsx,
recipes.scss : `.step-tech-step--manual`, couleur Turmeric au lieu
de Basil), avec un tooltip "(correction manuelle)" et un indicateur
de découvrabilité de la fonctionnalité dans RecipeDetailPanel
Corrige aussi deux bugs trouvés en testant en conditions réelles :
- StepDescription.tsx : le clic sur un highlight existant lisait la
variable `offset` (mutable, partagée par la boucle) au lieu d'une
valeur capturée, envoyant un `end` erroné (fin de la description
entière au lieu du span du mot cliqué)
- backfill-tech-steps.ts : le garde `import.meta.url ===
file://${process.argv[1]}` ne matche jamais sur Windows (chemins à
antislash), le script ne faisait donc rien en exécution directe ;
remplacé par `pathToFileURL(process.argv[1]).href`
335 tests apps/api passants, 40/40 composants Cypress, 75/76 e2e
Cypress (1 flake pré-existant sans rapport, non touché ici).
* fix(worker): corrige le build Docker de tech-step-llm-worker
docker compose build tech-step-llm-worker échouait sur deux problèmes
en cascade, tous deux liés à l'isolation volontaire de ce service hors
du monorepo pnpm (seul son propre package.json/tsconfig.json est copié
dans son contexte de build) :
- pnpm install --ignore-workspace --frozen-lockfile échouait
(ERR_PNPM_IGNORED_BUILDS) : sans "packageManager" dans son
package.json, corepack télécharge le pnpm le plus récent
(11.22.0), qui a durci en erreur bloquante ce qui n'était qu'un
avertissement sur les builds de dépendances ignorés
(esbuild/node-llama-cpp). Le reste du repo est épargné parce que
apps/api/Dockerfile copie le package.json racine, qui pinne déjà
pnpm@10.12.4 — ce pin ne pouvait pas atteindre ce service isolé.
Fixé en pinnant la même version ici.
- tsc échouait ensuite (TS5083 puis erreurs en cascade dans les .d.ts
de node-llama-cpp) : tsconfig.json de ce service extends le
tsconfig.base.json racine (skipLibCheck notamment), jamais copié
dans le contexte de build. Fixé en le copiant avant tsconfig.json.
Vérifié : `docker compose build tech-step-llm-worker` complet en local.
* fix(tech-steps): empêche le contexte d'un match d'avaler une correction manuelle voisine
La correction manuelle ne s'affichait pas quand elle portait sur du texte
qui n'était pas une technique à l'origine — reproduit en live : une
description avec un seul match auto-détecté ("mijoter") voit son
contexte de clause s'étendre sur toute la description dès que
splitIntoClauses (tech-step-matcher.ts) n'a trouvé qu'un seul candidat
NER (le cas courant), même quand ce candidat n'a aucun rapport avec le
reste du texte. splitDescriptionByTechSteps avançait alors son curseur
jusqu'à la fin de ce contexte large, ce qui faisait purement et
simplement disparaître (silencieusement, sans erreur) toute correction
manuelle ajoutée plus loin dans la même description — un mot pourtant
sans aucun rapport avec la technique auto-détectée.
Le contexte d'un match est purement cosmétique (StepDescription.tsx le
rend identique à du texte brut depuis que sa mise en valeur dédiée a
été désactivée) et ne doit donc jamais coûter son propre highlight à
un *autre* match. splitDescriptionByTechSteps distingue maintenant
deux notions : le chevauchement entre les spans *keyword* stricts de
deux entrées (toujours un vrai conflit, l'entrée la plus tardive est
toujours ignorée, comportement inchangé) et le chevauchement du
contexte *cosmétique* d'une entrée sur le keyword d'une autre (jamais
un vrai conflit désormais : le contexte est simplement rogné pour
laisser la place, plutôt que l'entrée voisine entière étant abandonnée).
Vérifié en conditions réelles (Docker) : une correction manuelle sur
"materiel" dans "Faire mijoter la sauce, puis ranger le materiel."
s'affiche maintenant correctement à côté du highlight auto "mijoter",
et survit à un rechargement complet de la page.
Nouveau test de régression dans highlight-tech-steps.cy.tsx
reproduisant exactement ce cas ; les 18 tests du fichier (dont tous
les cas de contexte/malformation déjà couverts) passent toujours.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(api): remplace la détection des tech steps par un pipeline NLP (node-nlp)
Le matching par regex ne généralisait jamais au-delà de son propre
vocabulaire — une étape décrivant la fonte du beurre comme "jusqu'à ce
que le beurre ait disparu dans la poêle" ne contient aucun verbe sur
lequel une regex pourrait s'ancrer, alors que le sens est sans
ambiguïté.
Nouveau pipeline en 3 étapes (TechStepClassifierService, node-nlp
4.27.0 — la 5.x est encore alpha, non retenue) :
1. NER (entités enum) trouve les mentions candidates + leur position
exacte, à partir de listes de synonymes (tech-step-training-data.ts)
plutôt que de regex écrites à la main. ner.threshold: 1 (exact,
après normalisation) — le défaut à 0.8 faisait matcher "faire" (verbe
auxiliaire omniprésent) contre "frire" par pure proximité de chaîne.
2. La description est découpée en clauses autour de ces candidats
(splitIntoClauses, pure/testable sans modèle).
3. Le NlpManager classe chaque clause individuellement, entraîné sur
des phrases qui n'emploient jamais le verbe de la technique — c'est
ce qui apporte la compréhension du sens. En dessous de
CONFIDENCE_THRESHOLD (0.65, ajusté empiriquement), retombe sur la
technique impliquée par l'ancre NER plutôt que d'abandonner un match
clairement ancré sur un mot-clé.
TechStepMapping (table de regex par technique/locale) supprimée —
migration 20260821130000_drop_tech_step_mapping — plus aucune table
n'est interrogée à l'exécution, les données de matching vivent en code.
TECH_STEPS (reference-seed-data.ts) simplifié en simple liste de uid,
les mappings ayant disparu.
Deux pièges trouvés en construisant ce pipeline, corrigés à la source :
- db/prisma.ts construisait PrismaClient sans importer config/env.ts —
un run de test isolé pouvait faire gagner la course au .env interne
de Prisma (dev) contre .env.test. Fixé en important config/env.js en
tout premier, pour effet de bord.
- NlpManager a autoSave/autoLoad: true par défaut — persiste le modèle
entraîné dans model.nlp et le recharge au lieu de ré-entraîner au
prochain démarrage. Les deux désactivés explicitement (sinon un
modèle obsolète masquerait silencieusement toute mise à jour du
corpus/seuil) ; model.nlp ajouté au .gitignore en garde-fou.
apps/api/src/db/prisma.ts, recipe.service.ts, sources.service.ts et
recipe-translation.ts adaptés à la matching async (le classifieur
entraîné remplace le couple loadTechStepMappingRules+matchTechStepSpans
synchrone) ; server.ts appelle techStepClassifier.warmUp() avant
d'accepter du trafic (le tout premier appel réel à
NlpManager.process() charge les ressources par langue de node-nlp,
plusieurs secondes).
Vérifié : tsc --noEmit, biome check (0 erreur), build complet des 6
packages, 308 tests API (dont un test-support/reset-db.ts corrigé —
référençait encore tech_step_mapping dans son TRUNCATE).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(api): ajoute la délimitation de contexte aux tech steps et étoffe le vocabulaire du classifieur
Deux évolutions du pipeline NLP de détection des tech steps (PR #63) :
1. Délimitation de contexte — en plus du mot-clé qui déclenche un match
(start/end), chaque TechStepMatch porte maintenant contextStart/
contextEnd : la clause complète autour du mot-clé (ex : "poêle chaude"
comme mot-clé, "Dans une poêle chaude" comme contexte). Persisté sur
StepTechStep (colonnes nullables, migration dédiée), exposé via
StepTechStepView, et rendu côté web avec un style plus discret que le
mot-clé (StepDescription.tsx, .step-tech-step-context). splitIntoClauses
coupe désormais sur l'espace le plus proche du milieu de l'écart entre
deux candidats plutôt que sur le milieu brut, pour ne jamais couper un
mot en deux (findGapSplitPoint).
2. Vocabulaire du classifieur — synonymes et locutions supplémentaires par
technique (FR/EN) pour fiabiliser la détection sur des formulations que
le corpus initial ne couvrait pas. Plusieurs bugs de fond trouvés et
corrigés en cours de route, tous confirmés par la suite de tests
complète (309 tests) :
- un synonyme multi-mots qui est un préfixe-mot d'un synonyme plus court
déjà enregistré pour la même technique fait matcher les deux comme
candidats NER distincts et chevauchants, corrompant le découpage en
clauses (parfois jusqu'à une mauvaise classification) — retiré
partout où ce motif a été repéré (cook, fry, deglaze, simmer, boil,
roast, chop, mince, marinate, preheat, bake, plate, coat) ;
- "poêlé"/"poêlée" comme synonymes de panFry sont réduits à la même
racine que le nom "poêle" par le stemmer français de node-nlp,
provoquant un faux positif sur toute mention nue de "poêle" (dont
celle de preheat) — retiré ;
- "Fouetter les blancs en neige" était mal classé en foldIn (la phrase
d'entraînement de foldIn partage la même locution) — corrigé en
ajoutant des phrases d'entraînement dédiées à whisk ;
- "Émincer les tomates" est passé sous le seuil de confiance vers melt
après l'ajout du nouveau vocabulaire ailleurs dans le corpus — corrigé
en élargissant les phrases d'entraînement de mince à un autre légume.
Le test unitaire de splitIntoClauses avec un point de coupure obsolète
(pré-datant findGapSplitPoint) est aussi corrigé.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(api): corrige la découpe des clauses et le seuil de confiance du classifieur de tech steps
Trouvé en examinant des vraies recettes déjà en base après le dernier
étoffement du vocabulaire : plusieurs étapes bien réelles se faisaient
classer sur la mauvaise technique, sans lien avec un mot-clé manquant.
- splitIntoClauses coupe désormais sur la limite de phrase (juste après
un ".", "!" ou "?") la plus proche du milieu de l'écart entre deux
candidats quand il y en a une, plutôt que sur l'espace brut le plus
proche du milieu. Une description à deux techniques dans deux phrases
distinctes ("Préchauffer le four à 180°C. Dans un saladier, mettre le
beurre... et mélanger.") ne coupait qu'au milieu brut, ce qui pouvait
trancher en pleine deuxième phrase et envoyer au classifieur une
clause tronquée ("...(thermostat 6). Dans un saladier, mettre" sans
complément) — assez éloignée des phrases d'entraînement courtes et
complètes pour se faire mal classer avec confiance (préchauffer prédit
"mix", mélanger prédit "melt").
- CONFIDENCE_THRESHOLD passe de 0.65 à 0.75 : du texte anglais passé
dans le classifieur français (qui doit ne rien trouver, garanti par
le test d'isolation des locales) scorait 0.69 sur "boil" — du bruit
de petit corpus, pas un vrai verdict. Les cas réels que ce seuil sert
à faire confiance scorent 0.91 à 1.0 en pratique ; 0.75 sépare
proprement le bruit du signal sans rien casser (309 tests toujours
verts).
- Deux phrases d'entraînement ajoutées à `cook` pour deux clauses
réelles mal classées (feu doux + remuant, découvert + laisser cuire)
qui n'avaient pourtant pas de mot-clé manquant.
Ajoute aussi src/scripts/backfill-tech-steps.ts : la détection ne
tourne qu'à la création/modification d'une recette, jamais
rétroactivement — ce script recalcule le start/end/contextStart/
contextEnd de chaque étape existante contre le classifieur actuel,
pour ne pas avoir à rouvrir et resauvegarder chaque recette à la main
après un changement de corpus.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(web): rend le contexte des tech steps réellement visible
Le style existant (fond teinté à 6% d'opacité, sans autre indice
visuel) était structurellement correct — vérifié en base, l'API et le
DOM contenaient bien les spans de contexte — mais imperceptible à
l'œil sur ce thème sombre : --color-primary n'est pas assez saturé
pour qu'une teinte de quelques % se distingue du fond de la carte.
Vérifié en créant une recette test dans le navigateur et en zoomant le
texte rendu : littéralement aucune différence visible avant, un
rectangle net après.
Passe à 10% de fond + une bordure basse pleine à 45% d'opacité comme
second indice visuel indépendant, tout en gardant le mot-clé
(soulignement pointillé + fond à 14% + curseur + tooltip) nettement
plus marqué que son contexte.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(web): retire l'affichage visuel du contexte des tech steps
Ne touche que le rendu — le backend continue de calculer et de
persister contextStart/contextEnd (tech-step-matcher.ts, StepTechStep),
et splitDescriptionByTechSteps continue de découper la description
autour du contexte (segments isKeyword: false). StepDescription.tsx
rend désormais ces segments comme du texte brut, comme un segment sans
technique — plus d'encadré/bordure autour de la clause, seul le
mot-clé reste surligné avec sa tooltip.
.step-tech-step-context (CSS) retirée, devenue inutilisée.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): corrige plusieurs bugs d'import TheMealDB
- Les instructions TheMealDB numérotées sur leur propre ligne ("1\n\ntexte...\n\n2\n\ntexte...") créaient des étapes parasites ne contenant qu'un chiffre — filtrées désormais (#52).
- Un ingrédient compté sans mot d'unité dans le texte source (ex. "4 Egg Yolks") laissait l'import bloqué sur "Importer" indéfiniment, sans indication visuelle de la ligne en cause — matchUnit retombe maintenant sur l'unité générique "piece" quand une quantité a été extraite, et RecipeImportForm/RecipeFormPage surlignent désormais toute ligne dont l'unité manque, avec un message explicite (#53).
- Ajout de INGREDIENT_LABEL_SYNONYMS_EN pour reconnaître des formulations alternatives fréquentes chez les sources anglophones ("vanilla pod" en plus de "vanilla bean") sans élargir INGREDIENT_LABELS_EN à un tableau pour ses ~550 entrées (#54).
- Effet de bord découvert en vérifiant #53 de bout en bout : deux lignes source résolues vers le même ingrédient catalogue (ex. "Egg Yolks"/"Eggs" -> "Œuf") faisaient planter la création en 500 (contrainte unique recipe_id+ingredient_id) au lieu d'un 400 propre. createRecipeSchema rejette maintenant les ingredientId en double, et le formulaire d'import surligne les doublons avant même de soumettre.
Vérifié de bout en bout dans le navigateur (import réel de la recette "Flan" depuis TheMealDB, jusqu'au planning) en plus des tests ajoutés.
Closes#52, #53, #54
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(layout): la sidebar réduite écrasait la barre mobile
`isCollapsed` (rail icône seule sur desktop) persiste dans localStorage
indépendamment de la largeur de fenêtre — un utilisateur ayant réduit la
sidebar sur desktop puis ouvrant la même session sur mobile (ou réduisant
la fenêtre sous 640px) gardait `.app-sidebar.collapsed` (spécificité
0,2,0 : width 4.25rem, flex-direction column), qui l'emportait sur la
règle mobile `@media (max-width: 640px)` (spécificité 0,1,0) censée passer
la sidebar en barre horizontale pleine largeur.
Le bloc `&.collapsed` est maintenant scopé sous `@media (min-width: 641px)`
— le complément exact du breakpoint mobile — donc il ne s'applique plus du
tout en dessous.
Vérifié dans le navigateur : sidebar collapsed=true dans localStorage,
viewport 375px — la sidebar calcule bien width: 375px / flex-direction:
row (barre horizontale pleine largeur) au lieu de 4.25rem/column.
Closes#27
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(readme): documente GET /planning?date=, plus /planning/current
Le README documentait encore `GET /planning/current` (401 sans session,
couvre "aujourd'hui"), une route qui n'existe plus — `planning.routes.ts`
ne définit que `GET /planning?date=YYYY-MM-DD` depuis l'introduction de la
grille de semaine complète. Sans session, `/planning/current` renvoie un
404 générique (route inexistante), pas le 401 documenté.
Documente aussi POST/DELETE /planning/items au passage, absents jusqu'ici.
Closes#55
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: met à jour README et specs/ avec l'état réel du code
Le code avait beaucoup évolué depuis la dernière mise à jour de la
documentation (sources externes, import de recettes, planning en
grille, pages de paramètres, thème, tests Cucumber...) sans que
README.md/specs/*.md ne suivent. Tour complet du code (backend +
frontend) et réécriture :
- specs/batch-cooking-modele.md : schéma de données réécrit depuis
schema.prisma (foyer/admin/invitation, sources, catalogue
ingrédients/unités, techniques détectées, visibilité des recettes).
- specs/backend-architecture.md : foyer, préférences/goûts, planning,
référence, sources externes (adaptateurs/registre/sync), matching
ingrédients/techniques, isolation base de test, suppression de compte.
- specs/frontend-architecture.md : routing complet, sidebar/paramètres,
thème, planning + picker, catalogue + import, composants UI partagés,
tests Cypress+Cucumber.
- specs/batch-cooking-architecture.md : module Import passe de TODO à
implémenté.
- specs/error-handling.md : liste complète des ~19 codes d'erreur.
- README.md : réécriture pour refléter tout ce qui précède, plus la
note (dangereusement obsolète) sur le partage base de test/dev — le
fix existe déjà (apps/api/.env.test), la doc décrivait encore le bug.
* feat(ingredients): ajoute jaune/blanc d'oeuf, coriandre en poudre, viandes hachées
Complète le catalogue d'ingrédients de référence (seed data) :
- jaune d'oeuf / blanc d'oeuf (dairyAndCheese/eggs, aux côtés d'"egg")
- coriandre en poudre (condimentsAndSpices/spices, aux côtés de
corianderSeeds/freshCilantro déjà présents)
- viandes hachées manquantes : veau, porc, agneau (meatAndSeafood/meats,
aux côtés de groundBeef déjà présent), dinde et poulet
(meatAndSeafood/poultry)
Libellés ajoutés dans apps/web/src/locales/fr/translation.json (source
d'affichage) et packages/shared/src/data/catalog-labels-en.ts (matching
anglais pour l'import de recettes depuis des sources comme TheMealDB).
Aucune icône ni régime dédiés : héritent des défauts de leur groupe
(EGG/SPICE/MEAT/POULTRY, mêmes dietUids que leurs groupes respectifs).
282 tests apps/api toujours au vert (resetDatabase() reseed le
catalogue à chaque test).
* fix(i18n): retire le œ ligaturé des libellés français de l'œuf
"Œuf"/"Œufs" (ingrédient, sous-catégorie, allergène) et "Jaune/Blanc
d'œuf" (ajoutés par #60) s'écrivaient avec le œ ligaturé — remplacé par
"oe" (deux lettres) partout où le mot apparaît. Ne touche pas "bœuf"
(mot différent, non concerné).
Le scénario Cucumber recipe-form.feature qui sélectionne l'ingrédient
par son libellé affiché est mis à jour en conséquence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): concatène les ingrédients dupliqués à l'import
Suite au retour utilisateur sur #53 (follow-up) : au lieu de bloquer
l'import et de demander à l'utilisateur de retirer une ligne en double
à la main, deux lignes source qui résolvent vers le même ingrédient
catalogue sont désormais fusionnées automatiquement, quantité
concaténée (sommée), avant même que l'écran de revue ne s'affiche.
- mergeDuplicateIngredients (recipe-translation.ts) : même unité des
deux côtés -> somme directe. Unité différente mais même UnitType
(MASS/VOLUME) -> conversion via toBaseFactor avant de sommer, exprimée
dans l'unité de la première ligne. UnitType différent, ou COUNT des
deux côtés (une "pincée" n'est pas une fraction fixe d'une "gousse",
cf. le commentaire de UnitView) -> jamais fusionnées, laissées en
double (createRecipeSchema/RecipeImportForm continuent de les
signaler, filet de sécurité déjà en place). Les lignes non résolues
(ingredientId: null) ne sont jamais fusionnées entre elles.
- rawText concaténé ("100g Sugar + 45g Sugar") pour la traçabilité.
- Branché dans previewSourceItem (sources.service.ts), juste après
translateRecipeIngredients — c'est le seul endroit où des doublons
peuvent apparaître (la création manuelle ne peut pas en produire,
IngredientPicker exclut déjà les ingrédients déjà sélectionnés).
Vérifié via l'API en local (import réel de "Flan" depuis TheMealDB) :
"100g Sugar"/"45g Sugar" -> une seule ligne Sucre, 145g.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore(lint): upgrade Biome vers 2.x, active noExplicitAny/noConsole/noFloatingPromises
`@biomejs/biome` passe de 1.9.4 à 2.5.9 (config migrée via `biome migrate
--write`) — nécessaire pour noFloatingPromises, une règle type-aware
apparue en 2.0 (nursery).
- noExplicitAny : déjà "recommended", actif depuis toujours, aucun changement.
- noConsole (biome.json) : bloque tout `console.*` sauf error/warn/info/
debug/table/assert — équivalent à "pas de console.log" sans interdire
les niveaux nommés (voir le nouveau log service dans le prochain commit,
qui centralise justement ces appels).
- noFloatingPromises (nursery) activé explicitement sous `rules.nursery`
sans avoir besoin d'activer le domaine "types" au sens large (ça aurait
aussi allumé des dizaines d'autres règles type-aware type
noUnresolvedImports/noUnnecessaryConditions, hors scope ici).
Le reste du diff, c'est soit du reformatage automatique (import sort, 2.x
ordonne différemment de 1.9.4 — `biome check --write --unsafe`), soit les
corrections des ~20 promesses flottantes que la nouvelle règle a fait
remonter :
- La plupart sont des `navigate(...)` non attendus (react-router v7 type
`navigate` en `void | Promise<void>`) — préfixés `void navigate(...)`,
aucun changement de comportement.
- Trois chargements initiaux en useEffect (OnboardingAllergensPage,
OnboardingDietPage, OnboardingHouseholdPage, HouseholdSettingsPage)
n'avaient jamais de `.catch()` du tout — ajouté (dégradation silencieuse
vers un état vide/par défaut, même raisonnement que le `.catch()` déjà
présent dans OnboardingSourcesPage).
- HouseholdSettingsPage : `loadHouse` était une fonction déclarée à chaque
render (donc une référence différente à chaque fois) utilisée comme
dépendance de useEffect ET passée en callback à des enfants — le
useEffect se re-déclenchait donc à chaque re-render provoqué par son
propre fetch, un vrai bug de boucle infinie de requêtes que
noFloatingPromises a fait remonter indirectement (via
useExhaustiveDependencies). Corrigé avec useCallback([]).
- RecipeDetailPanel : une clé de liste `${index}-...}` sur une liste
statique (draft.steps, sans id stable — DraftRecipeStepView n'en a pas)
— biome-ignore justifié, pas de bug réel.
- recipe.test.ts : variable `agent` non utilisée, retirée.
Vérifié : `pnpm --filter api test` (295/295), `pnpm lint` et `pnpm build`
clean sur tout le repo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(api): ajoute un log service pour les logs de fonctionnement côté serveur
Jusqu'ici, rien ne journalisait quoi que ce soit côté serveur : aucune
trace au démarrage à part un console.log ad hoc, et surtout aucune trace
des requêtes ni des erreurs gérées par ErrorHandlerService — un 500 en
production n'aurait laissé aucune trace exploitable.
- LoggerService (apps/api/src/lib/logger.service.ts) — classe (public
debug/info/warn/error, private emit), même convention que
ErrorHandlerService (packages/error-tools) : instance unique partagée
exportée (`export const logger = new LoggerService()`). Émet une ligne
JSON structurée par appel (timestamp/level/message + meta), filtrée par
seuil selon NODE_ENV (debug complet en dev, warn+ pendant les tests
pour ne pas alourdir la sortie de Mocha, info+ en production). Seul
endroit du code autorisé à toucher `console` directement (biome-ignore
justifié), toujours via une méthode nommée — jamais un console.log nu.
- requestLogger (middlewares/request-logger.ts) — une ligne par requête
terminée (méthode/chemin/statut/durée), montée en tout premier dans
app.ts, avant même setupCore (CORS/JSON/cookies), pour englober tout le
pipeline. Niveau déduit du statut (info/warn/error).
- errorLogger (middlewares/error-logger.ts) — monté juste avant
createErrorMiddleware : réutilise errorHandlerService.handle() (pur/
sans effet de bord) pour classifier l'erreur avant que la vraie réponse
ne soit construite, log en warn les 4xx routiniers (validation, 404,
401...) et en error les 5xx/exceptions non prévues (avec la stack).
- error-handler.service.ts : retire le `console.error(error)` ad hoc de
fromUnknownError — errorLogger voit désormais chaque erreur avant que
ce service ne la mappe, donc ce console.error faisait doublon (et
loggait en texte brut, pas en JSON structuré).
- server.ts : le console.log de démarrage passe par logger.info.
Vérifié : pnpm --filter api test (303/303, dont 8 nouveaux tests sur
LoggerService), pnpm lint/build clean, testé en live (pnpm dev:api +
curl) — logs JSON corrects pour un 200, un 404, un 401.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: préfixe tous les membres private/protected par _
Convention demandée par l'utilisateur : `emit` -> `_emit`, sur toutes les
classes du repo, pas seulement le nouveau code. `public` reste sans
préfixe.
- LoggerService (apps/api) : _minSeverity, _emit.
- ApiClient (apps/web) : _request (39 sites d'appel mis à jour).
- ErrorHandlerService (packages/error-tools) : _fromZodError,
_fromHttpError, _fromUnknownError.
- ExpressServer (packages/express-tools) : _app, _registeredRoutes.
Aucun changement de comportement — pur renommage interne, aucune méthode
private/protected n'était appelée depuis l'extérieur de sa classe.
Vérifié : pnpm --filter api test (303/303), pnpm lint/build clean sur
tout le repo (apps/api, apps/web, packages/*).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(specs): documente les conventions de développement du repo
Nouveau specs/dev-conventions.md — jusqu'ici ces règles n'existaient que
dans l'historique de commits/PR (classes vs objets littéraux pour la
logique de service, préfixe _ sur private/protected, règles Biome
actives, log service, tests sans mocks de la DB, conventions git/PR...),
rien de centralisé pour un futur contributeur (humain ou Claude Code).
Référencé depuis README.md, section "Qualité / Tests".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(web): regroupe pages/ par section au lieu d'un dossier à plat
pages/ mélangeait 8 fichiers directement à sa racine (LoginPage,
SignupPage, PlanningPage+scss, RecipesPage, RecipeFormPage,
ImportRecipePage, ShoppingListPage, ComingSoonPage+scss) à côté de deux
sous-dossiers déjà groupés (onboarding/, settings/) — incohérent, et
difficile à parcourir une fois le nombre de pages monté. Un sous-dossier
par section routée, même règle que onboarding/settings existants :
- pages/auth/ — LoginPage, SignupPage
- pages/planning/ — PlanningPage + planning-page.scss
- pages/recipes/ — RecipesPage, RecipeFormPage, ImportRecipePage
- pages/shopping-list/ — ShoppingListPage
ComingSoonPage (+ .scss) déménage vers components/ui/ — ce n'est pas une
page routée elle-même (ShoppingListPage l'enveloppe), c'est un composant
UI générique réutilisable, sa place est aux côtés de Dialog/Tooltip/etc.,
pas dans pages/.
Chemins relatifs internes de chaque fichier déplacé mis à jour (un niveau
de profondeur en plus), imports dans App.tsx repointés, tri Biome
réappliqué. specs/frontend-architecture.md mis à jour (arborescence +
références de chemin).
Vérifié : pnpm build clean (apps/web, 1952 modules), pnpm lint clean sur
tout le repo, testé en live dans le navigateur (login/signup, planning,
recettes, nouvelle recette, liste de courses, paramètres) — aucune route
cassée.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(web): regroupe features/recipes/ par sous-domaine au lieu d'un dossier à plat
20 fichiers à plat -> badges/ (DietTagSelect, DietBadges, AllergenBadges,
ReproducibleBadge, FavoriteStarButton), ingredients/ (IngredientPicker,
IngredientRow, ingredient-icons), steps/ (StepListEditor, StepDescription,
highlight-tech-steps), sources/ (RecipeSourcesPanel, SourceItemTable,
RecipeImportForm, recipe-import-draft, useEnabledSources).
RecipeTable/RecipeTabs/RecipeDetailPanel et recipes.scss restent à la
racine (composants transverses aux sous-dossiers, partagés par plusieurs
d'entre eux). Chemins relatifs corrigés dans les fichiers déplacés et chez
tous leurs importeurs externes (pages/recipes/*, features/planning/
RecipePickerDialog.tsx, features/profile/DislikedIngredientsField.tsx),
doc mise à jour (specs/frontend-architecture.md, specs/batch-cooking-
modele.md).
Vérifié : tsc --noEmit, biome check, build complet, 303 tests API,
vérification live navigateur (planning, /recettes, /recettes/nouvelle).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(api): regroupe lib/ par sous-domaine au lieu d'un dossier à plat
9 fichiers à plat -> recipe-sources/ (recipe-source-adapter, recipe-source-
errors, recipe-source-registry) et recipe-matching/ (recipe-translation,
ingredient-matcher, tech-step-matcher). jwt.ts, safe-profile.ts et
logger.service.ts restent à la racine de lib/ (pas de sous-domaine
partagé avec les autres).
Chemins relatifs corrigés dans les fichiers déplacés (profondeur +1 vers
db/) et chez tous leurs importeurs (modules/sources, modules/recipe,
sources/*, db/recipe-source-sync.ts, 12 fichiers de test), doc mise à
jour (specs/backend-architecture.md, specs/batch-cooking-architecture.md).
Vérifié : tsc --noEmit, biome check, build complet, 303 tests API.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(api): regroupe test/ par sous-domaine, miroir de src/lib/
18 fichiers à plat -> recipe-matching/ (ingredient-matcher, recipe-
translation, tech-step-matcher — miroir de lib/recipe-matching/),
recipe-sources/ (json-ld-recipe, recipe-source, recipe-source-sync,
the-meal-db — miroir de lib/recipe-sources/), sources/ (sources,
sources-index — module + registration src/sources/index.ts).
Les tests par domaine API sans regroupement naturel (auth, health,
house, logger.service, planning, preferences, profile, recipe,
reference) restent à la racine de test/, un fichier par domaine — même
logique que jwt.ts/safe-profile.ts restés à la racine de lib/.
Chemins relatifs corrigés (../src/ -> ../../src/, ../test-support/ ->
../../test-support/ dans les fichiers déplacés qui appellent
resetDatabase). .mocharc.json ("test/**/*.test.ts") couvre déjà les
sous-dossiers, aucun changement de config nécessaire.
Vérifié : biome check, 303 tests API.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(convention): impose try/catch autour de chaque await/corps async
Nouvelle règle de dev : aucun await nu, et un corps de fonction/méthode
async doit intégralement vivre dans un try/catch (pas seulement la ou
les lignes qui awaitent). Documentée dans specs/dev-conventions.md avec
son périmètre (code applicatif — services/hooks/composants/middlewares
— routes *.routes.ts exemptées car déjà couvertes par
wrapAsyncHandler ; tests et scripts one-off exemptés aussi).
Appliqué rétroactivement à tout le code applicatif qui ne l'était pas
déjà :
- api : auth/house/profile/preferences/planning/reference/recipe/
sources .service.ts, recipe-source-sync.ts, recipe-translation.ts,
ingredient-matcher.ts, tech-step-matcher.ts, json-ld-recipe.ts,
the-meal-db.ts — un try/catch par fonction async, rethrow simple
(le middleware d'erreur logge déjà tout centralement, voir
error-logger.ts) sauf quand un catch avait déjà une logique propre
(ex. le retry de createHouse).
- web : api/client.ts (_request), AuthContext.tsx, ThemeContext.tsx,
AppLayout.tsx (handleLogout), HouseholdSettingsPage.tsx (handleCopy/
handleRemove/handleDelete/handleLeave) — la plupart des handlers de
formulaire avaient déjà ce pattern, seuls ceux qui laissaient un
await nu ont été corrigés.
lint/complexity/noUselessCatch désactivé dans biome.json (interdisait
justement le catch-qui-rethrow que cette convention impose).
Vérifié : tsc --noEmit (api+web), biome check (0 erreur, repo entier),
build complet, 303 tests API, vérification live navigateur (thème,
déconnexion, copie du code d'invitation).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(web): corrige l'import cassé de highlight-tech-steps.cy.tsx
Oubli lors du regroupement de features/recipes/ par sous-domaine
(refactor(web): regroupe features/recipes/...) : le déplacement de
highlight-tech-steps.ts vers features/recipes/steps/ n'avait pas été
répercuté dans ce test composant Cypress (hors de apps/web/src, donc
raté par la recherche de référence externe à l'époque) — faisait
planter le job e2e en CI ("Failed to fetch dynamically imported
module").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Trois ajustements successifs sur le dialogue de sélection de recette
(RecipePickerDialog), demandés en continu après le premier correctif
de débordement :
1. Dialogue élargi et à hauteur fixe (95vw plafonné à 85rem, 80vh) au
lieu de dépendre du contenu, avec la répartition liste/détail
redéfinie en fractions du dialogue lui-même (3fr/2fr) plutôt qu'en
vw — cette dernière suivait la largeur du viewport, sans rapport
avec la largeur désormais fixe du dialogue.
2. Le formulaire de revue d'import (ex-ImportRecipePage) est extrait
dans un composant partagé, RecipeImportForm — toujours monté en
page autonome (route directe/rechargement), mais désormais aussi
intégré comme une étape du dialogue lui-même quand un item de
source a besoin d'une résolution manuelle, au lieu de naviguer et
perdre le contexte du picker (recherche, filtres, créneau).
3. Cliquer sur une recette dans le dialogue ne fait plus que la
sélectionner/prévisualiser (RecipeDetailPanel, comme /recettes) —
plus de saut automatique vers l'étape suivante. Un nouveau pied de
dialogue (Dialog.tsx gagne une prop ) porte Confirmer/
Fermer : Confirmer agit sur la sélection en cours (recette réelle
→ étape portions existante ; item de source pas encore importé →
import transparent ou formulaire intégré, point 2). Les onglets
réguliers gagnent leur propre paire maître-détail (RecipeTable +
RecipeDetailPanel, showActions=false) sur ce même modèle ; les
onglets source prévisualisent désormais aussi les items déjà
importés en interne (RecipeSourcesPanel), plus de saut direct.
Cypress (planning.feature/planning.ts) mis à jour en conséquence :
sélectionner puis confirmer sont deux étapes distinctes, le clic sur
la ligne ne déclenche plus rien tout seul.
Bug pré-existant trouvé en testant en direct (sans rapport avec ce qui
précède) : l'import d'une recette source plante avec une contrainte
d'unicité Prisma dès que deux lignes d'ingrédient se résolvent au même
ingrédient catalogue — signalé séparément (tâche en arrière-plan), pas
corrigé ici.
Vérifié en direct (navigateur, comptes de test) : sélection sans saut
d'écran, pied de dialogue activé/désactivé correctement, Confirmer sur
un item de source non résolu bascule vers le formulaire intégré,
Fermer ferme bien le dialogue.
pnpm exec tsc -b --force (web) — propre.
pnpm exec biome check — propre.
pnpm --filter web build — propre.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Nouvelle correction demandée sur cette PR : l'onglet générique
« Sources » (avec un <select> interne quand le foyer en a activé
plusieurs) devient une tab à part entière par source activée — au même
niveau que Favoris/Perso/Foyer/Publique, plus transparent qu'un
sélecteur caché dans un sous-menu.
- `RecipeTabs` accepte désormais `sources: SourceView[]` et rend une
tab par source (icône propre à la source si elle en a une, sinon
l'icône générique `SourcesIcon` ; libellé = le nom réel de la
source, pas une clé i18n). Nouveau type `RecipesPageTab` en
`RecipeTab | "source:<key>"`, avec `sourceTabValue`/
`parseSourceTabValue`/`isSourceTab` comme seul point d'assemblage/
lecture de ce format.
- Nouveau hook partagé `useEnabledSources` (déplacé hors de
`RecipeSourcesPanel`, maintenant utilisé par `RecipesPage` ET
`RecipePickerDialog` pour construire leurs tabs).
- `RecipeSourcesPanel` simplifié : `sourceKey` devient une prop requise
(fournie par la tab elle-même) au lieu d'un état interne avec son
propre sélecteur — plus de `<select>`, plus de message « aucune
source activée » (une tab qui n'existe pas ne peut plus être
cliquée). Remonté via `key={sourceKey}` par l'appelant au changement
de tab, même convention que `RecipePickerDialog`/`CalendarPopover`
ailleurs dans l'app.
- Un bug distinct trouvé en écrivant ce changement : passer tel quel
`initialSelection` (dérivé de l'URL) au panneau nouvellement monté
en changeant directement de tab source à tab source aurait fait
prévisualiser l'ancien item contre la nouvelle source. Gardé en ne
transmettant `initialSelection` que lorsqu'il appartient réellement
à `activeSourceKey`.
Aucun changement backend.
Tests :
- Vérifié manuellement en local (foyer avec TheMealDB activé) :
tab dédiée dans /recettes et dans le sélecteur du planning, parcours
d'un item, aperçu unifié, aucune régression console.
- Cypress : `recipe-sources.feature`/`planning.feature` mis à jour
(« I click the button "Sources" » → « ... "TheMealDB" »), scénario
« aucune source activée » réécrit pour vérifier l'absence de tab
plutôt qu'un message dans un onglet qui n'existe plus.
- `pnpm exec tsc -b --force` (web) — propre.
- `pnpm exec biome check` — propre.
- `pnpm --filter web build` — propre.
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>
Retire le dernier volet du scénario (rouvrir le sélecteur, rebrowser la
source, vérifier le badge « déjà importée ») — la CI l'a fait échouer
(le badge n'apparaissait jamais sur le second passage) sans qu'une
relecture du code de RecipePickerDialog/RecipeSourcesPanel/de leurs
mocks stateful n'explique pourquoi à distance, sans accès aux
screenshots Cypress (non remontés en artifact CI). Plutôt que
d'itérer à l'aveugle sur un mock complexe, on retire ce volet : le
comportement qu'il vérifiait (« browse marque bien alreadyImported une
fois la Recipe créée ») est déjà entièrement couvert côté Mocha
(sources.test.ts, étape 1) — cette assertion Cypress était redondante,
pas la seule preuve du comportement.
Le reste du scénario (le vrai objet de cette étape : sélectionner un
item non importé depuis le planning, l'importer en résolvant un
ingrédient, et le voir atterrir dans le bon créneau après le retour
sur "/") est inchangé et n'a jamais été mis en cause par cet échec.
Simplifie `planning.ts` en conséquence : `browsing TheMealDB returns
some items`/`importing the previewed item ...` redeviennent des mocks
statiques (le flag `fishPieImported` qu'ils entretenaient n'a plus de
lecteur), et le step Then devenu mort est retiré.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dernière étape du plan « onglet Sources » : le sélecteur de recette du
planning (`RecipePickerDialog`) gagne l'onglet « Sources », jusqu'ici
volontairement exclu faute d'écran de revue à qui transmettre un item
choisi (voir étape 3, #47).
- Sélectionner un item déjà importé se comporte exactement comme
choisir cette même recette depuis un onglet normal (résolue via
`GET /recipes/:id`, direction vers l'étape « combien de portions ? »
du dialogue, sans navigation).
- Sélectionner un item pas encore importé bascule vers l'écran de
revue existant (`ImportRecipePage`), avec le créneau du planning
porté par la query string (`?planningDate=&planningWeekDay=&planningMeal=`).
Un import réussi y ajoute alors automatiquement la recette
fraîchement créée à ce créneau (`POST /planning/items`, avec les
portions du formulaire) avant de revenir sur le planning — plutôt que
d'atterrir sur la page de la recette comme le fait un import « classique ».
- `RecipeSourcesPanel`/`SourceItemPreviewPanel` généralisés en
conséquence : la première ne navigue plus elle-même vers la recette
déjà importée (`onSelectImportedRecipe` renvoie l'id, chaque appelant
décide), la seconde propage le créneau optionnel sur son lien
d'import.
Aucun changement backend : `POST /sources/:sourceKey/import/:externalId`
et `POST /planning/items` existaient déjà et suffisent tels quels — une
fois la recette importée, `GET /sources/:sourceKey/browse` la marque
déjà `alreadyImported` automatiquement (logique déjà couverte par
`sources.test.ts`). 282 tests API toujours au vert, aucune régression.
Tests :
- Cypress : nouveau scénario Gherkin bout-en-bout
(`cypress/e2e/planning.feature`/`planning.ts`) — ouvrir le
sélecteur depuis un créneau vide, parcourir Sources, importer un
item non résolu (ingrédient à compléter compris), vérifier que la
requête d'ajout au planning porte bien le bon créneau/les bonnes
portions, que la recette apparaît dans la bonne case de la grille
après le retour sur "/", puis que rebrowser la source la marque
désormais comme déjà importée.
Suite : plan « onglet Sources » terminé (étapes 1 à 4).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le lien « Importer cette recette » de SourceItemPreviewPanel est un
<Link> (donc un <a>), pas un <button> — le scénario utilisait "I click
the button ..." qui ne cible que button, d'où l'échec en CI
("Expected to find content: 'Importer cette recette' within the
selector: 'button' but never did"). Bascule vers le step générique "I
click the link ..." déjà présent dans common.steps.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend :
- `createRecipe` refactorisé en fine enveloppe autour d'un nouvel
helper interne `createRecipeInternal`, paramétré par une source
d'import optionnelle ; nouvelle fonction exportée
`createImportedRecipe` qui réutilise toute la validation
ingrédients/unités/diets et le matching des tech steps, sans
dupliquer cette logique.
- La locale de l'adaptateur source est propagée jusqu'au chargement
des `TechStepMapping`, pour que le texte anglais (TheMealDB, etc.)
soit matché contre le bon jeu de règles au lieu du défaut français.
- Nouvel endpoint `POST /sources/:sourceKey/import/:externalId` —
valide le payload via `createRecipeSchema` (même schéma qu'une
création manuelle) et persiste une vraie `Recipe` liée à la source
(`sourceId`/`externalId`).
- Nouveau code d'erreur `RECIPE_ALREADY_IMPORTED` (4022) quand
l'item a déjà été importé pour ce foyer.
Frontend :
- `ImportRecipePage` (nouvelle page, `/recettes/importer/:sourceKey/:externalId`) —
pré-remplit le formulaire depuis `previewSourceItem`, en miroir de
`RecipeFormPage` (mêmes sous-composants : `IngredientRow`,
`IngredientPicker`, `StepListEditor`, `DietTagSelect`). Ajoute une
section dédiée aux lignes d'ingrédients non résolues automatiquement :
l'utilisateur choisit un ingrédient réel via l'`IngredientPicker`
existant ou retire la ligne — aucune recette invalide n'est jamais
soumise, le bouton d'import reste désactivé tant qu'il en reste.
- `SourceItemPreviewPanel` gagne un lien « Importer cette recette »
vers cet écran.
Tests :
- Mocha (`apps/api/test/sources.test.ts`) : 6 nouveaux tests sur
`POST /sources/:sourceKey/import/:externalId` (payload valide,
ingrédient/unité inconnus, déjà importé, deux foyers distincts,
locale de la source respectée pour les tech steps). 282 tests
passent au total, aucune régression.
- Cypress : nouveau scénario Gherkin bout-en-bout dans
`recipe-sources.feature` (parcourir → prévisualiser → importer →
résoudre un ingrédient non reconnu → confirmer → atterrir sur la
recette sauvegardée). Steps d'édition d'ingrédients/étapes
génériques déplacés de `recipe-form.ts` vers
`cypress/support/step_definitions/common.steps.ts`, réutilisables
par ce nouveau scénario.
Suite : étape 4 (ajouter au planning déclenche l'import si nécessaire).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La CI a révélé que la résolution des steps Cucumber n'est pas globale sur
tout cypress/e2e/ comme je le pensais — seuls le fichier/dossier de même
nom que la .feature et cypress/support/step_definitions/ sont cherchés
(voir le message d'erreur de la CI). recipes.ts vit directement dans
cypress/e2e/ (pas dans step_definitions/), donc ses steps ne résolvaient
que pour recipes.feature — recipe-sources.feature qui les réutilisait
plantait avec "Step implementation missing".
- Les steps génériques réellement partagés entre les deux features
(heading du panneau détail, technique surlignée, tooltip) migrent vers
common.steps.ts (step_definitions/, cherché globalement) plutôt que
d'être dupliqués une deuxième fois.
- Les steps propres à un fixture précis (recipe 2's detail, disliked
ingredients, catalogue vide) sont redéclarés localement dans
recipe-sources.ts avec leurs propres données minimales — même
précédent que recipes.cy.ts, qui duplique déjà indépendamment le
fixture omeletteDetail plutôt que de dépendre de recipes.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deuxième étape du chantier "onglet Sources" : l'UI de parcours, construite
contre les endpoints backend de l'étape 1 (#45). L'onglet désactivé
placeholder de RecipeTabs devient un vrai onglet fonctionnel.
- RecipeTabs.tsx : nouveau type RecipesPageTab (RecipeTab | "sources") —
gardé hors du type partagé RecipeTab puisque l'API n'a pas de
tab=sources à valider. Un prop `tabs` optionnel restreint quels onglets
s'affichent — RecipePickerDialog (choix d'une recette pour un planning)
s'y restreint aux 4 onglets réels, parcourir des sources externes en
plein milieu de ce dialogue n'a pas de sens sans le flux de revue/import.
- Nouveau RecipeSourcesPanel.tsx : contenu de l'onglet "Sources" —
autonome (son propre master-detail), ne partage pas le fetching
RecipeTab de RecipesPage puisqu'il parcourt le catalogue *live* d'une
source (GET /sources/:key/browse), pas la table Recipe sauvegardée.
Sélecteur de source si le foyer en a activé plusieurs ; sélectionner un
item déjà importé navigue directement vers la vraie recette
(SourceItemTable + navigate), un item pas encore importé affiche un
aperçu en lecture seule (SourceItemPreviewPanel, réutilise
StepDescription — les tech steps sont donc déjà surlignés dans
l'aperçu).
- Bug trouvé et corrigé en écrivant le scénario Cypress : cliquer un item
déjà importé changeait l'URL mais restait affiché sur l'onglet Sources
(RecipesPage ne rend RecipeDetailPanel/RecipeTable qu'en dehors de
l'onglet "sources"). RecipeSourcesPanel prend maintenant un callback
`onViewImportedRecipe` pour repasser sur un onglet réel avant de
naviguer.
Tests : nouveau recipe-sources.feature (parcours utilisateur complet —
onglet vide, parcours avec items importés/non importés, aperçu avec
surlignage de technique) ; recipes.cy.ts corrigé (assertion obsolète sur
l'ancien placeholder désactivé). Étape suivante (3/4) : écran de revue
(corriger les ingrédients non résolus) + finalisation de l'import.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jsonLdRecipeAdapter était enregistré (et donc synchronisé comme Source
sélectionnable par un foyer) au même titre que theMealDbAdapter — mais
c'est une structure générique de parsing schema.org destinée à être
déclinée par site web scrappé, pas une source qu'on peut raisonnablement
« activer » ou « faire confiance » en tant que telle (aucun catalogue à
parcourir : list() renvoie toujours vide).
registerAllRecipeSources() ne l'enregistre donc plus — elle reste
utilisable directement (un futur adapter par site l'utiliserait en
interne, ou un futur flux « importer depuis une URL » l'appellerait
directement), simplement plus comme Source autonome du registre.
Corrige aussi le mock Cypress qui prétendait « mirrorer ce qui est
vraiment seedé » avec cette même source — remplacé par un second exemple
clairement illustratif (Marmiton), qui ne correspond à aucun adapter réel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same class of failure as the household-settings sources scenario earlier
this session: the steps section sits below the detail panel's
header/photo/description, off the fold of .app-content's own scroll — a
bare .should("be.visible") doesn't auto-scroll.
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>