From 2954b929a6e5285ec5d767ab9e7f8ed5988dec79 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Sun, 1 Feb 2026 15:48:28 +1100 Subject: [PATCH] FE+BE: timexone fixes 1 #1440 Signed-off-by: jokob-sk --- front/js/common.js | 12 ++++++-- server/models/device_instance.py | 6 ++-- server/utils/datetime_utils.py | 53 +++++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/front/js/common.js b/front/js/common.js index da0f2cb8..ceeb82a5 100755 --- a/front/js/common.js +++ b/front/js/common.js @@ -450,10 +450,18 @@ function localizeTimestamp(input) { const date = new Date(str); if (!isFinite(date)) { console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`); - return 'Failed conversion - Check browser console'; + return 'Failed conversion'; } + + // CHECK: Does the input string have an offset (e.g., +11:00 or Z)? + // If it does, and we apply a 'tz' again, we double-shift. + const hasOffset = /[Z|[+-]\d{2}:?\d{2}]$/.test(str.trim()); + return new Intl.DateTimeFormat(LOCALE, { - timeZone: tz, + // If it has an offset, we display it as-is (UTC mode in Intl + // effectively means "don't add more hours"). + // If no offset, apply your variable 'tz'. + timeZone: hasOffset ? 'UTC' : tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false diff --git a/server/models/device_instance.py b/server/models/device_instance.py index 6037ab7f..4aa1544b 100755 --- a/server/models/device_instance.py +++ b/server/models/device_instance.py @@ -18,7 +18,7 @@ from db.authoritative_handler import ( unlock_fields ) from helper import is_random_mac, get_setting_value -from utils.datetime_utils import timeNowDB, format_date +from utils.datetime_utils import timeNowDB class DeviceInstance: @@ -489,8 +489,8 @@ class DeviceInstance: return None device_data = row_to_json(list(row.keys()), row) - device_data["devFirstConnection"] = format_date(device_data["devFirstConnection"]) - device_data["devLastConnection"] = format_date(device_data["devLastConnection"]) + device_data["devFirstConnection"] = device_data["devFirstConnection"] + device_data["devLastConnection"] = device_data["devLastConnection"] device_data["devIsRandomMAC"] = is_random_mac(device_data["devMac"]) # Fetch children diff --git a/server/utils/datetime_utils.py b/server/utils/datetime_utils.py index 1d513206..3465be49 100644 --- a/server/utils/datetime_utils.py +++ b/server/utils/datetime_utils.py @@ -112,12 +112,29 @@ def normalizeTimeStamp(inputTimeStamp): # ------------------------------------------------------------------------------------------- -def format_date_iso(date1: str) -> Optional[str]: - """Return ISO 8601 string for a date or None if empty""" - if not date1: +def format_date_iso(date_val: str) -> Optional[str]: + """Ensures a date string from DB is returned as a proper ISO string with TZ.""" + if not date_val: return None - dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1 - return dt.isoformat() + + try: + # 1. Parse the string from DB (e.g., "2026-01-20 07:58:18") + if isinstance(date_val, str): + # Use a more flexible parser if it's not strict ISO + dt = datetime.datetime.fromisoformat(date_val.replace(" ", "T")) + else: + dt = date_val + + # 2. If it has no timezone, ATTACH (don't convert) your config TZ + if dt.tzinfo is None: + target_tz = conf.tz if isinstance(conf.tz, datetime.tzinfo) else ZoneInfo(conf.tz) + dt = dt.replace(tzinfo=target_tz) + + # 3. Return the string. .isoformat() will now include the +11:00 or +10:00 + return dt.isoformat() + except Exception as e: + print(f"Error formatting date: {e}") + return str(date_val) # ------------------------------------------------------------------------------------------- @@ -156,21 +173,35 @@ def parse_datetime(dt_str): def format_date(date_str: str) -> str: try: - if isinstance(date_str, str): - # collapse all whitespace into single spaces - date_str = re.sub(r"\s+", " ", date_str.strip()) + if not date_str: + return "" + date_str = re.sub(r"\s+", " ", str(date_str).strip()) dt = parse_datetime(date_str) + + if dt.tzinfo is None: + if isinstance(conf.tz, str): + dt = dt.replace(tzinfo=ZoneInfo(conf.tz)) + else: + dt = dt.replace(tzinfo=conf.tz) + if not dt: return f"invalid:{repr(date_str)}" + # If the DB has no timezone, we tell Python what it IS, + # we don't CONVERT it. if dt.tzinfo is None: + # Option A: If the DB time is already AEDT, use AEDT. + # Option B: Use conf.tz if that is your 'source of truth' dt = dt.replace(tzinfo=conf.tz) - return dt.astimezone().isoformat() + # IMPORTANT: Return the ISO format of the object AS IS. + # Calling .astimezone() here triggers a conversion to the + # System Local Time , which is causing your shift. + return dt.isoformat() - except Exception: - return f"invalid:{repr(date_str)}" + except Exception as e: + return f"invalid:{repr(date_str)} e: {e}" def format_date_diff(date1, date2, tz_name):