feat(onboarding): Budget page step card and its highlight #406

Merged
maximus merged 2 commits from issue-400-onboarding-budget-card into main 2026-10-09 01:10:55 +00:00
Owner

Resolves #400

Generated autonomously by /autopilot run of 2026-10-08

PR empilée sur #405 (elle-même sur #404, #403 puis #402) : elle ne montre que son propre diff.

Ce qui change pour l'utilisateur

  • Quand l'étape suivante du parcours est « Saisir un premier budget », la page Budget affiche une carte de démarrage à la place du bandeau, en tête de page avant le titre : « Étape 6 sur 6 · Saisir un premier budget ».
  • La grille s'ouvre catégories repliées. La carte dit alors « Cliquez Tout déplier pour afficher vos catégories, puis cliquez sur un mois et saisissez un montant. Entrée enregistre ; Tab enregistre et passe au mois suivant. » et entoure « Tout déplier » d'un contour qui pulse.
  • Dès qu'une ligne modifiable est visible (« Tout déplier », un groupe ouvert à la main, ou une grille déjà dépliée par la préférence du profil), la phrase devient « Cliquez sur un mois et saisissez un montant… » et l'anneau passe sur la cellule du mois courant de la première catégorie modifiable affichée. La carte ne déplie jamais la grille à la place de l'utilisateur (D12).
  • Enregistrer un montant, 0 $ compris (D13), coche l'étape en direct : la carte reste, annonce « Fait. » et passe au mode fin (« Démarrage terminé… », « Ouvrir le guide », « Fermer »). L'anneau disparaît.
  • Sans cible (profil sans transaction, aucune catégorie, premier rendu de la grille), la carte garde son titre et « Passer cette étape », sans phrase ni anneau.
  • Édition Gratuite : la page n'est pas montée (RequireFeature) et la ligne Budget est verrouillée, donc jamais de carte Budget, et le parcours se termine à 5/5. Comportement déjà assuré par l'état partagé, vérifié et épinglé par un test.

Technique

  • budgetOnboardingTarget(visibleRows, hasGroups, monthIdx), module pur src/components/budget/budgetOnboardingTarget.ts (à côté de budgetTableResults.ts) : { kind: "cell", categoryId, monthIdx } pour la première feuille visible, sinon { kind: "expandAll" } s'il existe des groupes, sinon null.
  • BudgetTable :
    • calcule une fois par section les lignes affichées, dans l'ordre affiché (reorderRows(groups.visible(section), subtotalsOnTop)), que le rendu des sections réutilise, et les passe à la fonction dans l'ordre des sections, avec new Date().getMonth() ;
    • nouvelles props onboardingTarget?: boolean et onOnboardingTargetChange?: (kind) => void ;
    • data-onboarding-target (budget-cell ou budget-expand-all, constantes exportées) et data-onboarding-highlight sur le bouton de la cellule ou sur « Tout déplier », jamais les deux ;
    • le genre remonte par un useEffect (dépendances [kind, rappel]), placé avant le retour « aucune catégorie » ;
    • l'état replié reste celui de useCollapsibleGroups (clé budget-grid-expanded), inchangé.
  • budgetCardModel(input, checklist) (onboardingPath.ts) : texte collapsed (avec labels: { expand: "reports.collapse.expandAll" }) pour expandAll, texte flat pour cell, ni phrase ni cible pour null, et { text: null, done: true, target: null } une fois l'étape cochée. BudgetCardInput et BudgetOnboardingTargetKind dans shared/types. CARD_PAGES reçoit /budget.
  • BudgetPage : un useState garde la cible remontée, et son setter (stable) est le rappel passé à la grille. OnboardingStepCard est le premier enfant, avant l'en-tête, hors du voile de chargement. La grille ne reçoit onboardingTarget que si la carte est affichée (showsStepCard) et que le modèle a une cible.
  • i18n FR et EN : onboarding.card.budget.collapsed et .flat, textes exacts de l'issue. CHANGELOG FR et EN (Ajouté). docs/architecture.md inchangé : aucune ligne n'est rendue fausse. Aucun changement Rust, aucune migration.

