fix(recipes): synchronise les sources en base au démarrage de l'image de prod

Le conteneur de prod ne peuplait jamais la table Source : seed-runtime.ts
(l'entrée seed de l'image Docker, exécutée après `prisma migrate deploy`)
n'appelait que seedReferenceData(), jamais registerAllRecipeSources()/
syncRecipeSources() — contrairement à prisma/seed.ts (dev). server.ts
enregistre bien les adaptateurs dans son propre registre en mémoire, mais
c'est un processus distinct de celui qui lance seed-runtime.js dans la
chaîne CMD du Dockerfile ; sans ce sync, GET /reference/sources renvoyait
toujours [], et HouseholdSettingsPage masquait silencieusement toute la
section sources (sources.length === 0 → return null). C'est ce que
l'utilisateur a remarqué : impossible de paramétrer les sources visibles
du foyer en prod.

Vérifié en local : Source/HouseSource vidées, seed-runtime.js compilé
relancé exactement comme le ferait le conteneur (migrate deploy déjà
appliqué, puis ce script) → les deux sources (TheMealDB, JSON-LD) sont
bien resynchronisées.

Ajoute aussi la couverture Cypress du parcours "sources" qui manquait :
- onboarding.feature : nouveau scénario où le catalogue de sources n'est
  pas vide — l'étape /onboarding/sources s'affiche et se soumet, au lieu
  du seul scénario existant qui la voyait toujours skippée (catalogue
  vide).
- household-settings.feature : nouveaux scénarios pour la section sources
  de /parametres/foyer — affichage + sauvegarde (autosave incluse) quand
  des sources existent, et disparition complète de la section quand le
  catalogue est vide.
- Nouvelles steps partagées (reference-data.steps.ts pour le catalogue,
  household-mutations.steps.ts pour la sélection par foyer).

Non exécutés localement : Chromium/Electron headless plante au lancement
du process GPU dans cet environnement (limitation documentée du README,
reproductible sur main, sans lien avec ce changement) — vérifiés par
relecture attentive contre le code source réel (libellés de traduction,
routes, formes de requête/réponse) et en suivant le même gabarit que les
scénarios existants déjà verts en CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-08-20 13:48:29 +02:00
parent 6178205603
commit deec91c5a3
6 changed files with 124 additions and 4 deletions

View file

@ -6,6 +6,12 @@
"runtimeExecutable": "pnpm", "runtimeExecutable": "pnpm",
"runtimeArgs": ["--filter", "web", "dev"], "runtimeArgs": ["--filter", "web", "dev"],
"port": 5173 "port": 5173
},
{
"name": "api",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["--filter", "api", "dev"],
"port": 3000
} }
] ]
} }

View file

