`initializeGuidedPath` opened BEGIN, read the flag, then COMMIT at every
mount of the provider: start-up, and every profile switch, creation or
deletion. While a `withTransaction` callback runs, `db.ts` lets every
other query bypass its lock, the overlap its serialization exists to
prevent.
The flag is now read first, outside any transaction, through the queued
connection: once D9 ran, an opening costs that one read. Without it, the
transaction re-reads the flag before writing, so two first loads still
cannot apply the rule twice (CWE-362).
Pinned: no transaction and no write with the flag set; the flag counts
only for "1", on both reads; a first load that sets the flag between
the two reads wins; the in-transaction reads still follow BEGIN.
Review of #396 (PR #402).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Import a first file" step counted `imported_files` rows in status
`completed`. `executeImport` writes that row before its first insert, so
an import that added nothing (a file holding only its header, or one
whose rows were all in error in the preview) ticked the step: the Import
card then said "Done." at rest and sent the user to /transactions, which
had nothing to categorize and led back to the import.
The counter now keeps a completed file only when a transaction points at
it (`EXISTS` on `transactions.file_id`, covered by
`idx_transactions_file`). Replayed with sqlite3 on
`consolidated_schema.sql`: 0 after an import without a valid row, 0
before a successful import's first insert and 1 after, 0 once the file
turns to `error`. Duplicates only change nothing: their transactions
came with an earlier completed file, and a restore writes its own.
The tests run without SQLite, so the rule is pinned on the query, and
the Import page is pinned at rest after such an import: step 4 again,
on the first source card, without "Done.".
Review of #398 (PR #404), option A chosen by Max.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checklist state moves from DashboardPage to OnboardingProvider, mounted
in AppShell around the routed page; pages read it through useOnboarding().
The reducer stays in useOnboardingChecklist:
- loads are numbered and a stale answer is ignored;
- HIDE, SHOW, SKIP and RESUME win over the snapshot of a load started
before them (#384);
- a refresh keeps the counters and the ready status, and a failed re-read
after a first success keeps the last state; only the start of a failure
streak is logged;
- re-read at mount, on every route change and 150 ms after a burst of
data events.
onboardingService: getOnboardingSnapshot replaces getOnboardingCounters and
is a pure read (skipped steps included, one preference per step, filtered on
the six keys and the value "1"); initializeGuidedPath applies rule D9 once
per profile in one transaction (hidden first, then the flag, ROLLBACK on
failure), awaited once by the first load through a promise shared by
StrictMode's double mount; skipStep / resumeStep are idempotent.
computeChecklist counts skipped rows; the new pure onboardingPath module
holds nextStep, CARD_PAGES (empty for now), stepsOfPage, the card latch,
bannerMode and readOnboardingNavState. The dashboard reads the context and
keeps its compact bar for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>