BE/PLG: TZ timestamp work #1251

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2025-11-04 19:24:13 +11:00
parent 6dd7251c84
commit 59477e7b38
67 changed files with 164 additions and 133 deletions

View File

@@ -9,7 +9,7 @@ INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value
from db.db_helper import get_date_from_period
from app_state import updateState

View File

@@ -14,7 +14,7 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, timeNowTZ
from helper import is_random_mac, format_date, get_setting_value, timeNowDB
from db.db_helper import row_to_json, get_date_from_period
# --------------------------
@@ -28,7 +28,7 @@ def get_device_data(mac):
conn = get_temp_db_connection()
cur = conn.cursor()
now = timeNowTZ().astimezone().isoformat()
now = timeNowDB()
# Special case for new device
if mac.lower() == "new":
@@ -187,8 +187,8 @@ def set_device_data(mac, data):
data.get("devSkipRepeated", 0),
data.get("devIsNew", 0),
data.get("devIsArchived", 0),
data.get("devLastConnection", timeNowTZ().astimezone().isoformat()),
data.get("devFirstConnection", timeNowTZ().astimezone().isoformat()),
data.get("devLastConnection", timeNowDB()),
data.get("devFirstConnection", timeNowDB()),
data.get("devLastIP", ""),
data.get("devGUID", ""),
data.get("devCustomProps", ""),

View File

@@ -14,7 +14,7 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, timeNowTZ, mylog, ensure_datetime
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, ensure_datetime
from db.db_helper import row_to_json, get_date_from_period

View File

@@ -16,7 +16,7 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, timeNowTZ, format_date_diff, format_ip_long, parse_datetime
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, format_date_diff, format_ip_long, parse_datetime
from db.db_helper import row_to_json, get_date_from_period

View File

@@ -2,7 +2,7 @@ import os
import base64
from flask import jsonify, request
from logger import mylog
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value, timeNowDB
from messaging.in_app import write_notification
INSTALL_PATH = "/app"
@@ -16,19 +16,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", timeNowTZ())
write_notification(msg, "alert", timeNowDB())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
response_data = base64.b64encode(raw_data).decode("utf-8")
write_notification("[Plugin: SYNC] Data sent", "info", timeNowTZ())
write_notification("[Plugin: SYNC] Data sent", "info", timeNowDB())
return jsonify({
"node_name": get_setting_value("SYNC_node_name"),
"status": 200,
"message": "OK",
"data_base64": response_data,
"timestamp": timeNowTZ()
"timestamp": timeNowDB()
}), 200
@@ -61,11 +61,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", timeNowTZ())
write_notification(msg, "alert", timeNowDB())
mylog("verbose", [msg])
return jsonify({"error": msg}), 500
msg = f"[Plugin: SYNC] Data received ({file_path_new})"
write_notification(msg, "info", timeNowTZ())
write_notification(msg, "info", timeNowDB())
mylog("verbose", [msg])
return jsonify({"message": "Data received and stored successfully"}), 200

View File

@@ -4,7 +4,7 @@ import json
import conf
from const import *
from logger import mylog, logResult
from helper import timeNowTZ, timeNow, checkNewVersion
from helper import timeNowDB, timeNow, checkNewVersion
# Register NetAlertX directories
INSTALL_PATH="/app"
@@ -59,7 +59,7 @@ class app_state_class:
previousState = ""
# Update self
self.lastUpdated = str(timeNowTZ().astimezone().isoformat())
self.lastUpdated = str(timeNowDB())
if os.path.exists(stateFile):
try:

View File

@@ -31,18 +31,11 @@ INSTALL_PATH="/app"
#-------------------------------------------------------------------------------
# DateTime
#-------------------------------------------------------------------------------
# Get the current time in the current TimeZone
def timeNowTZ():
if conf.tz:
return datetime.datetime.now(conf.tz).replace(microsecond=0)
else:
return datetime.datetime.now().replace(microsecond=0)
# if isinstance(conf.TIMEZONE, str):
# tz = pytz.timezone(conf.TIMEZONE)
# else:
# tz = conf.TIMEZONE
# return datetime.datetime.now(tz).replace(microsecond=0)
def timeNow():
return datetime.datetime.now().replace(microsecond=0)
@@ -53,6 +46,23 @@ def get_timezone_offset():
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:
tz = ZoneInfo(conf.tz) if conf.tz else None
except Exception:
tz = None
return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
else:
return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
#-------------------------------------------------------------------------------
# Date and time methods
@@ -79,7 +89,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 timeNowDB()
if isinstance(dt, str):
return datetime.datetime.fromisoformat(dt)
return dt

View File

@@ -12,7 +12,7 @@ import re
# Register NetAlertX libraries
import conf
from const import fullConfPath, applicationPath, fullConfFolder, default_tz
from helper import getBuildTimeStamp, fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowTZ, get_setting_value, generate_random_string
from helper import getBuildTimeStamp, fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowDB, get_setting_value, generate_random_string
from app_state import updateState
from logger import mylog
from api import update_api
@@ -392,7 +392,7 @@ def importConfigs (pm, db, all_plugins):
# ccd(key, default, config_dir, name, inputtype, options, group, events=None, desc="", setJsonMetadata=None, overrideTemplate=None, forceDefault=False)
ccd('VERSION', buildTimestamp , c_d, '_KEEP_', '_KEEP_', '_KEEP_', '_KEEP_', None, "_KEEP_", None, None, True)
write_notification(f'[Upgrade] : App upgraded 🚀 Please clear the cache: <ol> <li>Click OK below</li> <li>Clear the browser cache (shift + browser refresh button)</li> <li> Clear app cache with the <i class="fa-solid fa-rotate"></i> (reload) button in the header</li><li>Go to Settings and click Save</li> </ol> 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', timeNowTZ())
write_notification(f'[Upgrade] : App upgraded 🚀 Please clear the cache: <ol> <li>Click OK below</li> <li>Clear the browser cache (shift + browser refresh button)</li> <li> Clear app cache with the <i class="fa-solid fa-rotate"></i> (reload) button in the header</li><li>Go to Settings and click Save</li> </ol> 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())
@@ -429,7 +429,7 @@ def importConfigs (pm, db, all_plugins):
mylog('minimal', msg)
# front end app log loggging
write_notification(msg, 'info', timeNowTZ())
write_notification(msg, 'info', timeNowDB())
return pm, all_plugins, True

View File

@@ -19,6 +19,23 @@ def timeNowTZ():
else:
return datetime.datetime.now().replace(microsecond=0)
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:
tz = ZoneInfo(conf.tz) if conf.tz else None
except Exception:
tz = None
return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
else:
return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
#-------------------------------------------------------------------------------
# Map custom debug levels to Python logging levels
custom_to_logging_levels = {

View File

@@ -20,7 +20,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from const import applicationPath, logPath, apiPath, confFileName, reportTemplatesPath
from logger import logResult, mylog
from helper import generate_mac_links, removeDuplicateNewLines, timeNowTZ, get_file_content, write_file, get_setting_value, get_timezone_offset
from helper import generate_mac_links, removeDuplicateNewLines, timeNowDB, get_file_content, write_file, get_setting_value, get_timezone_offset
NOTIFICATION_API_FILE = apiPath + 'user_notifications.json'
@@ -39,7 +39,7 @@ def write_notification(content, level='alert', timestamp=None):
None
"""
if timestamp is None:
timestamp = timeNowTZ()
timestamp = timeNowDB()
# Generate GUID
guid = str(uuid.uuid4())

View File

@@ -20,7 +20,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from const import applicationPath, logPath, apiPath, confFileName
from helper import timeNowTZ, get_file_content, write_file, get_timezone_offset, get_setting_value
from helper import get_file_content, write_file, get_timezone_offset, get_setting_value
from logger import logResult, mylog
from db.sql_safe_builder import create_safe_condition_builder
@@ -123,6 +123,9 @@ def get_notifications (db):
)
ORDER BY down_events.eve_DateTime;
"""
mylog("none", sqlQuery)
# Get the events as JSON
json_obj = db.get_table_as_json(sqlQuery)

View File

@@ -16,6 +16,7 @@ from const import applicationPath, logPath, apiPath, reportTemplatesPath
from logger import mylog, Logger
from helper import generate_mac_links, \
removeDuplicateNewLines, \
timeNowDB, \
timeNowTZ, \
write_file, \
get_setting_value, \
@@ -71,7 +72,7 @@ class NotificationInstance:
self.HasNotifications = True
self.GUID = str(uuid.uuid4())
self.DateTimeCreated = timeNowTZ()
self.DateTimeCreated = timeNowDB()
self.DateTimePushed = ""
self.Status = "new"
self.JSON = JSON
@@ -112,7 +113,7 @@ class NotificationInstance:
mail_html = mail_html.replace('<NEW_VERSION>', newVersionText)
# Report "REPORT_DATE" in Header & footer
timeFormated = timeNowTZ().strftime('%Y-%m-%d %H:%M')
timeFormated = timeNowDB()
mail_text = mail_text.replace('<REPORT_DATE>', timeFormated)
mail_html = mail_html.replace('<REPORT_DATE>', timeFormated)
@@ -231,7 +232,7 @@ class NotificationInstance:
# Updates the Published properties
def updatePublishedVia(self, newPublishedVia):
self.PublishedVia = newPublishedVia
self.DateTimePushed = timeNowTZ()
self.DateTimePushed = timeNowDB()
self.upsert()
# create or update a notification
@@ -282,7 +283,7 @@ class NotificationInstance:
SELECT eve_MAC FROM Events
WHERE eve_PendingAlertEmail = 1
)
""", (timeNowTZ(),))
""", (timeNowDB(),))
self.db.sql.execute("""
UPDATE Events SET eve_PendingAlertEmail = 0

View File

@@ -12,7 +12,7 @@ from collections import namedtuple
import conf
from const import pluginsPath, logPath, applicationPath, reportTemplatesPath
from logger import mylog, Logger
from helper import timeNowTZ, get_file_content, write_file, get_setting, get_setting_value
from helper import timeNowDB, timeNowTZ, get_file_content, write_file, get_setting, get_setting_value
from app_state import updateState
from api import update_api
from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting_obj, print_plugin_info, list_to_csv, combine_plugin_objects, resolve_wildcards_arr, handle_empty, custom_plugin_decoder, decode_and_rename_files
@@ -154,7 +154,7 @@ 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", timeNowTZ())
write_notification(f"[Ad-hoc events] Events executed: {executed_events_message}", "interrupt", timeNowDB())
return
@@ -163,7 +163,7 @@ class plugin_manager:
#-------------------------------------------------------------------------------
def handle_run(self, runType):
mylog('minimal', ['[', timeNowTZ(), '] START Run: ', runType])
mylog('minimal', ['[', timeNowDB(), '] START Run: ', runType])
# run the plugin
for plugin in self.all_plugins:
@@ -177,7 +177,7 @@ class plugin_manager:
current_plugin_state = self.get_plugin_states(pluginName) # get latest plugin state
updateState(pluginsStates={pluginName: current_plugin_state.get(pluginName, {})})
mylog('minimal', ['[', timeNowTZ(), '] END Run: ', runType])
mylog('minimal', ['[', timeNowDB(), '] END Run: ', runType])
return
@@ -186,7 +186,7 @@ class plugin_manager:
#-------------------------------------------------------------------------------
def handle_test(self, runType):
mylog('minimal', ['[', timeNowTZ(), '] [Test] START Test: ', runType])
mylog('minimal', ['[', timeNowDB(), '] [Test] START Test: ', runType])
# Prepare test samples
sample_json = json.loads(get_file_content(reportTemplatesPath + 'webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"]
@@ -221,7 +221,7 @@ class plugin_manager:
"""
sql = self.db.sql
plugin_states = {}
now_str = timeNowTZ().isoformat()
now_str = timeNowDB()
if plugin_name: # Only compute for single plugin
sql.execute("""
@@ -759,7 +759,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 = timeNowTZ().astimezone().isoformat()
tmpObj.changed = timeNowDB()
tmpObj.status = "missing-in-last-scan"
# mylog('debug', [f'[Plugins] Missing from last scan (PrimaryID | SecondaryID): {tmpObj.primaryId} | {tmpObj.secondaryId}'])

View File

@@ -4,7 +4,7 @@ import json
import conf
from logger import mylog
from const import pluginsPath, logPath, apiPath
from helper import timeNowTZ, get_file_content, write_file, get_setting, get_setting_value, setting_value_to_python_type
from helper import get_file_content, write_file, get_setting, get_setting_value, setting_value_to_python_type
from app_state import updateState
from crypto_utils import decrypt_data, generate_deterministic_guid

View File

@@ -10,7 +10,7 @@ from dateutil import parser
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value, check_IP_format
from helper import timeNowDB, timeNowTZ, get_setting_value, check_IP_format
from logger import mylog, Logger
from const import vendorsPath, vendorsPathNewest, sql_generateGuid
from models.device_instance import DeviceInstance
@@ -56,7 +56,7 @@ def exclude_ignored_devices(db):
#-------------------------------------------------------------------------------
def update_devices_data_from_scan (db):
sql = db.sql #TO-DO
startTime = timeNowTZ().astimezone().isoformat()
startTime = timeNowDB()
# Update Last Connection
mylog('debug', '[Update Devices] 1 Last Connection')
@@ -371,7 +371,7 @@ def print_scan_stats(db):
#-------------------------------------------------------------------------------
def create_new_devices (db):
sql = db.sql # TO-DO
startTime = timeNowTZ()
startTime = timeNowDB()
# Insert events for new devices from CurrentScan (not yet in Devices)
@@ -536,7 +536,7 @@ def update_devices_names(pm):
if isinstance(last_checked, str):
try:
last_checked = parser.parse(last_checked)
except Exception as e:
except (ValueError, TypeError) as e:
mylog('none', f'[Update Device Name] Could not parse last_checked timestamp: {last_checked!r} ({e})')
last_checked = None
elif not isinstance(last_checked, datetime.datetime):
@@ -544,7 +544,6 @@ def update_devices_names(pm):
# Collect and normalize valid state update timestamps for name-related plugins
state_times = []
latest_state = None
for p in name_plugins:
state_updated = pm.plugin_states.get(p, {}).get("stateUpdated")
@@ -561,13 +560,15 @@ def update_devices_names(pm):
mylog('none', f'[Update Device Name] Failed to parse timestamp for {p}: {state_updated!r} ({e})')
else:
mylog('none', f'[Update Device Name] Unexpected timestamp type for {p}: {type(state_updated)}')
# Determine the latest valid timestamp safely
try:
if state_times:
latest_state = max(state_times)
except Exception as e:
mylog('none', f'[Update Device Name] Failed to determine latest timestamp, using fallback ({e})')
latest_state = state_times[-1] if state_times else None
# Determine the latest valid timestamp safely (after collecting all timestamps)
latest_state = None
try:
if state_times:
latest_state = max(state_times)
except (ValueError, TypeError) as e:
mylog('none', f'[Update Device Name] Failed to determine latest timestamp, using fallback ({e})')
latest_state = state_times[-1] if state_times else None
# Skip if no plugin state changed since last check
@@ -672,7 +673,7 @@ def update_devices_names(pm):
# --- Step 3: Log last checked time ---
# After resolving names, update last checked
pm.name_plugins_checked = timeNowTZ().astimezone().isoformat()
pm.name_plugins_checked = timeNowDB()
#-------------------------------------------------------------------------------
# Updates devPresentLastScan for parent devices based on the presence of their NICs

View File

@@ -12,7 +12,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from const import *
from logger import mylog
from helper import timeNowTZ, get_setting_value
from helper import get_setting_value
# Load MAC/device-type/icon rules from external file
MAC_TYPE_ICON_PATH = Path(f"{INSTALL_PATH}/back/device_heuristics_rules.json")

View File

@@ -6,7 +6,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from scan.device_handling import create_new_devices, print_scan_stats, save_scanned_devices, exclude_ignored_devices, update_devices_data_from_scan
from helper import timeNowTZ, get_setting_value
from helper import timeNowDB, get_setting_value
from db.db_helper import print_table_schema
from logger import mylog, Logger
from messaging.reporting import skip_repeated_notifications
@@ -128,7 +128,7 @@ def create_sessions_snapshot (db):
#-------------------------------------------------------------------------------
def insert_events (db):
sql = db.sql #TO-DO
startTime = timeNowTZ()
startTime = timeNowDB()
# Check device down
mylog('debug','[Events] - 1 - Devices down')
@@ -191,7 +191,7 @@ def insert_events (db):
def insertOnlineHistory(db):
sql = db.sql # TO-DO: Implement sql object
scanTimestamp = timeNowTZ()
scanTimestamp = timeNowDB()
# Query to fetch all relevant device counts in one go
query = """

View File

@@ -7,7 +7,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from logger import mylog, Logger
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value
from models.device_instance import DeviceInstance
from models.plugin_object_instance import PluginObjectInstance

View File

@@ -10,7 +10,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
# Register NetAlertX modules
import conf
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value
# Make sure the TIMEZONE for logging is correct
# conf.tz = pytz.timezone(get_setting_value('TIMEZONE'))
@@ -20,7 +20,6 @@ from logger import mylog, Logger, logResult
Logger(get_setting_value('LOG_LEVEL'))
from const import applicationPath, logPath, apiPath, confFileName, sql_generateGuid
from helper import timeNowTZ
class AppEvent_obj:
def __init__(self, db):

View File

@@ -8,7 +8,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from logger import mylog, Logger
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))

View File

@@ -9,7 +9,7 @@ import conf
from const import fullConfFolder
import workflows.actions
from logger import mylog, Logger
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))

View File

@@ -7,7 +7,7 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from logger import mylog, Logger
from helper import get_setting_value, timeNowTZ
from helper import get_setting_value
from database import get_array_from_sql_rows
# Make sure log level is initialized correctly