`adminCreateUserSchema` (prénom/nom/email, sans mot de passe — généré
côté serveur) + les vues `AdminUserListItemView` / `AdminUserCreatedView`
consommées par `POST`/`GET /admin/api/users`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'intégration Forgejo (« Créer un ticket » depuis un message de contact,
issue #81) restait invisible en prod Docker : `docker-compose.yml` ne
listait pas `FORGEJO_BASE_URL` / `FORGEJO_TOKEN` / `FORGEJO_REPO` dans le
bloc `environment:` du service `app`, et `**/.env` est `.dockerignore`d — le
process API ne voyait donc jamais ces variables, `ForgejoService.isConfigured()`
renvoyait `false` et le bouton restait masqué sur `/admin/messages`.
- `docker-compose.yml` : les 3 `FORGEJO_*` passées au conteneur via
`${VAR:-}`, même forme que `ADMIN_JWT_SECRET` / `INTERNAL_WORKER_SECRET`.
- `config/env.ts` : `blankAsUnset` sur les 3 champs — `${VAR:-}` arrive
comme `""` quand le `.env` du stack laisse la variable vide ; sans ça une
valeur vide ferait échouer `.url()` / la regex `owner/repo` et planter
`envSchema.parse()` au boot. Même parade que `COOKIE_SECURE`. `envSchema`
exporté pour le test.
- `.env.example` (racine) : documente les 3 variables au niveau compose.
- `apps/api/test/env.test.ts` : blanc = absent, + non-régression sur une
valeur non vide mais mal formée.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`apps/web/package.json` 0.2.2 → 0.2.3 — alimente `__APP_VERSION__` (pied de
sidebar + comparaison de `useAppUpdateCheck`), même convention que
`chore(release): prépare la v0.2.2`.
Le tag `v0.2.3` déclenchera `release.yml` : APK signé (versionName 0.2.3,
versionCode 203) + Release Forgejo.
Contenu depuis v0.2.2 : tickets #128-#134 (issue #3 « Améliorations
diverses ») — assaisonnements « selon le goût », wizard d'inscription
réordonné, techniques hors-catalogue, régimes calculés depuis les
ingrédients, barre d'actions collante du formulaire recette, recherche
commune aux sources, afficher/masquer le mot de passe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le hash InRelease vs Packages.gz du dépôt apt Google Chrome (upstream)
correspond à nouveau (233e56de…) après ~21 min de désynchro CDN qui a fait
échouer runs 277-279 sur `apt-get update` (jobs test + e2e).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Run 278 : `test` et `e2e` ont échoué en ~1 min sur `apt-get update` (dépôt
apt Google Chrome incohérent en amont), avant toute exécution de code.
`lint` + `build` verts. Vérif locale complète verte.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nouveau composant `components/ui/PasswordInput` : un `<input>` de mot de
passe avec un bouton œil (`lucide-react` Eye/EyeOff) qui bascule le `type`
entre "password" et "text". Visibilité en état local, toujours masquée au
montage, jamais persistée. Bouton `type="button"` (ne soumet pas le
formulaire) + `aria-label` traduit (`common.passwordInput.*`) +
`aria-pressed`.
Branché dans `LoginPage` et `SignupPage` à la place du `<input
type="password">` nu. La règle `.auth-card button` de `auth-form.scss` est
resserrée sur `button[type="submit"]` pour que le bouton œil n'hérite pas
du traitement plein-largeur du bouton de connexion.
Tests : `cypress/component/PasswordInput.cy.tsx` (7 cas, composant isolé) +
`cypress/e2e/auth-form.cy.ts` (mode layout, l'œil sur /login et /signup).
Closes#134
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Avant : chaque onglet de source rendait son propre champ recherche, avec
son propre state, et `RecipeSourcesPanel` est remonté (`key={sourceKey}`)
à chaque changement de source — la requête tapée était donc perdue en
passant d'une source à une autre.
Maintenant : un seul champ, dans l'en-tête de `RecipesPage`. `search` /
`debouncedSearch` (déjà présents pour les onglets réels) sont rendus en
permanence et `debouncedSearch` est passé en prop à `RecipeSourcesPanel`,
qui n'a plus ni `useState` de recherche, ni `<input>`, ni effet de
debounce internes — son effet de `browse` dépend de la prop. La requête
survit donc au changement de source (le `key` remonte toujours la liste +
le préchargement, mais le terme vit au-dessus), et elle est commune aux
onglets réels comme aux sources.
`RecipePickerDialog` : même chose — le champ recherche sort du bloc de
filtres `activeSourceKey === null` (les filtres ingrédients/régimes/foyer
y restent, ils ne concernent que les onglets en base) et `debouncedSearch`
est passé au panneau.
- SCSS : `.recipes-page__header--sources` supprimé (le panneau n'a plus
d'en-tête). Clé i18n morte `recipes.sources.searchPlaceholder` retirée.
- `specs/frontend-architecture.md` : recherche au niveau page.
Tests : `recipe-sources.feature` — nouveau scénario « la requête est
conservée en changeant d'onglet de source » (tape « poulet » sur TheMealDB,
bascule sur Marmiton, le champ garde « poulet » et Marmiton est interrogé
avec `query=poulet`). `pnpm lint` / `pnpm build` verts ; Cypress local :
recipe-sources 8/8, recipes 5/5, recipes.cy 12/12, planning 3/3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nouvel ordre : régime → allergènes (les deux « préférences ») → foyer →
sources. L'étape sources devient la dernière et n'est atteinte que si
l'étape foyer vient de **créer** un foyer — rejoindre ou passer termine le
wizard vers `/` (un foyer rejoint a déjà ses sources ; un foyer passé n'a
rien à quoi les rattacher).
- `onboarding-steps.ts` (nouveau) : séquence des étapes + `onboardingStepIndicator()`
en un seul endroit. Les compteurs « Étape N sur M » étaient codés en dur
page par page, ce qu'un réordonnancement cassait. régime/allergènes/foyer
sont en amont du choix « créer vs rejoindre/passer » donc affichent « sur 3 » ;
seule l'étape sources, jamais atteinte hors du chemin à 4 étapes, affiche « 4 sur 4 ».
- `OnboardingDietPage` → enchaîne sur `/onboarding/allergenes`.
- `OnboardingAllergensPage` → enchaîne sur `/onboarding/foyer` ; bouton « Continuer »
(plus « Terminer ») ; suppression du `getCurrentHouse()` + `totalSteps` dynamique,
devenus inutiles maintenant que cette étape précède le foyer.
- `OnboardingHouseholdPage` → `goToNextStep(createdHousehold)` : `/onboarding/sources`
seulement si créé, sinon `/`. `JoinHouseholdForm`/`SkipButton`/branche défensive
« déjà un foyer » terminent vers `/`.
- `OnboardingSourcesPage` → dernière étape : submit, court-circuit « aucune source »
et catch vont tous vers `/` ; bouton « Terminer ».
- `App.tsx` : blocs `<Route>` réordonnés + commentaire de doc du groupe.
- `specs/frontend-architecture.md` : ordre, diagramme mermaid, condition sources.
Tests : `onboarding.feature` réécrit — 4 parcours (walk-through complet avec
foyer créé + sources ; préférences + foyer passés ; foyer créé sans source
implémentée → étape sources auto-sautée ; foyer rejoint → pas de sources),
compteurs vérifiés sur chaque chemin. `pnpm lint` / `pnpm build` verts ;
Cypress local : onboarding 4/4, régression auth/preferences/household-settings 16/16.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux specs n'avaient pas été mises à jour quand `sources.service` s'est mis
à renvoyer un `techStep` complet et que le titre du catalogue admin est
passé de « Ingrédients hors-catalogue » à « Éléments hors-catalogue » :
- `sources.test.ts` : les deux `deep.equal({ id, key })` sur le techStep
d'une étape de preview attendent maintenant aussi `isPlaceholder: false`
et `displayName: null`.
- `admin-layout.cy.ts` : l'assertion du `h1` de /admin/catalogue suit le
nouveau libellé.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Calque le pattern « ingrédient hors-catalogue » sur `TechStep` : cinq
colonnes additives (`isPlaceholder`/`displayName`/`createdById`/`createdAt`/
`reviewedAt`), migration purement additive, FK `createdById → user_profiles`
en `SET NULL`.
Correction d'étape : `submitTechStepCorrectionSchema` accepte
`correctedTechStepName` (mutuellement exclusif de `correctedTechStepId`) ;
`submitTechStepCorrection` crée une `TechStep` placeholder dans la
transaction et poursuit comme si son id avait été fourni. La correction
s'applique immédiatement (`source: manual`) ; nouvel event analytics
`tech_step.placeholder_proposed`.
`TechStepView` gagne `isPlaceholder`/`displayName` ; `GET
/reference/tech-steps` exclut les placeholders. Nouveau helper web
`techStepLabel` (`displayName ?? t(catalog.techSteps.<key>)`), utilisé par
`StepDescription` et le popover. Le popover a une échappatoire « Ma
technique n'est pas dans la liste » (champ libre).
Admin : `admin-tech-step-catalog.service.ts` + routes
`GET/PATCH /admin/api/catalog/tech-step-placeholders` (liste plate — les
propositions de technique sont rares) ; `CatalogPage` gagne un sélecteur
Ingrédients / Techniques.
Tests : Mocha (proposition placeholder, rejet id+nom, exclusion catalogue,
liste + mark-reviewed admin) + Cypress (composant popover, e2e admin
catalogue). Assertions `.techStep` existantes mises à jour pour les 2
nouveaux champs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`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>
Le parcours « tout voir » (filtre texte vide) de la source Manger Bouger
s'arrêtait après la 1re page (9 recettes) sur le serveur déployé.
Cause : `list()` ajoutait toujours un `&query=` final, même vide. Sur le
serveur déployé, `…/recettes?diet=ALL&page=N&query=` renvoyait la page 1
pour tout `N`, et le garde-fou anti-boucle de #38 (« 0 nouvel externalId
⇒ fin de liste ») coupait le défilement. La requête catalogue nue
`?diet=ALL&page=N` pagine correctement.
- `manger-bouger.ts` `list()` : n'ajoute `&query=` que si le terme de
recherche est non vide (après `trim`), comme les adaptateurs voisins
(marmiton / quitoque / ohmonbento). Le chemin filtré ne change pas.
- Commentaires `LIST_URL` / `hasMorePages` corrigés : la pagination
« vérifiée en direct » ne valait que pour le chemin avec `query` texte.
- Tests `manger-bouger.test.ts` : `list({})` construit une URL sans
paramètre `query` ; un `query` blanc = pas de filtre ; le catalogue nu
pagine quand même (`nextCursor`) ; `list({ query: salade })` ajoute
toujours `&query=salade` (non-régression).
Closes#78.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le job `test` a échoué sur `a252640` par contention Postgres avec la CI de
#124 lancée en parallèle (15 timeouts de hook « before each » + quelques
PrismaClientKnownRequestError, tous hors périmètre — les 7 tests `Contact`
passent). #124 (surensemble de cette branche) est vert. Relance isolée.
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>