fix(ui): export the transaction time column in the same 口径 as the cell - #229
Merged
Merged
Conversation
`exportTxCsv` wrote the view row's raw `t.time` — the UTC string the API returns, which `txsToView` does not convert — while the table cell renders that same field through `fmtPrecise()` (local time, #139 / rant 2026-08-24T12:38:44). In a UTC+8 session one and the same row therefore reads `23:04` in the table and `15:04` in the exported file (west of Greenwich the sign flips). This is the class of divergence C2054/C2055 already closed for the 点数 column: "the export covers the currently visible rows, so the column must use the same 口径 as the cell — otherwise the screen says -3.7 and the file says 3.7". Fix: print the time column with the very helper that renders the cell (`fmtPrecise(t.time)`) instead of keeping a second copy of "time to string". Verified with a jsdom probe (real ui/index.html + the four scripts, only `fetch` stubbed) that boots three transactions, reads the rendered cell and the bytes of the exported Blob: pre-change red on the time-column assertion only; fixed tree 6/6, in Asia/Shanghai and America/New_York. The probe also asserts the process TZ is not UTC, since under TZ=UTC the two 口径 coincide and the defect is invisible.
12 tasks
argszero
added a commit
that referenced
this pull request
Sep 14, 2026
…e them (#246) `src/dao.rs::utc_iso()` serialises every timestamp as `YYYY-MM-DDTHH:MM:SSZ`, and `ui/js/app.js` has a family of helpers for turning that into something a user can read (`fmtPrecise`, `timeCell`, `timeAgo`, `localMD`). Three consumers sliced the raw string themselves instead, and each slice produced its own wrong face on screen: admin raise-requests, handled rows: (r.created_at || "").slice(5, 16) -> "09-13T16:30": the ISO `T` separator leaks into the UI, and the hour is UTC. settings -> API keys, "created": String(k.created_at || "").slice(0, 10) -> the UTC day, so a UTC+8 user before 08:00 local is shown yesterday. transactions view row: time: (t.time || "").replace("T"," ").slice(0,16) -> the seconds are destroyed *before* the renderer sees the value, while both renderers of that value (the column via `timeCell(t.time, true)` and the CSV export via `fmtPrecise`) print `HH:MM:SS`. The seconds on screen are a fabricated `00` -- always. #229 made the export agree with the cell; once both read the same value, the truncation in the source became the only reading left. None of the three is a trade-off: the helpers already exist for exactly this, and the backend guarantees the format (comments at `app.js:3206` document UTC for *aggregate* buckets only, which is a different thing). The fix hands each value over intact: timeCell(r.created_at, true) -- local, to the second, relative in the title fmtPrecise(k.created_at).slice(0, 10) -- slicing a helper's *output* is fine time: t.time || "" -- the row carries the wire value `src/i18n_pack.rs::wire_timestamps_reach_the_renderer_unsliced` pins the shape in CI: a wire timestamp field (`created_at` / `last_used`) may not be processed inline by `.slice()` / `.replace()` (processing the *helper's* output is allowed, and a plain passthrough like `last: k.last_used || null` is allowed), and the transactions view row's `time` property must be a bare value. The extractor is proven on synthetic pre-fix and post-fix text, and on the pre-fix tree itself: restoring either `_at` slice panics with both offending expressions, restoring the view-row slice panics on the row shape. The control flow has no JS runner in CI, so the runtime half is carried by `ui/README.md` and its smoke-test notes. Verified with a jsdom instrument over the real `ui/index.html` + four real scripts (only `fetch` and the Blob download stubbed, with the backend's real wire format), seven legs driven through the real login form and the real nav: live (this tree) md5=c0f23c7c passed=13/13 reds={} pre-fix tree (301ca4d) passed=7/13 reds={A1 B1 B2 C1 C2 E1} F1 reverted only reds={B1 B2} F2 reverted only reds={C1 C2} F3 reverted only reds={A1 E1} half fix (localMD, loses time) reds={B2} wrong fix (pre-format, re-parsed as UTC) reds={A1 E1} The three per-face red sets are pairwise disjoint and their union is exactly the pre-fix set. The half fix shows the `T` alone is not the whole rule; the wrong fix shows "call a helper somewhere" is not either -- the value has to arrive raw. - ui/js/app.js : the three consumers go through the helpers - src/i18n_pack.rs: CI tripwire for the shape, with detector controls - ui/README.md : record the rule and the smoke-test shape - ui/index.html : cache-bust app.js Co-authored-by: EMRG Evolution <emrg@argszero.dev>
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.
Summary
The transactions CSV export printed the time column in a different 口径 than the table cell it sits next to:
exportTxCsvwrote the view row's rawt.time(the UTC string the API returns, whichtxsToViewdoes not convert), while the cell renders the very same field throughfmtPrecise()— i.e. local time. In a UTC+8 session one and the same row reads23:04in the table and15:04in the downloaded file (west of Greenwich the sign flips). A CSV whose header is localized (时间) and whose values disagree with the screen is a trap for anyone reconciling the file against the UI.This is the same class of divergence the project already closed for the 点数 column in #196 (C2054/C2055), whose own comment states the rule: "the export covers the currently visible rows, so the column must use the same 口径 as the cell — otherwise the screen says -3.7 and the file says 3.7". Time is simply the column that was never brought along.
Provenance: the cell moved to precise local time in #139 (rant 2026-08-24T12:38:44); that commit updated the cell but not the other consumer of the same field (
exportTxCsv, added earlier in #68).Related Issue
N/A — no open issue. Background: #139 (cell 口径 changed to local), #196 (the same divergence fixed for the 点数 column).
Changes
ui/js/app.js— the CSV 时间 column now usesfmtPrecise(t.time), the same helper that renders the cell, instead of a second copy of "timestamp to string" (one line + a comment stating why the two must stay同源).ui/README.md— the CSV convention section now records that every column shares the cell's 口径 (with time as the second instance after 点数).ui/index.html— cache-bust for the changedapp.js(?v=20260914-6).Tests
cargo test— 235 passed (unchanged; the change is JS-only)cargo fmt --check— exit 0cargo clippy --all-targets -- -D warnings— cleannode --checkon all fourui/js/*.jsui/index.html+api.js/data.js/i18n.js/app.js, onlyfetchstubbed): boots three transactions, reads the rendered cell text and the bytes of the exported Blob (captured fromURL.createObjectURL), and asserts that each CSV column equals its cell — time (the axis), plus type / pts / tokens as controls. Pre-change tree: 5/6, red on the time column only. Fixed tree: 6/6, exit 0, under bothTZ=Asia/ShanghaiandTZ=America/New_York.TZ=UTCthe two 口径 coincide and the value divergence is invisible (it exists only in the format then), so a UTC run would be meaningless.{time}· mirror-image repair (make the cell print UTC so the two "agree"){cell-local}— disjoint from the pre-change set, i.e. the instrument rejects consistency-by-breaking-the-cell · half fix (append":00"and stay UTC){time}(same assertion, different input — not mutually exclusive with the pre-change leg, recorded as such) · control-teeth leg (CSV type field printed raw){type-control}· fixed tree{}.Checklist
fix/…)fix(ui): …)