Commit graph

720 commits

Author SHA1 Message Date
le king fu
24da72b7f4 docs(review): apply the review of #460, version the calibration script
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m55s
REVIEW.md, from the suggestions of the first review of #460:
- stacked-PR pitfall: fs_commands.rs compiles two CSV files of
  src/__fixtures__/samples/ through include_str!, outside the paths of
  check-rust;
- high level: .claude/settings.json and .mcp.json, a pull_request_target
  trigger, an install step that changes the toolchain or the PATH (the
  check-workflow list is now given as examples of the principle), a lock
  origin whose host or scheme changes even with a new version, and a diff
  that removes or bypasses a consent (price consent lives on the front);
- db.ts and dataExportService.ts stay normal (Max's decision of
  2026-10-18), and the data zone now names the lock, the profile switch
  and the restore;
- release zone: cargo-xwin and the Rust toolchain, installed unpinned at
  each tag, also run with the signing key;
- windows-sys guard: EXPECTED joins CRATES and PIN;
- identifier: changing it orphans the databases and the app_data_dir
  files (license key, activation token, token fallback); the keychain
  service is a constant and does not follow.

scripts/review-levels.py is the calibration script of the PR body, now in
the repo: pages of 50 files read until an empty page (the instance caps
pages at 50), the new rules above, comment lines ignored by the workflow
checks, and --check, which compares its patterns with the Haut table
without network access. Fresh run: 3/28/8 on the 39 reference PRs and
6/33/12 on the 51, unchanged.

Refs #456

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:55:37 -04:00
le king fu
fa1abb115c docs(review): add REVIEW.md with calibrated risk levels and review rules per zone
REVIEW.md sets the review level of a PR from the files its diff touches
(high, normal or low; the highest wins, never the author), lists what
matters in each zone with its source (a CLAUDE.md section or an ADR,
completed by the repo file where the fact lives), the verification
commands of the CI, and the review pitfalls of this repo. The pitfalls
taken from the global pr-review skill are corrected for this repo:
stacked PRs do get CI here since #232.

Calibration on the merged PRs #344 to #452: the 39 reference PRs of the
pilot document come out 3 low / 28 normal / 8 high (20.5 %, target
20 %), the whole range of 51 PRs 6 / 33 / 12 (23.5 %), against 3 / 19 /
17 with the trial patterns. A lockfile-only version bump of a package
already present stays normal, per Max's decision of 2026-10-17.
CLAUDE.md points to the file.

Fixes #456

Note: plan check bypassed after 3 passes, last findings applied (Max's
decision of 2026-10-18).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:21:05 -04:00
le king fu
589aee0b38 state: sync after #369
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:46:13 -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
065f580c9f state: correct the product position line (v0.17.1, Unreleased not empty)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:28:16 -04:00
le king fu
d8037be11f state: sync after #447
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:21:57 -04:00
le king fu
1763a9fdd3 fix(deps): update rustls to 0.23.46
All checks were successful
PR Check — Rust / rust (pull_request) Successful in 9m33s
Clears RUSTSEC-2026-0346, RUSTSEC-2026-0347, RUSTSEC-2026-0348 and
RUSTSEC-2026-0349, published on 2026-10-14 against rustls 0.23.45. The
daily audit.yml has been red since run 553 (2026-10-15 06:00 UTC) with
the lockfile untouched since v0.17.1: the advisories alone turned it red.

rustls is reached only through tauri-plugin-updater (reqwest 0.13) on
both shipped targets, and none of the four applies to that use: no
custom ClientSessionStore (0346), ECH never enabled (0347), no `brotli`
feature (0348), and the ring provider offers no post-quantum group to
downgrade (0349). Rule 1 of .cargo/audit.toml still asks for a fix
rather than a suppression: the crate is in the graph and a fix exists.

`cargo update -p rustls --precise 0.23.46` changes two lines of
Cargo.lock (version and checksum). No companion bump: 0.23.46 declares
the same dependencies and MSRV. No windows-sys edge moves. The checksum
matches the crates.io index and the downloaded .crate.

The changelog entry scopes "no behaviour change" to the app's own code,
as 9abf474 did for the previous rustls bump: besides the four fixes,
0.23.46 carries about ten other TLS-layer corrections.

Verified locally, cargo-audit 0.22.2 (the CI binary) run from the repo
root: exit 1 with the four advisories before the bump, exit 0 after,
666 crates and the same 12 allowed warnings on both runs. cargo check,
both check-rust guards (2 and 9 checks, exit 0) and cargo test (267
passed, 0 failed) are green.

Refs #447

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:00:04 -04:00
le king fu
8620049585 state: sync after planned-2026-10-12-retours-onboarding-v0171 (#424, #422, #427, #426, #430, #431, #425, #432)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:39:18 -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
6b913709af docs(onboarding): changelog and QA cases of the Transactions and Budget cards
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m58s
The two older QA cases that quoted the previous card sentences now quote
the new ones.

Resolves #430

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:14:23 -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
e177a7561f docs(import): changelog, guide, architecture and QA cases of the read-only source name
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
Resolves #426

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:08:07 -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
2de4d9d255 docs(import): changelog, guide, architecture and QA cases of the root files warning
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
PR Check — Rust / rust (pull_request) Successful in 3m41s
Resolves #427

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:02:22 -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
22dcad7a29 docs(changelog): budget rows under their parent, subtotals on their own rows
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m59s
Resolves #424

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:43:37 -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
bf02b267b1 state: sync after planned-2026-10-10-fiabilite-tests (#408, #409, #389, #395)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:30:22 -04:00
le king fu
977ef3a1d3 docs: ADR 0024, MCP server (proposed)
Written before the code, from spec-decisions-mcp-server.md and the
reviewed spec-plan-mcp-server.md (local, git-ignored). Status Proposed;
to pass Accepted when the spec-mcp-server milestone merges. Indexed in
docs/architecture.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 20:29:06 -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
fbecfffa6e style(profiles): rewrap the sync comment of backup_to
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
PR Check — Rust / rust (pull_request) Successful in 3m46s
Resolves #395

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:27:33 -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
79ba1ed2f3 docs(architecture): add the tests section and the hook harness
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m57s
Resolves #409

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:44:43 -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
b161d54e3a docs(architecture): add the DB access and transactions section
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m55s
Strict lock, transaction handle, single section for a profile switch,
connection generation and blocking alert.

Resolves #408

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:32:05 -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
8ceb3de921 state: sync after #423
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12 18:49:04 -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
le king fu
04ac2e933e fix(categories): shrink the allowed parent depth by the height of the edited sub-tree
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
Refs #423

The review of PR #428 showed that the rule only looked at direct children:
editing a root that carries grandchildren still offered the other roots as
a parent, and saving created a fourth level, which nothing guards against
in updateCategory or moveCategory. The allowed parent depth is now
MAX_PARENT_DEPTH minus the height of the edited sub-tree (0, 1 or 2), so a
category with grandchildren gets no parent at all, only its current one.

Test written first on the reviewer's probe ([1, 10->1, 100->10, 2],
editing 1: [] expected, [2] obtained before the fix).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12 18:15:52 -04:00
le king fu
4bdb00402e feat(categories): type-ahead combobox for the parent and keyword category fields
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m57s
Fixes #423

The "Parent category" field of CategoryForm and the "Category" field of
AddKeywordDialog were native selects with no typing. Both now use
CategoryCombobox, like the category cell of the Transactions page: typed
letters filter the list, which is ordered hierarchically with indentation
and shows seed categories through their i18n_key.

- CategoryCombobox accepts a minimal structural type (CategoryLike), so
  CategoryTreeNode rows pass without conversion; sortHierarchical is
  generic over it. Callers are unchanged.
- buildParentOptions (pure, tested) keeps roots and level-1 categories,
  leaves out the edited category, and keeps only roots when the edited
  category has children. That rule alone also leaves out its descent (a
  descendant is never a root), so no separate subtree walk is needed. The
  parent currently stored is always kept so the field never looks blank.
  CategoryForm gets an editingId prop, passed by CategoryDetailPanel.
- AddKeywordDialog reads the Transactions list (active and inputable,
  decision D2) through loadKeywordCategories, keeps the pre-filled
  category only when it is in that list (reconcileInitialCategory), and
  holds the apply button until the list is in. Its two fields sit above
  the scrolling block of matches, so the open list is not clipped by the
  scrollport.
- Tests: parentOptions, keywordCategory, and static renders of both
  components. Changelogs and the user guide (with its i18n mirror).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12 17:22:31 -04:00