Preuves

  • npm run build vert (tsc + vite).
  • npx vitest run : 1844 → 1885 tests verts (103 → 108 fichiers).
    • budgetOnboardingTarget.test.ts (nouveau, +8) : groupes repliés, première feuille visible (sous-totaux au-dessus ou en dessous, ligne « (direct) »), feuille visible à côté de groupes repliés, sans groupes, aucune ligne, mois de janvier à décembre.
    • BudgetTable.render.test.tsx (nouveau, +9) : rendu statique, hook de repli remplacé par les vrais helpers purs sur un état posé par le test. Un seul élément mis en évidence, « Tout déplier » ou la cellule, dans la colonne du mois de l'horloge (figée par vi.setSystemTime) ; revenus d'abord, sous-totaux en bas ; rien sans onboardingTarget, rien sans ligne.
    • BudgetTable.wiring.test.ts (nouveau, +3) : hooks React remplacés, effets exécutés selon leurs dépendances comme React. Le genre remonte une fois au montage puis seulement à son changement, jamais pendant le rendu, et même sans mise en évidence.
    • BudgetPage.render.test.tsx (nouveau, +6) : carte avant le titre, « Étape 6 sur 6 », hors du voile de chargement, « Fait. » puis fin, profil sans transaction, pas de carte hors du mode carte.
    • BudgetPage.wiring.test.ts (nouveau, +5) : la cible remontée choisit la phrase et allume l'anneau ; jamais d'anneau sans carte ni après « Fait. » ; rappel identique d'un rendu à l'autre.
    • onboardingPath.test.ts (+9) : CARD_PAGES avec /budget ; budgetCardModel (deux textes, cible nulle, « Fait. », étape passée) ; les deux phrases résolues par la vraie instance i18n, en FR et en EN, contre le texte exact de l'issue.
    • AppShell.render.test.tsx (+1 net) : la table « bandeau tant qu'aucune carte n'est livrée » disparaît avec sa dernière ligne ; /budget gagne son test de carte, comme /import et /transactions ; édition Gratuite avec le vrai CARD_PAGES : pas de carte, fin à 5/5.
  • Passe de mutations sur l'arbre commité, une à la fois, restaurée par git checkout ; 8 fichiers de test concernés, 128 tests verts avant chaque mutation et après la dernière restauration :
# Mutation Rouges
M1 budgetOnboardingTarget rend la première ligne visible, sous-total compris 9
M2 plus de « Tout déplier » quand des groupes existent sans feuille visible 3
M3 mois décalé (getMonth() + 1) 6
M4 « Tout déplier » allumé dès que la page le demande (deux cibles) 4
M5a rappel sans dépendances (remonté à chaque rendu) 1
M5b rappel jamais appelé 2
M6 textes collapsed et flat intervertis 5
M7 CARD_PAGES sans /budget 2

8/8 rouges, arbre propre après chaque restauration.

Décisions

14 décisions consignées dans le journal du run ; les principales :

  • [MEDIUM] budgetCardModel prend aussi la liste (checklist), comme transactionsCardModel dans #405 : sans elle, pas de done, donc ni « Fait. » ni mode fin (critère d'acceptation). La signature de l'issue ne portait que { target }.
  • [MEDIUM] Mois courant = new Date().getMonth(), quelle que soit l'année affichée : le compteur compte les entrées de toute année, donc toute cellule coche l'étape, et la colonne ne saute pas au changement d'année. Écartés : janvier pour une autre année ; aucune cible hors de l'année courante.
  • [MEDIUM] Cible nulle : ni phrase ni anneau, le titre et « Passer » restent. Écartés : la phrase flat sur une page sans grille ; un modèle nul, qui laisserait la page verrouillée sans bandeau ni carte.
  • [MEDIUM] Carte hors du voile opacity-50 pointer-events-none : la grille se recharge après chaque enregistrement, au moment où la carte annonce « Fait. ». Le reste de la page garde son voile.
  • [MEDIUM] La fonction dans un module pur à côté de budgetTableResults.ts plutôt qu'exportée par BudgetTable.tsx, pour un test qui ne charge ni hook, ni préférences, ni base.
  • [MEDIUM] Deux tests légers de BudgetTable en plus du test pur, sans stockage ni hydratation : sans eux, quatre des mutations (M3, M4, M5a, M5b) restaient vertes.

