feat(onboarding): step card with highlight, and the Import page's card #404
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#404
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-398-onboarding-step-card-import"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Resolves #398
Generated autonomously by /autopilot run of 2026-10-08
Ce qui change pour l'utilisateur
role="status", puis « Étape suivante : Classer une transaction à la main → », ou la fin du parcours. Après « Terminé » ou « Annuler », elle garde « Fait. ».Technique
StepCard(props seules) etOnboardingStepCard(connectée, seule pièce de la carte à lireuseOnboarding()), danssrc/components/onboarding/. La carte se rend quandmode === "card"etcardPage === pathname(showsStepCard) : c'est le verrou qui la garde après sa réussite, pasnextStep. Le focus va au titre une fois par arrivée (garde surlocation.key, contre le double effet de StrictMode), puisnavigate(pathname, { replace: true, state: null }).onboardingPath.ts:CARD_PAGESreç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) etonboardingTargetAttributes.ImportCardInputdansshared/types, avecfolderNameen plus (voir Décisions).onboardingTarget?: booleansurEmptyStateAction(viaImportEmptyState), surSourceList(première carte seulement, parSourceCard) et surWizardNavigation(bouton suivant). Le composant renddata-onboarding-target="<clé fixe>"etdata-onboarding-highlight="true".ImportFolderConfigne reçoit pas de cible.styles.css:outline: 3px solid var(--accent); outline-offset: 2px, pulsationbox-shadowde 1,2 s × 3 = 3,6 s,animation: nonesousprefers-reduced-motion: reduce.--accentatteint 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 lefinallyde la lecture du dossier au montage (après le premier scan) et porté parRESET. L'état vide, la liste des sources et la carte (folderLoaded) l'attendent.ImportPageconstruit l'entrée, rendOnboardingStepCarden premier enfant et ne passe la cible que pendant que la carte est affichée.onboarding.card.*), CHANGELOG FR et EN (Ajouté, Corrigé),docs/architecture.md(seulement les lignes que ce changement rendait fausses).Preuves
npm run buildvert ;npx vitest run: 1727 → 1814 tests verts (94 → 101 fichiers). Aucune modification Rust.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(lefinally) ;styles.contrast.test.ts(contraste lu dansstyles.css, 3 cycles en 5 s au plus, garde de mouvement réduit). Ajouts dansonboardingPath.test.ts(chaque ligne de la table, ordre compris),EmptyState.test.tsx,ImportEmptyState.test.tsxetuseImportWizard.test.ts(RESET).git checkoutaprès chacune : 10/10 rouges.imported_filescomptéeonboardingPath.tsreportavec 0 transaction lu comme « Fait. »onboardingPath.tsnextStepau lieu du verrouOnboardingStepCard.tsxrole="status"rendue seulement après la réussiteStepCard.tsxSourceList.tsxRESETperdimportFolderLoadeduseImportWizard.tsimportFolderLoadedposé dans letry, pas dans lefinallyuseImportWizard.tsOnboardingStepCard.tsxprefers-reduced-motionstyles.cssImportPage.tsxDécisions
Détail et alternatives écartées dans le journal du run.
nextStep) : avec « non faite » au sens strict, « Passer » sur le dossier remontrait la même étape, en boucle.budgetReady, comme le compteur du bandeau etbannerMode.ImportCardInput.folderNameest ajouté pour{{folder}}: le modèle porte toutes ses interpolations, et la page ne passe que lui.useOnboarding()(la liste, entrée du modèle) et ne passe la cible que si la carte est affichée.data-onboarding-targetest fixe par contrôle (empty-state-action,source-card,wizard-next), la propriété étant un booléen.docs.*restent à #401 (issue 6 du plan).Pour relire
importCardModelet son test (onboardingPath.ts), puisOnboardingStepCard, puisImportPageet son test de rendu.ImportFolderConfig) avant la lecture. L'issue ne fait attendre que l'état vide et la carte.AppShell.render.test.tsxest adapté : il épinglait /import comme page sans carte.🤖 Generated with Claude Code
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>Auto-revue (worker autopilot) — verdict : APPROVE
Revue du diff de ce seul maillon (29 fichiers) contre chaque critère de #398. Le label
status:approvedn'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=-1avec « Étape n sur N » puis l'étape ; « Passer » seulement avant la réussite, « Étape suivante » seulement après ; mode fin (StepCard.render.test.tsx).onboardingTarget, exactement un élément portedata-onboarding-highlight="true", aucun sans (EmptyState,ImportEmptyState,SourceList/SourceCard,WizardNavigation).importCardModel: chaque ligne de la table, ordre compris ;importingpasse avant les étapes cochées ;reportà 0 transaction donnefailed, même quand la liste compte l'import (doublons seuls).styles.css:--accentà 3,27 et 3,44 (clair), 6,49 et 5,37 (sombre) ; 3 cycles de 1,2 s ; rien sousprefers-reduced-motion.CARD_PAGES), suit l'assistant, reste quand les étapes 3 et 4 se cochent (verrou, pasnextStep), annonce « Fait. » et propose « Étape suivante » ; pas de « Fait. » pour un import qui n'ajoute rien.importFolderLoadedposé dans lefinallyet porté parRESET(tests du reducer et de l'effet de montage).Constats
Aucun bloquant. Points à regarder en revue indépendante :
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 « seuleOnboardingStepCardlit le contexte » visait aussi la page.ImportCardInput.folderName(pour{{folder}}), « Passer » caché quand l'étape montrée est déjà faite, attente debudgetReadypour « n sur N » et la fin, attente de la relecture avant le lien « Étape suivante ».ImportFolderConfigclignote encore avant la lecture du dossier.Mutations : 10/10 rouges (table dans la description).
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 à
ee26fe0executeImportécrit la source, puis une ligneimported_filesencompletedpar fichier (useImportWizard.ts:1082-1096,row_count= lignes valides du fichier), puis insère (insertBatch,:1130). L'étape 4 compteCOUNT(*) 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 :nextDisabledvautstate.isLoadingseul (ImportPage.tsx:228), « Confirmer » et « Importer » n'ont aucune garde,parseAndPreviewnon plus. La source et la lignecompletedsont écrites,insertBatch([])n'insère rien : étapes 3 et 4 cochées, zéro transaction. C'est le cas réel, celui du cas QAguided-noop-importde #407, et il vise un premier import sur une banque sans signature (BMO, #371).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.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é
consolidated_schema.sql(sqlite3 de Python, base en mémoire), en reproduisant les écritures d'executeImport: après un import sans ligne valide,imports = 1ettransactions = 0.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 zonerole="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
firstRowCategoryabsente, 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
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 :
completedsans transaction est un artefact du même ordre.imports: 1garde son sens ; seulonboardingService.test.ts:134épingle le texte du SQL.importingde la table devient un filet, plus une condition nécessaire. Le cas « mise à jour du statut en échec » est couvert aussi.status = 'completed'reste : une insertion en échec, partielle comprise (insertBatchn'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 :
guide-utilisateur.md:38au tip, et la clédocs.*correspondante) ;guided-noop-import: retirer « À juger », attendre au repos « Étape 4 sur 6 » et la première carte de source entourée ;docs/architecture.md, et le commentaireimportedFileService.ts:42-43;completedqu'avec des transactions ;ImportPageau repos avecsources: 1, imports: 0ne dit pas « Fait. » et entoure la première carte de source (ma sonde le confirme sur le code actuel : « Étape 4 sur 6 », phrasesourceList, une seule cible).Risque : des transactions héritées à
file_idNULL (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)
styles.css:158. Leoutlinede[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é restesolid 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 dansstyles.contrast.test.tset un cas QA.Vérifications
ee26fe0, lue dans le journal du job :tsc && vite buildvert, 101 fichiers et 1814 tests verts.git archive ee26fe0,npm ci) :npx vitest run101 fichiers, 1814 tests verts.onboardingPath.tssource-listonboardingPath.tsonboardingPath.tscardSteps'arrête sur une étape passéeonboardingPath.tscardStepclasse parmi toutes les lignes, verrouillées comprisesonboardingPath.tscardNextdonne un lien vers une étape de la page mêmeonboardingPath.tsStepCard.tsxStepCard.tsxshownOnboardingStepCard.tsxOnboardingStepCard.tsxImportPage.tsxImportPage.tsxImportEmptyState.tsxuseImportWizard.tsuseImportWizard.tsstyles.csspull_refclosesvenant de #404 ; #384 unpull_refnone, elle ne sera pas fermée.escapeValue: falsecôté i18next). Commits au format conventionnel,Resolves #398dansee26fe0.Actions faites
status:review→status:approved./tmp/pr404-review, scripts, sondes et journal CI (/tmp/pr404-*) supprimés ; arbre du dépôt intact.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 dans74a6572. Détail et preuves : commentaire de traçabilité de #407.