feat(onboarding): step card with highlight, and the Import page's card #404

Merged
maximus merged 3 commits from issue-398-onboarding-step-card-import into main 2026-10-09 01:10:40 +00:00
Owner

Resolves #398

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

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

Ce qui change pour l'utilisateur

  • Sur la page Import, tant que l'étape suivante du parcours s'y fait (dossier, source, premier import), une carte d'étape remplace le bandeau, en tête de page avant le titre. Elle affiche « Étape n sur N » puis l'étape, dit quoi faire à chaque écran de l'assistant et entoure le contrôle à utiliser d'un contour en couleur d'accent qui pulse trois fois, et pas du tout quand le système demande de réduire les animations.
  • Elle suit l'assistant : « Choisir un dossier », « Créer un dossier d'exemple », la première carte de banque, puis le bouton suivant (Aperçu, Vérifier les doublons, Confirmer, Importer), et « Import en cours… » pendant l'import.
  • Elle reste affichée quand les étapes 3 et 4 se cochent : « Fait. », annoncé par une zone role="status", puis « Étape suivante : Classer une transaction à la main → », ou la fin du parcours. Après « Terminé » ou « Annuler », elle garde « Fait. ».
  • Un import qui n'ajoute aucune transaction (insertion en échec, doublons seuls) n'annonce pas « Fait. » : la carte dit « L'import n'a ajouté aucune transaction ; le rapport ci-dessous dit pourquoi. »
  • « Passer cette étape » marque l'étape comme passée ; la liste du bandeau montre alors « Passée » et « Reprendre ».
  • Une arrivée par un lien « Étape suivante » met le focus du clavier sur le titre de la carte ; une visite ordinaire ne change rien.
  • Corrigé (#384) : plus de flash de l'état vide « Aucun dossier d'import » à l'ouverture de la page, ni après « Annuler » ou « Terminé ».

Technique

  • StepCard (props seules) et OnboardingStepCard (connectée, seule pièce de la carte à lire useOnboarding()), dans src/components/onboarding/. La carte se rend quand mode === "card" et cardPage === pathname (showsStepCard) : c'est le verrou qui la garde après sa réussite, pas nextStep. Le focus va au titre une fois par arrivée (garde sur location.key, contre le double effet de StrictMode), puis navigate(pathname, { replace: true, state: null }).
  • onboardingPath.ts : CARD_PAGES reçoit /import ; cardStep (l'étape montrée et « n sur N »), cardNext (lien, fin ou attente), importCardModel (la table de l'issue, dans l'ordre) et onboardingTargetAttributes.
  • ImportCardInput dans shared/types, avec folderName en plus (voir Décisions).
  • La cible passe par props : onboardingTarget?: boolean sur EmptyStateAction (via ImportEmptyState), sur SourceList (première carte seulement, par SourceCard) et sur WizardNavigation (bouton suivant). Le composant rend data-onboarding-target="<clé fixe>" et data-onboarding-highlight="true". ImportFolderConfig ne reçoit pas de cible.
  • styles.css : outline: 3px solid var(--accent); outline-offset: 2px, pulsation box-shadow de 1,2 s × 3 = 3,6 s, animation: none sous prefers-reduced-motion: reduce. --accent atteint 3:1 dans les deux thèmes (clair : 3,27 sur le fond, 3,44 sur les cartes ; sombre : 6,49 et 5,37), donc pas de --onboarding-ring.
  • useImportWizard : importFolderLoaded, posé dans le finally de la lecture du dossier au montage (après le premier scan) et porté par RESET. L'état vide, la liste des sources et la carte (folderLoaded) l'attendent.
  • ImportPage construit l'entrée, rend OnboardingStepCard en premier enfant et ne passe la cible que pendant que la carte est affichée.
  • i18n FR et EN (onboarding.card.*), CHANGELOG FR et EN (Ajouté, Corrigé), docs/architecture.md (seulement les lignes que ce changement rendait fausses).

Preuves

  • npm run build vert ; npx vitest run : 1727 → 1814 tests verts (94 → 101 fichiers). Aucune modification Rust.
  • Nouveaux tests : StepCard.render.test.tsx ; OnboardingStepCard.test.tsx (hooks React simulés, effet de focus lancé à la main) ; ImportPage.render.test.tsx (une seule cible par état, la phrase nomme le bouton mis en évidence, rien avant la lecture du dossier, aucune cible sans carte) ; SourceList.test.tsx ; WizardNavigation.test.tsx ; useImportWizard.mount.test.ts (le finally) ; styles.contrast.test.ts (contraste lu dans styles.css, 3 cycles en 5 s au plus, garde de mouvement réduit). Ajouts dans onboardingPath.test.ts (chaque ligne de la table, ordre compris), EmptyState.test.tsx, ImportEmptyState.test.tsx et useImportWizard.test.ts (RESET).
  • Passe de mutations sur l'arbre commité, une mutation à la fois, git checkout après chacune : 10/10 rouges.
# Mutation Fichier Tests rouges
M1 « Fait. » pendant l'import, une fois la ligne imported_files comptée onboardingPath.ts 2/75
M2 report avec 0 transaction lu comme « Fait. » onboardingPath.ts 2/75
M3 carte pilotée par nextStep au lieu du verrou OnboardingStepCard.tsx 6/26
M4 zone role="status" rendue seulement après la réussite StepCard.tsx 1/28
M5 toutes les cartes de source mises en évidence SourceList.tsx 2/18
M6 RESET perd importFolderLoaded useImportWizard.ts 1/12
M7 importFolderLoaded posé dans le try, pas dans le finally useImportWizard.ts 1/4
M8 focus sans garde d'arrivée (consommé à chaque passage) OnboardingStepCard.tsx 1/11
M9 pulsation maintenue sous prefers-reduced-motion styles.css 1/7
M10 cible mise en évidence sans carte à l'écran ImportPage.tsx 1/15

Décisions

Détail et alternatives écartées dans le journal du run.

  • La dernière ligne de la table (« Étapes 3 et 4 déjà cochées, hors assistant ») ne pouvait jamais s'appliquer à sa place : les lignes au-dessus couvrent toutes les étapes de l'assistant. Lue comme « assistant au repos », elle est évaluée juste après « Aucun dossier ». À sa place littérale, après « Terminé », la carte redemandait de cliquer sur la carte de la banque et perdait « Étape suivante ».
  • L'étape montrée est la première ni faite ni passée (comme nextStep) : avec « non faite » au sens strict, « Passer » sur le dossier remontrait la même étape, en boucle.
  • « Passer » est caché aussi quand l'étape montrée est déjà faite selon la liste (second passage dans l'assistant, import de doublons seuls) : sinon une préférence « passée » resterait sur une étape faite.
  • « Étape n sur N » et le mode fin attendent budgetReady, comme le compteur du bandeau et bannerMode.
  • Après « Fait. », rien ne s'affiche tant que l'étape suivante est encore une étape de la page (la relecture de 150 ms n'est pas arrivée) : pas de lien vers la page même.
  • ImportCardInput.folderName est ajouté pour {{folder}} : le modèle porte toutes ses interpolations, et la page ne passe que lui.
  • La page lit useOnboarding() (la liste, entrée du modèle) et ne passe la cible que si la carte est affichée.
  • La clé de data-onboarding-target est fixe par contrôle (empty-state-action, source-card, wizard-next), la propriété étant un booléen.
  • Le guide utilisateur et les clés docs.* restent à #401 (issue 6 du plan).

Pour relire

  • Commencer par importCardModel et son test (onboardingPath.ts), puis OnboardingStepCard, puis ImportPage et son test de rendu.
  • Non gardé par les tests : le rendu réel du contour et de la pulsation dans les webviews (WebView2, WebKitGTK), le focus réel du titre, et l'annonce de « Fait. » par NVDA ou Orca ; c'est la liste QA de #401.
  • Reste un micro-flash hors périmètre : la ligne « Aucun dossier configuré » de la carte du dossier (ImportFolderConfig) avant la lecture. L'issue ne fait attendre que l'état vide et la carte.
  • AppShell.render.test.tsx est adapté : il épinglait /import comme page sans carte.

🤖 Generated with Claude Code

Resolves #398 Generated autonomously by /autopilot run of 2026-10-08 > PR empilée sur #403 (elle-même sur #402) : elle ne montre que son propre diff. ## Ce qui change pour l'utilisateur - Sur la page Import, tant que l'étape suivante du parcours s'y fait (dossier, source, premier import), une carte d'étape remplace le bandeau, en tête de page avant le titre. Elle affiche « Étape n sur N » puis l'étape, dit quoi faire à chaque écran de l'assistant et entoure le contrôle à utiliser d'un contour en couleur d'accent qui pulse trois fois, et pas du tout quand le système demande de réduire les animations. - Elle suit l'assistant : « Choisir un dossier », « Créer un dossier d'exemple », la première carte de banque, puis le bouton suivant (Aperçu, Vérifier les doublons, Confirmer, Importer), et « Import en cours… » pendant l'import. - Elle reste affichée quand les étapes 3 et 4 se cochent : « Fait. », annoncé par une zone `role="status"`, puis « Étape suivante : Classer une transaction à la main → », ou la fin du parcours. Après « Terminé » ou « Annuler », elle garde « Fait. ». - Un import qui n'ajoute aucune transaction (insertion en échec, doublons seuls) n'annonce pas « Fait. » : la carte dit « L'import n'a ajouté aucune transaction ; le rapport ci-dessous dit pourquoi. » - « Passer cette étape » marque l'étape comme passée ; la liste du bandeau montre alors « Passée » et « Reprendre ». - Une arrivée par un lien « Étape suivante » met le focus du clavier sur le titre de la carte ; une visite ordinaire ne change rien. - Corrigé (#384) : plus de flash de l'état vide « Aucun dossier d'import » à l'ouverture de la page, ni après « Annuler » ou « Terminé ». ## Technique - `StepCard` (props seules) et `OnboardingStepCard` (connectée, seule pièce de la carte à lire `useOnboarding()`), dans `src/components/onboarding/`. La carte se rend quand `mode === "card"` et `cardPage === pathname` (`showsStepCard`) : c'est le verrou qui la garde après sa réussite, pas `nextStep`. Le focus va au titre une fois par arrivée (garde sur `location.key`, contre le double effet de StrictMode), puis `navigate(pathname, { replace: true, state: null })`. - `onboardingPath.ts` : `CARD_PAGES` reçoit `/import` ; `cardStep` (l'étape montrée et « n sur N »), `cardNext` (lien, fin ou attente), `importCardModel` (la table de l'issue, dans l'ordre) et `onboardingTargetAttributes`. - `ImportCardInput` dans `shared/types`, avec `folderName` en plus (voir Décisions). - La cible passe par props : `onboardingTarget?: boolean` sur `EmptyStateAction` (via `ImportEmptyState`), sur `SourceList` (première carte seulement, par `SourceCard`) et sur `WizardNavigation` (bouton suivant). Le composant rend `data-onboarding-target="<clé fixe>"` et `data-onboarding-highlight="true"`. `ImportFolderConfig` ne reçoit pas de cible. - `styles.css` : `outline: 3px solid var(--accent); outline-offset: 2px`, pulsation `box-shadow` de 1,2 s × 3 = 3,6 s, `animation: none` sous `prefers-reduced-motion: reduce`. `--accent` atteint 3:1 dans les deux thèmes (clair : 3,27 sur le fond, 3,44 sur les cartes ; sombre : 6,49 et 5,37), donc pas de `--onboarding-ring`. - `useImportWizard` : `importFolderLoaded`, posé dans le `finally` de la lecture du dossier au montage (après le premier scan) et porté par `RESET`. L'état vide, la liste des sources et la carte (`folderLoaded`) l'attendent. - `ImportPage` construit l'entrée, rend `OnboardingStepCard` en premier enfant et ne passe la cible que pendant que la carte est affichée. - i18n FR et EN (`onboarding.card.*`), CHANGELOG FR et EN (Ajouté, Corrigé), `docs/architecture.md` (seulement les lignes que ce changement rendait fausses). ## Preuves - `npm run build` vert ; `npx vitest run` : 1727 → 1814 tests verts (94 → 101 fichiers). Aucune modification Rust. - Nouveaux tests : `StepCard.render.test.tsx` ; `OnboardingStepCard.test.tsx` (hooks React simulés, effet de focus lancé à la main) ; `ImportPage.render.test.tsx` (une seule cible par état, la phrase nomme le bouton mis en évidence, rien avant la lecture du dossier, aucune cible sans carte) ; `SourceList.test.tsx` ; `WizardNavigation.test.tsx` ; `useImportWizard.mount.test.ts` (le `finally`) ; `styles.contrast.test.ts` (contraste lu dans `styles.css`, 3 cycles en 5 s au plus, garde de mouvement réduit). Ajouts dans `onboardingPath.test.ts` (chaque ligne de la table, ordre compris), `EmptyState.test.tsx`, `ImportEmptyState.test.tsx` et `useImportWizard.test.ts` (`RESET`). - Passe de mutations sur l'arbre commité, une mutation à la fois, `git checkout` après chacune : **10/10 rouges**. | # | Mutation | Fichier | Tests rouges | |---|---|---|---| | M1 | « Fait. » pendant l'import, une fois la ligne `imported_files` comptée | `onboardingPath.ts` | 2/75 | | M2 | `report` avec 0 transaction lu comme « Fait. » | `onboardingPath.ts` | 2/75 | | M3 | carte pilotée par `nextStep` au lieu du verrou | `OnboardingStepCard.tsx` | 6/26 | | M4 | zone `role="status"` rendue seulement après la réussite | `StepCard.tsx` | 1/28 | | M5 | toutes les cartes de source mises en évidence | `SourceList.tsx` | 2/18 | | M6 | `RESET` perd `importFolderLoaded` | `useImportWizard.ts` | 1/12 | | M7 | `importFolderLoaded` posé dans le `try`, pas dans le `finally` | `useImportWizard.ts` | 1/4 | | M8 | focus sans garde d'arrivée (consommé à chaque passage) | `OnboardingStepCard.tsx` | 1/11 | | M9 | pulsation maintenue sous `prefers-reduced-motion` | `styles.css` | 1/7 | | M10 | cible mise en évidence sans carte à l'écran | `ImportPage.tsx` | 1/15 | ## Décisions Détail et alternatives écartées dans le journal du run. - La dernière ligne de la table (« Étapes 3 et 4 déjà cochées, hors assistant ») ne pouvait jamais s'appliquer à sa place : les lignes au-dessus couvrent toutes les étapes de l'assistant. Lue comme « assistant au repos », elle est évaluée juste après « Aucun dossier ». À sa place littérale, après « Terminé », la carte redemandait de cliquer sur la carte de la banque et perdait « Étape suivante ». - L'étape montrée est la première ni faite ni passée (comme `nextStep`) : avec « non faite » au sens strict, « Passer » sur le dossier remontrait la même étape, en boucle. - « Passer » est caché aussi quand l'étape montrée est déjà faite selon la liste (second passage dans l'assistant, import de doublons seuls) : sinon une préférence « passée » resterait sur une étape faite. - « Étape n sur N » et le mode fin attendent `budgetReady`, comme le compteur du bandeau et `bannerMode`. - Après « Fait. », rien ne s'affiche tant que l'étape suivante est encore une étape de la page (la relecture de 150 ms n'est pas arrivée) : pas de lien vers la page même. - `ImportCardInput.folderName` est ajouté pour `{{folder}}` : le modèle porte toutes ses interpolations, et la page ne passe que lui. - La page lit `useOnboarding()` (la liste, entrée du modèle) et ne passe la cible que si la carte est affichée. - La clé de `data-onboarding-target` est fixe par contrôle (`empty-state-action`, `source-card`, `wizard-next`), la propriété étant un booléen. - Le guide utilisateur et les clés `docs.*` restent à #401 (issue 6 du plan). ## Pour relire - Commencer par `importCardModel` et son test (`onboardingPath.ts`), puis `OnboardingStepCard`, puis `ImportPage` et son test de rendu. - Non gardé par les tests : le rendu réel du contour et de la pulsation dans les webviews (WebView2, WebKitGTK), le focus réel du titre, et l'annonce de « Fait. » par NVDA ou Orca ; c'est la liste QA de #401. - Reste un micro-flash hors périmètre : la ligne « Aucun dossier configuré » de la carte du dossier (`ImportFolderConfig`) avant la lecture. L'issue ne fait attendre que l'état vide et la carte. - `AppShell.render.test.tsx` est adapté : il épinglait /import comme page sans carte. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 3 commits 2026-10-08 12:37:14 +00:00
The wizard starts with `importFolder` null for lack of reading, which the
Import page took for "no folder": the "No import folder" empty state
flashed before the stored folder came back (#384).

`importFolderLoaded` is set in the `finally` of the mount read, after the
first scan, so neither variant of the empty state flashes, and it is kept
by RESET, which rebuilds from the initial state while the mount read
never runs again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the page of the step in progress, a card takes the place of the
banner. `StepCard` is driven by props: an h2 title that takes the focus
("Step n of N", then the step), the page's sentence, a status region
always rendered that receives "Done.", "Skip this step" until the step is
done, then "Next step" or the end of the path. `OnboardingStepCard` is
the only piece that reads the path: it renders on the latched card page
only, so the card survives its own success, focuses its title once per
arrival by "Next step" and consumes the navigation state.

The control to use carries `data-onboarding-highlight`, drawn in
styles.css as a 3px accent outline with a pulse bounded to three cycles
and none under prefers-reduced-motion; a test reads the stylesheet and
checks 3:1 against the background and the cards in both themes. The
target is passed by props to EmptyState's primary action, the first
SourceList card and WizardNavigation's next button, never queried.

`importCardModel` reads the wizard and the checklist in order: importing
before the counters, a report that added nothing as a failure, and the
"steps 3 and 4 checked" row at rest, where it can apply. CARD_PAGES gets
/import; the Import page builds the input, renders the card first and
highlights the model's target only while the card shows, and its empty
state waits for the folder to be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: changelog and architecture for the Import step card
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m55s
ee26fe0888
Added: the getting-started card of the Import page. Fixed: the "No import
folder" empty state no longer flashes while the folder is read.
architecture.md gets the lines this change made stale: the component
count, the onboarding folder, useImportWizard's `importFolderLoaded` and
the Import row of the empty states table.

Resolves #398

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

Auto-revue (worker autopilot) — verdict : APPROVE

Revue du diff de ce seul maillon (29 fichiers) contre chaque critère de #398. Le label status:approved n'est pas posé : il revient à la revue indépendante.

Critères d'acceptation

  • StepCard : role="status" rendu avant (vide) et après la réussite (« Fait. ») ; h2 à tabIndex=-1 avec « Étape n sur N » puis l'étape ; « Passer » seulement avant la réussite, « Étape suivante » seulement après ; mode fin (StepCard.render.test.tsx).
  • Composants cibles : avec onboardingTarget, exactement un élément porte data-onboarding-highlight="true", aucun sans (EmptyState, ImportEmptyState, SourceList/SourceCard, WizardNavigation).
  • importCardModel : chaque ligne de la table, ordre compris ; importing passe avant les étapes cochées ; report à 0 transaction donne failed, même quand la liste compte l'import (doublons seuls).
  • Contraste lu dans styles.css : --accent à 3,27 et 3,44 (clair), 6,49 et 5,37 (sombre) ; 3 cycles de 1,2 s ; rien sous prefers-reduced-motion.
  • /import, profil neuf : la carte remplace le bandeau (CARD_PAGES), suit l'assistant, reste quand les étapes 3 et 4 se cochent (verrou, pas nextStep), annonce « Fait. » et propose « Étape suivante » ; pas de « Fait. » pour un import qui n'ajoute rien.
  • Plus de flash « aucun dossier » : importFolderLoaded posé dans le finally et porté par RESET (tests du reducer et de l'effet de montage).
  • Focus sur le titre après « Étape suivante », une fois par arrivée ; une visite sans état ne plante pas.
  • FR et EN : les 13 textes comparés au caractère près à la table de l'issue ; CHANGELOG FR et EN ; build et vitest verts (1727 → 1814).

Constats

Aucun bloquant. Points à regarder en revue indépendante :

  1. Écart au texte de l'issue : la dernière ligne de la table (« étapes 3 et 4 cochées, hors assistant ») est évaluée juste après « Aucun dossier ». À sa place littérale, elle ne pouvait jamais s'appliquer, et après « Terminé » la carte perdait « Étape suivante ».
  2. La page lit useOnboarding() pour la liste (entrée du modèle) et pour ne mettre en évidence qu'avec la carte à l'écran. Les composants cibles restent purs ; la revue peut juger si « seule OnboardingStepCard lit le contexte » visait aussi la page.
  3. Ajouts au contrat de l'issue : ImportCardInput.folderName (pour {{folder}}), « Passer » caché quand l'étape montrée est déjà faite, attente de budgetReady pour « n sur N » et la fin, attente de la relecture avant le lien « Étape suivante ».
  4. Hors périmètre, signalé : la ligne « Aucun dossier configuré » d'ImportFolderConfig clignote encore avant la lecture du dossier.
  5. Non gardé par les tests : le rendu réel du contour dans WebView2 et WebKitGTK, le focus réel et l'annonce de « Fait. » par un lecteur d'écran (liste QA de #401).

Mutations : 10/10 rouges (table dans la description).

## Auto-revue (worker autopilot) — verdict : APPROVE Revue du diff de ce seul maillon (29 fichiers) contre chaque critère de #398. Le label `status:approved` n'est pas posé : il revient à la revue indépendante. ### Critères d'acceptation - [x] `StepCard` : `role="status"` rendu avant (vide) et après la réussite (« Fait. ») ; `h2` à `tabIndex=-1` avec « Étape n sur N » puis l'étape ; « Passer » seulement avant la réussite, « Étape suivante » seulement après ; mode fin (`StepCard.render.test.tsx`). - [x] Composants cibles : avec `onboardingTarget`, exactement un élément porte `data-onboarding-highlight="true"`, aucun sans (`EmptyState`, `ImportEmptyState`, `SourceList`/`SourceCard`, `WizardNavigation`). - [x] `importCardModel` : chaque ligne de la table, ordre compris ; `importing` passe avant les étapes cochées ; `report` à 0 transaction donne `failed`, même quand la liste compte l'import (doublons seuls). - [x] Contraste lu dans `styles.css` : `--accent` à 3,27 et 3,44 (clair), 6,49 et 5,37 (sombre) ; 3 cycles de 1,2 s ; rien sous `prefers-reduced-motion`. - [x] /import, profil neuf : la carte remplace le bandeau (`CARD_PAGES`), suit l'assistant, reste quand les étapes 3 et 4 se cochent (verrou, pas `nextStep`), annonce « Fait. » et propose « Étape suivante » ; pas de « Fait. » pour un import qui n'ajoute rien. - [x] Plus de flash « aucun dossier » : `importFolderLoaded` posé dans le `finally` et porté par `RESET` (tests du reducer et de l'effet de montage). - [x] Focus sur le titre après « Étape suivante », une fois par arrivée ; une visite sans état ne plante pas. - [x] FR et EN : les 13 textes comparés au caractère près à la table de l'issue ; CHANGELOG FR et EN ; build et vitest verts (1727 → 1814). ### Constats Aucun bloquant. Points à regarder en revue indépendante : 1. **Écart au texte de l'issue** : la dernière ligne de la table (« étapes 3 et 4 cochées, hors assistant ») est évaluée juste après « Aucun dossier ». À sa place littérale, elle ne pouvait jamais s'appliquer, et après « Terminé » la carte perdait « Étape suivante ». 2. **La page lit `useOnboarding()`** pour la liste (entrée du modèle) et pour ne mettre en évidence qu'avec la carte à l'écran. Les composants cibles restent purs ; la revue peut juger si « seule `OnboardingStepCard` lit le contexte » visait aussi la page. 3. Ajouts au contrat de l'issue : `ImportCardInput.folderName` (pour `{{folder}}`), « Passer » caché quand l'étape montrée est déjà faite, attente de `budgetReady` pour « n sur N » et la fin, attente de la relecture avant le lien « Étape suivante ». 4. Hors périmètre, signalé : la ligne « Aucun dossier configuré » d'`ImportFolderConfig` clignote encore avant la lecture du dossier. 5. Non gardé par les tests : le rendu réel du contour dans WebView2 et WebKitGTK, le focus réel et l'annonce de « Fait. » par un lecteur d'écran (liste QA de #401). Mutations : 10/10 rouges (table dans la description).
Author
Owner

Revue indépendante — PR #404

Verdict : APPROVE. Le code de #404 réalise la table de son issue, ordre compris, et chaque comportement est épinglé (16/16 mutations du relecteur rouges). Le critère « elle ne dit pas « Fait. » pour un import qui n'ajoute rien » ne tient qu'à moitié, à cause du compteur de l'étape 4 livré en phase 1, hors de ce diff. Condition de merge de la pile : trancher avant /merge-stack. Recommandation : corriger le compteur (voie A) dans un commit en tête de pile ; la tête de #404 ne change pas.

Le point prioritaire : l'import qui n'ajoute rien

Ce que fait le code à ee26fe0

executeImport écrit la source, puis une ligne imported_files en completed par fichier (useImportWizard.ts:1082-1096, row_count = lignes valides du fichier), puis insère (insertBatch, :1130). L'étape 4 compte COUNT(*) FROM imported_files WHERE status = 'completed' (onboardingService.ts:60). Trois cas d'import qui n'ajoute rien sont à séparer, parce que le rapport autopilot cite le moins utile :

  • Aucune ligne valide (fichier réduit à son en-tête, ou toutes les lignes en erreur dans l'aperçu : format mal détecté, format de date erroné). Rien n'arrête l'assistant : à l'aperçu, nextDisabled vaut state.isLoading seul (ImportPage.tsx:228), « Confirmer » et « Importer » n'ont aucune garde, parseAndPreview non plus. La source et la ligne completed sont écrites, insertBatch([]) n'insère rien : étapes 3 et 4 cochées, zéro transaction. C'est le cas réel, celui du cas QA guided-noop-import de #407, et il vise un premier import sur une banque sans signature (BMO, #371).
  • Doublons seuls : il faut des transactions déjà en base. Or toute transaction arrive avec une ligne completed (executeImport, attachTransactions à la restauration), et la suppression par l'historique retire les deux. L'étape 4 est donc déjà cochée par l'import qui a amené ces transactions, et la carte n'est pas verrouillée sur /import dans cet état, sauf au second passage après une réussite, où « Fait. » est vrai. Sans effet en pratique.
  • Insertion en échec : updateFileStatus(fId, "error", 0, …) sort la ligne du compte ; au repos, la carte guide une reprise. Correct, sauf si cette mise à jour échoue à son tour.

Rejoué

  • SQL sur consolidated_schema.sql (sqlite3 de Python, base en mémoire), en reproduisant les écritures d'executeImport : après un import sans ligne valide, imports = 1 et transactions = 0.
  • Sonde de rendu d'ImportPage (hors dépôt) avec ces compteurs (sources: 1, imports: 1, transactions: 0) : sur le rapport, la phrase d'échec et pas de « Fait. » ; au repos après « Terminé », « Fait. » dans la zone role="status", donc annoncé aux lecteurs d'écran aussitôt après l'échec, et « Étape suivante » vers /transactions.

Conséquence au tip de la pile : sur /transactions, la carte de #405 demande de classer une transaction (cible firstRowCategory absente, pas de tableau) au-dessus de l'état vide « Importer des transactions » ; revenu sur /import, l'utilisateur trouve le bandeau qui renvoie à /transactions, et la carte qui guiderait une reprise ne revient pas. La carte d'accueil du tableau de bord (zéro transaction) liste « Importer un premier fichier » comme faite.

Recommandation : voie A, corriger le compteur en tête de pile

(SELECT COUNT(*) FROM imported_files f
  WHERE f.status = 'completed'
    AND EXISTS (SELECT 1 FROM transactions t WHERE t.file_id = f.id)) AS imports

Sur le même rejeu : 0 après l'import sans ligne valide ; pour un import réussi, 0 avant la première insertion et 1 après. EXPLAIN QUERY PLAN : SEARCH t USING COVERING INDEX idx_transactions_file, index présent depuis le schéma v1.

Pourquoi A :

  1. C'est le principe de la phase 1 : « chaque case est cochée par une donnée ». La case 5 a écarté exprès les indicateurs qui la cochaient d'avance (seed de mots-clés, catégorisation à l'import) ; une ligne completed sans transaction est un artefact du même ordre.
  2. Les étapes suivantes demandent des transactions. La coche envoie dans la boucle décrite plus haut l'utilisateur qui a le plus besoin du guidage, celui dont le premier import a échoué.
  3. Le changement tient dans une sous-requête. Les fonctions pures et toutes les fixtures de la pile lisent des compteurs, où imports: 1 garde son sens ; seul onboardingService.test.ts:134 épingle le texte du SQL.
  4. En prime, le compteur ne coche plus pendant l'import (ligne écrite avant les insertions) : la ligne importing de la table devient un filet, plus une condition nécessaire. Le cas « mise à jour du statut en échec » est couvert aussi.
  5. status = 'completed' reste : une insertion en échec, partielle comprise (insertBatch n'est pas transactionnel), reste un échec pour la liste comme pour le rapport.

Pourquoi en tête de pile : le compteur vit dans onboardingService.ts (phase 1, touché par #402). Un commit sur #402 ou #404 obligerait à rebaser des maillons déjà revus ; en tête, les têtes revues restent intactes et le merge ff épinglé reste possible maillon par maillon (pratique retenue pour les suites de licence).

À aligner dans le même commit :

  • CHANGELOG FR et EN : la formulation de #407 (« mais pas sur le rapport d'un import… ») peut redevenir « jamais pour un import qui n'a ajouté aucune transaction », plus une entrée Corrigé / Fixed, puisque la v0.17.0 coche déjà « Importer un premier fichier » après un import sans ligne valide ;
  • le guide (guide-utilisateur.md:38 au tip, et la clé docs.* correspondante) ;
  • le cas QA guided-noop-import : retirer « À juger », attendre au repos « Étape 4 sur 6 » et la première carte de source entourée ;
  • la ligne « Importer un premier fichier » du tableau des cases dans docs/architecture.md, et le commentaire importedFileService.ts:42-43 ;
  • deux tests de régression : le compteur ne compte une ligne completed qu'avec des transactions ; ImportPage au repos avec sources: 1, imports: 0 ne dit pas « Fait. » et entoure la première carte de source (ma sonde le confirme sur le code actuel : « Étape 4 sur 6 », phrase sourceList, une seule cible).

Risque : des transactions héritées à file_id NULL (clé ON DELETE SET NULL) ne compteraient pas. Toutes les suppressions retirent les transactions avant la ligne de fichier, et D9 masque le parcours d'un profil qui avait des transactions à sa première ouverture ; au pire, « Passer ».

Coût de la voie B : amender le critère de #398 en « pas de « Fait. » sur le rapport », livrer « Fait. » au repos avec la boucle ci-dessus, et décrire un défaut connu dans le guide et la liste QA. #407 a déjà écrit la formulation étroite et le cas « À juger » : B ne coûte aucun code, mais livre l'incohérence.

Suggestions (non bloquantes)

  1. Focus invisible sur la cible (WCAG 2.4.7, échec F78), styles.css:158. Le outline de [data-onboarding-highlight="true"] est une règle d'auteur : il l'emporte sur l'anneau de focus du navigateur, et aucune cible ne porte de style de focus à elle. Mesuré dans Chromium headless (moteur de WebView2) : un bouton ordinaire focalisé passe à auto 1px, le bouton entouré reste solid 3px rgb(193, 119, 103), focalisé ou non. Un utilisateur au clavier qui atteint la cible ne voit rien changer. Correctif vérifié dans le même Chromium : [data-onboarding-highlight="true"]:focus-visible { outline: revert; } rend l'anneau du navigateur. À poser en tête de pile avec le compteur, avec une ligne dans styles.contrast.test.ts et un cas QA.
  2. « Passer » sur l'étape du dossier, sans dossier : la carte reste verrouillée et affiche « Étape 3 sur 6 · Configurer une source d'import » avec la phrase et l'anneau de l'étape du dossier ; au second « Passer », l'étape 4, même phrase ; le bandeau revient au troisième (sonde de rendu). Cohérent avec la dépendance des étapes, à juger pendant la QA de #401.
  3. #384 : ce PR règle la partie « flash » de l'item #382. Au merge, le noter sur #384 : les bannières succès et erreur simultanées et le message Rust en anglais restent.

Vérifications

  • CI run 502 sur ee26fe0, lue dans le journal du job : tsc && vite build vert, 101 fichiers et 1814 tests verts.
  • Copie hors dépôt (git archive ee26fe0, npm ci) : npx vitest run 101 fichiers, 1814 tests verts.
  • 16 mutations du relecteur, distinctes de celles de l'auteur, une à la fois, fichier restauré et comparé à l'octet après chacune : 16/16 rouges.
# Mutation Fichier Tests rouges
R1 ligne « Fait. » au repos retirée onboardingPath.ts 2
R2 ligne « Fait. » au repos sans la garde source-list onboardingPath.ts 1
R3 ligne « Fait. » au repos sur l'étape source seule onboardingPath.ts 1
R4 cardStep s'arrête sur une étape passée onboardingPath.ts 1
R5 cardStep classe parmi toutes les lignes, verrouillées comprises onboardingPath.ts 1
R6 cardNext donne un lien vers une étape de la page même onboardingPath.ts 2
R7 « Passer » offert sur une étape faite selon la liste StepCard.tsx 1
R8 « Étape suivante » offerte avant la réussite StepCard.tsx 2
R9 focus sans la garde shown OnboardingStepCard.tsx 1
R10 carte rendue quels que soient le mode et le verrou OnboardingStepCard.tsx 2
R11 état vide et liste sans attendre la lecture du dossier ImportPage.tsx 1
R12 bouton « Importer » sans sa cible ImportPage.tsx 1
R13 « Actualiser » entouré avec « Créer un dossier d'exemple » ImportEmptyState.tsx 2
R14 dossier marqué lu avant son scan useImportWizard.ts 3
R15 changer de dossier le marque non lu useImportWizard.ts 1
R16 pulsation sans fin styles.css 1
  • L'écart au corps (ligne « Étapes 3 et 4 déjà cochées » placée aussitôt après « Aucun dossier ») est correct : à sa place littérale, elle ne s'appliquait jamais ; R1 à R3 l'épinglent.
  • Timeline : #398 porte un pull_ref closes venant de #404 ; #384 un pull_ref none, elle ne sera pas fermée.
  • Ni migration ni secret ; le nom du dossier est interpolé comme texte (React l'échappe, escapeValue: false côté i18next). Commits au format conventionnel, Resolves #398 dans ee26fe0.

Actions faites

  • Label de #398 : status:review → status:approved.
  • Copie hors dépôt /tmp/pr404-review, scripts, sondes et journal CI (/tmp/pr404-*) supprimés ; arbre du dépôt intact.
## Revue indépendante — PR #404 **Verdict : APPROVE.** Le code de #404 réalise la table de son issue, ordre compris, et chaque comportement est épinglé (16/16 mutations du relecteur rouges). Le critère « elle ne dit pas « Fait. » pour un import qui n'ajoute rien » ne tient qu'à moitié, à cause du compteur de l'étape 4 livré en phase 1, hors de ce diff. **Condition de merge de la pile** : trancher avant `/merge-stack`. Recommandation : corriger le compteur (voie A) dans un commit en tête de pile ; la tête de #404 ne change pas. ### Le point prioritaire : l'import qui n'ajoute rien **Ce que fait le code à `ee26fe0`** `executeImport` écrit la source, puis une ligne `imported_files` en `completed` par fichier (`useImportWizard.ts:1082-1096`, `row_count` = lignes valides du fichier), puis insère (`insertBatch`, `:1130`). L'étape 4 compte `COUNT(*) FROM imported_files WHERE status = 'completed'` (`onboardingService.ts:60`). Trois cas d'import qui n'ajoute rien sont à séparer, parce que le rapport autopilot cite le moins utile : - **Aucune ligne valide** (fichier réduit à son en-tête, ou toutes les lignes en erreur dans l'aperçu : format mal détecté, format de date erroné). Rien n'arrête l'assistant : à l'aperçu, `nextDisabled` vaut `state.isLoading` seul (`ImportPage.tsx:228`), « Confirmer » et « Importer » n'ont aucune garde, `parseAndPreview` non plus. La source et la ligne `completed` sont écrites, `insertBatch([])` n'insère rien : étapes 3 et 4 cochées, zéro transaction. **C'est le cas réel**, celui du cas QA `guided-noop-import` de #407, et il vise un premier import sur une banque sans signature (BMO, #371). - **Doublons seuls** : il faut des transactions déjà en base. Or toute transaction arrive avec une ligne `completed` (`executeImport`, `attachTransactions` à la restauration), et la suppression par l'historique retire les deux. L'étape 4 est donc déjà cochée par l'import qui a amené ces transactions, et la carte n'est pas verrouillée sur /import dans cet état, sauf au second passage après une réussite, où « Fait. » est vrai. **Sans effet en pratique.** - **Insertion en échec** : `updateFileStatus(fId, "error", 0, …)` sort la ligne du compte ; au repos, la carte guide une reprise. **Correct**, sauf si cette mise à jour échoue à son tour. **Rejoué** - SQL sur `consolidated_schema.sql` (sqlite3 de Python, base en mémoire), en reproduisant les écritures d'`executeImport` : après un import sans ligne valide, `imports = 1` et `transactions = 0`. - Sonde de rendu d'`ImportPage` (hors dépôt) avec ces compteurs (`sources: 1, imports: 1, transactions: 0`) : sur le rapport, la phrase d'échec et pas de « Fait. » ; au repos après « Terminé », « Fait. » dans la zone `role="status"`, donc annoncé aux lecteurs d'écran aussitôt après l'échec, et « Étape suivante » vers /transactions. **Conséquence au tip de la pile** : sur /transactions, la carte de #405 demande de classer une transaction (cible `firstRowCategory` absente, pas de tableau) au-dessus de l'état vide « Importer des transactions » ; revenu sur /import, l'utilisateur trouve le bandeau qui renvoie à /transactions, et la carte qui guiderait une reprise ne revient pas. La carte d'accueil du tableau de bord (zéro transaction) liste « Importer un premier fichier » comme faite. ### Recommandation : voie A, corriger le compteur en tête de pile ```sql (SELECT COUNT(*) FROM imported_files f WHERE f.status = 'completed' AND EXISTS (SELECT 1 FROM transactions t WHERE t.file_id = f.id)) AS imports ``` Sur le même rejeu : 0 après l'import sans ligne valide ; pour un import réussi, 0 avant la première insertion et 1 après. `EXPLAIN QUERY PLAN` : `SEARCH t USING COVERING INDEX idx_transactions_file`, index présent depuis le schéma v1. Pourquoi A : 1. C'est le principe de la phase 1 : « chaque case est cochée par une donnée ». La case 5 a écarté exprès les indicateurs qui la cochaient d'avance (seed de mots-clés, catégorisation à l'import) ; une ligne `completed` sans transaction est un artefact du même ordre. 2. Les étapes suivantes demandent des transactions. La coche envoie dans la boucle décrite plus haut l'utilisateur qui a le plus besoin du guidage, celui dont le premier import a échoué. 3. Le changement tient dans une sous-requête. Les fonctions pures et toutes les fixtures de la pile lisent des compteurs, où `imports: 1` garde son sens ; seul `onboardingService.test.ts:134` épingle le texte du SQL. 4. En prime, le compteur ne coche plus pendant l'import (ligne écrite avant les insertions) : la ligne `importing` de la table devient un filet, plus une condition nécessaire. Le cas « mise à jour du statut en échec » est couvert aussi. 5. `status = 'completed'` reste : une insertion en échec, partielle comprise (`insertBatch` n'est pas transactionnel), reste un échec pour la liste comme pour le rapport. Pourquoi en tête de pile : le compteur vit dans `onboardingService.ts` (phase 1, touché par #402). Un commit sur #402 ou #404 obligerait à rebaser des maillons déjà revus ; en tête, les têtes revues restent intactes et le merge ff épinglé reste possible maillon par maillon (pratique retenue pour les suites de licence). À aligner dans le même commit : - CHANGELOG FR et EN : la formulation de #407 (« mais pas sur le rapport d'un import… ») peut redevenir « jamais pour un import qui n'a ajouté aucune transaction », plus une entrée **Corrigé / Fixed**, puisque la v0.17.0 coche déjà « Importer un premier fichier » après un import sans ligne valide ; - le guide (`guide-utilisateur.md:38` au tip, et la clé `docs.*` correspondante) ; - le cas QA `guided-noop-import` : retirer « À juger », attendre au repos « Étape 4 sur 6 » et la première carte de source entourée ; - la ligne « Importer un premier fichier » du tableau des cases dans `docs/architecture.md`, et le commentaire `importedFileService.ts:42-43` ; - deux tests de régression : le compteur ne compte une ligne `completed` qu'avec des transactions ; `ImportPage` au repos avec `sources: 1, imports: 0` ne dit pas « Fait. » et entoure la première carte de source (ma sonde le confirme sur le code actuel : « Étape 4 sur 6 », phrase `sourceList`, une seule cible). Risque : des transactions héritées à `file_id` NULL (clé `ON DELETE SET NULL`) ne compteraient pas. Toutes les suppressions retirent les transactions avant la ligne de fichier, et D9 masque le parcours d'un profil qui avait des transactions à sa première ouverture ; au pire, « Passer ». Coût de la voie B : amender le critère de #398 en « pas de « Fait. » sur le rapport », livrer « Fait. » au repos avec la boucle ci-dessus, et décrire un défaut connu dans le guide et la liste QA. #407 a déjà écrit la formulation étroite et le cas « À juger » : B ne coûte aucun code, mais livre l'incohérence. ### Suggestions (non bloquantes) 1. **Focus invisible sur la cible (WCAG 2.4.7, échec F78)**, `styles.css:158`. Le `outline` de `[data-onboarding-highlight="true"]` est une règle d'auteur : il l'emporte sur l'anneau de focus du navigateur, et aucune cible ne porte de style de focus à elle. Mesuré dans Chromium headless (moteur de WebView2) : un bouton ordinaire focalisé passe à `auto 1px`, le bouton entouré reste `solid 3px rgb(193, 119, 103)`, focalisé ou non. Un utilisateur au clavier qui atteint la cible ne voit rien changer. Correctif vérifié dans le même Chromium : `[data-onboarding-highlight="true"]:focus-visible { outline: revert; }` rend l'anneau du navigateur. À poser en tête de pile avec le compteur, avec une ligne dans `styles.contrast.test.ts` et un cas QA. 2. **« Passer » sur l'étape du dossier, sans dossier** : la carte reste verrouillée et affiche « Étape 3 sur 6 · Configurer une source d'import » avec la phrase et l'anneau de l'étape du dossier ; au second « Passer », l'étape 4, même phrase ; le bandeau revient au troisième (sonde de rendu). Cohérent avec la dépendance des étapes, à juger pendant la QA de #401. 3. **#384** : ce PR règle la partie « flash » de l'item #382. Au merge, le noter sur #384 : les bannières succès et erreur simultanées et le message Rust en anglais restent. ### Vérifications - CI run 502 sur `ee26fe0`, lue dans le journal du job : `tsc && vite build` vert, 101 fichiers et 1814 tests verts. - Copie hors dépôt (`git archive ee26fe0`, `npm ci`) : `npx vitest run` 101 fichiers, 1814 tests verts. - 16 mutations du relecteur, distinctes de celles de l'auteur, une à la fois, fichier restauré et comparé à l'octet après chacune : **16/16 rouges**. | # | Mutation | Fichier | Tests rouges | |---|---|---|---| | R1 | ligne « Fait. » au repos retirée | `onboardingPath.ts` | 2 | | R2 | ligne « Fait. » au repos sans la garde `source-list` | `onboardingPath.ts` | 1 | | R3 | ligne « Fait. » au repos sur l'étape source seule | `onboardingPath.ts` | 1 | | R4 | `cardStep` s'arrête sur une étape passée | `onboardingPath.ts` | 1 | | R5 | `cardStep` classe parmi toutes les lignes, verrouillées comprises | `onboardingPath.ts` | 1 | | R6 | `cardNext` donne un lien vers une étape de la page même | `onboardingPath.ts` | 2 | | R7 | « Passer » offert sur une étape faite selon la liste | `StepCard.tsx` | 1 | | R8 | « Étape suivante » offerte avant la réussite | `StepCard.tsx` | 2 | | R9 | focus sans la garde `shown` | `OnboardingStepCard.tsx` | 1 | | R10 | carte rendue quels que soient le mode et le verrou | `OnboardingStepCard.tsx` | 2 | | R11 | état vide et liste sans attendre la lecture du dossier | `ImportPage.tsx` | 1 | | R12 | bouton « Importer » sans sa cible | `ImportPage.tsx` | 1 | | R13 | « Actualiser » entouré avec « Créer un dossier d'exemple » | `ImportEmptyState.tsx` | 2 | | R14 | dossier marqué lu avant son scan | `useImportWizard.ts` | 3 | | R15 | changer de dossier le marque non lu | `useImportWizard.ts` | 1 | | R16 | pulsation sans fin | `styles.css` | 1 | - L'écart au corps (ligne « Étapes 3 et 4 déjà cochées » placée aussitôt après « Aucun dossier ») est correct : à sa place littérale, elle ne s'appliquait jamais ; R1 à R3 l'épinglent. - Timeline : #398 porte un `pull_ref` `closes` venant de #404 ; #384 un `pull_ref` `none`, elle ne sera pas fermée. - Ni migration ni secret ; le nom du dossier est interpolé comme texte (React l'échappe, `escapeValue: false` côté i18next). Commits au format conventionnel, `Resolves #398` dans `ee26fe0`. ### Actions faites - Label de #398 : `status:review` → `status:approved`. - Copie hors dépôt `/tmp/pr404-review`, scripts, sondes et journal CI (`/tmp/pr404-*`) supprimés ; arbre du dépôt intact.
maximus changed target branch from issue-397-onboarding-banner to main 2026-10-09 01:10:33 +00:00
maximus merged commit ee26fe0888 into main 2026-10-09 01:10:40 +00:00
Author
Owner

Décision de Max sur le critère « pas de « Fait. » pour un import qui n'ajoute rien » : voie A. Appliquée en tête de pile dans a58f8b9 (branche de #407), mergée avec la pile le 2026-10-08 : l'étape ne compte plus qu'un fichier qui a amené au moins une transaction, avec une entrée Corrigé au CHANGELOG puisque la v0.17.0 cochait déjà l'étape après un tel import. Le focus visible sur le contrôle entouré (suggestion 1) est dans 74a6572. Détail et preuves : commentaire de traçabilité de #407.

Décision de Max sur le critère « pas de « Fait. » pour un import qui n'ajoute rien » : **voie A**. Appliquée en tête de pile dans `a58f8b9` (branche de #407), mergée avec la pile le 2026-10-08 : l'étape ne compte plus qu'un fichier qui a amené au moins une transaction, avec une entrée Corrigé au CHANGELOG puisque la v0.17.0 cochait déjà l'étape après un tel import. Le focus visible sur le contrôle entouré (suggestion 1) est dans `74a6572`. Détail et preuves : commentaire de traçabilité de #407.
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#404
No description provided.