From 3cf7e2e2573e75d7c09072111bf67268fe1c2c80 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Sun, 27 Sep 2026 20:24:48 +0000 Subject: [PATCH] Show match finish times in the browser timezone The history table truncated finished_at, which the API serializes in UTC, so the Finished column always showed UTC wall time. Parse the timestamp with the browser's Date and render its local components, keeping the existing YYYY-MM-DD HH:MM layout. --- web/src/model.rs | 13 +++++++++++++ web/src/pages/history.rs | 24 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/web/src/model.rs b/web/src/model.rs index 3af51aa..38a75f3 100644 --- a/web/src/model.rs +++ b/web/src/model.rs @@ -291,6 +291,13 @@ pub fn fmt_points(points: f64) -> String { } } +/// Render a calendar instant as `YYYY-MM-DD HH:MM`. `month` is 0-based +/// (the JavaScript `Date` convention); the other components are as +/// returned by `Date`'s local-time getters. +pub fn fmt_datetime_parts(year: u32, month: u32, day: u32, hour: u32, minute: u32) -> String { + format!("{year:04}-{:02}-{day:02} {hour:02}:{minute:02}", month + 1) +} + /// The caller's Elo rating for one game type (GET /api/me/ratings). #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] @@ -658,6 +665,12 @@ mod tests { assert_eq!("2.5", fmt_points(2.5)); } + #[test] + fn fmt_datetime_parts_pads_and_offsets_month() { + assert_eq!("2026-09-19 05:02", fmt_datetime_parts(2026, 8, 19, 5, 2)); + assert_eq!("2026-01-01 00:00", fmt_datetime_parts(2026, 0, 1, 0, 0)); + } + #[test] fn leaderboard_points_accept_floats() { let e: LeaderboardEntry = serde_json::from_value(json!({ diff --git a/web/src/pages/history.rs b/web/src/pages/history.rs index c70a150..7522929 100644 --- a/web/src/pages/history.rs +++ b/web/src/pages/history.rs @@ -68,6 +68,23 @@ fn game_name(game_types: &[GameTypeInfo], game_type: &str) -> String { .unwrap_or_else(|| game_type.to_string()) } +/// The match's finish instant in the browser's local timezone, as +/// `YYYY-MM-DD HH:MM`. Falls back to the raw UTC prefix when the browser +/// cannot parse the timestamp. +fn local_finished(iso: &str) -> String { + let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_str(iso)); + if date.get_time().is_nan() { + return iso.replace('T', " ").chars().take(16).collect(); + } + crate::model::fmt_datetime_parts( + date.get_full_year(), + date.get_month(), + date.get_date(), + date.get_hours(), + date.get_minutes(), + ) +} + #[component] pub fn HistoryPage() -> View { let page = create_signal(Option::::None); @@ -131,12 +148,7 @@ pub fn HistoryPage() -> View { .get_clone() .into_iter() .map(|m| { - let finished = m - .finished_at - .replace('T', " ") - .chars() - .take(16) - .collect::(); + let finished = local_finished(&m.finished_at); let game = game_name(&names, &m.game_type); let mut cells: Vec = team_columns(&m) .iter()