diff --git a/front/css/app.css b/front/css/app.css
index facd9d99..6f3a2d0c 100755
--- a/front/css/app.css
+++ b/front/css/app.css
@@ -1011,7 +1011,7 @@ height: 50px;
display: block;
position: absolute;
font-size: x-small;
- top: -5px;
+ top: 5px;
}
/* --------------------------------------------------------- */
@@ -2221,7 +2221,7 @@ textarea[readonly],
.pluginsCore .helpIcon
{
- margin-left: -20px;
+ margin-left: -15px;
font-size: x-small;
}
@@ -2607,21 +2607,47 @@ table.dataTable tbody > tr.selected
}
/* -----------------------------------------------------------------------------
- Donations
+ Device -> Details
----------------------------------------------------------------------------- */
-.donations .box
+#panDetails
{
- padding:15px;
- margin-bottom: 0px;
+ padding:10px;
}
-.donations .box-header
+
+/* -----------------------------------------------------------------------------
+ Device -> Tools
+----------------------------------------------------------------------------- */
+#panTools
{
- color:15px;
+ padding:10px;
}
-.donations h3
+
+/* -----------------------------------------------------------------------------
+ Device -> Sessions
+----------------------------------------------------------------------------- */
+#panSessions
{
- margin-top: 10px;
+ padding:10px;
}
+
+
+/* -----------------------------------------------------------------------------
+ Device -> Presence
+----------------------------------------------------------------------------- */
+#panPresence
+{
+ padding:10px;
+}
+
+/* -----------------------------------------------------------------------------
+ Device -> Events
+----------------------------------------------------------------------------- */
+#panEvents
+{
+ padding:10px;
+}
+
+
/* -----------------------------------------------------------------------------
Field Lock/Unlock Buttons & Source Indicators
----------------------------------------------------------------------------- */
@@ -2660,8 +2686,14 @@ table.dataTable tbody > tr.selected
----------------------------------------------------------------------------- */
.change-log .helpIcon{
+ font-size: x-small;
+ top: 65px;
+}
+
+#panHistory .change-log .helpIcon{
left: 15px;
font-size: x-small;
+ top: 45px;
}
#panHistory
@@ -2669,6 +2701,11 @@ table.dataTable tbody > tr.selected
min-height:850px
}
+#navDevice .tab-content
+{
+ padding:0px;
+}
+
#panHistory .change-log-skeleton-class
{
margin-left: 0px;
@@ -2729,7 +2766,7 @@ table.dataTable tbody > tr.selected
#settings-skeleton, #devices-skeleton, #device-details-skeleton,
#events-skeleton, #presence-skeleton, #report-skeleton, #skel-app-events,
#notifications-skeleton, #workflows-skeleton, #plugins-skeleton { z-index: 50; }
-.skel-tab-pane { margin: -10px; z-index: 10; min-height: 400px; background-color: var(--skel-panel-bg);}
+.skel-tab-pane { z-index: 10; min-height: 400px; background-color: var(--skel-panel-bg);}
#settings-skeleton { margin: 15px; min-height: 500px; }
#devices-skeleton { margin: 15px; top: 50px; min-height: 500px; }
#skel-app-events { margin: 15px; top: 50px; min-height: 500px; z-index: inherit;}
@@ -2741,7 +2778,7 @@ table.dataTable tbody > tr.selected
#workflows-skeleton { top: 50px; width: 770px; margin: 0 auto; min-height: 300px; }
#plugins-skeleton { top: 50px; padding: 0; min-height: 400px; }
#change-log-skeleton { display: block; min-height: 400px; z-index: 1; position: absolute; margin-left: 15px; padding-top: 15px; width:100%;}
-#skel-tab-sessions, #skel-tab-events, #skel-tab-presence {margin: -10px; padding: 20px;}
+#skel-tab-sessions, #skel-tab-events, #skel-tab-presence { padding: 20px;}
/* Hide scrollbars */
#skel-tab-sessions, #skel-tab-events, #skel-tab-presence {
diff --git a/front/plugins/unifi_api_import/unifi_api_import.py b/front/plugins/unifi_api_import/unifi_api_import.py
index 678e8a46..5e365358 100755
--- a/front/plugins/unifi_api_import/unifi_api_import.py
+++ b/front/plugins/unifi_api_import/unifi_api_import.py
@@ -100,12 +100,12 @@ def get_device_data(site, api):
# --- Devices ---
unifi_devices_resp = api.get_unifi_devices(site_id)
unifi_devices = unifi_devices_resp.get("data", [])
- mylog('verbose', [f'[{pluginName}] Site: {site_name} unifi devices: {json.dumps(unifi_devices_resp, indent=2)}'])
+ mylog('trace', [f'[{pluginName}] Site: {site_name} unifi devices: {json.dumps(unifi_devices_resp, indent=2)}'])
# --- Clients ---
clients_resp = api.get_clients(site_id)
clients = clients_resp.get("data", [])
- mylog('verbose', [f'[{pluginName}] Site: {site_name} clients: {json.dumps(clients_resp, indent=2)}'])
+ mylog('trace', [f'[{pluginName}] Site: {site_name} clients: {json.dumps(clients_resp, indent=2)}'])
# Build a lookup for devices by their 'id' to find parent MAC easily
device_id_to_mac = {}
diff --git a/front/report_templates/report_template.html b/front/report_templates/report_template.html
index 58c600bd..cbd30152 100755
--- a/front/report_templates/report_template.html
+++ b/front/report_templates/report_template.html
@@ -11,6 +11,7 @@
|
+ PREHEADER
NEW_DEVICES_TABLE
DOWN_DEVICES_TABLE
DOWN_RECONNECTED_TABLE
diff --git a/server/messaging/reporting.py b/server/messaging/reporting.py
index 52cbf475..ea9f9a29 100755
--- a/server/messaging/reporting.py
+++ b/server/messaging/reporting.py
@@ -221,8 +221,8 @@ def get_notifications(db):
sqlQuery = template.format(**fallback_vars)
parameters = {}
- mylog("debug", [f"[Notification] {section} SQL query: ", sqlQuery])
- mylog("debug", [f"[Notification] {section} parameters: ", parameters])
+ mylog("trace", [f"[Notification] {section} SQL query: ", sqlQuery])
+ mylog("trace", [f"[Notification] {section} parameters: ", parameters])
try:
json_obj = db.get_table_as_json(sqlQuery, parameters)
diff --git a/server/models/device_history_instance.py b/server/models/device_history_instance.py
index 9547ffff..01f84bfa 100644
--- a/server/models/device_history_instance.py
+++ b/server/models/device_history_instance.py
@@ -206,12 +206,6 @@ class DevicesHistoryInstance:
clauses, params = self._build_clauses(devGUID, changedColumn, changedBy, search)
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
- mylog("none", f"[HISTORY] SQL {where} params={params}")
- mylog("none", f"[HISTORY] sort={sort}")
- mylog("none", f"[HISTORY] sort type={type(sort)}")
- mylog("none", f"[HISTORY] search {search} ")
- mylog("none", f"[HISTORY] FINAL CALL limit={limit} offset={offset} sort={sort}")
-
rows = self._fetchall(
f"""
SELECT devGUID, timestamp, changedBy, changedColumn, oldValue, newValue
diff --git a/server/models/notification_instance.py b/server/models/notification_instance.py
index 38095093..007e0656 100755
--- a/server/models/notification_instance.py
+++ b/server/models/notification_instance.py
@@ -126,9 +126,14 @@ class NotificationInstance:
mail_text = mail_text.replace("REPORT_DASHBOARD_URL", self.serverUrl)
mail_html = mail_html.replace("REPORT_DASHBOARD_URL", self.serverUrl)
+ # Preheaders for preview
+ preheaders = []
+
# Generate TEXT & HTML for each notification section
for section in SECTION_ORDER:
- html, text = construct_notifications(self.JSON, section)
+ html, text, preheader = construct_notifications(self.JSON, section)
+ if preheader:
+ preheaders.append(preheader)
placeholder = f"{section.upper()}_TABLE"
mail_text = mail_text.replace(placeholder, text + "\n")
mail_html = mail_html.replace(placeholder, html)
@@ -141,6 +146,11 @@ class NotificationInstance:
mail_html, conf.REPORT_DASHBOARD_URL + "/deviceDetails.php?mac="
)
+ # Add Preheaders for preview after mac links created
+ mail_html = mail_html.replace(
+ "PREHEADER", " • ".join(preheaders)
+ )
+
final_html = indent(
mail_html, indentation=" ", newline="\r\n", indent_text=True
)
@@ -276,24 +286,23 @@ class NotificationInstance:
# -----------------------------------------------------------------------------
# Reporting
# -----------------------------------------------------------------------------
-
-
# ------------------------------------------------------------------------------
def construct_notifications(JSON, section):
jsn = JSON[section]
# Return if empty
if jsn == []:
- return "", ""
+ return "", "", ""
tableTitle = JSON[section + "_meta"]["title"]
headers = JSON[section + "_meta"]["columnNames"]
html = ""
text = ""
+ preheader_preview = ""
table_attributes = {
- "style": "border-collapse: collapse; font-size: 12px; color:#70707",
+ "style": "border-collapse: collapse; font-size: 12px; color: #70707;",
"width": "100%",
"cellspacing": 0,
"cellpadding": "3px",
@@ -326,6 +335,9 @@ def construct_notifications(JSON, section):
table_attributes=table_attributes,
)
+ # pre-header in emails (html) to show a more valuable preview
+ preheader_preview = build_preheader(tableTitle, jsn, headers)
+
# Cleanup the generated HTML table notification
html = (
format_table(html, "data", headerProps, tableTitle)
@@ -357,7 +369,7 @@ def construct_notifications(JSON, section):
for header in headers:
html = format_table(html, header, thProps)
- return html, text
+ return html, text, preheader_preview
# -----------------------------------------------------------------------------
@@ -378,3 +390,26 @@ def format_table(html, thValue, props, newThValue=""):
return html.replace(
" | " + thValue + " | ", "" + newThValue + " | "
)
+
+
+# -----------------------------------------------------------------------------
+# Pre-header Preview
+def build_preheader(tableTitle, jsn, headers):
+ previews = []
+
+ # pre-headers for email previews
+ preview_fields = headers[:3]
+
+ for entry in jsn[:3]:
+ vals = [
+ str(entry[h])
+ for h in preview_fields
+ if entry.get(h)
+ ]
+ previews.append(" • ".join(vals))
+
+ suffix = ""
+ if len(jsn) > 3:
+ suffix = f" (+{len(jsn) - 3} more)"
+
+ return f"{tableTitle}: " + "; ".join(previews) + suffix
diff --git a/server/workflows/conditions.py b/server/workflows/conditions.py
index d68b8c48..db6d1299 100755
--- a/server/workflows/conditions.py
+++ b/server/workflows/conditions.py
@@ -56,7 +56,7 @@ class ConditionGroup:
"""Handles condition groups with AND, OR logic, supporting nested groups."""
def __init__(self, group_json):
- mylog("verbose", f"[WF] ConditionGroup json.dumps(group_json): {json.dumps(group_json)}")
+ mylog("trace", f"[WF] ConditionGroup json.dumps(group_json): {json.dumps(group_json)}")
self.logic = group_json.get("logic", "AND").upper()
self.conditions = []
@@ -76,5 +76,5 @@ class ConditionGroup:
return any(results)
else:
m = f"[WF] ConditionGroup unsupported logic: {self.logic}"
- mylog("verbose", [m])
+ mylog("none", [m])
raise ValueError(m)
diff --git a/server/workflows/manager.py b/server/workflows/manager.py
index e171866f..df094c1a 100755
--- a/server/workflows/manager.py
+++ b/server/workflows/manager.py
@@ -28,7 +28,6 @@ class WorkflowManager:
# -------------------------------------------------------------------------
# Token validation
-
def _validate_workflow_tokens(self, workflow):
"""Recursively scan a workflow dict for {{trigger.X}} tokens.
Returns True if every token maps to a valid Devices column."""
@@ -52,7 +51,6 @@ class WorkflowManager:
# -------------------------------------------------------------------------
# Loading
-
def load_workflows(self):
"""Load workflows from workflows.json, rejecting any with invalid tokens."""
try:
@@ -87,7 +85,6 @@ class WorkflowManager:
# -------------------------------------------------------------------------
# Event processing
-
def process_event(self, event):
"""Process one AppEvent against all enabled workflows."""
evGuid = event["guid"]
@@ -116,7 +113,6 @@ class WorkflowManager:
# -------------------------------------------------------------------------
# Workflow execution
-
def execute_workflow(self, workflow, trigger):
"""Execute workflow actions if any condition group evaluates to True."""
wfName = workflow["name"]
@@ -130,11 +126,11 @@ class WorkflowManager:
evaluator = ConditionGroup(condition_group)
if evaluator.evaluate(trigger):
mylog("none", f"[WF] Workflow {wfName} will be executed - conditions were evaluated as TRUE")
- mylog("debug", [f"[WF] Workflow condition_group: {condition_group}"])
+ mylog("trace", [f"[WF] Workflow condition_group: {condition_group}"])
self.execute_actions(workflow["actions"], trigger)
return
- mylog("none", ["[WF] No condition group matched. Actions not executed."])
+ mylog("trace", ["[WF] No condition group matched. Actions not executed."])
def _resolve_target_devices(self, action, trigger_device):
"""Return the list of device dicts that the action should be applied to.
@@ -180,7 +176,7 @@ class WorkflowManager:
target_devices = self._resolve_target_devices(action, trigger_obj)
if not target_devices:
- mylog("debug", [f"[WF] No target devices matched for action '{action_type}'"])
+ mylog("trace", [f"[WF] No target devices matched for action '{action_type}'"])
continue
for target_device in target_devices:
diff --git a/server/workflows/triggers.py b/server/workflows/triggers.py
index 86e305ec..fb365ad4 100755
--- a/server/workflows/triggers.py
+++ b/server/workflows/triggers.py
@@ -24,7 +24,7 @@ class Trigger:
self.object_type == event["objectType"] and self.event_type == event["appEventType"]
)
- mylog("debug", f"""[WF] self.triggered '{self.triggered}' for event '{get_array_from_sql_rows(event)} and trigger {json.dumps(triggerJson)}' """)
+ mylog("trace", f"""[WF] self.triggered '{self.triggered}' for event '{get_array_from_sql_rows(event)} and trigger {json.dumps(triggerJson)}' """)
if self.triggered:
# object type corresponds with the DB table name
@@ -45,10 +45,10 @@ class Trigger:
WHERE {refField} = '{event["objectGuid"]}'
"""
- mylog("debug", [query])
+ mylog("trace", [query])
result = db.sql.execute(query).fetchall()
-
+
if len(result) > 0:
self.object = result[0]
else:
diff --git a/test/backend/test_notification_templates.py b/test/backend/test_notification_templates.py
index 3a17237f..1e8b8d9a 100644
--- a/test/backend/test_notification_templates.py
+++ b/test/backend/test_notification_templates.py
@@ -74,9 +74,10 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
mock_setting.return_value = ""
json_data = _make_json("new_devices", [], [])
- html, text = construct_notifications(json_data, "new_devices")
+ html, text, preheader_preview = construct_notifications(json_data, "new_devices")
self.assertEqual(html, "")
self.assertEqual(text, "")
+ self.assertEqual(preheader_preview, "")
# -----------------------------------------------------------------
# Legacy fallback: no template → vertical Header: Value per device
@@ -93,7 +94,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
json_data = _make_json(
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
- html, text = construct_notifications(json_data, "new_devices")
+ html, text, preheader_preview = construct_notifications(json_data, "new_devices")
# Section header must be present
self.assertIn("🆕 New devices", text)
@@ -123,7 +124,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
json_data = _make_json(
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
- _, text = construct_notifications(json_data, "new_devices")
+ _, text, _ = construct_notifications(json_data, "new_devices")
self.assertIn("MyPhone (aa:bb:cc:dd:ee:ff) - 192.168.1.42", text)
self.assertIn("Laptop (11:22:33:44:55:66) - 192.168.1.99", text)
@@ -143,7 +144,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
json_data = _make_json(
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
- _, text = construct_notifications(json_data, "new_devices")
+ _, text, _ = construct_notifications(json_data, "new_devices")
self.assertIn("MyPhone - {NonExistent}", text)
self.assertIn("Laptop - {NonExistent}", text)
@@ -163,7 +164,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
json_data = _make_json(
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
- _, text = construct_notifications(json_data, "new_devices")
+ _, text, _ = construct_notifications(json_data, "new_devices")
self.assertNotIn("🆕 New devices", text)
self.assertNotIn("---------", text)
@@ -183,7 +184,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
json_data = _make_json(
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
- _, text = construct_notifications(json_data, "new_devices")
+ _, text, _ = construct_notifications(json_data, "new_devices")
# Headers should be shown by default
self.assertIn("🆕 New devices", text)
@@ -204,7 +205,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
json_data = _make_json(
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
- _, text = construct_notifications(json_data, "new_devices")
+ _, text, _ = construct_notifications(json_data, "new_devices")
self.assertIn("MyPhone ({BadField}) - 192.168.1.42", text)
@@ -234,7 +235,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
columns = ["devName", "eveMac", "devVendor", "eveIp", "eveDateTime", "eveEventType", "devComments"]
json_data = _make_json("down_devices", down_devices, columns, "🔴 Down devices")
- _, text = construct_notifications(json_data, "down_devices")
+ _, text, _ = construct_notifications(json_data, "down_devices")
self.assertIn("Router (ff:ee:dd:cc:bb:aa) down since 2025-01-15 08:00:00", text)
@@ -264,7 +265,7 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
columns = ["devName", "eveMac", "devVendor", "eveIp", "eveDateTime", "eveEventType", "devComments"]
json_data = _make_json("down_reconnected", reconnected, columns, "🔁 Reconnected down devices")
- _, text = construct_notifications(json_data, "down_reconnected")
+ _, text, _ = construct_notifications(json_data, "down_reconnected")
self.assertIn("Switch (aa:11:bb:22:cc:33) reconnected at 2025-01-15 09:30:00", text)
@@ -283,14 +284,14 @@ class TestConstructNotificationsTemplates(unittest.TestCase):
json_data = _make_json(
"new_devices", SAMPLE_NEW_DEVICES, NEW_DEVICE_COLUMNS, "🆕 New devices"
)
- html_without, _ = construct_notifications(json_data, "new_devices")
+ html_without, _, _ = construct_notifications(json_data, "new_devices")
# Get HTML with template
mock_setting.side_effect = self._setting_factory({
"NTFPRCS_TEXT_SECTION_HEADERS": True,
"NTFPRCS_TEXT_TEMPLATE_new_devices": "{devName} ({eveMac})",
})
- html_with, _ = construct_notifications(json_data, "new_devices")
+ html_with, _, _ = construct_notifications(json_data, "new_devices")
self.assertEqual(html_without, html_with)