This commit is contained in:
Jokob @NetAlertX
2026-02-11 03:56:37 +00:00
parent 45157b6156
commit 933004e792
5 changed files with 26 additions and 23 deletions

View File

@@ -452,11 +452,11 @@ function localizeTimestamp(input) {
// - Has GMT±offset: "Wed Feb 11 2026 12:34:12 GMT+1100 (...)" // - Has GMT±offset: "Wed Feb 11 2026 12:34:12 GMT+1100 (...)"
// - Has offset at end: "2026-02-11 11:37:02+11:00" // - Has offset at end: "2026-02-11 11:37:02+11:00"
// - Has timezone name in parentheses: "(Australian Eastern Daylight Time)" // - Has timezone name in parentheses: "(Australian Eastern Daylight Time)"
const hasOffset = /Z$/i.test(str.trim()) || const hasOffset = /Z$/i.test(str.trim()) ||
/GMT[+-]\d{2,4}/.test(str) || /GMT[+-]\d{2,4}/.test(str) ||
/[+-]\d{2}:?\d{2}$/.test(str.trim()) || /[+-]\d{2}:?\d{2}$/.test(str.trim()) ||
/\([^)]+\)$/.test(str.trim()); /\([^)]+\)$/.test(str.trim());
// ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers. // ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers.
// If no offset is present, we must explicitly mark it as UTC by appending 'Z' // If no offset is present, we must explicitly mark it as UTC by appending 'Z'
// so JavaScript doesn't interpret it as local browser time. // so JavaScript doesn't interpret it as local browser time.
@@ -464,9 +464,9 @@ function localizeTimestamp(input) {
if (!hasOffset) { if (!hasOffset) {
// Ensure proper ISO format before appending Z // Ensure proper ISO format before appending Z
// Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z" // Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
isoStr = isoStr.replace(' ', 'T') + 'Z'; isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z';
} }
const date = new Date(isoStr); const date = new Date(isoStr);
if (!isFinite(date)) { if (!isFinite(date)) {
console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`); console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`);

View File

@@ -93,7 +93,7 @@ class EventInstance:
eve_EventType, eve_AdditionalInfo, eve_EventType, eve_AdditionalInfo,
eve_PendingAlertEmail, eve_PairEventRowid eve_PendingAlertEmail, eve_PairEventRowid
) VALUES (?,?,?,?,?,?,?) ) VALUES (?,?,?,?,?,?,?)
""", (mac, ip, timeNowUTC(as_string=False), eventType, info, """, (mac, ip, timeNowUTC(), eventType, info,
1 if pendingAlert else 0, pairRow)) 1 if pendingAlert else 0, pairRow))
conn.commit() conn.commit()
conn.close() conn.close()

View File

@@ -27,18 +27,18 @@ DATETIME_REGEX = re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$')
def timeNowUTC(as_string=True): def timeNowUTC(as_string=True):
""" """
Return the current time in UTC. Return the current time in UTC.
This is the ONLY function that calls datetime.datetime.now() in the entire codebase. This is the ONLY function that calls datetime.datetime.now() in the entire codebase.
All timestamps stored in the database MUST use UTC format. All timestamps stored in the database MUST use UTC format.
Args: Args:
as_string (bool): If True, returns formatted string for DB storage. as_string (bool): If True, returns formatted string for DB storage.
If False, returns datetime object for operations. If False, returns datetime object for operations.
Returns: Returns:
str: UTC timestamp as 'YYYY-MM-DD HH:MM:SS' when as_string=True str: UTC timestamp as 'YYYY-MM-DD HH:MM:SS' when as_string=True
datetime.datetime: UTC datetime object when as_string=False datetime.datetime: UTC datetime object when as_string=False
Examples: Examples:
timeNowUTC() → '2025-11-04 07:09:11' (for DB writes) timeNowUTC() → '2025-11-04 07:09:11' (for DB writes)
timeNowUTC(as_string=False) → datetime.datetime(2025, 11, 4, 7, 9, 11, tzinfo=UTC) timeNowUTC(as_string=False) → datetime.datetime(2025, 11, 4, 7, 9, 11, tzinfo=UTC)
@@ -48,9 +48,12 @@ def timeNowUTC(as_string=True):
def get_timezone_offset(): def get_timezone_offset():
now = timeNowUTC(as_string=False).replace(tzinfo=conf.tz) if conf.tz else timeNowUTC(as_string=False) if conf.tz:
offset_hours = now.utcoffset().total_seconds() / 3600 if now.utcoffset() else 0 now = timeNowUTC(as_string=False).astimezone(conf.tz)
offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60)) offset_hours = now.utcoffset().total_seconds() / 3600
else:
offset_hours = 0
offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60))
return offset_formatted return offset_formatted
@@ -107,7 +110,7 @@ def normalizeTimeStamp(inputTimeStamp):
# ------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------
def format_date_iso(date_val: str) -> Optional[str]: def format_date_iso(date_val: str) -> Optional[str]:
"""Ensures a date string from DB is returned as a proper ISO string with TZ. """Ensures a date string from DB is returned as a proper ISO string with TZ.
Assumes DB timestamps are stored in UTC and converts them to user's configured timezone. Assumes DB timestamps are stored in UTC and converts them to user's configured timezone.
""" """
if not date_val: if not date_val:
@@ -173,7 +176,7 @@ def parse_datetime(dt_str):
def format_date(date_str: str) -> str: def format_date(date_str: str) -> str:
"""Format a date string from DB for display. """Format a date string from DB for display.
Assumes DB timestamps are stored in UTC and converts them to user's configured timezone. Assumes DB timestamps are stored in UTC and converts them to user's configured timezone.
""" """
try: try:

