feat(ui): let the transactions trend card switch between four metrics - #292
Merged
Merged
Conversation
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.
This was referenced Sep 24, 2026
Merged
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The transactions page trend card
#tx-trendunconditionally drew two bars perbucket (spend + income) and offered no control at all. The design prototype
(
docs/prototype/aitokenpool-console.html) draws the same two bars, so this is anew feature request, not a control that was lost when the card was built.
The card gets a
.tabssegment —#tx-trend-modes, the shape already used by#tx-tabs— with four modes:No backend change, and no extra request
/api/transactions/trend(src/routes/wallet.rs) already applies the range, thetype tab and the column filters, and already returns
net,incomeandexpenseper bucket. All four modes are therefore derived from the payload thecard already has.
Consequently the mode is not part of the request: it does not enter
txQuerySig()and the mode buttons do not callreloadTransactions().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
netfrom 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 notmean anything).
So that mode swaps the card title and subtitle for
tx.trend.card.netandtx.trend.net.sub, which state the definition outright, and draws a line(
sparkline()) rather than bars — bars would relativise the level and make thebalance 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
maxis taken from the series actually displayed (solooking at income alone is not flattened by a spend peak);
titletooltip and the legend name only the displayed series — ahidden series does not linger in the legend or the tooltips;
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 inrenderView()orrenderTxTrend(): those two are also reached from theatp:langchangehandler, 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 existingmetric keys (
tx.trend.metric.expense,tx.trend.metric.income) are reused onthe buttons. No existing value was changed. Cache-bust query bumped to
20260924-1oncss/style.css,js/i18n.jsandjs/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).r109_logic_probe.js) —legs=9 failed=0; the 6 syntheticmutations each fail exactly the leg they target.
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….r109_e2e_probe.js, realindex.html+ the four realscripts + a real login + real
click()): pre-fix tree 7 of 7 legs red;fixed tree
legs=7 misdeclared=[]. It also checks the two edit tablesagree (
L0) and, separately, that clicking a mode does not reach the list atall — no request issued and the pager stays on page 2 (
L4) — which arequest count alone would miss, since the wrong wiring
(
reloadTransactions()) sends no request but still throws the user back topage 1.
cargo test: 373 passed / 0 failed (unchanged from the parent — this PRadds no Rust gate).
cargo fmt --checkclean.cargo clippy --all-targets -- -D warnings: rc=101 from a pre-existingcollapsible_matchatsrc/protocol.rs:662, which a pristine parent-committree reports identically. Not part of this diff; left untouched.