The page help and the guide (Markdown and in-app) described a Budget split
button removed in v0.2.5, an Annual-column warning that can no longer
appear, report tabs and four report cards (the hub has five, no tabs), a
"Category Zoom" whose displayed title is "Category Analysis", and a "+"
icon on transactions (the keyword button is a tag icon, shown on
categorized rows only; the right-click menu works on every row).
Resolves#431
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both Transactions sentences name the context menu's "Add as keyword" and
carry the advice on the word and the boxes (D18); both Budget sentences say
only last-level rows take an amount and what the Annual column does. The
labels go through StepCardText.labels, quoted in the locale files (D11).
The keyword dialog is unchanged; a test pins that it marks the transactions
as categorized by hand, so the step still checks off through it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The source name typed during configuration was saved, but the next import
looks the source and its imported files up by the subfolder name, so a
renamed source looked new: format guessed again, every file pre-checked,
and a second source created under the folder name.
The name field becomes a read-only SourceNameDisplay showing the subfolder
name with a hint, and executeImport writes selectedSource.folder_name on
create and update, whatever config.name holds, behind a selectedSource
guard. The import card sentence no longer asks to check the source name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scan_import_folder now returns ImportFolderScan { sources, root_files }:
the CSV/TXT files at the root, which no source reads, are named in a
warning on the Import page (above the source list, or under the "no
source" empty state). One action sets sources and root files, RESET keeps
both. Names stay on screen, never in the Journal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Import empty state now shows an "Expected structure" box in both of its
variants (no folder, folder without sources): the parent folder to choose,
one subfolder per account, and files at its root ignored, as
scan_import_folder reads them. New ImportFolderStructure component, a nested
list with decorative icons.
The getting-started cards ask for the parent folder and one subfolder per
account instead of one per bank. The preview card says that an expense must
count among the outflows and names "Flip the signs" next to "Check
duplicates": WIZARD_ROWS rows now carry every label their sentence names.
Every Import card sentence quotes the controls it names, in French and in
English, pinned by a test that reads the locale files.
Help tips, docs.import.steps, the user guide, the QA list and architecture.md
follow; CHANGELOG entry under Changed.
Resolves#422
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildBudgetYearRows sorted each top group by depth, so an expanded
subcategory showed at the end of its group instead of under its parent.
It now builds one block per child (lone leaf, or [subtotal, ...children]),
sorts the blocks once ("(direct)" first, then by name, leaves and
sub-groups mixed, as the Budget vs Actual report) and concatenates them;
top groups are sorted by type, sort_order and name.
reorderRows put a leaf that follows a sibling sub-group into that
sub-group's block. A row at the subtotal's depth or shallower now closes
the block, which also fixes "subtotals at the bottom" in the Budget vs
Actual and Compare reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The small badge next to "Fetch price" on a stock rendered the literal
"best-effort" without going through t, so French users saw English.
It now reads balance.priceFetching.bestEffortBadge ("non garanti" in
French). Raised by the review of PR #435 (suggestion 4).
Refs #389
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- withoutFolders no longer keeps the first word of a folder that holds
a blank (`C:\Users\Jean Dupont\...\profile_x.db` gave
`Jean profile_x.db`): a path starts at a recognised prefix (drive,
`/`, `~/`, `./`, UNC, `<profile folder>`) and runs to its last
separator, blanks included, until a line break, a double quote, a
backtick, `;`, `|`, a `,` or `:` before a blank, a closing parenthesis
not followed by a separator, or a blank before another path. When
nothing ends it, the text up to the last separator of the line is
dropped rather than a folder let through. A quote before the path is
not part of it, so `'C:\Users\Bob\x.db'` gives `'x.db'`. Relative runs
with no recognised prefix keep the previous fallback. No error line
contains a `/` nor a `\`, as before.
- ADR 0021 section 7 describes that rule and its limit.
- CHANGELOG.md, CHANGELOG.fr.md: no blank line between the two Fixed /
Corrigé bullets.
Refs #395
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- renderCategoryLabel.test.ts passes markedT itself, its assertions in
the marked form (`t(balance.category.cash[defaultValue=cash])`): the
second fake `t` of the repository is gone.
- i18n-mock-guard: a fake `t` is also caught as a binding (`t = (`,
`t = vi.fn(`) or as a `t: vi.fn(` property, and a file that mocks
react-i18next must call `i18nMock(`, comments dropped before the
search. Each new rule has its own case in the guard's tests; the
binding rule found renderCategoryLabel.test.ts before its migration.
- docs/architecture.md: the "only fake t" sentence is scoped to test
files and says the guard only refuses the forms it recognises.
Refs #389
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- useOnboardingChecklist.wiring.test.ts: a data event, then a route
change 100 ms later, still gets its deferred re-read (two LOAD_START).
Pins the follower's deps: with `pathname` among them, the cleanup drops
the pending timer and the test goes red (mutation M1 of the review). The
comment that claimed "one subscription only" now points to this test.
- hookHarness.test.ts: a dependency array that shrinks, or grows by
`undefined`, recomputes the memo (mutation M8, length branch of
depsChanged).
- NOT SIMULATED, in hookHarness.ts and docs/architecture.md: the dispatch
of a useReducer keeps the reducer of the first render, where React
applies the latest one.
Refs #409
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- close() on a getDb() handle runs as a "connection close" section and
checks its generation: a handle kept from a previous profile rejects
with DbConnectionClosedError instead of closing the current profile.
- closeUnlocked never rejects: the state is cleared and the generation
bumped before close(), a failing close of the old pool is logged with
logWarn, connectToProfile still opens the next profile and closeDb()
resolves without a connection.
- The blocking alert names a getDb() call in a callback as a possible
cause only: a long restore also waits 15 s.
- The generation check of the transaction handle is documented as a
defence, not reachable today.
- Tests: the "behind the queue" alert (pins the reset of lockHolder),
close() on a stale and on the current handle, a failing close during a
switch and in closeDb().
Refs #408
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ProfilePreparation.catch_up_backup carries the file name of the copy as
soon as it exists, so the Journal error line of a catch-up that stops
afterwards (history_write_failed, pragma_failed, apply_pending) names it.
- Error lines and the JS fallback message keep only the last segment of a
path (withoutFolders): no error line contains a slash or a backslash.
- The copy is taken again at every catch-up, written as .tmp, synced and
renamed over a same-name copy; after a successful catch-up only, the
other copies of that profile are pruned (remove_backups keep argument).
- Pins: a pre-existing foreign key violation in the profile without
category 31 (RV4, RV7) and the Journal line that reports it (RT2).
- Ignored test measuring prepare_at on 100 000 transactions at floor 6:
306 ms in release, so STARTUP_TIMEOUT_MS stays 10 s (ADR 0021).
- Guide, docs.profiles.tips, ADR 0021, architecture and changelogs.
Resolves#395
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 10 s startup delay of App.tsx sets the error page while the profile
is still opening; a connection that succeeded afterwards set dbReady but
left the error, so the error page stayed. The startup logic moves to
src/utils/startupConnection.ts (startProfileConnection), which clears the
error on success and is tested with fake timers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/__integration__/i18n-mock-guard.test.ts reads every test file of src
and fails on a useTranslation: or t: ( property (word-bounded) outside
src/test/i18nMock.ts, or on a react-i18next mock that does not go
through i18nMock. docs/architecture.md gets the convention: never a raw
t, always i18nMock.
Resolves#389
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 12 test files that already marked their output each carried a copy
of the same fake t. They now use the shared i18nMock; no assertion
changes. LicenseCard keeps its per-test language through a getter.
Refs #389
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 15 test files whose fake t returned the bare key (the 12 of the
issue, plus the three category render tests added by #423) now mock
react-i18next with i18nMock. Assertions naming a displayed text are
rewritten in the marked form, t(key[options]), so a text that skipped
t fails them. Components without such an assertion get one:
PriceFetchControl, PriceFetchConsentToggle (rendered through the hook
harness, its loading slot preset), CategoryDetailPanel, SourceList and
SourceCard, and the region label of OnboardingChecklist.
Each component was proved by a mutation removing one t call: every one
turns at least one test red (exit 1).
Refs #389
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/test/i18nMock.ts holds the one fake t of the suite: markedT renders
t(key) or t(key[a=1,b=2]), and i18nMock({ language?, fixture? }) builds
the react-i18next module for vi.mock. The local echo of
useLicenseMachines.test.ts is replaced by markedT.
Refs #389
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useOnboardingChecklist.wiring gains three pins the old stubs could not hold:
one loader for the provider's life (refresh and the shared value keep their
identity, no re-read), 'Skip' moving the next step on the next render, and a
change of mode alone reaching the consumers. UsersSettingsPage now commits
its effects, so an effect that read the preference goes red on the
assertion rather than on 'Invalid hook call'. BudgetTable.wiring,
BudgetPage.wiring, AppShell.slot and OnboardingStepCard drop their local
slot copies; OnboardingStepCard plays StrictMode through replayEffects and
runs its effect only on a changed dependency, which now pins location.key.
Refs #409
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/test/hookHarness.ts gives the tests that call hooks and components as
plain functions one fake React with React's semantics: state initialized
once with stable setters, the reducer applied with a dispatch spy, refs kept,
memos and callbacks recomputed only on a changed dependency, effects run by
commit() only when a dependency changed (cleanups first), and every hook it
does not cover throws instead of reaching the real React. Each semantic is
proved by hookHarness.test.ts, which also checks that the module imports
React only as a type.
Refs #409
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The global inTransaction flag let every execute/select bypass the FIFO
lock while a withTransaction callback ran, wherever it came from. The
callback now receives its own handle, which runs directly, refuses
close() and throws once the callback settled; any other call, getDb()
from a callback included, queues behind the transaction.
connectToProfile, initializeNewProfileDb and closeDb each run as one
exclusive section (close the old pool, prepare, load), and every open
or close bumps a connection generation: an operation queued for a
closed connection throws "connection closed by profile switch". An
operation waiting more than 15 s for the lock writes one Journal
warning naming the holder and since when.
findOrCreateSecurity now requires its executor: its getDb() fallback
would deadlock if called from a transaction callback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #423
- parentOptions.ts: say that this list is the only guard today (updateCategory
checks neither depth nor cycle, #393 will), instead of pointing to a
service-side validation that does not exist yet. The height walk is also
bounded by the number of categories, so a missing cycle guard gives a wrong
height instead of an endless loop; a test edits a member of a cycle.
- AddKeywordDialog.tsx: drop `categoriesLoaded`, which only repeated what the
null initial category already guarantees.
- Tests: the form and the detail panel are pinned to what they hand to
buildParentOptions (editingId, currentParentId), through a pass-through spy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Refs #423
The review of PR #428 showed that the rule only looked at direct children:
editing a root that carries grandchildren still offered the other roots as
a parent, and saving created a fourth level, which nothing guards against
in updateCategory or moveCategory. The allowed parent depth is now
MAX_PARENT_DEPTH minus the height of the edited sub-tree (0, 1 or 2), so a
category with grandchildren gets no parent at all, only its current one.
Test written first on the reviewer's probe ([1, 10->1, 100->10, 2],
editing 1: [] expected, [2] obtained before the fix).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fixes#423
The "Parent category" field of CategoryForm and the "Category" field of
AddKeywordDialog were native selects with no typing. Both now use
CategoryCombobox, like the category cell of the Transactions page: typed
letters filter the list, which is ordered hierarchically with indentation
and shows seed categories through their i18n_key.
- CategoryCombobox accepts a minimal structural type (CategoryLike), so
CategoryTreeNode rows pass without conversion; sortHierarchical is
generic over it. Callers are unchanged.
- buildParentOptions (pure, tested) keeps roots and level-1 categories,
leaves out the edited category, and keeps only roots when the edited
category has children. That rule alone also leaves out its descent (a
descendant is never a root), so no separate subtree walk is needed. The
parent currently stored is always kept so the field never looks blank.
CategoryForm gets an editingId prop, passed by CategoryDetailPanel.
- AddKeywordDialog reads the Transactions list (active and inputable,
decision D2) through loadKeywordCategories, keeps the pre-filled
category only when it is in that list (reconcileInitialCategory), and
holds the apply button until the list is in. Its two fields sit above
the scrolling block of matches, so the open list is not clipped by the
scrollport.
- Tests: parentOptions, keywordCategory, and static renders of both
components. Changelogs and the user guide (with its i18n mirror).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review follow-ups of PR #414.
- audit.yml points to the concurrency comment of check-rust.yml, whose
measurement covers push and pull_request runs only: say this workflow
runs on schedule and workflow_dispatch, which it does not cover.
- reportService.test.ts said CI runs Node 20, so no node:sqlite; CI runs
Node 24 since #376, and the harness itself is #392's.
- The release skill said /actions/tasks ignores `limit`. Measured
2026-10-12: only without `page` (613 rows); with `page` it pages by 30
or by `limit`.
Refs #376
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Guide (FR, EN, in-app keys and `.md`) and CHANGELOG: the Import card
never says "Done." for an import that added no transaction, and the
step waits for a file that brought one; a Fixed entry, since v0.17.0
already ticked the step after such an import.
- CHANGELOG: the Free edition shows the Budget step locked, it does not
lack one.
- architecture.md: the import counter's query, and D9's flag read
outside any transaction, its log line written after the COMMIT.
- ADR 0022: `applyMigration` and `deleteTransaction` named among the
writes that do not notify, with their reason.
- QA list (44 cases): the noop import at rest, the focus ring on the
highlighted control, "Skip" on the folder step without a folder, the
Transactions and Budget cards on a profile without transactions, the
focus after "Hide" and "Close", the doubled counter of the unfolded
list, and a month-agnostic note on the off-screen cell.
Reviews of #402 to #407.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a profile without transactions whose import steps were skipped, the
rows can land before the transaction count, both reads queueing behind
the DB lock: the grid mounted, reported "Expand all", then gave way to
the empty state, and the card kept "Click Expand all…" above it with no
grid on the page. The page now passes no target while it shows the
empty state, and a wiring test replays that sequence. Invisible in dev,
where StrictMode reorders the two reads.
The month test sets the clock to 23:00 on December 31, which is still
December in UTC: under the CI runner's zone, a month read in UTC passed.
The zone is pinned west of Greenwich, as in useImportWizard.test.ts.
Review of #400 (PR #406).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reducer tests sent SET_PAGE_RESULT right after the initial state,
while the hook sends SET_LOADING then SET_ERROR(null) before each read,
and the mount effect's categories, sources and count before or after
the first page. Had any of those three actions disarmed the guided
fallback, the fallback would never run in the app, and the tests stayed
green: a test now reduces both real orders.
The page tests now also let the list's filters move, as a later
dispatch would: after the fallback the card names every transaction,
and an ordinary visit with "Uncategorized" picked by hand names the
uncategorized ones. The sentence follows the list, not the navigation
state.
Review of #399 (PR #405).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring of `[data-onboarding-highlight="true"]` is an author outline:
it wins over the browser's focus ring, so a keyboard user who reached
the highlighted control saw nothing change (WCAG 2.4.7, failure F78).
Measured by the review in headless Chromium, WebView2's engine: the
highlighted button kept `solid 3px` focused or not. `outline: revert`
under `:focus-visible` gives the browser's ring back, kept as is in the
built stylesheet; the contrast test reads the rule from the stylesheet.
Review of #398 (PR #404).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0022 wants a transactional write to notify once `withTransaction`
has returned, never from its callback. Only the categorization test
checked the lock; the restore tests checked that the notification
followed the COMMIT, which a notification sent from inside the callback
also satisfies. Each restore mode now asserts the lock was released.
Review of #396 (PR #402).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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 guide and the changelog said the card never says "Done." for an
import that added no transaction. It holds it back on the report only:
on the Import page the card moves from one step to the next, and says
"Done." once the file is imported. The locale test no longer depends on
the order of the sections in the files, which DocsContent ignores.
Resolves#401
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DocsContent and DocsSubsections read the guide straight from fr.json and
en.json, but their tests run on fixtures: a string where an array is
expected, a sub-section without its steps or a French list longer than
its English one only showed in the app. DocsContent.locales.test.ts reads
the real files: every section DocsContent renders and no other, the
strings and string arrays it reads, sub-sections that readSubsections
keeps whole, anchors of their own, French and English item for item, and
the path's sub-section in the section "Open the guide" opens.
DocsContent exports SECTIONS for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Getting Started" section, which "Open the guide" opens at the end of
the path, gains a "The getting-started path" sub-section in both locales
and in docs/guide-utilisateur.md: the banner and its list, the step cards
and their outline, "Skip this step" and "Resume", the end of the path,
the profiles that find it hidden from the start, and how to show it again
from Settings -> Users. It uses the sub-section shape DocsSubsections
already reads for the Import section.
The Dashboard and Settings sections no longer describe the compact bar
the banner replaced, and the Profiles section says the path belongs to
each profile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget grid opens collapsed and only its leaf rows take an amount,
so the card first points at "Expand all", then at the current month's
cell of the first editable row shown (D12). `budgetOnboardingTarget`
(pure, beside budgetTableResults.ts) decides it from the rows each
section shows, in the order it shows them; the collapse state stays
BudgetTable's own and the card never unfolds the grid.
BudgetTable rings that one control when the page asks
(`onboardingTarget`) and reports its kind through
`onOnboardingTargetChange`, in an effect, at mount and on change only:
the page needs it for the card's sentence before it asks for the ring.
`budgetCardModel(input, checklist)` gives the "collapsed" sentence for
"Expand all", the "flat" one for a cell, no sentence and no ring before
the grid says, and "Done." once the budget step is checked, which the
card needs to end the path. CARD_PAGES gets /budget. BudgetPage renders
the card first, outside the loading dim the grid gets after each save,
and asks for the ring only while the card shows. The Free edition never
mounts the page and never latches it: its path ends at 5/5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The link of the "categorize by hand" step carries `guided`, which the
Transactions page reads through `readOnboardingNavState` and turns into
start filters: `useTransactions(initialFilters?)` puts them over the
defaults through `initTransactionsState`, at mount only, so the card
consuming the navigation state changes nothing. The guided entry lists
the uncategorized transactions whatever their date (the default filter
starts on January 1st, and in January the sample statements fall in the
previous year). When its first page comes back empty, the reducer drops
"uncategorized only" once: a new filters object, so the existing filter
effect reads again, and the loading placeholder meanwhile. The first
page and any filter the user sets disarm the fallback, so it never loops
and never drops a filter behind the user's back.
`transactionsCardModel(filters, checklist)` says which sentence matches
the list, names the filter by its own label, points at the first row's
category, and says "Done." once the step is checked, which the card
needs to offer the Budget step or the end of the path. The label loses
its capital through an i18next `lowercase` formatter used by the
sentence itself, `{{filter, lowercase}}`.
TransactionTable wraps every row's category in the same element and
puts the ring on the first one only, so the ring coming or going never
remounts the combobox; CategoryCombobox is unchanged. CARD_PAGES gets
/transactions, and the page renders the card first and highlights its
target only while the card shows.
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>
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>
Settings → Users no longer reads the hidden preference at mount: it
takes hidden and show from useOnboarding(), the provider being the only
writer of the path's preferences. Hiding the path from the banner of
this very page now offers "Show the getting-started checklist" at once,
and a local flag keeps the confirmation in place of the button until
the path is hidden again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The getting-started path now follows the user: AppShell mounts
OnboardingBanner at the top of <main>, in the page flow, and the
provider's mode keeps it off a hidden path, the dashboard of a profile
without transactions, a page that carries a step card and the first
load. The banner shows "Getting started n/N" once the Budget
entitlement is known, the next step linked with its navigation state,
"Show the list", which unfolds the detailed list in place, and "Hide";
at the end, the closing message, "Open the guide" and "Close". Each
route mounts its own banner slot, so a page always starts folded.
The dashboard's compact bar and the compact mode of
OnboardingChecklist are gone (D7). The detailed list reads each state
out to screen readers, shows a skipped step with its badge and
"Resume", and gives every link to a step the same navigation state
(stepNavState), with the guided entry for the manual category.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
renderToStaticMarkup runs no effect, so the hook's own wiring was guarded by
no test: the route re-read (pathname in the deps), the data-channel
subscription and its cleanup, the card latch stored during render, and the
order "write the preference, then update the state" of hide, show, skip and
resume. React's hooks are stubbed so the hook runs as a plain function on a
prepared state, its effects captured and run by hand.
Resolves#396
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>
Add src/services/dataEvents.ts (ADR 0022): notifyDataChanged() and
subscribeDataChanged(), without React, on the model of logService.subscribe.
A throwing subscriber is logged and does not stop the others.
Notify after every successful write of the tables the getting-started
counters read, plus the import folder: setImportFolder, createSource,
deleteSource, createImportedFile, updateFileStatus, both import deletions,
insertBatch, updateTransactionCategory, applyKeywordWithReassignment (after
COMMIT), the four budget writes and runRestore (after COMMIT, one point for
the three restore modes). executeImport notifies once more when it ends,
succeeded or not. A failed write notifies nothing; transactional writes
notify once withTransaction has returned. applyTemplate notifies once for
the whole template through a private writeBudgetEntry helper.
Each service suite proves the notification after a success and its absence
after a failure; executeImport is driven as a plain function with React's
hooks stubbed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the /pr-review of PR #394:
- the profile database opens only once prepare_profile_db has resolved
(deferred preparation; a load started early used to pass, RT1);
- a real VACUUM INTO failure, through a dangling symlink, names the full
path and comes out scrubbed (RV6); the test first checks the raw message
does name the folder;
- deleting a profile removes the -journal side file, the only one this
app produces (RV5);
- test folders are removed when each test ends, a failing one included
(TestDir guard), instead of 42 folders left in /tmp per run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0021 records the catch-up rules and amends ADR 0003 and 0005, whose
claims about secondary profiles were false. architecture.md, CLAUDE.md
and the migration rule describe the two migration paths; the guide tells
where the pre-update copy lives and how to remove a database a deletion
on Linux left behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tauri-plugin-sql migrates only the database registered with add_migrations,
the default profile's. A secondary profile kept the consolidated schema of
the version that created it and never received a migration afterwards.
prepare_profile_db runs before every Database.load:
- secondary profiles catch up with the same migrations() list (pinned
SHA-384), history kept in _sqlx_migrations; a database without history
gets a floor inferred from its schema (the seven released creation
states are test fixtures), so nothing at or below it is ever replayed;
- a VACUUM INTO copy is taken before the first write, synced, then renamed;
- each version runs in its own transaction with foreign keys on; one that
fails on a foreign key is retried once without them and its orphans are
set to NULL where the key declares ON DELETE SET NULL;
- every profile gets its TEXT manual-categorization flags repaired;
- failures are logged with file names only and the profile still opens.
New profiles are born with their full history (get_new_profile_init_sql).
The plugin keeps databases under app_config_dir(), not app_data_dir():
the same folder on Windows and macOS, not on Linux. repair_migrations,
delete_profile_db and compute_account_return looked in the wrong folder
there. profile_db_path resolves the plugin's folder and validates the
file name; deleting a profile now also removes its SQLite side files and
catch-up copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateTransactionCategory bound the JS boolean `true`. tauri-plugin-sql
passes a bool to sqlx as a JSON value, so the column received the TEXT
'true', which the getting-started checklist's `is_manually_categorized = 1`
never matches: step 5 never ticked.
toSqlFlag turns a boolean, a number or a backup's string into 0/1. It is
used when a category is set by hand and when a JSON or CSV backup is
restored, since a backup exported while the bug stood carries "true"
(the CSV path read it as 0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-ups of the three /pr-review passes on #386, #387 and #388, applied
on the stack head before the fast-forward merge:
- #386: pin the relay of the revoked state by revocationCheckLauncher
(a launcher passing false instead of wasRevoked passed the whole suite),
and say in the rate budget comment that two checks in one minute bring
the machine-limit path to 6 requests.
- #387 S1: the fake `t` of the removal tests and of the License card tests
marks what it translates, so a string that skipped `t` fails its test.
- #387 S2: pin that storing another key stops on Storage while the
previous token cannot be dropped, the previous key staying.
- #387 S3: reload the license context after a failed removal too. Rust
deletes the key first, so a token that resisted left the key gone while
the card kept showing it under "the key was not removed".
- #388: pin that an unreadable token stays (the directory case could not
tell, its deletion fails anyway).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>