Pour relire

  • src/components/budget/BudgetTable.tsx : le bloc placé avant le retour « aucune catégorie » (lignes affichées, cible, effet) ; renderTypeSection lit maintenant shownByType.
  • src/pages/BudgetPage.tsx : le diff est surtout une ré-indentation sous la nouvelle racine ; git diff -w le réduit à la carte, à l'état et aux deux props.
  • La cellule désignée peut sortir de la zone visible quand la grille défile horizontalement (octobre est à droite) : rien ne défile tout seul, l'issue ne le demande pas (choix MEDIUM). Suite possible si c'est gênant à l'usage.
  • À vérifier à la main (liste QA de #401) : profil neuf en édition Base, « Étape suivante » depuis la carte Transactions, carte et anneau sur « Tout déplier », puis sur la cellule du mois après dépliage ; montant saisi et Entrée, puis « Fait. » et la fin ; Tab enregistre et passe au mois suivant ; grille déjà dépliée par la préférence du profil, anneau directement sur la cellule ; édition Gratuite, pas de carte et 5/5 ; rendu de l'anneau sous WebKitGTK.
  • Comportement existant (D13) : cliquer une cellule puis la quitter vide enregistre 0 $ et coche l'étape.

🤖 Generated with Claude Code

Resolves #400 Generated autonomously by /autopilot run of 2026-10-08 > PR empilée sur #405 (elle-même sur #404, #403 puis #402) : elle ne montre que son propre diff. ## Ce qui change pour l'utilisateur - Quand l'étape suivante du parcours est « Saisir un premier budget », la page Budget affiche une carte de démarrage à la place du bandeau, en tête de page avant le titre : « Étape 6 sur 6 · Saisir un premier budget ». - La grille s'ouvre catégories repliées. La carte dit alors « Cliquez Tout déplier pour afficher vos catégories, puis cliquez sur un mois et saisissez un montant. Entrée enregistre ; Tab enregistre et passe au mois suivant. » et entoure « Tout déplier » d'un contour qui pulse. - Dès qu'une ligne modifiable est visible (« Tout déplier », un groupe ouvert à la main, ou une grille déjà dépliée par la préférence du profil), la phrase devient « Cliquez sur un mois et saisissez un montant… » et l'anneau passe sur la cellule du mois courant de la première catégorie modifiable affichée. La carte ne déplie jamais la grille à la place de l'utilisateur (D12). - Enregistrer un montant, 0 $ compris (D13), coche l'étape en direct : la carte reste, annonce « Fait. » et passe au mode fin (« Démarrage terminé… », « Ouvrir le guide », « Fermer »). L'anneau disparaît. - Sans cible (profil sans transaction, aucune catégorie, premier rendu de la grille), la carte garde son titre et « Passer cette étape », sans phrase ni anneau. - Édition Gratuite : la page n'est pas montée (`RequireFeature`) et la ligne Budget est verrouillée, donc jamais de carte Budget, et le parcours se termine à 5/5. Comportement déjà assuré par l'état partagé, vérifié et épinglé par un test. ## Technique - `budgetOnboardingTarget(visibleRows, hasGroups, monthIdx)`, module pur `src/components/budget/budgetOnboardingTarget.ts` (à côté de `budgetTableResults.ts`) : `{ kind: "cell", categoryId, monthIdx }` pour la première feuille visible, sinon `{ kind: "expandAll" }` s'il existe des groupes, sinon `null`. - `BudgetTable` : - calcule une fois par section les lignes affichées, dans l'ordre affiché (`reorderRows(groups.visible(section), subtotalsOnTop)`), que le rendu des sections réutilise, et les passe à la fonction dans l'ordre des sections, avec `new Date().getMonth()` ; - nouvelles props `onboardingTarget?: boolean` et `onOnboardingTargetChange?: (kind) => void` ; - `data-onboarding-target` (`budget-cell` ou `budget-expand-all`, constantes exportées) et `data-onboarding-highlight` sur le bouton de la cellule ou sur « Tout déplier », jamais les deux ; - le genre remonte par un `useEffect` (dépendances `[kind, rappel]`), placé avant le retour « aucune catégorie » ; - l'état replié reste celui de `useCollapsibleGroups` (clé `budget-grid-expanded`), inchangé. - `budgetCardModel(input, checklist)` (`onboardingPath.ts`) : texte `collapsed` (avec `labels: { expand: "reports.collapse.expandAll" }`) pour `expandAll`, texte `flat` pour `cell`, ni phrase ni cible pour `null`, et `{ text: null, done: true, target: null }` une fois l'étape cochée. `BudgetCardInput` et `BudgetOnboardingTargetKind` dans `shared/types`. `CARD_PAGES` reçoit `/budget`. - `BudgetPage` : un `useState` garde la cible remontée, et son setter (stable) est le rappel passé à la grille. `OnboardingStepCard` est le premier enfant, avant l'en-tête, hors du voile de chargement. La grille ne reçoit `onboardingTarget` que si la carte est affichée (`showsStepCard`) et que le modèle a une cible. - i18n FR et EN : `onboarding.card.budget.collapsed` et `.flat`, textes exacts de l'issue. CHANGELOG FR et EN (Ajouté). `docs/architecture.md` inchangé : aucune ligne n'est rendue fausse. Aucun changement Rust, aucune migration. ## Preuves - `npm run build` vert (tsc + vite). - `npx vitest run` : 1844 → **1885** tests verts (103 → 108 fichiers). - `budgetOnboardingTarget.test.ts` (nouveau, +8) : groupes repliés, première feuille visible (sous-totaux au-dessus ou en dessous, ligne « (direct) »), feuille visible à côté de groupes repliés, sans groupes, aucune ligne, mois de janvier à décembre. - `BudgetTable.render.test.tsx` (nouveau, +9) : rendu statique, hook de repli remplacé par les vrais helpers purs sur un état posé par le test. Un seul élément mis en évidence, « Tout déplier » ou la cellule, dans la colonne du mois de l'horloge (figée par `vi.setSystemTime`) ; revenus d'abord, sous-totaux en bas ; rien sans `onboardingTarget`, rien sans ligne. - `BudgetTable.wiring.test.ts` (nouveau, +3) : hooks React remplacés, effets exécutés selon leurs dépendances comme React. Le genre remonte une fois au montage puis seulement à son changement, jamais pendant le rendu, et même sans mise en évidence. - `BudgetPage.render.test.tsx` (nouveau, +6) : carte avant le titre, « Étape 6 sur 6 », hors du voile de chargement, « Fait. » puis fin, profil sans transaction, pas de carte hors du mode carte. - `BudgetPage.wiring.test.ts` (nouveau, +5) : la cible remontée choisit la phrase et allume l'anneau ; jamais d'anneau sans carte ni après « Fait. » ; rappel identique d'un rendu à l'autre. - `onboardingPath.test.ts` (+9) : `CARD_PAGES` avec `/budget` ; `budgetCardModel` (deux textes, cible nulle, « Fait. », étape passée) ; les deux phrases résolues par la vraie instance i18n, en FR et en EN, contre le texte exact de l'issue. - `AppShell.render.test.tsx` (+1 net) : la table « bandeau tant qu'aucune carte n'est livrée » disparaît avec sa dernière ligne ; /budget gagne son test de carte, comme /import et /transactions ; édition Gratuite avec le vrai `CARD_PAGES` : pas de carte, fin à 5/5. - Passe de mutations sur l'arbre commité, une à la fois, restaurée par `git checkout` ; 8 fichiers de test concernés, 128 tests verts avant chaque mutation et après la dernière restauration : | # | Mutation | Rouges | |---|---|---| | M1 | `budgetOnboardingTarget` rend la première ligne visible, sous-total compris | 9 | | M2 | plus de « Tout déplier » quand des groupes existent sans feuille visible | 3 | | M3 | mois décalé (`getMonth() + 1`) | 6 | | M4 | « Tout déplier » allumé dès que la page le demande (deux cibles) | 4 | | M5a | rappel sans dépendances (remonté à chaque rendu) | 1 | | M5b | rappel jamais appelé | 2 | | M6 | textes `collapsed` et `flat` intervertis | 5 | | M7 | `CARD_PAGES` sans `/budget` | 2 | **8/8 rouges**, arbre propre après chaque restauration. ## Décisions 14 décisions consignées dans le journal du run ; les principales : - [MEDIUM] `budgetCardModel` prend aussi la liste (`checklist`), comme `transactionsCardModel` dans #405 : sans elle, pas de `done`, donc ni « Fait. » ni mode fin (critère d'acceptation). La signature de l'issue ne portait que `{ target }`. - [MEDIUM] Mois courant = `new Date().getMonth()`, quelle que soit l'année affichée : le compteur compte les entrées de toute année, donc toute cellule coche l'étape, et la colonne ne saute pas au changement d'année. Écartés : janvier pour une autre année ; aucune cible hors de l'année courante. - [MEDIUM] Cible nulle : ni phrase ni anneau, le titre et « Passer » restent. Écartés : la phrase `flat` sur une page sans grille ; un modèle nul, qui laisserait la page verrouillée sans bandeau ni carte. - [MEDIUM] Carte hors du voile `opacity-50 pointer-events-none` : la grille se recharge après chaque enregistrement, au moment où la carte annonce « Fait. ». Le reste de la page garde son voile. - [MEDIUM] La fonction dans un module pur à côté de `budgetTableResults.ts` plutôt qu'exportée par `BudgetTable.tsx`, pour un test qui ne charge ni hook, ni préférences, ni base. - [MEDIUM] Deux tests légers de `BudgetTable` en plus du test pur, sans stockage ni hydratation : sans eux, quatre des mutations (M3, M4, M5a, M5b) restaient vertes. ## Pour relire - `src/components/budget/BudgetTable.tsx` : le bloc placé avant le retour « aucune catégorie » (lignes affichées, cible, effet) ; `renderTypeSection` lit maintenant `shownByType`. - `src/pages/BudgetPage.tsx` : le diff est surtout une ré-indentation sous la nouvelle racine ; `git diff -w` le réduit à la carte, à l'état et aux deux props. - La cellule désignée peut sortir de la zone visible quand la grille défile horizontalement (octobre est à droite) : rien ne défile tout seul, l'issue ne le demande pas (choix MEDIUM). Suite possible si c'est gênant à l'usage. - À vérifier à la main (liste QA de #401) : profil neuf en édition Base, « Étape suivante » depuis la carte Transactions, carte et anneau sur « Tout déplier », puis sur la cellule du mois après dépliage ; montant saisi et Entrée, puis « Fait. » et la fin ; Tab enregistre et passe au mois suivant ; grille déjà dépliée par la préférence du profil, anneau directement sur la cellule ; édition Gratuite, pas de carte et 5/5 ; rendu de l'anneau sous WebKitGTK. - Comportement existant (D13) : cliquer une cellule puis la quitter vide enregistre 0 $ et coche l'étape. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 2 commits 2026-10-08 13:24:27 +00:00
The budget grid opens collapsed and only its leaf rows take an amount,
so the card first points at "Expand all", then at the current month's
cell of the first editable row shown (D12). `budgetOnboardingTarget`
(pure, beside budgetTableResults.ts) decides it from the rows each
section shows, in the order it shows them; the collapse state stays
BudgetTable's own and the card never unfolds the grid.

BudgetTable rings that one control when the page asks
(`onboardingTarget`) and reports its kind through
`onOnboardingTargetChange`, in an effect, at mount and on change only:
the page needs it for the card's sentence before it asks for the ring.

`budgetCardModel(input, checklist)` gives the "collapsed" sentence for
"Expand all", the "flat" one for a cell, no sentence and no ring before
the grid says, and "Done." once the budget step is checked, which the
card needs to end the path. CARD_PAGES gets /budget. BudgetPage renders
the card first, outside the loading dim the grid gets after each save,
and asks for the ring only while the card shows. The Free edition never
mounts the page and never latches it: its path ends at 5/5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: changelog for the Budget step card
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m0s
d76c387fa5
Added: the getting-started card of the Budget page. architecture.md
stays as it is: no line of it is made stale (the budget folder still
holds five components, the pure module is not one).

Resolves #400

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus added the
autopilot:pending-human
label 2026-10-08 13:24:35 +00:00
Author
Owner

Auto-revue (autopilot) : APPROVE

Revue du diff de cette PR seule (16 fichiers, tête d76c387) contre le corps de #400.

Critères d'acceptation

  • budgetOnboardingTarget testée : groupes repliés, première feuille visible, sans groupes, aucune ligne. En plus : sous-totaux en bas, ligne « (direct) », catégorie sans sous-catégorie à côté de groupes repliés, mois 0 à 11.
  • budgetCardModel : les deux textes, et les deux phrases résolues par la vraie instance i18n, en FR et en EN, contre le texte exact de l'issue.
  • Profil neuf en édition Base : carte sur /budget, anneau sur « Tout déplier », puis sur la cellule du mois courant (rendu de la grille, câblage de la page). Un montant coche l'étape en direct par le canal de #402 (upsertBudgetEntry notifie déjà) ; la carte reste, annonce « Fait. » et passe au mode fin (rendu de la page).
  • Édition Gratuite : jamais de carte Budget, fin à 5/5. L'état partagé le faisait déjà ; épinglé en plus dans AppShell avec le vrai CARD_PAGES.
  • FR et EN (parité des clés vérifiée), CHANGELOG FR et EN, npm run build vert, vitest 1844 → 1885, CI frontend run 504 verte (log lu : tête d76c387, build, 108 fichiers et 1885 tests).

Caveats de l'issue

  • D12 respecté : la carte ne déplie jamais la grille ; l'état replié reste dans useCollapsibleGroups (budget-grid-expanded), inchangé.
  • Écart assumé au second caveat : la fonction pure est testée, et deux tests légers de BudgetTable s'y ajoutent, sans stockage ni hydratation (hook de repli remplacé par les vrais helpers purs). Sans eux, M3, M4, M5a et M5b restaient vertes.

Points relevés, non bloquants

  1. La cellule désignée peut sortir du champ quand la grille défile horizontalement (octobre est à droite) ; rien ne défile tout seul. Décision MEDIUM, à juger pendant la QA de #401.
  2. BudgetPage gagne une racine, pour que la carte reste hors du voile de chargement ; le diff de la page est surtout de la ré-indentation (git diff -w).
  3. budgetCardModel prend checklist en plus de { target }, comme transactionsCardModel dans #405 : écart à la signature de l'issue, consigné au journal.
  4. Ni code mort, ni secret, ni donnée personnelle dans les lignes ajoutées (balayage fait) ; commentaires en anglais ; règles dans des modules purs, composants en affichage seul.

Label laissé à status:review : seule une revue indépendante (/pr-review) pose status:approved.

## Auto-revue (autopilot) : APPROVE Revue du diff de cette PR seule (16 fichiers, tête `d76c387`) contre le corps de #400. **Critères d'acceptation** - [x] `budgetOnboardingTarget` testée : groupes repliés, première feuille visible, sans groupes, aucune ligne. En plus : sous-totaux en bas, ligne « (direct) », catégorie sans sous-catégorie à côté de groupes repliés, mois 0 à 11. - [x] `budgetCardModel` : les deux textes, et les deux phrases résolues par la vraie instance i18n, en FR et en EN, contre le texte exact de l'issue. - [x] Profil neuf en édition Base : carte sur /budget, anneau sur « Tout déplier », puis sur la cellule du mois courant (rendu de la grille, câblage de la page). Un montant coche l'étape en direct par le canal de #402 (`upsertBudgetEntry` notifie déjà) ; la carte reste, annonce « Fait. » et passe au mode fin (rendu de la page). - [x] Édition Gratuite : jamais de carte Budget, fin à 5/5. L'état partagé le faisait déjà ; épinglé en plus dans AppShell avec le vrai `CARD_PAGES`. - [x] FR et EN (parité des clés vérifiée), CHANGELOG FR et EN, `npm run build` vert, vitest 1844 → 1885, CI frontend run 504 verte (log lu : tête `d76c387`, build, 108 fichiers et 1885 tests). **Caveats de l'issue** - D12 respecté : la carte ne déplie jamais la grille ; l'état replié reste dans `useCollapsibleGroups` (`budget-grid-expanded`), inchangé. - Écart assumé au second caveat : la fonction pure est testée, et deux tests légers de `BudgetTable` s'y ajoutent, sans stockage ni hydratation (hook de repli remplacé par les vrais helpers purs). Sans eux, M3, M4, M5a et M5b restaient vertes. **Points relevés, non bloquants** 1. La cellule désignée peut sortir du champ quand la grille défile horizontalement (octobre est à droite) ; rien ne défile tout seul. Décision MEDIUM, à juger pendant la QA de #401. 2. `BudgetPage` gagne une racine, pour que la carte reste hors du voile de chargement ; le diff de la page est surtout de la ré-indentation (`git diff -w`). 3. `budgetCardModel` prend `checklist` en plus de `{ target }`, comme `transactionsCardModel` dans #405 : écart à la signature de l'issue, consigné au journal. 4. Ni code mort, ni secret, ni donnée personnelle dans les lignes ajoutées (balayage fait) ; commentaires en anglais ; règles dans des modules purs, composants en affichage seul. Label laissé à `status:review` : seule une revue indépendante (`/pr-review`) pose `status:approved`.
Author
Owner

Revue /pr-review : APPROVE

Résumé : la carte Budget, la cible calculée par la grille et budgetCardModel tiennent leur contrat sur le parcours nominal, et ma passe de mutations le confirme (16/16 rouges sous le fuseau local). Un état figé touche le cas « profil sans transaction », que le corps de la PR annonce couvert. Non bloquant (phrase trompeuse, cas rare), mais le correctif tient en une ligne : à appliquer en tête de pile avant /merge-stack.

Exécuté (copie hors dépôt, tête d76c387)

  • tsc --noEmit vert ; vitest run : 108 fichiers, 1885 tests verts. CI run 504 lue dans les logs : checkout de d76c387, build, 108 fichiers et 1885 tests.
  • Fermeture : la timeline de #400 porte un pull_ref de #406 en closes, et le commit d76c387 porte Resolves #400 : l'issue se ferme aussi par un merge ff local.

Mutations du relecteur

Suite complète à chaque mutation, restauration par git checkout, arbre propre vérifié après chacune.

# Mutation Rouges
R1 cellule cerclée sans que la page le demande 1
R2 genre remonté pendant le rendu, hors effet 2
R3 dépendances de l'effet à [] (montage seulement) 1
R4 effet placé après le retour « aucune catégorie » 1
R5 cible calculée sur les lignes brutes, repli ignoré 5
R6 sections parcourues dépenses d'abord 1
R7 anneau demandé sans carte à l'écran 1
R8 carte déplacée dans le voile de chargement 2
R9 la page ne passe plus la liste (pas de « Fait. ») 2
R10 anneau gardé après « Fait. » 2
R11 « Tout déplier » même sans groupe 2
R12 mois lu en UTC (getUTCMonth) 1 sous America/Toronto, 0 sous TZ=UTC
R13 « Tout déplier » cerclé sans que la page le demande 3
R14 test du mois retiré (toute la ligne cerclée) 6
R15 nouveau rappel à chaque rendu 1
R16 la phrase « collapsed » nomme « Tout replier » 2

Suggestions (non bloquantes)

1. Cible figée quand la grille quitte la page (profil sans transaction) : src/pages/BudgetPage.tsx:35 et :47.

gridTarget ne revient jamais à null quand BudgetTable est démonté. Or, en production, useBudget reçoit les lignes avant le nombre de transactions : ses deux effets de montage (src/hooks/useBudget.ts:426 et :433) passent par le verrou FIFO de db.ts, et refreshData entre dans la file avant getTransactionCount. SET_DATA part donc pendant que totalInDb vaut encore null. La grille se monte avec les catégories du seed (racines non saisissables, donc repliées) et remonte expandAll. Ensuite totalInDb = 0 la remplace par l'état vide. La carte garde alors « Cliquez Tout déplier pour afficher vos catégories, puis cliquez sur un mois… » au-dessus de « Importez d'abord… », sans aucune grille sur la page.

  • Le corps de la PR annonce l'inverse (« Sans cible (profil sans transaction…) … sans phrase ni anneau »), tout comme shared/types/index.ts:1037 et onboardingPath.ts:391 (« no grid on the page »). Le test BudgetPage.render.test.tsx:173 ne peut pas voir ce défaut : un rendu statique n'exécute aucun effet, donc la grille n'y remonte jamais rien.
  • Reproduit dans le harnais de BudgetPage.wiring.test.ts, avec un test jetable hors dépôt. Profil aux étapes 3 à 5 passées, totalInDb: null avec des lignes, la grille remonte expandAll. Puis totalInDb: 0 : la grille disparaît, et model.text vaut toujours onboarding.card.budget.collapsed. La séquence des requêtes est déduite du code (verrou FIFO et ordre des effets). La conséquence est reproduite dans le harnais, pas dans l'app.
  • Le défaut est invisible en dev. Sous StrictMode, le premier refreshData est jeté (fetchIdRef) et le second passe après le compte, donc la grille est démontée avant d'avoir reçu ses lignes. Seul un build de production le montre, et la liste QA de #401 ne couvre pas ce cas.
  • Correctif vérifié dans la copie : avec budgetCardModel({ target: isEmptyProfile ? null : gridTarget }, onboarding.checklist), le test jetable passe et les 7 fichiers de src/pages/ restent verts. Autre voie : un nettoyage d'effet dans BudgetTable qui remonte null au démontage. Dans les deux cas, un test de câblage doit rejouer cette séquence.
  • Le cas n'est atteignable que si la carte Budget s'affiche sans transaction, donc après des étapes passées : d'où « non bloquant ».

2. Épingle du mois aveugle sous TZ=UTC : src/components/budget/BudgetTable.render.test.tsx:175-178.

« the cell follows the clock's month » règle l'horloge à 23 h le 31 décembre. Cette heure ne bascule sur un autre mois en UTC qu'à l'ouest de Greenwich : R12 rougit sous America/Toronto et reste verte sous TZ=UTC, le fuseau du runner. Le dépôt a déjà le motif dans useImportWizard.test.ts:153 (#356) : process.env.TZ = "America/Toronto" en beforeAll, restauré en afterAll. Vérifié dans la copie : avec cette épingle, R12 rougit sous TZ=UTC (1 rouge), et sans la mutation la suite reste verte.

3. Cellule hors champ à la taille de fenêtre par défaut : l'auteur l'a déjà relevé, voici le chiffrage. La fenêtre fait 1200 px (tauri.conf.json), la barre latérale w-60 240 px, et main a un p-6. Il reste environ 912 px visibles pour une grille d'au moins 1160 px (140 + 90 + 90 + 12 × 70), et la colonne catégorie s'élargit avec les noms du seed. D'août à décembre, l'anneau de la cellule tombe donc hors champ à droite, et rien ne défile. La phrase reste juste (« cliquez sur un mois »), d'où « non bloquant ». À trancher pendant la QA de #401 : en octobre, le cas se présentera d'office.

Vérifié sans remarque

  • D12 tenu : la grille ne se déplie jamais seule ; useCollapsibleGroups et la clé budget-grid-expanded sont inchangés.
  • Le seed n'a que des racines non saisissables, donc un profil neuf voit d'abord « Tout déplier », puis « Paie régulière » une fois la grille dépliée. buildBudgetYearRows n'émet jamais de parent sans feuille, donc « Tout replier » ne peut pas recevoir l'anneau.
  • Ordre des hooks : l'effet précède le retour « aucune catégorie » (R4 rougit).
  • Pas de boucle de rendu : setGridTarget est stable, et l'effet ne repart qu'au changement de genre.
  • Édition Gratuite : la ligne Budget est verrouillée, elle n'est jamais l'étape suivante, donc /budget n'est jamais verrouillée en mode carte. Le test AppShell le prouve avec le vrai CARD_PAGES.
  • i18n : les clés existent en FR et en EN, les phrases sont celles du corps de l'issue, et « Cliquez {{expand}} » suit les phrases de la carte Import. CHANGELOG FR et EN sous Ajouté/Added.
  • Aucun .only/.skip, aucun secret ni donnée personnelle dans les lignes ajoutées (dépôt public). Commits conventionnels, ni Rust ni migration.
  • Le tip de la pile (#407) ne touche ni BudgetPage.tsx ni src/components/budget/ : la suggestion 1 reste vraie sur le tip cumulé.

Actions faites

Commentaire posté, et #400 passée de status:review à status:approved. La copie /tmp/pr406-review et les fichiers /tmp/pr406-* sont supprimés en fin de revue.

## Revue `/pr-review` : APPROVE **Résumé** : la carte Budget, la cible calculée par la grille et `budgetCardModel` tiennent leur contrat sur le parcours nominal, et ma passe de mutations le confirme (16/16 rouges sous le fuseau local). Un état figé touche le cas « profil sans transaction », que le corps de la PR annonce couvert. Non bloquant (phrase trompeuse, cas rare), mais le correctif tient en une ligne : à appliquer en tête de pile avant `/merge-stack`. ### Exécuté (copie hors dépôt, tête `d76c387`) - `tsc --noEmit` vert ; `vitest run` : 108 fichiers, 1885 tests verts. CI run 504 lue dans les logs : checkout de `d76c387`, build, 108 fichiers et 1885 tests. - Fermeture : la timeline de #400 porte un `pull_ref` de #406 en `closes`, et le commit `d76c387` porte `Resolves #400` : l'issue se ferme aussi par un merge ff local. ### Mutations du relecteur Suite complète à chaque mutation, restauration par `git checkout`, arbre propre vérifié après chacune. | # | Mutation | Rouges | |---|---|---| | R1 | cellule cerclée sans que la page le demande | 1 | | R2 | genre remonté pendant le rendu, hors effet | 2 | | R3 | dépendances de l'effet à `[]` (montage seulement) | 1 | | R4 | effet placé après le retour « aucune catégorie » | 1 | | R5 | cible calculée sur les lignes brutes, repli ignoré | 5 | | R6 | sections parcourues dépenses d'abord | 1 | | R7 | anneau demandé sans carte à l'écran | 1 | | R8 | carte déplacée dans le voile de chargement | 2 | | R9 | la page ne passe plus la liste (pas de « Fait. ») | 2 | | R10 | anneau gardé après « Fait. » | 2 | | R11 | « Tout déplier » même sans groupe | 2 | | R12 | mois lu en UTC (`getUTCMonth`) | 1 sous `America/Toronto`, **0 sous `TZ=UTC`** | | R13 | « Tout déplier » cerclé sans que la page le demande | 3 | | R14 | test du mois retiré (toute la ligne cerclée) | 6 | | R15 | nouveau rappel à chaque rendu | 1 | | R16 | la phrase « collapsed » nomme « Tout replier » | 2 | ### Suggestions (non bloquantes) **1. Cible figée quand la grille quitte la page (profil sans transaction)** : `src/pages/BudgetPage.tsx:35` et `:47`. `gridTarget` ne revient jamais à `null` quand `BudgetTable` est démonté. Or, en production, `useBudget` reçoit les lignes avant le nombre de transactions : ses deux effets de montage (`src/hooks/useBudget.ts:426` et `:433`) passent par le verrou FIFO de `db.ts`, et `refreshData` entre dans la file avant `getTransactionCount`. `SET_DATA` part donc pendant que `totalInDb` vaut encore `null`. La grille se monte avec les catégories du seed (racines non saisissables, donc repliées) et remonte `expandAll`. Ensuite `totalInDb = 0` la remplace par l'état vide. La carte garde alors « Cliquez Tout déplier pour afficher vos catégories, puis cliquez sur un mois… » au-dessus de « Importez d'abord… », sans aucune grille sur la page. - Le corps de la PR annonce l'inverse (« Sans cible (profil sans transaction…) … sans phrase ni anneau »), tout comme `shared/types/index.ts:1037` et `onboardingPath.ts:391` (« no grid on the page »). Le test `BudgetPage.render.test.tsx:173` ne peut pas voir ce défaut : un rendu statique n'exécute aucun effet, donc la grille n'y remonte jamais rien. - Reproduit dans le harnais de `BudgetPage.wiring.test.ts`, avec un test jetable hors dépôt. Profil aux étapes 3 à 5 passées, `totalInDb: null` avec des lignes, la grille remonte `expandAll`. Puis `totalInDb: 0` : la grille disparaît, et `model.text` vaut toujours `onboarding.card.budget.collapsed`. La séquence des requêtes est déduite du code (verrou FIFO et ordre des effets). La conséquence est reproduite dans le harnais, pas dans l'app. - Le défaut est invisible en dev. Sous StrictMode, le premier `refreshData` est jeté (`fetchIdRef`) et le second passe après le compte, donc la grille est démontée avant d'avoir reçu ses lignes. Seul un build de production le montre, et la liste QA de #401 ne couvre pas ce cas. - Correctif vérifié dans la copie : avec `budgetCardModel({ target: isEmptyProfile ? null : gridTarget }, onboarding.checklist)`, le test jetable passe et les 7 fichiers de `src/pages/` restent verts. Autre voie : un nettoyage d'effet dans `BudgetTable` qui remonte `null` au démontage. Dans les deux cas, un test de câblage doit rejouer cette séquence. - Le cas n'est atteignable que si la carte Budget s'affiche sans transaction, donc après des étapes passées : d'où « non bloquant ». **2. Épingle du mois aveugle sous `TZ=UTC`** : `src/components/budget/BudgetTable.render.test.tsx:175-178`. « the cell follows the clock's month » règle l'horloge à 23 h le 31 décembre. Cette heure ne bascule sur un autre mois en UTC qu'à l'ouest de Greenwich : R12 rougit sous `America/Toronto` et reste verte sous `TZ=UTC`, le fuseau du runner. Le dépôt a déjà le motif dans `useImportWizard.test.ts:153` (#356) : `process.env.TZ = "America/Toronto"` en `beforeAll`, restauré en `afterAll`. Vérifié dans la copie : avec cette épingle, R12 rougit sous `TZ=UTC` (1 rouge), et sans la mutation la suite reste verte. **3. Cellule hors champ à la taille de fenêtre par défaut** : l'auteur l'a déjà relevé, voici le chiffrage. La fenêtre fait 1200 px (`tauri.conf.json`), la barre latérale `w-60` 240 px, et `main` a un `p-6`. Il reste environ 912 px visibles pour une grille d'au moins 1160 px (140 + 90 + 90 + 12 × 70), et la colonne catégorie s'élargit avec les noms du seed. D'août à décembre, l'anneau de la cellule tombe donc hors champ à droite, et rien ne défile. La phrase reste juste (« cliquez sur un mois »), d'où « non bloquant ». À trancher pendant la QA de #401 : en octobre, le cas se présentera d'office. ### Vérifié sans remarque - D12 tenu : la grille ne se déplie jamais seule ; `useCollapsibleGroups` et la clé `budget-grid-expanded` sont inchangés. - Le seed n'a que des racines non saisissables, donc un profil neuf voit d'abord « Tout déplier », puis « Paie régulière » une fois la grille dépliée. `buildBudgetYearRows` n'émet jamais de parent sans feuille, donc « Tout replier » ne peut pas recevoir l'anneau. - Ordre des hooks : l'effet précède le retour « aucune catégorie » (R4 rougit). - Pas de boucle de rendu : `setGridTarget` est stable, et l'effet ne repart qu'au changement de genre. - Édition Gratuite : la ligne Budget est verrouillée, elle n'est jamais l'étape suivante, donc `/budget` n'est jamais verrouillée en mode carte. Le test AppShell le prouve avec le vrai `CARD_PAGES`. - i18n : les clés existent en FR et en EN, les phrases sont celles du corps de l'issue, et « Cliquez {{expand}} » suit les phrases de la carte Import. CHANGELOG FR et EN sous Ajouté/Added. - Aucun `.only`/`.skip`, aucun secret ni donnée personnelle dans les lignes ajoutées (dépôt public). Commits conventionnels, ni Rust ni migration. - Le tip de la pile (#407) ne touche ni `BudgetPage.tsx` ni `src/components/budget/` : la suggestion 1 reste vraie sur le tip cumulé. ### Actions faites Commentaire posté, et #400 passée de `status:review` à `status:approved`. La copie `/tmp/pr406-review` et les fichiers `/tmp/pr406-*` sont supprimés en fin de revue.
maximus changed target branch from issue-399-onboarding-transactions-card to main 2026-10-09 01:10:49 +00:00
maximus merged commit d76c387fa5 into main 2026-10-09 01:10:55 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#406
No description provided.