mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-02 16:22:20 -07:00
timestamp cleanup
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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}'])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.",)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 "",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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}'])
|
||||
|
||||
|
||||
@@ -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()}
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user