feat(onboarding): transaction count, onboarding counters and checklist reducer (#351) #377
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#377
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-351-onboarding-core"
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?
Fixes #351
What
Data foundation of the getting-started checklist (wave
planned-2026-10-03-onboarding-phase1, link 1 of 7). No UI, no DB migration, no CHANGELOG entry (centralised in #357).transactionService.getTransactionCount()— unfilteredSELECT COUNT(*) FROM transactions.src/services/onboardingService.ts—getOnboardingCounters()reads the aggregated five-count query,getImportFolder()and theonboarding_checklist_hiddenpreference in onePromise.all;hideChecklist()/showChecklist()set / delete the preference (value"1") andlogInfo.src/utils/onboardingChecklist.ts— purecomputeChecklist(counters, { importFolderSet, budgetAllowed })→{ items, done, total, complete }; six rows in order (profilealways done,importFolder,source,import,manualCategory,budget); the budget row islockedwhen Budget is not in the edition, excluded fromtotal(5 instead of 6) and fromcomplete, and links to/settings/users.src/hooks/useOnboardingChecklist.ts—onboardingChecklistReducer+initialOnboardingChecklistStateexported; state{ status, counters, importFolderSet, hidden, error }; actionsrefresh(),hide(),show(); composed withuseEntitlement("budget")→ returns{ status, checklist, hidden, budgetReady, refresh, hide, show }. A load error setsstatus: "error"and never toucheshidden.OnboardingCounters,ChecklistItem,ChecklistItemKeyinsrc/shared/types/index.ts.Why
#352 (dashboard empty state + checklist) and #353 (Transactions/Budget empty states) consume these pieces; keeping them pure and exported lets the state machine be tested without a renderHook harness.
Decisions
OnboardingCountersfields are camelCase (as the issue names them incomputeChecklist); the SQL aliases stay snake_case and are mapped in the service.importFolderSetbeside the four listed fields, sincecomputeChecklisttakes it separately fromcounters.hide()/show()persist first, then dispatch; a failed write is logged (logError) and leaves the state unchanged.tovalues for six rows; #352 renders a link only on unchecked rows andprofileis always checked, so: importFolder/source/import →/import, manualCategory →/transactions, budget →/budget(/settings/userswhen locked);profile.to=/settings/users, never rendered.How tested
src/utils/onboardingChecklist.test.ts(14): each row in isolation, zero counters → 1/6, folder without source → 2/6,budgetAllowed=false→total=5+budget.locked, complete at 5/5 locked and 6/6 open, not at 5/6, routes.src/hooks/useOnboardingChecklist.test.ts(10): every action; error keepshiddenand the last counters; refresh after error → loading → ready.src/services/onboardingService.test.ts(10): aggregated SQL shape, preference keys, mapping, hidden only for"1", empty folder treated as unset, hide/show writes + log.src/services/transactionService.test.ts(3): 0, N, no row.npm test: 72 files, 1385 tests green.npm run build(tsc + vite) green.Generated autonomously by /autopilot run of 2026-10-03
🤖 Generated with Claude Code
Lay the data foundation of the getting-started checklist, without UI: - transactionService.getTransactionCount(): unfiltered COUNT(*) of transactions - onboardingService: getOnboardingCounters() reads the five aggregated counts, the import folder and the onboarding_checklist_hidden preference in one Promise.all; hideChecklist()/showChecklist() persist the preference and log - utils/onboardingChecklist.computeChecklist(): pure computation of the six rows (profile, importFolder, source, import, manualCategory, budget), with the budget row locked and excluded from the total when the edition has no Budget - hooks/useOnboardingChecklist: exported reducer + initial state, actions refresh/hide/show, composed with useEntitlement("budget"); a load error sets status "error" and keeps the hidden flag - types OnboardingCounters, ChecklistItem, ChecklistItemKey Tests cover every row in isolation, the budget gating (total 5 vs 6, complete at 5/5 locked and 6/6 open), the aggregated query and preference reads, and every reducer action. No DB migration, no CHANGELOG entry (centralised in #357). Resolves #351 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>Self-review — Verdict: APPROVE
Autonomous review of the full diff (
GET /pulls/377.diff, 9 files, 2 commitsf77ce94+76fd8d3) against the acceptance criteria of #351.Summary
Pure data layer: one unfiltered count, one aggregated five-count query read with the two preferences in a single
Promise.all, a purecomputeChecklist, and an exported reducer composed withuseEntitlement("budget"). No UI, no migration, no i18n key, no CHANGELOG edit. The tidy commit76fd8d3only removes a duplicatederrorMessage(e)call found during this review.Security
userPreferenceService(parametrised$1/$2). No string interpolation into SQL.Acceptance criteria
computeChecklist: each row tested in isolationonboardingChecklist.test.ts— source, import, manualCategory (and nottransactions), budget, importFolderbudgetAllowed=false→total=5,budget.locked=trueto="/settings/users")completetrue at 5/5 locked and 6/6 opendone=2done=1useOnboardingChecklist.test.tshidden, setsstatus="error"refreshaftererror→loadingthenreadygetTransactionCountwith mockeddb(0 and N)transactionService.test.ts(0, 1234, no row)npm test+npm run buildgreen, no UI touched.tsxin the difftoper row,/settings/usersfor locked budgetstatus = 'completed',is_manually_categorized = 1,budget_entries, unfilteredtransactionshideChecklist→"1",showChecklist→ delete,logInfobothlogInfoper call, none on failuresrc/shared/types/index.tsOnboardingCounters,ChecklistItem,ChecklistItemKeyQuality
licenseReducer/feedbackReducer/migrationReducer.getOnboardingCounterstolerates an empty result set (falls back to zeros) like the otherCOUNT(*)readers.Concerns (non-blocking)
hide()vs in-flightrefresh():LOAD_DONEapplies thehiddenread at the start of the load. A dismissal clicked while a refresh is running could be undone by that load's result. The window is a few ms of local SQLite reads and the Hide button only exists once the list is shown; if #352 triggersrefresh()after imports, it may want to call it before re-rendering the Hide control, or a sequence guard can be added then.tovalues for six rows in the issue: resolved by mapping the five uncheckable rows (import →/import, manualCategory →/transactions);profile.tois never rendered (#352 links unchecked rows only). Flagged in the PR body for Max.Generated autonomously by /autopilot run of 2026-10-03
Revue adversariale (passe indépendante) — Verdict : APPROVE
Revue de la tête
76fd8d3contremain, indépendante du self-review du worker (commentaire 19000). Rien d'exécuté dans le dépôt : copie hors dépôt (git archive FETCH_HEAD), supprimée en fin de revue.Résumé
Socle de données conforme au contrat de #351 : requête agrégée identique à celle de l'issue (tables et colonnes vérifiées dans
consolidated_schema.sql;status = 'completed'correspond aux deux seules valeurs écrites par le wizard,useImportWizard.ts:1009et:1060),computeChecklistpure, reducer exporté qui préservehiddensur erreur,useEntitlement("budget")composé comme demandé. Aucune migration, aucune chaîne UI, CHANGELOG laissé à #357 comme la vague l'impose.Ce qui a tourné
tsc --noEmitpropre.unlocked = items(ligne verrouillée comptée danstotal) → 3/14 rougeshidden: hiddenPref !== null→ 1/10importFolderSet: importFolder !== null(dossier vide = choisi) → 1/10LOAD_ERRORremethidden: false→ 1/10return rows[0].cntsans repli → 1/3/budget→ 1/14LOAD_DONEignoresnapshot.hidden→ 2/10check-rustcorrectement sauté (aucun fichier Rust).pull_refclosesdepuis #377 → fermeture au merge. Commits en format conventionnel, les deux référencent #351.skip/onlydans les 4 fichiers de test.Suggestions (non bloquantes)
src/utils/onboardingChecklist.ts:50— la lignebudgetpeut être{ done: true, locked: true }(entrées de budget créées avant le gating, puis édition Gratuite). #378 s'en sort en résolvantlockedavantdone; une phrase dans le doc-comment deChecklistItem(«lockedprime surdone») protégerait un troisième consommateur.src/hooks/useOnboardingChecklist.ts:92-113—hide()persiste puis dispatche ; unrefresh()en vol qui a luhidden=falseavant l'écriture et se résout après remethidden=falsejusqu'au prochain chargement. Sans effet tant que le dashboard ne rafraîchit qu'au montage ; à revoir si #352/#353 ajoutent un refresh sur focus (compteur de requête pour ignorer unLOAD_DONEpérimé).docs/architecture.mdnon mis à jour pour le nouveau service et le nouveau hook — confirmé centralisé dans #357, rien à faire ici.Actions faites
status:approved(self-review), label inchangé.pr378.diffsupprimés du scratchpad ; dépôt de travail intact (git statuspropre).