Commit graph

363 commits

Author SHA1 Message Date
le king fu
dad81fa53f docs: fix the stale header of PriceFetchControl.test.tsx (#437)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m2s
Self-review of PR #473: the header still said every React rendering of the
file was bypassed, while its "displayed texts" tests render the control with
its hooks from the shared harness. architecture.md: the slot presets are
commented in each test file (the two direct writes of the onboarding wiring
test are covered by the comment of mountWith).

Resolves #437

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:04:15 -04:00
le king fu
ae54406354 docs: name the slot-order dependency and the known test limits (#437)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m7s
- Where a test presets a harness slot by position (PriceFetchConsentToggle,
  useOnboardingChecklist's mountWith, SystemsSettingsPage), a comment names
  the dependency on the component's hook order: any hook added before shifts
  the slots without a word (the `slot` helper of fakeReact).
- docs/architecture.md, Tests: "Limites connues" lists the slot presets, and
  links to ADR 0021 for withoutFolders and for the catch-up measurement
  instead of repeating them.
- ADR 0021 section 7: the relative-path limit is stated precisely (a run with
  two separators or more is still cut to its last segment; only the words of
  a folder holding a blank stay), and a second limit is written: a folder
  name holding one of the stops (a backtick, `;`, `|`, a closing parenthesis
  not followed by a separator...) ends the path there, and that name, or its
  start, stays in the line.
- db.ts: the comment of withoutFolders says "a closing parenthesis not
  followed by a separator", as the code does, and points to ADR 0021.

No behaviour change.

Resolves #437

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:59:54 -04:00
le king fu
3e8c79f057 test: pin the \r path stop and the surviving marked-t keys (#437)
- db.test.ts: `\r` joins the path stops of withoutFolders (removing it from
  PATH_STOP now fails).
- The six keys whose "t removed" mutation survived the review of PR #435
  (comment 23499) are asserted in their marked form:
  transactions.transferIcon.ariaLabel (a row linked to a transfer) and
  transactions.notes.placeholder (title of every row's notes button) in
  TransactionTable.render.test.tsx; license.keyPlaceholder and
  license.activate in LicenseKeyForm.render.test.tsx;
  dashboard.kpiSectionTitle in DashboardPage.render.test.tsx;
  balance.priceFetching.consent.accept in PriceFetchControl.test.tsx, the
  consent dialog opened by presetting the control's first harness slot
  (the dialog's title, body and decline texts are pinned with it).
- TransactionTable.wiring.test.ts opens a row's notes through its button
  and pins the textarea placeholder, the second call of the notes key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:46:47 -04:00
le king fu
8a9d9909f9 test: pin the reattachment count to active descendants (#393)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m9s
A deactivated child of the reattached category is not counted in the
number of categories put back, which the Journal line reports.

Resolves #393

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:28:45 -04:00
le king fu
cb0e2d2536 fix: reload the category tree even when the check after a reattachment fails (#393)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m13s
The reattachment is committed before the orphan list is checked again: if
that check failed, the tree kept showing the categories as they were. The
tree now reloads in a finally block, and the failure shows in the window.

Resolves #393

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:24:31 -04:00
le king fu
03e32afc02 docs: describe the hierarchy check and the parent rules (#393)
CHANGELOG (Added: the "Check hierarchy" window; Fixed: the parent checks
at save time and the budget rows of a category whose parent is deleted or
gone), the Categories section of the user guide and its docs.* keys in
both languages, and docs/architecture.md: the categoryHierarchy module, the
diagnostic, the validation and its typed error, the window, the budget
root rule, and what stays out of scope (drag-and-drop, archive restore).

Resolves #393

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:23:14 -04:00
le king fu
21da7a3827 fix: show a category whose parent is deleted or gone in the budget (#393)
The budget grid and the Budget vs Actual report read active categories
only and started from the categories without a parent, so a category whose
parent was deactivated or no longer exists vanished from both, budget
amounts included. It now stands at the top level, as in the tree and the
category pickers (displaysAsRoot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:16:25 -04:00
le king fu
db5ee65720 fix: refuse a looping or too deep parent, and reattach orphan categories (#393)
updateCategory checks a new parent before writing: the category itself,
one of its descendants or a category caught in a cycle (cycle), a fourth
level counting its sub-tree (too_deep), a parent holding transactions of
its own (parent_has_transactions). The refusal is a CategoryHierarchyError
that the Categories page shows by the translation of its code. The reads
and the writes, the new parent's is_inputable included, run in one
transaction through its handle; an unchanged parent is not checked, so a
category stored before these rules can still be renamed.

getCategoryDepth keeps the ids it has seen and ends on a cycle.

A "Check hierarchy" button, between "All keywords" and "Re-initialize",
opens a window that lists the orphan categories (caught in a parent loop,
parent deleted, parent gone), one per group, with a new parent for each:
the root by default, or a parent of the category form's list.
reattachCategory applies the same rules; after it, the list and the tree
reload and the Journal gets one line with the reason and the number of
categories put back, no name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:16:21 -04:00
le king fu
df3fefe934 feat: gather the category hierarchy rules in one module (#393)
src/utils/categoryHierarchy.ts holds the depth, height and descent walks
that buildParentOptions had inline, each with a set of the categories it
has seen, so a parent_id cycle ends the walk. A category caught in a cycle,
or under one, now has no depth instead of a made-up one, and the parent
list of the category form never offers it: a child placed there would be
hidden with it. The module also carries the orphan diagnostic, the parent
change refusal and the root rule that the service, the Categories page and
the budget use next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:16:14 -04:00
le king fu
7053ae5fc6 docs: describe the category combobox in a portal and its new places (#429)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m55s
CHANGELOG (EN and FR, Changed): typing in the category of an adjustment
line and in the category filter of the "Link transfers" window, the
adjustment lines limited to the categories open to input, and the open
list no longer cut by the edge of a window or of the Transactions table.

Guide, section 7, and its in-app twin (docs.adjustments.steps, FR and
EN): type a few letters to filter the category list of a line.

architecture.md, Tests section: how a test renders a window drawn by
createPortal (identity mock, document stub), what of the combobox's
portal is pinned without a DOM (placeListbox.ts) and what is checked in
the app, and that the list no longer sits in its container's DOM.

Resolves #429

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:41:37 -04:00
le king fu
aa229484f6 feat: filter the transfers to link by category with a combobox (#429)
The category filter of the "Link transfers" window was a native select.
It now takes the compact CategoryCombobox, modelled on the Transactions
filter bar: "Any category" first as an extra option, active while the
filter is empty, and picking it sets the filter back to null; the list
of transactions reloads as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:34:52 -04:00
le king fu
619637e91b feat: pick an adjustment line's category in a combobox (#429)
Each line of the adjustment form chose its category in a native select
showing raw names. It now takes the compact CategoryCombobox of the
split window: typing filters, hierarchical order, seed names
translated. A new line still takes the first category of the list.

D2: the hook reads the Transactions list, getAllCategories (active and
open to input), instead of its own query on every active category. The
list feeds two pickers only, the form and the split window of the
Adjustments page; the detail view names a line's category from the
entries' join, so a line saved on a category hidden from input keeps
its name there and its value, its field empty until changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:34:49 -04:00
le king fu
3631ef0b64 feat: render the category combobox's open list in a portal (#429)
The open list of CategoryCombobox was positioned `absolute` inside its
input's container, so a scrolling ancestor clipped it: the body of the
split window, the `overflow-x-auto` wrapper of the Transactions table.
It is now rendered in a portal on document.body, `position: fixed`, at
z-[300] (above every window, the highest at z-[210]).

The pure rule lives in placeListbox.ts: under the input, as wide as it,
4 px below; above it when the room below is short of the list's 240 px
and the room above has them; below when neither side has them. The
list is placed in a layout effect before the first paint, then follows
the input on every scroll (capture phase, any scrolling container) and
resize while it is open, skipping scrolls of the list itself; the
listeners are removed with the list (followAnchor returns the cleanup).
The outside press counts the list as inside, since it no longer sits in
the container's DOM. The closed markup is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:34:44 -04:00
le king fu
5decb985f1 fix: list the System sections on the Settings home in the page's order (#372)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
Self-review of PR #470: the System card of the Settings home lists the
page's sections as chips, still with the version history before the logs.
The chips now follow the page (version, update, logs, history), and a test
renders the page and compares its section headings with the card, so the
two cannot drift apart again. SECTIONS is exported for that test, as
DocsContent exports its own. The /settings/systems row of
docs/architecture.md names both unreleased headings and the card.

Resolves #372

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:12:35 -04:00
le king fu
0513d717c7 docs: describe the Systems page order and the folded version history (#372)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m2s
User guide (section 11) and the in-app docs.settings keys, FR and EN: the
order of Settings → System and how the version history folds. One Changed
entry in both changelogs. docs/architecture.md: the /settings/systems row,
why ChangelogContent keeps a local Set rather than useCollapsibleGroups,
and the utils count (19: startupConnection and rootFiles were not counted).
CLAUDE.md no longer describes /changelog as a page with the full history:
it redirects to Settings → System since #190.

Resolves #372

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:07:41 -04:00
le king fu
92b3e54d54 feat: show the Journal before the version history, folded by version (#372)
Settings → System now orders its sections Version, Update, Logs, version
history, data notice: the Journal is reachable without scrolling past the
whole changelog, which grows at each release.

The version history opens on the installed version's notes, every other
version folded behind its title (a button with aria-expanded, a chevron and
aria-controls), with an Expand all / Collapse all button. An empty
Unreleased heading is hidden; one with content (development build) stays.
State is a local Set seeded by a pure rule, in memory only.

parseChangelog moves unchanged to src/utils/changelog.ts with the rule:
installed version unfolded, or the first published version when the
installed one is unknown or absent; a published version is told from the
Unreleased / Non publié heading by its shape, so both languages read alike.
SystemsSettingsPage mounts ChangelogContent once getVersion() settles; a
failure is written to the Journal and leaves the version unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:02:48 -04:00
le king fu
8a250b98b5 test: guard the key parity of fr.json and en.json (#453)
No test compared the key trees of the two locale files: a key added to one
language only passed the build and the suite, then showed raw to an English
user (missing from en.json) or in English to a French user (missing from
fr.json, English being the fallback).

src/i18n/locales.keys.test.ts imports both files statically, flattens every
node to a dotted path, array indices included (docs.dashboard.features.6),
and fails with the keys missing from each language in a test of its own,
plus a third test for a path held with another kind in each file (an array
against an object, a leaf against an object or an array). Values are never
compared. A canary checks both files were read whole, array items included,
and four tests on small fixtures pin the rules of the comparison.

Run on the base (6e8e157): 1712 leaves on each side, no gap, so no
translation to add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:12:32 -04:00
le king fu
ff9656d015 test(dialogs): guard against aliases of confirmAction and other native forms
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m0s
Review of PR #465 (comment 24953), suggestion 1: an alias
(`const c = confirmAction; if (!c(...))`) called confirmAction out of reach
of the "awaited" rule, and window["confirm"](...), confirm?.(...) and
`const { confirm } = window` escaped the native-call rule. Any mention of
confirmAction other than the awaited call, its declaration, a plain named
import or `confirm: confirmAction` is now refused; the three native forms
are flagged. Samples added for each.

Refs #421

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:29:17 -04:00
le king fu
57ac056bf2 fix(dialogs): wait for the confirmation before deleting or removing the key
On desktop, tauri-plugin-dialog 2.6.0 replaces window.confirm with an async
function: confirm(...) returned a Promise, always truthy, so every guard
`if (!confirm(...)) return;` let the action run before the user answered.
Deleting a secondary profile removed its database even on "Cancel".

- confirmAction() (src/services/confirmService.ts) calls the plugin's ask()
  with a warning icon and buttons labelled in the app language
  (common.confirm / common.cancel); a failed call resolves false and is
  logged (fail closed). @tauri-apps/plugin-dialog 2.6.0 added, the crate's
  version.
- The six sites await it: profile, category, adjustment, budget template and
  Balance category deletions, and both confirmations of the license key
  removal (performRemoveLicense takes an async confirm).
- native-dialog-guard.test.ts fails on any native confirm/alert in src/ and
  on any confirmation not awaited (TypeScript does not flag `!promise`).
- Tests: confirmService, the deletion of a profile in ProfileFormModal, and
  the license removal tests with an async confirm (test: reproduces #421:
  six of them were red on the old code).

Refs #421

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:29:17 -04:00
le king fu
a8c3fd6997 refactor(gating): one constant for the split's feature key
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
Review of PR #452 (comment 24129): "adjustments" was written three
times for the split (the page's gate, the button's edition, the
dialog's locked screen). SPLIT_FEATURE in splitGate.ts is now the one
source, so moving the split to its own key or to Premium is a one-line
change. No behaviour change; setting it to "budget" turns 2 tests red.

Refs #369

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:36:33 -04:00
le king fu
d5d0e89d2c test(gating): pin that one click never arms the other window
Review of PR #452 (comment 24129): the upgrade to Base checked that the
upsell goes away, not that no split window appears in its place, so a
locked click that also armed the split window (R14) left the suite
green. Both edition changes now assert the other window stays closed,
during the license refresh and after it.

R14 now turns 1 test red, and its mirror (a split click that also arms
the upsell) 1 test red.

Refs #369

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:36:33 -04:00
le king fu
cb7f8479e4 feat(gating): reserve transaction splits for the Base edition
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m16s
The Répartition (split) button of the Transactions page now belongs to
the `adjustments` feature, as the unlock screen already promised
(upsell.features.adjustments). On the Free edition the button keeps its
place and its icon, carries a lock and opens the locked screen in a
dialog instead of the split window, for every row, split or not. While
the license is not ready it shows no lock and does nothing. Existing
splits stay in the data, counted by the reports and flagged by the
orange icon.

- splitGate.ts: pure splitAccessFor(gate) -> open | locked | pending,
  stricter than profileGate: `allowed` does not short-circuit `ready`.
- SplitButton: stateless button that decides what a click opens.
- UpsellDialog: UpsellGate in a named modal dialog, closed by its X and
  by "I already have a key".
- TransactionTable: required splitAccess prop. The split window is not
  rendered once the edition resolves to Free, and stays mounted through
  a license refresh; the upsell is not rendered once it resolves to Base.
- TransactionsPage reads useEntitlement("adjustments").
- ADR 0017 amendment and matrix, user guide, in-app docs (FR, EN),
  architecture, entitlements header comment, both changelogs.

Tests: the predicate, the button's markup and click, the dialog, the
table's wiring through the shared hook harness (#409), the page under
the three gate states, and the new key in both locales.

Fixes #369

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:02:47 -04:00
le king fu
934bcbf9aa docs(onboarding): cards of the Categories and Reports pages
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m57s
Guide and in-app docs name the five card pages, "Got it" on the two
visit cards and the Trends card that also checks the reports step;
architecture lists the new models and targets; the QA list gets the
steps 5 and 8 of the guided walk, the Free padlock variant and the
Trends click.

Resolves #432

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:48:47 -04:00
le king fu
00be2877f3 feat(onboarding): step cards of the Categories and Reports pages
The two visit steps get their card: /categories explains keywords and
points at "All keywords" (never "Reset"), /reports explains the hub and
points at the Trends card, with a padlock variant in the Free edition.
"Got it" acknowledges the step; opening the highlighted Trends card does
too (D17), since following the ring leaves the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:45:05 -04:00
le king fu
a064043c20 docs(onboarding): eight steps, visit steps and their one-time initialization
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m58s
- Guide and docs.gettingStarted / docs.dashboard keys (FR, EN): eight
  steps, the two visit steps in the quick-start list, "Got it", 3/8 and
  7/7, a finished path finds them done.
- ADR 0022: acknowledgeStep and initializeVisitSteps do not notify (§2),
  ACKNOWLEDGE among the local actions (§4), the one-time initialization
  admitted like rule D9 (§5).
- architecture.md: eight rows, the amended "never by a click" invariant,
  the two new preferences, the provider's new action and initializer.
- QA list: step numbers moved to eight, five cases for the visit steps.
- CHANGELOG: Added entry in both files.

Resolves #425

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:37:30 -04:00
le king fu
eff77e47cc test(onboarding): pin the order of CHECKLIST_ITEM_KEYS to the rows
A mutation that moved "categories" after "manualCategory" in the
constant survived: the rows come from computeChecklist, the constant
only orders the skipped and acknowledged steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:33:37 -04:00
le king fu
64a07f359f feat(onboarding): visit steps, eight rows, "Got it" and one-time initialization
Two visit steps join the getting-started path: "Browse categories and
keywords" (5th) and "Browse the reports" (8th). Nothing in the database
counts them: they are done by "Got it" on their card, stored as
onboarding_acknowledged_<key> in user_preferences (no migration).

- computeChecklist: eight rows; the visit rows are locked until the
  profile's visit steps are initialized, so a finished path does not
  flash a "Next step" before they are marked done.
- initializeVisitSteps: once per profile, after the first successful
  load with the Budget entitlement known; acknowledges both steps when
  every other step is done or skipped, then sets the flag. No
  transaction (D16): idempotent writes, the flag last.
- Reducer: ACKNOWLEDGE wins over an older snapshot like SKIP but keeps
  the card latch, so the card says "Done." and offers the next step.
- StepCard: "Got it" before "Skip this step" when onAcknowledge is
  given; OnboardingStepCard passes it for a visit step.
- Banner counter, card rank and end of path wait for
  budgetReady && visitStepsInitialized; the list shows the visit rows
  "pending" until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:32:20 -04:00
le king fu
fade01d955 docs(help): fix stale help and guide texts of Budget, Reports and Transactions
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m58s
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>
2026-10-13 21:18:03 -04:00
le king fu
63d8ba7589 feat(onboarding): Transactions card suggests Add as keyword, Budget card names leaves and the Annual column
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>
2026-10-13 21:12:25 -04:00
le king fu
00b4319c44 test(import): pin the read-only source name of the configuration panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:06:07 -04:00
le king fu
ed3a4bb9a0 fix(import): name a source after its subfolder, read-only
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>
2026-10-13 21:05:46 -04:00
le king fu
7d3a616312 feat(import): warn about the CSV files placed directly in the import folder
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>
2026-10-13 20:59:20 -04:00
le king fu
af884ca13b feat(import): show the expected folder structure, one subfolder per account, signs at the preview
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m57s
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>
2026-10-13 20:53:05 -04:00
le king fu
1c427a1b63 fix(budget): emit the grid rows depth-first and close subtotal blocks
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>
2026-10-13 20:42:27 -04:00
le king fu
c17bf5dcb4 fix(balance): translate the best-effort badge of the price fetch button
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m59s
PR Check — Rust / rust (pull_request) Successful in 3m58s
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>
2026-10-13 20:01:11 -04:00
le king fu
86eff9b7a3 fix(profiles): follow-ups of the review of PR #436
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m8s
PR Check — Rust / rust (pull_request) Successful in 3m54s
- 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>
2026-10-13 19:57:29 -04:00
le king fu
ac3a981f64 test(i18n): follow-ups of the review of PR #435
- 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>
2026-10-13 19:53:18 -04:00
le king fu
a1c31a1da7 test(harness): follow-ups of the review of PR #434
- 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>
2026-10-13 19:51:20 -04:00
le king fu
74fae140b1 fix(db): follow-ups of the review of PR #433
- 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>
2026-10-13 19:50:12 -04:00
le king fu
419b20e769 feat(profiles): name the copy of a stopped catch-up and keep one copy per profile
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m1s
PR Check — Rust / rust (pull_request) Successful in 3m56s
- 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>
2026-10-12 20:18:18 -04:00
le king fu
3882df772b fix(startup): clear the startup error when the profile opens after the delay
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>
2026-10-12 20:18:18 -04:00
le king fu
2bd347b4a0 test(i18n): guard the test sources against a local fake t
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m5s
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>
2026-10-12 19:58:30 -04:00
le king fu
063f6c3d7d test(i18n): replace the local marked fake t copies with i18nMock
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>
2026-10-12 19:56:51 -04:00
le king fu
f5e5897df1 test(i18n): move the raw fake t files to the marked i18nMock
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>
2026-10-12 19:56:27 -04:00
le king fu
69ef92e65f test(i18n): add the shared marked fake t and use it in useLicenseMachines
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>
2026-10-12 19:49:44 -04:00
le king fu
1ba3b8bae9 test: move the React stubs of six test files to the shared hook harness
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>
2026-10-12 19:44:43 -04:00
le king fu
86fd496e77 test: add a shared React hook harness with its own proofs
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>
2026-10-12 19:44:36 -04:00
le king fu
8efe99aa20 test(db): release gated transactions in finally so a red assertion holds no lock
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:28 -04:00
le king fu
08946e6d08 fix(db): keep the lock-free path for the transaction handle only
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>
2026-10-12 19:28:36 -04:00
le king fu
8898213e02 refactor(categories): follow-ups of the review of PR #428
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m55s
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>
2026-10-12 18:44:54 -04:00