141 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 0447c68b69 |
merge: rapatrie le correctif de serialisation CI (#96)
All checks were successful
|
|||
| 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> |
|||
| 854d795c5d |
fix(release): corrige release.yml pour Forgejo et y attache un APK signe
All checks were successful
release.yml datait d'avant la migration vers Forgejo et n'avait jamais ete reteste : refs d'action bares (actions/checkout@v4, softprops/action-gh- release@v2) sans le prefixe URL complet qu'exige ce runner self-hosted (meme contrainte deja rencontree et documentee dans ci.yml). Ajoute aussi ce que #65 attendait reellement : un job build-android-apk (meme recette que le job build-android de ci.yml, issue #64) qui construit et signe l'APK, deduit versionName/versionCode du tag pousse (vX.Y.Z -> versionName X.Y.Z, versionCode majeur*10000+mineur*100+correctif) et l'attache a la Release Forgejo sous un nom de fichier stable (batchcooking.apk) via softprops/action-gh-release. C'est ce nom stable qui permet a /releases/latest/download/batchcooking.apk (page d'accueil, #65) de toujours pointer vers la derniere release sans jamais changer de lien. build.gradle : versionCode/versionName deviennent surchargeables via -PandroidVersionCode/-PandroidVersionName (defaut inchange : 1 / "1.0" pour tout build qui ne passe pas ces proprietes, comme le job build-android existant de ci.yml sur un simple push main). Duplique le job de build Android plutot que de factoriser en workflow_call reutilisable — le support des workflows reutilisables sur ce runner Forgejo/act self-hoste n'est pas verifie, et cette recette de ~15 lignes est deja eprouvee telle quelle dans ci.yml ; pas la peine de risquer une nouvelle surprise de compatibilite pour economiser un peu de duplication. Non teste de bout en bout dans cette PR : release.yml ne se declenche que sur un vrai tag pousse (jamais sur une simple branche/PR), et pousser un tag ici declencherait potentiellement un vrai redeploiement Portainer en production si PORTAINER_WEBHOOK_URL est deja configure — a tester deliberement avec l'utilisateur, pas par un tag de test improvise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 644164b25a |
Merge pull request 'feat(landing): lien de téléchargement de l'app Android (#65)' (#68) from feat/landing-apk-download into main
Some checks failed
|
|||
| 9c6feab064 |
fix(ci): corrige le bit exécutable de gradlew et le fiabilise en CI
All checks were successful
Le tout premier run réel du job build-android (après le merge de #64) échouait avec './gradlew: Permission denied' — gradlew était suivi par git en 100644 (non exécutable) au lieu de 100755, probablement perdu au commit initial du projet Android natif (#63) sur cette machine Windows. Corrige le mode suivi par git (git update-index --chmod=+x) et ajoute un chmod +x défensif juste avant l'exécution dans ci.yml, puisque ce bit n'a aucun sens sous Windows et peut être reperdu silencieusement par un futur commit sur cette machine. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| c0a07e8adc |
Merge pull request 'feat(ci): build, teste et empaquette un APK Android signé sur chaque push main' (#67) from feat/mobile-ci-android-apk into main
Some checks failed
|
|||
| 5d4d6be2dc |
feat(mobile): vérification automatique de mise à jour de version
Réutilise l'API Releases de Forgejo (publique, sans authentification) comme source de vérité pour "quelle est la dernière version" — zéro nouvel endpoint backend : useAppUpdateCheck compare App.getInfo().version (@capacitor/app, nouvelle dépendance) au tag_name de GET .../releases/latest, et pointe "Mettre à jour" vers l'URL stable de téléchargement (issue #65). Comparaison de version en logique pure (version-compare.ts), séparée du hook effectful — testée isolément via un test de composant Cypress (version-compare.cy.tsx, même patron que highlight-tech-steps.cy.tsx : aucun montage, juste describe/it/expect sur des fonctions pures). Natif uniquement (Capacitor.isNativePlatform()) : @capacitor/app et @capacitor/preferences restent en import dynamique dans le hook, jamais dans le bundle web (même patron que capacitor-token-store.ts) — vérifié dans le build (deux petits chunks séparés, pas dans le bundle principal). Vérification au démarrage à froid + à la reprise depuis l'arrière-plan (appStateChange), avec un throttle de 6h et une mémorisation de la version déjà rejetée dans @capacitor/preferences pour ne jamais re-harceler sur la même version. Bannière discrète (pas une Dialog modale), bouton "Mettre à jour" ouvrant le lien de téléchargement dans le navigateur système plutôt qu'un flux PackageInstaller in-app. Vérifié : pnpm lint, pnpm --filter web build, le test de composant (9/9), et layout/smoke/planning-page e2e (18/18, la bannière étant un no-op sur le web ne devait rien casser — confirmé). Closes #66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 76a99f0e77 |
feat(ci): build, teste et empaquette un APK Android signé sur chaque push main
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / build (push) Successful in 3m21s
CI / lint (push) Successful in 3m31s
CI / intent-service-test (push) Successful in 4m51s
CI / e2e (push) Successful in 10m34s
CI / test (push) Failing after 14m37s
Nouveau job build-android dans ci.yml, gardé par needs: sur les 5 jobs existants (lint/test/intent-service-test/build/e2e) et limité à main via if: github.ref == 'refs/heads/main' — c'est ce qui garantit que l'APK n'est empaqueté qu'une fois la suite de tests existante verte, pas juste construit en parallèle d'elle. APK signé avec un vrai keystore de release (jamais généré par la CI — Android refuse une mise à jour signée par une clé différente de celle déjà installée) : ajoute signingConfigs.release à build.gradle, lu depuis l'environnement (ANDROID_KEYSTORE_PATH/_PASSWORD, ANDROID_KEY_ALIAS/_PASSWORD), jamais codé en dur. *.jks/*.keystore désormais réellement ignorés par git (étaient commentés dans le .gitignore généré par Capacitor). README : documente les 5 secrets Forgejo requis et la génération du keystore. Aucune suite de tests instrumentés Android n'existe (zéro code natif au-delà de la coquille Capacitor) — "testée" est satisfait par le gate sur la suite existante, pas par une suite native à inventer. Risque documenté, pas résolu par avance : aucun build Android n'a jamais tourné sur ce runner Forgejo auto-hébergé, contrairement aux 4 autres jobs. Closes #64 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> |
|||
| fd5841d045 |
feat(mobile): ajoute le projet natif Android Capacitor (#39)
Dernière étape du portage mobile #39 : génère et versionne le projet natif Android (`npx cap add android`) que les phases 1 à 3 (jeton Bearer, passe responsive, plomberie web) préparaient déjà. - `apps/web/android/` — projet natif Capacitor 8.5. Permission `INTERNET` présente par défaut, `applicationId` = `fr.kyuno.batchcooking`. - Thème natif aligné sur la palette « Mise en Place » (`_theme.scss`) : `colors.xml` redéfinit `colorPrimary`/`Dark`/`Accent` (basilic/vermillon) par-dessus les défauts Material de la lib capacitor-android ; barre de statut basilic foncé, fond de fenêtre « porcelaine » pour ne pas flasher blanc au démarrage à froid ; `postSplashScreenTheme` pour la transition. - Icône adaptative + splash (clair et sombre, toutes densités) générés depuis `apps/web/assets/logo.svg` (monogramme « bC », géométrie seule, pas de `<text>`) via `@capacitor/assets` ; sources et commande de régénération documentées dans `apps/web/assets/README.md`. `@capacitor/assets` n'est pas ajouté aux dépendances (il tire `sharp`, hors `onlyBuiltDependencies`) — lancé via `npx`. - `.gitignore` racine : ignore `/.idea/` (Android Studio ouvre désormais le dépôt comme projet). `biome.json` : exclut `apps/web/android` du lint. - README : section « Application mobile (Android) » (prérequis + flux de build `build:capacitor` → `cap sync` → Android Studio). Le bundle web copié sous `android/app/src/main/assets/public/` reste git-ignoré (régénéré par `pnpm build:capacitor && npx cap sync android`). Build de l'APK + test sur appareil : à finir dans Android Studio — la CI self-hosted ne build pas Android. Vérifs : `pnpm build` + `pnpm lint` OK ; `npx cap sync android` OK ; tous les XML natifs bien formés. 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> |
|||
| 2beff139a3 |
feat(api): CRUD admin des scénarios de didacticiel (#44)
Deuxième volet backend du tutoriel guidé, sur le socle de #42/#44 : permet à un admin de créer/éditer/supprimer les scénarios que `GET /reference/tours` sert au moteur front. `/admin/api/tours` (agrégateur `admin.routes.ts`, tout derrière `requireAdmin`) : - `GET /` — tous les scénarios (activés **et** désactivés) + étapes ordonnées. - `GET /targets` — le catalogue `TourTarget` pour le sélecteur de cible de l'éditeur. - `POST /` — crée un scénario vide (`key` + `name`) ; 409 `TOUR_KEY_ALREADY_IN_USE` (nouveau code 4025) sur une clé prise. - `PATCH /:id` — édite `name`/`enabled` **et remplace toute la liste d'étapes en bloc** (delete-all puis recreate avec `order` = index), même convention que `updateRecipe` pour les étapes d'une recette. `key` immuable. 404 `TOUR_NOT_FOUND` (id inconnu), 404 `TOUR_TARGET_NOT_FOUND` (nouveau code 4054, `targetKey` d'une étape hors catalogue — vérifié avant d'écrire, les étapes réelles restent intactes si la requête est rejetée). - `DELETE /:id` — 204 ; les étapes et les `TourProgress` des utilisateurs cascadent. Tests Mocha (`admin-tours.test.ts`) : 401 sans session admin sur chaque route ; list (activés+désactivés) ; targets ; create→patch pour remplir les étapes ; remplacement en bloc ; 409 clé dupliquée ; 404 cible inconnue (sans toucher aux étapes existantes) ; 404 id inconnu ; delete + cascade vérifiée ; un scénario désactivé disparaît de `/reference/tours` mais reste dans la liste admin. Refs #44 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| cd8ce42df7 |
feat(api): modèle de données + API des scénarios de didacticiel (#42/#44)
Socle backend, entièrement testable en CI, pour le tutoriel guidé. Le moteur front (driver.js), l'instrumentation `data-tour-*` et le CRUD admin viendront ensuite. Schéma (`schema.prisma` + migration `20260906120000_tour_scenarios`) — les 4 modèles de #44, qui révisent le registre statique prévu par #42 : - `TourTarget` (`key` PK + `description`) : catalogue des cibles de surlignage valides, seedé, comme `TechStep`/`Unit`. - `TourScenario` (`key` stable, `name`, `enabled`) : un scénario rejouable. - `TourScenarioStep` (`order`, `route?`, `targetKey`, `title`, `content`) : `title`/`content` sont du **texte libre**, pas des clés i18n (#44 : contenu administrable → texte, comme le nom d'une recette). - `TourProgress` (`userProfileId` + `scenarioId`, PK composite) : présence d'une ligne = scénario vu (terminé *ou* passé). Seed (`reference-seed-data.ts`) : les 9 `TourTarget` correspondant aux `data-tour-*` à poser côté front, + les 2 scénarios de #42 (`general`, `techStepMetadata`) **créés seulement s'ils sont absents** (un admin en devient propriétaire ensuite). `techStepMetadata` reste court pour l'instant — #36 (popover qui se ferme en pleine sélection) est ouvert, v1 ne couvre que les gestes qui marchent déjà. API : - `GET /reference/tours` (public) → scénarios **activés** + étapes ordonnées, forme consommée directement par le futur `useTour()`. - `GET /profile/tours` → `{ completedTourKeys: string[] }`. - `POST /profile/tours/:tourKey/complete` → upsert idempotent ; corps vide = terminé, `{ skipped: true }` = passé. Émet `analytics.recordEvent` `tour.completed` / `tour.skipped` (le dashboard admin agrège déjà tout type d'`AnalyticsEvent` génériquement — analytics de #44 quasi gratuites). 404 `TOUR_NOT_FOUND` (nouveau code 4053) sur une clé inconnue/désactivée. `reset-db.ts` truncate les 4 nouvelles tables (reseedées juste après). Tests Mocha : `GET /reference/tours` (scénarios + étapes ordonnées, exclusion d'un scénario désactivé) ; `/profile/tours` GET+POST (upsert idempotent, `skipped`, 404 clé inconnue, 404 tour désactivé, isolation par profil). Pas de base Postgres locale → exécutés en CI. Refs #42, #44 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> |
|||
| ff36cefcde |
feat(web): plomberie web pour la coquille Capacitor Android (#39 phase 2)
Tout ce qui est vérifiable en CI de la phase 2 du portage mobile. La génération du projet natif (`npx cap add android`), le build de l'APK et le test sur appareil restent une étape manuelle (pas d'Android Studio ici) — cf. le corps de l'issue #39. - Dépendances : `@capacitor/core`, `@capacitor/android`, `@capacitor/preferences` (+ `@capacitor/cli` en dev), toutes en 8.5.x. - `capacitor.config.ts` : `appId` `fr.kyuno.batchcooking`, `webDir: "dist"`. - `apps/web/src/api/capacitor-token-store.ts` : implémentation `TokenStore` adossée à `@capacitor/preferences`, branchée via `setTokenStore()` (le point d'injection posé en phase 1). Importée dynamiquement dans `main.tsx` uniquement quand `Capacitor.isNativePlatform()` — le bundle web ne charge jamais `@capacitor/preferences` (vérifié : chunk séparé de 0,5 ko jamais requis côté navigateur). - `main.tsx` : `HashRouter` en natif (le bundle packagé sert `dist/` depuis l'origine locale de la WebView, sans réécriture des liens profonds vers `index.html`), `BrowserRouter` inchangé sur le web ; bootstrap async pour poser le token store avant le premier rendu. - `build:capacitor` (`vite build --mode capacitor`) + `.env.capacitor.example` : le build packagé a besoin de l'URL absolue de l'API en dur (pas de "même origine que la page" possible). Vérifié : `pnpm build` + `pnpm lint` verts ; Cypress smoke/layout/sidebar 14/14 en local (le refactor du bootstrap et le choix de routeur ne cassent rien côté web). Refs #39 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> |
|||
| 1d8c001035 |
feat(auth): accepte un jeton Bearer en repli du cookie de session
Phase 1 du portage mobile (#39) : purement additif côté navigateur, qui continue d'utiliser le cookie `session` httpOnly inchangé. - `POST /auth/login` et `POST /auth/signup` renvoient aussi le JWT dans le corps de la réponse (`AuthResponse` = `SafeUserProfile` + `token`), en plus de poser le cookie. Un client dont l'origine ne peut pas recevoir un cookie `SameSite=Lax` (WebView Capacitor) le stocke et le renvoie. - `requireAuth` lit le cookie en priorité, puis se rabat sur un en-tête `Authorization: Bearer <jwt>` quand le cookie est absent. Les deux mécanismes coexistent, aucun n'est retiré. - `apps/web` : `ApiClient` gagne une abstraction `TokenStore` (`get`/`set`/`clear`) ; l'implémentation par défaut est un no-op navigateur (le cookie fait tout). `_request` ajoute l'en-tête `Authorization` quand un jeton existe ; `login`/`signup` le stockent, `logout`/`deleteAccount` l'effacent. `setTokenStore` est le point d'injection de la version Capacitor (`@capacitor/preferences`), branchée en phase 2 quand la dépendance sera ajoutée. Tests Mocha : `requireAuth` authentifie via l'en-tête `Authorization` sans cookie, rejette un jeton Bearer invalide ou un schéma non-Bearer ; `login`/`signup` exposent bien le jeton dans le corps. 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> |
|||
| 1c62625c47 |
feat(planning): attribution et jet des restes + exclusion liste de courses / cuisine
PR 2/4 du chantier « gestion des restes » (issue #34). Deux nouvelles routes sur `/planning` : - `POST /planning/items/leftover` → `assignLeftoverPlanningItem`, 201. Crée un item « restes » (`sourcePlanningItemId` non nul) sur un créneau, même recette que la source (`recipeId` copié, jamais envoyé par le client). `portions` revérifié serveur contre le pool réellement disponible via `computeAvailableLeftover` → 409 `LEFTOVER_QUANTITY_EXCEEDED` (nouveau code, 4024) si dépassé ; 404 `PLANNING_ITEM_NOT_FOUND` si source inconnue / d'un autre foyer / elle- même un item « restes » (pas de restes-sur-restes). - `PATCH /planning/items/:id/discard-leftover` → `discardLeftover`, 200. Jette tout le pool restant (`discardedPortions += disponible`) ; idempotent (rien à jeter → renvoyé inchangé, aucun évènement). Exclusion : `shopping-list.service.ts` et `cooking-session.service.ts` filtrent `items: { where: { sourcePlanningItemId: null } }` dans leur `include` — un créneau qui ne fait que resservir un plat déjà cuisiné n'a rien à acheter ni à cuisiner. `getPlanningForDate` ne filtre pas (la grille affiche ces items). Analytics : `planning.leftover_assigned` / `planning.leftover_discarded`. Le mapping `PlanningItem` → `PlanningItemView` est factorisé (`toPlanningItemView`) — 4 chemins le partagent désormais. Tests (Mocha, DB) : `assignLeftoverPlanningItem` (succès + `recipeId` copié, 409 sur dépassement, 404 source étrangère/inconnue/« restes », cascade à la suppression de la source) ; `discardLeftover` (jette le disponible, idempotent, ne jette pas ce qui est déjà servi, 404) ; non-régression liste de courses et session de cuisine ignorant les items « restes ». 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>
|
|||
| 3b78582d29 |
test(recipe-form): corrige la visibilité flaky de l'étape d'ingrédient préchargé
Le step Gherkin partagé « the recipe should include the ingredient »
utilisait un `.should("be.visible")` nu sur `.ingredient-row__name`.
Sur le formulaire d'édition de recette, la liste d'ingrédients peut se
trouver sous le pli du scroll propre à `.app-content` (Nom/Description/
Photo/Portions/Visibilité/Régimes viennent avant) — un `.should`
seul ne scrolle pas automatiquement, contrairement aux commandes
d'interaction (`.click()`/`.check()`).
Repéré comme bug pré-existant et hors périmètre lors de la vérification
de #29/#30 (voir la note dans tech-step-metadata-editor-redesign.md).
Même correctif que « I should see the highlighted technique »
(common.steps.ts) et « I should see the highlighted temperature »
(recipes.ts) : `.scrollIntoView()` avant l'assertion.
Reproduit avant correctif (recipe-form.feature, scénario « Preloads an
existing recipe's ingredients when editing »: 4/5 puis 5/5 après le
correctif). Suite e2e complète: 27/28 fichiers verts (la seule
exception, admin-monitoring.cy.ts, est un flake distinct et
sans rapport, signalé séparément).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 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> |
|||
| 3d5998a535 | merge(main): rattrape main (#21 CI, #22 températures) | |||
| 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> |
|||
| 189df73b4c |
feat(tech-steps): detecte les temperatures dans les etapes
3e type de metadonnee de clause, a cote des ingredients et des ustensiles : temperature en °C (« 180°C », « 200 degres », °F converti), numero de thermostat (« th. 6 ») et intensite qualitative (« feu doux/moyen/vif » -> low/medium/high). services/tech-step-intent-service : - `temperature_extraction.py` : `extract_temperatures(text, locale)` pur, a base de regex (independant du modele spaCy et de l'entrainement). - `ProcessResult` / `ProcessResponse` gagnent `temperatures` (ajout additif au contrat ; cle `gas_mark` en snake_case sur le fil). `process()` les extrait meme pour une locale pas encore entrainee. - Tests `test_temperature_extraction.py` (10) ; `test_routes_process.py` : 2 assertions d'egalite stricte gagnent `"temperatures": []`, +1 cas. apps/api : - `IntentServiceTemperature` (mappe `gas_mark` -> `gasMark` a la frontiere). - `TechStepMatch.temperatures` : filtrees par appartenance de span a la clause, meme regle que les ustensiles. - `model StepTechStepTemperature` (aucune FK — la valeur structuree EST la donnee) + migration manuelle `20260829120000_step_tech_step_temperature` (cascade via le TRUNCATE de `step_tech_step`, rien a ajouter a reset-db.ts). Persistance + lecture dans `recipe.service.ts` (`recipeInclude`, `toStepTechStepViews`) et l'include de sequence fraiche du service de correction ; `sources.service.ts` (apercu d'import) les fait transiter. packages/shared : `StepTechStepTemperatureView` + `temperatures` sur `StepTechStepView`. apps/web : - `splitDescriptionSegments` enrobe `splitDescriptionByTechSteps` et redecoupe les segments non-keyword autour des spans de temperature (la logique technique intriquee reste intacte). `?? []` tolere une payload d'avant `temperatures`. - `StepDescription` : surlignage `.step-temperature` + Tooltip via `temperatureLabel` ; i18n `recipes.temperature.*`. - Tests composants +4 (22 verts) ; pas d'UI de correction (v1, comme les ustensiles). Verifie : biome + tsc + `pnpm -r build` ; web 102/103 e2e (l'echec est le flake pre-existant recipe-form.feature, sans rapport) + 49/49 composants ; pytest temperature + routes 15/15. `pnpm --filter api test` (Postgres + intent-service requis) non lance ici. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 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> |
|||
| fcaecb4e07 |
feat(admin): tri des corrections + declenchement du gate F1/backfill
PR 5 (derniere) du chantier admin. Remplace le duo CLI list-pending-training-suggestions.ts / retrain-tech-steps.ts par une UI. API (admin-tech-steps.service.ts, routes /admin/tech-steps/*, requireAdmin) : - GET /suggestions : TechStepTrainingSuggestion filtrees, groupees par technique, enrichies du contexte de la correction source. - GET /corrections : corrections brutes filtrables, incluant les suppressions correctedTechStepId:null invisibles ailleurs. - PATCH /suggestions/:id : edite synonymes/phrases et/ou status. - GET /training-data-snippet : bloc training_data.py a coller (lecture seule). - POST /retrain : runTechStepEvalSuite() (gate F1 vs MIN_OVERALL_F1) puis si passe backfillTechSteps() + marquage applied/rejected. Verrou memoire -> 409 RETRAIN_ALREADY_RUNNING. Gate echoue -> 200 gatePassed:false. N'edite pas le .py ni ne redemarre l'intent-service (manuel). Shared : nouveau ErrorCode RETRAIN_ALREADY_RUNNING (4023, + cle i18n apps/web), schemas (list*/update*/retrain*/snippet), types (TrainingSuggestion*/Correction*/RetrainResultView...). Front : CorrectionsPage (onglets Suggestions / Corrections brutes, bandeau caveat permanent, cartes editables + Appliquer/Rejeter, panneau snippet, panneau gate F1). Logique pure corrections.ts. i18n admin.corrections.*. AdminApiClient : 5 methodes. Tests : Mocha admin-tech-steps.test.ts (401 partout, groupement+filtre, PATCH 400/404/ok, corrections incluant removals, snippet, retrain shape + 409 concurrent) ; Cypress corrections.cy.ts (4 verts). Admin-web Cypress 13/13. specs/backend-architecture.md : section tri + retrain. 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> |
|||
| cc4278ec68 | Actualiser apps/web/package.json |