Commit graph

390 commits

Author SHA1 Message Date
le king fu
13dca87d04 test(categories): a failed re-read of the categories shows in the window (#393)
Review 25974, suggestion 1 (Q6): the JSDoc, architecture.md and the PR
body say a failure of readCategoriesAgain shows in the hierarchy window,
and no test held it. The read is rejected once at the opening (the
diagnostic does not run on the old categories) and once after a
committed reattachment: each time the window shows the message, the
page's banner behind it stays empty, and saving is over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:15:05 -04:00
le king fu
a17c1b7604 docs: placeListbox test header and CLAUDE.md module count (#429, #384)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m2s
PR Check — Rust / rust (pull_request) Successful in 3m52s
Review 25921, points 4 and 6: the header of placeListbox.test.ts still
listed the re-read after a render among what needs a DOM, though
CategoryCombobox.placement.test.ts pins it; only the paint and React's
skipped render remain. CLAUDE.md announced 10 command modules and listed
twelve: 12 modules, 10 of them with commands, 45 commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:43:18 -04:00
le king fu
b626bcbabe fix(i18n): say « become a parent » precisely, and what to do with budget amounts (#393)
Decision of Max of 2026-10-20 and review 25921, point 3. The #393
changelog entries, the guide tip and docs.categories.tips (FR and EN)
now say a category that still takes input cannot become a parent, and
that an existing parent, no longer inputable, accepts new subcategories
as before. The parent_has_budget message and the tip say amounts of any
year count, to be set to zero in the grid year by year, and that a
template cannot be edited, only deleted; the tip adds that zeroing a
past year also takes its planned amounts out of Budget vs Actual.

architecture.md: the rule and its reason, the hierarchy rows now
carrying is_inputable, readCategoriesAgain after a reattachment (a
form open on the orphan itself keeps its old parent: known limit), and
the hook harness users presented as a count with the command that lists
them (21 on 2026-10-20), not as an exhaustive list (25921, point 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:43:18 -04:00
le king fu
bc6610103b fix(categories): a reattachment keeps a form open behind the window (#393)
Review 25921, point 5 (pre-existing since #472): after a reattachment,
reattachOrphan reloaded through loadCategories, whose loading state
unmounted the detail panel, so a category form open behind the window
lost its unsaved changes. The silent re-read of checkHierarchy becomes
readCategoriesAgain (waits for a drag-and-drop's writes, a newer load
wins, no loading state), used by both; a failed re-read after a
reattachment now shows in the window, where the user is.

Also pins the guard « a newer load wins » (review 25921, R8): a load
started during the check's read lands, the check's older answer is
dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:41:17 -04:00
le king fu
1386abd91e fix(categories): check a parent's content only when it would become one (#393)
Decision of Max of 2026-10-20 (review 25921): transactions, non-zero
budget amounts and active keywords are checked only while the target
still takes input (is_inputable = 1, the condition of markAsParent),
that is when it really becomes a parent and stops taking input. An
existing parent, no longer inputable, accepts a new or reattached
subcategory whatever it holds, as in every released version: the
refusal protected nothing there. All three codes and all three entry
points follow the rule; the hierarchy rows now carry is_inputable,
and createCategory reads them through its transaction's handle.

Also pins a negative amount, in the grid and in a template, as a
refusal (review 25921, R1/R2): the grid stores what is typed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:40:04 -04:00
le king fu
273cf36eb7 fix(categories): the hierarchy check reads after the drag-and-drop's writes (#393)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m20s
PR Check — Rust / rust (pull_request) Successful in 3m52s
Self-review of the reload: updateCategorySortOrders writes one UPDATE
per category, each through the lock on its own, so a check opened right
after a drag-and-drop could read halfway and show half the move in the
tree. moveCategory keeps its write's promise, settled either way, and
checkHierarchy waits for it before reading. Pinned by a hook test with
the write held open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:25:49 -04:00
le king fu
a2c4fd4685 test(budget): a collapsed sub-group last in its group keeps its subtotal (#446)
Review 25487, point 2 (R13): [A, z, Vide] gives z, Vide, A; dropping
the subtotal of an empty block at the final flush turned no test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:10:03 -04:00
le king fu
0df60f6042 test: the transfer tooltip, the cancel button and the opened notes row (#437)
Review 25443, point 2: the transfer icon's tooltip goes through t with
its account and direction (X4), LicenseKeyForm's cancel button through
t (X11), and the notes textarea opens under the row whose button was
clicked, read in the Fragment keyed by that row's id (X8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:10:03 -04:00
le king fu
2389d51b48 test(settings): a late failure of the previous language keeps the history (#372)
Review 25424, point 1: the `if (!cancelled)` of ChangelogContent's
.catch had no test (V1). Modelled on the late-answer test: the French
file rejects after the English one was read, and the history still shows
the English notes, not the empty message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:10:03 -04:00
le king fu
4f060ba7d7 test(i18n): the key guard's canary sees two modules, and lists leaves only (#453)
Review 25409, points 1 and 2: the canary now fails if both imports name
one file (T4), and a fixture pins that a missing branch is reported by
its leaves, never by its container (T1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:10:03 -04:00
le king fu
ffe6288e4a test(import): the sample folder messages name the folder in both languages (#384)
Review 25592, point 3: the page's tests take the marked t and the key
guard compares no value, so a translation that lost {{name}} passed the
suite. The real locale files, through a real i18next with the app's
options, now render folderExists with the folder's name in French and in
English, and the two other codes without a leftover placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:09:01 -04:00
le king fu
48d1314d6d fix(import): clear the error banner when the folder is read again (#384)
Review 25592, point 2 (pre-existing): the banner of a refused sample
folder asks to refresh the list, yet survived a "Refresh" that went
through, and a folder change. refreshFolder, and browseFolder once a
folder is chosen, now clear the error before reading the folder, as
selectSource does; a failed rescan puts up its own error, and a
cancelled picker keeps the banner. The hook tests also pin browseFolder's
order (review 25487, point 1): the folder is set before its scan, so the
root files shown are the new folder's. Fixed entry in both changelogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:09:01 -04:00
le king fu
5f2c4dd5bd fix(combobox): follow the input when a render moves it (#429)
Review 25431: a window centered on screen grows when its content
arrives, and moves its fields with no scroll nor resize; the open list,
placed in a portal, stayed where it was. While open, the combobox now
reads its position again after every render (useLayoutEffect without
dependencies); `keepPlacement` hands back the same state object for an
unchanged position, so React renders nothing more. Pinned by
CategoryCombobox.placement.test.ts (harness, useLayoutEffect mapped on
its useEffect, fake input) and keepPlacement's own tests. The boundary
test of placeListbox now sits at exactly 240 px below with room above
(point 1), and AddKeywordDialog's comment no longer claims the list is
cut (point 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:06:22 -04:00
le king fu
813ee07559 fix(i18n): name the eight getting-started steps in the dashboard help (#446)
Decision of Max of 2026-10-19 (review 25487, point 4):
docs.dashboard.steps[0] (FR and EN) and step 1 of the guide's Dashboard
section listed six of the eight steps. They now name the eight, by the
labels of onboarding.checklist.items, in the list's order, and say the
two visit steps are checked with the card's "Got it" or, for the
reports, a click on the outlined Trends card. A test holds the step to
the checklist's labels and order; the #446 changelog entries follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:03:59 -04:00
le king fu
7f25293669 test(categories): pin the rules the review of #393 left green (#393)
Review 25437, point 2: the loop named by its own smallest member, not by
a smaller id under it (RM1); the page hands the window every category,
not the tree (RM11); a category that is its own parent stays out of the
budget, and displaysAsRoot says so (RM15). RM7 (depth counted on active
categories at the service) is pinned in categoryService.test.ts by
1a36d4e, RM2 by be4b923.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:59:07 -04:00
le king fu
be4b923451 fix(categories): reload the categories before the hierarchy check (#393)
After a drag-and-drop, SET_TREE leaves state.categories with what the
tree shows, without the members of a loop: the window then showed "—"
for the orphan and offered a parent the service refused (review 25437,
point 1). checkHierarchy now reads the categories again before the
diagnostic, without the page's loading state, so a form open behind
the window stays as it is; a load started meanwhile wins. Pinned by a
drag-and-drop-then-check hook test, plus the refusal-then-success case
of the window's message (RM2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:59:02 -04:00
le king fu
1a36d4e618 feat(categories): refuse a parent that holds budget amounts or keywords, on create too (#393)
Decision of Max of 2026-10-19 (review 25437, point 3): a category that
holds direct transactions, a non-zero budget amount (budget_entries or
budget_template_entries) or active keywords cannot become a parent. The
check (`checkParentContent`) runs through the transaction's handle,
before any write, in `updateCategory` (changed parent), `reattachCategory`
and now `createCategory`, which writes its row and the parent's flag in
one transaction. Order: transactions, budget, keywords; the first found
is the refusal (`parent_has_budget`, `parent_has_keywords` added to
`CategoryHierarchyError`, translated in FR and EN).

The service tests run the content queries as written on node:sqlite,
tables created from consolidated_schema.sql.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:51:24 -04:00
le king fu
08e6f045ae test: read the banner's dots once in the presence test (#446)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m2s
Self-review tidy-up: the presence test looked the dots up twice; it now
keeps the one list it asserts on. No assertion changed.

Resolves #446

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:07:44 -04:00
le king fu
d068c7ecbb fix(i18n): name the reports by their cards, and how the visit steps get checked (#446)
reports.help.tips[2] and [3] named "Monthly Trends" and "Category Over
Time" ("Tendances mensuelles", "Catégories dans le temps"), tabs that
no longer exist: they now name the Trends and Category Analysis cards
of the hub. docs.dashboard.features[6] said the eight steps tick
themselves from the data; the two visit steps are checked with "Got it".
French texts as validated by Max on 2026-10-16, English aligned; the
guide's line of the dashboard features follows docs.dashboard.features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:59:22 -04:00
le king fu
d551c4b0d4 test: pin the collapsed sub-group order and the banner's dot (#446)
reorderRows: a collapsed sub-group (a subtotal with no child row)
followed by a sibling leaf gives Vide, z, A, the order read by running
the case first; then the same block followed by a sibling root, a group
(Vide, z, A, b, B) or a leaf (Vide, z, A, R).

OnboardingBanner: the decorative h-1 w-1 dot is absent while the visit
steps are not initialized and while the Budget entitlement is unknown,
and present, between the counter and the next step, once the total is
known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:59:17 -04:00
le king fu
a62150d926 fix(import): empty the root files when another import folder is chosen (#446)
SET_IMPORT_FOLDER now empties rootFiles. Until the new folder's scan
landed, or for good when that scan failed, the warning about files
placed directly in the import folder named the previous folder's files
under the new folder's name. scannedSources is left as it was: the
source list of the last scan stays until the new one, as before.

Reducer tests next to "a scan error leaves the root files of the last
scan in place": a folder change empties the root files and keeps the
sources until the next scan, and a scan error after a folder change
names no root file of the previous folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:59:13 -04:00
le king fu
1ed6a67af0 fix(i18n): the "already exists" message also holds for a file (#384)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m6s
PR Check — Rust / rust (pull_request) Successful in 3m46s
Self-review finding: create_sample_import_folders refuses on any existing
entry named like a sample folder, a file included, which "Refresh the list"
never shows. The message now says to refresh the list and, if the entry
does not show up, to rename it before trying again (FR and EN), and the
changelog entries follow.

Resolves #384

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:47:05 -04:00
le king fu
a64ac3b8a8 docs: commands and hooks recounted, sample folder codes, guide wording (#384)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m59s
PR Check — Rust / rust (pull_request) Successful in 9m48s
architecture.md: the Tauri commands recounted from generate_handler! (45,
was 39), with the five that were never named documented in new sections
of their modules (feedback_commands.rs: send_feedback,
get_feedback_user_agent; backup_commands.rs: ensure_backup_dir,
get_file_size, file_exists) and fetch_price described as it is, not as
"to come in Phase 5" (also in the balance service and the tree's command
modules, now 10). Hooks: 29 files, the three undocumented ones added to the
table. The Transactions empty state reads the count alone (D1), the mount
reads stand alone, and the sample folder flow describes its codes, its
errorValues and its Journal line.

CLAUDE.md: 29 hooks in the tree. Guide and docs.gettingStarted.steps:
"the keywords that come with the app" instead of the seed jargon.
CHANGELOG (EN, FR): Changed for the Transactions card, Fixed for the sample
folder messages and banners.

Resolves #384

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:42:23 -04:00
le king fu
038bc41cb3 test(import): pin the empty state's variant for a folder stored as "" (#384)
With folderName as its only prop, ImportEmptyState tells "no folder" from
"a folder" by null alone. The page hands "" for an import folder stored as
"", as hasFolder did, so the empty state keeps the "no source" variant the
step card describes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:31:50 -04:00
le king fu
66bdb19f44 test: correct the seed priority comment of the onboarding samples test (#384)
The seed is not all priority 0: COSTCO and WALMART are at 50, COUCHE-TARD
ESSENCE at 10. The comment now says why the mock's sort stays right: it
sorts by descending priority like the SQL, and no line of either sample is
matched by more than one seed keyword, so the order cannot move the counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:30:42 -04:00
le king fu
3d917ed803 fix(transactions): show the import-first card on the count alone (#384)
D1 of Max: showsEmptyState of Transactions reads totalInDb === 0 only, like
Budget. On an empty profile the card comes up as soon as the count is
loaded, instead of the filter bar, the totals and the loading line until
the first page is back. A null count (not loaded, or failed) keeps the
regular layout.

The mount reads of useTransactions and useBudget run under
Promise.allSettled: each read that succeeds is dispatched, the first
failure becomes the page's error, and a failed count leaves totalInDb null
instead of emptying the category, source and account filters.

Tests: the reducer test of the loading page rewritten for D1, the page's
empty-profile case rendered (card in place of the filter bar, table and
pagination), and both hooks' mount reads run through the shared harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:30:42 -04:00
le king fu
11eb911657 fix(import): translate the sample folder refusals, banner after a good rescan (#384)
create_sample_import_folders refuses with stable codes instead of English
sentences: sample_folder_exists:<name>, import_folder_missing (no longer
carrying the folder's path) and invalid_date. The wizard maps them to
import.empty.sampleErrors.* through the pure sampleFolderError, the folder
name interpolated from the new errorValues of the state; any other message
stays raw. The raw refusal also goes to the Journal as a warning: a code, or
a write error naming a sample folder and the OS error, never a path.

scanFolderInternal now tells whether the scan went through, and the success
banner only follows a rescan that did: a failed rescan leaves its error
alone on screen.

ImportEmptyState keeps folderName alone (null without a folder); the page
passes "" for a folder stored as "", as hasFolder did.

Rust tests: the three codes asserted exactly, the case of the second sample
folder already there (nothing written), and {{M1}} pinned as the most
recent month line by line against the templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:30:35 -04:00
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