Suite à la maquette validée : redessine TechStepCorrectionPopover et ajoute
l'édition de la température, jusqu'ici la seule des 4 métadonnées sans
aucune interface (ni dans l'UI, ni côté API — voir la PR back-end empilée
sous celle-ci).
- StepDescription.tsx : le surlignage de température devient cliquable
(un <button>, plus un <span> mort) — rouvre le popover de correction de
la technique à laquelle il est rattaché (findTechStepForTemperature,
résolu par span puisque la température n'a pas d'existence propre, elle
est une métadonnée de clause comme les ingrédients/ustensiles).
- TechStepCorrectionPopover.tsx : réécrit. Les 4 sections (Technique,
Ingrédients, Ustensiles, Température) restent toutes visibles en même
temps — plus de bascule de tout le popover vers un sous-écran pendant
l'ajout d'un ingrédient/ustensile/température, seule la section
concernée affiche son propre mini-formulaire, les autres (dont le
picker de technique) restent inchangées à côté. Le picker de technique
se cache derrière "Changer" une fois une technique choisie (rien à
décider une fois confirmée correcte). Chaque ingrédient/ustensile/
température attaché s'affiche en ligne avec sa citation exacte du texte
source (nouveau : la traçabilité vers le texte existait déjà en donnée,
mais n'était affichée nulle part). Nouveau sélecteur à 3 modes
(°C / Thermostat / Qualitatif) pour la température, cohérent avec le
modèle de données (un seul des trois à la fois). Icônes SVG par
catégorie (tag/feuille/poêle/thermomètre), même langage stroke-based
que layouts/nav-icons.tsx.
- recipes.scss : nouveau langage visuel du popover (sections avec icône,
lignes de métadonnées façon carte plutôt que puces, bouton "+ Ajouter"
en pointillés plutôt qu'un lien texte, sélecteurs segmentés). Les deux
surlignages (.step-tech-step, .step-temperature) passent de
`cursor: help` à `cursor: pointer` — les deux sont maintenant de vrais
déclencheurs cliquables.
- i18n : nouvelles clés (changeTechnique, temperatureSection,
addTemperature, removeTemperature, temperatureMode.*) ; clé
currentTechnique supprimée (le pictogramme remplace la phrase "Technique
retenue : X") ; préfixe "+ " retiré de addIngredient/addUtensil (l'icône
+ le porte déjà visuellement).
- Tests : TechStepCorrectionPopover.cy.tsx largement réécrit (10 tests,
dont 2 nouveaux sur la température) ; recipes.feature/recipes.ts : nouveau
scénario Gherkin "cliquer un surlignage de température rouvre le popover
de sa technique" (fixture Gratin dédiée), step "Changer" ajouté au
scénario de relabel existant.
Vérifié : pnpm lint + build (shared/api/web) clean. Suites Cypress
exécutées localement (Chrome headless) : TechStepCorrectionPopover.cy.tsx
10/10, suite component complète 50/50, recipes.feature 5/5, recipes.cy.ts
8/8, recipe-sources.feature 6/6, planning-page.cy.ts 7/7. Un échec
pré-existant et sans rapport (recipe-form.feature, "Preloads an existing
recipe's ingredients when editing") reproduit identiquement avec ce
travail entièrement stashé — confirmé non lié.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Jusqu'ici, submitTechStepCorrectionSchema (POST /recipes/:id/steps/:stepId/
corrections) acceptait ingredients/utensils pour compléter les métadonnées
d'une technique corrigée, mais aucun champ temperatures — StepTechStepTemperature
n'était jamais écrit que par toStepTechStepCreateInputs, lors d'un create/update
complet de recette. La température ajoutée en #22 n'avait jamais eu son
propre circuit de correction manuelle.
- packages/shared/src/schemas/recipe.ts : nouveau
manualStepTechStepTemperatureInputSchema (celsius/gasMark/qualitative
mutuellement exclusifs — exactement un des trois — même span start/end
que ingredients/utensils). Le champ temperatures rejoint ingredients/
utensils dans le même lot combiné (les trois voyagent ensemble : dès que
l'un est présent, les trois sont résolus, un absent devient [] donc
effacé — jamais une fusion partielle catégorie par catégorie).
- recipe-tech-step-correction.service.ts : applyManualCorrection écrit
désormais StepTechStepTemperature (delete+recreate, même logique "le
manuel remplace tout" que les deux autres tables). raw n'est jamais
envoyé par le client — dérivé de description.slice(start, end), pour ne
pas faire confiance à une copie cliente qui pourrait diverger du texte
réel. loadVisibleStepOrThrow retourne désormais le texte complet de la
description (pas seulement sa longueur), nécessaire pour cette dérivation.
- specs/backend-architecture.md + specs/batch-cooking-modele.md : les deux
routes de correction et step_tech_step_temperature n'étaient documentées
nulle part — comblé au passage.
- Tests Mocha ajoutés (celsius/gasMark/qualitative, rejet si zéro ou
plusieurs champs renseignés, remplacement sans accumulation, dérivation
de raw, span hors limites).
Périmètre : back-end uniquement (voir la maquette proposée précédemment).
Le popover de correction (apps/web) ne sera mis à jour que dans une PR
suivante, empilée sur celle-ci.
Vérifié : pnpm lint + build (shared, api) clean. Mocha non exécuté — pas
de Postgres/Docker accessible dans cette session (ni Bash ni PowerShell).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cliquer sur une recette de l'onglet Recettes de /cuisiner naviguait vers
/recettes/:id, quittant la session de cuisine en cours. La ligne devient un
bouton qui déplie désormais le détail complet de la recette directement
sous elle (RecipeDetailPanel, showActions=false — même traitement "aperçu"
que RecipePickerDialog), chargé à la demande via GET /recipes/:id, un seul
dépliant ouvert à la fois (même logique que le sous-menu Paramètres de la
sidebar). RecipesTab est désormais keyed par semaine comme PlanTab, pour
que changer de semaine referme un dépliant devenu obsolète.
Clé i18n recipesTab.viewRecipe supprimée (plus d'appelant).
Tests : cooking-session-page.cy.ts (déplier/replier, un seul ouvert à la
fois) + cooking-session.feature (scénario dédié). pnpm lint + pnpm --filter
web build clean ; 16/16 tests passants (Chrome headless).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dans l'onglet Plan de /cuisiner, la liste de tâches d'une phase était un
flux plat — la recette concernée n'apparaissait qu'en petite ligne sous
chaque carte. Regroupe désormais les tâches par combinaison exacte de
recettes (groupTasksByRecipes, cooking-session.ts) : chaque groupe devient
une mini-section titrée par le(s) nom(s) de recette(s), un pool
merged-prep/merged-setup formant sa propre section partagée plutôt que
d'être rattaché à une seule recette. La ligne "pour X" par carte devient
redondante avec le titre de section et est retirée (clé i18n forRecipes
supprimée).
Couverture ajoutée : cooking-session-page.cy.ts (mode layout, 3 sections
distinctes sur la fixture à préchauffages multiples) + cooking-session.feature
(parcours Gherkin, section partagée + sections solo).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`createRecipeInternal` écrivait, pour chaque technique détectée, ses
ingrédients / ustensiles / températures de clause ; `updateRecipe` et le
script `backfill-tech-steps.ts` n'écrivaient que le span de la technique.
Toute recette ré-enregistrée via l'UI (ou touchée par le backfill) perdait
donc silencieusement ces métadonnées — et `cooking-optimizer.ts` ne pouvait
plus détecter de conflit de four entre deux préchauffages dès qu'une recette
avait été éditée une fois (ni sur les imports antérieurs à la détection des
températures, jamais ré-enregistrés).
- `toStepTechStepCreateInputs` (nouveau, `lib/recipe-matching/`) : seule
source de vérité du payload `techSteps.create`, utilisée par les trois
appelants pour qu'ils ne divergent plus.
- `backfill-tech-steps.ts` : `createMany` -> `create` par ligne (createMany
ne fait pas les écritures imbriquées) + doc à jour.
- Rattrapage automatique au déploiement : `model BackfillState` + migration
`backfill_state`, script `backfill-runtime.ts` ajouté au CMD Docker après
`seed-runtime`. Gardé par un marqueur (tourne une seule fois), n'échoue
jamais le boot (une panne du backfill est loggée, le serveur démarre
quand même, retente au prochain start).
- Test `PATCH /recipes/:id` : une température de clause survit à l'édition.
- `reset-db.ts` : `backfill_state` ajouté au TRUNCATE.
Vérifié : `tsc --noEmit` + `pnpm --filter api build` + `pnpm lint` OK ;
migration = SQL généré par Prisma pour le modèle, à l'identique. Mocha
`recipe.test.ts` non lancé (Postgres + intent-service requis, indisponibles
ici) — à jouer en CI / sur la machine de dev.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le premier essai (length=2048 + ngram_size=2) a fait chuter le F1 agrege de
`tech-step-eval.test.ts` de 0.815 a 0.753, sous le plancher de 0.8. Balayage
mesure (fr, 25 iters, ngram=1, jeu de dev = derniere utterance tenue hors
entrainement ; collisions = tokens distincts partageant un bucket, sur 920) :
length=262144 dev 97.3% conf 0.809 2/920 collisions 400 s <- defaut
length=65536 dev 97.3% conf 0.809 10/920 collisions 106 s <- retenu
length=16384 dev 97.3% conf 0.808 50/920 collisions 35 s
length=2048 dev 97.3% conf 0.816 305/920 collisions 13 s <- livre puis recule
Deux corrections :
- ngram_size 2 -> 1. Les bigrammes rendent le classifieur nettement plus
confiant (0.809 -> 0.919), ce qui degrade la justesse bout-en-bout :
`_classifyClause` (tech-step-matcher.ts) ne retombe sur l'ancre NER que
sous CONFIDENCE_THRESHOLD, donc une classification confiante mais fausse
sur une technique voisine (blindBake au lieu de bake, poach au lieu de
boil) passe le seuil et ecrase l'ancre correcte. C'etait la seule cause
de la chute du F1.
- length 2048 -> 65536. A 2048, 305/920 tokens entrent en collision (33 %)
et la confiance moyenne se decale (0.816 vs 0.809). 65536 : 10 collisions,
confiance identique au bit pres au defaut spaCy, memes verdicts sur les
probes difficiles. ~5x plus rapide (vs ~30x pour 2048), 15x moins de
parametres. Descendre plus bas est possible mais reste du travail pour la
phase ou le jeu d'evaluation aura ete elargi et pourra le valider.
initialize() fr+en (chargement + entrainement) : 208.9 s contre ~930 s.
Suite pytest : 46 passed en 219.8 s (l'essentiel est le fixture qui entraine
le vrai corpus des deux locales, session-scoped).
start_period / timeout d'attente : 300s -> 600s (plafond pour runner lent).
Non verifie ici : le gate F1 lui-meme (tech-step-eval.test.ts, Postgres +
service requis, pas de Docker dans cet environnement) -> CI. Les 6 autres
echecs du job `test` (resetDatabase / seedReferenceData : contrainte unique
sur `key`, timeouts de hook) sont pre-existants sur `main` @ 81f7cb9 et sans
rapport avec ce changement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le service mettait ~930 s (15-16 min) a devenir `healthy`, ce que le README
presentait comme "un compromis assume, pas un defaut de configuration a
corriger". Mauvais diagnostic : le cout ne venait ni des iterations ni de la
taille du corpus, mais du parametre `length` de `spacy.TextCatBOW.v3`, laisse
a son defaut spaCy de 262144 faute d'etre passe explicitement.
Avec `no_output_layer: False` et 74 techniques, la couche de sortie faisait
19 398 730 parametres mis a jour par Adam a chaque pas de gradient, pour une
table de hachage vide a 99 % (le corpus tient dans quelques milliers de
tokens distincts).
Mesure contre le vrai corpus (fr, 25 iterations, jeu de dev = derniere
utterance de chaque technique tenue hors entrainement) :
length=262144 ngram=1 dev 97.3% conf 0.809 394.7 s <- avant
length=2048 ngram=2 dev 97.3% conf 0.919 12.3 s <- apres
Meme accuracy, meilleure confiance, 32x plus rapide. Les bigrammes profitent
des locutions du corpus ("faire fondre", "laisser mijoter") qu'un sac
d'unigrammes ne distingue pas de leurs constituants isoles.
PipelineRegistry.initialize() (fr+en, chargement + entrainement) mesure a
28,5 s contre ~930 s. Suite pytest complete : 46 passed en 40,6 s.
- locale_pipeline.py : `_TEXTCAT_MODEL_LENGTH` / `_TEXTCAT_NGRAM_SIZE`
explicites et commentes avec les mesures ; historique de calibration de
`_TRAINING_ITERATIONS` corrige (il attribuait le cout aux iterations)
- README : sections "Temps de demarrage" et "Limitations connues" reecrites
(la couverture d'evaluation, 26/74 techniques, y devient la limitation
bloquante ; ajout des collisions de classes observees)
- docker-compose.yml / ci.yml : commentaires citant ~930 s corriges,
start_period et timeout 1200s -> 300s (plafond pour runner lent, pas cible)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'optimiseur ne mutualisait que le pré-découpage. Il regroupe maintenant
aussi les actions SETUP (préchauffage, ébullition) entre recettes, mais
seulement quand elles sont compatibles : deux préchauffages ne fusionnent
que s'ils visent la même température détectée. Deux préchauffages à des
températures différentes restent séparés, ordonnés du plus froid au plus
chaud, et la mise en place porte une note « Four requis à N températures —
enchaîner … ». Défaut optimiste : deux préchauffages sans température
détectée fusionnent quand même (supposés compatibles).
- cooking-optimizer.ts : temperatureKey (comparaison exacte, aucune
conversion inter-représentations), isPureSetupStep / setupSignature /
extractMergedSetup en miroir du chemin merged-prep ; comparableTemp pour
l'ordre seul ; detectApplianceTemperatureConflicts → CookingPhaseView.notes
- cooking-session.service.ts : `temperatures` dans l'include + mapping
- packages/shared : CookingTemperatureView, kind « merged-setup »,
CookingTaskView.temperatures, CookingPhaseNoteView, CookingPhaseView.notes
- web : temperature-label.ts extrait de StepDescription et réutilisé ;
taskHeadline gère merged-setup ; phaseNoteText ; bandeau .cooking-phase__note
- i18n : cookingSession.task.mergedSetupLabel, cookingSession.note.*
- tests : 7 cas Mocha « setup pooling » + test DB cooking-session,
cas Cypress mode layout + scénario Gherkin
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Remanie /cuisiner : au lieu d'empiler toutes les phases, deux onglets
(Recettes, Plan) et un stepper qui parcourt les phases une à une. Chaque
tâche est cochable ; la progression est mémorisée par semaine dans
localStorage — aucun état serveur, même parti pris que le reste de la page.
- CookingSessionPage : barre d'onglets + PlanTab (stepper, nav
précédent/suivant, clic direct sur un nœud) + RecipesTab (liste des
recettes de la semaine, lien vers la fiche)
- cooking-progress.ts : CookingProgressStore (load/save localStorage,
clé batchcooking:cookingProgress:<semaine>)
- cooking-session.ts : reconcileChecked / phaseProgress (helpers purs)
- réutilise CheckboxOption pour les cartes de tâche cochables
- i18n : cookingSession.tabs / stepper / progress / recipesTab
- tests Cypress (mode layout + Gherkin) mis à jour et étendus
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le runner Forgejo `act` ne garde pas un conteneur `services:` comme *la
meme* instance d'une etape a l'autre : `prisma migrate deploy` posait le
schema dans un Postgres, puis le `beforeAll` de Mocha en frappait un vide
(« table … does not exist » juste apres « migrations successfully
applied »). Trois occurrences en une journee, dont une sur `main` apres le
merge de #18 — code identique a `ccec30b` qui, lui, etait vert.
Le job `test` demarre desormais Postgres lui-meme via `docker run -d`
(socket docker deja monte dans le conteneur de job), sur le reseau du job
decouvert dynamiquement, joignable comme `postgres` — exactement la meme
posture « vraie dependance lancee par le job » que l'etape uvicorn de
`tech-step-intent-service` juste en dessous, que `services:` ne pouvait
deja pas couvrir. Un seul conteneur, partage par toutes les etapes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le job `test` du run precedent a echoue sur un flake du conteneur Postgres
`services:` du runner act (`relation "user_profile_allergy" does not exist`
juste apres « All migrations successfully applied », 0 test execute). Aucun
fichier lie a la DB n'a change dans cette PR, et le superset feat/temperature-
metadata a fait tourner `test` vert (516 passing).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI: 7 `deep.equal` sur un `TechStepMatch` / `StepTechStepView` echouaient
(l'objet reel a une cle de plus depuis l'ajout de `temperatures`).
- tech-step-matcher.test.ts x4 : `temperatures: []`, sauf le cas
« Faire mijoter a feu doux » ou « feu doux » [16,24) est une mention
qualitative -> `qualitative: "low"`.
- recipe-tech-step-correction.test.ts x3 : `temperatures: []` (l'etape
fixture « Faire mijoter la sauce. » n'a pas de mention de temperature).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3e type de metadonnee de clause, a cote des ingredients et des ustensiles :
temperature en °C (« 180°C », « 200 degres », °F converti), numero de
thermostat (« th. 6 ») et intensite qualitative (« feu doux/moyen/vif » ->
low/medium/high).
services/tech-step-intent-service :
- `temperature_extraction.py` : `extract_temperatures(text, locale)` pur,
a base de regex (independant du modele spaCy et de l'entrainement).
- `ProcessResult` / `ProcessResponse` gagnent `temperatures` (ajout additif
au contrat ; cle `gas_mark` en snake_case sur le fil). `process()` les
extrait meme pour une locale pas encore entrainee.
- Tests `test_temperature_extraction.py` (10) ; `test_routes_process.py` :
2 assertions d'egalite stricte gagnent `"temperatures": []`, +1 cas.
apps/api :
- `IntentServiceTemperature` (mappe `gas_mark` -> `gasMark` a la frontiere).
- `TechStepMatch.temperatures` : filtrees par appartenance de span a la
clause, meme regle que les ustensiles.
- `model StepTechStepTemperature` (aucune FK — la valeur structuree EST la
donnee) + migration manuelle `20260829120000_step_tech_step_temperature`
(cascade via le TRUNCATE de `step_tech_step`, rien a ajouter a
reset-db.ts). Persistance + lecture dans `recipe.service.ts`
(`recipeInclude`, `toStepTechStepViews`) et l'include de sequence fraiche
du service de correction ; `sources.service.ts` (apercu d'import) les
fait transiter.
packages/shared : `StepTechStepTemperatureView` + `temperatures` sur
`StepTechStepView`.
apps/web :
- `splitDescriptionSegments` enrobe `splitDescriptionByTechSteps` et
redecoupe les segments non-keyword autour des spans de temperature (la
logique technique intriquee reste intacte). `?? []` tolere une payload
d'avant `temperatures`.
- `StepDescription` : surlignage `.step-temperature` + Tooltip via
`temperatureLabel` ; i18n `recipes.temperature.*`.
- Tests composants +4 (22 verts) ; pas d'UI de correction (v1, comme les
ustensiles).
Verifie : biome + tsc + `pnpm -r build` ; web 102/103 e2e (l'echec est le
flake pre-existant recipe-form.feature, sans rapport) + 49/49 composants ;
pytest temperature + routes 15/15. `pnpm --filter api test` (Postgres +
intent-service requis) non lance ici.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La route page `/admin/monitoring` et le chemin API `GET /admin/monitoring`
sont identiques : un glob `**/admin/monitoring` capturait aussi la requete
document du `cy.visit()`. Le hardcode `http://localhost:3000/...` marchait
en local (VITE_API_URL dans apps/web/.env) mais pas en CI (pas de .env ->
API en meme origine sur :5173, donc collision totale) : `cy.wait
(@getMonitoring)` timeout.
`resourceType: "fetch"` epingle l'intercept sur le seul XHR d'AdminApiClient.
Verifie en simulant la CI (sans apps/web/.env) : 16/16 specs admin verts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'admin etait une 2e app front Vite independante (apps/admin-web, port 5174,
Dockerfile nginx, service compose dedie, job CI propre) non demandee. Toute
l'UI passe dans apps/web sous le prefixe /admin ; seul le frontend est
fusionne, l'authentification admin reste entierement separee.
Front (apps/web/src) :
- pages -> pages/admin/{login,dashboard,monitoring,corrections,catalog}/,
layout -> layouts/AdminLayout.tsx, contexte + garde -> features/admin/.
- client API -> api/admin-client.ts : classe AdminApiError (evite la
collision avec ApiError), lit VITE_API_URL (plus de VITE_ADMIN_API_URL).
- routes /admin/* dans App.tsx, enveloppees d'AdminAuthProvider +
RequireAdmin -> le probe GET /admin/auth/me ne tourne que sous /admin.
- reutilise l'i18n, lib/zod-errors, services/error-message.service et le
theme SCSS de apps/web ; bloc i18n admin.* fusionne dans la locale fr
(les cles errors etaient deja toutes presentes).
- corrige une race dans CatalogPage (reponse d'un onglet precedent qui
ecrasait l'onglet courant, exposee par le double-mount StrictMode) via
un ref requestSeq.
Auth admin inchangee : table AdminUser, cookie admin_session,
ADMIN_JWT_SECRET, script create-admin.ts.
Infra :
- docker-compose : service admin-web + ADMIN_WEB_PORT supprimes (l'app
`app` sert deja le front construit).
- ADMIN_CORS_ORIGIN retire (meme origine) : env.ts, app.ts, .env.example.
- job CI "Run admin-web E2E tests" supprime ; les specs admin-* tournent
dans le job web (apps/web/cypress/e2e/admin-*.{cy.ts,feature}).
- apps/api/.env.example : ajout ADMIN_JWT_SECRET / ADMIN_INITIAL_*.
- recharts ajoute a apps/web ; pnpm-lock regenere.
- specs/backend-architecture.md : section admin mise a jour.
Verifie : biome + tsc -b (web/api) + pnpm -r build verts ; Cypress web
102/103 (l'unique echec est le flake pre-existant recipe-form.feature
"Preloads ..." de clipping headless, sans rapport) ; 16/16 specs admin ;
45/45 composants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ces suites ont ete ecrites pendant le dev des features cooking / admin /
hors-catalogue mais jamais executees (pas de Postgres dans ces sessions).
Leur 1re execution reelle sur `main` echouait — bugs dans les tests, pas
dans le code merge.
- reference.test.ts : `GET /reference/ingredients` renvoie desormais
`isPlaceholder` (toujours false) et `displayName` (toujours null) depuis
la PR #16 (champs de `IngredientView`). L'assertion `to.have.keys([...])`
exacte est mise a jour.
- cooking-session.test.ts : la fixture "pooling merged-prep" avait 2
recettes symetriques (chop -> simmer) ; apres mise en commun du chop les
deux simmer tournent dans l'unique phase de cuisson, donc aucun
`background` possible (l'optimiseur est correct, cf. le test pur
equivalent). « Tarte » recoit une etape active `mix` de plus pour que son
simmer flotte en background pendant que « Soupe » est en hold.
- admin-tech-steps.test.ts : une requete supertest ne part qu'a l'`await`/
`.then` ; la 1re requete /retrain concurrente n'etait jamais lancee, donc
le verrou process n'etait jamais tenu et la 2e recevait 200 au lieu de
409. Ajout d'un `.then(res => res, err => err)` pour la declencher avant
l'attente de 100 ms.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Quand le catalogue seede ne couvre pas un ingredient, l'utilisateur pouvait
etre bloque (creation manuelle) ou perdre silencieusement la ligne (import).
Une ligne de recette accepte desormais `placeholderName` (texte libre) au
lieu de `ingredientId` : l'API cree une ligne `Ingredient` `isPlaceholder`
(cle `placeholder:<uuid>`, `displayName`, `createdById`) dans la transaction
de la recette, et emet `ingredient.placeholder_created`. Ces lignes sont
exclues de `GET /reference/ingredients` et de `ingredient-matcher`.
Front : bouton "Ajouter << ... >>" dans l'etat vide de `IngredientPicker`
(formulaire + import), badge "a completer" sur la ligne, helper
`ingredientLabel` applique partout ou un libelle d'ingredient est rendu.
Admin : `/admin/catalog/*` (+ page `apps/admin-web`) liste les placeholders
regroupes par nom normalise, "marquer traite" (`reviewedAt`) et purge des
orphelins. La promotion en vraie entree catalogue reste manuelle.
Migration `ingredient_placeholder` ecrite a la main (Postgres indisponible).
Suites Mocha DB-backed ecrites, non executees en session ; test pur
`normalizePlaceholderName` + Cypress admin-web/web verts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR 4 du chantier admin. Board de sante temps reel des dependances.
- POST /internal/tech-steps/heartbeat (requireInternalWorker) ->
recordWorkerHeartbeat : upsert WorkerHeartbeat (cle fixe
"tech-step-llm-worker"), lastRunAt/lastResult pour un ping "job".
Schema workerHeartbeatSchema dans packages/shared.
- services/tech-step-llm-worker : api-client.postHeartbeat (best-effort,
ne throw jamais) appele au boot (index.ts), a chaque tick et apres
chaque job (scheduler.ts, avec job/ok/counts).
- admin-monitoring.service.ts + GET /admin/monitoring (requireAdmin) :
sonde active bornee (~2 s) de Postgres (SELECT 1), l'API (uptime/RSS),
tech-step-intent-service (/health), et le worker via son heartbeat.
Statut up/degraded/down/unknown ; une sonde down ne casse ni les autres
ni l'endpoint. Seuils worker : > 8 j degraded, > 21 j down.
- MonitoringView / ServiceHealthView dans packages/shared.
- Front : MonitoringPage (grille de cartes coloree par statut, re-poll
15 s), logique pure monitoring.ts, i18n admin.monitoring.*,
AdminApiClient.getMonitoring.
- Tests : Mocha admin-monitoring.test.ts (heartbeat 401/400/upsert
job+boot ; GET /admin/monitoring 401, board 4 cibles, worker unknown
sans heartbeat puis up apres) ; Cypress monitoring.cy.ts (2 verts).
Worker mocha : 6/6 toujours verts.
- specs/backend-architecture.md : section monitoring. .gitignore :
apps/admin-web/cypress/{screenshots,videos,downloads}.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Bouton "Commencer a cuisiner" dans l'en-tete du planning, actif seulement
quand la semaine affichee contient >= 1 recette ; navigue vers
/cuisiner?date=<semaine>.
- Page CookingSessionPage (/cuisiner) : consomme GET /cooking-session, rend
les phases (mise en place / cuisson / dressage), les taches mutualisees
avec badge "Mutualise" + ingredients/ustensiles resolus, et la bande
"Pendant ce temps" pour les cuissons de fond. Semaine lue depuis ?date=,
WeekNavigator en fallback ; recalcul a chaque visite (comme la liste de
courses).
- Logique pure extraite dans cooking-session.ts (composition des libelles,
formatage des quantites).
- i18n : bloc cookingSession.* + planning.startCooking.
- apiClient.getCookingPlanForWeek.
- Tests Cypress : cooking-session-page.cy.ts (layout, 3 cas), cooking-session
.feature (parcours planning -> /cuisiner), assertions bouton dans
planning-page.cy.ts ; step generique "button should be disabled" mutualise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Coeur pur du module "Calcul batch-cooking" (specs/batch-cooking-architecture.md,
jusqu'ici TODO) : optimizeCookingPlan() prend les recettes planifiees d'une
semaine et les reorganise en phases ordonnees.
- Mutualisation de la mise en place : une meme technique de decoupe (chop, peel,
mince...) appliquee au meme ingredient par >= 2 recettes est regroupee en une
seule tache "merged-prep" (les oignons de plusieurs recettes = une decoupe).
- Parallelisme : les cuissons passives (simmer, braise, bake, marinate...) sont
poussees en tache de fond des phases suivantes pendant qu'une autre recette
avance en actif.
- Fonction pure sans base (meme split matchXxx pur / loadXxx DB-backed que
ingredient-matcher / tech-step-matcher), testable en isolation.
Types partages : OptimizedCookingPlanView + schema getCookingSessionSchema.
Tests Mocha purs (6 cas) : merge, non-merge d'une decoupe solo, tache de fond,
mise a l'echelle par portions, plan vide, legende.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>