Onboarding : état partagé du parcours, relecture en direct et canal d'événements de données (ADR 0022) #396
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#396
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Milestone
planned-2026-10-06-onboarding-parcours-guide, issue 1/6. Phase 2 de l'onboarding (#391) : bandeau de démarrage sur chaque page, et cartes d'étape avec mise en évidence. La spec (spec-decisions-onboarding-parcours-guide.mdetspec-plan-onboarding-parcours-guide.md) est locale et ignorée par git : ce corps est auto-suffisant.Aujourd'hui, l'état de la liste de démarrage ne vit que dans
DashboardPage, qui le lit à son montage (useOnboardingChecklist, appelé àDashboardPage.tsx:51). Pour qu'un bandeau et des cartes suivent l'utilisateur sur toutes les pages et voient une étape se cocher en direct, cet état devient partagé par toute l'application et se relit après chaque écriture concernée. Cette issue ne change rien de visible, sauf la règle D9 (profils déjà actifs masqués d'office).Prérequis hors milestone, déjà rempli : la PR #394 est mergée sur
main(5910387). L'étape 5 se détecte donc correctement, et les profils secondaires ont rattrapé leurs migrations.À faire
src/services/dataEvents.ts(nouveau) :notifyDataChanged()etsubscribeDataChanged(listener): () => void, sans React, sur le modèle delogService.subscribe(logService.ts:144-147). Un abonné qui lève n'empêche pas les autres. Le nom est côté données : la couche de données ignore qui écoute.docs/adr/0022-canal-evenements-donnees.md: portée du canal (« des données que lisent des compteurs ont peut-être changé »), liste des écritures qui notifient, attente de 150 ms côté abonné, réponses périmées ignorées, abonnés admis. Ajouter sa ligne à la table des ADR dedocs/architecture.md.notifyDataChanged()part après chaque écriture réussie de cette liste, construite depuis les tables que litCOUNTERS_SQL(onboardingService.ts:39-46) plus le dossier d'import :setImportFolder(userPreferenceService.ts) ;createSourceetdeleteSource(importSourceService.ts) :updateSourcene change aucun compte ;createImportedFile,updateFileStatus,deleteImportWithTransactionsetdeleteAllImportsWithTransactions(importedFileService.ts) ;insertBatchetupdateTransactionCategory(transactionService.ts) ;applyKeywordWithReassignment(categorizationService.ts, après sonCOMMIT) ;upsertBudgetEntry,deleteBudgetEntry,upsertBudgetEntriesForYearetapplyTemplate(budgetService.ts) ;runRestoreaprès sonCOMMIT(dataExportService.ts) : un seul point couvre les trois modes de restauration ;executeImport(useImportWizard.ts), en succès comme en échec.src/services/onboardingService.ts:getOnboardingSnapshot()remplacegetOnboardingCounters(). C'est une lecture pure, qui n'écrit rien : compteurs, dossier choisi, masquage, étapes passées.onboarding_skipped_<clé>="1". Elles se lisent en une requête (key LIKE 'onboarding_skipped_%'), filtrée sur les sixChecklistItemKeyet sur la valeur"1".skipStep(key)etresumeStep(key)écrivent ou effacent la préférence, de façon idempotente. Pas de JSON.initializeGuidedPath()(règle D9) s'exécute dans unwithTransaction:BEGIN, relecture d'onboarding_guided_initialized. S'il est absent :onboarding_checklist_hidden = "1"d'abord sitransactions > 0, puisonboarding_guided_initialized = "1", puisCOMMIT. La fonction rend{ hidden }, écrit une ligne au Journal (logInfo) quand elle masque, et ne notifie pas.hideChecklist,showChecklist,skipStepetresumeStepne notifient pas : le fournisseur met son état à jour lui-même.src/utils/onboardingChecklist.ts:computeChecklist(counters, { importFolderSet, budgetAllowed, skipped }).done,skippedetlocked.completeles exige toutes.src/utils/onboardingPath.ts(nouveau, pur) :nextStep(checklist): première ligne déverrouillée ni faite ni passée, ounull. Le lien d'une étape estChecklistItem.to.CARD_PAGES, un ensemble vide dans cette issue ; les issues de carte y ajoutent leur page.stepsOfPage(pathname):/importporteimportFolder,sourceetimport;/transactionsportemanualCategory;/budgetportebudget.latchCardPage(previous, { pathname, nextStep, hidden }), le verrou de carte :pathnameest la route de l'étape suivante et figure dansCARD_PAGES;bannerMode({ status, checklist, hidden, pathname, isEmptyDashboard, budgetReady, cardPage })rendnone,banner,cardoucomplete.readOnboardingNavState(state: unknown): { focus: boolean; guided: boolean }, seule voie de lecture delocation.state(typéanypar react-router).null,undefined, une chaîne ou un objet partiel donnentfalse.src/hooks/useOnboardingChecklist.tsgarde le reducer (règle CLAUDE.md : un hookuseReducerpar domaine), étendu :initializeGuidedPath()une fois, par une promesse partagée contre le double montage de StrictMode ;LOAD_DONEpérimé est ignoré ;HIDE,SHOW,SKIP,RESUME) priment sur le snapshot d'un chargement parti avant elles (course relevée en #384) ;ready(aujourd'huiLOAD_STARTrepasse enloading,:51-53) ;readyet le dernier état, et n'écrit au Journal qu'au passage en échec ;subscribeDataChanged, avec une attente de 150 ms qui regroupe les rafales ;pathname(useLocation) ;latchCardPage;src/contexts/OnboardingContext.tsx(nouveau) :OnboardingProviderappelle le hook une seule fois et partage son résultat.useOnboarding()rendstatus,checklist,counters,hidden,budgetReady,nextStep,mode,cardPage,hide,show,skip,resumeetrefresh. Le fournisseur est monté dansAppShell, autour de<Outlet/>(AppShell.tsx:4-13).DashboardPagelituseOnboarding()au lieu d'appeler le hook. La barre compacte reste affichée dans cette issue (elle part à l'issue 2).Fichiers concernés
dbdoivent couvrir la notification)executeImport)Critères d'acceptation
dataEvents: un abonné reçoit, un désabonné ne reçoit plus, et un abonné qui lève n'empêche pas les autres (tests).dbmoqué) ;executeImportnotifie à la fin, en succès comme en échec.getOnboardingSnapshotn'écrit rien (test). Les étapes passées ne comptent que pour les six clés et la valeur"1".initializeGuidedPath, cinq tests :transactions > 0: masquage puis initialisation, dans la transaction ;ROLLBACK, drapeau non posé ;computeChecklistcouvre les quatre cas du point 5 de « À faire ».nextStepsaute les faites, les passées et les verrouillées, et rendnullà la fin.latchCardPageetbannerModecouvrent toute la table de vérité :CARD_PAGESou non ;budgetReadyfaux.readOnboardingNavStatecouvrenull,undefined, une chaîne,{},{ onboarding: null },{ onboarding: { focus: "yes" } }et les cas valides.LOAD_DONEpérimé est ignoré ;HIDE,SHOW,SKIPetRESUMEpendant un chargement priment sur son snapshot ;readyet les compteurs ;AppShelletDashboardPagelit le contexte. Le tableau de bord se comporte comme avant, sauf pour un profil déjà actif, masqué d'office.architecture.md. CHANGELOG FR et EN.npm run buildetnpx vitest runverts.Review caveats
insertBatchniupdateFileStatus, la dernière relecture d'un import partait avant les insertions de transactions. Ne retirer aucune écriture de la liste.latchCardPage) est ce qui laisse une carte affichée après sa réussite. Sans lui, « Fait. » et « Étape suivante » n'apparaîtraient jamais (issues 3 à 5).location.stateest typéany: toute lecture passe parreadOnboardingNavState(CWE-476).Décisions prises en planification
onboarding_checklist_hiddenpar profil ; masquer le bandeau masque la liste, et le réaffichage depuis Paramètres → Utilisateurs rétablit tout.hidden, étapes passées, initialisation).dataEvents), attente de 150 ms, réponses périmées ignorées.hooks/useOnboardingChecklist.ts, avec un fournisseur mince.Spec source
spec-plan-onboarding-parcours-guide.md (et spec-decisions-onboarding-parcours-guide.md)