@ -1,5 +1,7 @@
import { prisma } from "../db/prisma.js"; import { prisma } from "../db/prisma.js";
import { syncRecipeSources } from "../db/recipe-source-sync.js";
import { seedReferenceData } from "../db/reference-seed-data.js"; import { seedReferenceData } from "../db/reference-seed-data.js";
import { registerAllRecipeSources } from "../sources/index.js";
/** /**
* Runtime seed entry point for the production Docker image run via * Runtime seed entry point for the production Docker image run via
@ -14,11 +16,25 @@ import { seedReferenceData } from "../db/reference-seed-data.js";
* alongside everything else, and it runs under plain `node`, no tsx * alongside everything else, and it runs under plain `node`, no tsx
* needed at runtime. * needed at runtime.
* *
* Also registers and syncs the recipe-source registry
* (`registerAllRecipeSources`/`syncRecipeSources`) mirroring
* `prisma/seed.ts`'s own two calls. Without this, `server.ts`'s own
* `registerAllRecipeSources()` call only populates *that* process' in-memory
* registry (each `node` invocation in the Docker CMD chain is a separate
* process), so the `Source` table itself would stay permanently empty in
* production and `GET /reference/sources` would always return `[]` which
* is exactly what silently hid the whole "sources" section of
* `HouseholdSettingsPage` (`apps/web`) until this was added.
*
* Safe to run on every container start: `seedReferenceData` upserts by * Safe to run on every container start: `seedReferenceData` upserts by
* each row's unique name, so re-running it against a database that * each row's unique name, and `syncRecipeSources` is equally idempotent
* (see its own doc comment) re-running both against a database that
* already has this data is a no-op. * already has this data is a no-op.
*/ */
registerAllRecipeSources();
seedReferenceData(prisma) seedReferenceData(prisma)
.then(() => syncRecipeSources(prisma))
.then(() => prisma.$disconnect()) .then(() => prisma.$disconnect())
.catch(async (err) => { .catch(async (err) => {
console.error(err); console.error(err);

View file

@ -53,6 +53,32 @@ Feature: Household settings
Then the household deletion request should have been made Then the household deletion request should have been made
And I should see "Créer un foyer" And I should see "Créer un foyer"
Scenario: Shows and saves the household's enabled recipe sources
Given I am signed in as "Alice" "Martin"
And my household id is 1
And the household request returns the two-member household
And the sources reference list has options
And the household's enabled sources are empty
And saving the source selection will succeed
When I visit "/parametres/foyer"
Then I should see the section "Sources disponibles"
And I should see "TheMealDB"
And I should see "Officielle"
And I should see "Import générique (JSON-LD)"
And I should see "Non officielle"
When I check the checkbox "TheMealDB"
Then the source selection update request should have been made with source id 1
And I should see "Enregistré "
Scenario: Hides the sources section entirely when no source is implemented yet
Given I am signed in as "Alice" "Martin"
And my household id is 1
And the household request returns the two-member household
And the sources reference list is empty
And the household's enabled sources are empty
When I visit "/parametres/foyer"
Then I should not see "Sources de recettes"
Scenario: Leaves the household Scenario: Leaves the household
Given I am signed in as "Bob" "Dupont" Given I am signed in as "Bob" "Dupont"
And my user id is 2 And my user id is 2

View file

@ -52,6 +52,31 @@ Feature: Onboarding wizard
Then the allergies update request should have been made with no allergy ids Then the allergies update request should have been made with no allergy ids
And the URL should be the home page And the URL should be the home page
Scenario: Shows and saves the sources step instead of skipping it, when sources are available
Given the diets reference list is empty
And selecting the diet will succeed
And the household request returns no household
And creating a household will succeed
And the sources reference list has options
And the household's enabled sources are empty
And saving the source selection will succeed
And the allergies reference list is empty
And updating allergies will succeed
And I have signed up
When I click the button "Continuer"
Then the URL should include "/onboarding/foyer"
When I fill in the "houseName" field with "Chez Alice"
And I click the button "Créer"
Then the household creation request should have been made with name "Chez Alice"
And the URL should include "/onboarding/sources"
And I should see "Étape 3 sur 4"
And I should see the section "Sources disponibles"
And I should see "TheMealDB"
When I check the checkbox "TheMealDB"
And I click the button "Continuer"
Then the source selection update request should have been made with source id 1
And the URL should include "/onboarding/allergenes"
Scenario: Lets the household step be completed by joining an existing household instead of creating one Scenario: Lets the household step be completed by joining an existing household instead of creating one
Given the diets reference list is empty Given the diets reference list is empty
And selecting the diet will succeed And selecting the diet will succeed

View file

@ -61,3 +61,25 @@ Then(
cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: code }); cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: code });
}, },
); );
// Shared between onboarding.feature's sources step and household-settings.feature's
// sources section — both read/write the same `/house/current/sources` endpoint.
Given("the household's enabled sources are empty", () => {
cy.intercept("GET", "**/house/current/sources", { statusCode: 200, body: [] });
});
Given("saving the source selection will succeed", () => {
cy.intercept("PATCH", "**/house/current/sources", (req) => {
req.reply({ statusCode: 200, body: req.body.sourceIds });
}).as("updateSources");
});
Then(
"the source selection update request should have been made with source id {int}",
(id: number) => {
cy.wait("@updateSources")
.its("request.body")
.should("deep.equal", { sourceIds: [id] });
},
);

View file

@ -35,9 +35,34 @@ Given("the allergies reference list is empty", () => {
// Every onboarding scenario that reaches the household step also reaches // Every onboarding scenario that reaches the household step also reaches
// `/onboarding/sources` right after (when a household got created/joined — // `/onboarding/sources` right after (when a household got created/joined —
// see `OnboardingHouseholdPage`'s `goToNextStep`), which reads this before // see `OnboardingHouseholdPage`'s `goToNextStep`), which reads this before
// self-skipping to `/onboarding/allergenes`. No "has options" counterpart // self-skipping to `/onboarding/allergenes` if it comes back empty.
// yet — no source is implemented in the app itself, so there's nothing
// real to mock a populated catalog with.
Given("the sources reference list is empty", () => { Given("the sources reference list is empty", () => {
cy.intercept("GET", "**/reference/sources", { statusCode: 200, body: [] }); cy.intercept("GET", "**/reference/sources", { statusCode: 200, body: [] });
}); });
// Mirrors what's actually seeded (`reference-seed-data.ts`'s
// `registerAllRecipeSources`/`syncRecipeSources`) — one official API source
// with an icon, one unofficial scraper without one — so the sources step
// (onboarding and `/parametres/foyer` alike) has something real to show
// instead of self-skipping.
Given("the sources reference list has options", () => {
cy.intercept("GET", "**/reference/sources", {
statusCode: 200,
body: [
{
id: 1,
key: "theMealDb",
name: "TheMealDB",
official: true,
iconUrl: "https://www.themealdb.com/images/logo.svg",
},
{
id: 2,
key: "jsonLdRecipe",
name: "Import générique (JSON-LD)",
official: false,
iconUrl: null,
},
],
});
});