timestamp cleanup

This commit is contained in:
Jokob @NetAlertX
2026-02-11 01:55:02 +00:00
parent e0d4e9ea9c
commit 45157b6156
44 changed files with 775 additions and 190 deletions

View File

@@ -25,7 +25,7 @@ import conf
from const import fullConfPath, sql_new_devices
from logger import mylog
from helper import filePermissions
from utils.datetime_utils import timeNowTZ
from utils.datetime_utils import timeNowUTC
from app_state import updateState
from api import update_api
from scan.session_events import process_scan
@@ -104,7 +104,7 @@ def main():
pm, all_plugins, imported = importConfigs(pm, db, all_plugins)
# update time started
conf.loop_start_time = timeNowTZ()
conf.loop_start_time = timeNowUTC(as_string=False)
loop_start_time = conf.loop_start_time # TODO fix

View File

@@ -23,7 +23,7 @@ from const import (
)
from logger import mylog
from helper import write_file, get_setting_value
from utils.datetime_utils import timeNowTZ
from utils.datetime_utils import timeNowUTC
from app_state import updateState
from models.user_events_queue_instance import UserEventsQueueInstance
@@ -105,7 +105,7 @@ def update_api(
class api_endpoint_class:
def __init__(self, db, forceUpdate, query, path, is_ad_hoc_user_event=False):
current_time = timeNowTZ()
current_time = timeNowUTC(as_string=False)
self.db = db
self.query = query
@@ -163,7 +163,7 @@ class api_endpoint_class:
# ----------------------------------------
def try_write(self, forceUpdate):
current_time = timeNowTZ()
current_time = timeNowUTC(as_string=False)
# Debugging info to understand the issue
# mylog('debug', [f'[API] api_endpoint_class: {self.fileName} is_ad_hoc_user_event
@@ -183,7 +183,7 @@ class api_endpoint_class:
write_file(self.path, json.dumps(self.jsonData))
self.needsUpdate = False
self.last_update_time = timeNowTZ() # Reset last_update_time after writing
self.last_update_time = timeNowUTC(as_string=False) # Reset last_update_time after writing
# Update user event execution log
# mylog('verbose', [f'[API] api_endpoint_class: is_ad_hoc_user_event {self.is_ad_hoc_user_event}'])

View File

@@ -12,7 +12,7 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value, format_ip_long # noqa: E402 [flake8 lint suppression]
from db.db_helper import get_date_from_period # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB, format_date_iso, format_event_date, format_date_diff, format_date # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC, format_date_iso, format_event_date, format_date_diff, format_date # noqa: E402 [flake8 lint suppression]
# --------------------------
@@ -165,7 +165,7 @@ def get_sessions_calendar(start_date, end_date, mac):
rows = cur.fetchall()
conn.close()
now_iso = timeNowDB()
now_iso = timeNowUTC()
events = []
for row in rows:

View File

@@ -3,7 +3,7 @@ import base64
from flask import jsonify, request
from logger import mylog
from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from utils.datetime_utils import timeNowUTC
from messaging.in_app import write_notification
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
@@ -22,19 +22,19 @@ def handle_sync_get():
raw_data = f.read()
except FileNotFoundError:
msg = f"[Plugin: SYNC] Data file not found: {file_path}"
write_notification(msg, "alert", timeNowDB())
write_notification(msg, "alert", timeNowUTC())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
response_data = base64.b64encode(raw_data).decode("utf-8")
write_notification("[Plugin: SYNC] Data sent", "info", timeNowDB())
write_notification("[Plugin: SYNC] Data sent", "info", timeNowUTC())
return jsonify({
"node_name": get_setting_value("SYNC_node_name"),
"status": 200,
"message": "OK",
"data_base64": response_data,
"timestamp": timeNowDB()
"timestamp": timeNowUTC()
}), 200
@@ -68,11 +68,11 @@ def handle_sync_post():
f.write(data)
except Exception as e:
msg = f"[Plugin: SYNC] Failed to store data: {e}"
write_notification(msg, "alert", timeNowDB())
write_notification(msg, "alert", timeNowUTC())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
msg = f"[Plugin: SYNC] Data received ({file_path_new})"
write_notification(msg, "info", timeNowDB())
write_notification(msg, "info", timeNowUTC())
mylog("verbose", [msg])
return jsonify({"message": "Data received and stored successfully"}), 200

View File

@@ -4,7 +4,7 @@ import json
from const import applicationPath, apiPath
from logger import mylog
from helper import checkNewVersion
from utils.datetime_utils import timeNowDB, timeNow
from utils.datetime_utils import timeNowUTC
from api_server.sse_broadcast import broadcast_state_update
# Register NetAlertX directories using runtime configuration
@@ -67,7 +67,7 @@ class app_state_class:
previousState = ""
# Update self
self.lastUpdated = str(timeNowDB())
self.lastUpdated = str(timeNowUTC())
if os.path.exists(stateFile):
try:
@@ -95,7 +95,7 @@ class app_state_class:
self.showSpinner = False
self.processScan = False
self.isNewVersion = checkNewVersion()
self.isNewVersionChecked = int(timeNow().timestamp())
self.isNewVersionChecked = int(timeNowUTC(as_string=False).timestamp())
self.graphQLServerStarted = 0
self.currentState = "Init"
self.pluginsStates = {}
@@ -135,10 +135,10 @@ class app_state_class:
self.buildTimestamp = buildTimestamp
# check for new version every hour and if currently not running new version
if self.isNewVersion is False and self.isNewVersionChecked + 3600 < int(
timeNow().timestamp()
timeNowUTC(as_string=False).timestamp()
):
self.isNewVersion = checkNewVersion()
self.isNewVersionChecked = int(timeNow().timestamp())
self.isNewVersionChecked = int(timeNowUTC(as_string=False).timestamp())
# Update .json file
# with open(stateFile, 'w') as json_file:

View File

@@ -17,6 +17,7 @@ from db.db_upgrade import (
ensure_Settings,
ensure_Indexes,
ensure_mac_lowercase_triggers,
migrate_timestamps_to_utc,
)
@@ -187,6 +188,9 @@ class DB:
# Parameters tables setup
ensure_Parameters(self.sql)
# One-time UTC timestamp migration (must run after Parameters table exists)
migrate_timestamps_to_utc(self.sql)
# Plugins tables setup
ensure_plugins_tables(self.sql)

View File

@@ -228,7 +228,7 @@ def ensure_views(sql) -> bool:
)
SELECT
d.*, -- all Device fields
r.* -- all CurrentScan fields
r.* -- all CurrentScan fields
FROM Devices d
LEFT JOIN RankedScans r
ON d.devMac = r.scanMac
@@ -494,3 +494,219 @@ def ensure_plugins_tables(sql) -> bool:
); """)
return True
# ===============================================================================
# UTC Timestamp Migration (added 2026-02-10)
# ===============================================================================
def is_timestamps_in_utc(sql) -> bool:
"""
Check if existing timestamps in Devices table are already in UTC format.
Strategy:
1. Sample 10 non-NULL devFirstConnection timestamps from Devices
2. For each timestamp, assume it's UTC and calculate what it would be in local time
3. Check if timestamps have a consistent offset pattern (indicating local time storage)
4. If offset is consistently > 0, they're likely local timestamps (need migration)
5. If offset is ~0 or inconsistent, they're likely already UTC (skip migration)
Returns:
bool: True if timestamps appear to be in UTC already, False if they need migration
"""
try:
# Get timezone offset in seconds
import conf
from zoneinfo import ZoneInfo
import datetime as dt
now = dt.datetime.now(dt.UTC).replace(microsecond=0)
current_offset_seconds = 0
try:
if isinstance(conf.tz, dt.tzinfo):
tz = conf.tz
elif conf.tz:
tz = ZoneInfo(conf.tz)
else:
tz = None
except Exception:
tz = None
if tz:
local_now = dt.datetime.now(tz).replace(microsecond=0)
local_offset = local_now.utcoffset().total_seconds()
utc_offset = now.utcoffset().total_seconds() if now.utcoffset() else 0
current_offset_seconds = int(local_offset - utc_offset)
# Sample timestamps from Devices table
sql.execute("""
SELECT devFirstConnection, devLastConnection, devLastNotification
FROM Devices
WHERE devFirstConnection IS NOT NULL
LIMIT 10
""")
samples = []
for row in sql.fetchall():
for ts in row:
if ts:
samples.append(ts)
if not samples:
mylog("verbose", "[db_upgrade] No timestamp samples found in Devices - assuming UTC")
return True # Empty DB, assume UTC
# Parse samples and check if they have timezone info (which would indicate migration already done)
has_tz_marker = any('+' in str(ts) or 'Z' in str(ts) for ts in samples)
if has_tz_marker:
mylog("verbose", "[db_upgrade] Timestamps have timezone markers - already migrated to UTC")
return True
mylog("debug", f"[db_upgrade] Sampled {len(samples)} timestamps. Current TZ offset: {current_offset_seconds}s")
mylog("verbose", "[db_upgrade] Timestamps appear to be in system local time - migration needed")
return False
except Exception as e:
mylog("warn", f"[db_upgrade] Error checking UTC status: {e} - assuming UTC")
return True
def migrate_timestamps_to_utc(sql) -> bool:
"""
Migrate all timestamp columns in the database from local time to UTC.
This function determines if migration is needed based on the VERSION setting:
- Fresh installs (no VERSION): Skip migration - timestamps already UTC from timeNowUTC()
- Version >= 26.2.6: Skip migration - already using UTC timestamps
- Version < 26.2.6: Run migration - convert local timestamps to UTC
Affected tables:
- Devices: devFirstConnection, devLastConnection, devLastNotification
- Events: eve_DateTime
- Sessions: ses_DateTimeConnection, ses_DateTimeDisconnection
- Notifications: DateTimeCreated, DateTimePushed
- Online_History: Scan_Date
- Plugins_Objects: DateTimeCreated, DateTimeChanged
- Plugins_Events: DateTimeCreated, DateTimeChanged
- Plugins_History: DateTimeCreated, DateTimeChanged
- AppEvents: DateTimeCreated
Returns:
bool: True if migration completed or wasn't needed, False on error
"""
try:
import conf
from zoneinfo import ZoneInfo
import datetime as dt
# Check VERSION from Settings table (from previous app run)
sql.execute("SELECT setValue FROM Settings WHERE setKey = 'VERSION'")
result = sql.fetchone()
prev_version = result[0] if result else ""
# Fresh install: VERSION is empty → timestamps already UTC from timeNowUTC()
if not prev_version or prev_version == "" or prev_version == "unknown":
mylog("verbose", "[db_upgrade] Fresh install detected - timestamps already in UTC format")
return True
# Parse version - format: "26.2.6" or "v26.2.6"
try:
version_parts = prev_version.strip('v').split('.')
major = int(version_parts[0]) if len(version_parts) > 0 else 0
minor = int(version_parts[1]) if len(version_parts) > 1 else 0
patch = int(version_parts[2]) if len(version_parts) > 2 else 0
# UTC timestamps introduced in v26.2.6
# If upgrading from 26.2.6 or later, timestamps are already UTC
if (major > 26) or (major == 26 and minor > 2) or (major == 26 and minor == 2 and patch >= 6):
mylog("verbose", f"[db_upgrade] Version {prev_version} already uses UTC timestamps - skipping migration")
return True
mylog("verbose", f"[db_upgrade] Upgrading from {prev_version} (< v26.2.6) - migrating timestamps to UTC")
except (ValueError, IndexError) as e:
mylog("warn", f"[db_upgrade] Could not parse version '{prev_version}': {e} - checking timestamps")
# Fallback: use detection logic
if is_timestamps_in_utc(sql):
mylog("verbose", "[db_upgrade] Timestamps appear to be in UTC - skipping migration")
return True
# Get timezone offset
try:
if isinstance(conf.tz, dt.tzinfo):
tz = conf.tz
elif conf.tz:
tz = ZoneInfo(conf.tz)
else:
tz = None
except Exception:
tz = None
if tz:
now_local = dt.datetime.now(tz)
offset_hours = (now_local.utcoffset().total_seconds()) / 3600
else:
offset_hours = 0
mylog("verbose", f"[db_upgrade] Starting UTC timestamp migration (offset: {offset_hours} hours)")
# List of tables and their datetime columns
timestamp_columns = {
'Devices': ['devFirstConnection', 'devLastConnection', 'devLastNotification'],
'Events': ['eve_DateTime'],
'Sessions': ['ses_DateTimeConnection', 'ses_DateTimeDisconnection'],
'Notifications': ['DateTimeCreated', 'DateTimePushed'],
'Online_History': ['Scan_Date'],
'Plugins_Objects': ['DateTimeCreated', 'DateTimeChanged'],
'Plugins_Events': ['DateTimeCreated', 'DateTimeChanged'],
'Plugins_History': ['DateTimeCreated', 'DateTimeChanged'],
'AppEvents': ['DateTimeCreated'],
}
for table, columns in timestamp_columns.items():
try:
# Check if table exists
sql.execute(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'")
if not sql.fetchone():
mylog("debug", f"[db_upgrade] Table '{table}' does not exist - skipping")
continue
for column in columns:
try:
# Update non-NULL timestamps
if offset_hours > 0:
# Convert local to UTC (subtract offset)
sql.execute(f"""
UPDATE {table}
SET {column} = DATETIME({column}, '-{int(offset_hours)} hours', '-{int((offset_hours % 1) * 60)} minutes')
WHERE {column} IS NOT NULL
""")
elif offset_hours < 0:
# Convert local to UTC (add offset absolute value)
abs_hours = abs(int(offset_hours))
abs_mins = int((abs(offset_hours) % 1) * 60)
sql.execute(f"""
UPDATE {table}
SET {column} = DATETIME({column}, '+{abs_hours} hours', '+{abs_mins} minutes')
WHERE {column} IS NOT NULL
""")
row_count = sql.rowcount
if row_count > 0:
mylog("verbose", f"[db_upgrade] Migrated {row_count} timestamps in {table}.{column}")
except Exception as e:
mylog("warn", f"[db_upgrade] Error updating {table}.{column}: {e}")
continue
except Exception as e:
mylog("warn", f"[db_upgrade] Error processing table {table}: {e}")
continue
mylog("none", "[db_upgrade] ✓ UTC timestamp migration completed successfully")
return True
except Exception as e:
mylog("none", f"[db_upgrade] ERROR during timestamp migration: {e}")
return False

View File

@@ -12,7 +12,7 @@ import uuid
import conf
from const import fullConfPath, fullConfFolder, default_tz
from helper import getBuildTimeStampAndVersion, collect_lang_strings, updateSubnets, generate_random_string
from utils.datetime_utils import timeNowDB
from utils.datetime_utils import timeNowUTC
from app_state import updateState
from logger import mylog
from api import update_api
@@ -419,7 +419,7 @@ def importConfigs(pm, db, all_plugins):
# TODO cleanup later ----------------------------------------------------------------------------------
# init all time values as we have timezone - all this shoudl be moved into plugin/plugin settings
conf.time_started = datetime.datetime.now(conf.tz)
conf.time_started = timeNowUTC(as_string=False)
conf.plugins_once_run = False
# timestamps of last execution times
@@ -645,7 +645,7 @@ def importConfigs(pm, db, all_plugins):
if run_val == "schedule":
newSchedule = Cron(run_sch).schedule(
start_date=datetime.datetime.now(conf.tz)
start_date=timeNowUTC(as_string=False)
)
conf.mySchedules.append(
schedule_class(
@@ -682,7 +682,7 @@ def importConfigs(pm, db, all_plugins):
Check out new features and what has changed in the \
<a href="https://github.com/jokob-sk/NetAlertX/releases" target="_blank">📓 release notes</a>.""",
'interrupt',
timeNowDB()
timeNowUTC()
)
# -----------------
@@ -721,7 +721,7 @@ def importConfigs(pm, db, all_plugins):
mylog('minimal', msg)
# front end app log loggging
write_notification(msg, 'info', timeNowDB())
write_notification(msg, 'info', timeNowUTC())
return pm, all_plugins, True
@@ -770,7 +770,7 @@ def renameSettings(config_file):
# If the file contains old settings, proceed with renaming and backup
if contains_old_settings:
# Create a backup file with the suffix "_old_setting_names" and timestamp
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
timestamp = timeNowUTC(as_string=False).strftime("%Y%m%d%H%M%S")
backup_file = f"{config_file}_old_setting_names_{timestamp}.bak"
mylog("debug", f"[Config] Old setting names will be replaced and a backup ({backup_file}) of the config created.",)

