Commit graph

688 commits

Author SHA1 Message Date
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
le king fu
1954502d10 state: sync after the release-deps milestone (#376, #345, #320, #358)
Milestone planned-2026-10-10-release-deps merged (PRs #414 to #418,
main 9e8c41b): new position paragraph and decision entry, the oldest
decision (#365) rotated out. Blockers: #419 point 1 before the next tag,
stale mentions of #376, #317, #345, #358 and #320 replaced by their
closure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:36:13 -04:00
le king fu
9e8c41b1f3 docs: say what still reports npm advisories after review of #417
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m53s
PR Check — Rust / rust (pull_request) Successful in 3m48s
Review follow-up of PR #417. CLAUDE.md and docs/architecture.md said
nothing re-evaluates `npm audit` in CI. No job fails on it, but the
`npm ci` step of check-frontend audits on every run and prints the count
(run 520: found 0 vulnerabilities; runs 512 and 519: 8), and the
Defenseur reports advisories on its side, which is where #358 came from.

Refs #358

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:05:44 -04:00
le king fu
d1d025df8d docs: fix three comments after review of #414
Review follow-ups of PR #414.
- audit.yml points to the concurrency comment of check-rust.yml, whose
  measurement covers push and pull_request runs only: say this workflow
  runs on schedule and workflow_dispatch, which it does not cover.
- reportService.test.ts said CI runs Node 20, so no node:sqlite; CI runs
  Node 24 since #376, and the harness itself is #392's.
- The release skill said /actions/tasks ignores `limit`. Measured
  2026-10-12: only without `page` (613 rows); with `page` it pages by 30
  or by `limit`.

Refs #376

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:05:44 -04:00
le king fu
08d54fd176 docs(adr): correct ADR 0023 and the guard's comment after review of #415
Review follow-ups of PR #415. The container does have python3 (pulled by
the apt packages of the install step, run 517): awk is kept because mawk
is part of the ubuntu:22.04 base image, which the workflow comment and
the ADR now say. #344 is a PR and the second drift, not the third. The
awk exit-code test is described by what it alone catches (an awk that
fails after printing every fact). The repair procedure gains the two
failures it had no answer for, and the ADR names the canary's limit.

The release commit edits Cargo.lock on main without a PR, so /release
now replays the guard's exact step text right after that edit (step 2),
checked on the stack's lock (exit 0) and on a drifted edge (exit 1).

Refs #345

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:32:51 -04:00
le king fu
230ecd85ad fix(release): drop the generic linux-x86_64 key from latest.json
Max's decision on PR #416, after its review measured who still read the
key. tauri-plugin-updater looks up "{os}-{arch}-{installer}" first, and
every version that ships the updater (v0.2.1 and later, plugin 2.10.0,
tauri-utils 2.8.2 in each tag's Cargo.lock) carries its bundle-type
marker, so .deb and .rpm installs never reach the generic key. Its last
readers were the AppImage installs v0.4.3 to v0.4.5 (v0.4.2 targets a
route that answers 404, earlier versions read GitHub) and binaries run
outside a package; none of them can install the .deb it served, and the
updater wrote that .deb over the running file. They now get
TargetsNotFound when checking: an error on the Updates card, nothing
installed.

The key leaves the script, scripts/check-latest-json.jq and
verify-release.sh together; the test compares the three lists. Docs fix
the readers named by #416 (v0.4.2 is not one) and the CHANGELOG entry
says what changes for those AppImage installs.

Refs #320

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:30:34 -04:00
le king fu
ba3db7c35d ci(release): check latest.json with one jq filter that the tests also run
Review follow-ups of PR #416. The check that release.yml runs before
deleting the published manifest moves from an inline jq program to
scripts/check-latest-json.jq, which release.yml applies with
`jq -e --arg version ... -f`. scripts/build-latest-json.test.mjs now runs
that same filter on every pull request: on the script's output, on a
wrong version, on nine broken manifests and on the shape published up to
v0.17.1. It also compares the key lists of the script, of the filter and
of verify-release.sh, so a list left behind shows before the tag.

Two new cases pin the version argument: 0.18.0 must come out as 0.18.0
(the other fixtures pass 0.17.1, the version the bundle names carry, so a
script that ignored its argument passed them all), and an empty version
must fail. Both mutations survived the review's pass.

No change to what the manifest holds: still four keys.

Refs #320

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:28:48 -04:00
le king fu
e8306aa260 fix(deps): refresh npm lock to clear 8 advisories
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m53s
npm audit reported 8 vulnerabilities (5 high, 3 moderate) on the lock
of main: react-router and react-router-dom (one advisory,
GHSA-qwww-vcr4-c8h2), vitest and @vitest/mocker (one advisory,
GHSA-82fw-gwwq-j7x9), browserslist, baseline-browser-mapping, nanoid
and source-map-js. Every fix sits within the range package.json
already declares, so only the lock moves; package.json is untouched.

Lock generated with npm 11.21.0 exactly:

  npx --yes npm@11.21.0 update react-router-dom react-router vitest \
    @vitest/mocker browserslist baseline-browser-mapping nanoid \
    source-map-js --package-lock-only --ignore-scripts

259 entries before and after, 20 changed, each resolved from
registry.npmjs.org with a new sha512 integrity. npm ci, under npm 10
and under npm 11.19.0, leaves the lock as is; npm audit: 0.

Only react-router 7.18.1 -> 7.18.4 changes the built app: a build from
a lock where only react-router moved equals the full build byte for
byte. Since 7.18.3, useNavigate() and <Navigate> reject a navigation
to another origin; the app only navigates to internal paths.

CLAUDE.md and docs/architecture.md no longer describe 2 high
advisories accepted for good: npm audit is at 0 on 2026-10-10, a
dated reading that nothing re-runs in CI.

Resolves #358
Resolves #317

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:14:34 -04:00
le king fu
5ce136f45e docs: name the readers of the linux-x86_64 fallback after self-review
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m57s
The fallback key is also read by the AppImage installs of v0.4.2 to
v0.4.5 (bundled until v0.7.3, published on Forgejo for v0.4.2-v0.4.3
and on the GitHub mirror for v0.4.3-v0.4.5, all reading the registry),
which look up linux-x86_64-appimage, absent, then linux-x86_64. Like a
marker-less binary, they cannot install the .deb it serves: the updater
writes its bytes over the running AppImage. State since v0.4.2, not
changed by this PR; the key stays as decided.

Also quote the error the update card really shows for a refused
package (invalid updater binary format).

Refs #320

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:55:02 -04:00
le king fu
da7129d529 fix: give rpm installs their own key in latest.json
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
latest.json only had linux-x86_64, built from the .deb. An .rpm install
looks up linux-x86_64-rpm first, then fell back on linux-x86_64,
downloaded the .deb and refused it (InvalidUpdaterFormat).

- scripts/build-latest-json.sh writes linux-x86_64-deb,
  linux-x86_64-rpm, linux-x86_64 (the .deb, as a fallback) and
  windows-x86_64 from the release assets. It fails unless it finds
  exactly one .deb, one .rpm and one -setup.exe, each with a non-empty
  .sig. The JSON comes from jq alone, notes read with --rawfile, file
  names encoded with @uri like the upload step.
- scripts/build-latest-json.test.mjs (vitest) runs it under
  bash -e -o pipefail on a temporary assets dir; check-frontend
  installs jq.
- release.yml calls the script, writes the notes output under a random
  delimiter, checks the manifest with jq before the registry DELETE,
  and names the .rpm instead of an AppImage in the release notes.
- verify-release.sh requires the four keys, the extension of each URL
  and the minisign file: of each signature, and exits 1 on any failure.
- Docs: release skill, qa-update-cycle section 3 (rpm procedure, keys,
  reason and removal condition of the fallback), architecture, README,
  CHANGELOG, STATE.

Resolves #320

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:49:12 -04:00
le king fu
3b8105c253 docs: fix two sentences of ADR 0023 after self-review
All checks were successful
PR Check — Rust / rust (pull_request) Successful in 3m36s
"Ce sous-arbre" lost its referent once the trigger paragraph stopped
naming a subtree: the sentence now says what is measured, that both past
drifts (#310, #344) came with RustSec advisory fixes. Rule 1's sentence on
a missing block is reworded.

Refs #345

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:17:09 -04:00
le king fu
8d9a3ca9e9 ci: guard the windows-sys 0.61.2 pin in check-rust
All checks were successful
PR Check — Rust / rust (pull_request) Successful in 5m12s
Seven crates in Cargo.lock accept windows-sys through a wide bound
(colored, dirs-sys, errno, rustix, rustls-platform-verifier, tempfile,
winapi-util), so cargo may point them at any windows-sys version already in
the graph. All seven point at 0.61.2, the state every Windows release has
shipped with since v0.14.0. Some updates re-resolve them (cargo update -p
hyper, -p h2, -p rustls-webpki -p tar did) down to 0.48/0.52/0.59 while
cargo only reports the packages it bumped: the Windows build then compiles
five windows-sys versions instead of three, and nothing says so.

A new blocking step, "Verify windows-sys pinning", reads the lockfile with
awk right after the suppression guard and fails if any edge of the seven
crates leaves 0.61.2 (a bare edge only passes when the lockfile holds a
single windows-sys, at 0.61.2), if a guarded crate has no block, or if the
set of windows-sys versions changes. awk's exit code is tested apart from
its output, every check is logged with a summary line, and a canary
(tempfile) must be read and checked by the loop.

The step text was extracted from the YAML and run as is, with the runner's
bash options, on copies of the lockfile: main's and the five release tags'
locks pass (9 checks, exit 0); the hyper-bumped lock, an added
windows-sys 0.53.0, a renamed guarded crate, a missing lockfile and a
guarded list without the canary all exit 1.

ADR 0023 records why 0.61.2, the measured cost (3 versions compiled for
Windows instead of 5), the triggers, the exit condition (windows-sys 0.62,
or convergence on a single version) and the repair when the step fails.
CLAUDE.md and docs/architecture.md cite the guard next to the suppression
check. Cargo.lock and .cargo/audit.toml are untouched.

Resolves #345

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:14:25 -04:00
le king fu
f19b027c5d ci: fix the release run lookup in the /release skill
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
PR Check — Rust / rust (pull_request) Successful in 3m41s
The /actions/tasks endpoint ignores `limit` and returns the whole list,
newest first, so `?limit=5` promised five rows it does not give. The
skill now selects the release.yml run of the tag directly (python3, as
verify-release.sh does), checked on v0.17.1: run 508, `v20.20.2`.

Also clarify check-frontend.yml's concurrency comment: same setup as
check-rust.yml, not the same group.

Refs #376

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:43:57 -04:00
le king fu
71a4e1e88f ci: move check-frontend, audit and release from Node 20 to Node 24
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m51s
PR Check — Rust / rust (pull_request) Successful in 4m46s
Node 20 reached end of life on 2026-04-30. check-frontend.yml, audit.yml
and release.yml now install Node.js 24 through NodeSource's setup_24.x,
as check-rust.yml has done since #375. Each step still prints
`node --version` in the job log.

The concurrency comment of check-rust.yml claimed the key cancels
obsolete runs on force-push. la-compagnie-maximus docs/forgejo-ops.md
measured the opposite on Forgejo 8.0.3: no pull_request run was
cancelled by a later one, and push runs are cancelled without the key.
The comment now states that measurement; the keys stay (harmless,
meaningful where honoured), and check-frontend.yml and audit.yml point
to it.

release.yml names the trust it places in NodeSource: the script runs as
root in the job container, whose Node.js then runs `npx tauri build`
with TAURI_SIGNING_PRIVATE_KEY. Kept on purpose, also documented in
docs/architecture.md, whose release.yml section described two jobs
(windows-latest, ubuntu-22.04) where there is one containerised job
cross-compiling Windows with cargo-xwin.

release.yml only runs on a tag, so Node 24 there is proven at the next
release: the /release skill now reads `node --version` from the release
run's log (step 9), and STATE.md lists it under the checks due at the
next release.

No CHANGELOG entry: CI only.

Resolves #376

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:40:28 -04:00
le king fu
87920ec02d state: sync after the go-live closure and the last manual checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:59:48 -04:00
le king fu
571104393b state: sync after the manual checks on the published releases
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:18:12 -04:00
le king fu
c7febe582e state: sync after v0.17.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:48:37 -04:00
le king fu
e4db715d6c chore: release v0.17.1
All checks were successful
Release / build-and-release (push) Successful in 22m49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:21:41 -04:00
le king fu
6de73b3c22 state: sync after the guided getting-started path (#396-#401)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:14:18 -04:00
le king fu
ee17f31352 docs(onboarding): the import counter, D9 and the QA cases from the stack's review
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m1s
- Guide (FR, EN, in-app keys and `.md`) and CHANGELOG: the Import card
  never says "Done." for an import that added no transaction, and the
  step waits for a file that brought one; a Fixed entry, since v0.17.0
  already ticked the step after such an import.
- CHANGELOG: the Free edition shows the Budget step locked, it does not
  lack one.
- architecture.md: the import counter's query, and D9's flag read
  outside any transaction, its log line written after the COMMIT.
- ADR 0022: `applyMigration` and `deleteTransaction` named among the
  writes that do not notify, with their reason.
- QA list (44 cases): the noop import at rest, the focus ring on the
  highlighted control, "Skip" on the folder step without a folder, the
  Transactions and Budget cards on a profile without transactions, the
  focus after "Hide" and "Close", the doubled counter of the unfolded
  list, and a month-agnostic note on the off-screen cell.

Reviews of #402 to #407.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:05:30 -04:00
le king fu
abc9076d23 fix(budget): drop the grid's target once the grid leaves the page
On a profile without transactions whose import steps were skipped, the
rows can land before the transaction count, both reads queueing behind
the DB lock: the grid mounted, reported "Expand all", then gave way to
the empty state, and the card kept "Click Expand all…" above it with no
grid on the page. The page now passes no target while it shows the
empty state, and a wiring test replays that sequence. Invisible in dev,
where StrictMode reorders the two reads.

The month test sets the clock to 23:00 on December 31, which is still
December in UTC: under the CI runner's zone, a month read in UTC passed.
The zone is pinned west of Greenwich, as in useImportWizard.test.ts.

Review of #400 (PR #406).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:05:30 -04:00
le king fu
ba71a6421d test(transactions): replay the hook's real sequence and pin the card's sentence
The reducer tests sent SET_PAGE_RESULT right after the initial state,
while the hook sends SET_LOADING then SET_ERROR(null) before each read,
and the mount effect's categories, sources and count before or after
the first page. Had any of those three actions disarmed the guided
fallback, the fallback would never run in the app, and the tests stayed
green: a test now reduces both real orders.

The page tests now also let the list's filters move, as a later
dispatch would: after the fallback the card names every transaction,
and an ordinary visit with "Uncategorized" picked by hand names the
uncategorized ones. The sentence follows the list, not the navigation
state.

Review of #399 (PR #405).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:05:30 -04:00
le king fu
74a6572475 fix(onboarding): give the focus ring back to the highlighted control
The ring of `[data-onboarding-highlight="true"]` is an author outline:
it wins over the browser's focus ring, so a keyboard user who reached
the highlighted control saw nothing change (WCAG 2.4.7, failure F78).
Measured by the review in headless Chromium, WebView2's engine: the
highlighted button kept `solid 3px` focused or not. `outline: revert`
under `:focus-visible` gives the browser's ring back, kept as is in the
built stylesheet; the contrast test reads the rule from the stylesheet.

Review of #398 (PR #404).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:05:30 -04:00
le king fu
ec3b5d5185 test(data): pin each restore's notification after the DB lock
ADR 0022 wants a transactional write to notify once `withTransaction`
has returned, never from its callback. Only the categorization test
checked the lock; the restore tests checked that the notification
followed the COMMIT, which a notification sent from inside the callback
also satisfies. Each restore mode now asserts the lock was released.

Review of #396 (PR #402).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:05:14 -04:00
le king fu
696b83fb10 fix(onboarding): skip the D9 transaction once its flag is set
`initializeGuidedPath` opened BEGIN, read the flag, then COMMIT at every
mount of the provider: start-up, and every profile switch, creation or
deletion. While a `withTransaction` callback runs, `db.ts` lets every
other query bypass its lock, the overlap its serialization exists to
prevent.

The flag is now read first, outside any transaction, through the queued
connection: once D9 ran, an opening costs that one read. Without it, the
transaction re-reads the flag before writing, so two first loads still
cannot apply the rule twice (CWE-362).

Pinned: no transaction and no write with the flag set; the flag counts
only for "1", on both reads; a first load that sets the flag between
the two reads wins; the in-transaction reads still follow BEGIN.

Review of #396 (PR #402).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:05:14 -04:00
le king fu
a58f8b98bc fix(onboarding): count an import only once the file brought a transaction
The "Import a first file" step counted `imported_files` rows in status
`completed`. `executeImport` writes that row before its first insert, so
an import that added nothing (a file holding only its header, or one
whose rows were all in error in the preview) ticked the step: the Import
card then said "Done." at rest and sent the user to /transactions, which
had nothing to categorize and led back to the import.

The counter now keeps a completed file only when a transaction points at
it (`EXISTS` on `transactions.file_id`, covered by
`idx_transactions_file`). Replayed with sqlite3 on
`consolidated_schema.sql`: 0 after an import without a valid row, 0
before a successful import's first insert and 1 after, 0 once the file
turns to `error`. Duplicates only change nothing: their transactions
came with an earlier completed file, and a restore writes its own.

The tests run without SQLite, so the rule is pinned on the query, and
the Import page is pinned at rest after such an import: step 4 again,
on the first source card, without "Done.".

Review of #398 (PR #404), option A chosen by Max.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:05:14 -04:00
le king fu
5c18fddfc0 docs: say where the Import card holds back "Done." (self-review)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m6s
The guide and the changelog said the card never says "Done." for an
import that added no transaction. It holds it back on the report only:
on the Import page the card moves from one step to the next, and says
"Done." once the file is imported. The locale test no longer depends on
the order of the sections in the files, which DocsContent ignores.

Resolves #401

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:59:55 -04:00
le king fu
d6f42dbe32 docs: architecture, QA checklist and changelog for the guided path
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m59s
- architecture.md: the Onboarding section describes the guided path as
  delivered (provider and useOnboarding(), the data-change channel of
  ADR 0022, the three onboarding preferences, rule D9, the pure models of
  onboardingPath.ts, the banner, the card latch and CARD_PAGES, the step
  cards, the highlight by props and CSS); the utils count and the
  user_preferences row follow the stack.
- qa-onboarding.html: the phase 1 cases that still hold, without the
  compact bar ones, and four guided-path groups, including the manual
  checks the five links left for a release build (WebKitGTK and WebView2,
  Orca and NVDA, reduced motion, keyboard, Free edition, rule D9).
- CHANGELOG (EN and FR): one entry for what every step card does, one per
  page, the path hidden on active profiles covers the banner too, the
  skipped state, and the guide's new sub-section.
- STATE.md: manual checks due on the next release.

Resolves #401

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:53:58 -04:00
le king fu
7ce5816bb2 test(docs): hold both locales to the shape the user guide reads
DocsContent and DocsSubsections read the guide straight from fr.json and
en.json, but their tests run on fixtures: a string where an array is
expected, a sub-section without its steps or a French list longer than
its English one only showed in the app. DocsContent.locales.test.ts reads
the real files: every section DocsContent renders and no other, the
strings and string arrays it reads, sub-sections that readSubsections
keeps whole, anchors of their own, French and English item for item, and
the path's sub-section in the section "Open the guide" opens.

DocsContent exports SECTIONS for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:53:52 -04:00
le king fu
b0db1d3102 docs(guide): describe the guided getting-started path in the user guide
The "Getting Started" section, which "Open the guide" opens at the end of
the path, gains a "The getting-started path" sub-section in both locales
and in docs/guide-utilisateur.md: the banner and its list, the step cards
and their outline, "Skip this step" and "Resume", the end of the path,
the profiles that find it hidden from the start, and how to show it again
from Settings -> Users. It uses the sub-section shape DocsSubsections
already reads for the Import section.

The Dashboard and Settings sections no longer describe the compact bar
the banner replaced, and the Profiles section says the path belongs to
each profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:53:47 -04:00
le king fu
d76c387fa5 docs: changelog for the Budget step card
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m0s
Added: the getting-started card of the Budget page. architecture.md
stays as it is: no line of it is made stale (the budget folder still
holds five components, the pure module is not one).

Resolves #400

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:21:33 -04:00
le king fu
2386486762 feat(onboarding): the Budget page's step card and its highlight
The budget grid opens collapsed and only its leaf rows take an amount,
so the card first points at "Expand all", then at the current month's
cell of the first editable row shown (D12). `budgetOnboardingTarget`
(pure, beside budgetTableResults.ts) decides it from the rows each
section shows, in the order it shows them; the collapse state stays
BudgetTable's own and the card never unfolds the grid.

BudgetTable rings that one control when the page asks
(`onboardingTarget`) and reports its kind through
`onOnboardingTargetChange`, in an effect, at mount and on change only:
the page needs it for the card's sentence before it asks for the ring.

`budgetCardModel(input, checklist)` gives the "collapsed" sentence for
"Expand all", the "flat" one for a cell, no sentence and no ring before
the grid says, and "Done." once the budget step is checked, which the
card needs to end the path. CARD_PAGES gets /budget. BudgetPage renders
the card first, outside the loading dim the grid gets after each save,
and asks for the ring only while the card shows. The Free edition never
mounts the page and never latches it: its path ends at 5/5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:21:29 -04:00
le king fu
9a695a2504 docs: changelog and architecture for the Transactions step card
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m5s
Added: the getting-started card of the Transactions page and the guided
entry its step link opens. architecture.md gets the lines this change
touches: useTransactions' start filters and single fallback, and the
i18next `lowercase` formatter in the Internationalisation section.

Resolves #399

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:56:49 -04:00