Commit graph

100 commits

Author SHA1 Message Date
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
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
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
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
86eff9b7a3 fix(profiles): follow-ups of the review of PR #436
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m8s
PR Check — Rust / rust (pull_request) Successful in 3m54s
- withoutFolders no longer keeps the first word of a folder that holds
  a blank (`C:\Users\Jean Dupont\...\profile_x.db` gave
  `Jean profile_x.db`): a path starts at a recognised prefix (drive,
  `/`, `~/`, `./`, UNC, `<profile folder>`) and runs to its last
  separator, blanks included, until a line break, a double quote, a
  backtick, `;`, `|`, a `,` or `:` before a blank, a closing parenthesis
  not followed by a separator, or a blank before another path. When
  nothing ends it, the text up to the last separator of the line is
  dropped rather than a folder let through. A quote before the path is
  not part of it, so `'C:\Users\Bob\x.db'` gives `'x.db'`. Relative runs
  with no recognised prefix keep the previous fallback. No error line
  contains a `/` nor a `\`, as before.
- ADR 0021 section 7 describes that rule and its limit.
- CHANGELOG.md, CHANGELOG.fr.md: no blank line between the two Fixed /
  Corrigé bullets.

Refs #395

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:57:29 -04:00
le king fu
ac3a981f64 test(i18n): follow-ups of the review of PR #435
- renderCategoryLabel.test.ts passes markedT itself, its assertions in
  the marked form (`t(balance.category.cash[defaultValue=cash])`): the
  second fake `t` of the repository is gone.
- i18n-mock-guard: a fake `t` is also caught as a binding (`t = (`,
  `t = vi.fn(`) or as a `t: vi.fn(` property, and a file that mocks
  react-i18next must call `i18nMock(`, comments dropped before the
  search. Each new rule has its own case in the guard's tests; the
  binding rule found renderCategoryLabel.test.ts before its migration.
- docs/architecture.md: the "only fake t" sentence is scoped to test
  files and says the guard only refuses the forms it recognises.

Refs #389

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:53:18 -04:00
le king fu
a1c31a1da7 test(harness): follow-ups of the review of PR #434
- useOnboardingChecklist.wiring.test.ts: a data event, then a route
  change 100 ms later, still gets its deferred re-read (two LOAD_START).
  Pins the follower's deps: with `pathname` among them, the cleanup drops
  the pending timer and the test goes red (mutation M1 of the review). The
  comment that claimed "one subscription only" now points to this test.
- hookHarness.test.ts: a dependency array that shrinks, or grows by
  `undefined`, recomputes the memo (mutation M8, length branch of
  depsChanged).
- NOT SIMULATED, in hookHarness.ts and docs/architecture.md: the dispatch
  of a useReducer keeps the reducer of the first render, where React
  applies the latest one.

Refs #409

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:51:20 -04:00
le king fu
74fae140b1 fix(db): follow-ups of the review of PR #433
- close() on a getDb() handle runs as a "connection close" section and
  checks its generation: a handle kept from a previous profile rejects
  with DbConnectionClosedError instead of closing the current profile.
- closeUnlocked never rejects: the state is cleared and the generation
  bumped before close(), a failing close of the old pool is logged with
  logWarn, connectToProfile still opens the next profile and closeDb()
  resolves without a connection.
- The blocking alert names a getDb() call in a callback as a possible
  cause only: a long restore also waits 15 s.
- The generation check of the transaction handle is documented as a
  defence, not reachable today.
- Tests: the "behind the queue" alert (pins the reset of lockHolder),
  close() on a stale and on the current handle, a failing close during a
  switch and in closeDb().

Refs #408

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:50:12 -04:00
le king fu
419b20e769 feat(profiles): name the copy of a stopped catch-up and keep one copy per profile
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m1s
PR Check — Rust / rust (pull_request) Successful in 3m56s
- ProfilePreparation.catch_up_backup carries the file name of the copy as
  soon as it exists, so the Journal error line of a catch-up that stops
  afterwards (history_write_failed, pragma_failed, apply_pending) names it.
- Error lines and the JS fallback message keep only the last segment of a
  path (withoutFolders): no error line contains a slash or a backslash.
- The copy is taken again at every catch-up, written as .tmp, synced and
  renamed over a same-name copy; after a successful catch-up only, the
  other copies of that profile are pruned (remove_backups keep argument).
- Pins: a pre-existing foreign key violation in the profile without
  category 31 (RV4, RV7) and the Journal line that reports it (RT2).
- Ignored test measuring prepare_at on 100 000 transactions at floor 6:
  306 ms in release, so STARTUP_TIMEOUT_MS stays 10 s (ADR 0021).
- Guide, docs.profiles.tips, ADR 0021, architecture and changelogs.

Resolves #395

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:18:18 -04:00
le king fu
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
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
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
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
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
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
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
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
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
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
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
le king fu
ee26fe0888 docs: changelog and architecture for the Import step card
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m55s
Added: the getting-started card of the Import page. Fixed: the "No import
folder" empty state no longer flashes while the folder is read.
architecture.md gets the lines this change made stale: the component
count, the onboarding folder, useImportWizard's `importFolderLoaded` and
the Import row of the empty states table.

Resolves #398

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:34:57 -04:00
le king fu
46e714234e docs: changelog and architecture for the getting-started banner
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
CHANGELOG (EN and FR): the banner under Added; the compact bar replaced
by the banner, the states read out by the list, and the re-show button
of Settings → Users under Changed. docs/architecture.md: the lines this
change made false (component count, the onboarding/ folder, the compact
mode and the re-show sentences). The user guide is rewritten by the
last issue of the milestone.

Resolves #397

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:02:47 -04:00
le king fu
68b90f5f16 docs: ADR 0022 and changelog for the data-change channel
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m46s
ADR 0022 records the channel: its scope, the writes that notify and when,
the 150 ms wait on the subscriber side, stale answers ignored, and who may
subscribe. architecture.md gets its row in the ADR table, plus the rows this
change made wrong (onboardingService, useOnboardingChecklist, the new
dataEvents service, the contexts folder). The changelogs record rule D9: a
profile that already had transactions hides the getting-started checklist
the first time it is opened with this version.

Resolves #396

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:34:02 -04:00
le king fu
02fca2ad3c docs: ADR 0021, guide and changelog for the profile catch-up (#390)
ADR 0021 records the catch-up rules and amends ADR 0003 and 0005, whose
claims about secondary profiles were false. architecture.md, CLAUDE.md
and the migration rule describe the two migration paths; the guide tells
where the pre-update copy lives and how to remove a database a deletion
on Linux left behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:35:00 -04:00
le king fu
ed35570121 fix(license): drop an activation token that no longer stands for this machine
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
PR Check — Rust / rust (pull_request) Successful in 3m21s
A data directory written by v0.15.0 or earlier can hold a valid key next
to the activation token of another license, typically a refunded hence
revoked one. The License card read "Activated", market prices were
refused, and "Activate" was never offered: the folder could not recover
without a manual file removal.

- Prices: a 403 license_revoked drops the refused token, and only while
  the file still holds it, so an activation that lands while the refused
  request is in flight keeps its new token (CWE-367). The key's revoked
  state is never written from this route. The token is read through
  ACTIVATION_FILE by the directory-based read_stored_activation_token_in,
  in fetch_price_in, testable without AppHandle.
- Machine list: list_activated_machines returns { machines,
  token_dropped }. A complete list that names this machine under neither
  identifier drops a token that activates this machine, when the OS id is
  readable and the token is the one on disk when the list was asked for.
  An error, a revoked key or a rate limit drops nothing.
  machines_from_verify counts the rows it skips, which the list still
  tolerates. This also gives "Activate" back to a machine deactivated from
  another computer.
- Front: the activation status is read again after every list load; a
  fixed log line (ACTIVATION_TOKEN_LOG) is written on token_dropped and on
  a price refused as revoked; the revoked-license price message invites to
  activate the machine again.
- Docs: architecture, ADR 0017 (#365 amendment), ADR 0020 (rule 4), user
  guide and in-app tips, changelogs.

Resolves #367

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:58:25 -04:00
le king fu
e2f0fb86da feat(license): remove the stored license key from the License card
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
PR Check — Rust / rust (pull_request) Successful in 3m26s
"Remove the key" on the License card removes license.key, then the
activation token, and keeps license.check, so a revoked key removed then
entered again stays Free. Removing the key stops the revocation check,
which until now only storing another key could stop.

When this machine is activated with a valid key, its slot is freed on
the license server first (remove_license with release_slot). A release
that fails removes nothing; on network, server, rate_limit or
invalid_response the card asks whether to remove the key anyway, the
slot then staying occupied. no_active_activation, license_not_found and
license_revoked (recorded for the key sent) mean the slot is free.

- Rust: remove_license / remove_license_in, release_this_machine_slot,
  this_machine_token_id, and the shared remove_activation_token_in
  helper, now also used by store_license_in and the deactivation.
- Front: removeLicense service; performRemoveLicense in
  useLicenseMachines (confirmations by state, error table, in-app log
  lines); removing and removeError in the machines reducer, which lock
  key entry, Activate and Deactivate; the button for any stored key,
  valid or revoked.
- Docs: user guide, in-app tips, architecture, ADR 0017 amendment,
  changelogs.

Resolves #370

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:14:36 -04:00
le king fu
ab78aa9ddc fix(license): check a revoked key as soon as it is entered
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m44s
PR Check — Rust / rust (pull_request) Successful in 3m25s
A license key entered during a session was only checked against the
license server at the next start: a revoked key kept its edition until
the app restarted, or until a machine activation was refused (#365
checked at start only).

Front: performSubmitKey launches the revocation check after
VALIDATE_DONE, without waiting for it, and never after a refused key.
SubmitKeyDeps gets a single launcher, startRevocationCheck(wasRevoked),
built once by the provider with revocationCheckLauncher and shared with
the start check, whose starter is unchanged.

Rust: the command runs the checks one at a time. A static
tauri::async_runtime::Mutex (REVOCATION_CHECK_TURN) is held by
check_revocation_in_turn over the whole check, request and verdict
included, and the time is read once the turn has come. A key entered
before the first ready state, checked by the entry and by the start,
sends one request; the check of a key entered while another one is in
flight waits for its verdict instead of overwriting license.check.
check_revocation_in is unchanged, and so are Cargo.toml and Cargo.lock.

Docs: ADR 0017 (amendment #365, point 5), user guide, in-app docs
(docs.settings.tips, docs.editions.tips), architecture, and both
changelogs under [Unreleased] (Fixed / Corrigé).

Resolves #373

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:26:10 -04:00
le king fu
667e058a77 fix(onboarding): apply the three review follow-ups on the stack head
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m47s
- Guide: drop the internal provenance (author, date, issue number) from the
  BMO status line in the three copies (fr.json, en.json, guide-utilisateur.md).
  The status now only says the procedure is reconstructed and unconfirmed.
- Dashboard: gate `showCompactChecklist` on `budgetReady` so a Base+ profile
  with the five other items done no longer sees the compact list vanish (5/5
  under the fail-closed entitlement) and reappear (5/6) once the licence
  resolves.
- Test: pin TZ=America/Toronto around the `formatLocalDate` tests and assert
  that `toISOString()` disagrees, so a UTC regression is caught on the UTC CI
  runner too (measured: the mutation passed 7/7 under TZ=UTC before, fails now).

Follow-ups raised by the independent reviews of PRs #381, #378 and #382.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 08:47:30 -04:00
le king fu
d11337ccf6 docs(onboarding): changelog, architecture, user guide and interactive QA checklist (#357)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
Closes phase 1 of the onboarding wave (#351-#356) on the documentation side.

- CHANGELOG.md / CHANGELOG.fr.md: Added (guided empty states, getting-started
  checklist, sample folder, bank CSV export guide) and Changed (re-show button
  in Settings > Users, Import "?" popover linking to the guide) under
  [Unreleased] / [Non publié], same entries in both languages.
- docs/architecture.md: create_sample_import_folders (fs_commands 6 -> 7,
  total 36 -> 37) with its contract, onboardingService, useOnboardingChecklist,
  the onboarding_checklist_hidden preference, the samples/ corpus next to csv/,
  and an "Onboarding" section (four empty states and their signal, source of
  truth of the six checklist rows, sample-folder flow). Tree counts measured
  for the directories the wave touched.
- docs/guide-utilisateur.md chapters 3 and 11 + docs.dashboard.* and
  docs.settings.* keys (FR/EN). Hand-off from #355: docs.gettingStarted.steps
  aligned with the six-step quick start of the markdown, docs.import.steps
  mentions the sample folder, so the three guide copies agree.
- docs/qa-onboarding.html: interactive manual-test checklist, French only,
  static, no network dependency, state in localStorage, plain-text report to
  the clipboard with a visible fallback, reset button.

Resolves #357

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:25:55 -04:00
le king fu
e0ee09bca7 feat(docs): sub-sections in the user guide and bank CSV export guides (#355)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m53s
Add an optional `subsections` array to a user-guide section, rendered by
the new pure `DocsSubsections` component between the "How to" block and
the tips. Each entry has an id, a title, an optional italic status line,
an ordered list of steps and an optional trailing note. The ids are
registered in `sectionRefs` of `DocsContent`, so the existing hash
navigation (`/settings/users#import-export`) reaches the sub-sections
too. Only the Import section declares the key; the eleven other
sections render exactly as before.

The Import section gets six blocks in French and English: a generic
"export a CSV from your bank" block, then Desjardins, Tangerine, RBC,
National Bank and BMO with their verification status. The National Bank
and BMO blocks, which ask the user to send a header line through the
feedback form, carry the warning never to paste an account number, a
balance or a real transaction, in the JSON files and in the markdown
guide alike.

`docs/guide-utilisateur.md` receives the same six blocks in chapter 4,
a mention of the "Create a sample folder" button in its "How to", and a
"Quick start" in chapter 1 rewritten as the six steps of the
getting-started checklist, same order and wording as
`onboarding.checklist.items.*`.

`DocsSubsections.test.tsx` calls the component directly (no jsdom):
null for a section without sub-sections, one block per entry with its
id and ref registration, status line and note rendered only when
present, malformed entries dropped.

Resolves #355

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:00:09 -04:00
le king fu
f821492274 ci: document the cache evictions and key inputs found in review (#234)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m45s
PR Check — Rust / rust (pull_request) Successful in 3m21s
Applies the three suggestions of the review of #375:

- The runner's cleanup keeps one entry per key, whatever its scope: a PR
  run that saves under the shared entry's key evicts it for every PR until
  the next Monday, and two PRs on the same non-main lockfile evict each
  other. Added to the list of cold runs.
- The cache key also hashes the job's CARGO*/RUST*/CC*/CFLAGS*/CXX*/CMAKE*
  variables, and ignores the app's own version: changing env: means a cold
  run until Monday, a release does not.
- A Monday restore that fails with a 404 after a quiet week is a known
  runner race (forgejo/runner#1741), not #234 coming back.

Refs #234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:04:50 -04:00
le king fu
20db766f52 docs: describe the Rust cache, its weekly writer and the absent npm cache (#234)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m41s
PR Check — Rust / rust (pull_request) Successful in 3m22s
CLAUDE.md and docs/architecture.md still said the Actions cache was
removed until #234 was fixed. They now describe rust-cache, the per-PR
isolation of what a pull_request run saves, the weekly run on main that
writes the shared cache, the expected cold runs, and why check-frontend
has no npm cache. The audit.yml paragraph no longer quotes the outdated
"1 PR in 40" figure.

Refs #234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:22:08 -04:00
le king fu
b107cf42ce fix(license): apply the review of #368
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m45s
PR Check — Rust / rust (pull_request) Successful in 8m54s
- A state file that cannot be written now has a test on both of its paths:
  the attempt (nothing is sent, or a read-only data directory would send a
  request at every start) and the verdict (the key is not reported revoked
  when nothing on disk says so). The first path was correct but unguarded:
  the review's mutation left all 185 tests green.
- The storage outcome no longer claims that nothing was sent: it also covers
  a verdict that could not be written. Reworded in licenseService.ts, the
  in-app log line and docs/architecture.md.
- The Updates card names a revoked key in its Free branch, between the
  message and the CTAs: a refunded Base buyer first meets the loss of
  automatic updates there (Max, 2026-10-03). The error page stays as it is.
- A revoked key keeps being checked once per 24 h while it stays stored, so
  that a correction made on the license server is picked up (Max,
  2026-10-03). Declared in the user guide, the in-app docs, the changelogs
  and the ADR 0017 amendment.

Refs #365

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:29:47 -04:00
le king fu
e24c30fe6a feat(license): online revocation check — a revoked key falls back to Free (#365)
A refunded license was revoked on the server but kept working in the app:
the edition is resolved offline from the key alone, and the flag added by
#347 lasted the session only and never touched the edition.

Rust
- license.check, next to the key, holds what the license server last said
  about it: {key_sha256, attempted_at, revoked}. Never the key itself. A
  missing, unreadable or invalid file, or a state about another key, reads
  as "no state": not revoked, check due.
- entitlements_from_dir resolves a revoked key to ("free", []), which also
  locks auto-update. read_license and store_license report the state.
- check_license_revocation: POST /v1/licenses/verify with the product and
  the key only. Nothing is sent without a stored key that validates, nor
  within 24 h of the last attempt, which is written before the request.
  Fail-open: only "valid" lifts the state, only "revoked" sets it.
- Activation, machine list and deactivation record a license_revoked answer
  under the key they sent.
- fetch_price reads error.code of a 403 and names license_revoked instead of
  premium_required. It records nothing: the price route judges the license
  of the activation token, which the client cannot tie to the stored key,
  and 0.15.0 left the token of a previous key next to a new one (#367).

Front
- LicenseContext takes the revoked state from Rust (info.revoked); the
  session flag and markRevoked are gone. The check starts once per app
  start, writes its outcome to the in-app log, and reloads the context only
  when the state changed. A revoked key entered again stays Free.
- UpsellGate and the License card name the revocation.

Docs: ADR 0017 amendment (D13 replaced, the first unsolicited request and
its bounds), architecture, user guide, price contract annex, changelogs.

Refs #365

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:45:34 -04:00
le king fu
0a9c9f71dd fix(license): apply the review of #366
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m47s
PR Check — Rust / rust (pull_request) Successful in 8m46s
The review approved the previous head with five suggestions; this commit
applies the four that touch the repository (the fifth is the manual check
in the pull request description).

- Tests: a valid token issued for another machine goes with any row of
  this machine. Two rows added to the deactivation table: the reviewer's
  mutation that kept such a token stayed green before them.
- ADR 0020, removal condition: counted from the last raw-claim token the
  server issued, not from a client version. A Free 0.15.0 no longer
  updates and can still activate with the raw id months from now.
- ADR 0020: names what the transition leaves behind when a machine is
  activated again while its raw row is still active (a second row, one
  more slot), and what a public key does not cover (whoever already holds
  the OS id can recompute the derived one).
- "No command hands the OS id to the webview" was wider than the code: the
  server's list still carries it for a row from before the derivation.
  Bounded in the ADR, architecture.md and the licenseService.ts comment.

No behaviour change. Rust 168, vitest 1314.

Refs #350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:12:32 -04:00
le king fu
e5662ad018 fix(license): send a derived machine identifier, never the OS machine id (#350)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
PR Check — Rust / rust (pull_request) Successful in 9m19s
Activation sent `machine_uid::get()` as is: /etc/machine-id on Linux,
MachineGuid on Windows. machine-id(5) calls that id confidential and asks
applications to send a keyed hash under a fixed, application-specific key
instead. The license server stores what it receives next to the license
email and the host name, lists it back and signs it into the activation
token.

- derive_machine_id: HMAC-SHA256 under a frozen key, 64 lowercase hex
  characters, pinned by two reference vectors. An empty OS id is refused,
  never hashed.
- Activation sends the derived identifier and requires the issued token to
  carry it. Nothing else originates a machine identifier.
- A machine activated before this change stays activated: a valid token
  whose claim is the raw id of this machine still reads "Activated" (D1).
- The machine list flags `is_this_machine` in Rust, for a row under either
  identifier (D3). `ActivationStatus` no longer carries an identifier and
  the unused `get_machine_id` command is removed: no command hands the OS
  id to the webview any more.
- After a deactivation the local token is dropped when it stood for the
  freed slot: any row of this machine takes it, except the old raw row
  while the token stands for the derived one.
- ADR 0020 records the derivation, the transition and the condition for
  removing the raw-id compatibility.

Behaviour changes beyond the issue text: the machine list and a
deactivation no longer fail on an unreadable OS id (best effort), the
activation status then reads "not activated" instead of failing, and the
unused `store_activation_token` command refuses a token issued for the
raw id.

No new dependency (hmac and sha2 were already declared); Cargo.lock is
untouched. Rust 151 -> 168 tests, vitest 1311 -> 1314.

Refs #350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:52:48 -04:00
le king fu
ce408bfb7d docs: add the onboarding analysis behind spec-onboarding-phase1
The seven issues of the spec-onboarding-phase1 milestone cite
docs/analyse-onboarding-2026-09.md as their source analysis, but the
file had stayed untracked since 2026-09-24 and only existed on one
machine. Phases 2 and 3 of the onboarding work are described nowhere
else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:21:14 -04:00
le king fu
cdf4df85f7 fix(license): apply the second review of #364
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m43s
PR Check — Rust / rust (pull_request) Successful in 9m15s
- Key entry and machine activation can no longer overlap. An activation
  in flight writes the token of the key it read: a key stored meanwhile
  ended up next to the previous key's token, the dead end 3f90635
  closes. "Enter a key" and the form's submit are disabled while an
  activation runs (LicenseKeyForm gains a busy prop), and "Activate"
  while a key is being stored.
- The "Activated" badge is hidden for a revoked key: the server
  deactivated every machine of the license.
- ADR 0017: the #347 amendment also points at section 1, which still
  mentions the machine-binding downgrade.

Refs #347

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 20:44:51 -04:00
le king fu
3f90635298 fix(license): apply the review of #364
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
PR Check — Rust / rust (pull_request) Successful in 10m35s
- store_license_in drops activation.token when the stored key changes.
  The token names the previous license to the price server (its sub is
  the license id, the key carries the email), so the card showed the
  machine as activated while prices were refused for the old license.
  The same key entered again keeps its token; a refused key changes
  nothing.
- get_activation_status goes through activation_status_in(dir,
  machine_id, key), tested in a temporary directory: a missing,
  expired, foreign-machine, foreign-key or unreadable token reads as
  not activated. It alone decides "Not activated" and "Activate".
- LicenseCard is keyed by licenseIdentity(info): another key remounts
  the body, which re-reads the activation and drops the previous key's
  machine list.
- invalidSignature asks to check the copy first. jsonwebtoken checks
  the signature before decoding the payload, so a damaged copy often
  reads as a signature failure (characterization test). User guide and
  changelogs aligned.
- Render tests: the other errors stay visible under Machines, and the
  edition's green check gives way to an alert for a revoked key.

Refs #347

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 19:58:22 -04:00