View File

@@ -9,7 +9,7 @@ import logging
# NetAlertX imports
import conf
from const import logPath
from utils.datetime_utils import timeNowTZ
from utils.datetime_utils import timeNowUTC
DEFAULT_LEVEL = "none"
@@ -124,7 +124,7 @@ def start_log_writer_thread():
# -------------------------------------------------------------------------------
def file_print(*args):
result = timeNowTZ().strftime("%H:%M:%S") + " "
result = timeNowUTC(as_string=False).strftime("%H:%M:%S") + " "
for arg in args:
if isinstance(arg, list):
arg = " ".join(

View File

@@ -13,7 +13,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
from const import apiPath # noqa: E402 [flake8 lint suppression]
from logger import mylog # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowUTC # noqa: E402 [flake8 lint suppression]
from api_server.sse_broadcast import broadcast_unread_notifications_count # noqa: E402 [flake8 lint suppression]
@@ -64,7 +64,7 @@ def write_notification(content, level="alert", timestamp=None):
None
"""
if timestamp is None:
timestamp = timeNowDB()
timestamp = timeNowUTC()
notification = {
"timestamp": str(timestamp),

View File

@@ -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
from utils.datetime_utils import timeNowUTC
class DeviceInstance:
@@ -407,7 +407,7 @@ class DeviceInstance:
def getDeviceData(self, mac, period=""):
"""Fetch device info with children, event stats, and presence calculation."""
now = timeNowDB()
now = timeNowUTC()
# Special case for new device
if mac.lower() == "new":
@@ -639,8 +639,8 @@ class DeviceInstance:
data.get("devSkipRepeated") or 0,
data.get("devIsNew") or 0,
data.get("devIsArchived") or 0,
data.get("devLastConnection") or timeNowDB(),
data.get("devFirstConnection") or timeNowDB(),
data.get("devLastConnection") or timeNowUTC(),
data.get("devFirstConnection") or timeNowUTC(),
data.get("devLastIP") or "",
data.get("devGUID") or "",
data.get("devCustomProps") or "",

View File

@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from logger import mylog
from database import get_temp_db_connection
from db.db_helper import row_to_json, get_date_from_period
from utils.datetime_utils import ensure_datetime
from utils.datetime_utils import ensure_datetime, timeNowUTC
# -------------------------------------------------------------------------------
@@ -43,7 +43,7 @@ class EventInstance:
# Get events in the last 24h
def get_recent(self):
since = datetime.now() - timedelta(hours=24)
since = timeNowUTC(as_string=False) - timedelta(hours=24)
conn = self._conn()
rows = conn.execute("""
SELECT * FROM Events
@@ -59,7 +59,7 @@ class EventInstance:
mylog("warn", f"[Events] get_by_hours({hours}) -> invalid value")
return []
since = datetime.now() - timedelta(hours=hours)
since = timeNowUTC(as_string=False) - timedelta(hours=hours)
conn = self._conn()
rows = conn.execute("""
SELECT * FROM Events
@@ -93,14 +93,14 @@ class EventInstance:
eve_EventType, eve_AdditionalInfo,
eve_PendingAlertEmail, eve_PairEventRowid
) VALUES (?,?,?,?,?,?,?)
""", (mac, ip, datetime.now(), eventType, info,
""", (mac, ip, timeNowUTC(as_string=False), eventType, info,
1 if pendingAlert else 0, pairRow))
conn.commit()
conn.close()
# Delete old events
def delete_older_than(self, days: int):
cutoff = datetime.now() - timedelta(days=days)
cutoff = timeNowUTC(as_string=False) - timedelta(days=days)
conn = self._conn()
result = conn.execute("DELETE FROM Events WHERE eve_DateTime < ?", (cutoff,))
conn.commit()

View File

@@ -16,7 +16,7 @@ from helper import (
getBuildTimeStampAndVersion,
)
from messaging.in_app import write_notification
from utils.datetime_utils import timeNowDB, get_timezone_offset
from utils.datetime_utils import timeNowUTC, get_timezone_offset
# -----------------------------------------------------------------------------
@@ -68,7 +68,7 @@ class NotificationInstance:
self.HasNotifications = True
self.GUID = str(uuid.uuid4())
self.DateTimeCreated = timeNowDB()
self.DateTimeCreated = timeNowUTC()
self.DateTimePushed = ""
self.Status = "new"
self.JSON = JSON
@@ -107,7 +107,7 @@ class NotificationInstance:
mail_html = mail_html.replace("NEW_VERSION", newVersionText)
# Report "REPORT_DATE" in Header & footer
timeFormated = timeNowDB()
timeFormated = timeNowUTC()
mail_text = mail_text.replace("REPORT_DATE", timeFormated)
mail_html = mail_html.replace("REPORT_DATE", timeFormated)
@@ -208,7 +208,7 @@ class NotificationInstance:
# Updates the Published properties
def updatePublishedVia(self, newPublishedVia):
self.PublishedVia = newPublishedVia
self.DateTimePushed = timeNowDB()
self.DateTimePushed = timeNowUTC()
self.upsert()
# create or update a notification
@@ -274,7 +274,7 @@ class NotificationInstance:
SELECT eve_MAC FROM Events
WHERE eve_PendingAlertEmail = 1
)
""", (timeNowDB(),))
""", (timeNowUTC(),))
self.db.sql.execute("""
UPDATE Events SET eve_PendingAlertEmail = 0

View File

@@ -3,7 +3,7 @@ import uuid
from const import logPath
from logger import mylog
from utils.datetime_utils import timeNowDB
from utils.datetime_utils import timeNowUTC
class UserEventsQueueInstance:
@@ -90,7 +90,7 @@ class UserEventsQueueInstance:
success - True if the event was successfully added.
message - Log message describing the result.
"""
timestamp = timeNowDB()
timestamp = timeNowUTC()
# Generate GUID
guid = str(uuid.uuid4())

View File

@@ -11,7 +11,7 @@ import conf
from const import pluginsPath, logPath, applicationPath, reportTemplatesPath
from logger import mylog, Logger
from helper import get_file_content, get_setting, get_setting_value
from utils.datetime_utils import timeNowTZ, timeNowDB
from utils.datetime_utils import timeNowUTC
from app_state import updateState
from api import update_api
from utils.plugin_utils import (
@@ -113,7 +113,7 @@ class plugin_manager:
schd = self._cache["schedules"].get(prefix)
if schd:
# note the last time the scheduled plugin run was executed
schd.last_run = timeNowTZ()
schd.last_run = timeNowUTC(as_string=False)
# ===============================================================================
# Handling of user initialized front-end events
@@ -166,14 +166,14 @@ class plugin_manager:
if len(executed_events) > 0 and executed_events:
executed_events_message = ', '.join(executed_events)
mylog('minimal', ['[check_and_run_user_event] INFO: Executed events: ', executed_events_message])
write_notification(f"[Ad-hoc events] Events executed: {executed_events_message}", "interrupt", timeNowDB())
write_notification(f"[Ad-hoc events] Events executed: {executed_events_message}", "interrupt", timeNowUTC())
return
# -------------------------------------------------------------------------------
def handle_run(self, runType):
mylog('minimal', ['[', timeNowDB(), '] START Run: ', runType])
mylog('minimal', ['[', timeNowUTC(), '] START Run: ', runType])
# run the plugin
for plugin in self.all_plugins:
@@ -190,15 +190,13 @@ class plugin_manager:
pluginsStates={pluginName: current_plugin_state.get(pluginName, {})}
)
mylog('minimal', ['[', timeNowDB(), '] END Run: ', runType])
mylog('minimal', ['[', timeNowUTC(), '] END Run: ', runType])
return
# -------------------------------------------------------------------------------
def handle_test(self, runType):
mylog("minimal", ["[", timeNowTZ(), "] [Test] START Test: ", runType])
mylog('minimal', ['[', timeNowDB(), '] [Test] START Test: ', runType])
mylog('minimal', ['[', timeNowUTC(), '] [Test] START Test: ', runType])
# Prepare test samples
sample_json = json.loads(
@@ -235,7 +233,7 @@ class plugin_manager:
"""
sql = self.db.sql
plugin_states = {}
now_str = timeNowDB()
now_str = timeNowUTC()
if plugin_name: # Only compute for single plugin
sql.execute(
@@ -799,7 +797,7 @@ def process_plugin_events(db, plugin, plugEventsArr):
if isMissing:
# if wasn't missing before, mark as changed
if tmpObj.status != "missing-in-last-scan":
tmpObj.changed = timeNowDB()
tmpObj.changed = timeNowUTC()
tmpObj.status = "missing-in-last-scan"
# mylog('debug', [f'[Plugins] Missing from last scan (PrimaryID | SecondaryID): {tmpObj.primaryId} | {tmpObj.secondaryId}'])

View File

@@ -3,7 +3,7 @@ import os
import re
import ipaddress
from helper import get_setting_value, check_IP_format
from utils.datetime_utils import timeNowDB, normalizeTimeStamp
from utils.datetime_utils import timeNowUTC, normalizeTimeStamp
from logger import mylog, Logger
from const import vendorsPath, vendorsPathNewest, sql_generateGuid, NULL_EQUIVALENTS
from models.device_instance import DeviceInstance
@@ -227,7 +227,7 @@ def update_devLastConnection_from_CurrentScan(db):
Update devLastConnection to current time for all devices seen in CurrentScan.
"""
sql = db.sql
startTime = timeNowDB()
startTime = timeNowUTC()
mylog("debug", f"[Update Devices] - Updating devLastConnection to {startTime}")
sql.execute(f"""
@@ -600,7 +600,7 @@ def print_scan_stats(db):
# -------------------------------------------------------------------------------
def create_new_devices(db):
sql = db.sql # TO-DO
startTime = timeNowDB()
startTime = timeNowUTC()
# Insert events for new devices from CurrentScan (not yet in Devices)
@@ -1109,7 +1109,7 @@ def update_devices_names(pm):
# --- Step 3: Log last checked time ---
# After resolving names, update last checked
pm.plugin_checks = {"DIGSCAN": timeNowDB(), "AVAHISCAN": timeNowDB(), "NSLOOKUP": timeNowDB(), "NBTSCAN": timeNowDB()}
pm.plugin_checks = {"DIGSCAN": timeNowUTC(), "AVAHISCAN": timeNowUTC(), "NSLOOKUP": timeNowUTC(), "NBTSCAN": timeNowUTC()}
# -------------------------------------------------------------------------------

View File

@@ -14,11 +14,11 @@ from scan.device_handling import (
)
from helper import get_setting_value
from db.db_helper import print_table_schema
from utils.datetime_utils import timeNowDB
from utils.datetime_utils import timeNowUTC
from logger import mylog, Logger
from messaging.reporting import skip_repeated_notifications
from messaging.in_app import update_unread_notifications_count
from const import NULL_EQUIVALENTS, NULL_EQUIVALENTS_SQL
from const import NULL_EQUIVALENTS_SQL
# Make sure log level is initialized correctly
@@ -167,7 +167,7 @@ def create_sessions_snapshot(db):
# -------------------------------------------------------------------------------
def insert_events(db):
sql = db.sql # TO-DO
startTime = timeNowDB()
startTime = timeNowUTC()
# Check device down
mylog("debug", "[Events] - 1 - Devices down")
@@ -234,7 +234,7 @@ def insert_events(db):
def insertOnlineHistory(db):
sql = db.sql # TO-DO: Implement sql object
scanTimestamp = timeNowDB()
scanTimestamp = timeNowUTC()
# Query to fetch all relevant device counts in one go
query = """

View File

@@ -3,7 +3,7 @@
import datetime
from logger import mylog
import conf
from utils.datetime_utils import timeNowUTC
# -------------------------------------------------------------------------------
@@ -28,11 +28,11 @@ class schedule_class:
# Initialize the last run time if never run before
if self.last_run == 0:
self.last_run = (
datetime.datetime.now(conf.tz) - datetime.timedelta(days=365)
timeNowUTC(as_string=False) - datetime.timedelta(days=365)
).replace(microsecond=0)
# get the current time with the currently specified timezone
nowTime = datetime.datetime.now(conf.tz).replace(microsecond=0)
nowTime = timeNowUTC(as_string=False)
# Run the schedule if the current time is past the schedule time we saved last time and
# (maybe the following check is unnecessary)

View File

@@ -20,47 +20,40 @@ DATETIME_PATTERN = "%Y-%m-%d %H:%M:%S"
DATETIME_REGEX = re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$')
def timeNowTZ():
if conf.tz:
return datetime.datetime.now(conf.tz).replace(microsecond=0)
else:
return datetime.datetime.now().replace(microsecond=0)
def timeNow():
return datetime.datetime.now().replace(microsecond=0)
# ⚠️ CRITICAL: ALL database timestamps MUST be stored in UTC
# This is the SINGLE SOURCE OF TRUTH for current time in NetAlertX
# Use timeNowUTC() for DB writes (returns UTC string by default)
# Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons, logging)
def timeNowUTC(as_string=True):
"""
Return the current time in UTC.
This is the ONLY function that calls datetime.datetime.now() in the entire codebase.
All timestamps stored in the database MUST use UTC format.
Args:
as_string (bool): If True, returns formatted string for DB storage.
If False, returns datetime object for operations.
Returns:
str: UTC timestamp as 'YYYY-MM-DD HH:MM:SS' when as_string=True
datetime.datetime: UTC datetime object when as_string=False
Examples:
timeNowUTC() → '2025-11-04 07:09:11' (for DB writes)
timeNowUTC(as_string=False) → datetime.datetime(2025, 11, 4, 7, 9, 11, tzinfo=UTC)
"""
utc_now = datetime.datetime.now(datetime.UTC).replace(microsecond=0)
return utc_now.strftime(DATETIME_PATTERN) if as_string else utc_now
def get_timezone_offset():
now = datetime.datetime.now(conf.tz)
offset_hours = now.utcoffset().total_seconds() / 3600
now = timeNowUTC(as_string=False).replace(tzinfo=conf.tz) if conf.tz else timeNowUTC(as_string=False)
offset_hours = now.utcoffset().total_seconds() / 3600 if now.utcoffset() else 0
offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60))
return offset_formatted
def timeNowDB(local=True):
"""
Return the current time (local or UTC) as ISO 8601 for DB storage.
Safe for SQLite, PostgreSQL, etc.
Example local: '2025-11-04 18:09:11'
Example UTC: '2025-11-04 07:09:11'
"""
if local:
try:
if isinstance(conf.tz, datetime.tzinfo):
tz = conf.tz
elif conf.tz:
tz = ZoneInfo(conf.tz)
else:
tz = None
except Exception:
tz = None
return datetime.datetime.now(tz).strftime(DATETIME_PATTERN)
else:
return datetime.datetime.now(datetime.UTC).strftime(DATETIME_PATTERN)
# -------------------------------------------------------------------------------
# Date and time methods
# -------------------------------------------------------------------------------
@@ -113,7 +106,10 @@ def normalizeTimeStamp(inputTimeStamp):
# -------------------------------------------------------------------------------------------
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.
"""
if not date_val:
return None
@@ -125,10 +121,14 @@ def format_date_iso(date_val: str) -> Optional[str]:
else:
dt = date_val
# 2. If it has no timezone, ATTACH (don't convert) your config TZ
# 2. If it has no timezone, assume it's UTC (our DB storage format)
# then CONVERT to user's configured timezone
if dt.tzinfo is None:
# Mark as UTC first
dt = dt.replace(tzinfo=datetime.UTC)
# Convert to user's timezone
target_tz = conf.tz if isinstance(conf.tz, datetime.tzinfo) else ZoneInfo(conf.tz)
dt = dt.replace(tzinfo=target_tz)
dt = dt.astimezone(target_tz)
# 3. Return the string. .isoformat() will now include the +11:00 or +10:00
return dt.isoformat()
@@ -151,7 +151,7 @@ def format_event_date(date_str: str, event_type: str) -> str:
# -------------------------------------------------------------------------------------------
def ensure_datetime(dt: Union[str, datetime.datetime, None]) -> datetime.datetime:
if dt is None:
return timeNowTZ()
return timeNowUTC(as_string=False)
if isinstance(dt, str):
return datetime.datetime.fromisoformat(dt)
return dt
@@ -172,6 +172,10 @@ def parse_datetime(dt_str):
def format_date(date_str: str) -> str:
"""Format a date string from DB for display.
Assumes DB timestamps are stored in UTC and converts them to user's configured timezone.
"""
try:
if not date_str:
return ""
@@ -179,25 +183,21 @@ def format_date(date_str: str) -> str:
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 the DB timestamp has no timezone, assume it's UTC (our storage format)
# then CONVERT to user's configured timezone
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)
# Mark as UTC first
dt = dt.replace(tzinfo=datetime.UTC)
# Convert to user's timezone
if isinstance(conf.tz, str):
dt = dt.astimezone(ZoneInfo(conf.tz))
else:
dt = dt.astimezone(conf.tz)
# 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 ISO format with timezone offset
return dt.isoformat()
except Exception as e:
@@ -207,7 +207,7 @@ def format_date(date_str: str) -> str:
def format_date_diff(date1, date2, tz_name):
"""
Return difference between two datetimes as 'Xd HH:MM'.
Uses app timezone if datetime is naive.
Assumes DB timestamps are stored in UTC and converts them to user's configured timezone.
date2 can be None (uses now).
"""
# Get timezone from settings
@@ -215,20 +215,22 @@ def format_date_diff(date1, date2, tz_name):
def parse_dt(dt):
if dt is None:
return datetime.datetime.now(tz)
# Get current UTC time and convert to user's timezone
return timeNowUTC(as_string=False).astimezone(tz)
if isinstance(dt, str):
try:
dt_parsed = email.utils.parsedate_to_datetime(dt)
except (ValueError, TypeError):
# fallback: parse ISO string
dt_parsed = datetime.datetime.fromisoformat(dt)
# convert naive GMT/UTC to app timezone
# If naive (no timezone), assume it's UTC from DB, then convert to user's timezone
if dt_parsed.tzinfo is None:
dt_parsed = tz.localize(dt_parsed)
dt_parsed = dt_parsed.replace(tzinfo=datetime.UTC).astimezone(tz)
else:
dt_parsed = dt_parsed.astimezone(tz)
return dt_parsed
return dt if dt.tzinfo else tz.localize(dt)
# If datetime object without timezone, assume it's UTC from DB
return dt.astimezone(tz) if dt.tzinfo else dt.replace(tzinfo=datetime.UTC).astimezone(tz)
dt1 = parse_dt(date1)
dt2 = parse_dt(date2)