Skip to content

Commit 7af850c

Browse files
authored
Merge pull request #1508 from netalertx/chore_timestamps
timestamp cleanup
2 parents e0d4e9e + 9ac8f6f commit 7af850c

44 files changed

Lines changed: 776 additions & 188 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/skills/code-standards/SKILL.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,18 @@ Nested subprocess calls need their own timeout—outer timeout won't save you.
4242
## Time Utilities
4343

4444
```python
45-
from utils.datetime_utils import timeNowDB
45+
from utils.datetime_utils import timeNowUTC
4646

47-
timestamp = timeNowDB()
47+
timestamp = timeNowUTC()
4848
```
4949

50+
This is the ONLY function that calls datetime.datetime.now() in the entire codebase.
51+
52+
⚠️ CRITICAL: ALL database timestamps MUST be stored in UTC
53+
This is the SINGLE SOURCE OF TRUTH for current time in NetAlertX
54+
Use timeNowUTC() for DB writes (returns UTC string by default)
55+
Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons, logging)
56+
5057
## String Sanitization
5158

5259
Use sanitizers from `server/helper.py` before storing user input.

.github/workflows/run-all-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212
type: boolean
1313
default: false
1414
run_backend:
15-
description: '📂 backend/ (SQL Builder & Security)'
15+
description: '📂 backend/ & db/ (SQL Builder, Security & Migration)'
1616
type: boolean
1717
default: false
1818
run_docker_env:
@@ -43,9 +43,9 @@ jobs:
4343
run: |
4444
PATHS=""
4545
# Folder Mapping with 'test/' prefix
46-
if [ "${{ github.event.inputs.scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi
46+
if [ "${{ github.event.inputs.run_scan }}" == "true" ]; then PATHS="$PATHS test/scan/"; fi
4747
if [ "${{ github.event.inputs.run_api }}" == "true" ]; then PATHS="$PATHS test/api_endpoints/ test/server/"; fi
48-
if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/"; fi
48+
if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/ test/db/"; fi
4949
if [ "${{ github.event.inputs.run_docker_env }}" == "true" ]; then PATHS="$PATHS test/docker_tests/"; fi
5050
if [ "${{ github.event.inputs.run_ui }}" == "true" ]; then PATHS="$PATHS test/ui/"; fi
5151

front/js/common.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -447,21 +447,35 @@ function localizeTimestamp(input) {
447447
return formatSafe(input, tz);
448448

449449
function formatSafe(str, tz) {
450-
const date = new Date(str);
450+
// CHECK: Does the input string have timezone information?
451+
// - Ends with Z: "2026-02-11T11:37:02Z"
452+
// - Has GMT±offset: "Wed Feb 11 2026 12:34:12 GMT+1100 (...)"
453+
// - Has offset at end: "2026-02-11 11:37:02+11:00"
454+
// - Has timezone name in parentheses: "(Australian Eastern Daylight Time)"
455+
const hasOffset = /Z$/i.test(str.trim()) ||
456+
/GMT[+-]\d{2,4}/.test(str) ||
457+
/[+-]\d{2}:?\d{2}$/.test(str.trim()) ||
458+
/\([^)]+\)$/.test(str.trim());
459+
460+
// ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers.
461+
// If no offset is present, we must explicitly mark it as UTC by appending 'Z'
462+
// so JavaScript doesn't interpret it as local browser time.
463+
let isoStr = str.trim();
464+
if (!hasOffset) {
465+
// Ensure proper ISO format before appending Z
466+
// Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
467+
isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z';
468+
}
469+
470+
const date = new Date(isoStr);
451471
if (!isFinite(date)) {
452472
console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`);
453473
return 'Failed conversion';
454474
}
455475

456-
// CHECK: Does the input string have an offset (e.g., +11:00 or Z)?
457-
// If it does, and we apply a 'tz' again, we double-shift.
458-
const hasOffset = /[Z|[+-]\d{2}:?\d{2}]$/.test(str.trim());
459-
460476
return new Intl.DateTimeFormat(LOCALE, {
461-
// If it has an offset, we display it as-is (UTC mode in Intl
462-
// effectively means "don't add more hours").
463-
// If no offset, apply your variable 'tz'.
464-
timeZone: hasOffset ? 'UTC' : tz,
477+
// Convert from UTC to user's configured timezone
478+
timeZone: tz,
465479
year: 'numeric', month: '2-digit', day: '2-digit',
466480
hour: '2-digit', minute: '2-digit', second: '2-digit',
467481
hour12: false

front/plugins/_publisher_apprise/apprise.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import conf # noqa: E402 [flake8 lint suppression]
1313
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
14-
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
14+
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
1515
from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
1616
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
1717
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
@@ -60,7 +60,7 @@ def main():
6060
# Log result
6161
plugin_objects.add_object(
6262
primaryId = pluginName,
63-
secondaryId = timeNowDB(),
63+
secondaryId = timeNowUTC(),
6464
watched1 = notification["GUID"],
6565
watched2 = result,
6666
watched3 = 'null',

front/plugins/_publisher_email/email_smtp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import conf # noqa: E402 [flake8 lint suppression]
2020
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
2121
from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
22-
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
22+
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
2323
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
2424
from helper import get_setting_value, hide_email # noqa: E402 [flake8 lint suppression]
2525
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
@@ -80,7 +80,7 @@ def main():
8080
# Log result
8181
plugin_objects.add_object(
8282
primaryId = pluginName,
83-
secondaryId = timeNowDB(),
83+
secondaryId = timeNowUTC(),
8484
watched1 = notification["GUID"],
8585
watched2 = result,
8686
watched3 = 'null',

front/plugins/_publisher_mqtt/mqtt.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from helper import get_setting_value, bytes_to_string, \
2727
sanitize_string, normalize_string # noqa: E402 [flake8 lint suppression]
2828
from database import DB, get_device_stats # noqa: E402 [flake8 lint suppression]
29-
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
29+
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
3030
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
3131

3232
# Make sure the TIMEZONE for logging is correct
@@ -583,7 +583,7 @@ def publish_notifications(db, mqtt_client):
583583

584584
# Optional: attach meta info
585585
payload["_meta"] = {
586-
"published_at": timeNowDB(),
586+
"published_at": timeNowUTC(),
587587
"source": "NetAlertX",
588588
"notification_GUID": notification["GUID"]
589589
}
@@ -631,7 +631,7 @@ def prepTimeStamp(datetime_str):
631631
except ValueError:
632632
mylog('verbose', [f"[{pluginName}] Timestamp conversion failed of string '{datetime_str}'"])
633633
# Use the current time if the input format is invalid
634-
parsed_datetime = datetime.now(conf.tz)
634+
parsed_datetime = timeNowUTC(as_string=False)
635635

636636
# Convert to the required format with 'T' between date and time and ensure the timezone is included
637637
return parsed_datetime.isoformat() # This will include the timezone offset

front/plugins/_publisher_ntfy/ntfy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import conf # noqa: E402 [flake8 lint suppression]
1414
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
1515
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
16-
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
16+
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
1717
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
1818
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
1919
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
@@ -63,7 +63,7 @@ def main():
6363
# Log result
6464
plugin_objects.add_object(
6565
primaryId = pluginName,
66-
secondaryId = timeNowDB(),
66+
secondaryId = timeNowUTC(),
6767
watched1 = notification["GUID"],
6868
watched2 = handleEmpty(response_text),
6969
watched3 = response_status_code,

front/plugins/_publisher_pushover/pushover.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
1616
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
1717
from helper import get_setting_value, hide_string # noqa: E402 [flake8 lint suppression]
18-
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
18+
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
1919
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
2020
from database import DB # noqa: E402 [flake8 lint suppression]
2121

@@ -60,7 +60,7 @@ def main():
6060
# Log result
6161
plugin_objects.add_object(
6262
primaryId=pluginName,
63-
secondaryId=timeNowDB(),
63+
secondaryId=timeNowUTC(),
6464
watched1=notification["GUID"],
6565
watched2=handleEmpty(response_text),
6666
watched3=response_status_code,

front/plugins/_publisher_pushsafer/pushsafer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
1414
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
1515
from helper import get_setting_value, hide_string # noqa: E402 [flake8 lint suppression]
16-
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
16+
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
1717
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
1818
from database import DB # noqa: E402 [flake8 lint suppression]
1919
from pytz import timezone # noqa: E402 [flake8 lint suppression]
@@ -61,7 +61,7 @@ def main():
6161
# Log result
6262
plugin_objects.add_object(
6363
primaryId = pluginName,
64-
secondaryId = timeNowDB(),
64+
secondaryId = timeNowUTC(),
6565
watched1 = notification["GUID"],
6666
watched2 = handleEmpty(response_text),
6767
watched3 = response_status_code,

front/plugins/_publisher_telegram/tg.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import conf # noqa: E402 [flake8 lint suppression]
1212
from const import confFileName, logPath # noqa: E402 [flake8 lint suppression]
1313
from plugin_helper import Plugin_Objects # noqa: E402 [flake8 lint suppression]
14-
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
14+
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
1515
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
1616
from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
1717
from models.notification_instance import NotificationInstance # noqa: E402 [flake8 lint suppression]
@@ -60,7 +60,7 @@ def main():
6060
# Log result
6161
plugin_objects.add_object(
6262
primaryId=pluginName,
63-
secondaryId=timeNowDB(),
63+
secondaryId=timeNowUTC(),
6464
watched1=notification["GUID"],
6565
watched2=result,
6666
watched3='null',

0 commit comments

Comments
 (0)