Skip to content

feat(ui): let the transactions trend card switch between four metrics - #292

Merged
argszero merged 1 commit into
mainfrom
feat/tx-trend-four-modes
Sep 24, 2026
Merged

argszero merged 1 commit into
mainfrom
feat/tx-trend-four-modes

Conversation

@argszero

Copy link
Copy Markdown
Owner

What this is

The transactions page trend card #tx-trend unconditionally drew two bars per
bucket
(spend + income) and offered no control at all. The design prototype
(docs/prototype/aitokenpool-console.html) draws the same two bars, so this is a
new feature request, not a control that was lost when the card was built.

The card gets a .tabs segment — #tx-trend-modes, the shape already used by
#tx-tabs — with four modes:

mode rendering
total points (default) line, cumulative net change over the window
spend bars, spend series only
income bars, income series only
spend + income bars, both series

No backend change, and no extra request

/api/transactions/trend (src/routes/wallet.rs) already applies the range, the
type tab and the column filters, and already returns net, income and
expense per bucket. All four modes are therefore derived from the payload the
card already has.

Consequently the mode is not part of the request: it does not enter
txQuerySig() and the mode buttons do not call reloadTransactions().
Folding it into the reload trigger would issue a fresh request on every click and
make the four modes stop sharing one payload.

The mode that is easiest to get wrong

"Total points" is the cumulative net change inside the window: the running sum
of net from the earliest bucket, starting at 0, and it can be negative. Under
"all time" with no column filter its last point equals the real current balance —
but it is not an absolute balance level, and with a column filter applied an
absolute level is not even defined ("the balance of model=deepseek" does not
mean anything).

So that mode swaps the card title and subtitle for tx.trend.card.net and
tx.trend.net.sub, which state the definition outright, and draws a line
(sparkline()) rather than bars — bars would relativise the level and make the
balance look like it keeps resetting to zero. The y domain is taken from the
signed range and keeps the zero baseline inside it. The other three modes keep
the existing bar rendering and normalisation.

Self-consistency of the four modes

  • the normalisation max is taken from the series actually displayed (so
    looking at income alone is not flattened by a spend peak);
  • the title tooltip and the legend name only the displayed series — a
    hidden series does not linger in the legend or the tooltips;
  • the three empty states are unchanged (not loaded / load failed / no records in
    window).

Where the reset lives

Each entry to the transactions view returns to the default mode. The reset is at
the view entry in switchView(), not in renderView() or
renderTxTrend(): those two are also reached from the atp:langchange
handler, so hanging the reset on the render chain would silently reset the user's
chosen mode on a language switch. Switching is not persisted across sessions.

i18n

Four keys added to both language packs (tx.trend.mode.net,
tx.trend.mode.both, tx.trend.card.net, tx.trend.net.sub); the two existing
metric keys (tx.trend.metric.expense, tx.trend.metric.income) are reused on
the buttons. No existing value was changed. Cache-bust query bumped to
20260924-1 on css/style.css, js/i18n.js and js/app.js.

Scope

