226 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| a02d289c92 |
chore(release): prépare la v0.2.3
All checks were successful
`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> |
|||
| 24bd2239fc |
feat(auth): bouton afficher / masquer le mot de passe (#134)
Some checks failed
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> |
|||
| 51b7b8eec3 |
feat(recipes): barre de recherche commune à tous les onglets de sources (#133)
All checks were successful
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>
|
|||
| 2a3fb0434a |
feat(onboarding): réordonne le wizard en compte → préférences → foyer → sources (#129)
All checks were successful
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> |
|||
| 380f22d988 |
test: aligne les assertions restantes sur TechStepView { isPlaceholder, displayName } (#130)
All checks were successful
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>
|
|||
| b49ef5bb40 |
feat(recipes): signaler une technique absente du catalogue (#130)
Some checks failed
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> |
|||
| e8596a55bc |
feat(recipes): régimes d'une recette calculés depuis ses ingrédients (#131)
All checks were successful
`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> |
|||
| ba030ec18c |
feat(recipes): ingrédients d'assaisonnement « selon le goût » sans quantité (#128)
All checks were successful
`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> |
|||
| 8ae06f10c9 |
feat(recipes): barre Enregistrer/Annuler collée en pied du formulaire
All checks were successful
`.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> |
|||
| 46069727ad |
chore(release): prépare la v0.2.2
All checks were successful
- `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> |
|||
| 7d20404b6f |
fix(sources): Manger Bouger sans filtre déroule tout le catalogue (#78)
All checks were successful
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> |
|||
| 30f975d29a |
feat(admin): crée un ticket Forgejo depuis un message de contact (#81)
All checks were successful
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>
|
|||
| 0c8cda7f3e |
feat(admin): consultation des messages de contact (#80)
All checks were successful
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> |
|||
| a252640fe6 |
feat(contact): modèle + POST /contact + formulaire utilisateur (#80)
Some checks failed
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> |
|||
| e9040f93f4 |
fix(mobile): retire les doubles tirets des commentaires du values-night XML
All checks were successful
`: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> |
|||
| 592933b1ac |
feat(shopping-list): coche les articles « dans le panier » (#109)
All checks were successful
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>
|
|||
| ef8dbe8388 |
fix(mobile): la barre d'état Android suit le thème clair/sombre système (#104)
All checks were successful
`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> |
|||
| 8bcea74b56 |
fix(layout): masque l'onglet « Paramètres d'application » hors de l'app native (#107)
All checks were successful
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> |
|||
| e702e21296 |
fix(layout): barre de navigation mobile en icônes seules (#105)
All checks were successful
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> |
|||
| 126cd840f5 |
fix(landing): masque « Ouvrir dans l'application » dans l'app native (#103)
All checks were successful
`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> |
|||
| a54cb85027 |
fix(auth): déconnexion forcée quand une requête 401 en cours de session (#108)
All checks were successful
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>
|
|||
| 3df24fe71c |
fix(mobile): lance la vérification de mise à jour à chaque ouverture (#106)
All checks were successful
`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> |
|||
| b8cbf2d777 |
fix(test): fige l'horloge dans le spec vue-mobile-jour de layout.cy.ts
All checks were successful
`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> |
|||
| c4c2c2ae39 |
fix(planning): la grille remplit l'écran, une case ne grandit plus (#110)
All checks were successful
`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>
|
|||
| 642b37ebec |
fix(planning): affiche les recettes du picker sur mobile (#111)
All checks were successful
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> |
|||
| e62430ee5f |
chore(web): version 0.2.1
Some checks failed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 4f553147ed |
Merge pull request '[UI mobile] La landing page met en avant le téléchargement sur mobile' (#101) from feat/mobile-landing-download into main
Some checks failed
|
|||
| c82f0969a8 |
Merge pull request '[UI mobile] Onglet "Paramètres d'application"' (#100) from feat/mobile-app-settings into main
Some checks are pending
|
|||
| 8d1c4a7f0b |
Merge pull request '[UI mobile] Liste de courses et catalogue de recettes : renforcement mobile' (#99) from feat/mobile-shopping-recipes into main
Some checks are pending
|
|||
| f0f5e9f344 |
Merge pull request '[UI mobile] Session de cuisine : passe mobile' (#98) from feat/mobile-cooking-session into main
Some checks are pending
|
|||
| 615e320be1 |
feat(landing): met en avant le téléchargement/ouverture de l'appli sur mobile (#85)
Some checks failed
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> |
|||
| e1589e63d0 |
feat(settings): onglet "Paramètres d'application" (#86)
Some checks failed
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> |
|||
| 7d1a69cf12 |
fix(mobile): renforce le catalogue de recettes et la liste de courses sur mobile (#90)
Some checks failed
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> |
|||
| 2b5ae6722a |
fix(cooking-session): premier passage responsive dédié à l'écran de cuisine (#89)
Some checks failed
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> |
|||
| 964269d3f3 |
feat(planning): vue mobile dédiée "un jour à la fois" pour la grille (#88)
Some checks failed
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> |
|||
| 23bc19b482 |
Merge pull request 'test(cypress): convention de viewports téléphone/tablette' (#94) from feat/mobile-cypress-viewports into main
Some checks failed
|
|||
| 0ff94c61ca |
Merge pull request 'fix(planning): popovers flottants sûrs sur mobile' (#95) from feat/mobile-popovers-safe into main
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 3m26s
CI / build (push) Successful in 3m30s
CI / intent-service-test (push) Successful in 5m31s
CI / e2e (push) Successful in 9m45s
CI / test (push) Has been cancelled
|
|||
| 0447c68b69 |
merge: rapatrie le correctif de serialisation CI (#96)
All checks were successful
|
|||
| a3701acae4 |
fix(planning): popovers flottants surs sur mobile
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 3m18s
CI / intent-service-test (push) Successful in 4m8s
CI / build (push) Successful in 3m42s
CI / test (push) Failing after 10m11s
CI / e2e (push) Has been cancelled
Ticket #87 du chantier UI mobile & tablette. LeftoversBasket et WeekNavigator's CalendarPopover etaient tous les deux des popovers a largeur fixe (18-20rem), ancres right: 0, sans aucune gestion de bord d'ecran - sur un telephone etroit ils pouvaient deborder hors du viewport. Les deux sont declenches depuis l'en-tete du planning, un ecran central de l'usage quotidien - tres probablement la cause concrete derriere le retour terrain "je n'ai pas acces a tous les boutons" sur mobile qui a lance ce chantier. Les deux basculent desormais sur BottomSheet (ticket #91) sous le breakpoint phone via useIsMobileViewport() - meme contenu, juste une autre enveloppe (le popover flottant + son ecouteur de clic exterieur manuel restent inchanges sur desktop). Le clic-exterieur devient un no-op propre sur mobile plutot qu'un ecouteur qui ne fait jamais rien (le ref ne s'attache a rien dans la branche BottomSheet). Nouvelle cle i18n common.calendar.title ("Choisir une semaine") pour le titre de la feuille - pas de raccourci en decoupant une autre cle interpolee. Verifie en local : tsc -b --force, biome check, build de prod, suite Cypress e2e complete (142/142, 2 nouveaux tests phone dans leftovers-basket.cy.ts et planning-page.cy.ts) et composants (63/63). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 967be6df4a |
test(cypress): convention de viewports telephone/tablette
Some checks failed
Ticket #83 du chantier UI mobile & tablette. Le repo n'avait aucun override de viewportWidth/Height ni pour e2e ni pour les tests de composants - tout tournait a la taille par defaut, jamais un vrai telephone/une vraie tablette. - cypress/support/viewports.ts : PHONE_VIEWPORT (390x844) et TABLET_VIEWPORT (900x1200), tailles Android reelles plutot que les presets iPhone/iPad de Cypress (appli Android uniquement) - dans les bandes de _breakpoints.scss ($phone/$tablet du ticket #91), pas collees au pixel de bordure. Commande cy.setViewport("phone" | "tablet") (pas de surcharge du cy.viewport natif). Importe par support/e2e.ts et support/component.ts, disponible dans les deux modes. - layout.cy.ts : les cy.viewport(...) en dur pour du telephone (deux tests ajoutes par #91, plus les deux plus anciens du passage #39) et le test "au-dessus du point de rupture telephone" migrent vers la commande partagee - ce dernier passe a un vrai viewport tablette (900px, dans la bande 641-1024) plutot qu'un 1000x700 arbitraire, prouvant au passage que le preset tablette fonctionne. - Nouveau cypress/component/Dialog.cy.tsx : premier test de Dialog en isolation (jusqu'ici seulement couvert indirectement via RecipePickerDialog) - verifie le plein-ecran sur telephone du ticket #91, qui n'avait pas encore son propre test, et prouve au passage que cy.setViewport fonctionne en mode composant. Trouvaille en verifiant : le mode composant de Cypress a son propre viewport par defaut, plus petit que celui du mode e2e (1000x660) - deja sous le seuil telephone. Le premier essai du test "desktop" de Dialog echouait pour cette raison (le CSS plein-ecran s'appliquait sans meme appeler setViewport). Corrige en fixant un viewport large explicite pour ce cas plutot que de compter sur un defaut implicite - note ajoutee a dev-environment-gotchas pour la prochaine fois. Verifie en local : tsc -b --force, biome check, build de prod, suite Cypress e2e complete (140/140) et composants (65/65, incluant les 2 nouveaux tests de Dialog.cy.tsx). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| d6cf4aa63d |
feat(mobile): redirection vers l'appli + page de telechargement
Some checks failed
Ticket #84 du chantier UI mobile & tablette. Aucune infra de deep-link n'existait (verifie en amont : pas de scheme personnalise, pas d'App Links, pas d'assetlinks.json, pas de listener appUrlOpen). - lib/mobile-app-redirect.ts : attemptOpenNativeApp() (pattern "smart app banner" classique - tente le scheme batchcooking://, bascule sur onFallback si la page est toujours visible passe un court delai) et isAndroidPhoneBrowser() (UA Android+Mobile, deliberement distinct de useIsMobileViewport() - une question de plateforme, pas de largeur). Respecte une preference "rester sur le web" (localStorage, cle exportee pour que l'onglet Parametres d'application - ticket #86 - la reutilise) sans jamais forcer la redirection. - AndroidManifest.xml : nouvel intent-filter additif (scheme batchcooking, sans toucher au filtre MAIN/LAUNCHER existant). - Nouvelle page /telecharger (publique, hors RequireAuth - un visiteur deja connecte sur le web mais sans l'appli sur cet appareil a une raison legitime d'y atterrir) : lien direct vers le proxy existant ${API_BASE_URL}/mobile/download (rien a changer cote API), bouton "reessayer d'ouvrir l'appli". Deliberement pas de changement sur la landing page ici (lien discret actuel laisse tel quel) - c'est le ticket #85, separe pour rester petit/revuable. Verifie en local : tsc -b --force, biome check, build de prod, suite Cypress e2e complete (141/141 sur cette branche - #91 n'est pas encore merge, layout.cy.ts y est donc encore dans son etat pre-#91) et composants (69/69, incluant le nouveau mobile-app-redirect.cy.tsx). Note technique : le premier essai de test du composant tentait un vrai window.location.href vers le scheme personnalise depuis Cypress/ Electron, ce qui bloquait le runner indefiniment (protocole inconnu mal gere par Electron) - corrige en rendant ce seul appel injectable (openAppUrl, reserve aux tests) sans toucher au comportement reel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 0fd4928efc |
feat(layout): socle UI mobile - nav, feuille et dialogs plein ecran
All checks were successful
Fondation du chantier UI mobile & tablette (#91) : jusqu'ici, toute adaptation mobile de ce repo etait du CSS pur sur le meme DOM (aucun composant ne branchait son JSX selon la largeur/plateforme). Ce ticket introduit le premier vrai branchement, et les primitifs que les 8 tickets suivants du chantier reutiliseront. - useIsMobileViewport() (apps/web/src/lib/) : point de verite unique, base sur matchMedia, live (reagit a un resize/une rotation). - _breakpoints.scss : $phone/$tablet Sass + mixins phone/phone-up/ tablet-down, remplacant les nombres magiques 640/1024 repetes en dur dans 6 fichiers (AppLayout, landing, recipes, recipe-picker-dialog, planning-page, dialog, tours-page). Les deux occurrences de 860px (landing hero, tours-page) sont deliberement laissees telles quelles - un collapse de grille 2 colonnes qui n'a rien a voir avec le breakpoint tablette du catalogue recettes, les unifier aurait change un comportement visuel reel sans raison. - useNativeDialogBehavior : mecanique (showModal/close event/clic exterieur) extraite de Dialog.tsx, partagee avec le nouveau BottomSheet.tsx (feuille mobile plein-largeur, meme fondation <dialog> natif que Dialog, juste ancree en bas). - AppLayout : sur telephone, la sidebar n'est plus reflow-ee en CSS en barre horizontale (l'ancien traitement) - elle est remplacee par une vraie barre d'onglets basse (Planning/Recettes/Liste de courses + Plus), le "Plus" ouvrant une BottomSheet combinant Parametres et Compte (deux dropdowns flottants separes cote desktop -> un seul point d'entree mobile, IA plus simple, un seul emplacement d'onglet). - dialog.scss : vrai plein-ecran (bord a bord, sans arrondi) sous le breakpoint phone au lieu d'une marge simplement reduite. Verifie en local : pnpm --filter web exec tsc -b --force, biome check, pnpm --filter web build, suite Cypress e2e complete (140/140) et composants (63/63) - y compris layout.cy.ts, reecrit pour tester la nouvelle barre basse (l'ancien test de la barre horizontale n'a plus de sens, ce traitement n'existe plus). Closes #91 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 5e478188a2 |
fix(api): autorise l'origine WebView Capacitor dans CORS_ORIGIN
All checks were successful
L'app Android n'arrivait a se logger/inscrire nulle part (204 + "erreur inattendue" cote client) : CORS_ORIGIN n'etait jamais defini en prod (docker-compose.yml ne le passe pas a `app`), donc il retombait sur son default `http://localhost:5173` (le serveur Vite de dev), qui ne matche evidemment jamais l'origine reelle de l'appli packagee (`https://localhost`, cf. capacitor.config.ts). Le site web n'est lui jamais touche : il est servi depuis la meme origine que l'API en prod, donc jamais de preflight CORS du tout sur /auth/*. Le symptome trompeur vient du package `cors` : un preflight OPTIONS recoit toujours 204, meme pour une origine rejetee — la difference invisible sans inspecter les headers est l'absence de Access-Control-Allow-Origin, qui fait ensuite bloquer le vrai POST cote client sans jamais atteindre le serveur. Vu depuis les logs serveur, la route a bien l'air "appelee" (le preflight matche le meme chemin) alors que /auth/login lui-meme n'est jamais reellement recu. CORS_ORIGIN devient une liste separee par des virgules (le type `ExpressServerCoreOptions.corsOrigin` acceptait deja `string | string[]`) et son default inclut desormais `https://localhost` — l'origine de l'appli packagee est fixe et non secrete (contrairement au domaine du site en prod, qui n'a de toute facon pas besoin d'etre dans cette liste puisqu'il est same-origin), donc la coder en dur dans le default couvre tout deploiement sans configuration supplementaire. Verifie en local : script ad hoc qui demarre le vrai `createServer()` et envoie un preflight OPTIONS /auth/login avec differentes origines — `https://localhost` et `http://localhost:5173` recoivent maintenant Access-Control-Allow-Origin, une origine arbitraire non listee continue de ne pas le recevoir (pas de bypass en wildcard). Egalement `pnpm --filter api exec tsc --noEmit` et `pnpm exec biome check` sur les fichiers touches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 8315f737d4 |
Merge pull request 'fix(auth): la déconnexion retombe sur / plutôt que /login' (#79) from fix/logout-redirect-landing into main
All checks were successful
|
|||
| 21cd8f014d |
fix(auth): la deconnexion retombe sur / plutot que /login
All checks were successful
RequireAuth.tsx affiche deja la LandingPage publique pour un visiteur deconnecte exactement sur / (issue #41) — seul handleLogout naviguait encore explicitement vers /login. Change juste la destination de navigate() ; aucun autre changement necessaire. Scenario Cypress mis a jour en consequence (le pas existant "the URL should be the home page" suffit, pas besoin d'un nouveau pas). Verifie en local, suite complete : pnpm lint, pnpm --filter web build, pnpm run e2e (139/139, apres avoir nettoye un process vite zombie sur le port 5173 laisse par une precedente invocation cypress dans cette meme session — voir dev-environment-gotchas pour le detail). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 3abb7217dd |
feat(mobile): proxie le telechargement de l'APK via l'API (pas de CORS)
All checks were successful
Suite au fix de /releases/latest (precedent), l'utilisateur voulait un
lien qui declenche le telechargement du .apk directement, sans quitter
l'appli web. Verifie en direct : fetch("https://git.dev.kyuno.fr/api/...")
depuis le navigateur echoue avec "Failed to fetch" (confirme via un essai
en mode no-cors : la requete reseau aboutit bien, seule la reponse est
bloquee) — Forgejo n'envoie aucun header CORS sur son API Releases,
publique ou non. Ca ne concerne pas que la page d'accueil : le hook de
verification de mise a jour (#66, useAppUpdateCheck) faisait exactement le
meme fetch cross-origin depuis la WebView native, et echouait donc tout
aussi silencieusement en production sans que personne ne puisse s'en
rendre compte (le hook avale toute erreur reseau par design).
Ajoute un petit module apps/api/src/modules/mobile/ (2 routes publiques,
aucune ne touche la base de donnees) :
- GET /mobile/latest-release -> { version } (proxy JSON, remplace le fetch
direct de useAppUpdateCheck)
- GET /mobile/download -> redirection 302 vers l'asset .apk reel de la
derniere release (ou vers la page /releases/latest en repli si
Forgejo est injoignable ou qu'aucun .apk n'est encore attache)
Cote frontend : LandingPage.tsx et UpdateBanner.tsx pointent maintenant
vers ce proxy (`${API_BASE_URL}/mobile/download`, target="_blank") au lieu
d'une URL Forgejo directe — un clic, telechargement immediat, jamais de
navigation hors de l'appli. useAppUpdateCheck.ts appelle desormais
apiClient.getLatestMobileVersion() au lieu d'un fetch direct.
Verifie en local (sans Postgres, ces routes n'en ont pas besoin) contre
les vraies donnees Forgejo :
GET /mobile/latest-release -> {"version":"v0.2.0"}
GET /mobile/download -> 302, Location vers le vrai .apk signe
pnpm lint / tsc (api + web) / pnpm --filter web build : tous verts. Le
scenario Cypress "offers the Android app download" echoue encore en local
(meme souci pre-existant deja documente sur la PR precedente, confirme
independant du code par CI reel) ; mis a jour pour asserter le nouveau
href relatif.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 29bd413de3 |
fix(mobile): corrige les liens vers /releases/latest/download
All checks were successful
Verifie en conditions reelles apres le premier vrai succes de bout en bout de release.yml (v0.2.0, Release Forgejo #12 avec batchcooking.apk attache) : Forgejo, contrairement a GitHub, n'implemente pas le raccourci /releases/latest/download/<fichier>. Seul /releases/latest lui-meme resout (redirige vers la page de la release) : /releases/latest/download/batchcooking.apk -> 404 /releases/latest -> 303 vers /releases/tag/v0.2.0 (OK) /releases/download/v0.2.0/batchcooking.apk -> 200 (OK, mais pas "latest") Corrige les deux endroits qui presumaient le raccourci GitHub (issue #65 et #66, deja mergees) pour pointer vers /releases/latest tout court : un clic de plus via la page de release Forgejo pour atteindre le fichier, mais un lien qui resout toujours plutot qu'un raccourci qui 404 en permanence. Met a jour le test Cypress de la page d'accueil et le commentaire de release.yml en consequence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 854d795c5d |
fix(release): corrige release.yml pour Forgejo et y attache un APK signe
All checks were successful
release.yml datait d'avant la migration vers Forgejo et n'avait jamais ete reteste : refs d'action bares (actions/checkout@v4, softprops/action-gh- release@v2) sans le prefixe URL complet qu'exige ce runner self-hosted (meme contrainte deja rencontree et documentee dans ci.yml). Ajoute aussi ce que #65 attendait reellement : un job build-android-apk (meme recette que le job build-android de ci.yml, issue #64) qui construit et signe l'APK, deduit versionName/versionCode du tag pousse (vX.Y.Z -> versionName X.Y.Z, versionCode majeur*10000+mineur*100+correctif) et l'attache a la Release Forgejo sous un nom de fichier stable (batchcooking.apk) via softprops/action-gh-release. C'est ce nom stable qui permet a /releases/latest/download/batchcooking.apk (page d'accueil, #65) de toujours pointer vers la derniere release sans jamais changer de lien. build.gradle : versionCode/versionName deviennent surchargeables via -PandroidVersionCode/-PandroidVersionName (defaut inchange : 1 / "1.0" pour tout build qui ne passe pas ces proprietes, comme le job build-android existant de ci.yml sur un simple push main). Duplique le job de build Android plutot que de factoriser en workflow_call reutilisable — le support des workflows reutilisables sur ce runner Forgejo/act self-hoste n'est pas verifie, et cette recette de ~15 lignes est deja eprouvee telle quelle dans ci.yml ; pas la peine de risquer une nouvelle surprise de compatibilite pour economiser un peu de duplication. Non teste de bout en bout dans cette PR : release.yml ne se declenche que sur un vrai tag pousse (jamais sur une simple branche/PR), et pousser un tag ici declencherait potentiellement un vrai redeploiement Portainer en production si PORTAINER_WEBHOOK_URL est deja configure — a tester deliberement avec l'utilisateur, pas par un tag de test improvise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 644164b25a |
Merge pull request 'feat(landing): lien de téléchargement de l'app Android (#65)' (#68) from feat/landing-apk-download into main
Some checks failed
|
|||
| 9c6feab064 |
fix(ci): corrige le bit exécutable de gradlew et le fiabilise en CI
All checks were successful
Le tout premier run réel du job build-android (après le merge de #64) échouait avec './gradlew: Permission denied' — gradlew était suivi par git en 100644 (non exécutable) au lieu de 100755, probablement perdu au commit initial du projet Android natif (#63) sur cette machine Windows. Corrige le mode suivi par git (git update-index --chmod=+x) et ajoute un chmod +x défensif juste avant l'exécution dans ci.yml, puisque ce bit n'a aucun sens sous Windows et peut être reperdu silencieusement par un futur commit sur cette machine. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |