From 98e0949d08ca49a02e6ec0e8ed084cb25dd6d658 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:48:12 -0400 Subject: [PATCH] Fix timezone consistency: apply server timezone everywhere in the app - Extract getServerTimezone() helper in timeUtils.js; apply it in formatChartTime and formatLocalTimeOnly (previously ignored TZ) - Status.js: remove local formatLocalTime (was bare toLocaleString), import the shared util that reads server timezone - SettingsWizard: immediately update localStorage when timezone is saved so new value takes effect without waiting for cache to expire Co-Authored-By: Claude Sonnet 4.6 --- client/src/SettingsWizard.js | 4 ++ client/src/Status.js | 9 +--- client/src/utils/timeUtils.js | 78 +++++++++++++++-------------------- 3 files changed, 38 insertions(+), 53 deletions(-) diff --git a/client/src/SettingsWizard.js b/client/src/SettingsWizard.js index 1bfdbf9..b5f7484 100644 --- a/client/src/SettingsWizard.js +++ b/client/src/SettingsWizard.js @@ -61,6 +61,10 @@ function SettingsWizard() { setSavedMessage(null); try { await axios.post('/api/settings', sectionData); + if (sectionData.timezone) { + localStorage.setItem('serverTimezone', sectionData.timezone); + localStorage.setItem('serverTimezoneTime', Date.now().toString()); + } setSavedMessage({ section: sectionName, success: true }); // Clear message after 3 seconds setTimeout(() => setSavedMessage(null), 3000); diff --git a/client/src/Status.js b/client/src/Status.js index f4c6559..735bbbc 100644 --- a/client/src/Status.js +++ b/client/src/Status.js @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; +import { formatLocalTime } from './utils/timeUtils'; import './Status.css'; function Status() { @@ -31,14 +32,6 @@ function Status() { } }; - const formatLocalTime = (timestamp) => { - try { - return new Date(timestamp).toLocaleString(); - } catch { - return timestamp; - } - }; - const formatDuration = (ms) => { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); diff --git a/client/src/utils/timeUtils.js b/client/src/utils/timeUtils.js index 7c51ef1..5ce5e90 100644 --- a/client/src/utils/timeUtils.js +++ b/client/src/utils/timeUtils.js @@ -9,6 +9,21 @@ export const getCurrentTimezone = () => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }; +const getServerTimezone = () => { + try { + const tz = localStorage.getItem('serverTimezone'); + const ts = localStorage.getItem('serverTimezoneTime'); + if (tz && ts && (Date.now() - parseInt(ts)) < 3600000 && tz !== 'null' && tz !== 'undefined' && tz.trim() !== '') { + return tz.replace(/^["']|["']$/g, '').trim(); + } + if (tz) { + localStorage.removeItem('serverTimezone'); + localStorage.removeItem('serverTimezoneTime'); + } + } catch (_) {} + return null; +}; + /** * Format a timestamp to local time string with timezone info * Uses server timezone from localStorage if available, otherwise browser timezone @@ -23,40 +38,19 @@ export const formatLocalTime = (timestamp, options = {}) => { } const date = new Date(ts); - - // Try to get server timezone from settings API response (cached in localStorage) - let timeZone; - try { - const cachedSettings = localStorage.getItem('serverTimezone'); - const cacheTime = localStorage.getItem('serverTimezoneTime'); - const now = Date.now(); - // Invalidate cache every 1 hour - if (cachedSettings && cacheTime && (now - parseInt(cacheTime)) < 3600000 && cachedSettings !== 'null' && cachedSettings !== 'undefined' && cachedSettings.trim() !== '') { - // Remove any quotes that might be in the string - timeZone = cachedSettings.replace(/^["']|["']$/g, '').trim(); - } else if (cachedSettings) { - // Clear stale cache - localStorage.removeItem('serverTimezone'); - localStorage.removeItem('serverTimezoneTime'); - } - } catch (e) { - // Timezone load error handled silently - } - + const tz = getServerTimezone(); + const defaultOptions = { year: 'numeric', - month: 'short', + month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', - timeZoneName: 'short' + timeZoneName: 'short', + ...(tz ? { timeZone: tz } : {}) }; - - if (timeZone) { - defaultOptions.timeZone = timeZone; - } - + return date.toLocaleString('en-US', { ...defaultOptions, ...options }); }; @@ -65,15 +59,17 @@ export const formatLocalTime = (timestamp, options = {}) => { */ export const formatLocalTimeOnly = (timestamp, options = {}) => { if (!timestamp) return 'Never'; - + const date = new Date(timestamp); + const tz = getServerTimezone(); const defaultOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit', - timeZoneName: 'short' + timeZoneName: 'short', + ...(tz ? { timeZone: tz } : {}) }; - + return date.toLocaleTimeString('en-US', { ...defaultOptions, ...options }); }; @@ -83,26 +79,18 @@ export const formatLocalTimeOnly = (timestamp, options = {}) => { */ export const formatChartTime = (timestamp) => { if (!timestamp) return ''; - + const date = new Date(timestamp); const now = new Date(); const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); - - // If within last 24 hours, show time only + const tz = getServerTimezone(); + const tzOpt = tz ? { timeZone: tz } : {}; + if (diffDays === 0) { - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit' - }); + return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', ...tzOpt }); } - - // Otherwise show date and time - return date.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); + + return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', ...tzOpt }); }; /**