Render timestamps as local-with-offset for humans, UTC for JSON#351
Merged
Conversation
Timestamp display was inconsistent across the CLI, TUI, and dash. Some fields rendered raw RFC3339, others stripped the offset to local time without a suffix, and the dash showed local time with no way to tell it apart from UTC. After PR #350 added job.start_time, the inconsistency was about to spread to a new field. Apply a single policy: * humans (TUI, dash, CLI table mode, CLI detail printouts) see local time with an explicit ±HHMM offset suffix * JSON mode keeps the server's raw UTC RFC3339 value The server already stores UTC, so this is purely a client-side rendering change. File mtimes get the same treatment as RFC3339 timestamps. A new pair of helpers in `src/client/utils.rs` (format_local_timestamp and format_local_timestamp_epoch) covers the CLI and TUI surfaces; the dash routes everything through formatDateLocal in app-utils.js; and the Python convert_timestamp helper now returns a tz-aware UTC datetime so callers no longer silently inherit the client's local zone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Unifies timestamp rendering across Torc’s human-facing clients (CLI/TUI/dash/Python) so humans consistently see local time with an explicit ±HHMM offset, while JSON output preserves UTC RFC3339 values.
Changes:
- Added shared Rust timestamp helpers (
format_local_timestamp*) and updated CLI/TUI call sites to use local-with-offset formatting for human output. - Updated dash timestamp formatting to append the local offset and updated job details to format
job.start_timeconsistently. - Updated Python
convert_timestampto return a tz-aware UTCdatetimeto prevent naive/local-time ambiguity.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| torc-dash/static/js/app-utils.js | Adds ±HHMM suffix for local timestamp rendering in the dash. |
| torc-dash/static/js/app-job-details.js | Routes job.start_time through the dash timestamp formatter. |
| src/tui/ui.rs | Updates TUI timestamp formatting (events + file mtimes) to include ±HHMM and adjusts column widths. |
| src/tui/components.rs | Formats job.start_time in the job details popup via the shared helper. |
| src/client/utils.rs | Introduces shared human timestamp format constant + RFC3339/epoch formatting helpers and tests. |
| src/client/commands/workflows.rs | Formats workflow timestamps for human list/detail output via helper (JSON unchanged). |
| src/client/commands/results.rs | Formats result completion time for human list/detail output via helper. |
| src/client/commands/jobs.rs | Formats job start time for human detail output via helper. |
| src/client/commands/events.rs | Human event timestamps now include offset; JSON “formatted” timestamp becomes UTC RFC3339 w/ millis. |
| src/client/commands/compute_nodes.rs | Formats compute node start time for human list/detail output via helper. |
| python_client/src/torc/common.py | Makes convert_timestamp return tz-aware UTC datetimes and updates docstring accordingly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Three robustness fixes flagged by Copilot review on PR #351: * `format_local_timestamp_epoch` split epoch_secs into (i64 secs, u32 nsecs), which could produce `nsecs == 1_000_000_000` from float rounding (chrono rejects this, returning None) or underflow the u32 cast for pre-epoch fractional values. Both cases silently fell back to the raw float string. Route through total nanos via `from_timestamp_nanos` instead — that helper saturates cleanly on NaN/inf and handles negative epochs. * TUI `format_timestamp` had the same bug, with worse fallout: `unwrap_or_default()` blanks the file's Modified cell when chrono returns None. Removed the duplicate helper and pointed the only caller at the shared one. * Dash `formatTimestamp` / `formatUnixTimestamp` relied on a try/catch to handle bad input, but `new Date("garbage")` returns an Invalid Date object instead of throwing — `formatDateLocal` then rendered it as `NaN-NaN-NaN ... +NaNNaN`. Guard with `isNaN(date.getTime())` and fall back to the escaped original (formatTimestamp) or "-" (formatUnixTimestamp). Added two unit tests covering the subsecond-rounding and pre-epoch edge cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
±HHMMoffset suffix (e.g.2026-05-25 14:30:00 -0700)job.start_timefield from PR Add start_time and compute_node_id to the job table #350 — it lands consistently from day one rather than inheriting the prior raw-passthrough pattern.Why
Timestamp display had drifted into three different conventions: raw RFC3339 in some CLI sites, local-without-offset in others, and silent local in the dash. A user SSH'd into a remote host couldn't tell whether a dash time was local or server-side. Now every human surface shows a self-describing string.
What changed
src/client/utils.rs— addedformat_local_timestamp(&str)andformat_local_timestamp_epoch(f64)helpers, plus a sharedHUMAN_TIMESTAMP_FORMATconstant and three unit tests.workflows.rs,results.rs,compute_nodes.rs,jobs.rs) — route detail-view printlns andtabledtable rows through the helper. JSON paths untouched (already UTC RFC3339).events.rs—format_timestamp_ms(human) now includes the offset; newformat_timestamp_ms_utcformats the JSON conveniencetimestamp_formattedfield as UTC RFC3339 with millisecond precision.ui.rs,components.rs) —format_timestamp(file mtimes, was UTCZ) andformat_timestamp_ms(events, was local-no-offset) both now emit local-with-offset; job-detail popup formatsstart_timethrough the new helper.app-utils.js,app-job-details.js) —formatDateLocalappends±HHMM;app-job-details.jsroutesjob.start_timethroughformatTimestampinstead of rawescapeHtml.common.py) —convert_timestampnow returns a tz-aware UTCdatetime(datetime.fromtimestamp(ts/1000, tz=timezone.utc)) so callers can.astimezone()for local rather than silently inheriting the client zone. Docstring updated.Notes
tabledstruct as table mode, so CSV is also local-with-offset. If a downstream tool wants machine-readable UTC, that would need a separate row struct — happy to follow up if needed.format_timestamp_msin events keeps millisecond precision (events arrive frequently enough that subsecond ordering matters); the file-mtime / RFC3339 helpers use second precision.Test plan
cargo build --workspace— cleancargo fmt -- --check— cleancargo clippy --all --all-targets --all-features -- -D warnings— cleandprint check— cleancargo nextest run --lib— 372/372 pass (3 new tests for the helpers)torc results list <wf>andtorc -f json results list <wf>— confirm table shows local, JSON shows UTC RFC3339from torc.common import convert_timestamp; print(convert_timestamp(...).tzinfo)returns UTC🤖 Generated with Claude Code