Commit graph

423 commits

Author SHA1 Message Date
7d1ee934ca ci(release): build et push des images Docker sur le registre Forgejo
All checks were successful
CI / lint (push) Successful in 2m48s
CI / build (push) Successful in 2m55s
CI / intent-service-test (push) Successful in 4m57s
CI / e2e (push) Successful in 9m28s
CI / test (push) Successful in 19m10s
CI / build-android (push) Has been skipped
Le job `sanity-build` (build de `apps/api/Dockerfile` sans push) devient
`build-and-push-images` : `docker login` sur le registre Forgejo intégré
via le token Actions du run (`permissions: packages: write`), puis build +
push des 3 images `batchcooking-{app,intent-service,llm-worker}` taguées
`vX.Y.Z` et `latest`. Un job séquentiel plutôt qu'une matrix — 3 builds
concurrents saturent le daemon Docker du runner unique (cf. mémoire
`ci-forgejo-runner-constraints`) sans gain réel pour une release.

`docker-compose.yml` référence maintenant ces images
(`image: git.dev.kyuno.fr/kyuno/batchcooking-*:${IMAGE_TAG:-latest}`) avec
le bloc `build:` gardé en repli local — Portainer peut tirer les images
pré-construites ou continuer à builder depuis Git. `.env.example` documente
`IMAGE_TAG` ; le README (section registre + Release) est réécrit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:03:26 +02:00
5a915a6cee Merge pull request 'feat(admin): espace « Utilisateurs » pour créer des comptes' (#144) from feat/admin-user-accounts into main
Some checks failed
CI / lint (push) Successful in 3m13s
CI / build (push) Successful in 3m15s
CI / intent-service-test (push) Successful in 5m18s
CI / e2e (push) Successful in 11m42s
CI / test (push) Failing after 17m38s
CI / build-android (push) Has been skipped
Reviewed-on: #144
2026-09-10 21:48:29 +02:00
9e847e15a1 feat(admin): page « Utilisateurs » pour créer des comptes
All checks were successful
CI / lint (push) Successful in 3m10s
CI / build (push) Successful in 3m16s
CI / intent-service-test (push) Successful in 5m35s
CI / e2e (push) Successful in 11m32s
CI / test (push) Successful in 19m46s
CI / build-android (push) Has been skipped
Nouvelle section `/admin/utilisateurs` (nav + route) : tableau des comptes
(date, nom, email, foyer) et un `Dialog` de création en deux temps —
formulaire prénom/nom/email, puis affichage unique du mot de passe
temporaire généré, avec bouton « Copier ». Validation cliente via
`adminCreateUserSchema`, erreurs API traduites (`EMAIL_ALREADY_IN_USE`
déjà mappé). Spec Cypress `admin-users.cy.ts` (API mockée).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 20:59:23 +02:00
0f8a640da2 feat(api): endpoints admin de gestion des comptes utilisateurs
`GET /admin/api/users` (liste, plus récent d'abord, plafonnée à 200) et
`POST /admin/api/users` (création à la main), tous deux derrière
`requireAdmin`. La création génère un mot de passe temporaire aléatoire
(`randomInt`, charset sans caractères ambigus, comme `generateInviteCode`)
renvoyé une seule fois ; le compte démarre sans foyer, comme une
inscription self-service. Évènement `admin.user_created` (fire-and-forget)
pour distinguer les comptes provisionnés à la main dans le dashboard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 20:59:15 +02:00
9123f6358e feat(shared): schéma et vues pour la création de comptes par un admin
`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>
2026-09-10 20:59:08 +02:00
d1430498e0 Merge pull request 'fix(admin): passe les variables FORGEJO_* au conteneur app' (#143) from fix/forgejo-env-passthrough into main
Some checks failed
CI / lint (push) Successful in 3m26s
CI / intent-service-test (push) Successful in 4m15s
CI / build (push) Successful in 3m45s
CI / e2e (push) Successful in 10m2s
CI / test (push) Successful in 24m26s
CI / build-android (push) Failing after 2m19s
Reviewed-on: #143
2026-09-09 23:09:11 +02:00
429aa06ca6 fix(admin): passe les variables FORGEJO_* au conteneur app
Some checks failed
CI / lint (push) Successful in 2m56s
CI / build (push) Successful in 3m4s
CI / intent-service-test (push) Successful in 4m52s
CI / e2e (push) Successful in 11m1s
CI / test (push) Failing after 18m29s
CI / build-android (push) Has been skipped
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>
2026-09-09 22:58:13 +02:00
26bd48bf4b Merge pull request 'chore(release): prépare la v0.2.3' (#142) from chore/release-0.2.3 into main
All checks were successful
CI / lint (push) Successful in 4m21s
CI / build (push) Successful in 3m37s
CI / intent-service-test (push) Successful in 6m58s
Release / sanity-build (push) Successful in 10m28s
Release / build-android-apk (push) Successful in 9m32s
Release / forgejo-release (push) Successful in 49s
Release / notify-portainer (push) Successful in 1s
CI / test (push) Successful in 31m32s
CI / e2e (push) Successful in 7m15s
CI / build-android (push) Has been skipped
2026-09-09 21:45:18 +02:00
a02d289c92 chore(release): prépare la v0.2.3
All checks were successful
CI / lint (push) Successful in 3m28s
CI / build (push) Successful in 3m59s
CI / intent-service-test (push) Successful in 5m0s
CI / e2e (push) Successful in 10m45s
CI / test (push) Successful in 19m44s
CI / build-android (push) Has been skipped
`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>
2026-09-09 21:24:35 +02:00
09904cffc1 Merge pull request 'feat(auth): bouton afficher / masquer le mot de passe (#134)' (#141) from feat/login-password-toggle into main
All checks were successful
CI / lint (push) Successful in 3m20s
CI / build (push) Successful in 3m23s
CI / intent-service-test (push) Successful in 5m13s
CI / e2e (push) Successful in 12m8s
CI / test (push) Successful in 20m48s
CI / build-android (push) Successful in 5m54s
2026-09-09 20:44:32 +02:00
5a6ddfc287 chore(ci): relance — dépôt apt Google Chrome de nouveau cohérent
All checks were successful
CI / lint (push) Successful in 2m38s
CI / build (push) Successful in 2m44s
CI / intent-service-test (push) Successful in 6m0s
CI / e2e (push) Successful in 11m56s
CI / test (push) Successful in 20m1s
CI / build-android (push) Has been skipped
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>
2026-09-09 20:03:02 +02:00
16ceeda031 chore(ci): relance après un flake d'infra (apt « Hash Sum mismatch » Google Chrome)
Some checks failed
CI / test (push) Failing after 26s
CI / e2e (push) Failing after 1m13s
CI / lint (push) Successful in 1m59s
CI / build (push) Successful in 2m11s
CI / intent-service-test (push) Successful in 4m5s
CI / build-android (push) Has been skipped
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>
2026-09-09 19:39:01 +02:00
24bd2239fc feat(auth): bouton afficher / masquer le mot de passe (#134)
Some checks failed
CI / test (push) Failing after 1m0s
CI / e2e (push) Failing after 1m43s
CI / lint (push) Successful in 2m19s
CI / build (push) Successful in 2m28s
CI / intent-service-test (push) Successful in 4m22s
CI / build-android (push) Has been skipped
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>
2026-09-09 19:33:29 +02:00
0c4e8d089c Merge pull request 'feat(recipes): barre de recherche commune à tous les onglets de sources (#133)' (#140) from feat/common-source-search into main
Some checks failed
CI / build (push) Successful in 3m1s
CI / lint (push) Successful in 3m4s
CI / intent-service-test (push) Successful in 4m51s
CI / e2e (push) Successful in 10m50s
CI / test (push) Failing after 17m11s
CI / build-android (push) Has been skipped
2026-09-09 19:18:38 +02:00
51b7b8eec3 feat(recipes): barre de recherche commune à tous les onglets de sources (#133)
All checks were successful
CI / lint (push) Successful in 3m39s
CI / build (push) Successful in 3m32s
CI / intent-service-test (push) Successful in 5m51s
CI / e2e (push) Successful in 8m32s
CI / test (push) Successful in 22m26s
CI / build-android (push) Has been skipped
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>
2026-09-09 17:10:09 +02:00
36d26c2739 Merge pull request 'feat(onboarding): réordonne le wizard en compte → préférences → foyer → sources (#129)' (#139) from feat/onboarding-wizard-order into main
Some checks failed
CI / lint (push) Successful in 3m8s
CI / build (push) Successful in 3m22s
CI / intent-service-test (push) Successful in 5m42s
CI / e2e (push) Successful in 10m40s
CI / test (push) Failing after 16m36s
CI / build-android (push) Has been skipped
Closes #129.
2026-09-09 16:59:49 +02:00
2a3fb0434a feat(onboarding): réordonne le wizard en compte → préférences → foyer → sources (#129)
All checks were successful
CI / lint (push) Successful in 3m54s
CI / build (push) Successful in 4m10s
CI / intent-service-test (push) Successful in 5m15s
CI / e2e (push) Successful in 8m32s
CI / test (push) Successful in 23m8s
CI / build-android (push) Has been skipped
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>
2026-09-09 16:34:49 +02:00
61079b3a2a Merge pull request 'feat(recipes): signaler une technique absente du catalogue (#130)' (#138) from feat/missing-tech-steps into main
Some checks failed
CI / lint (push) Successful in 3m12s
CI / build (push) Successful in 4m10s
CI / intent-service-test (push) Successful in 4m45s
CI / e2e (push) Successful in 13m30s
CI / test (push) Failing after 16m32s
CI / build-android (push) Has been skipped
Closes #130.
2026-09-09 16:25:27 +02:00
380f22d988 test: aligne les assertions restantes sur TechStepView { isPlaceholder, displayName } (#130)
All checks were successful
CI / lint (push) Successful in 3m4s
CI / build (push) Successful in 3m8s
CI / intent-service-test (push) Successful in 5m0s
CI / e2e (push) Successful in 10m58s
CI / test (push) Successful in 19m18s
CI / build-android (push) Has been skipped
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>
2026-09-09 16:04:57 +02:00
b49ef5bb40 feat(recipes): signaler une technique absente du catalogue (#130)
Some checks failed
CI / lint (push) Successful in 1m37s
CI / intent-service-test (push) Successful in 5m11s
CI / build (push) Successful in 1m9s
CI / e2e (push) Failing after 7m13s
CI / test (push) Failing after 18m46s
CI / build-android (push) Has been skipped
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>
2026-09-09 15:40:35 +02:00
81b4a149ff Merge pull request 'feat(recipes): régimes calculés depuis les ingrédients (#131)' (#137) from feat/auto-recipe-diets into main
All checks were successful
CI / lint (push) Successful in 3m2s
CI / build (push) Successful in 3m7s
CI / intent-service-test (push) Successful in 4m59s
CI / e2e (push) Successful in 10m47s
CI / test (push) Successful in 18m59s
CI / build-android (push) Successful in 7m27s
2026-09-09 15:20:14 +02:00
e8596a55bc feat(recipes): régimes d'une recette calculés depuis ses ingrédients (#131)
All checks were successful
CI / lint (push) Successful in 2m58s
CI / build (push) Successful in 2m58s
CI / intent-service-test (push) Successful in 4m57s
CI / e2e (push) Successful in 10m45s
CI / test (push) Successful in 18m56s
CI / build-android (push) Has been skipped
`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>
2026-09-09 15:00:52 +02:00
019e3fb5e5 Merge pull request 'feat(recipes): ingrédients d'assaisonnement « selon le goût » (#128)' (#136) from feat/seasoning-ingredients into main
All checks were successful
CI / lint (push) Successful in 3m4s
CI / build (push) Successful in 3m6s
CI / intent-service-test (push) Successful in 5m9s
CI / e2e (push) Successful in 10m33s
CI / test (push) Successful in 19m6s
CI / build-android (push) Successful in 5m12s
2026-09-09 14:35:24 +02:00
ba030ec18c feat(recipes): ingrédients d'assaisonnement « selon le goût » sans quantité (#128)
All checks were successful
CI / lint (push) Successful in 3m5s
CI / build (push) Successful in 3m15s
CI / intent-service-test (push) Successful in 5m12s
CI / e2e (push) Successful in 11m1s
CI / test (push) Successful in 19m13s
CI / build-android (push) Has been skipped
`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>
2026-09-09 14:15:28 +02:00
c3560eccec Merge pull request 'feat(recipes): barre Enregistrer/Annuler collée en pied du formulaire (#132)' (#135) from feat/recipe-form-sticky-actions into main
Some checks failed
CI / lint (push) Successful in 3m9s
CI / build (push) Successful in 3m15s
CI / intent-service-test (push) Successful in 5m59s
CI / e2e (push) Successful in 11m7s
CI / test (push) Failing after 17m47s
CI / build-android (push) Has been skipped
2026-09-09 14:00:21 +02:00
8ae06f10c9 feat(recipes): barre Enregistrer/Annuler collée en pied du formulaire
All checks were successful
CI / lint (push) Successful in 56s
CI / build (push) Successful in 1m29s
CI / intent-service-test (push) Successful in 3m53s
CI / e2e (push) Successful in 8m26s
CI / test (push) Successful in 17m27s
CI / build-android (push) Has been skipped
`.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>
2026-09-09 13:42:14 +02:00
3dfed7e2e3 Merge pull request 'chore(release): prépare la v0.2.2' (#127) from chore/release-0.2.2 into main
Some checks failed
CI / lint (push) Successful in 4m49s
CI / build (push) Successful in 4m32s
CI / intent-service-test (push) Successful in 8m56s
Release / sanity-build (push) Successful in 11m8s
CI / e2e (push) Successful in 16m18s
Release / build-android-apk (push) Successful in 9m53s
Release / forgejo-release (push) Successful in 1m33s
Release / notify-portainer (push) Successful in 2s
CI / test (push) Successful in 47m42s
CI / build-android (push) Failing after 11m25s
2026-09-08 22:59:45 +02:00
46069727ad chore(release): prépare la v0.2.2
All checks were successful
CI / build (push) Successful in 2m38s
CI / lint (push) Successful in 2m38s
CI / intent-service-test (push) Successful in 4m19s
CI / e2e (push) Successful in 10m12s
CI / test (push) Successful in 18m11s
CI / build-android (push) Has been skipped
- `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>
2026-09-08 22:40:56 +02:00
b20bcca07e Merge pull request 'fix(sources): Manger Bouger sans filtre déroule tout le catalogue (#78)' (#126) from fix/manger-bouger-empty-filter into main
All checks were successful
CI / lint (push) Successful in 1m12s
CI / build (push) Successful in 1m18s
CI / intent-service-test (push) Successful in 4m6s
CI / e2e (push) Successful in 8m22s
CI / test (push) Successful in 17m39s
CI / build-android (push) Successful in 5m18s
2026-09-08 21:14:36 +02:00
7d20404b6f fix(sources): Manger Bouger sans filtre déroule tout le catalogue (#78)
All checks were successful
CI / lint (push) Successful in 2m48s
CI / build (push) Successful in 2m53s
CI / intent-service-test (push) Successful in 4m29s
CI / e2e (push) Successful in 9m49s
CI / test (push) Successful in 18m54s
CI / build-android (push) Has been skipped
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>
2026-09-08 20:30:25 +02:00
46c23c24ab Merge pull request 'feat(admin): crée un ticket Forgejo depuis un message de contact (#81)' (#125) from feat/forgejo-issue-from-contact-message into main
All checks were successful
CI / lint (push) Successful in 4m32s
CI / build (push) Successful in 4m40s
CI / intent-service-test (push) Successful in 4m43s
CI / e2e (push) Successful in 10m33s
CI / test (push) Successful in 18m53s
CI / build-android (push) Successful in 5m8s
2026-09-08 20:30:08 +02:00
30f975d29a feat(admin): crée un ticket Forgejo depuis un message de contact (#81)
All checks were successful
CI / lint (push) Successful in 3m16s
CI / build (push) Successful in 3m29s
CI / intent-service-test (push) Successful in 4m39s
CI / e2e (push) Successful in 10m24s
CI / test (push) Successful in 18m48s
CI / build-android (push) Has been skipped
Depuis /admin/messages, un admin peut créer une issue Forgejo à partir
d'un message utilisateur : bouton « Créer un ticket » → modale pré-remplie
éditable → l'issue est créée via l'API Forgejo et le lien (#N) est
mémorisé sur le message. Première intégration Forgejo d'apps/api.

- Modèle : `forgejoIssueNumber` / `forgejoIssueUrl` sur `ContactMessage`
  (migration `20260908130000_contact_message_forgejo_link`, 2 colonnes
  nullables, aucun backfill).
- Config : `FORGEJO_BASE_URL` / `FORGEJO_TOKEN` / `FORGEJO_REPO`, toutes
  `.optional()` — **fail-closed** (même posture que `ADMIN_JWT_SECRET`) :
  bouton masqué + endpoint `503` tant que les trois ne sont pas là.
  Documentées dans `.env.example` ; valeurs de test dans `ci.yml`.
- `ForgejoService` (`lib/forgejo/forgejo.service.ts`, singleton `forgejo`) :
  `isConfigured()` + `createIssue({ title, body })` → `POST .../issues`,
  en-tête `Authorization: token <pat>`, lit `{ number, html_url }` ;
  erreur réseau / non-2xx / JSON invalide ⇒ `HttpError 502
  FORGEJO_REQUEST_FAILED`.
- `admin-contact` : `POST /admin/api/contact/:id/forgejo-issue`
  (`createForgejoIssueFromMessage` — ordre 503 → 404 → 409 → Forgejo →
  écriture BDD ; un échec Forgejo ne touche pas la BDD) et
  `GET /admin/api/contact/forgejo/status`.
- Contrat partagé : `createForgejoIssueSchema` (title 1–255, body 1–60000),
  `ContactMessageAdminView` étendu, `ForgejoIntegrationStatus`, nouveaux
  `ErrorCode` (`FORGEJO_NOT_CONFIGURED`=5001, `FORGEJO_REQUEST_FAILED`=5002,
  `CONTACT_MESSAGE_ALREADY_LINKED`=4026, `CONTACT_MESSAGE_NOT_FOUND`=4055)
  + labels i18n.
- Front : `MessagesPage` — sonde `forgejo/status` au montage, colonne
  « Ticket » (lien `#N` si lié, bouton sinon, « non configurée » sinon),
  `CreateForgejoIssueModal` + helpers purs `messages.ts`
  (`buildIssueTitle` / `buildIssueBody`).
- Tests : `forgejo.service.test.ts` (stub `globalThis.fetch`),
  `admin-contact.test.ts` étendu (nominal / 503 / 404 / 409 / 502 +
  ligne intacte / 400), `admin-messages.cy.ts` étendu (modale pré-remplie,
  lien après création, masquage si non configuré).
- Docs : `backend-architecture.md`, `batch-cooking-modele.md`.

Closes #81.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 20:10:54 +02:00
71758e6d25 Merge pull request 'feat(admin): consultation des messages de contact (#80)' (#124) from feat/admin-contact-messages into main
Some checks failed
CI / lint (push) Successful in 2m30s
CI / build (push) Successful in 2m34s
CI / intent-service-test (push) Successful in 5m31s
Release / sanity-build (push) Successful in 6m39s
Release / build-android-apk (push) Successful in 8m47s
CI / e2e (push) Successful in 12m20s
CI / test (push) Successful in 20m26s
CI / build-android (push) Has been skipped
Release / forgejo-release (push) Failing after 33s
Release / notify-portainer (push) Has been skipped
2026-09-08 09:51:45 +02:00
0c8cda7f3e feat(admin): consultation des messages de contact (#80)
All checks were successful
CI / lint (push) Successful in 4m17s
CI / build (push) Successful in 5m11s
CI / intent-service-test (push) Successful in 5m42s
CI / e2e (push) Successful in 11m26s
CI / test (push) Successful in 29m58s
CI / build-android (push) Has been skipped
PR 2/2 de l'issue #80 — la vue admin en lecture seule des messages
remontés via le formulaire de contact (PR 1 = #123).

- Contrat partagé : `listContactMessagesQuerySchema` (filtre `type`
  optionnel) + `types/contact.ts` `ContactMessageAdminView` (`author` =
  `{ id, firstName, lastName, email } | null`, `null` = compte supprimé).
- API : `admin-contact.{routes,service}.ts` — `GET /admin/api/contact?type=`
  (`requireAdmin`), tri `createdAt desc`, plafond `CONTACT_MESSAGES_PAGE_SIZE
  = 200` (même esprit que `CORRECTIONS_PAGE_SIZE`). Branché dans
  `admin.routes.ts` (`adminRouter.use(/contact, …)`). Aucune route
  d'écriture.
- Front : page `/admin/messages` (`MessagesPage` + `.scss`) — tableau
  lecture seule (date, expéditeur ou « Compte supprimé », badge de type,
  message, page, user-agent tronqué + titre au survol), un filtre par type.
  Entrée de nav « Messages » (icône `Inbox`) dans `AdminLayout` +
  `adminApiClient.getContactMessages` + i18n `admin.nav.messages` /
  `admin.messages.*`.
- Tests : `apps/api/test/admin-contact.test.ts` (Mocha — 401 sans session
  admin, tri récent d'abord + `author` résolu, filtre `type`, type inconnu →
  400, plafond 200, `author: null` après suppression du compte),
  `apps/web/cypress/e2e/admin-messages.cy.ts` (rendu lecture seule, filtre
  par type via le query param).
- Docs : `backend-architecture.md` (section `admin` + event
  `contact.submitted` dans la liste).

Closes #80.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 09:17:45 +02:00
9b6cee9950 Merge pull request 'feat(contact): modèle + POST /contact + formulaire utilisateur (#80)' (#123) from feat/contact-message-model-and-form into main
Some checks failed
CI / lint (push) Successful in 4m7s
CI / build (push) Successful in 5m5s
CI / intent-service-test (push) Successful in 5m14s
CI / e2e (push) Successful in 12m42s
CI / test (push) Failing after 19m31s
CI / build-android (push) Has been skipped
2026-09-08 09:17:19 +02:00
b873b70946 chore: relance la CI (#123)
All checks were successful
CI / lint (push) Successful in 3m40s
CI / build (push) Successful in 3m43s
CI / intent-service-test (push) Successful in 4m35s
CI / e2e (push) Successful in 9m39s
CI / test (push) Successful in 18m14s
CI / build-android (push) Has been skipped
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>
2026-09-08 08:57:37 +02:00
a252640fe6 feat(contact): modèle + POST /contact + formulaire utilisateur (#80)
Some checks failed
CI / build (push) Successful in 2m22s
CI / lint (push) Successful in 2m26s
CI / intent-service-test (push) Successful in 4m8s
CI / e2e (push) Successful in 12m40s
CI / test (push) Failing after 15m4s
CI / build-android (push) Has been skipped
Formulaire in-app de remontée de bug / suggestion pour les utilisateurs
connectés (PR 1/2 de l'issue #80).

- Modèle Prisma `ContactMessage` (+ relation inverse sur `UserProfile`) et
  migration `20260908120000_contact_message` — table neuve, aucun backfill.
  `authorId` nullable + `onDelete: SetNull` : un signalement survit à la
  suppression de son compte.
- Contrat partagé `schemas/contact.ts` : `CONTACT_MESSAGE_TYPES`,
  `submitContactMessageSchema` (type enum, message 10-5000 trim, pageUrl /
  appVersion optionnels — jamais le user-agent).
- Module API `contact` : `POST /contact` (`requireAuth`, monté `/contact`),
  `204` sans corps. Le user-agent est lu de l'en-tête de requête, jamais du
  corps. Émet `analytics.recordEvent(contact.submitted, …)`.
- Page `/parametres/contact` (`ContactPage` + `contact.ts` + `.scss`) :
  select de type, textarea, validation cliente via le schéma partagé, états
  succès / erreur (`ErrorMessageService`, jamais `err.message`). Entrée de
  nav « Nous contacter » dans `SETTINGS_ITEMS` + clé i18n + icône.
  `apiClient.submitContactMessage(input)` ajoute `pageUrl` / `appVersion`.
- Tests : `apps/api/test/contact.test.ts` (Mocha — insertion, en-tête UA,
  champs nullables, message trop court / trop long, type inconnu),
  `contact.feature` + steps (Cypress e2e — envoi + confirmation + contexte),
  `contact-page.cy.ts` (rendu + garde `RequireAuth` + label d'erreur).
- Docs : `batch-cooking-modele.md`, `backend-architecture.md`,
  `frontend-architecture.md`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 08:26:47 +02:00
206ab6fa0c Merge pull request 'fix(mobile): retire les doubles tirets des commentaires du values-night XML' (#122) from fix/android-values-night-xml-comment into main
All checks were successful
CI / lint (push) Successful in 2m57s
CI / build (push) Successful in 3m19s
CI / intent-service-test (push) Successful in 5m15s
CI / e2e (push) Successful in 10m0s
CI / test (push) Successful in 18m12s
CI / build-android (push) Successful in 5m8s
2026-09-08 07:51:25 +02:00
e9040f93f4 fix(mobile): retire les doubles tirets des commentaires du values-night XML
All checks were successful
CI / lint (push) Successful in 2m48s
CI / build (push) Successful in 2m52s
CI / intent-service-test (push) Successful in 4m37s
CI / e2e (push) Successful in 10m8s
CI / test (push) Successful in 17m18s
CI / build-android (push) Has been skipped
`: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>
2026-09-08 07:33:32 +02:00
22119adf07 Merge pull request 'feat(shopping-list): coche les articles « dans le panier » (#109)' (#121) from feat/shopping-list-check-items into main
Some checks failed
CI / lint (push) Successful in 2m45s
CI / build (push) Successful in 3m0s
CI / intent-service-test (push) Successful in 4m45s
CI / e2e (push) Successful in 10m12s
CI / test (push) Successful in 17m31s
CI / build-android (push) Failing after 4m19s
2026-09-08 07:08:47 +02:00
592933b1ac feat(shopping-list): coche les articles « dans le panier » (#109)
All checks were successful
CI / lint (push) Successful in 2m18s
CI / build (push) Successful in 3m34s
CI / intent-service-test (push) Successful in 6m36s
CI / e2e (push) Successful in 10m22s
CI / test (push) Successful in 33m30s
CI / build-android (push) Has been skipped
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>
2026-09-08 06:31:37 +02:00
f68e8dbdc6 Merge pull request 'fix(mobile): la barre d'état Android suit le thème clair/sombre système (#104)' (#120) from fix/android-status-bar-dark-mode into main
Some checks failed
CI / lint (push) Successful in 4m5s
CI / build (push) Successful in 4m22s
CI / intent-service-test (push) Successful in 5m16s
CI / e2e (push) Successful in 14m5s
CI / test (push) Successful in 21m28s
CI / build-android (push) Failing after 5m18s
2026-09-08 06:31:24 +02:00
ef8dbe8388 fix(mobile): la barre d'état Android suit le thème clair/sombre système (#104)
All checks were successful
CI / lint (push) Successful in 2m37s
CI / build (push) Successful in 3m49s
CI / intent-service-test (push) Successful in 6m57s
CI / e2e (push) Successful in 11m18s
CI / test (push) Successful in 31m37s
CI / build-android (push) Has been skipped
`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>
2026-09-08 05:55:34 +02:00
f8b224dc8c Merge pull request 'fix(layout): masque l'onglet « Paramètres d'application » hors de l'app native (#107)' (#119) from fix/hide-app-settings-tab-on-web into main
Some checks failed
CI / build (push) Successful in 4m0s
CI / lint (push) Successful in 4m28s
CI / intent-service-test (push) Successful in 4m36s
CI / e2e (push) Failing after 12m25s
CI / test (push) Successful in 21m29s
CI / build-android (push) Has been skipped
2026-09-08 05:55:22 +02:00
8bcea74b56 fix(layout): masque l'onglet « Paramètres d'application » hors de l'app native (#107)
All checks were successful
CI / lint (push) Successful in 4m25s
CI / build (push) Successful in 2m18s
CI / intent-service-test (push) Successful in 5m41s
CI / e2e (push) Successful in 8m25s
CI / test (push) Successful in 30m0s
CI / build-android (push) Has been skipped
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>
2026-09-08 05:23:06 +02:00
1b96b891db Merge pull request 'fix(layout): barre de navigation mobile en icônes seules (#105)' (#118) from fix/mobile-bottom-nav-icons-only into main
Some checks failed
CI / lint (push) Successful in 2m10s
CI / build (push) Successful in 2m21s
CI / intent-service-test (push) Successful in 5m4s
CI / e2e (push) Successful in 11m30s
CI / test (push) Failing after 18m32s
CI / build-android (push) Has been skipped
2026-09-08 05:21:58 +02:00
e702e21296 fix(layout): barre de navigation mobile en icônes seules (#105)
All checks were successful
CI / lint (push) Successful in 4m37s
CI / build (push) Successful in 3m17s
CI / intent-service-test (push) Successful in 5m34s
CI / e2e (push) Successful in 8m59s
CI / test (push) Successful in 30m12s
CI / build-android (push) Has been skipped
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>
2026-09-08 04:48:56 +02:00
6828809081 Merge pull request 'fix(landing): masque « Ouvrir dans l'application » dans l'app native (#103)' (#117) from fix/landing-hide-open-app-in-native into main
Some checks failed
CI / lint (push) Successful in 2m8s
CI / build (push) Successful in 2m40s
CI / intent-service-test (push) Successful in 4m58s
CI / e2e (push) Successful in 11m23s
CI / test (push) Failing after 18m21s
CI / build-android (push) Has been skipped
2026-09-08 04:48:44 +02:00
126cd840f5 fix(landing): masque « Ouvrir dans l'application » dans l'app native (#103)
All checks were successful
CI / lint (push) Successful in 2m49s
CI / build (push) Successful in 4m5s
CI / intent-service-test (push) Successful in 6m9s
CI / e2e (push) Successful in 10m31s
CI / test (push) Successful in 29m20s
CI / build-android (push) Has been skipped
`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>
2026-09-08 04:15:00 +02:00
3d9f93e204 Merge pull request 'fix(auth): déconnexion forcée quand une requête 401 en cours de session (#108)' (#116) from fix/client-force-logout-on-401 into main
Some checks failed
CI / lint (push) Successful in 3m51s
CI / build (push) Successful in 4m49s
CI / intent-service-test (push) Successful in 5m10s
CI / e2e (push) Successful in 12m10s
CI / test (push) Failing after 19m12s
CI / build-android (push) Has been skipped
2026-09-08 04:14:48 +02:00