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>
- architecture.md: the Onboarding section describes the guided path as
delivered (provider and useOnboarding(), the data-change channel of
ADR 0022, the three onboarding preferences, rule D9, the pure models of
onboardingPath.ts, the banner, the card latch and CARD_PAGES, the step
cards, the highlight by props and CSS); the utils count and the
user_preferences row follow the stack.
- qa-onboarding.html: the phase 1 cases that still hold, without the
compact bar ones, and four guided-path groups, including the manual
checks the five links left for a release build (WebKitGTK and WebView2,
Orca and NVDA, reduced motion, keyboard, Free edition, rule D9).
- CHANGELOG (EN and FR): one entry for what every step card does, one per
page, the path hidden on active profiles covers the banner too, the
skipped state, and the guide's new sub-section.
- STATE.md: manual checks due on the next release.
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>
Added: the getting-started card of the Budget page. architecture.md
stays as it is: no line of it is made stale (the budget folder still
holds five components, the pure module is not one).
Resolves#400
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>
Added: the getting-started card of the Transactions page and the guided
entry its step link opens. architecture.md gets the lines this change
touches: useTransactions' start filters and single fallback, and the
i18next `lowercase` formatter in the Internationalisation section.
Resolves#399
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>
Added: the getting-started card of the Import page. Fixed: the "No import
folder" empty state no longer flashes while the folder is read.
architecture.md gets the lines this change made stale: the component
count, the onboarding folder, useImportWizard's `importFolderLoaded` and
the Import row of the empty states table.
Resolves#398
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>
CHANGELOG (EN and FR): the banner under Added; the compact bar replaced
by the banner, the states read out by the list, and the re-show button
of Settings → Users under Changed. docs/architecture.md: the lines this
change made false (component count, the onboarding/ folder, the compact
mode and the re-show sentences). The user guide is rewritten by the
last issue of the milestone.
Resolves#397
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>
ADR 0022 records the channel: its scope, the writes that notify and when,
the 150 ms wait on the subscriber side, stale answers ignored, and who may
subscribe. architecture.md gets its row in the ADR table, plus the rows this
change made wrong (onboardingService, useOnboardingChecklist, the new
dataEvents service, the contexts folder). The changelogs record rule D9: a
profile that already had transactions hides the getting-started checklist
the first time it is opened with this version.
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>
The released-states loop stops at v0.10-v0.14 (state E). State F, the
consolidated schema of v0.15.0 to v0.17.0 without a history, only had
its floor checked; this test runs the whole preparation on it: history
recorded up to v17, nothing applied, no copy, flag repaired.
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>
v0.17.0 is tagged on 07255e1 and published by release run 491: seven
artifacts and latest.json checked, the updater endpoint serves 0.17.0.
The update-cycle check was not due (nothing on the updater path changed
since v0.16.0) and the reason is written on the release. #234 is closed
on its fourth proof (actcache at 999 MB, one entry) with its milestone,
and the two suites-licence texts are posted on la-compagnie-maximus#184.
Rotate out the 2026-09-27 #270 decision to keep ten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The autopilot stack of 2026-10-04 is merged fast-forward through the API,
main at 2cbc886, milestone planned-2026-10-03-suites-licence closed 3/3.
The seven review suggestions were applied on the stack head (2cbc886),
each proven by a red mutation. Record the decision, the manual checks due
with v0.17.0 (ordered so the activation token is destroyed last), #234
points 2 and 3 done, and the #389 follow-up. Rotate out the 2026-09-24
decision to keep ten.
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>
The English in-app tip now says, as the French one does, that the
revoked license is that of a refunded key left next to the current key,
not the key itself. The header of the price tests says that the tests of
the stored token call fetch_price_in with a temporary data directory,
instead of claiming that every test bypasses the token file.
Resolves#367
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A data directory written by v0.15.0 or earlier can hold a valid key next
to the activation token of another license, typically a refunded hence
revoked one. The License card read "Activated", market prices were
refused, and "Activate" was never offered: the folder could not recover
without a manual file removal.
- Prices: a 403 license_revoked drops the refused token, and only while
the file still holds it, so an activation that lands while the refused
request is in flight keeps its new token (CWE-367). The key's revoked
state is never written from this route. The token is read through
ACTIVATION_FILE by the directory-based read_stored_activation_token_in,
in fetch_price_in, testable without AppHandle.
- Machine list: list_activated_machines returns { machines,
token_dropped }. A complete list that names this machine under neither
identifier drops a token that activates this machine, when the OS id is
readable and the token is the one on disk when the list was asked for.
An error, a revoked key or a rate limit drops nothing.
machines_from_verify counts the rows it skips, which the list still
tolerates. This also gives "Activate" back to a machine deactivated from
another computer.
- Front: the activation status is read again after every list load; a
fixed log line (ACTIVATION_TOKEN_LOG) is written on token_dropped and on
a price refused as revoked; the revoked-license price message invites to
activate the machine again.
- Docs: architecture, ADR 0017 (#365 amendment), ADR 0020 (rule 4), user
guide and in-app tips, changelogs.
Resolves#367
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Remove the key" on the License card removes license.key, then the
activation token, and keeps license.check, so a revoked key removed then
entered again stays Free. Removing the key stops the revocation check,
which until now only storing another key could stop.
When this machine is activated with a valid key, its slot is freed on
the license server first (remove_license with release_slot). A release
that fails removes nothing; on network, server, rate_limit or
invalid_response the card asks whether to remove the key anyway, the
slot then staying occupied. no_active_activation, license_not_found and
license_revoked (recorded for the key sent) mean the slot is free.
- Rust: remove_license / remove_license_in, release_this_machine_slot,
this_machine_token_id, and the shared remove_activation_token_in
helper, now also used by store_license_in and the deactivation.
- Front: removeLicense service; performRemoveLicense in
useLicenseMachines (confirmations by state, error table, in-app log
lines); removing and removeError in the machines reducer, which lock
key entry, Activate and Deactivate; the button for any stored key,
valid or revoked.
- Docs: user guide, in-app tips, architecture, ADR 0017 amendment,
changelogs.
Resolves#370
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three tests of #373 run two branches with tokio::join! and rely on
the first branch being polled first: the check that starts first, or the
check that waits for a held turn before the time moves. tokio's join!
already starts with the first branch on its first poll, but rotates on
the next ones; `biased;` keeps the declared order on every poll and says
so in the code. The three mutations (no turn, time read before the turn,
guard dropped at once) were run again: same red tests.
Resolves#373
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A license key entered during a session was only checked against the
license server at the next start: a revoked key kept its edition until
the app restarted, or until a machine activation was refused (#365
checked at start only).
Front: performSubmitKey launches the revocation check after
VALIDATE_DONE, without waiting for it, and never after a refused key.
SubmitKeyDeps gets a single launcher, startRevocationCheck(wasRevoked),
built once by the provider with revocationCheckLauncher and shared with
the start check, whose starter is unchanged.
Rust: the command runs the checks one at a time. A static
tauri::async_runtime::Mutex (REVOCATION_CHECK_TURN) is held by
check_revocation_in_turn over the whole check, request and verdict
included, and the time is read once the turn has come. A key entered
before the first ready state, checked by the entry and by the start,
sends one request; the check of a key entered while another one is in
flight waits for its verdict instead of overwriting license.check.
check_revocation_in is unchanged, and so are Cargo.toml and Cargo.lock.
Docs: ADR 0017 (amendment #365, point 5), user guide, in-app docs
(docs.settings.tips, docs.editions.tips), architecture, and both
changelogs under [Unreleased] (Fixed / Corrigé).
Resolves#373
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#350: the #365 check had stored the revoked key on the Linux machine,
which deleted the v0.15.0 activation token; a read-only local script
confirmed it. Re-entering the tester key and activating produced a token
carrying the derived identifier, so step 3 holds in production. Steps 1-2
(raw token still accepted) can no longer be checked there; Max closed the
issue on step 3 and the two Rust tests that pin raw-token acceptance.
#346: checked in the Free edition given by the revoked key ("I already
have a key" closes the profile modal on the License card), so #370's PR
now uses Fixes and no longer waits on it.
Rotate out the 2026-09-21 decision to keep ten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 8c55e26 sync updated the header and decisions but left the Blockers
section at 2026-10-03. Bring it in line with Forgejo: onboarding phase 1
closed 7/7, #365 closed, #367/#370 ready in planned-2026-10-03-suites-licence,
maximus-api#62 and la-compagnie-maximus#150 closed, #317 absorbed by #358,
[Unreleased] carrying the onboarding entries.
Record that the #350 manual check is probably compromised on the Linux
machine: verifying #365 stored another key there, which deletes the
activation token, so the raw-ID token from v0.15.0 is likely gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Guide: drop the internal provenance (author, date, issue number) from the
BMO status line in the three copies (fr.json, en.json, guide-utilisateur.md).
The status now only says the procedure is reconstructed and unconfirmed.
- Dashboard: gate `showCompactChecklist` on `budgetReady` so a Base+ profile
with the five other items done no longer sees the compact list vanish (5/5
under the fail-closed entitlement) and reappear (5/6) once the licence
resolves.
- Test: pin TZ=America/Toronto around the `formatLocalDate` tests and assert
that `toISOString()` disagrees, so a UTC regression is caught on the UTC CI
runner too (measured: the mutation passed 7/7 under TZ=UTC before, fails now).
Follow-ups raised by the independent reviews of PRs #381, #378 and #382.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes phase 1 of the onboarding wave (#351-#356) on the documentation side.
- CHANGELOG.md / CHANGELOG.fr.md: Added (guided empty states, getting-started
checklist, sample folder, bank CSV export guide) and Changed (re-show button
in Settings > Users, Import "?" popover linking to the guide) under
[Unreleased] / [Non publié], same entries in both languages.
- docs/architecture.md: create_sample_import_folders (fs_commands 6 -> 7,
total 36 -> 37) with its contract, onboardingService, useOnboardingChecklist,
the onboarding_checklist_hidden preference, the samples/ corpus next to csv/,
and an "Onboarding" section (four empty states and their signal, source of
truth of the six checklist rows, sample-folder flow). Tree counts measured
for the directories the wave touched.
- docs/guide-utilisateur.md chapters 3 and 11 + docs.dashboard.* and
docs.settings.* keys (FR/EN). Hand-off from #355: docs.gettingStarted.steps
aligned with the six-step quick start of the markdown, docs.import.steps
mentions the sample folder, so the three guide copies agree.
- docs/qa-onboarding.html: interactive manual-test checklist, French only,
static, no network dependency, state in localStorage, plain-text report to
the clipboard with a visible fallback, reset button.
Resolves#357
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The import page without a source no longer shows a bare "no source found"
card. `ImportEmptyState` (built on the shared `EmptyState`) takes the place
of the empty `SourceList` in both situations: without an import folder it
offers "Choose a folder" as the single primary action; with a folder and no
bank sub-folder it offers "Create a sample folder" (primary, disabled while
in flight) and "Refresh" (secondary). Both variants, and the "?" help
popover through the new optional `linkTo` prop of `PageHelp`, link to the
bank CSV export guide at `/settings/users#import-export`. `SourceList`
stays an untouched pure list.
`useImportWizard` gains `createSampleFolders`: it invokes the Rust command
`create_sample_import_folders` with the folder and the webview's LOCAL date
(built from the local getters, not `toISOString()`), rescans the folder so
the two cards appear without another click, then raises a closable success
banner from the new `sampleCreated` state, which `reset` and a folder change
clear. A refusal from Rust (a sample folder already exists) lands in the
existing error banner. The log line carries the folder NAMES only, never
the absolute path, since the logs travel with a feedback report.
"Browse" on the folder card drops to the secondary style while no folder is
configured, so a single primary button is on screen. The reducer and its
initial state are exported as `wizardReducer` / `initialWizardState` and
unit-tested; `ImportEmptyState` is tested by direct call.
Resolves#356
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add an optional `subsections` array to a user-guide section, rendered by
the new pure `DocsSubsections` component between the "How to" block and
the tips. Each entry has an id, a title, an optional italic status line,
an ordered list of steps and an optional trailing note. The ids are
registered in `sectionRefs` of `DocsContent`, so the existing hash
navigation (`/settings/users#import-export`) reaches the sub-sections
too. Only the Import section declares the key; the eleven other
sections render exactly as before.
The Import section gets six blocks in French and English: a generic
"export a CSV from your bank" block, then Desjardins, Tangerine, RBC,
National Bank and BMO with their verification status. The National Bank
and BMO blocks, which ask the user to send a header line through the
feedback form, carry the warning never to paste an account number, a
balance or a real transaction, in the JSON files and in the markdown
guide alike.
`docs/guide-utilisateur.md` receives the same six blocks in chapter 4,
a mention of the "Create a sample folder" button in its "How to", and a
"Quick start" in chapter 1 rewritten as the six steps of the
getting-started checklist, same order and wording as
`onboarding.checklist.items.*`.
`DocsSubsections.test.tsx` calls the component directly (no jsdom):
null for a section without sub-sections, one block per entry with its
id and ref registration, status line and note rendered only when
present, malformed entries dropped.
Resolves#355
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add a Tauri command that writes two synthetic bank statements (Desjardins
chequing account, Tangerine credit card) into the import folder, one
sub-folder each, so a new user has sources to import on the first run.
The command takes the import folder and the webview's local date only
(chrono is compiled without `clock`), renders the `{{M1}}` / `{{M2}}`
tokens as the two complete months before that date, names each file
`releve-YYYY-MM.csv` after the most recent one, and refuses to run when
the folder is missing or when either sample folder already exists -
checked before anything is written. It returns the two folder names.
The templates live under `src/__fixtures__/samples/` so the Rust command
(`include_str!`) and vitest read the same bytes. The integration test
proves detection recognises each file as its bank with a signed single
amount and reads 40/40 rows, that the production `mapRow` parses every
row and sums to +740.00 / -368.46, and that the 180 seed keywords
categorise exactly 33 / 36 lines while leaving the hand-classified ones
untouched. The frozen detection corpus (`src/__fixtures__/csv`) is not
modified.
Five Rust tests cover the missing folder, the existing folder, the
success path, an unparseable date and the year boundary, each in its own
sandbox so they run in parallel.
CHANGELOG and architecture docs are centralised in #357.
Resolves#354
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A profile without any transaction now gets an "import first" card with a
button to the import page on both pages, in place of the filter bar, the
table and the pagination (Transactions) and of the filter panel and the
grid (Budget). Title, page help and template actions stay.
The signal is a profile-wide `totalInDb` count loaded once at mount via
`getTransactionCount()`, not `rows.length`: the default Transactions
filter is "since January 1st", so an empty page cannot tell an empty
profile from a year without transactions. `null` (not loaded yet) keeps
the regular layout, and so does a loading Transactions page.
`transactionsReducer` / `initialTransactionsState` and `budgetReducer` /
`initialBudgetState` are exported with a pure `showsEmptyState` selector
per hook, so the reducer tests pin that the count never touches filters,
sort or page and that 0 then N > 0 brings the table back. The existing
"no transactions found" / "no categories found" rows are unchanged.
Resolves#353
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A profile without any transaction now opens a dashboard that says what to
do: a welcome card with the expanded getting-started checklist and one
primary action towards the import page, instead of widgets showing 0 $.
With transactions, the compact checklist sits between the title and the
filter panel until it is complete or dismissed.
- `EmptyState` (pure, dashed card from the import source list; a `to`
action renders a react-router `<Link>`, an `onClick` action a button)
- `OnboardingChecklist` (pure, expanded and compact modes; undone rows
link to their route, the locked Budget row shows a lock, the "Base and
up" label and links to the license card; neutral budget row and hidden
counter until the entitlement is known)
- `DashboardEmptyState` composes both
- `DashboardPage` branches on the hook `status`: loading renders the title,
help and banner only; error falls back to the v0.15.0 page without the
list and logs a warning; ready with zero transactions renders the empty
state outside the `isLoading` veil
- `useOnboardingChecklist` exposes `counters` and `error` for the page
- Users settings: "Show the getting-started checklist" button in the Guide
section, visible only when the preference is set, with an inline
confirmation after the click
- FR + EN keys `dashboard.empty.*`, `onboarding.checklist.*`,
`settings.users.onboarding.*`
Tests: `EmptyState.test.tsx` and `OnboardingChecklist.test.tsx` by direct
call (FilterPanel pattern). No DB migration. CHANGELOG left to #357.
Resolves#352
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>