From 6b60c11408ff4fc1aaaf5c634626cf4b5f6bfc43 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 28 Aug 2026 08:56:59 +0200 Subject: [PATCH] feat(cooking): endpoint GET /cooking-session (plan de cuisine optimise) Module cooking-session : charge le Planning couvrant ?date= (meme requete "plage couvrante" + degradation "jamais null" que /shopping-list), mappe chaque PlanningItem vers l'entree pure de optimizeCookingPlan (ingredients/ unites/techniques/ustensiles resolus via toIngredientView/toUnitView reutilisees de recipe.service), renvoie OptimizedCookingPlanView. - cookingSessionPlanningInclude reprend le sous-arbre steps de recipeInclude. - Route requireAuth, contrat ?date= identique a /shopping-list. - Monte /cooking-session dans app.ts. - Tests d'integration Mocha (401, date invalide, plan vide sans foyer / sans planning, mutualisation d'une decoupe entre 2 recettes planifiees). - specs/batch-cooking-architecture.md : module "Calcul batch-cooking" TODO -> v1 implementee ; nouvelle section dans backend-architecture.md. Co-Authored-By: Claude Sonnet 5 --- apps/api/src/app.ts | 2 + .../cooking-session/cooking-session.routes.ts | 37 ++++ .../cooking-session.service.ts | 168 +++++++++++++++ apps/api/test/cooking-session.test.ts | 204 ++++++++++++++++++ specs/backend-architecture.md | 51 +++++ specs/batch-cooking-architecture.md | 24 +-- 6 files changed, 474 insertions(+), 12 deletions(-) create mode 100644 apps/api/src/modules/cooking-session/cooking-session.routes.ts create mode 100644 apps/api/src/modules/cooking-session/cooking-session.service.ts create mode 100644 apps/api/test/cooking-session.test.ts diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 1d62303..028fc4b 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -6,6 +6,7 @@ import { env } from "./config/env.js"; import { errorLogger } from "./middlewares/error-logger.js"; import { requestLogger } from "./middlewares/request-logger.js"; import { authRouter } from "./modules/auth/auth.routes.js"; +import { cookingSessionRouter } from "./modules/cooking-session/cooking-session.routes.js"; import { houseRouter } from "./modules/house/house.routes.js"; import { techStepWorkerRouter } from "./modules/internal/tech-step-worker.routes.js"; import { planningRouter } from "./modules/planning/planning.routes.js"; @@ -52,6 +53,7 @@ export function createServer(): ExpressServer { server.mountRouter("/recipes", recipeRouter); server.mountRouter("/reference", referenceRouter); server.mountRouter("/shopping-list", shoppingListRouter); + server.mountRouter("/cooking-session", cookingSessionRouter); server.mountRouter("/sources", sourcesRouter); // Serves the built frontend (production Docker image only — see diff --git a/apps/api/src/modules/cooking-session/cooking-session.routes.ts b/apps/api/src/modules/cooking-session/cooking-session.routes.ts new file mode 100644 index 0000000..3744f7f --- /dev/null +++ b/apps/api/src/modules/cooking-session/cooking-session.routes.ts @@ -0,0 +1,37 @@ +import { parseDateOnly } from "@batch-cooking/date-tools"; +import { HttpError } from "@batch-cooking/error-tools"; +import { wrapAsyncHandler } from "@batch-cooking/express-tools"; +import { ErrorCode, getCookingSessionSchema } from "@batch-cooking/shared"; +import { Router } from "express"; +import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js"; +import { getCookingPlanForDate } from "./cooking-session.service.js"; + +/** Router mounted at `/cooking-session` in app.ts. */ +export const cookingSessionRouter = Router(); + +/** + * Returns the authenticated user's household's optimized cooking plan for + * the week covering `?date=` (`YYYY-MM-DD`) — every recipe planned that + * week reorganized into ordered phases (see {@link getCookingPlanForDate}). + * Always `200`, never `null` — no household or nothing planned that week + * both come back as a normal `OptimizedCookingPlanView` with empty + * `recipes`/`phases`. Same request contract as `GET /shopping-list`. + */ +cookingSessionRouter.get( + "/", + requireAuth, + wrapAsyncHandler(async (req, res) => { + const input = getCookingSessionSchema.parse(req.query); + const date = parseDateOnly(input.date); + if (date === null) { + throw new HttpError( + 400, + ErrorCode.VALIDATION_ERROR, + `Not a real calendar date: ${input.date}`, + ); + } + + const plan = await getCookingPlanForDate(res.locals.userProfile.houseId, date); + res.status(200).json(plan); + }), +); diff --git a/apps/api/src/modules/cooking-session/cooking-session.service.ts b/apps/api/src/modules/cooking-session/cooking-session.service.ts new file mode 100644 index 0000000..ee5d9de --- /dev/null +++ b/apps/api/src/modules/cooking-session/cooking-session.service.ts @@ -0,0 +1,168 @@ +import { type DateTime, getWeekStart, toDateOnly } from "@batch-cooking/date-tools"; +import type { CookingTaskIngredientView, OptimizedCookingPlanView } from "@batch-cooking/shared"; +import type { Prisma } from "@prisma/client"; +import { prisma } from "../../db/prisma.js"; +import { + type OptimizerRecipeInput, + type OptimizerStepInput, + optimizeCookingPlan, +} from "../../lib/recipe-matching/cooking-optimizer.js"; +import { toIngredientView, toUnitView } from "../recipe/recipe.service.js"; + +/** + * Prisma `include` for a `Planning` query that needs, for every item, its + * recipe's ordered steps with the full detected-technique tree — the raw + * material the optimizer works on (see `cooking-optimizer.ts`). It's the + * `steps` sub-tree of `recipe.service.ts`'s own `recipeInclude`, resolved + * the same way so {@link toIngredientView}/{@link toUnitView} can be reused + * as-is; deliberately narrower than a full `RecipeView` fetch (no + * diets/favorites/recipe-level ingredient list — the optimizer reads + * quantities off the technique clauses, not the recipe header). + */ +function cookingSessionPlanningInclude() { + return { + items: { + include: { + recipe: { + select: { + id: true, + name: true, + portions: true, + steps: { + orderBy: { order: "asc" }, + include: { + techSteps: { + orderBy: { order: "asc" }, + include: { + techStep: true, + ingredients: { + include: { + ingredient: { + include: { + allergies: { include: { allergy: { include: { category: true } } } }, + diets: { include: { diet: true } }, + }, + }, + unit: true, + }, + }, + utensils: { include: { utensil: true } }, + }, + }, + }, + }, + }, + }, + }, + }, + } satisfies Prisma.PlanningInclude; +} + +type PlanningWithSteps = Prisma.PlanningGetPayload<{ + include: ReturnType; +}>; +type PlanningItemWithSteps = PlanningWithSteps["items"][number]; + +/** + * Maps one planning item's recipe (with {@link cookingSessionPlanningInclude}) + * to the optimizer's pure input shape — ingredient/unit/technique/utensil + * rows resolved to their reference views here so the optimizer itself never + * touches Prisma. `Decimal` quantities become plain numbers (same + * `Number(...)` conversion as `recipe.service.ts`'s own view mappers); an + * unresolved-unit line keeps `unit: null`. + */ +function toOptimizerRecipe(item: PlanningItemWithSteps): OptimizerRecipeInput { + const steps: OptimizerStepInput[] = item.recipe.steps.map((step) => ({ + stepId: step.id, + order: step.order, + description: step.description, + techSteps: step.techSteps.map((techStep) => { + const ingredients: CookingTaskIngredientView[] = techStep.ingredients.map((line) => ({ + ingredient: toIngredientView(line.ingredient), + quantity: line.quantity === null ? null : Number(line.quantity), + unit: line.unit === null ? null : toUnitView(line.unit), + })); + return { + techStep: { id: techStep.techStep.id, key: techStep.techStep.key }, + order: techStep.order, + ingredients, + utensils: techStep.utensils.map(({ utensil }) => ({ id: utensil.id, key: utensil.key })), + }; + }), + })); + + return { + recipeId: item.recipe.id, + name: item.recipe.name, + // The slot's own portion count vs. the recipe's as-written yield — the + // optimizer scales technique-clause quantities by the ratio, same + // reasoning as `shopping-list.service.ts`'s `aggregateShoppingList`. + portions: item.portions, + recipePortions: item.recipe.portions, + steps, + }; +} + +/** + * Builds the household's optimized cooking plan for the week covering + * `date` — every recipe planned that week, reorganized into ordered phases + * that pool shared prep and float passive cooks into the background (see + * `cooking-optimizer.ts`). `date` follows the same convention as + * `planning.service.ts`'s `getPlanningForDate` (a caller-parsed `?date=`, + * not necessarily a Monday). + * + * Like `getShoppingListForDate` and unlike `getPlanningForDate`, this + * **never** returns `null` — no household and "no planning covers this week + * yet" both degrade to an empty `phases`/`recipes` on an otherwise normal + * {@link OptimizedCookingPlanView} (the week's date range is always + * computable from `date` alone). + */ +export async function getCookingPlanForDate( + houseId: number | null, + date: DateTime, +): Promise { + try { + const weekStart = getWeekStart(toDateOnly(date)); + const weekFinish = weekStart.plus({ days: 6 }); + const emptyPlan: OptimizedCookingPlanView = { + startDate: weekStart.toJSDate().toISOString(), + finishDate: weekFinish.toJSDate().toISOString(), + recipes: [], + phases: [], + }; + + if (houseId === null) { + return emptyPlan; + } + + // Same "covering range" lookup as getShoppingListForDate — see + // getPlanningForDate's doc comment for the UTC-midnight `Date` rationale. + const dateOnly = toDateOnly(date).toJSDate(); + const planning = await prisma.planning.findFirst({ + where: { + houseId, + startDate: { lte: dateOnly }, + finishDate: { gte: dateOnly }, + }, + orderBy: { startDate: "desc" }, + include: cookingSessionPlanningInclude(), + }); + + if (!planning) { + return emptyPlan; + } + + const { recipes, phases } = optimizeCookingPlan(planning.items.map(toOptimizerRecipe)); + return { + startDate: planning.startDate.toISOString(), + finishDate: planning.finishDate.toISOString(), + recipes, + phases, + }; + } catch (err) { + // Rethrown as-is — `wrapAsyncHandler`/the error middleware handles it, + // this service layer just isn't allowed a bare `await` per the repo's + // async/try-catch convention. + throw err; + } +} diff --git a/apps/api/test/cooking-session.test.ts b/apps/api/test/cooking-session.test.ts new file mode 100644 index 0000000..b218aff --- /dev/null +++ b/apps/api/test/cooking-session.test.ts @@ -0,0 +1,204 @@ +import type { DateTime } from "@batch-cooking/date-tools"; +import { ErrorCode, type SignupInput } from "@batch-cooking/shared"; +import { faker } from "@faker-js/faker"; +import { expect } from "chai"; +import request from "supertest"; +import { createApp } from "../src/app.js"; +import { prisma } from "../src/db/prisma.js"; +import { TEST_REFERENCE_DATE } from "../test-support/reference-date.js"; +import { resetDatabase } from "../test-support/reset-db.js"; + +/** See `auth.test.ts` — same rationale for generating rather than hardcoding. */ +function buildSignupPayload(): SignupInput { + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + return { + firstName, + lastName, + email: faker.internet.email({ firstName, lastName }).toLowerCase(), + password: faker.internet.password({ length: 16 }), + }; +} + +/** `toISODate()` only returns `null` for an invalid `DateTime` — never the always-valid values here. */ +function isoDate(date: DateTime): string { + const iso = date.toISODate(); + if (iso === null) throw new Error("Unexpectedly invalid DateTime in a test helper"); + return iso; +} + +/** The fixed test "today", as the `YYYY-MM-DD` string the `?date=` query expects. */ +function today(): string { + return isoDate(TEST_REFERENCE_DATE); +} + +/** Resolves a reference row's id by its `reference-seed-data.ts` uid (also its DB `key`) — same helpers as `recipe.test.ts`. */ +async function ingredientId(key: string): Promise { + return (await prisma.ingredient.findFirstOrThrow({ where: { key } })).id; +} +async function unitId(key: string): Promise { + return (await prisma.unit.findFirstOrThrow({ where: { key } })).id; +} +async function techStepId(key: string): Promise { + return (await prisma.techStep.findFirstOrThrow({ where: { key } })).id; +} + +describe("Cooking session", () => { + const app = createApp(); + + beforeEach(async () => { + await resetDatabase(); + }); + + after(async () => { + await prisma.$disconnect(); + }); + + describe("GET /cooking-session", () => { + it("rejects requests without a session cookie with 401 NOT_AUTHENTICATED", async () => { + const res = await request(app).get("/cooking-session").query({ date: today() }); + + expect(res.status).to.equal(401); + expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED); + }); + + it("rejects a malformed date with 400 VALIDATION_ERROR", async () => { + const agent = request.agent(app); + await agent.post("/auth/signup").send(buildSignupPayload()); + + const res = await agent.get("/cooking-session").query({ date: "not-a-date" }); + + expect(res.status).to.equal(400); + expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR); + }); + + it("returns an empty plan when the profile has no household", async () => { + const agent = request.agent(app); + await agent.post("/auth/signup").send(buildSignupPayload()); + + const res = await agent.get("/cooking-session").query({ date: today() }); + + expect(res.status).to.equal(200); + expect(res.body.recipes).to.deep.equal([]); + expect(res.body.phases).to.deep.equal([]); + }); + + it("returns an empty plan when no planning covers that week", async () => { + const agent = request.agent(app); + await agent.post("/auth/signup").send(buildSignupPayload()); + await agent.post("/house").send({ name: "Chez moi" }); + + const res = await agent.get("/cooking-session").query({ date: today() }); + + expect(res.status).to.equal(200); + expect(res.body.phases).to.deep.equal([]); + }); + + it("pools an identical prep step from two planned recipes into one merged-prep task", async () => { + const agent = request.agent(app); + await agent.post("/auth/signup").send(buildSignupPayload()); + const houseRes = await agent.post("/house").send({ name: "Chez moi" }); + const houseId: number = houseRes.body.id; + const authorId: number = houseRes.body.adminId; + + const onionId = await ingredientId("onion"); + const pieceId = await unitId("piece"); + const chopId = await techStepId("chop"); + const simmerId = await techStepId("simmer"); + + /** A recipe: one pure-prep "chop onion" step, then one simmer step. */ + async function makeRecipe(name: string, onionQty: number) { + return prisma.recipe.create({ + data: { + name, + authorId, + portions: 4, + steps: { + create: [ + { + order: 0, + description: "Émincer les oignons", + techSteps: { + create: [ + { + techStepId: chopId, + order: 0, + ingredients: { + create: [ + { + ingredientId: onionId, + quantity: onionQty, + unitId: pieceId, + start: 0, + end: 1, + }, + ], + }, + }, + ], + }, + }, + { + order: 1, + description: "Faire mijoter", + techSteps: { create: [{ techStepId: simmerId, order: 0 }] }, + }, + ], + }, + }, + }); + } + + const soupe = await makeRecipe("Soupe", 2); + const tarte = await makeRecipe("Tarte", 3); + + const planning = await prisma.planning.create({ + data: { + houseId, + startDate: TEST_REFERENCE_DATE.minus({ days: 2 }).toJSDate(), + finishDate: TEST_REFERENCE_DATE.plus({ days: 2 }).toJSDate(), + }, + }); + await prisma.planningItem.createMany({ + data: [ + { + planningId: planning.id, + weekDay: "lundi", + meal: "dejeuner", + recipeId: soupe.id, + portions: 4, + }, + { + planningId: planning.id, + weekDay: "mardi", + meal: "diner", + recipeId: tarte.id, + portions: 4, + }, + ], + }); + + const res = await agent.get("/cooking-session").query({ date: today() }); + + expect(res.status).to.equal(200); + expect(res.body.recipes.map((r: { name: string }) => r.name)).to.have.members([ + "Soupe", + "Tarte", + ]); + + const mise = res.body.phases[0]; + expect(mise.kind).to.equal("mise-en-place"); + const merged = mise.tasks.filter((t: { kind: string }) => t.kind === "merged-prep"); + expect(merged).to.have.length(1); + expect(merged[0].technique.key).to.equal("chop"); + expect(merged[0].ingredients[0].ingredient.key).to.equal("onion"); + expect(merged[0].ingredients[0].quantity).to.equal(5); + expect(merged[0].sourceRecipes).to.have.length(2); + + // The simmer steps land in a later phase, and one shows as background. + const later = res.body.phases.slice(1); + const backgrounds = later.flatMap((p: { background: unknown[] }) => p.background); + expect(backgrounds.length).to.be.greaterThan(0); + }); + }); +}); diff --git a/specs/backend-architecture.md b/specs/backend-architecture.md index 6c5902c..5d2e6ff 100644 --- a/specs/backend-architecture.md +++ b/specs/backend-architecture.md @@ -296,6 +296,57 @@ telles quelles par typage structurel. --- +## Cooking session — optimisation des étapes planifiées + +Router `/cooking-session` (`cooking-session.routes.ts`/`.service.ts`), +`requireAuth` — un seul endpoint : `GET /cooking-session?date=YYYY-MM-DD` → +`getCookingPlanForDate` → `OptimizedCookingPlanView`. Même contrat `?date=` +que `GET /shopping-list` (schéma shape-only + `parseDateOnly`), même requête +"plage couvrante" que `getPlanningForDate`, et **jamais `null`** de la même +façon que la liste de courses : pas de foyer / aucun `Planning` couvrant la +semaine ⇒ `recipes: []`, `phases: []`. + +C'est la première brique du module « Calcul batch-cooking » +([batch-cooking-architecture.md](./batch-cooking-architecture.md)), +jusqu'ici `TODO`. Le service ne fait que **charger + façonner** : sa requête +Prisma (`cookingSessionPlanningInclude`) reprend le sous-arbre `steps` de +`recipe.service.ts`'s `recipeInclude` (steps ordonnés → `StepTechStep` +ordonnés → `techStep` + `ingredients` résolus + `utensils`), puis +`toOptimizerRecipe` mappe chaque `PlanningItem` vers l'entrée pure de +l'optimiseur (ingrédients/unités/techniques/ustensiles déjà en vues de +référence via `toIngredientView`/`toUnitView` réutilisées — même raison que +`shopping-list.service.ts`). Un `PlanningItem` = une entrée d'optimiseur, +même si deux créneaux pointent la même recette à des portions différentes +(deux vraies préparations ; la mutualisation de la découpe les regroupe +quand même). + +**`optimizeCookingPlan`** (`lib/recipe-matching/cooking-optimizer.ts`, +pure/synchrone — testable sans base, même split que `ingredient-matcher.ts` +/ `tech-step-matcher.ts`) réorganise les recettes en **phases ordonnées** : + +- **Mutualisation de la mise en place** : une technique de découpe + (`PREP_TECHNIQUES` : `chop`/`peel`/`mince`/`julienne`/…) appliquée au même + ingrédient dans une étape *purement prep* (toutes ses techniques sont des + `PREP_TECHNIQUES`) de **≥ 2 recettes** est regroupée en une tâche + `merged-prep` ; les étapes d'origine sont *absorbées* (ne produisent plus + de tâche). Une découpe présente dans une seule recette reste inline (juste + classée en mise en place). Les découpes à l'intérieur d'une étape de + cuisson ne sont pas mutualisées en v1. +- **Parallélisme** : `TECHNIQUE_ATTENTION` classe chaque étape en `SETUP` + (préchauffage, eau à ébullition — poussé en mise en place), `PASSIVE` + (mijoter, braiser, cuire au four, mariner… — non surveillé une fois + lancé) ou `ACTIVE` (défaut). Les phases de cuisson interclassent les + recettes (une « prochaine étape de chaque recette » par phase) ; une + étape `PASSIVE` fait patienter sa recette une phase et s'affiche en + `background` des phases suivantes tant que sa consommatrice n'est pas + remontée. + +Quantités mises à l'échelle par `PlanningItem.portions / Recipe.portions` +comme la liste de courses. Sommes d'ingrédients uniquement à unité +identique (aucune conversion — même posture que `ShoppingListItemView`). + +--- + ## `reference` — catalogues publics (pas de session requise) Router `/reference` (`reference.routes.ts`/`.service.ts`) — **toutes les diff --git a/specs/batch-cooking-architecture.md b/specs/batch-cooking-architecture.md index f8d9881..5e67dd4 100644 --- a/specs/batch-cooking-architecture.md +++ b/specs/batch-cooking-architecture.md @@ -16,7 +16,7 @@ L'application repose sur une architecture **client-serveur** classique : flowchart TB subgraph SERVER["Server"] API["API (REST)"] - CALC["Calcul batch-cooking
(TODO)"] + CALC["Calcul batch-cooking
v1 implémentée (GET /cooking-session)"] IMPORT["Import d'une recette
implémenté"] IMP1["Import depuis source
(RecipeSourceAdapter)"] IMP2["Traduction en étapes
(ingrédients + techniques)"] @@ -38,8 +38,8 @@ flowchart TB ``` *(Le canal websocket envisagé dans la conception d'origine pour le calcul -batch-cooking temps réel n'existe pas encore — rien à documenter tant que ce -module reste TODO ; voir la note plus bas.)* +batch-cooking temps réel n'existe pas encore — le module v1 est un `GET` +recalculé à chaque visite, pas de temps réel ; voir la note plus bas.)* --- @@ -49,7 +49,7 @@ module reste TODO ; voir la note plus bas.)* Point d'entrée principal pour les échanges entre les clients et le serveur — REST classique, `requireAuth` (cookie JWT httpOnly) sur toute route qui n'est pas une donnée de référence publique. Détail complet des modules : [backend-architecture.md](./backend-architecture.md). ### Module « Calcul batch-cooking » -Logique de calcul du batch-cooking (optimisation du planning/des recettes selon le planning). **Statut : TODO — reste à développer**, avec `packages/shared`'s `assertIsNever` déjà en place comme outil prêt à l'emploi pour ce futur module (voir [backend-architecture.md](./backend-architecture.md#packagesshared--assertisnever)). +Logique de calcul du batch-cooking (optimisation des recettes entre elles selon le planning). **Statut : v1 implémentée** — `GET /cooking-session?date=` → `optimizeCookingPlan` (`apps/api/src/lib/recipe-matching/cooking-optimizer.ts`, pur) réorganise les recettes d'une semaine planifiée en **phases ordonnées** : une « mise en place » qui mutualise la découpe commune (même technique de découpe + même ingrédient dans une étape purement prep de ≥ 2 recettes = une seule tâche), puis des phases de cuisson qui interclassent les recettes en poussant les cuissons passives (mijotage, four…) en tâche de fond. v1 hors périmètre : fusion de cuissons, durées estimées, persistance/progression, canal websocket. Détail : [backend-architecture.md](./backend-architecture.md#cooking-session--optimisation-des-étapes-planifiées). ### Module « Import d'une recette » **Statut : implémenté.** Pipeline en trois étapes, comme prévu à la conception : @@ -91,15 +91,15 @@ détectées, favoris, visibilité des recettes). [backend-architecture.md](./backend-architecture.md#liste-de-courses--agrégation-des-ingrédients-planifiés)) — une simple **agrégation** des ingrédients déjà planifiés (somme par ingrédient/unité, mise à l'échelle par les portions de chaque créneau), - pas une optimisation. Le module « Calcul batch-cooking » lui-même reste - `TODO` : il désigne quelque chose de plus ambitieux qu'une somme - d'ingrédients — optimiser le planning/les recettes entre elles (ex. - mutualiser une préparation entre plusieurs recettes de la semaine), pas - encore défini plus précisément. C'est le principal chantier restant côté - serveur. + pas une optimisation. Le module « Calcul batch-cooking », lui, optimise les + recettes **entre elles** (mutualiser une préparation commune, paralléliser + les cuissons passives) — **v1 implémentée** via `GET /cooking-session` + (voir [backend-architecture.md](./backend-architecture.md#cooking-session--optimisation-des-étapes-planifiées)). - Le canal websocket envisagé pour la communication temps réel n'a pas encore - été construit — rien ne le remplace aujourd'hui (pas de polling), à - reconsidérer au moment d'attaquer le calcul batch-cooking. + été construit — rien ne le remplace aujourd'hui (pas de polling). Le calcul + batch-cooking v1 est un simple `GET` recalculé à chaque visite (comme la + liste de courses), pas de temps réel ; à reconsidérer si une session de + cuisine partagée/synchronisée est ajoutée. ---