View File

@@ -188,7 +188,7 @@ def test_get_sessions_calendar(client, api_token, test_mac):
Cleans up test sessions after test. Cleans up test sessions after test.
""" """
# --- Setup: create two sessions for the test MAC --- # --- Setup: create two sessions for the test MAC ---
now = datetime.now() now = timeNowUTC(as_string=False)
start1 = (now - timedelta(days=2)).isoformat(timespec="seconds") start1 = (now - timedelta(days=2)).isoformat(timespec="seconds")
end1 = (now - timedelta(days=1, hours=20)).isoformat(timespec="seconds") end1 = (now - timedelta(days=1, hours=20)).isoformat(timespec="seconds")

View File

@@ -44,7 +44,7 @@ class TestTimeNowUTC:
def test_timeNowUTC_datetime_has_UTC_timezone(self): def test_timeNowUTC_datetime_has_UTC_timezone(self):
"""Test that datetime object has UTC timezone""" """Test that datetime object has UTC timezone"""
result = timeNowUTC(as_string=False) result = timeNowUTC(as_string=False)
assert result.tzinfo is datetime.UTC or result.tzinfo is not None assert result.tzinfo is datetime.UTC
def test_timeNowUTC_datetime_no_microseconds(self): def test_timeNowUTC_datetime_no_microseconds(self):
"""Test that datetime object has microseconds set to 0""" """Test that datetime object has microseconds set to 0"""
@@ -55,7 +55,7 @@ class TestTimeNowUTC:
"""Test that string and datetime modes return consistent values""" """Test that string and datetime modes return consistent values"""
dt_obj = timeNowUTC(as_string=False) dt_obj = timeNowUTC(as_string=False)
str_result = timeNowUTC(as_string=True) str_result = timeNowUTC(as_string=True)
# Convert datetime to string and compare (within 1 second tolerance) # Convert datetime to string and compare (within 1 second tolerance)
dt_str = dt_obj.strftime(DATETIME_PATTERN) dt_str = dt_obj.strftime(DATETIME_PATTERN)
# Parse both to compare timestamps # Parse both to compare timestamps
@@ -68,7 +68,7 @@ class TestTimeNowUTC:
"""Test that timeNowUTC() returns actual UTC time, not local time""" """Test that timeNowUTC() returns actual UTC time, not local time"""
utc_now = datetime.datetime.now(datetime.UTC).replace(microsecond=0) utc_now = datetime.datetime.now(datetime.UTC).replace(microsecond=0)
result = timeNowUTC(as_string=False) result = timeNowUTC(as_string=False)
# Should be within 1 second # Should be within 1 second
diff = abs((utc_now - result).total_seconds()) diff = abs((utc_now - result).total_seconds())
assert diff <= 1 assert diff <= 1
@@ -77,10 +77,10 @@ class TestTimeNowUTC:
"""Test that string result matches datetime object conversion""" """Test that string result matches datetime object conversion"""
dt_obj = timeNowUTC(as_string=False) dt_obj = timeNowUTC(as_string=False)
str_result = timeNowUTC(as_string=True) str_result = timeNowUTC(as_string=True)
# Convert datetime to string using same format # Convert datetime to string using same format
expected = dt_obj.strftime(DATETIME_PATTERN) expected = dt_obj.strftime(DATETIME_PATTERN)
# Should be same or within 1 second # Should be same or within 1 second
t1 = datetime.datetime.strptime(expected, DATETIME_PATTERN) t1 = datetime.datetime.strptime(expected, DATETIME_PATTERN)
t2 = datetime.datetime.strptime(str_result, DATETIME_PATTERN) t2 = datetime.datetime.strptime(str_result, DATETIME_PATTERN)
@@ -95,12 +95,12 @@ class TestTimeNowUTC:
def test_timeNowUTC_multiple_calls_increase(self): def test_timeNowUTC_multiple_calls_increase(self):
"""Test that subsequent calls return increasing timestamps""" """Test that subsequent calls return increasing timestamps"""
import time import time
t1_str = timeNowUTC() t1_str = timeNowUTC()
time.sleep(0.1) time.sleep(0.1)
t2_str = timeNowUTC() t2_str = timeNowUTC()
t1 = datetime.datetime.strptime(t1_str, DATETIME_PATTERN) t1 = datetime.datetime.strptime(t1_str, DATETIME_PATTERN)
t2 = datetime.datetime.strptime(t2_str, DATETIME_PATTERN) t2 = datetime.datetime.strptime(t2_str, DATETIME_PATTERN)
assert t2 >= t1 assert t2 >= t1