`RecipeDiet` (tag manuel) est supprimé — table droppée, `dietIds` retiré de
`createRecipeSchema`, `DietTagSelect` retiré des formulaires de recette
(création + import). `RecipeSummaryView.diets` / `RecipeView.diets` sont
désormais **dérivés** via `computeRecipeDietViews` (`@batch-cooking/shared`,
pur, testé isolément) :
- un régime (vegetarian / vegan / pescatarian) est retenu ssi *tous* les
ingrédients y sont compatibles (`IngredientDiet`, exposé par
`IngredientView.diets`) ;
- `glutenFree` est dérivé des allergènes (aucun ingrédient ne porte
`gluten`) — cohérent avec le fait que le seed ne le stocke pas dans
`IngredientDiet` ;
- `[]` dès qu'une recette contient un ingrédient placeholder (hors-catalogue) :
un ingrédient inconnu ne certifie rien.
Filtres du sélecteur de planning (`suitableForHousehold`, `dietIds`) : ne
peuvent plus être du SQL sur une colonne stockée — devenus des post-filtres
sur `diets` calculé dans `listRecipes` ; `omnivore` traité comme « aucune
contrainte ». `assertDietsExist` / `ErrorCode.DIET_NOT_FOUND` sur la création
recette supprimés (le code d'erreur reste, utilisé par la préférence profil).
`DietBadges` inchangé (affiche `diets` tel quel). `RecipePickerDialog` garde
son filtre régime (fonctionne via le post-filtre).
Tests : `recipe-diets.test.ts` (fonction pure) + `recipe.test.ts` réécrit
(régimes calculés attendus, tests DIET_NOT_FOUND retirés) ; Cypress OK
(formulaire sans sélecteur de régime, régression recettes/sources/planning).
Migration `DROP TABLE recipe_diet` — aucune donnée à conserver.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`RecipeIngredient.quantity`/`unitId` deviennent nullable (migration purement
additive) : une ligne de recette peut n'avoir ni quantité ni unité — sel,
poivre… ajoutés selon le goût. Le schéma partagé impose « quantité et unité,
ou aucune des deux » ; `toRecipeView` renvoie `null`/`null`.
`aggregateShoppingList` fait apparaître une telle ligne une seule fois, sans
quantité ni unité, jamais sommée ni mise à l'échelle. `ShoppingListPage`
affiche « selon le goût » à la place de la quantité.
Formulaire : `IngredientRow` gagne une case « Selon le goût » qui masque
quantité/unité ; `RecipeFormPage` et `RecipeImportForm` n'exigent plus
quantité/unité pour ces lignes et les envoient sans. Un import dont la source
laisse une ligne résolue sans quantité (« salt to taste ») arrive
directement en « selon le goût ».
Le chemin clause-de-technique (session de cuisine) tolérait déjà `quantity`
null — inchangé.
Tests : Mocha (création/édition d'une ligne sans quantité, rejet
quantité-sans-unité, liste de courses non sommée) ; Cypress (case à cocher +
parcours), régression recette/liste de courses OK.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`.recipe-form__actions` gagne un modificateur `--sticky`
(`position: sticky; bottom: 0`) : sur une recette longue (beaucoup
d'ingrédients/étapes) la barre reste visible sans redescendre. Ajoute un
bouton « Annuler » secondaire qui revient à la recette éditée
(`/recettes/:id`) ou au catalogue (`/recettes`) pour une création — sans
garde « abandonner les modifications ? », cohérent avec les autres
annulations de l'app (seule la suppression, irréversible, est confirmée).
Tests Cypress : la barre reste ancrée en bas après défilement vers le haut
d'un formulaire plus haut que la fenêtre ; « Annuler » navigue correctement
en création comme en édition.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- `apps/web/package.json` 0.2.1 → 0.2.2 (alimente `__APP_VERSION__` : pied
de sidebar + comparaison de `useAppUpdateCheck`), même convention que le
commit `chore(web): version 0.2.1`.
- `release.yml` : la step « Crée la Release Forgejo » devient idempotente —
si le tag est déjà connu de Forgejo (tag poussé à la main, ou ré-exécution
du job), l'API répond 409 ; on récupère alors la release existante au lieu
de faire échouer le job. C'est ce 409 qui avait fait tomber le run du tag
v0.2.2 poussé en avance.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`:app:mergeReleaseResources` (job CI `build-android`, main uniquement)
échoue : AAPT interdit la chaîne « -- » à l'intérieur d'un commentaire XML,
et `res/values-night/colors.xml` (issue #104, PR #120) citait les tokens
`_theme.scss` avec leur préfixe de custom-property CSS (`--color-*`) dans
ses commentaires.
Commentaires réécrits sans aucun « -- » (tokens nommés « color-x »), même
convention que `res/values/colors.xml`. Valeurs et éléments `<color>`
inchangés. Non vérifiable en local (pas de JDK 21 sur cette machine) ; le
job `build-android` du prochain run sur `main` valide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La liste de courses était volontairement en lecture seule. Elle gagne un
état « acheté » par article, utile en magasin (au doigt sur mobile).
- `shopping-list-progress.ts` (nouveau) : `ShoppingListProgressStore`,
calqué sur `CookingProgressStore` — `localStorage` uniquement, par
semaine (clé `batchcooking:shoppingListChecked:<weekStart>`), aucun état
serveur. Une coche n'est jamais autoritaire (la liste est recalculée
depuis le planning à chaque visite) et se perd sans dommage.
- `ShoppingListPage.tsx` : chaque ligne devient un `CheckboxOption` ;
`<ShoppingListItems key={weekKey}>` remonte à chaque changement de
semaine (checklist réinitialisée). Clé d'article = `ingredientId-unitId`
(déjà la `key` React de la ligne).
- `shopping-list-page.scss` : override scopé du look « carte sélectionnable »
global — un article coché est barré et estompé, pas surligné en primary.
Cible tactile ≥ 44 px sous le breakpoint phone.
- `specs/frontend-architecture.md` : la note « délibérément en lecture
seule » est remplacée.
Tests : `shopping-list.feature` — nouveau scénario « cocher un article »
(coché → barré, persiste au reload, n'affecte pas les autres).
`shopping-list-page.cy.ts` — la cible tactile de la ligne (le label
`CheckboxOption`) fait 16 px de padding + ≥ 44 px de haut sur téléphone.
Local : `pnpm lint` + `pnpm --filter web build` OK, `shopping-list.feature`
4/4, `shopping-list-page.cy.ts` 1/1.
Closes#109
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`AppTheme.NoActionBar` a bien un parent `DayNight`, mais sans dossier
`res/values-night/` ses `@color/colorPrimaryDark` / `@color/colorBackground`
résolvaient aux valeurs claires même en mode sombre système : la bande de
la barre d'état et la fenêtre pré-WebView (flash de démarrage à froid +
fond du splash) restaient sur la palette « porcelaine » claire pendant que
le téléphone — et le contenu WebView de l'app — passaient en sombre.
- `android/app/src/main/res/values-night/colors.xml` (nouveau) : variantes
sombres de `colorPrimary` / `colorPrimaryDark` / `colorAccent` /
`colorBackground`, calées sur les tokens `--color-*` sombres de
`_theme.scss`. En sombre, la bande de la barre d'état devient le
`--color-background` sombre (`#14181A`), sans raccord avec l'app.
`windowLightStatusBar=false` (icônes claires) reste correct dans les deux
modes. Aucun changement de `styles.xml` : il référence déjà ces noms de
couleur, le parent `DayNight` + `values-night/` fait le reste.
- `index.html` : `<meta name=theme-color>` (light/dark) pour teinter
aussi le chrome du navigateur mobile.
Vérification : impossible sur appareil/émulateur dans ce contexte — le job
CI `build-android` (main uniquement) compilera au moins la ressource. La
synchro live de la barre d'état avec le sélecteur de thème *in-app* (via
`@capacitor/status-bar` + JS) reste un éventuel suivi ; ce correctif règle
le « ne suit pas les réglages système » du rapport.
Closes#104
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ses sections n'ont pas de sens sur le web (pas de « version installée », la
vérif de mise à jour y est un no-op) et son seul réglage web-pertinent — la
case « toujours rester sur le site web » — vit mieux dans le flux qu'il
pilote.
- `AppLayout.tsx` : `SETTINGS_ITEMS` gagne un `nativeOnly`, filtré une fois
au chargement du module (`settingsNavItems`) via `Capacitor.isNativePlatform()`
— l'entrée disparaît du menu Paramètres (sidebar + feuille « Plus »).
- `App.tsx` : la route `/parametres/application` redirige vers
`/parametres/compte` sur le web, comme le précédent `/foyer`, pour qu'un
bookmark ne tombe pas sur une page orpheline.
- `AppSettingsPage.tsx` : devient natif-only — la branche `webHint` et la
section « rester sur le site web » sont retirées.
- La case « toujours rester sur le site web » déménage sur `/telecharger`
(`DownloadPage`) : page publique qu'un visiteur non connecté sur
navigateur téléphone peut atteindre, et c'est exactement le flux que la
préférence concerne. `localStorage` sur l'origine web publique, là où
`attemptOpenNativeApp` la lit. i18n `appSettings.redirect.*` →
`download.stayOnWeb.*` ; `appSettings.version.webHint` supprimée.
Tests : `sidebar.cy.ts` / `app-settings-page.cy.ts` — l'entrée et la page
sont absentes sur le web, la route redirige. `download-page.cy.ts` — la
case « rester sur le site web » par défaut décochée, persistée au reload.
Local OK (sidebar 5/5, app-settings 2/2, download 3/3).
Closes#107
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sur téléphone, l'onglet « Liste de courses » (16 caractères) passait sur 2
lignes dans un onglet à ~90px alors que « Planning »/« Recettes »/« Plus »
tenaient sur 1 ligne : son bloc icône+libellé centré était plus haut, donc
son icône visiblement plus haute que les autres.
`AppLayout.scss` `.app-bottom-nav` : le `.label` est masqué visuellement (à
la manière du rail desktop replié — `position: absolute; clip; …`), pas
retiré du DOM, donc chaque `<NavLink>`/bouton garde son nom accessible (un
lecteur d'écran annonce toujours « Planning », « Liste de courses »…) sans
`aria-label` qui dupliquerait la chaîne i18n. Icônes légèrement agrandies
(1.375rem → 1.5rem) puisqu'elles sont désormais seules.
Tests : `layout.cy.ts` — l'onglet expose son `span.label` dans le DOM mais
`not.be.visible`, l'icône `svg` visible (mise à jour de l'assertion qui
attendait le libellé visible). `sidebar.cy.ts` / `planning-page.cy.ts`
(nav desktop) inchangés. Suite layout/sidebar/planning revérifiée, 27/27.
Closes#105
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`isMobile = isAndroidPhoneBrowser()` est une détection d'user-agent : la
WebView Capacitor contient « Android »/« Mobile », donc le bouton
« Ouvrir dans l'application » s'affichait aussi *dans* l'app packagée — où
le taper tente de relancer l'app via le scheme, échoue, et retombe sur
`/telecharger`.
- Nouveau `canOfferApp = isMobile && !Capacitor.isNativePlatform()` — « un
navigateur *téléphone* où pousser l'app native a du sens ». Gate le
bouton et la rétrogradation de « Créer un compte » en secondaire.
- Le lien `/telecharger` (variante desktop) gagne aussi `&&
!Capacitor.isNativePlatform()` : sur tablette la WebView Capacitor peut
ne pas avoir « Mobile » dans son UA, donc `!isMobile` ne suffisait pas.
Aucun changement sur le web (`Capacitor.isNativePlatform()` y est toujours
`false`, donc `canOfferApp === isMobile`) — `landing.feature`
(navigateur téléphone + desktop) revérifiée, 7/7. Le chemin natif n'a pas
de couture de test depuis un run web (limitation déjà documentée, cf.
`app-settings-page.cy.ts`).
Closes#103
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La session expire déjà (JWT 7 j), mais le client web n'y réagissait pas :
seul le `GET /auth/me` du montage de `AuthProvider` attrapait un 401 ; un
401 de n'importe quel autre endpoint ne faisait que remonter une erreur
locale, l'UI connectée restant affichée jusqu'au prochain rechargement
complet. Sur l'app native (rarement redémarrée, token persisté) la session
paraissait ne jamais finir.
- `client.ts` : sur un `401 NOT_AUTHENTICATED` pour toute requête autre que
le probe `GET /auth/me`, `ApiClient._request` purge le token store et
appelle un handler `onSessionExpired` (enregistré via
`setSessionExpiredHandler`). L'`ApiError` continue d'être levée comme
avant.
- `AuthContext.tsx` : `AuthProvider` enregistre ce handler au montage —
`setUser(null)` + `navigate(/login, { replace: true })`, gardé par un
`userRef` pour être un no-op si déjà déconnecté. `ApiClient` restant
agnostique du framework, c'est bien lui qui délègue la navigation/état.
Pas de changement API : le back renvoie déjà 401 sur JWT expiré
(`require-auth.ts`), couvert par sa suite Mocha.
Tests : `apps/web/cypress/e2e/session-expiry.cy.ts` — un 401 en cours de
session redirige vers `/login` et vide l'état (un retour sur une route
gardée re-redirige) ; le 401 du `/auth/me` initial d'un visiteur non
connecté ne déclenche pas ce chemin (la landing reste affichée). Suite e2e
existante (auth / layout / account / recipes / onboarding / preferences)
revérifiée sans régression.
Closes#108
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`useAppUpdateCheck` tournait au montage de `<UpdateBanner />`, mais celui-ci
n'était monté que dans `AppLayout`, derrière `RequireAuth` : un utilisateur
non connecté sur l'app native voit `LandingPage`, `AppLayout` n'est jamais
monté, donc aucune vérification tant qu'on n'est pas connecté — alors que
le back suppose l'inverse (`mobile.routes.ts` : « runs on every app start,
session or not »).
- `<UpdateBanner />` monté dans `App.tsx`, hors `<Routes>`, donc rendu pour
toute route quel que soit l'état d'auth. Garde `!isNativePlatform()` et
throttle 6 h inchangés ; rend toujours `null` sur le web.
- `update-banner.scss` : passe en `position: fixed` (bandeau haut,
`env(safe-area-inset-top)`, `z-index: 100`) pour ne pas entrer dans la
mise en page / le calcul de hauteur d'une page routée (`.app-layout` est
`height: 100vh; overflow: hidden`).
Pas de test e2e : `Capacitor.isNativePlatform()` n'a pas de couture pour
simuler le natif depuis un run web (posture déjà assumée pour
`useAppUpdateCheck` / `UpdateBanner`, voir `app-settings-page.cy.ts`). Suite
e2e existante (layout / smoke / app-settings) revérifiée : aucun régression.
Closes#106
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`PlanningDayView` ouvre sur le jour *courant* de la semaine affichée
(`defaultDayIndexForWeek`, dérivé de `Date`). Le test « shows the one-day
mobile view » ne figeait pas l'horloge : il ne passait que quand la CI
tournait un lundi (UTC), et échouait les autres jours (`Curry de
lentilles`, moqué sur « lundi », hors du jour affiché).
`cy.clock(new Date(2026-08-17T09:00:00.000Z), [Date])` avant le
`cy.visit` — même lundi de référence que `planning-page.cy.ts` ; `[Date]`
seulement, les timers/debounces tournent toujours.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`table.planning-grid` a `height: 100%` mais des hauteurs de ligne pilotées
par le contenu : un créneau avec plusieurs recettes étirait son `<td>`,
donc toute la ligne (les autres cases gagnant la même hauteur morte sous
`vertical-align: top`), puis la table débordait de `.planning-grid-wrapper`
en scroll vertical.
- `tbody tr { height: 1px }` : les cinq lignes se partagent la hauteur du
wrapper à parts égales (une hauteur de ligne est un minimum que le
navigateur étire pour absorber la hauteur restante de la table), au lieu
d'être aussi hautes que la colonne la plus chargée.
- `.meal-cell__recipes` : `max-height: 8.5rem; overflow-y: auto` — au-delà
d'une poignée de recettes la liste scrolle dans la case. Cap en `rem` et
pas en `%` : les hauteurs en pourcentage sur un `<td>` ne se résolvent
pas de façon fiable selon le moteur. `.add-recipe-btn` (frère flex hors
de cette boîte) reste épinglé juste en dessous.
- `min-width: 62rem` laissé tel quel : les 7 colonnes ont besoin d'un
plancher pour rester lisibles, et le scroll horizontal dessous est
couvert par `layout.cy.ts`. Le breakpoint phone (vue `PlanningDayView`)
n'est pas concerné.
Closes#110
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sur téléphone, le dialogue d'ajout d'une recette au planning devenait
plein écran mais `.recipe-picker-dialog` gardait un `height: 80vh` figé.
Dans cette colonne flex de hauteur bornée, le bloc `.recipe-picker__filters`
(rendu pour les 4 onglets non-source) consommait toute la place et
`.recipes-page__catalog` (`flex: 1`) s'effondrait à ~0 : la table des
recettes était dans le DOM mais dans un scrollport de hauteur nulle, donc
les onglets Favoris / Perso / Foyer / Publique paraissaient vides.
Sous le breakpoint phone : le dialogue passe vraiment plein écran (comme
tout autre `Dialog`), `.recipe-picker__filters` ne se laisse plus
compresser, et le catalogue prend sa hauteur de contenu — c'est
`.dialog-panel__body` qui scrolle.
Closes#111
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sur un vrai navigateur mobile Android (isAndroidPhoneBrowser(), déjà
utilisé par le ticket #84), la ligne de CTA de la landing page affiche
désormais "Ouvrir dans l'application" comme action PRIMAIRE — plus
utile qu'un compte web puisque l'expérience web n'est de toute façon
pas optimisée mobile (au cœur de ce chantier), et un visiteur avec
l'appli déjà installée peut s'inscrire/se connecter depuis l'appli
elle-même. "Créer un compte"/"Se connecter" sont rétrogradés en
actions secondaires plutôt que masqués : certains visiteurs préfèrent
sciemment le web.
Le clic déclenche attemptOpenNativeApp (tentative d'ouverture du
scheme, repli automatique après le délai) plutôt que d'afficher un
second lien séparé — remplace .landing__mobile-link, ne le duplique
pas. Sur desktop, comportement inchangé (CTA "Créer un compte" toujours
primaire) sauf le lien Android discret, qui pointe maintenant vers
/telecharger (page de repli du ticket #84) au lieu du lien direct
/mobile/download — un seul point d'entrée "obtenir l'appli" pour tout
le site plutôt que deux mécanismes légèrement différents à maintenir
en synchro.
Couverture Cypress : landing.feature étendu avec un nouveau scénario
mobile (UA Android spoofée via onBeforeLoad, même chaîne que le test
unitaire existant d'isAndroidPhoneBrowser dans
mobile-app-redirect.cy.tsx) — vérifie l'affichage du bouton et la
rétrogradation de Créer un compte/Se connecter, SANS jamais cliquer le
bouton réel (son onClick appellerait un vrai window.location.href vers
un scheme personnalisé, qui bloque indéfiniment le runner Cypress/
Electron — piège déjà documenté, la fonction elle-même est déjà
testée unitairement avec un stub injecté). Le scénario desktop existant
est mis à jour pour le nouveau href /telecharger. Suite complète :
biome check, tsc -b --force, build, 145/145 e2e, 71/71 composants —
tous verts.
Closes#85
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nouvelle route /parametres/application + entrée dans SETTINGS_ITEMS
(desktop et sheet mobile, un seul tableau partagé). Thème et "rejouer
un tutoriel" restent inchangés dans /parametres/preferences-utilisateur
(décision explicite du chantier).
Le contenu affiché diffère réellement selon la plateforme, pas par
simple choix cosmétique — chaque moitié de la page est inerte sur
l'autre plateforme, pas juste hors-sujet :
- Version installée + "Vérifier les mises à jour" (natif seulement) :
useAppUpdateCheck (déjà utilisé par la bannière silencieuse
existante) gagne un checkNow() qui relance la même comparaison sans
attendre le throttle de 6h, réutilisant la logique existante plutôt
que de la dupliquer (fetchLatestVersionResult extraite en fonction
partagée). Nouveau hook useInstalledVersion (App.getInfo() natif,
__APP_VERSION__ web).
- "Toujours rester sur le site web" (web seulement, tout appareil) :
pilote STAY_ON_WEB_PREFERENCE_KEY (déjà exporté par
mobile-app-redirect.ts, ticket #84). Volontairement masqué côté natif
: le WebView Capacitor tourne sur sa propre origine https://localhost,
un localStorage totalement séparé de celui du vrai site web que lit
attemptOpenNativeApp — modifier la préférence depuis l'app ne
changerait donc jamais rien pour un visiteur mobile réel. Documenté
explicitement dans le commentaire de la page pour ne pas réintroduire
ce contrôle-fantôme plus tard.
Couverture Cypress : nouveau app-settings-page.cy.ts (contenu web,
persistance du toggle après reload) ; sidebar.cy.ts étendu pour couvrir
le nouveau lien. Le déclenchement natif réel (App.getInfo(), l'appel
réseau de checkNow) n'a pas de point d'entrée testable depuis un run
Cypress web — même posture déjà acceptée pour useAppUpdateCheck/
UpdateBanner, la logique de comparaison pure reste couverte par
version-compare.cy.tsx (inchangé). Suite complète : biome check,
tsc -b --force, build, 146/146 e2e, 71/71 composants — tous verts.
Closes#86
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux correctifs, regroupés en un seul ticket comme prévu :
- Catalogue de recettes (`recipes.scss`) : `.recipe-table-wrap` n'avait
aucun garde-fou de scroll horizontal, contrairement à la grille
planning — sur un viewport étroit, les 4 colonnes (photo/nom/
allergènes/régimes) se compressaient au lieu de scroller. Ajout
d'`overflow-x: auto` sur le wrapper + `min-width: 36rem` sur la
table (échelle réduite par rapport aux 62rem de la grille planning :
4 colonnes plus étroites, pas 7). Partagé par le catalogue complet
ET par `RecipePickerDialog`, qui hérite du même correctif au passage.
- Liste de courses (`shopping-list-page.scss`) : audit dédié — la page
n'a en réalité aucune case à cocher ni bouton de suppression (elle
est volontairement en lecture seule, voir le commentaire de
ShoppingListPage.tsx), contrairement à ce que supposait le ticket
d'origine ; rien à agrandir comme cible tactile de ce côté. La
structure ne déborde déjà pas non plus (`&-name` a déjà flex:1/
min-width:0). Amélioration retenue à la place : une ligne un peu
plus aérée et une icône plus grande sous le breakpoint phone, pour
une lecture plus confortable en faisant les courses (liste tenue
d'une main, coups d'œil répétés).
Couverture Cypress : nouveau describe "Recipe catalog (phone)" dans
recipes.cy.ts (garde-fou de scroll horizontal effectivement actif) ;
nouveau fichier shopping-list-page.cy.ts, spec layout simple (pas
Gherkin, cette page n'a pas encore ce type de fichier) pour le padding
de ligne. Suite complète : biome check, tsc -b --force, build, 146/146
e2e, 71/71 composants (un skip ponctuel de TechStepCorrectionPopover
en run groupé, reconfirmé 10/10 isolément — flake local sans lien avec
ce ticket) — tous verts.
Closes#90
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cooking-session-page.scss n'avait encore aucune règle @media. Audit
dédié à cet écran (minuteur inexistant sur cette page — rien à faire
de ce côté, liste d'étapes, actions) :
- Bug d'overflow réel dans l'onglet Recettes : `.cooking-recipes__name`
n'avait aucune borne dans sa ligne flex (contrairement au
`.recipe-chip__name` équivalent côté planning) — un nom de recette
long pouvait repousser le badge de portions et le chevron hors de la
case (silencieusement coupés par le `overflow: hidden` de la carte
parente). Corrigé avec le même traitement flex:1/min-width:0/
ellipsis que son équivalent.
- Cibles tactiles sous le breakpoint phone : les cartes de tâches (`.
cooking-task`, l'action principale et répétée de tout l'onglet Plan),
les nœuds du stepper, les onglets, la ligne de l'onglet Recettes et
les boutons Précédent/Suivant passent tous à --space-md pour dépasser
confortablement les 48dp minimums d'Android (l'app est Android-only)
même dans le pire cas (une tâche à une seule ligne, sans ustensiles).
- Piège rencontré : surcharger `.cooking-task` par un simple sélecteur
de classe ne fonctionnait pas silencieusement — la règle globale
partagée `label:has(> input[type="checkbox"])` (global.scss) a une
spécificité CSS plus élevée qu'il n'y paraît (l'argument de :has()
compte comme un élément + un attribut). Corrigé en réutilisant le
même niveau d'imbrication que `.cooking-phase__tasks .cooking-task
--done` juste au-dessus dans le même fichier, qui contournait déjà
ce problème pour la même raison.
Couverture Cypress : nouveau describe "Cooking session page (phone)"
dans cooking-session-page.cy.ts (2 tests — padding effectivement
appliqué + troncature du nom de recette sans pousser le badge hors
champ). Suite complète : biome check, tsc -b --force, build, 146/146
e2e, 71/71 composants — tous verts.
Closes#89
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La grille planning (7 colonnes × 5 repas, min-width: 62rem) était le
principal écran inaccessible sur téléphone : sous ce seuil elle ne se
réorganisait pas, elle scrollait juste horizontalement — un tableau
desktop rétréci, pas un vrai écran mobile.
Sous le breakpoint phone (useIsMobileViewport()), PlanningPage bascule
désormais entièrement sur PlanningDayView : un sélecteur de jour (jour
précédent/suivant, borné au lundi/dimanche de la semaine affichée, pas
de bouclage vers la semaine adjacente — ça reste le rôle des flèches du
WeekNavigator) suivi des 5 repas du jour sélectionné, empilés en
cartes.
Le contenu de chaque case (pastilles de recettes + bouton "+") est
mutualisé entre les deux layouts via une fonction non-hook partagée,
mealCellBody, appelée aussi bien par MealCell (<td>, grille desktop)
que par le nouveau MealCellRow (<div>, vue jour) — même logique de
drop-target @dnd-kit (useDroppable) des deux côtés, donc le glisser-
déposer d'un reste continue de fonctionner à l'identique sur mobile.
Mise à jour de layout.cy.ts : son test #39 "la grille reste scrollable
horizontalement sur phone" décrivait un comportement désormais remplacé
par ce ticket — réécrit pour vérifier la bascule vers la vue jour au
lieu du scroll horizontal.
Closes#88
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
StepDescription faisait `useState(techSteps)` puis `liveTechSteps.flatMap(...)` :
quand la prop `techSteps` arrivait `undefined`, `.flatMap` levait
`TypeError: Cannot read properties of undefined (reading 'flatMap')`, remontée
par Cypress comme une erreur applicative non capturée. Observé de façon
intermittente sur le parcours « modifier une recette » : après enregistrement,
`RecipeFormPage` navigue vers `/recettes/:id`, dont le détail se rendait parfois
avant que `techSteps` ne soit peuplé.
- `StepDescription` : `techSteps` devient optionnel et `liveTechSteps` retombe
sur `[]` (dans le `useState` et le `useEffect` de resync), pour que tous les
consommateurs (`.flatMap`, `splitDescriptionSegments`,
`findTechStepForTemperature`, lookup `activeStepTechStep`) aient toujours un
vrai tableau — même posture « tolérer la donnée manquante, ne pas planter »
que le reste du composant.
- `recipe-form.ts` : le mock de la recette 7 envoyait une étape sans `techSteps`,
contrairement au vrai `StepView` et à tous les autres mocks de la suite —
corrigé en `techSteps: []`.
- `recipes.cy.ts` : nouveau test de non-régression déterministe — un payload
d'étape sans `techSteps` doit rendre la description en texte brut. Vérifié
qu'il échoue bien (`flatMap` undefined) sans le correctif du composant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'UI par-dessus l'API `/admin/api/tours*`. Nouvelle entrée
`/admin/didacticiels` dans `AdminLayout` (i18n `admin.nav.tours`), route
sous `RequireAdmin`+`AdminLayout` dans `App.tsx`.
`ToursPage.tsx` : liste des scénarios à gauche (nom, état activé/désactivé,
nombre d'étapes) ; à droite l'éditeur du scénario sélectionné —
- renommer, activer/désactiver ;
- éditer la liste d'étapes (route, cible via `<select>` alimenté par
`GET /tours/targets`, titre, contenu), réordonner (↑/↓), ajouter,
retirer ;
- « Enregistrer » → un seul `PATCH /:id` qui remplace tout (name/enabled +
étapes rénumérotées `order` = index), erreurs API affichées telles quelles ;
- « Supprimer le scénario » (confirmation `window.confirm`) ;
- « Nouveau scénario » → formulaire clé + nom → `POST /`.
`admin-client.ts` gagne `getTourScenarios` / `getTourTargets` /
`createTourScenario` / `updateTourScenario` / `deleteTourScenario`.
Tests Cypress (`admin-tours.cy.ts`, 4) : liste (activés + désactivés,
nombre d'étapes) ; édition nom + réordonnancement d'étapes → un seul PATCH
avec la liste complète réordonnée ; création → POST `{key,name}` ;
suppression après confirmation → DELETE. 4/4 en local ; `pnpm build` +
`pnpm lint` verts.
Closes#44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consomme le socle backend de #42/#44 (`GET /reference/tours`,
`GET/POST /profile/tours`). Deux tours : orientation générale (auto sur
`/`) et correction des métadonnées de recette (auto à l'ouverture du
premier popover de correction).
- Dépendance `driver.js` (~5 ko, MIT, zéro dépendance).
- `features/tour/tour-engine.ts` : wrapper vanilla autour de `driver.js` —
gère ce que la lib ne fait pas seule : la navigation React Router entre
étapes situées sur des routes différentes, et un `onFinish` unique
(`"completed"` seulement si on atteint la fin, `"skipped"` pour la croix
/ Échap / clic sur l'overlay).
- `features/tour/TourContext.tsx` : `TourProvider` (charge scénarios +
progression, keyed sur `user?.id` comme `ThemeContext`) et `useTour()`
(`startTour(key)` / `isTourCompleted(key)` / `tours` / `isReady`).
`isReady` est **calculé au render** contre l'id utilisateur chargé — pas
stocké — pour qu'un changement de `user` le repasse à `false`
immédiatement, avant qu'un effet enfant (l'auto-déclenchement) ne tourne
sur des données de complétion périmées.
- `main.tsx` : `<TourProvider>` sous `ThemeProvider`.
- Attributs `data-tour-*` : liens de nav (`AppLayout`), grille / bouton `+`
du 1er créneau / bouton Cuisiner (`PlanningPage`), page liste de courses
(`ShoppingListPage`), description d'étape + section technique du popover
(`StepDescription` / `TechStepCorrectionPopover`).
- Déclenchement auto : tour `general` sur `/` (`PlanningPage`, ref-gardé),
tour `techStepMetadata` à `activeCorrection` non-null (`StepDescription`).
- Section « Revoir un tutoriel » dans `/parametres/preferences-utilisateur`
— un `map` sur `tours`, un bouton « Revoir » par scénario.
- `tour.scss` re-skinne le popover `driver.js` sur les tokens du thème.
- i18n `tour.*` (chrome uniquement : boutons du popover, section « Revoir » ;
le contenu des étapes vient de la base).
Tests Cypress (`tour.feature`, 5 scénarios) : auto-démarrage + élément
surligné pour un nouveau venu ; « Terminé » → complétion enregistrée
`skipped:false` ; croix → `skipped:true` ; un tour déjà vu ne se relance
pas ; « Revoir » le relance depuis les paramètres. Vérifié 5/5 en local.
`pnpm build` + `pnpm lint` verts.
Refs #42
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>