Only this one card on the transactions page. The dashboard card
(#dash-trend / renderDashTrend()) is deliberately untouched.

Evidence

Three instruments, all run against the tree in this PR; the pre-fix tree is
materialised from the parent commit (git archive 1f1bd68 ui).

  • logic probe (r109_logic_probe.js) — legs=9 failed=0; the 6 synthetic
    mutations each fail exactly the leg they target.
  • render probe (r109_render_probe.js, jsdom, applies the edit sheet itself):
    pre-fix tree exactly 15 of 15 legs red; fixed tree legs=16 failed=0.
    Its edit output is byte-identical to the landed files —
    app.js 908aeb05… → 12788e38…, index.html 6b8365b9… → 8b14c8d3…,
    i18n.js 3cbd93d9… → e61c006e…, css/style.css e5dd3518… → be072471….
  • end-to-end probe (r109_e2e_probe.js, real index.html + the four real
    scripts + a real login + real click()): pre-fix tree 7 of 7 legs red;
    fixed tree legs=7 misdeclared=[]. It also checks the two edit tables
    agree (L0) and, separately, that clicking a mode does not reach the list at
    all — no request issued and the pager stays on page 2 (L4) — which a
    request count alone would miss, since the wrong wiring
    (reloadTransactions()) sends no request but still throws the user back to
    page 1.
  • cargo test: 373 passed / 0 failed (unchanged from the parent — this PR
    adds no Rust gate). cargo fmt --check clean.
  • cargo clippy --all-targets -- -D warnings: rc=101 from a pre-existing
    collapsible_match at src/protocol.rs:662, which a pristine parent-commit
    tree reports identically. Not part of this diff; left untouched.

The card `#tx-trend` always drew two bars per bucket (spend + income) and
offered no control at all — the design prototype does the same, so this is a
new feature request, not a lost control.

Add a `.tabs` segment (`#tx-trend-modes`, the shape already used by
`#tx-tabs`) with four modes: total points (default) / spend / income /
spend + income. All four are derived from the payload the card already
fetches — `/api/transactions/trend` returns `net`, `income` and `expense`
per bucket — so no endpoint or backend change is needed and the mode must
NOT enter `txQuerySig()` or reach `reloadTransactions()`: switching repaints
this card only and never issues a request.

The "total points" mode is the window's cumulative net change (running sum
of `net` from the earliest bucket, starting at 0; it can be negative) drawn
as a line via `sparkline()`, with the y domain taken from the signed range
and the zero baseline kept inside it. Because that is not an absolute
balance level — with a column filter applied, "the balance of
model=deepseek" is not defined — the card title and subtitle say so
explicitly (`tx.trend.card.net` / `tx.trend.net.sub`). Bar modes keep the
existing normalisation, except that `max`, the tooltip and the legend now
follow only the series actually on screen.

The mode resets to the default on each entry to the view; the reset lives at
the view entry in `switchView()` rather than in `renderView()` /
`renderTxTrend()`, because those two are also reached on
`atp:langchange` — hanging it there would silently reset the user's mode on
a language switch. Switching is not persisted across sessions.

Adds 4 keys to each language pack and bumps the cache-bust query to
`20260924-1` for `css/style.css`, `js/i18n.js` and `js/app.js`; the
i18n-pack positive-control constants are updated to the values the gate
itself reports.
@argszero
argszero merged commit c2f047d into main Sep 24, 2026
1 check passed
@argszero
argszero deleted the feat/tx-trend-four-modes branch September 24, 2026 14:17
argszero added a commit that referenced this pull request Sep 27, 2026
…7.28 token it misquoted (#305)

The release chain states its version in four places, and until now only
three had an executor: `Cargo.toml` is the runtime truth (`/healthz`
reports `CARGO_PKG_VERSION`), the copies in `Dockerfile` /
`docker-compose.yml` are guarded by the second rule in
`src/deploy_gate.rs`, and the release tag is guarded by
`docker-publish.yml`. The fourth — the latest `## v<semver>` heading in
`CHANGELOG.md` — had none. It happened to be correct on all 23 tags
(v0.7.6…v0.7.28), which is exactly the shape of a claim with consumers
and no executor (the same shape R76 found for the MSRV and R80 for the
tag). After the deploy-artifact rule landed a release PR touches five
files: forgetting `Dockerfile`/`docker-compose.yml` is caught, a
mismatched tag is caught, and forgetting `CHANGELOG.md` was not.

Add a third rule to `src/deploy_gate.rs`: the latest `## v<semver>`
heading in `CHANGELOG.md` must equal the version `Cargo.toml` declares.
The expectation is derived (`declared_version()`, no snapshot),
`include_str!` keeps it dependency-free, and it is a separate file
constant rather than an entry in `FILES` — so the deploy-artifact rule
and its "prose is out of scope" statement are untouched. The heading is
guarded because it sits at a machine-readable structural position in a
file whose job is to state versions; the body stays out of scope.

While instrumenting this, the one place the entry already got wrong
surfaced: the v0.7.28 entry lists the front-end asset tokens and pairs
`data.js` with `style.css` at `?v=20260914-11`, but the release tree
(63f5975) has `style.css?v=20260924-1`. The two shared that value before
#292 (c2f047d) pushed `style.css` alone to `20260924-1`, so the entry
was written from memory rather than from the tree. Correct it in place
with the repo's existing convention for a false CHANGELOG claim
(strikethrough plus a note, as v0.7.10 / #267 did).

Tests: two companions — a positive control proving the scanner really
sees the heading it guards, and a teeth check on synthetic input (a
stale heading and a heading-less file both turn it red). Verified by
mutation: a stale heading makes exactly one test fail and names the
line. `cargo test` 399 → 402, `cargo fmt --check` and
`clippy --all-targets -- -D warnings` clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant