mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
BE+FE: refactor timezone UTC additional work #1506
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
@@ -27,8 +27,11 @@ function initOnlineHistoryGraph() {
|
|||||||
var archivedCounts = [];
|
var archivedCounts = [];
|
||||||
|
|
||||||
res.data.forEach(function(entry) {
|
res.data.forEach(function(entry) {
|
||||||
var dateObj = new Date(entry.Scan_Date);
|
console.log(entry.Scan_Date);
|
||||||
var formattedTime = dateObj.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', hour12: false});
|
|
||||||
|
// var dateObj = new Date(entry.Scan_Date);
|
||||||
|
var formattedTime = localizeTimestamp(entry.Scan_Date).slice(11, 17);
|
||||||
|
// dateObj.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', hour12: false});
|
||||||
|
|
||||||
timeStamps.push(formattedTime);
|
timeStamps.push(formattedTime);
|
||||||
onlineCounts.push(entry.Online_Devices);
|
onlineCounts.push(entry.Online_Devices);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
"description": [
|
"description": [
|
||||||
{
|
{
|
||||||
"language_code": "en_us",
|
"language_code": "en_us",
|
||||||
"string": "Selects the ICMP engine to use. <code>ping</code> checks devices individually and works even when the ARP / neighbor cache is empty, but is slower on larger networks. <code>fping</code> scans IP ranges in parallel and is significantly faster, but relies on the system neighbor cache to resolve IP addresses to MAC addresses. For most networks, <code>fping</code> is recommended. The default command arguments <code>ICMP_ARGS</code> are compatible with both modes."
|
"string": "Selects the ICMP engine to use. <code>ping</code> checks devices individually, works even with an empty ARP/neighbor cache, but is slower on large networks. <code>fping</code> scans IP ranges in parallel and is much faster, but depends on the system neighbor cache, which can delay MAC resolution. For most networks, <code>fping</code> is recommended, unless precise and timely offline/online detection is needed. Default <code>ICMP_ARGS</code> work with both engines."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -534,7 +534,6 @@ def is_timestamps_in_utc(sql) -> bool:
|
|||||||
try:
|
try:
|
||||||
# Get timezone offset in seconds
|
# Get timezone offset in seconds
|
||||||
import conf
|
import conf
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
now = dt.datetime.now(dt.UTC).replace(microsecond=0)
|
now = dt.datetime.now(dt.UTC).replace(microsecond=0)
|
||||||
@@ -754,4 +753,3 @@ def migrate_timestamps_to_utc(sql) -> bool:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
mylog("none", f"[db_upgrade] ERROR during timestamp migration: {e}")
|
mylog("none", f"[db_upgrade] ERROR during timestamp migration: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logging
|
|||||||
# NetAlertX imports
|
# NetAlertX imports
|
||||||
import conf
|
import conf
|
||||||
from const import logPath
|
from const import logPath
|
||||||
from utils.datetime_utils import timeNowUTC
|
from utils.datetime_utils import timeNowTZ
|
||||||
|
|
||||||
DEFAULT_LEVEL = "none"
|
DEFAULT_LEVEL = "none"
|
||||||
|
|
||||||
@@ -124,12 +124,12 @@ def start_log_writer_thread():
|
|||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
def file_print(*args):
|
def file_print(*args):
|
||||||
result = timeNowUTC(as_string=False).strftime("%H:%M:%S") + " "
|
result = timeNowTZ(as_string=False).strftime("%H:%M:%S") + " "
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if isinstance(arg, list):
|
if isinstance(arg, list):
|
||||||
arg = " ".join(
|
arg = " ".join(
|
||||||
str(a) for a in arg
|
str(a) for a in arg
|
||||||
) # so taht new lines are handled correctly also when passing a list
|
) # so that new lines are handled correctly also when passing a list
|
||||||
result += str(arg)
|
result += str(arg)
|
||||||
|
|
||||||
logging.log(custom_to_logging_levels.get(currentLevel, logging.NOTSET), result)
|
logging.log(custom_to_logging_levels.get(currentLevel, logging.NOTSET), result)
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
# cvc90 2023 https://github.com/cvc90 GNU GPLv3 #
|
# cvc90 2023 https://github.com/cvc90 GNU GPLv3 #
|
||||||
# ---------------------------------------------------------------------------------#
|
# ---------------------------------------------------------------------------------#
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
# Register NetAlertX directories
|
# Register NetAlertX directories
|
||||||
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
||||||
@@ -23,231 +24,237 @@ from helper import ( # noqa: E402 [flake8 lint suppression]
|
|||||||
)
|
)
|
||||||
from logger import mylog # noqa: E402 [flake8 lint suppression]
|
from logger import mylog # noqa: E402 [flake8 lint suppression]
|
||||||
from db.sql_safe_builder import create_safe_condition_builder # noqa: E402 [flake8 lint suppression]
|
from db.sql_safe_builder import create_safe_condition_builder # noqa: E402 [flake8 lint suppression]
|
||||||
from utils.datetime_utils import get_timezone_offset # noqa: E402 [flake8 lint suppression]
|
from utils.datetime_utils import format_date_iso # noqa: E402 [flake8 lint suppression]
|
||||||
|
import conf # noqa: E402 [flake8 lint suppression]
|
||||||
|
|
||||||
|
# ===============================================================================
|
||||||
|
# Timezone conversion
|
||||||
|
# ===============================================================================
|
||||||
|
|
||||||
|
DATETIME_FIELDS = {
|
||||||
|
"new_devices": ["Datetime"],
|
||||||
|
"down_devices": ["eve_DateTime"],
|
||||||
|
"down_reconnected": ["eve_DateTime"],
|
||||||
|
"events": ["Datetime"],
|
||||||
|
"plugins": ["DateTimeChanged"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_datetime_fields_from_columns(column_names):
|
||||||
|
return [
|
||||||
|
col for col in column_names
|
||||||
|
if "date" in col.lower() or "time" in col.lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def apply_timezone_to_json(json_obj, section=None):
|
||||||
|
data = json_obj.json["data"]
|
||||||
|
columns = json_obj.columnNames
|
||||||
|
|
||||||
|
fields = DATETIME_FIELDS.get(section) or get_datetime_fields_from_columns(columns)
|
||||||
|
|
||||||
|
return apply_timezone(data, fields)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_timezone(data, fields):
|
||||||
|
"""
|
||||||
|
Convert UTC datetime fields in a list of dicts to the configured timezone.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (list[dict]): Rows returned from DB
|
||||||
|
fields (list[str]): Field names to convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: Modified data with timezone-aware ISO strings
|
||||||
|
"""
|
||||||
|
if not data or not fields:
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Determine local timezone
|
||||||
|
tz = conf.tz
|
||||||
|
if isinstance(tz, str):
|
||||||
|
tz = ZoneInfo(tz)
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
value = row.get(field)
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Convert DB UTC string → local timezone ISO
|
||||||
|
# format_date_iso already assumes UTC if naive
|
||||||
|
row[field] = format_date_iso(value)
|
||||||
|
except Exception:
|
||||||
|
# Never crash, leave original value if conversion fails
|
||||||
|
continue
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
# ===============================================================================
|
# ===============================================================================
|
||||||
# REPORTING
|
# REPORTING
|
||||||
# ===============================================================================
|
# ===============================================================================
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
|
||||||
def get_notifications(db):
|
def get_notifications(db):
|
||||||
sql = db.sql # TO-DO
|
"""
|
||||||
|
Fetch notifications for all configured sections, applying timezone conversions.
|
||||||
|
|
||||||
# Reporting section
|
Args:
|
||||||
mylog("verbose", ["[Notification] Check if something to report"])
|
db: Database object with `.sql` for executing queries.
|
||||||
|
|
||||||
# prepare variables for JSON construction
|
Returns:
|
||||||
json_new_devices = []
|
dict: JSON-ready dict with data and metadata for each section.
|
||||||
json_new_devices_meta = {}
|
"""
|
||||||
json_down_devices = []
|
sql = db.sql
|
||||||
json_down_devices_meta = {}
|
|
||||||
json_down_reconnected = []
|
|
||||||
json_down_reconnected_meta = {}
|
|
||||||
json_events = []
|
|
||||||
json_events_meta = {}
|
|
||||||
json_plugins = []
|
|
||||||
json_plugins_meta = {}
|
|
||||||
|
|
||||||
# Disable reporting on events for devices where reporting is disabled based on the MAC address
|
mylog("verbose", "[Notification] Check if something to report")
|
||||||
|
|
||||||
# Disable notifications (except down/down reconnected) on devices where devAlertEvents is disabled
|
# Disable events where reporting is disabled
|
||||||
sql.execute("""UPDATE Events SET eve_PendingAlertEmail = 0
|
sql.execute("""
|
||||||
WHERE eve_PendingAlertEmail = 1 AND eve_EventType not in ('Device Down', 'Down Reconnected', 'New Device' ) AND eve_MAC IN
|
UPDATE Events SET eve_PendingAlertEmail = 0
|
||||||
(
|
WHERE eve_PendingAlertEmail = 1
|
||||||
SELECT devMac FROM Devices WHERE devAlertEvents = 0
|
AND eve_EventType NOT IN ('Device Down', 'Down Reconnected', 'New Device')
|
||||||
)""")
|
AND eve_MAC IN (SELECT devMac FROM Devices WHERE devAlertEvents = 0)
|
||||||
|
""")
|
||||||
# Disable down/down reconnected notifications on devices where devAlertDown is disabled
|
sql.execute("""
|
||||||
sql.execute("""UPDATE Events SET eve_PendingAlertEmail = 0
|
UPDATE Events SET eve_PendingAlertEmail = 0
|
||||||
WHERE eve_PendingAlertEmail = 1 AND eve_EventType in ('Device Down', 'Down Reconnected') AND eve_MAC IN
|
WHERE eve_PendingAlertEmail = 1
|
||||||
(
|
AND eve_EventType IN ('Device Down', 'Down Reconnected')
|
||||||
SELECT devMac FROM Devices WHERE devAlertDown = 0
|
AND eve_MAC IN (SELECT devMac FROM Devices WHERE devAlertDown = 0)
|
||||||
)""")
|
""")
|
||||||
|
|
||||||
sections = get_setting_value("NTFPRCS_INCLUDED_SECTIONS")
|
sections = get_setting_value("NTFPRCS_INCLUDED_SECTIONS")
|
||||||
|
|
||||||
mylog("verbose", ["[Notification] Included sections: ", sections])
|
mylog("verbose", ["[Notification] Included sections: ", sections])
|
||||||
|
|
||||||
if "new_devices" in sections:
|
# Define SQL templates per section
|
||||||
# Compose New Devices Section (no empty lines in SQL queries!)
|
sql_templates = {
|
||||||
# Use SafeConditionBuilder to prevent SQL injection vulnerabilities
|
"new_devices": """
|
||||||
condition_builder = create_safe_condition_builder()
|
SELECT
|
||||||
new_dev_condition_setting = get_setting_value("NTFPRCS_new_dev_condition")
|
eve_MAC as MAC,
|
||||||
|
eve_DateTime as Datetime,
|
||||||
try:
|
devLastIP as IP,
|
||||||
safe_condition, parameters = condition_builder.get_safe_condition_legacy(
|
eve_EventType as "Event Type",
|
||||||
new_dev_condition_setting
|
devName as "Device name",
|
||||||
)
|
devComments as Comments
|
||||||
sqlQuery = """SELECT
|
FROM Events_Devices
|
||||||
eve_MAC as MAC,
|
WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'New Device' {condition}
|
||||||
eve_DateTime as Datetime,
|
ORDER BY eve_DateTime
|
||||||
devLastIP as IP,
|
""",
|
||||||
eve_EventType as "Event Type",
|
"down_devices": f"""
|
||||||
devName as "Device name",
|
SELECT
|
||||||
devComments as Comments FROM Events_Devices
|
devName,
|
||||||
WHERE eve_PendingAlertEmail = 1
|
eve_MAC,
|
||||||
AND eve_EventType = 'New Device' {}
|
devVendor,
|
||||||
ORDER BY eve_DateTime""".format(safe_condition)
|
eve_IP,
|
||||||
except (ValueError, KeyError, TypeError) as e:
|
eve_DateTime,
|
||||||
mylog("verbose", ["[Notification] Error building safe condition for new devices: ", e])
|
eve_EventType
|
||||||
# Fall back to safe default (no additional conditions)
|
FROM Events_Devices AS down_events
|
||||||
sqlQuery = """SELECT
|
WHERE eve_PendingAlertEmail = 1
|
||||||
eve_MAC as MAC,
|
AND down_events.eve_EventType = 'Device Down'
|
||||||
eve_DateTime as Datetime,
|
AND eve_DateTime < datetime('now', '-{int(get_setting_value("NTFPRCS_alert_down_time") or 0)} minutes')
|
||||||
devLastIP as IP,
|
AND NOT EXISTS (
|
||||||
eve_EventType as "Event Type",
|
SELECT 1
|
||||||
devName as "Device name",
|
FROM Events AS connected_events
|
||||||
devComments as Comments FROM Events_Devices
|
WHERE connected_events.eve_MAC = down_events.eve_MAC
|
||||||
WHERE eve_PendingAlertEmail = 1
|
AND connected_events.eve_EventType = 'Connected'
|
||||||
AND eve_EventType = 'New Device'
|
AND connected_events.eve_DateTime > down_events.eve_DateTime
|
||||||
ORDER BY eve_DateTime"""
|
)
|
||||||
parameters = {}
|
ORDER BY down_events.eve_DateTime
|
||||||
|
""",
|
||||||
mylog("debug", ["[Notification] new_devices SQL query: ", sqlQuery])
|
"down_reconnected": """
|
||||||
mylog("debug", ["[Notification] new_devices parameters: ", parameters])
|
SELECT
|
||||||
|
devName,
|
||||||
# Get the events as JSON using parameterized query
|
eve_MAC,
|
||||||
json_obj = db.get_table_as_json(sqlQuery, parameters)
|
devVendor,
|
||||||
|
eve_IP,
|
||||||
json_new_devices_meta = {
|
eve_DateTime,
|
||||||
"title": "🆕 New devices",
|
eve_EventType
|
||||||
"columnNames": json_obj.columnNames,
|
FROM Events_Devices AS reconnected_devices
|
||||||
}
|
WHERE reconnected_devices.eve_EventType = 'Down Reconnected'
|
||||||
|
AND reconnected_devices.eve_PendingAlertEmail = 1
|
||||||
json_new_devices = json_obj.json["data"]
|
ORDER BY reconnected_devices.eve_DateTime
|
||||||
|
""",
|
||||||
if "down_devices" in sections:
|
"events": """
|
||||||
# Compose Devices Down Section
|
SELECT
|
||||||
# - select only Down Alerts with pending email of devices that didn't reconnect within the specified time window
|
eve_MAC as MAC,
|
||||||
minutes = int(get_setting_value("NTFPRCS_alert_down_time") or 0)
|
eve_DateTime as Datetime,
|
||||||
tz_offset = get_timezone_offset()
|
devLastIP as IP,
|
||||||
sqlQuery = f"""
|
eve_EventType as "Event Type",
|
||||||
SELECT devName, eve_MAC, devVendor, eve_IP, eve_DateTime, eve_EventType
|
devName as "Device name",
|
||||||
FROM Events_Devices AS down_events
|
devComments as Comments
|
||||||
WHERE eve_PendingAlertEmail = 1
|
FROM Events_Devices
|
||||||
AND down_events.eve_EventType = 'Device Down'
|
WHERE eve_PendingAlertEmail = 1
|
||||||
AND eve_DateTime < datetime('now', '-{minutes} minutes', '{tz_offset}')
|
AND eve_EventType IN ('Connected', 'Down Reconnected', 'Disconnected','IP Changed') {condition}
|
||||||
AND NOT EXISTS (
|
ORDER BY eve_DateTime
|
||||||
SELECT 1
|
""",
|
||||||
FROM Events AS connected_events
|
"plugins": """
|
||||||
WHERE connected_events.eve_MAC = down_events.eve_MAC
|
SELECT
|
||||||
AND connected_events.eve_EventType = 'Connected'
|
Plugin,
|
||||||
AND connected_events.eve_DateTime > down_events.eve_DateTime
|
Object_PrimaryId,
|
||||||
)
|
Object_SecondaryId,
|
||||||
ORDER BY down_events.eve_DateTime;
|
DateTimeChanged,
|
||||||
"""
|
Watched_Value1,
|
||||||
|
Watched_Value2,
|
||||||
# Get the events as JSON
|
Watched_Value3,
|
||||||
json_obj = db.get_table_as_json(sqlQuery)
|
Watched_Value4,
|
||||||
|
Status
|
||||||
json_down_devices_meta = {
|
FROM Plugins_Events
|
||||||
"title": "🔴 Down devices",
|
"""
|
||||||
"columnNames": json_obj.columnNames,
|
|
||||||
}
|
|
||||||
json_down_devices = json_obj.json["data"]
|
|
||||||
|
|
||||||
mylog("debug", f"[Notification] json_down_devices: {json.dumps(json_down_devices)}")
|
|
||||||
|
|
||||||
if "down_reconnected" in sections:
|
|
||||||
# Compose Reconnected Down Section
|
|
||||||
# - select only Devices, that were previously down and now are Connected
|
|
||||||
sqlQuery = """
|
|
||||||
SELECT devName, eve_MAC, devVendor, eve_IP, eve_DateTime, eve_EventType
|
|
||||||
FROM Events_Devices AS reconnected_devices
|
|
||||||
WHERE reconnected_devices.eve_EventType = 'Down Reconnected'
|
|
||||||
AND reconnected_devices.eve_PendingAlertEmail = 1
|
|
||||||
ORDER BY reconnected_devices.eve_DateTime;
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Get the events as JSON
|
|
||||||
json_obj = db.get_table_as_json(sqlQuery)
|
|
||||||
|
|
||||||
json_down_reconnected_meta = {
|
|
||||||
"title": "🔁 Reconnected down devices",
|
|
||||||
"columnNames": json_obj.columnNames,
|
|
||||||
}
|
|
||||||
json_down_reconnected = json_obj.json["data"]
|
|
||||||
|
|
||||||
mylog("debug", f"[Notification] json_down_reconnected: {json.dumps(json_down_reconnected)}")
|
|
||||||
|
|
||||||
if "events" in sections:
|
|
||||||
# Compose Events Section (no empty lines in SQL queries!)
|
|
||||||
# Use SafeConditionBuilder to prevent SQL injection vulnerabilities
|
|
||||||
condition_builder = create_safe_condition_builder()
|
|
||||||
event_condition_setting = get_setting_value("NTFPRCS_event_condition")
|
|
||||||
|
|
||||||
try:
|
|
||||||
safe_condition, parameters = condition_builder.get_safe_condition_legacy(
|
|
||||||
event_condition_setting
|
|
||||||
)
|
|
||||||
sqlQuery = """SELECT
|
|
||||||
eve_MAC as MAC,
|
|
||||||
eve_DateTime as Datetime,
|
|
||||||
devLastIP as IP,
|
|
||||||
eve_EventType as "Event Type",
|
|
||||||
devName as "Device name",
|
|
||||||
devComments as Comments FROM Events_Devices
|
|
||||||
WHERE eve_PendingAlertEmail = 1
|
|
||||||
AND eve_EventType IN ('Connected', 'Down Reconnected', 'Disconnected','IP Changed') {}
|
|
||||||
ORDER BY eve_DateTime""".format(safe_condition)
|
|
||||||
except Exception as e:
|
|
||||||
mylog("verbose", f"[Notification] Error building safe condition for events: {e}")
|
|
||||||
# Fall back to safe default (no additional conditions)
|
|
||||||
sqlQuery = """SELECT
|
|
||||||
eve_MAC as MAC,
|
|
||||||
eve_DateTime as Datetime,
|
|
||||||
devLastIP as IP,
|
|
||||||
eve_EventType as "Event Type",
|
|
||||||
devName as "Device name",
|
|
||||||
devComments as Comments FROM Events_Devices
|
|
||||||
WHERE eve_PendingAlertEmail = 1
|
|
||||||
AND eve_EventType IN ('Connected', 'Down Reconnected', 'Disconnected','IP Changed')
|
|
||||||
ORDER BY eve_DateTime"""
|
|
||||||
parameters = {}
|
|
||||||
|
|
||||||
mylog("debug", ["[Notification] events SQL query: ", sqlQuery])
|
|
||||||
mylog("debug", ["[Notification] events parameters: ", parameters])
|
|
||||||
|
|
||||||
# Get the events as JSON using parameterized query
|
|
||||||
json_obj = db.get_table_as_json(sqlQuery, parameters)
|
|
||||||
|
|
||||||
json_events_meta = {"title": "⚡ Events", "columnNames": json_obj.columnNames}
|
|
||||||
json_events = json_obj.json["data"]
|
|
||||||
|
|
||||||
if "plugins" in sections:
|
|
||||||
# Compose Plugins Section
|
|
||||||
sqlQuery = """SELECT
|
|
||||||
Plugin,
|
|
||||||
Object_PrimaryId,
|
|
||||||
Object_SecondaryId,
|
|
||||||
DateTimeChanged,
|
|
||||||
Watched_Value1,
|
|
||||||
Watched_Value2,
|
|
||||||
Watched_Value3,
|
|
||||||
Watched_Value4,
|
|
||||||
Status
|
|
||||||
from Plugins_Events"""
|
|
||||||
|
|
||||||
# Get the events as JSON
|
|
||||||
json_obj = db.get_table_as_json(sqlQuery)
|
|
||||||
|
|
||||||
json_plugins_meta = {"title": "🔌 Plugins", "columnNames": json_obj.columnNames}
|
|
||||||
json_plugins = json_obj.json["data"]
|
|
||||||
|
|
||||||
final_json = {
|
|
||||||
"new_devices": json_new_devices,
|
|
||||||
"new_devices_meta": json_new_devices_meta,
|
|
||||||
"down_devices": json_down_devices,
|
|
||||||
"down_devices_meta": json_down_devices_meta,
|
|
||||||
"down_reconnected": json_down_reconnected,
|
|
||||||
"down_reconnected_meta": json_down_reconnected_meta,
|
|
||||||
"events": json_events,
|
|
||||||
"events_meta": json_events_meta,
|
|
||||||
"plugins": json_plugins,
|
|
||||||
"plugins_meta": json_plugins_meta,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Titles for metadata
|
||||||
|
section_titles = {
|
||||||
|
"new_devices": "🆕 New devices",
|
||||||
|
"down_devices": "🔴 Down devices",
|
||||||
|
"down_reconnected": "🔁 Reconnected down devices",
|
||||||
|
"events": "⚡ Events",
|
||||||
|
"plugins": "🔌 Plugins"
|
||||||
|
}
|
||||||
|
|
||||||
|
final_json = {}
|
||||||
|
|
||||||
|
# Pre-initialize final_json with all expected keys
|
||||||
|
final_json = {}
|
||||||
|
for section in ["new_devices", "down_devices", "down_reconnected", "events", "plugins"]:
|
||||||
|
final_json[section] = []
|
||||||
|
final_json[f"{section}_meta"] = {"title": section_titles.get(section, section), "columnNames": []}
|
||||||
|
|
||||||
|
# Loop through each included section
|
||||||
|
for section in sections:
|
||||||
|
try:
|
||||||
|
# Build safe condition for sections that support it
|
||||||
|
condition_builder = create_safe_condition_builder()
|
||||||
|
condition_setting = get_setting_value(f"NTFPRCS_{section}_condition")
|
||||||
|
safe_condition, parameters = condition_builder.get_safe_condition_legacy(condition_setting)
|
||||||
|
sqlQuery = sql_templates.get(section, "").format(condition=safe_condition)
|
||||||
|
except Exception:
|
||||||
|
# Fallback if safe condition fails
|
||||||
|
sqlQuery = sql_templates.get(section, "").format(condition="")
|
||||||
|
parameters = {}
|
||||||
|
|
||||||
|
mylog("debug", [f"[Notification] {section} SQL query: ", sqlQuery])
|
||||||
|
mylog("debug", [f"[Notification] {section} parameters: ", parameters])
|
||||||
|
|
||||||
|
# Fetch data as JSON
|
||||||
|
json_obj = db.get_table_as_json(sqlQuery, parameters)
|
||||||
|
|
||||||
|
mylog("debug", [f"[Notification] json_obj.json: {json.dumps(json_obj.json)}"])
|
||||||
|
|
||||||
|
# Apply timezone conversion
|
||||||
|
json_obj.json["data"] = apply_timezone_to_json(json_obj, section=section)
|
||||||
|
|
||||||
|
# Save data and metadata
|
||||||
|
final_json[section] = json_obj.json["data"]
|
||||||
|
final_json[f"{section}_meta"] = {
|
||||||
|
"title": section_titles.get(section, section),
|
||||||
|
"columnNames": json_obj.columnNames
|
||||||
|
}
|
||||||
|
|
||||||
|
mylog("debug", [f"[Notification] final_json: {json.dumps(final_json)}"])
|
||||||
|
|
||||||
return final_json
|
return final_json
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from helper import (
|
|||||||
getBuildTimeStampAndVersion,
|
getBuildTimeStampAndVersion,
|
||||||
)
|
)
|
||||||
from messaging.in_app import write_notification
|
from messaging.in_app import write_notification
|
||||||
from utils.datetime_utils import timeNowUTC, get_timezone_offset
|
from utils.datetime_utils import timeNowUTC, timeNowTZ, get_timezone_offset
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -107,7 +107,7 @@ class NotificationInstance:
|
|||||||
mail_html = mail_html.replace("NEW_VERSION", newVersionText)
|
mail_html = mail_html.replace("NEW_VERSION", newVersionText)
|
||||||
|
|
||||||
# Report "REPORT_DATE" in Header & footer
|
# Report "REPORT_DATE" in Header & footer
|
||||||
timeFormated = timeNowUTC()
|
timeFormated = timeNowTZ()
|
||||||
mail_text = mail_text.replace("REPORT_DATE", timeFormated)
|
mail_text = mail_text.replace("REPORT_DATE", timeFormated)
|
||||||
mail_html = mail_html.replace("REPORT_DATE", timeFormated)
|
mail_html = mail_html.replace("REPORT_DATE", timeFormated)
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,33 @@ def timeNowUTC(as_string=True):
|
|||||||
return utc_now.strftime(DATETIME_PATTERN) if as_string else utc_now
|
return utc_now.strftime(DATETIME_PATTERN) if as_string else utc_now
|
||||||
|
|
||||||
|
|
||||||
|
def timeNowTZ(as_string=True):
|
||||||
|
"""
|
||||||
|
Return the current time in the configured local timezone.
|
||||||
|
Falls back to UTC if conf.tz is invalid or missing.
|
||||||
|
"""
|
||||||
|
# Get canonical UTC time
|
||||||
|
utc_now = timeNowUTC(as_string=False)
|
||||||
|
|
||||||
|
# Resolve timezone safely
|
||||||
|
tz = None
|
||||||
|
try:
|
||||||
|
if isinstance(conf.tz, datetime.tzinfo):
|
||||||
|
tz = conf.tz
|
||||||
|
elif isinstance(conf.tz, str) and conf.tz:
|
||||||
|
tz = ZoneInfo(conf.tz)
|
||||||
|
except Exception:
|
||||||
|
tz = None
|
||||||
|
|
||||||
|
if tz is None:
|
||||||
|
tz = datetime.UTC # fallback to UTC
|
||||||
|
|
||||||
|
# Convert to local timezone (or UTC fallback)
|
||||||
|
local_now = utc_now.astimezone(tz)
|
||||||
|
|
||||||
|
return local_now.strftime(DATETIME_PATTERN) if as_string else local_now
|
||||||
|
|
||||||
|
|
||||||
def get_timezone_offset():
|
def get_timezone_offset():
|
||||||
if conf.tz:
|
if conf.tz:
|
||||||
now = timeNowUTC(as_string=False).astimezone(conf.tz)
|
now = timeNowUTC(as_string=False).astimezone(conf.tz)
|
||||||
|
|||||||
Reference in New Issue
Block a user