82 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 23bc19b482 |
Merge pull request 'test(cypress): convention de viewports téléphone/tablette' (#94) from feat/mobile-cypress-viewports into main
Some checks failed
|
|||
| 0ff94c61ca |
Merge pull request 'fix(planning): popovers flottants sûrs sur mobile' (#95) from feat/mobile-popovers-safe into main
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 3m26s
CI / build (push) Successful in 3m30s
CI / intent-service-test (push) Successful in 5m31s
CI / e2e (push) Successful in 9m45s
CI / test (push) Has been cancelled
|
|||
| 0447c68b69 |
merge: rapatrie le correctif de serialisation CI (#96)
All checks were successful
|
|||
| a3701acae4 |
fix(planning): popovers flottants surs sur mobile
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 3m18s
CI / intent-service-test (push) Successful in 4m8s
CI / build (push) Successful in 3m42s
CI / test (push) Failing after 10m11s
CI / e2e (push) Has been cancelled
Ticket #87 du chantier UI mobile & tablette. LeftoversBasket et WeekNavigator's CalendarPopover etaient tous les deux des popovers a largeur fixe (18-20rem), ancres right: 0, sans aucune gestion de bord d'ecran - sur un telephone etroit ils pouvaient deborder hors du viewport. Les deux sont declenches depuis l'en-tete du planning, un ecran central de l'usage quotidien - tres probablement la cause concrete derriere le retour terrain "je n'ai pas acces a tous les boutons" sur mobile qui a lance ce chantier. Les deux basculent desormais sur BottomSheet (ticket #91) sous le breakpoint phone via useIsMobileViewport() - meme contenu, juste une autre enveloppe (le popover flottant + son ecouteur de clic exterieur manuel restent inchanges sur desktop). Le clic-exterieur devient un no-op propre sur mobile plutot qu'un ecouteur qui ne fait jamais rien (le ref ne s'attache a rien dans la branche BottomSheet). Nouvelle cle i18n common.calendar.title ("Choisir une semaine") pour le titre de la feuille - pas de raccourci en decoupant une autre cle interpolee. Verifie en local : tsc -b --force, biome check, build de prod, suite Cypress e2e complete (142/142, 2 nouveaux tests phone dans leftovers-basket.cy.ts et planning-page.cy.ts) et composants (63/63). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 967be6df4a |
test(cypress): convention de viewports telephone/tablette
Some checks failed
Ticket #83 du chantier UI mobile & tablette. Le repo n'avait aucun override de viewportWidth/Height ni pour e2e ni pour les tests de composants - tout tournait a la taille par defaut, jamais un vrai telephone/une vraie tablette. - cypress/support/viewports.ts : PHONE_VIEWPORT (390x844) et TABLET_VIEWPORT (900x1200), tailles Android reelles plutot que les presets iPhone/iPad de Cypress (appli Android uniquement) - dans les bandes de _breakpoints.scss ($phone/$tablet du ticket #91), pas collees au pixel de bordure. Commande cy.setViewport("phone" | "tablet") (pas de surcharge du cy.viewport natif). Importe par support/e2e.ts et support/component.ts, disponible dans les deux modes. - layout.cy.ts : les cy.viewport(...) en dur pour du telephone (deux tests ajoutes par #91, plus les deux plus anciens du passage #39) et le test "au-dessus du point de rupture telephone" migrent vers la commande partagee - ce dernier passe a un vrai viewport tablette (900px, dans la bande 641-1024) plutot qu'un 1000x700 arbitraire, prouvant au passage que le preset tablette fonctionne. - Nouveau cypress/component/Dialog.cy.tsx : premier test de Dialog en isolation (jusqu'ici seulement couvert indirectement via RecipePickerDialog) - verifie le plein-ecran sur telephone du ticket #91, qui n'avait pas encore son propre test, et prouve au passage que cy.setViewport fonctionne en mode composant. Trouvaille en verifiant : le mode composant de Cypress a son propre viewport par defaut, plus petit que celui du mode e2e (1000x660) - deja sous le seuil telephone. Le premier essai du test "desktop" de Dialog echouait pour cette raison (le CSS plein-ecran s'appliquait sans meme appeler setViewport). Corrige en fixant un viewport large explicite pour ce cas plutot que de compter sur un defaut implicite - note ajoutee a dev-environment-gotchas pour la prochaine fois. Verifie en local : tsc -b --force, biome check, build de prod, suite Cypress e2e complete (140/140) et composants (65/65, incluant les 2 nouveaux tests de Dialog.cy.tsx). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| d6cf4aa63d |
feat(mobile): redirection vers l'appli + page de telechargement
Some checks failed
Ticket #84 du chantier UI mobile & tablette. Aucune infra de deep-link n'existait (verifie en amont : pas de scheme personnalise, pas d'App Links, pas d'assetlinks.json, pas de listener appUrlOpen). - lib/mobile-app-redirect.ts : attemptOpenNativeApp() (pattern "smart app banner" classique - tente le scheme batchcooking://, bascule sur onFallback si la page est toujours visible passe un court delai) et isAndroidPhoneBrowser() (UA Android+Mobile, deliberement distinct de useIsMobileViewport() - une question de plateforme, pas de largeur). Respecte une preference "rester sur le web" (localStorage, cle exportee pour que l'onglet Parametres d'application - ticket #86 - la reutilise) sans jamais forcer la redirection. - AndroidManifest.xml : nouvel intent-filter additif (scheme batchcooking, sans toucher au filtre MAIN/LAUNCHER existant). - Nouvelle page /telecharger (publique, hors RequireAuth - un visiteur deja connecte sur le web mais sans l'appli sur cet appareil a une raison legitime d'y atterrir) : lien direct vers le proxy existant ${API_BASE_URL}/mobile/download (rien a changer cote API), bouton "reessayer d'ouvrir l'appli". Deliberement pas de changement sur la landing page ici (lien discret actuel laisse tel quel) - c'est le ticket #85, separe pour rester petit/revuable. Verifie en local : tsc -b --force, biome check, build de prod, suite Cypress e2e complete (141/141 sur cette branche - #91 n'est pas encore merge, layout.cy.ts y est donc encore dans son etat pre-#91) et composants (69/69, incluant le nouveau mobile-app-redirect.cy.tsx). Note technique : le premier essai de test du composant tentait un vrai window.location.href vers le scheme personnalise depuis Cypress/ Electron, ce qui bloquait le runner indefiniment (protocole inconnu mal gere par Electron) - corrige en rendant ce seul appel injectable (openAppUrl, reserve aux tests) sans toucher au comportement reel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 0fd4928efc |
feat(layout): socle UI mobile - nav, feuille et dialogs plein ecran
All checks were successful
Fondation du chantier UI mobile & tablette (#91) : jusqu'ici, toute adaptation mobile de ce repo etait du CSS pur sur le meme DOM (aucun composant ne branchait son JSX selon la largeur/plateforme). Ce ticket introduit le premier vrai branchement, et les primitifs que les 8 tickets suivants du chantier reutiliseront. - useIsMobileViewport() (apps/web/src/lib/) : point de verite unique, base sur matchMedia, live (reagit a un resize/une rotation). - _breakpoints.scss : $phone/$tablet Sass + mixins phone/phone-up/ tablet-down, remplacant les nombres magiques 640/1024 repetes en dur dans 6 fichiers (AppLayout, landing, recipes, recipe-picker-dialog, planning-page, dialog, tours-page). Les deux occurrences de 860px (landing hero, tours-page) sont deliberement laissees telles quelles - un collapse de grille 2 colonnes qui n'a rien a voir avec le breakpoint tablette du catalogue recettes, les unifier aurait change un comportement visuel reel sans raison. - useNativeDialogBehavior : mecanique (showModal/close event/clic exterieur) extraite de Dialog.tsx, partagee avec le nouveau BottomSheet.tsx (feuille mobile plein-largeur, meme fondation <dialog> natif que Dialog, juste ancree en bas). - AppLayout : sur telephone, la sidebar n'est plus reflow-ee en CSS en barre horizontale (l'ancien traitement) - elle est remplacee par une vraie barre d'onglets basse (Planning/Recettes/Liste de courses + Plus), le "Plus" ouvrant une BottomSheet combinant Parametres et Compte (deux dropdowns flottants separes cote desktop -> un seul point d'entree mobile, IA plus simple, un seul emplacement d'onglet). - dialog.scss : vrai plein-ecran (bord a bord, sans arrondi) sous le breakpoint phone au lieu d'une marge simplement reduite. Verifie en local : pnpm --filter web exec tsc -b --force, biome check, pnpm --filter web build, suite Cypress e2e complete (140/140) et composants (63/63) - y compris layout.cy.ts, reecrit pour tester la nouvelle barre basse (l'ancien test de la barre horizontale n'a plus de sens, ce traitement n'existe plus). Closes #91 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 8315f737d4 |
Merge pull request 'fix(auth): la déconnexion retombe sur / plutôt que /login' (#79) from fix/logout-redirect-landing into main
All checks were successful
|
|||
| 21cd8f014d |
fix(auth): la deconnexion retombe sur / plutot que /login
All checks were successful
RequireAuth.tsx affiche deja la LandingPage publique pour un visiteur deconnecte exactement sur / (issue #41) — seul handleLogout naviguait encore explicitement vers /login. Change juste la destination de navigate() ; aucun autre changement necessaire. Scenario Cypress mis a jour en consequence (le pas existant "the URL should be the home page" suffit, pas besoin d'un nouveau pas). Verifie en local, suite complete : pnpm lint, pnpm --filter web build, pnpm run e2e (139/139, apres avoir nettoye un process vite zombie sur le port 5173 laisse par une precedente invocation cypress dans cette meme session — voir dev-environment-gotchas pour le detail). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 3abb7217dd |
feat(mobile): proxie le telechargement de l'APK via l'API (pas de CORS)
All checks were successful
Suite au fix de /releases/latest (precedent), l'utilisateur voulait un
lien qui declenche le telechargement du .apk directement, sans quitter
l'appli web. Verifie en direct : fetch("https://git.dev.kyuno.fr/api/...")
depuis le navigateur echoue avec "Failed to fetch" (confirme via un essai
en mode no-cors : la requete reseau aboutit bien, seule la reponse est
bloquee) — Forgejo n'envoie aucun header CORS sur son API Releases,
publique ou non. Ca ne concerne pas que la page d'accueil : le hook de
verification de mise a jour (#66, useAppUpdateCheck) faisait exactement le
meme fetch cross-origin depuis la WebView native, et echouait donc tout
aussi silencieusement en production sans que personne ne puisse s'en
rendre compte (le hook avale toute erreur reseau par design).
Ajoute un petit module apps/api/src/modules/mobile/ (2 routes publiques,
aucune ne touche la base de donnees) :
- GET /mobile/latest-release -> { version } (proxy JSON, remplace le fetch
direct de useAppUpdateCheck)
- GET /mobile/download -> redirection 302 vers l'asset .apk reel de la
derniere release (ou vers la page /releases/latest en repli si
Forgejo est injoignable ou qu'aucun .apk n'est encore attache)
Cote frontend : LandingPage.tsx et UpdateBanner.tsx pointent maintenant
vers ce proxy (`${API_BASE_URL}/mobile/download`, target="_blank") au lieu
d'une URL Forgejo directe — un clic, telechargement immediat, jamais de
navigation hors de l'appli. useAppUpdateCheck.ts appelle desormais
apiClient.getLatestMobileVersion() au lieu d'un fetch direct.
Verifie en local (sans Postgres, ces routes n'en ont pas besoin) contre
les vraies donnees Forgejo :
GET /mobile/latest-release -> {"version":"v0.2.0"}
GET /mobile/download -> 302, Location vers le vrai .apk signe
pnpm lint / tsc (api + web) / pnpm --filter web build : tous verts. Le
scenario Cypress "offers the Android app download" echoue encore en local
(meme souci pre-existant deja documente sur la PR precedente, confirme
independant du code par CI reel) ; mis a jour pour asserter le nouveau
href relatif.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 29bd413de3 |
fix(mobile): corrige les liens vers /releases/latest/download
All checks were successful
Verifie en conditions reelles apres le premier vrai succes de bout en bout de release.yml (v0.2.0, Release Forgejo #12 avec batchcooking.apk attache) : Forgejo, contrairement a GitHub, n'implemente pas le raccourci /releases/latest/download/<fichier>. Seul /releases/latest lui-meme resout (redirige vers la page de la release) : /releases/latest/download/batchcooking.apk -> 404 /releases/latest -> 303 vers /releases/tag/v0.2.0 (OK) /releases/download/v0.2.0/batchcooking.apk -> 200 (OK, mais pas "latest") Corrige les deux endroits qui presumaient le raccourci GitHub (issue #65 et #66, deja mergees) pour pointer vers /releases/latest tout court : un clic de plus via la page de release Forgejo pour atteindre le fichier, mais un lien qui resout toujours plutot qu'un raccourci qui 404 en permanence. Met a jour le test Cypress de la page d'accueil et le commentaire de release.yml en consequence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 503781c95f |
feat(landing): lien de téléchargement de l'app Android (#65)
Sur la page vitrine `/`, sous la rangée de CTA du hero (pas un 3e bouton : un compte reste nécessaire quelle que soit la plateforme), un lien discret vers l'APK Android + une ligne d'aide. - Cible : l'asset `batchcooking.apk` de la dernière release taguée Forgejo (`/releases/latest/download/…`, nom de fichier stable) — pas un artefact de CI. 404 tant que #64 + la correction de release.yml ne sont pas faits : état transitoire assumé, ce ticket n'ajoute que le lien. - `mobileHint` : rappel honnête que c'est un .apk à sideloader, pas une distribution Play Store. - i18n `landing.hero.mobileDownload` / `mobileHint` ; style discret dans landing-page.scss ; scénario Cypress de plus (présence + href du lien). Vérifs : `pnpm build` + `pnpm lint` OK. `landing.feature` non lancé en local (le hoist `tslib` de `@cypress/vite-dev-server` dans ce node_modules est cassé par un `pnpm install --force` interrompu + un verrou concurrent sur `@esbuild/win32-x64` ; `pnpm-lock.yaml` inchangé, la CI n'est pas affectée) → couvert par le job `e2e` de la CI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 8e55f3d3f9 |
fix(recipes): rend StepDescription tolérant à un techSteps absent
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> |
|||
| b39105ac27 |
feat(web): page admin de CRUD des didacticiels (#44)
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>
|
|||
| 38fcf03417 |
feat(web): moteur de tutoriel guidé driver.js (#42)
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> |
|||
| da7b077098 |
feat(web): page vitrine publique à la racine (#41)
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> |
|||
| a8d86c7099 |
feat(web): passe responsive téléphone pour le contenu des pages (#39 phase 3)
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> |
|||
| 1bc9304cdc |
feat(planning): glisser-déposer un reste sur un autre créneau
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> |
|||
| 8278878898 |
feat(planning): panier des restes dans l'en-tête (lecture seule + jeter)
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> |
|||
| eba0ea70bd |
feat(planning): sépare portions demandées et portions préparées
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> |
|||
| bc077b770e |
fix(recipes): garde le popover de correction ouvert pendant la sélection d'un span
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> |
|||
| 1cd886cb8a |
test(recipes): fiabilise le scénario 3 pages (scroll réel + log de curseurs)
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>
|
|||
| c842fbb348 |
test(recipes): pilote le scroll dans le scénario 3 pages des sources
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>
|
|||
| c48625cb0c |
fix(recipes): stoppe le scroll infini des sources au lieu de boucler
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> |
|||
| 13d92f8fa8 |
feat(admin): pousse le monitoring en WebSocket au lieu du polling client
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> |
|||
| ba131bd8c8 |
fix(admin): isole l'API admin sous /admin/api pour éviter la collision avec les pages front
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>
|
|||
| c1aba1f18c |
fix(admin-monitoring): remplace le filtre resourceType cassé par une vérification manuelle
`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>
|
|||
| bd607b17f3 |
fix(recipes): corrige la numérotation des étapes faussée par le popover de correction
.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> |
|||
| 6c5978dd1c |
feat(recipes): refond le panneau de correction des métadonnées de tech-step
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> |
|||
| bdc1b4c9e9 |
feat(cooking-session): déplie une recette de l'onglet Recettes au lieu de rediriger
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> |
|||
| a24ef6a78b |
feat(cooking-session): regroupe les tâches du stepper en mini-sections par recette
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> |
|||
| 85268729e6 |
feat(cooking-session): regroupement d'étapes sensible aux métadonnées d'action
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> |
|||
| dfe0310b63 |
feat(cooking-session): onglets recettes/plan + stepper d'étapes cochables
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> |
|||
| 1a28d67218 |
test(admin): fixe l'intercept monitoring (route page == chemin API)
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> |
|||
| bbd9afe8aa |
refactor(admin): fusionne apps/admin-web dans apps/web sous /admin/*
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>
|
|||
| 16f593d5d0 |
feat(recipes): permet d'ajouter des ingredients hors-catalogue
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> |
|||
| e8ac2d1629 |
feat(cooking): page /cuisiner + bouton "Commencer a cuisiner"
- 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> |
|||
| 478914787f |
feat(recipes): fusionne l'edition de la technique et des metadonnees
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> |
|||
| 5ab9832131 |
fix(recipes): pre-selectionne la technique deja detectee dans le correcteur
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> |
|||
|
|
eef5db92b5
|
feat(recipes): permet d'associer ingredients/ustensiles a une correction de technique
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> |
||
|
|
109dde9c7b
|
feat(shopping-list): liste de courses agrégée depuis le planning (#73)
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> |
||
|
|
ba3c978c25
|
feat(recipes): scroll infini + placeholders sur le parcours des sources externes (#71)
* 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> |
||
|
|
92bea914e8
|
feat(tech-steps): fiabilise la détection des tech steps (corpus + LLM + corrections utilisateur) (#66)
* 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>
|
||
|
|
5d63ff9ea9
|
fix: corrige les bugs ouverts du repo (import TheMealDB, sidebar mobile) + doc (#59)
* 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>
|
||
| 85fd9bae7d |
feat(planning): le picker prévisualise avant de confirmer, formulaire de revue intégré
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> |
|||
| 73ae8169a1 |
fix(recipes): retire l'import manuel, le planning importe seul
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> |
|||
| 4381e63045 |
feat(recipes): chaque source activée devient sa propre tab
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>
|
|||
| 368ea08960 |
feat(recipes): unifie l'affichage des recettes externes avec les recettes réelles
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> |
|||
| 81848179bf |
fix(web-tests): simplifie le scénario planning, retire une vérif redondante/instable
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> |
|||
| 991f91bc0e |
feat(planning): ajouter au planning déclenche l'import si besoin (étape 4/4)
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> |